摘要
本文是手搓大模型系列的第四篇文章,本文主要介绍了模型预训练的一些小技巧,LoRA微调以及SFT微调。
后续文章会继续探索强化学习相关内容,喜欢的朋友麻烦点个赞吧~
项目开源说明
本项目的代码,数据,权重均已开源,可以在单张5090显卡上跑通模型预训练,全量SFT和LoRA微调的全流程。
同时项目还提供便捷的WebUI,可以轻松体验本项目的Base模型和微调模型。
仓库链接:https://github.com/QZero233/TinyLM
重新预训练
书接上文。在预训练了一个305M的模型之后,我尝试使用全量SFT和LoRA微调两种方式进行微调,但是效果都很差,甚至没法输出一段逻辑连贯的话。
与之相对比的,tiny-llm-zh里面的92M模型能够流畅地完成问答和对话。于是我开始停下来反思其中的原因。
由于两者在模型架构上几乎相同,所以问题只可能是数据和训练量。
在数据方面,tiny-llm-zh用的是中文百科数据(百度百科和Wiki CN),数据质量会干净很多,也天然的适合做常识类的QA任务,而我们的Open Web Text数据集相对而言更杂乱,数据量比百科要少,数据分布也更为稀疏,更不利于模型学习到一些常识类的知识。
在训练量方面,tiny-llm-zh的92M模型训练了9B个token,这直接是模型参数量的100倍了;相比之下,我们的模型训练量还不到参数量的20倍,这里的差距是很明显的。
所以,我决定使用tiny-llm-zh提供的中文数据来重新训练一个更小的模型。也就是遵循Chinchilla经验法则的指导,在算力资源有限的情况下,用更小的模型参数来换取更大的训练量。
最终,模型的参数量从305M缩减到了139M,这里主要是缩减了d_model的大小,从原来的1024调整到了768,并且tokenizer也换成了tiny-llm-zh同款的chatglm3 tokenizer。
此外,还有一个比较大的改动就是:模型的LM Head和Embedding层共享权重了。
权重共享
所谓权重共享,就是指模型最开始的Embedding层和最后的LM Head层使用同一个矩阵作为参数。
如下图所示,Embedding层的参数是一个vocab_size * d_model的矩阵,在输入阶段,一个token会被编码成一个vocab_size大小的one-hot向量,然后与vocab_size * d_model大小的Embedding矩阵相乘,得到d_model大小的向量。
然后这个向量会经过一系列Transformer层,最终结果还是一个d_model维向量。
最后,这个d_model向量和之前那个Embedding矩阵的转置相乘,便能回到vocab_size维向量。
LM Head和Embedding层共享权重已经是业界一个比较常见的做法了。GPT系列,LLAMA系列,Qwen系列的部分模型都有使用权重共享。
在transformers库中,可以配置
config.tie_word_embeddings = True来启用权重共享。
至于为什么能进行权重共享,从直觉上来看,Embedding和LM Head本质上都是在做查字典的操作,Embedding负责把某一个具体的token映射到模型空间中,而LM Head则负责把模型空间中的词映射回token空间。
既然是字典,那英译中和中译英使用同样的字典自然是很合理的。
当然,在深度学习的世界里,只有直觉是不够的。
《Using the Output Embedding to Improve Language Models》这篇文章通过实验验证了权重共享能起到一种正则化的效果,能够提升模型的泛化性。
从训练成本的角度来看,权重共享本身也能减少参数数量,从而减少训练成本。
这个收益在小模型上更为明显:我们模型的vocab_size是64798,d_model是768,那Embedding矩阵的大小就是49M,占了模型参数量的35%
如果能把这部分参数量节省下来,就可以在同等参数量的情况下提升模型的深度和d_model数量,从而提升模型的知识积累能力。
混合精度训练
接下来的几个小节会展示几个重新预训练时发现的新的小技巧,以及遇到的一些坑。
首先是使用混合精度进行训练。
在之前的训练中,我们一直都是在使用FP32精度来进行训练,即一个浮点数占用4个字节。
但是对于某些精度不敏感的操作(例如Linear,ReLU),在这些算子上面使用FP32会在增加显存占用的同时拖慢计算速度。
❝
(这里有个参考数据:在NVIDIA B200上,FP32的最大吞吐量是80TFLOPS,在FP16或者BF16上的最大吞吐量是2500TFLOPS,这个差距是很大的)
这时我们就可以借助PyTorch提供的autocast来进行混合精度训练。
同样的,在PyTorch里,接入混合精度和DDP,torch.compile一样简单,不需要对模型做任何修改,只需要使用一行代码套上一个Context即可,示例代码如下所示:
with torch.autocast(device_type="cuda", dtype=torch.bfloat16): logits = model(input_ids, None) ce_loss = cross_entropy_loss(logits, labels)在autocast上下文里,PyTorch会根据算子类型自动调整算子的数据类型,对于精度不敏感的类型会使用BF16
对于精度敏感的类型,例如包含求和规约的LayerNorm等,则会使用FP32。
在使用了混合精度之后,训练速度提升是很明显的,训练速度几乎是原来的2倍。
与此同时,显存占用也从原来的26GB降到了18GB,这使得我们可以进一步减少gradient checkpoint的数量,来提升训练速度。
序列数据的采样方式
这是这次预训练前期遇到的一个坑,这个问题可以描述为:数据集里是一系列连续的token,但是训练时需要的是长度为1024的连续的token序列,应该怎么采样这1024个token呢?
一个很直观的思路就是每次采样的时候随机选择一个起点,然后从这个起点出发采样1024个token。
这种方法很直观,但是会有token利用率的问题,即:多次采样到的片段可能有重叠(Overlap)的部分,这会导致实际训练到的不同的token数量小于理论值。
下图是一个模拟实验的数据图,这里假定总token数量为3.6B,橙色曲线表示随机起点采样训练到的不同的token占数据集总量的比值(即真实训练token覆盖率)
而蓝色图则是理论最大值(即采样到的总token数量与总token数量的比值)。
❝
(注:这里每一步采样16组长度为1024的token序列,和预训练实验里的batch size对齐,图中数据是蒙特卡洛出来的)
可以观察到,在20w步的时候,理论上模型应该已经见过88%的训练集数据了,但是在随机起点采样的情况下,模型才只见过58%的数据,有相当一部分的算力被浪费在了训练已经见过的token上面。
从第一性原理的角度来看,模型没法回答出它没见过的问题,因此这种情况下模型的表现必然会更差。
说完反面案例,接下来我们来看看正面案例。
现在主流的方法是先分块,再进行不放回采样,具体两者的区别如下图所示。
由于每次选择Block都是不放回采样,所以这种方法的训练token覆盖率就严格等于理论最大值,即不会有算力浪费在训练已经见过的token上面。
这种方法的确也会存在一些问题,例如:一段连续的关键知识可能会被切分到两个block里。
这个问题完全可以靠暴力scale来解决:一方面,现在大模型的预训练上下文长度在提升;另一方面,同一个知识在海量训练数据中肯定会出现多次,这就导致模型训练时一次都见不到这个完整知识的概率是微乎其微的。
DataLoader的shuffle的代价
这是上面那个小节的一个补充,也是踩到的一个坑。
在309M那个时期,我的Dataset的实现方式是把getitem里传来的index作为序列第一个token在数据集中的下标,然后以此往后采样1024个token,并使用了DataLoader提供的shuffle=True来实现随机采样。
当时遇到的一个很大的问题就是:训练启动很慢,而且很占内存。当时以为这个慢是因为torch.compile,占内存是因为加载数据,所以也没太在意。
直到后面尝试把随机起点采样放到Dataset侧,并取消了shuffle=True时,我才发现原来训练启动能这么快,并且内存占用也比之前低了一个数量级。
出现这个现象的原因在于shuffle=True的实现方式,DataLoader为了实现不放回采样,会构造一个和Dataset相同大小的下标数组,然后把这个数组shuffle。也就是说,这里其实显式地构造了一个和数据集大小相同的下标映射。
我们可以算一下构造这个映射的代价有多大:由于数据集大小远大于序列长度1024,所以这里直接认为数据集大小就是3.6B,如果以Python的list来存储,每个元素会占用28字节的int对象,和8字节的指针,那么总内存消耗能达到惊人的129GB。
这里给我带来的启发是:在使用DataLoader自带的shuffle之前,要考虑数据集的大小。
对于很大量的数据,能在Dataset里直接做随机采样自然是最好的。如果不能,那最好先对数据做分片,在分片内进行shuffle,以避免构造整个数据集的shuffle映射。
在项目实际实现中,我还是使用了Dataset分块+DataLoader shuffle
这是因为在分块之后,Dataset的大小缩小为了以前的1/1024,下标映射数组的大小也同步的下降了3个数量级,这种代价已经完全是可接受的了。
Tensorboard可视化
这是一个很有用的数据记录工具。它可以在每个step中记录某个标量值,例如当前的lr,valid loss等,然后根据这些记录日志绘图。
更重要的是,Tensorboard的日志记录是不断追加的,也就是说,即使是中途从之前的某个checkpoint开始重新训练,step发生了回退,Tensorboard终端也会智能的处理这种情况,并且之前记录的数据也不会丢掉。
Tensorboard的一个常见用法如下所示:
from torch.utils.tensorboard import SummaryWriterwriter = SummaryWriter(log_dir="./runs/exp1")global_step = 0for epoch in range(num_epochs): for batch in dataloader: loss = train_step(batch) writer.add_scalar("train/loss", loss.item(), global_step) global_step += 1writer.close()如果是在autodl上训练,可以把日志目录设置为/root/tf-logs,然后打开AutoPanel的TensorBoard,就能直接查看相关数据了。
如果是运行项目自带的webui,这个webui还会把本机的TensorBoard端口转发到本机的6006端口,这使得我们可以直接使用autodl提供的自定义服务链接来访问。
过拟合实验排查明显错误
我们经常会遇到这种情况:在训练了一段时间之后,模型的表现仍然不好,此时可能会纠结要不要继续训练下去。
这时候可以考虑先用少量重复数据来做一个过拟合实验,以确认模型效果不佳不是代码实现错误导致的
(当然,在这之前最好先用AI检查一轮,看看有没有很明显的实现错误)。
例如,可以收集10条“法国首都是巴黎”这种数据,然后反复训练到过拟合,确认loss能下降到0.1以下的水平,然后用数据集里存在的Prompt做自回归推理。
经过这一步检查之后,基本就可以排除大部分代码实现错误了。
虽说这样检查之后,也不能保证继续训练下去效果会变好,但是至少能够避免事后发现Label没有移位之类的低级错误而血压飙升。
观察参数更新率
在遇到train loss震荡,但是valid loss还在缓慢下降时,可以考虑观察一下参数的实际更新率,以此来判断当前的学习率是否合适,有没有出现死掉的参数。
参数更新率(Optimizer Update Ratio)的计算方法是:
其中norm是取的二阶范数。
本项目自带了打印参数更新率的功能,把json里的print_optimizer_update_ratio设置为true即可。
一般而言,在预训练中,参数更新率在1e-4左右是个比较健康的范围,如果太小了就需要考虑调大一些学习率。
(不同的情况可能会有所变化,具体的可以考虑和GPT老师交流一下,这里只提供一个思路)
结果展示
最终在训练了9.3B个token之后,模型的valid loss来到了2.68。训练时的lr曲线和loss曲线如下图所示:
并且模型在few shot的情况下能够表现出一定的知识储备和泛化能力。以下是一些实际的例子:
日期推理
英文翻译
地理常识
分类常识
不过当前依然有很多case效果表现不好,一个典型的例子就是模型几乎没有算数能力
并且现在base模型还不能进行QA问答,如果Prompt是类似于“介绍一下地球”的指令,模型会答非所问。
小结
预训练的主要目的是让模型具有语言能力和世界知识,从这个角度来看,我们的139M中文模型已经具备这种能力了。
在预训练过程中,我最深的一个感悟就是:数据才是决定模型效果的第一因,数据的重要性远远超过炼丹本身。
即使是大模型,也依旧遵循Rubbish in, Rubbish out的法则。
之前的305M模型的训练数据是OWT,模型能够接一些日常对话,但是没法对一些知识类问答做出响应;这次139M模型的训练数据是中文百科,所以这次模型就有了回答一些百科知识的能力。
如果按照这个思路推广开来,一个足够体量的模型,加上足够强大的算力基建,和全网大量的数据,就能够训练出一个表现出“涌现”能力的大模型。
LoRA微调
预训练后的Base模型只是有一定的语言能力和知识,还不能直接和人类对话。
这其中的本质原因在于:预训练阶段模型只见过大量连续的文本,几乎没有见过日常对话的示例。
例如,预训练数据里面可能有“北京,是中国的首都”,但是没有“问:中国的首都是哪? 答:北京”。所以我们需要通过微调来修正模型在面对各种指令时的答案分布。
这里我首先尝试了使用LoRA技术来进行微调,尝试之后发现效果比较差,于是最后使用了全量微调。开源的代码仓库里有两次微调的结果权重,感兴趣的读者可以自行体验一下两者的差别。
这一章节主要介绍一下LoRA微调的原理,实现以及效果。
原理
在介绍LoRA之前首先需要回顾一下传统的全量微调是如何做到的。
传统的全量微调流程和预训练几乎相同,微调的数据集是由一组一组的问题-答案对组成,如下所示:
{"question": "为以下文本生成一个摘要:在明天的会议上,我们将讨论公司的财务状况,并提出几个建议来改进状况。\n在此会议上,我们会讨论公司目前的财务状况以及如何改进它。","answer": "会议的议程将会涉及公司的财务状况和如何改进它。"}微调的时候首先把数据组织成
<|user|> question <|assistant|> answer <|eos|>的形式(其中形如<|xxx|>的是一个special token)。
然后把数据送入模型,得到预测结果,把label的问题部分设置为Ignore Label,计算交叉熵,反向传播更新参数,over。
(关于Ignore Label:在上一篇文章里有过详细介绍,本质是为了让模型预测答案而不是复读问题,可以参考下面这张图理解一下,这里不再赘述)
通过上面的描述,我们可以看出:全量微调所需要的显存等资源和预训练几乎是相同的。
如果我们想要微调Qwen 32B这种体量的模型,即使是在使用了ZeRO-3的情况下也需要4-8张A100-80G。
于是,LoRA诞生了。
LoRA的全称是Low-Rank Adaptation,其基本思想是:在原模型的参数基础上再外挂一个参数量更小的参数,然后在微调时,冻结模型原本的参数,只训练这个参数量更小的外挂参数。
一个具体的例子如下图所示:对于需要微调的Linear层,LoRA会额外新增两个参数矩阵A和B,其中B的形状是(in_features, r),A的形状是(r, out_features)
两个矩阵相乘正好能得到和参数矩阵形状相同的(in_features, out_features),所以可以把BA认为是一个外挂的参数参与线性层的计算。
在现实中,一般in_features和out_features的数量级都是几千,r一般取8或者16
此时LoRA引入的增量参数A和B的大小是远小于原始参数矩阵的参数大小的。这就是LoRA微调节省显存的秘密所在。
实现
在实现层面,需要做两件事。
首先是把模型里需要微调的线性层换成上述的LoRA线性层。对于Transformer模型,一般的处理方式是对Multi Head Attention处的QKV投影层进行微调,如下图所示。
然后就是需要把不需要参与训练的原始模型参数进行冻结,这一步在PyTorch里实现起来很简单,只需要把不需要更新的参数的requires_grad设置为False即可,用下面这两行代码就能完成:
for name, param in self.named_parameters(): param.requires_grad = False是否冻结Embedding
我们在微调的时候会引入预训练数据中没有的一些special token,例如<|user|>
在这种情况下如果依然冻结Embedding显然是不合理的,因为模型在预训练时没有见过这些special token,其对应的Embedding层自然也没有学会关于这些token的语义。
但是另一方面,我们又不希望在微调时改动其他token对应的Embedding层参数,也就是说,我们其实希望只训练这些新增的token对应的Embedding。
这里HuggingFace的transformers库提出了一种只训练部分Embedding参数的解决方案:把Embedding层参数的requires_grad设置为True,但是更新参数之前把除了新增token以外的行的梯度置零。
这个方法非常简单粗暴,在得知这个之前我一直以为transformers库的实现是像LoRA那样新增了一个小的Embedding参数,在训练时把这个小参数的requires_grad设置为True,最后把两个参数合并。
不过在本项目的代码实现中,Embedding层依然是全量微调的。感兴趣的读者可以自行尝试一下,如果在LoRA微调时只微调那几个special token对应的Embedding,效果会不会更好。
效果展示
LoRA微调之后,其在SFT验证集上的loss为2.83,训练过程的valid loss曲线如下图所示
虽然loss很好看,但是模型的实际表现很糟糕。对于一些在全量微调下能很好回答的问题,LoRA微调之后无法正常回答,例如下图中的“介绍一下玫瑰花”,模型直接复读了玫瑰花然后就输出eos了。
但是很奇怪的是,如果在问题后面加一个问号,那模型的表现又不同了
还有一个勉强算是输出了一些有意义的内容的例子:
小结
LoRA微调的效果并不是很好,可能主要原因是Base模型本身能力不够。
一般来说,我们都是对Qwen 32B这类比较强,参数量也比较大的模型进行LoRA微调的,这里LoRA微调也只是起到一个演示作用了。
全参数量微调
在LoRA微调失败以后我又尝试了全参数量微调,最终valid loss降到了2.63,训练时的valid loss和lr曲线图如下所示:
可以发现,valid loss在1w步左右的时候出现了一个peak,这是在微调过程中需要非常小心的一个点:微调时特别容易过拟合,所以需要用更小的学习率来训练,一般是训练时的1/20到1/30。
结合右图的lr曲线可以看得更明白:我在1w步以前还是用的cosine lr scheduler,在1w步的时候观察到了loss反弹,于是手动把学习率固定在了5e-7。
最终的自回归效果如下:
可以发现,模型已经能够用逻辑连贯的流畅的语言回答问题
并且也学会了用言简意赅的短语来回答问题
但是依然还存在一些复读的问题
小结
目前全量微调之后的模型效果已经足够惊艳了,“训练出一个能说人话的模型”是我这个项目的最终目标,现在它无疑是已经做到了。
传统产品经理,正在成为下个被淘汰的“传统岗位”。
过去画原型、写 PRD、跟进度的“传统技能包”,在AI时代正迅速贬值。63% 的企业转型做 AI 产品!当下的问题不再是“要不要学 AI ”,而是“如何构建 AI 产品”。
前段时间还跟字节、腾讯的资深 AI 产品经理沟通,他们反馈:在大量招人,只要有 AI 相关的项目经验,基本都能拿到面试机会,而且领导很舍得给钱,涨薪 40-60% 很正常!
01
接下来的产品人,得卷AI能力了!
如今AI大火,行业极速发展的背后,懂AI 产品人才却严重稀缺。这不是要你转技术岗,而是要掌握构建 AI 产品的核心方法:
- 如何将你的领域知识,转化为 AI 产品的核心竞争力?
- 如何用 AI 技术实现你的产品需求?
- 如何设计真正懂用户的 AI 交互体验?
- ……
懂AI,就是产品经理的“救命稻草”!
风口之下,与其焦虑被行业淘汰
不如先人一步享受AI技术带来的红利!
我把AI产品经理的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】
(不限年龄!不限岗位!没有代码基础也能学!)
🎁现在扫码,完课还送:
《AI产品面试题库》《AI大模型应用案例集》
02
掌握技术+实战,快速转型!
想成为一名卓越的AI大模型产品经理,需要从技术、到项目实战的全方位转型指南!
**1)**AI产品应用原理解析,产品经理也能听懂!
对于产品经理来说,如果你不懂技术,做不了业务和AI大模型技术衔接、定义不了数据需求,是没法完整的落地一个产品的!
本次课程,专门面向产品经理人群,解析当下最热门的AI产品应用的必备的「大模型」、「多模态」的实际应用和算法原理!解析AI产品应用技术,积累大模型能力!简单易懂,不需要会代码,小白也能掌握!
- 大模型微调:掌握主流大模型(如DeepSeek、Qwen等)的微调技术,针对特定场景优化模型性能。学习如何利用领域数据(如制造、医药、金融等)进行模型定制
- AI Agent智能体搭建:学习如何设计和开发AI Agent,实现多任务协同、自主决策和复杂问题解决。构建垂类场景下的智能助手产品(如制造业中的设备故障诊断Agent、金融领域的投资分析Agent等)
2)超全行业案例解析!
课程详细讲解现阶段,大模型在各个行业和领域的应用现状!包括:零售与电商、教育、医疗、泛娱乐、法律等等10大行业!
详细讲解案例的思路、应用场景,以及背后的技术原理、核心技术!揭秘各个行业、场景的真实现状,和未来产品的发展与机遇!
可以说,讲解完一个案例,就能积累一个AI产品实践的经验!
课程中所涉及到的实战项目,都可以直接在自己的工作中使用,让自己的产品/项目有可借鉴的成功案例!
3)AI产品经理求职专项辅导
课程中会系统的帮助大家拆解字节、腾讯、百度等大厂AI PM岗位JD关键词,掌握AI PM高频面试题型与回答框架;展示 AI 相关能力的关键技巧:Prompt设计、模型评估、A/B测试、成本意识、与算法/工程协作经验;
- To B类AI产品经理:突出“行业理解 + 技术落地 + 商业闭环”能力的简历结构设计,展示项目成果;从客户需求洞察到技术方案设计,展现端到产品思维;如何评估To B AI产品的可行性、客户付费意愿与实施成本
- To C类AI产品经理:拆解头部公司岗位JD,将过往尽力转化为AI产品叙事逻辑;从行业趋势、产品设计题、案例分析&数据分析题、技术理解边界等全流程辅导面试;避免无效海投、锁定最适合的AI产品岗位;
03
本次课程,全程直播讲解,能直接对话大佬和专业助教,不懂就问,超详细的案例,小白也能轻松get!
完课后,还赠送《AI产品经理面试题库》、《AI大模型应用案例集》!不断更新中……
适合人群:
- 想转型AI产品经理、AI项目管理专家、AI产品解决方案等岗位
- 想进行AI产品创业的创业者
- 想成为制作AI产品的程序员
- 想利用AI解决企业问题的管理岗
- 想在AI方向寻找就业方向的毕业生
- AI方向前景广阔、待遇好!
目前,很多产品人已经通过完整学习拿到大厂高薪offer,收入嗷嗷涨!
我把AI产品经理的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~