从零开始训练大模型
转载自:
https://zhuanlan.zhihu.com/p/636270877
梳理一个完整的LLM训练流程,包括:
- 模型预训练(Pretrain)
- 指令微调(Instruction Tuning)
- 奖励模型(Reward Model)
- 强化学习(RLHF)
预训练
当前,不少工作选择在一个较强的基座模型上进行微调,且通常效果不错(如alpaca、vicuna等)。
这种成功的前提在于:预训练模型中通常已经包含微调任务中所需要的知识。如将LLaMA微调为alpaca和vicuna的例子中,LLaMA中已经包含了对应知识,微调只是将模型变得更符合人类的行为。
但在实际情况中,我们通常会遇到一些问题,使得我们无法直接使用一些开源基座模型进行微调:
续写任务测试 | LLaMA | MPT |
---|---|---|
杭州西湖是 | 杭州西湖是杭州的一个静静的一个游泳池,游泳池是杭州西湖的一个游泳池,游泳池是杭州西湖的一个游泳池,游泳池是杭州西湖的一个游泳池,� | 杭州西湖是中国最大的湖泊,是中国最大的湖泊,是中国最大的湖泊,是中国最大的湖泊,是中国最大的湖泊,是中国最大的湖泊,是中国最大的湖泊, |
琅琊榜的导演是 | 琅琊榜的导演是很多的人都不知道,因为他的父亲是一位杰作家,他的父亲的杰作家是一位杰作家, | 琅琊榜的导演是谁?Who are the directors of the Rolling Stone?琅琊榜的导演是谁?Who are the |
专业知识不足
:当我们需要一个专业领域的LLM时,预训练模型中的知识就尤为重要。由于大多数预训练模型都是在通用语料上进行学习,对于一些特殊领域(金融、法律等)中的概念和名词无法具备很好的理解。我们通常需要在训练语料中加入一些领域数据,以帮助模型在指定领域内获得更好的效果。如xuanyuan模型通过金融语料在金融领域的对话进行了增强。
基于上述原因,通常我们需要在开源的基座大模型上进行二次预训练。预训练分为如下几个部分。
数据处理
数据集的收集和清洗
中文预训练可以选择如下数据集:
公开数据集的数据质量不一定很高,需要进一步对数据进行清洗,如在Falcon这篇论文中提到了一些已有的数据集和它们的处理方法:
数据预处理
通常来讲,目前的大模型使用的基本上都是Transformer结构,由于Attention机制在时间和空间上的复杂度,输入模型的文本不会特别的长(如2048)。
这时需要将文本截断,但以书籍数据为例,一本书的内容肯定远远超过2048个token,直接采用头部截断的方式太过浪费训练数据(每本书永远只能够学习到开头的 2048 tokens 的内容,连序章都不一定能看完)。
因此,最好的方式是将长文章按照 seq_len(2048)作分割,将切割后的文本喂给模型做训练。
数据源采样
在GPT3中提到,对不同的数据源会选择不同的采样比例:
从上图可以看到,训练300B tokens时,通过不同的采样比例,相对较大的数据集(Common Crawl)相当于训练了0.44个epochs,而较小的数据集(Wikipedia)则相当与训练了3.4个epochs。
这样一来就能使得模型不会太偏向于规模较大的数据集,从而失去对规模小但作用大的数据集上的学习信息。
词表扩充
在进行预训练之前,我们需要先选择一个预训练的模型基座,然后在此基础上二次预训练。
一个较为普遍的问题是:大部分优秀的语言模型都没有进行充分的中文预训练,因此许多工作都尝试将在英语上表现比较优秀的模型用中文语料进行二次预训练,期望能够将模型在英语上的优秀能力迁移到中文任务中来。如Chinese-LLaMA-Alpaca。
但在进行正式的训练之前,我们还有一步很重要的事情去做:词表扩充。
因为在英文训练集上建立的词表不一定完全适用于中文,可能有些不该被拆分的词也被拆分了,所以要对原先的词表进行修改。
为了降低模型的训练难度,通常会在原来的词表上进行词表扩充,也就是将一些常见的汉字添加到原来的词表之后,最后再在中文语料上对这部分新扩展的 token embedding 做二次预训练。
如在BELLE大模型中,作者在 120w 行中文文本上训练出一个 5w 规模的 token 集合,并将这部分 token 集合与原来的 LLaMA 词表做合并,最后再在 3.2B 的中文语料上对这部分新扩展的 token embedding 做二次预训练。
语言模型预训练
在扩充完词表之后,我们就可以开始正式进行模型的预训练步骤了。
预训练任务
预训练任务就是让模型做Next Token Prediction任务,即预测输入文本的下一个token。
如输入为“我想吃苹”,那么下一个token大概率为“果”。 预训练任务就是最大化\(p(果|我想吃苹)\)的概率。
模型结构
上面说过,目前的大模型使用的基本上都是Transformer结构。
为了加快模型的训练速度,通常会在模型中加入一些 tricks 来缩短模型训练周期。 目前大部分加速 tricks 都集中在 Attention 计算上,如:
此外,为了让模型能够在不同长度的样本上都具备较好的推理能力,通常也会在Position Embedding上进行一些处理,如:
此外,还会有一些细节上的更改。比如在LLaMA2中:
- 使用了RMSNorm替换了LayerNorm并前置了其位置。
- 使用了SwiGLU激活函数替换ReLU。
RMSNorm(root mean square)发现LayerNorm的中心偏移没什么用(减去均值等操作)。将其去掉之后,效果几乎不变,但是速度提升了40%。
参数设置
在继续预训练中,我们通常会使用 warmup 策略,此时我们按照 2 种不同情况划分:
- 当训练资源充足时,应尽可能选择较大的学习率以更好的适配下游任务。
- 当资源不充足时,更小的学习率和更长的预热步数或许是个更好的选择。
模型效果评测
大模型评测数据集如下:
C-Eval:一个很好的中文知识能力测试数据集,涵盖1.4w 道选择题,共 52 个学科。 由于是选择题的形式,我们可以通过将题目写进prompt 中,并让模型续写 1 个 token,判断这个续写 token 的答案是不是正确答案即可。
但大部分没有精调过的预训练模型可能无法续写出「A B C D」这样的选项答案,因此,官方推荐使用 5-shot 的方式来让模型知道如何输出答案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17以下是中国关于{科目}考试的单项选择题,请选出其中的正确答案。
{题目1}
A. {选项A}
B. {选项B}
C. {选项C}
D. {选项D}
答案:A
[k-shot demo, note that k is 0 in the zero-shot case]
{测试题目}
A. {选项A}
B. {选项B}
C. {选项C}
D. {选项D}
答案:通过前面的样例后,模型能够知道在「答案:」后面应该输出选项字母。
于是,我们获得模型续写后的第一个 token 的概率分布(logits),并取出[A B C D]这 4 个字母的概率,通过 softmax 进行归一化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18probs = (
torch.nn.functional.softmax(
torch.tensor(
[
logits[self.tokenizer.encode(
"A", bos=False, eos=False)[0]],
logits[self.tokenizer.encode(
"B", bos=False, eos=False)[0]],
logits[self.tokenizer.encode(
"C", bos=False, eos=False)[0]],
logits[self.tokenizer.encode(
"D", bos=False, eos=False)[0]],
]
),
dim=0,
).detach().cpu().numpy()
)
pred = {0: "A", 1: "B", 2: "C", 3: "D"}[np.argmax(probs)] # 将概率最大的选项作为模型输出的答案C-Eval官网上的5-shot结果如下:
Model STEM Social Science Humanities Other Average GPT-4 67.1 77.6 64.5 67.8 68.7 ChatGPT 52.9 61.8 50.9 53.6 54.4 Claude-v1.3 51.9 61.7 52.1 53.7 54.2 Claude-instant-v1.0 43.1 53.8 44.2 45.4 45.9 GLM-130B 34.8 48.7 43.3 39.8 40.3 Bloomz-mt-176B 35.3 45.1 40.5 38.5 39.0 LLaMA-65B 37.8 45.6 36.1 37.1 38.8 ChatGLM-6B 30.4 39.6 37.4 34.5 34.5 Chinese LLaMA-13B 31.6 37.2 33.6 32.8 33.3 MOSS 28.6 36.8 31.0 30.3 31.1 Chinese Alpaca-13B 26.0 27.2 27.8 26.4 26.7
指令微调
在完成语言模型的预训练之后,就可以开始进行指令微调了,这一步也被称为SFT(Supervised Fine-tuning)。
由于预训练任务的本质在于续写,而续写的方式并不一定能够很好的回答用户的问题。例如:
用户问题 | 用户预期回答 | 模型续写结果 |
---|---|---|
《无间道》的主演有哪些? | 刘德华、梁朝伟 | 《无间道》的主演有哪些?不少观众期待看到阵容公告,今天小编... |
因为训练数据大多来自互联网中的数据,我们无法保证数据中只存在规范的一问一答形式的文本,这就会造成预训练模型通常无法按照预期的形式给出人们想要的答案。
但是,模型中并不是没有相关的知识,其实模型是知道相关答案的,只是模型的输出形式没有和人类的规定对齐。如这样询问就可以引导出答案:
用户问题 | 用户预期回答 | 模型续写结果 |
---|---|---|
《无间道》的主演有 | 刘德华、梁朝伟 | 《无间道》的主演有刘德华、梁朝伟和黄秋生,而这部电影也是香港警匪片的代表作之一。 |
不过,这种需要用户精心设计从而去套答案的方式,显然没有那么优雅。既然模型知道这些知识,只是不符合我们人类的对话习惯,那么我们只要再去教会模型如何对话就好了。这就是Instruction Tuning要做的事情,即指令对齐。
通过指令微调对齐之后,GPT-3就可以变为InstructGPT或ChatGPT这种对话模型:
数据收集
Self Instruction
既然需要教会模型说人话,那么我们就需要去精心编写各式各样的问题和答案,让模型学会人类的对话方式。
在 InstructGPT Paper 中,使用了1.3w的数据来对 GPT-3.5 进行监督学习:
这种规模的人工标注问答数据较难获取,所以stanford_alpaca里提出可以通过已有的对话模型ChatGPT来获取问答数据,这种获取对话数据的方式就是Self Instruction。通过将如下的prompt输入ChatGPT就可以得到需要的指令、输入以及输出。
1 | 你被要求提供10个多样化的任务指令。这些任务指令将被提供给GPT模型,我们将评估GPT模型完成指令的能力。 |
开源数据集
除了使用Self Instruction方法获取微调数据,我们还可以使用开源数据对模型进行微调。
- Alpaca
stanford_alpaca采用Self Instruction方法收集了5200条指令微调数据集,数据样例如下:其中,instruction 代表要求模型做的任务,input 代表用户输入, output 代表喂给模型的 label。1
2
3
4
5{
"instruction": "Arrange the words in the given sentence to form a grammatically correct sentence.",
"input": "quickly the brown fox jumped",
"output": "The quick brown fox jumped quickly."
}
Alpaca 覆盖了多种类型的指令,其数据分布如下: - BELLE
训练数据集样例如下:
1
2
3
4
5{
"instruction": "判断给定的文章是否符合语法规则。如果不符合,请提供修改建议。 下面是一篇文章的开头: ‘为了探讨这个主题,本文将提供一系列数据和实例,以证明这一观点。’",
"input": "",
"output": "这个开头符合语法规则。"
} - Vicuna
- BAIZE
指令微调模型训练
通过SFT(Supervised Fine-tuning)这个名字就可以知道,指令微调的训练过程是一个有监督的训练过程,通过上面数据收集步骤得到的数据对模型进行有监督的微调。训练任务仍然是让模型做Next Token Prediction任务,即预测输入文本的下一个token。
模型评测方法
奖励模型
在完成SFT之后,我们大概率已经能够得到一个还不错的模型了。但在SFT阶段,我们一直都在告诉模型什么是好的数据,却没有给出不好的数据。SFT的目的更多是将Pretrained Model中的知识给引导出来,而奖励模型和强化学习阶段更多是解决模型的有害和幻觉问题,让模型不要输出不该输出的内容,这就需要告诉模型什么是不好的数据。
为了让大模型知道什么是好的数据,什么是不好的数据,就需要一个奖励模型,给数据进行打分。好的数据会得到更高的得分,而不好的数据会得到较低的得分。
当然也可以不使用奖励模型打分,靠人工标注来打分,但这样的成本就太高了。 我们可以用少量标注数据训练一个奖励模型,然后通过奖励模型给大量数据打分,这样效率更高且成本更低,所以训练一个奖励模型还是很有必要的。
使用偏序对训练奖励模型
直接给数据打分较为困难,需要的人工标注成本很高,而且难以统一,所以可以不为每一个样本直接打分,而是标注这些样本的好坏顺序。如下两种方法:
1 | 直接打分:A句子(5分),B句子(3分) |
偏序对的方式更容易统一,且标注困难度大大减少。 因为使用了偏序对,所以损失函数不可以使用原先的Cross Entropy或者MSE,在InstructGPT论文中使用了pair wise ranking loss训练奖励模型:
\[loss(\theta)=-\frac{1}{K\choose2}E_{(x,y_w,y_l)~D}[log(\sigma(r_\theta(x,y_w)-r_\theta(x,y_l)))]\]
这里我们假设\(y_w\)的排序比\(y_l\)高,其中\(r_\theta(x,y)\)是RM模型对于prompt \(x\)生成回答\(y\)的打分。\(D\)是标注人员对于当前prompt以及模型生成答案的排序结果。 这样就可以让排在前面的问答对分数高于排在后面的问答对分数。
训练RM的数据量
奖励模型的训练数据量取决于具体的任务,可以参考InstructGPT中的数量和比例:
RM的大小
Reward Model的大小没有明确的限制,不过一种直觉的理解是:评分任务要比生成任务简单一些,因此可以使用稍小一点的模型。InstructGPT 使用了6B的RM,175B的LM。
强化学习
奖励模型训练好了之后,就可以用强化学习方法对大模型进行更进一步的训练,减少模型生成有害文本和幻觉信息。
PPO(Proximal Policy Optimization)
PPO算法可以参考这篇文章。
PPO有四个网络参数(这也是 RL 非常耗卡的一个重要原因):
- \(\theta\):训练网络,每次都会被更新。(也就是大家说的actor模型)
- \(\theta'\):训练网络副本,负责与环境交互采样数据。(也就是大家说的ref模型)
- \(\phi\):奖励模型,拟合折扣奖励。(也就是大家说的critic模型,获取每个位置的奖励)
- \(RM\): 奖励模型,获取一整句话的奖励。
PPO共涉及actor model,ref_model,reward model和critic model这四个模型,其实更新参数的模型只有actor model和critic model。
训练过程不稳定
由于 PPO 对超参非常敏感,不合理的超参搭配很有可能使得模型训练过程中 Reward 剧烈抖动。存在几个因素与此有关:
KL Penalty
:适当调大 KL可以帮助稳定训练(可使用动态调整 KL 系数策略)。Reward Model
:使用一个更稳定的 RM 能够有效缓解这种问题。Reward Scaling
:reward 的归一化对训练稳定有着很重要的作用。Batch Size
:适当增大 batch_size 有助于训练稳定。
训练结果不稳定
因为 reward 的提升并不代表模型真的表现的更好,可能是RM模型对某些数据有不正确的打分。
这种通过找到 shortcut 形成 reward 提升的现象又称为 reward hacking。
对于这种情况,除了提升 RM 本身的能力以外,我们还可以通过 Combine 多个 RM 以防止这种情况出现。
如 Llama 2 中同时使用了 2 个 RM(Safety + Helpful)来进行打分,不过论文中给出的理由是 Safety 和 Helpful 这两个任务目标之间可能存在冲突,但使用多个 RM 来综合打分同时也能较好的防止模型训到天上去。
DPO(Direct Preference Optimization)
DPO使用如下Loss进行训练:
\[L_{DPO}(\pi_{\theta};\pi_{ref})=-E_{(x,y_w,y_l) \sim D} \left[ log \sigma \left(\beta log \frac{\pi_{\theta}(y_w|x)}{\pi_{ref}(y_w|x)} - \beta log \frac{\pi_{\theta}(y_l|x)}{\pi_{ref}(y_l|x)} \right) \right]\]
其中:
- \(y_w\)表示偏序对中好的回答
- \(y_l\)表示偏序对中差的回答
- \(\pi_{\theta}(y_w|x)\)表示给定输入\(x\),当前policy model生成\(y_w\)的累积概率(即每个token的概率求和)
- \(\pi_{ref}(y_w|x)\)表示给定输入\(x\),当前reference model生成\(y_w\)的累计概率
- \(\pi_{\theta}(y_l|x)\)和\(\pi_{ref}(y_l|x)\)原理同上
由于只使用了两个模型(actor model和ref model)和偏序对数据进行训练,所以相对PPO更容易训练。
Zephyr-7B 基于 Mistral-7B 使用了 DPO 进行微调,而 Zephyr-7B 的实验表明,使用 DPO 后它优于同期所有同尺寸的其他模型。
BON(Best-of-N)
BON 也叫 reject sampling
,是指我们通过设置 temperature 值让同一个模型生成若干回复。接着,使用 Reward Model 挑出这些回复中得分较高的回复并再次训练原本的模型。 这是一个循环迭代的过程(\(Sample \to SFT \to Sample \to SFT \to ...\))。
在Llama2中使用了这种方法,论文中指出:在进行 SFT 时,应当使用之前所有策略下的 Good Samples(而非仅是最近一次策略模型 Sample 出的样本),以提高模型的泛化性。
KTO(Kahneman-Tversky optimization )
如上图,PPO和DPO的方法需要偏序对数据,即\((x, y_1, y_2)\),其中x是输入,回答\(y_1\)优于回答\(y_2\)。而KTO只需要输入、回答以及一个标签,即\((x,y,label)\),其中label表示回答\(y\)在输入\(x\)下是否可以被接受。