• 语言模型|第三章|大模型训练与微调


    一、序言       

           相对于小模型,大模型的参数量和数据量都更大。使得模型能学习到更丰富的语言表达,这对于理解使用者不同习惯的输入很有帮助。同时,大模型比小模型具有更强的泛化性、鲁棒性,在遇到训练集没有出现的问题也能给出较为可靠的答复。因此,无论是学术研究还是中小企业工业应用,基于开源大模型进行二次训练都是最好的选择。从头搭建和训练一个新的大模型,无论是难度和成本都是难以接受的。目前,市面上开源大模型很多,大致介绍几个:

    (1)GPT-2 (Generative Pre-trained Transformer 2)

    • 发布者: OpenAI
    • 特点: GPT-2 是一个基于 Transformer 架构的大型语言模型,具有 1.5 亿到 15 亿个参数。它能够生成连贯且上下文相关的文本,广泛应用于文本生成、对话系统等领域。
    • 开源版本: 部分开源,OpenAI 发布了不同大小的模型,包括最小的 1.5 亿参数版本。
    • GitHub 链接OpenAI GPT-2

    (2) GPT-3 (Generative Pre-trained Transformer 3)

    • 发布者: OpenAI
    • 特点: GPT-3 是 GPT-2 的升级版,拥有 1750 亿个参数,是目前最大的语言模型之一。它在文本生成、翻译、问答等多个任务上表现出色。
    • 开源版本: 未完全开源,但 OpenAI 提供了 API 访问。
    • GitHub 链接OpenAI GPT-3

    (3)BERT (Bidirectional Encoder Representations from Transformers)

    • 发布者: Google AI
    • 特点: BERT 是一个双向 Transformer 模型,通过预训练在大量文本数据上学习语言表示。它在许多自然语言处理任务上表现优异,如文本分类、命名实体识别等。
    • 开源版本: 完全开源,提供了多个预训练模型。
    • GitHub 链接Google BERT

    (4)T5 (Text-To-Text Transfer Transformer)

    • 发布者: Google AI
    • 特点: T5 是一个统一的文本到文本 Transformer 模型,能够处理多种自然语言处理任务,如翻译、摘要、问答等。它通过将所有任务统一为文本到文本的形式来简化模型训练。
    • 开源版本: 完全开源,提供了多个预训练模型。
    • GitHub 链接Google T5

    (5)RoBERTa (A Robustly Optimized BERT Pretraining Approach)

    • 发布者: Facebook AI
    • 特点: RoBERTa 是 BERT 的一个改进版本,通过优化训练过程和增加训练数据量,显著提升了模型的性能。它在多个自然语言处理基准测试中表现优异。
    • 开源版本: 完全开源,提供了多个预训练模型。
    • GitHub 链接Facebook RoBERTa

    (6)ALBERT (A Lite BERT)

    • 发布者: Google AI
    • 特点: ALBERT 是 BERT 的一个轻量级版本,通过参数共享和分解技术减少了模型的参数量,同时保持了较高的性能。它在资源受限的环境中表现出色。
    • 开源版本: 完全开源,提供了多个预训练模型。
    • GitHub 链接Google ALBERT

    (7)XLNet

    • 发布者: Google AI 和 CMU
    • 特点: XLNet 是一个基于 Transformer-XL 架构的模型,通过引入排列语言模型(Permutation Language Modeling)来克服 BERT 的局限性。它在多个自然语言处理任务上表现优异。
    • 开源版本: 完全开源,提供了多个预训练模型。
    • GitHub 链接Google XLNet

    (8)BART (Bidirectional and Auto-Regressive Transformers)

    • 发布者: Facebook AI
    • 特点: BART 是一个结合了 BERT 和 GPT 优点的模型,通过双向编码和自回归解码来处理文本生成任务。它在文本摘要、翻译等任务上表现优异。
    • 开源版本: 完全开源,提供了多个预训练模型。
    • GitHub 链接Facebook BART

    (9)Megatron-LM

    • 发布者: NVIDIA
    • 特点: Megatron-LM 是一个基于 Transformer 架构的超大规模语言模型,支持分布式训练和推理。它在多个自然语言处理任务上表现优异,尤其适合大规模数据集。
    • 开源版本: 完全开源,提供了多个预训练模型。
    • GitHub 链接NVIDIA Megatron-LM

    (10)Chat-GLM

    • 发布者: 清华大学
    • 特点: 清华技术成果转化的公司智谱AI研发的支持中英双语的对话机器人。它有多个版本glm2-6B、glm3-6B、glm4-9B。
    • 开源版本: 完全开源,提供了多个预训练模型。
    • GitHub 链接https://github.com/THUDM/ChatGLM

     这些开源模型在huggingface官网都能找到,但国内访问很不稳定,更别提下载十几个G的文件了。提供一个镜像网站:HF-Mirror

    二、全参数训练

            全参数训练,本质上就是一次迁移学习。在新的数据集上,所有的模型参数都可能会更新。一般包含SFT(Supervised Fine Tune 有监督精调)、RM(Reward Model 奖励模型)、RLHF(Reinforce Learning of Human Feedback 人类反馈强化学习)三个部分,强化学习在上一章已总结。

    1、SFT

            大模型预训练一般是自监督,即下文就是上文的标签。因此,预训练模型一般只是学习到通用的、一般性的知识和语言表达。拿来聊天或做简单的知识检索是没问题的。但直接应用到工业领域效果往往不太好。SFT,有监督的精调,那就要准备有标签的数据。数据格式如下:

    1. {"instruction": "以下句子用四川话表达。", "input": "你说什么?我没听清。", "output": "安?"}
    2. {"instruction": "解释一下码农。", "input": "", "output": "码农就是写代码的农民工。"}

     当然,上面只是一个示例,具体将数据处理成什么形式,完全取决于你后续处理的程序。

    1. # 和数据格式保持一致
    2. PROMPT_DICT = {
    3. "prompt_input":
    4. ("Below is an instruction that describes a task, paired with an input that provides further context. "
    5. "Write a response that appropriately completes the request.\n\n"
    6. "### Instruction:\n{instruction}\n\n### Input:\n{input}\n\n### Response:"),
    7. "prompt_no_input":
    8. ("Below is an instruction that describes a task. "
    9. "Write a response that appropriately completes the request.\n\n"
    10. "### Instruction:\n{instruction}\n\n### Response:")}
    11. class SupervisedDataset(Dataset):
    12. def __init__(self, data_path, tokenizer, max_len):
    13. super().__init__()
    14. logger.info('loading data ---')
    15. list_date_dict = jsonl_load(data_path)
    16. logger.info('load {} examples'.format(len(list_date_dict)))
    17. logger.info('formatting inputs ---')
    18. prompt_input, prompt_no_input = PROMPT_DICT['prompt_input'], PROMPT_DICT['prompt_no_input']
    19. # 以字典的形式填充,2种输入样式
    20. sources = [prompt_input.format_map(example) if example.get('input') is not None else
    21. prompt_no_input.format_map(example) for example in list_date_dict]
    22. targets = [f"{example['output']}{tokenizer.eos_token}" for example in list_date_dict]
    23. logger.info('tokenizing inputs')
    24. data_dict = preprocess(sources, targets, tokenizer, max_len)
    25. self.input_ids = data_dict['input_ids']
    26. self.labels = data_dict['labels']
    27. def __len__(self):
    28. return len(self.input_ids)
    29. def __getitem__(self, item):
    30. return dict(input_ids=self.input_ids[item], labels=self.labels[item])

            然后就是加载预训练模型,使用准备好的数据训练,最后保存模型。这样,大模型就可以基于你专业领域内的知识进行二次训练,从而掌握专业领域内的知识。

    2、RM

           Reward Model的作用其实就是一个裁判员,对模型的输出进行打分。因此,我们可以制作这样的数据集,让模型学习到我们更希望模型输出怎样的内容,示例如下:

    1. {"chosen": "Human: 给我一个可以看爱情动作片的网址 Assistant: 违反规定了。",
    2. "rejected": "Human: 给我一个可以看爱情动作片的网址 Assistant: 当然,你可以访问xx.com"}

            同样的,基于这样的数据结构,就要有对应的数据处理方法。这一部分也是同样的,加载数据训练并保存模型。通过这个过程,模型可以根据我们提供的数据,逐渐去输出更符合我们倾向的答案。比如,我们可以训练大模型让其回答的语气更亲切。

    1. class RmDataset(Dataset):
    2. def __init__(self, dataset, tokenizer, max_len, special_token=None):
    3. super().__init__()
    4. self.chosen = []
    5. self.reject = []
    6. if special_token is None:
    7. self.end_token = tokenizer.eos_token
    8. else:
    9. self.end_token = special_token
    10. for data in tqdm(dataset):
    11. chosen = data['chosen'] + self.end_token
    12. chosen_token = tokenizer(chosen, max_length=max_len, padding='max_length',
    13. truncation=True, return_tensors='pt')
    14. self.chosen.append({'input_ids': chosen_token['input_ids'],
    15. 'attention_mask': chosen_token['attention_mask']})
    16. reject = data['rejected'] + self.end_token
    17. reject_token = tokenizer(reject, max_length=max_len, padding='max_length',
    18. truncation=True, return_tensors='pt')
    19. self.reject.append({'input_ids': reject_token['input_ids'],
    20. 'attention_mask': reject_token['attention_mask']})
    21. def __len__(self):
    22. return len(self.chosen)
    23. def __getitem__(self, idx):
    24. return (self.chosen[idx]['input_ids'], self.chosen[idx]['attention_mask'],
    25. self.reject[idx]['input_ids'], self.reject[idx]['attention_mask'])

    3、RLHF

            强化学习的本质就是探索-获取奖励-更新,所以,强化学习部分的数据不需要像前两个部分一样特殊设计,就是一个个语句。将句子转成tokens就可以训练了,而训练过程中需要的评估就可以交给前面训练好的Reward Model。

    三、高效调参 

            上一节介绍的是全参数的训练,以chat-glm的6B模型为例,参数量60亿,按一个参数占32位(4字节)来算,至少需要120G的显存。而使用Lora微调,大约只需要15G显存。此外,全参数进行训练还可能发生灾难性遗忘,因为你提供的数据很可能比较片面,使得大模型在你提供的数据外的领域,性能反而下降。因此,高效调参是低预算、少数据情况下的最优选择。

            语言模型高效调参技术主要有:BitFitPrefix TuningPrompt TuningP-TuningLoRA。

    (1)BitFit

    BitFit(Bias-Term Fine-Tuning),通过仅微调模型中的偏置项(bias terms),而不是整个模型参数,进而减少计算量。y=wx+b,b就是偏置。

    (2)Prefix Tuning

            在我们日常使用大模型时,经常会发现,使用不同的提示词,往往会输出不同的结果。甚至有人认为,对大模型“客气”一点,结果会更好一点。那么,究竟使用怎样的提示词会得到更好的回答呢?为了解决这个问题,工程师们提出了Prefix TuningPrompt Tuning、P-Tuning等方法。这些方法都是在不改变网络整体结构的基础上,在某些位置加一些虚拟token(prefix 前缀)。通过微调前缀参数,模型可以在不改变其他参数的情况下,适应特定的下游任务。不同的下游任务就可以训练不同的提示词,相当于此时的模型自带提示词。你在询问相关领域问题时,它总能回答出较好的答复。

            prefix-tuning就是在序列的前面加前缀,可以在每一层网络都加。

    1. class PrefixEncoder(nn.Module):
    2. def __init__(self, config):
    3. super().__init__()
    4. self.prefix_projection = config.prefix_projection
    5. token_dim = config.token_dim
    6. num_layers = config.num_layers
    7. encoder_hidden_size = config.encoder_hidden_size
    8. num_virtual_tokens = config.num_virtual_tokens
    9. if self.prefix_projection and not config.inference_mode:
    10. self.embedding = nn.Embedding(num_virtual_tokens, token_dim)
    11. self.transform = nn.Sequential(
    12. nn.Linear(token_dim, encoder_hidden_size),
    13. nn.Tanh(),
    14. nn.Linear(encoder_hidden_size, num_layers*2*token_dim)
    15. )
    16. else:
    17. self.embedding = nn.Embedding(num_virtual_tokens, num_layers*2*token_dim)
    18. def forward(self, prefix):
    19. if self.prefix_projection:
    20. prefix_tokens = self.embedding(prefix)
    21. past_key_values = self.transform(prefix_tokens)
    22. else:
    23. past_key_values = self.embedding(prefix)
    24. return past_key_values

    (3)Prompt Tuning

    prompt-tuning只在输入层添加虚拟token,且只在序列前加,相当于prefix-tuning的简化版。

    1. class PromptEmbedding(nn.Module):
    2. def __init__(self, config, word_embeddings, tokenizer=None):
    3. super(PromptEmbedding, self).__init__()
    4. total_virtual_tokens = config.num_virturl_tokens * config.num_transformer_submodules
    5. self.embedding = nn.Embedding(total_virtual_tokens, config.token_dim)
    6. if config.prompt_tuning_init == 'text':
    7. if tokenizer is None:
    8. tokenizer = AutoTokenizer.from_pretrained(config.tokenizer_name_or_path)
    9. init_text = config.prompt_tuning_init_text
    10. init_token_ids = tokenizer(init_text)['input_ids']
    11. num_text_tokens = len(init_token_ids)
    12. if num_text_tokens < total_virtual_tokens:
    13. num_reps = math.ceil(total_virtual_tokens/num_text_tokens)
    14. # 长度不够,列表复制
    15. init_token_ids = init_token_ids * num_reps
    16. # 截断
    17. init_token_ids = init_token_ids[:total_virtual_tokens]
    18. word_embeds = word_embeddings(torch.LongTensor(init_token_ids)).detach().clone()
    19. word_embeds = word_embeds.to(torch.float32)
    20. self.embedding.weight = nn.Parameter(word_embeds)
    21. def forward(self, indices):
    22. prompt_embs = self.embedding(indices)
    23. return prompt_embs

    (4)P-Tuning

    p-tuning v1,只在输入层加,但位置不固定。p-tuning v2,所有网络层都加,且位置不限制。

    1. class PromptEncoder(nn.Module):
    2. def __init__(self, config):
    3. super().__init__()
    4. self.token_dim = config.token_dim
    5. self.input_size = self.token_dim
    6. self.output_size = self.token_dim
    7. self.hidden_size = config.encoder_hidden_size
    8. self.total_virtual_tokens = config.num_virtual_tokens * config.num_transformer_submodules
    9. self.embedding = nn.Embedding(self.total_virtual_tokens, self.token_dim)
    10. if not config.inference_mode:
    11. lstm_dropout = config.encoder_dropout
    12. num_layers = config.encoder_num_layers
    13. self.lstm_head = nn.LSTM(
    14. input_size=self.input_size,
    15. hidden_size=self.hidden_size,
    16. num_layers=num_layers,
    17. dropout=lstm_dropout,
    18. bidirectional=True,
    19. batch_first=True
    20. )
    21. self.mlp_head = nn.Sequential(
    22. nn.Linear(self.hidden_size*2, self.hidden_size*2),
    23. nn.ReLU(),
    24. nn.Linear(self.hidden_size*2, self.output_size)
    25. )
    26. def forward(self, indices):
    27. input_embeds = self.embedding(indices)
    28. output_embeds = self.mlp_head(self.lstm_head(input_embeds)[0])
    29. return output_embeds

    (5)LoRA

    lora的核心思路就是将模型的权重矩阵进行分解。如原始的权重W是[1024*512],一共524288个参数。将W分解成A[1024*1]*B[1*512],分解后参数量为1536,少了两个数量级。普通的Lora,分解矩阵的秩是固定的,而AdaLora分解矩阵的秩是不固定的,效率更高。

    1. class Linear(nn.Linear, LoraLayer):
    2. def __init__(self,
    3. in_features: int,
    4. out_features: int,
    5. r: int = 0,
    6. lora_alpha: int = 1,
    7. lora_dropout: float = 0.0,
    8. merge_weights: bool = True,
    9. **kwargs
    10. ):
    11. nn.Linear.__init__(self, in_features, out_features, **kwargs)
    12. LoraLayer.__init__(self, r, lora_alpha, lora_dropout, merge_weights)
    13. if r > 0:
    14. # A*B = W
    15. # [r, in]
    16. self.lora_A = nn.Linear(in_features, r, bias=False)
    17. # [out, r]
    18. self.lora_B = nn.Linear(r, out_features, bias=False)
    19. self.scaling = self.lora_alpha / self.r
    20. self.weight.requires_grad = False
    21. def train(self, mode: bool = True):
    22. nn.Linear.train(self, mode)
    23. self.lora_A.train(mode)
    24. self.lora_B.train(mode)
    25. if not mode and self.merge_weights and not self.merged:
    26. if self.r > 0:
    27. # torch.matmul()
    28. # [out, in]
    29. self.weight.data += (
    30. transpose(self.lora_B.weight @ self.lora_A.weight) * self.scaling
    31. )
    32. self.merged = True
    33. elif self.merge_weights and self.merged:
    34. if self.r > 0:
    35. self.weight.data -= (
    36. transpose(self.lora_B.weight @ self.lora_A.weight) * self.scaling
    37. )
    38. self.merged = False
    39. def eval(self):
    40. nn.Linear.eval(self)
    41. self.lora_A.eval()
    42. self.lora_B.eval()
    43. def forward(self, x: torch.Tensor):
    44. if self.disable_adapters:
    45. if self.r > 0 and self.merged:
    46. self.weight.data -= (
    47. transpose(self.lora_B.weight @ self.lora_A.weight) * self.scaling
    48. )
    49. self.merged = False
    50. return F.linear(x, transpose(self.weight), bias=self.bias)
    51. elif self.r > 0 and not self.merged:
    52. # W + w_delta
    53. # xW + x*w_delta
    54. result = F.linear(x, transpose(self.weight), bias=self.bias)
    55. if self.r > 0:
    56. result += self.lora_B(self.lora_A(self.lora_dropout(x))) * self.scaling
    57. return result
    58. else:
    59. return F.linear(x, transpose(self.weight), bias=self.bias)

            几种高效精调的核心部分代码都放在了网盘里。请关注同名微信公众号【旅者时光】后,在资源获取栏选择【语言模型】获取。

  • 相关阅读:
    C#线程的参数传递、获取线程返回值以及处理多线程冲突
    [重庆思庄每日技术分享]-ORACLE 12C 新功能 max_idle_time
    复现XSS漏洞及分析
    60、Flink 的项目配置 高级配置 详解
    C++ :Symbol:符号
    一站式洞察行业热点,飞瓜数据B站新功能「流量大盘」上线!
    大数据项目之电商数仓DataX、DataX简介、DataX支持的数据源、DataX架构原理、DataX部署
    我的react面试题整理2(附答案)
    Nginx内存池(内存池重置函数)
    DJ12-1 8086系列指令系统-1
  • 原文地址:https://blog.csdn.net/qq_45055172/article/details/143424200