Femtotron开发日志 #11 监督微调 Supervised Fine-Tuning, SFT

这是一个相对较小的更新工作量。虽然是minor的feature,但SFT的数据划分模式还是值得总结一下的,毕竟俗话说的好,魔鬼藏在细节中嘛。

当然,这篇不会很长就是了。

SFT和预训练的区别

数据格式

SFT仍然需要把数据划分为若干个规整的段落,只不过不是像预训练一样把无穷无尽的连续文本截断、截断、截断,而是反过来,把来自不同场景的单轮或者多轮对话拼接在一起。对话之间没有因果关系,因此如果多个对话被切分在一起,就需要正确的设置Attention当中的因果mask矩阵。如果不想处理这个问题,naive的方式是让每个序列里一定只有一个场景的连续对话,而seq_len直接等同于这么多连续对话当中最长的那一个。这个就是所谓的Padding方案。这个方案很明显会产生巨大的Padding开销(因为大部分对话不可能有最长的那么长,而且很可能短很多),因此实践当中根本没有人会真的这么去做。另一种是Packing方案,即允许不同场景的对话存在于同一个序列里,通过因果mask遮罩处理它们的非相关性。这是生产当中更经常采用的方案。它的具体实现是一种叫做“首次适应递减算法”(FFD, First-Fit-Decreasing)的经典近似启发式算法:意思就是把对话按照长度降序排列,然后依次取出所有对话,对每一个检查每个现存的seq序列;如果现有序列有能够容纳的,就装入第一个长度允许的序列里;如果所有序列都不能容纳,就增加一个seq作为新的单独sample。

有意思的是,数学上可以证明,FFD算法是非常优秀的近似算法,其使用的序列数绝不会超过最优解的$11/9$倍再加上1。

Loss Mask设置

这是另一个工程细节问题。具体来说,当我们对大模型进行后训练的时候,通常不希望LLM学习到是如何提问的,而是学习到是如何回答的。因此,对于用户提问的部分(以及广义上的其他“不需要学习的部分”),我们会设置它们在计算loss的时候被视为ignore_index(一般默认值是-100),设置loss_mask为false,从而mask掉这些我们不期望的部分中的loss的计算。我们幸运的是,Pytorch对其的支持已经很好,直接复用Attention相关的基础组件支持即可,其能够正确处理各种细枝末节的工程细节,例如重新计算有效Token数量和对loss进行缩放这些麻烦的事情。

学习率

这是另一个需要注意的点:预训练和后训练所需要的学习率并不相同,一般需要比预训练小半个到一个数量级。这某种意义上相当于延续了预训练后期decay的学习率,而不是它的标称值。

如果能够有更多工程的实践经验就好了。我依稀记得,对于不同训练阶段、不同数据量和不同参数量的最佳学习率,学术界已经有了一些相当精彩的理论工作。如果后面有空的话,也会抽空更新一些学习日志,专门研究这些的。