你是不是也有这种感觉:聊大模型的时候,别人张口就是Loss曲线、梯度裁剪、LoRA微调,一套一套的,听起来很专业,但问到最底层——模型到底是怎么一点点变聪明的?很多人就卡在“反向传播”和“梯度下降”这两个词上。
说白了,大模型再大,本质也就是一个参数很多很多的函数。训练过程里,梯度下降负责回答“往哪个方向调整能让错误变小”,反向传播负责回答“每一个参数对最终错误有多大贡献”。一个管方向,一个管算账,配合起来,模型才能在几万亿次迭代里真正学到东西。
这篇文章不搞那种贴满公式、看得人头皮发麻的推导。我会先讲清楚概念,再给一个完整的手算示例,最后说说我在预训练和微调实战里踩过的坑,包括学习率怎么选、梯度消失和爆炸怎么排查。适合两类人看:一是刚入门大模型、想搞懂基础原理的,二是已经在跑训练但总被各种玄学问题折磨的。
1. 先弄清大模型是怎么“学会”的:训练到底在做什么
1.1 模型的本质是一堆参数,训练就是调参数
先说一个反常识的事实:大模型不是像数据库一样存了一堆语料。训练完成之后,你问它“1+1等于几”,它不是去查“1+1=2”这条记录,而是把它见过的大量文本的统计规律,压缩进了几百上千亿个小数里。这些小数就是模型的参数,也叫权重。
你打开任何一个开源模型的权重文件,比如Llama、Qwen,会看到一堆张量,每个张量就是一个矩阵,里面填满了0.1、-0.03这种看起来毫无规律的数字。模型的所有“知识”,从语法到推理能力,都藏在这些数字的组合里。
那训练在做什么?本质上就是不断修改这些数字,让模型在给定输入的时候,输出的结果越来越接近期望答案。我习惯把它类比成调音响:你面前有一堆旋钮,每一个旋钮都会影响最终音效,你的目标是找到一组旋钮位置,让声音最接近理想效果。问题是,一台大模型的“旋钮”数量动辄几十亿甚至上千亿,靠人手一个个试根本不现实,只能靠算法自动去调。
为什么不能直接暴力枚举所有参数组合?因为组合数量是天文数字。哪怕每个参数只取两个值(比如0和1),一个700亿参数的模型,组合数就是2的700亿次方,这个数字大到宇宙里所有原子数量在它面前都不值一提。所以必须有一套聪明的方法,能从当前这组参数出发,逐步逼近一个足够好的解。
1.2 两个机制的配合:一个管方向,一个管算账
于是就有了两个分工明确的机制。
梯度下降负责回答方向问题:基于当前的错误情况,参数该往哪个方向调?它的思路非常朴素。想象你站在一座山上,周围一片大雾,你看不到山底在哪,但你能感觉到脚下的坡度——哪个方向是往下走的,你就朝哪个方向迈一步。这里的“山高”就是模型的整体错误,也就是损失函数的值,“坡度”就是梯度。
反向传播则负责回答算账问题:为了算出来这个“坡度”,需要知道每一个参数对最终错误的影响程度。前面那层参数变动0.01,对最终输出影响多大?后面那层又是多大?如果没有一个高效算法,挨个去试的话,成本高到完全不可接受。反向传播就是那个能在一趟计算里,把所有参数的“影响程度”全部算完的方法。
一句话总结这两者的关系:梯度下降是大脑,决定往哪走;反向传播是眼睛,负责看清每一步的坡度。没有梯度下降,模型连改进方向都没有;没有反向传播,梯度下降也只能原地打转,因为梯度的计算会卡死你。
下面我会先单独把梯度下降讲透,再单独讲反向传播,最后把它们放回大模型的真实场景里看实战会发生什么。
2. 梯度下降:靠直觉找下山路的数学方案
2.1 先得有一个“错误数值”来衡量好坏
要让梯度下降工作,第一步是把“模型现在有多差”量化成一个数字,这个数字就是损失函数的值。
最基础的损失函数是均方误差MSE,把预测值和真实值之间的差距平方之后取平均。为什么用平方?两个原因:一是平方之后误差恒为正数,大小可以比较;二是大误差会被放大得更厉害,让模型更重视那些错得离谱的样本。
但大模型训练,尤其是语言模型,几乎不用MSE,而是用交叉熵损失。原因在于,语言模型的输出不是一个数值,而是一个概率分布——给定前文,每个词都有预测概率,交叉熵能直接衡量“预测概率分布”和“真实词分布”之间的差异。
我拿做菜打个比方:MSE像评分制,只看最终分数差多少;交叉熵像点菜盲猜,你给的是一整桌菜的概率排序,不仅要菜名对,概率高低也得对。语言模型需要在几万个词里区分细微概率差异,所以交叉熵更合适。而且交叉熵在预测概率接近正确时,梯度依然能给出合理信号,不会像MSE的两端那样梯度极小、训练推不动。
这也是为什么你在看GPT类模型训练日志时,loss基本都叫CE Loss。如果有一天你自己跑大模型预训练,看到loss是0.1、0.2这种量级,不要慌,交叉熵loss本来就不可能到0,它衡量的只是分布差异。
2.2 梯度:把“坡度”翻译成能用的数学语言
有了损失值,下一步要回答“往哪调能让损失降低”。这时候需要梯度的概念。
回忆一下高数里的导数:它衡量的是,当你把某个变量调大一点点,函数值会变大还是变小、变化多大。对有两个以上参数的模型来说,“导数”推广成了“偏导数”,把所有偏导数拼成一个向量,就叫梯度。
梯度有个非常实用的性质:它指向函数值上升最快的方向。所以我们只需要沿着梯度的反方向调整参数,loss就会下降。这个动作写成一个很简单的更新公式:参数 = 参数 - 学习率 × 梯度。
学习率就是这个公式里的“步子大小”。学习率设得太大,你可能一下迈过头,越过山谷直接冲到对面山坡上,loss来回震荡甚至爆炸;设得太小,每一步只挪动一点点,可能几万年都到不了山底。这个超参数是整个训练里最关键、也最容易翻车的设置之一,后面我会专门讲怎么选。
这里有个值得想的细节:为什么不能直接算出让loss等于0的参数,而要一步一步走?因为真实模型的loss是极度复杂的非线性函数,坑坑洼洼,根本没有解析解,只能靠迭代逼近。而且哪怕有办法一步到位,计算成本也高到无法想象。梯度下降的价值就在于,每一步只需要计算局部的坡度信息,就能稳步前进。
2.3 从全量到Mini-batch再到Adam:优化器也在进化
梯度下降的思想很古老,但工程实现上经历了几个版本,你平时听到的SGD、Mini-batch、Adam,都是它的变体。
最初版本叫批量梯度下降,每次更新参数时,要把整个数据集的所有样本全部算一遍,取平均梯度再更新。好处是梯度方向很准确;坏处是数据集一大根本算不完。对大模型来说,训练语料动不动几个T的文本,把全量样本扫一遍再更新一次参数,一次更新可能都要几天,显存更是装不下。
于是有了随机梯度下降SGD,每次只随机抽一个样本来算梯度、更新参数。单样本计算快,但噪声极大,loss曲线像喝醉了一样来回抖,收敛效率很差,动不动抖出舒适区。
工程上真正常用的妥协方案是Mini-batch梯度下降:每次随机取一小批样本,比如32、64、128个,算这批样本的平均梯度来更新。这一步既利用了批内样本的统计均匀性,让梯度方向比单样本稳定,又不需要等全量数据算完才更新,是速度和精度的折中。
到了大模型时代,连Mini-batch SGD都很少用了,因为它的学习率不会对参数做自适应调整,对每层参数量差异极大的深度网络很不友好。现在主流是Adam,以及改进版AdamW。Adam把梯度的历史信息用起来:维护梯度的一阶矩估计(相当于动量)和二阶矩估计(相当于自适应学习率),让每个参数有自己独立的更新步幅,平坦区域走大步,陡峭区域走小步。
我实际训练的感受是:预训练和微调里直接用AdamW默认参数,lr设在1e-4到3e-4之间,其他保持beta1=0.9、beta2=0.999,基本都能跑动。AdamW和Adam的区别主要是把权重衰减从梯度里抽出来单独处理,在大模型上能明显减少过拟合。所以Hugging Face上几乎所有LLM训练脚本都默认用AdamW。
下面这张表可以帮你快速理解几代优化器的区别:
| 优化器 | 更新依据 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| BGD | 全量样本平均梯度 | 方向准确 | 算不动、显存爆炸 | 小数据集教学示例 |
| SGD | 单个样本梯度 | 计算快 | 噪声大、震荡严重 | 简易模型、教学 |
| Mini-batch SGD | 一小批样本平均梯度 | 速度与精度折中 | 每层学习率固定 | 中型模型训练 |
| Adam/AdamW | 梯度一阶矩+二阶矩 | 自适应学习率、稳定 | 显存占用略高 | 大模型预训练、微调标配 |
2.4 学习率:最让人头大的超参数
学习率是整个训练里最强的“放大器”,也是我每次跑实验最先检查的变量。学习率过大时,最常见的现象是训练刚开始loss不降反升,过几个step直接变NaN。原因是参数一步被推得太远,激活值溢出,梯度变成无穷大,数值计算直接崩掉。
学习率过小则相反,loss持续下降,但每分钟只降零点零零几。你可能跑了几百步才发现,这种龟速收敛根本到不了好的结果。我自己的经验是:跑新模型的第一件事不是急着开长训练,而是先用很小的模型和数据试几个量级的学习率,看哪个量级让loss下降最快且稳定。
进阶一点的做法是warmup策略:前几百步学习率从0逐渐升到目标值,之后再按余弦退火慢慢降下来。为什么需要warmup?因为训练初期参数离最优点很远,梯度方向波动巨大,一上来就用大学习率,等于刚睁眼就被迫冲下坡,很容易摔。慢慢加速让优化器先“熟悉地形”,后面反而跑得更稳。这个细节在GPT原始论文里就有体现,现代预训练流程基本都沿用了。
3. 反向传播:让误差“倒流”回每一层
3.1 为什么要“反着算”,而不是挨个暴力试
现在到了反向传播的重头戏。梯度下降告诉我们要沿着负梯度走,但真正动手算梯度时,麻烦来了:模型那么多层、那么多参数,每个参数的梯度怎么算?
最笨的办法是数值差分:为了算某个参数w的梯度,把w的值稍微改一点点(比如加0.0001),然后整条网络前向跑一遍,看loss变化多少。这个近似梯度能算,但代价极其昂贵——每个参数都要单独跑一次完整前向流程。
我们来算笔账。一个70B参数的模型,哪怕一次前向只要1秒,为了算出所有参数梯度,你需要做70B次前向,一年365天不停跑也得两千多年,还不算显存根本放不下。所以数值差分只适合调试,绝不用于训练。
反向传播的巧妙之处在于,它利用链式法则把梯度计算复杂度从“参数个数 × 前向成本”降到了“一次前向 + 一次后向”。核心观察是:loss对每个参数的偏导,可以拆成“loss对神经元输出的偏导 × 神经元输出对该参数的偏导”这种递推乘积。所以我们先做一次前向,把每层输出和loss算出来;然后从最后一层开始,把loss的梯度一层一层往回传,每一层只需要用到上一层传下来的值,不必重新跑前向。
这就是“反向”二字的含义:误差信息从输出层倒着流回输入层,像一列火车从终点站往回卸货。
3.2 手算示例:一个最小规模的网络
光讲理论容易飘,我来手算一个具体例子。假设一个最简单的网络,输入两个数 x1=1、x2=-2,真实目标 y=1。模型是一个线性变换加sigmoid激活:
z = w1 × x1 + w2 × x2 + b a = sigmoid(z)
初始权重随便设:w1=0.3、w2=-0.4、b=0.1。损失函数用简化版MSE:Loss = 0.5 × (a - y)²,0.5是为了求导抵消系数,老教材里很常见,实际框架里用普通MSE也差不多。
先做前向: z = 0.3×1 + (-0.4)×(-2) + 0.1 = 0.3 + 0.8 + 0.1 = 1.2 a = sigmoid(1.2) ≈ 0.7685 Loss = 0.5 × (0.7685 - 1)² ≈ 0.0268
现在反着传。第一步,算loss对a的偏导: ∂Loss/∂a = a - y = 0.7685 - 1 = -0.2315 这个值的意思是把a调大一点点,loss会降一点,所以梯度是负的。
第二步,算a对z的偏导,也就是sigmoid的导数: ∂a/∂z = a × (1 - a) ≈ 0.7685 × 0.2315 ≈ 0.1779 这里有个值得记住的点:sigmoid的导数等于自身乘以(1减自身),最大的地方在z=0附近,两边会迅速趋近0。这正是梯度消失问题的一个源头,后面会再提。
第三步,把两步乘起来,得到loss对z的偏导: ∂Loss/∂z = ∂Loss/∂a × ∂a/∂z ≈ -0.2315 × 0.1779 ≈ -0.0412
第四步,继续往回算loss对每个参数的偏导: ∂z/∂w1 = x1 = 1,所以 ∂Loss/∂w1 ≈ -0.0412 ∂z/∂w2 = x2 = -2,所以 ∂Loss/∂w2 ≈ 0.0824 ∂z/∂b = 1,所以 ∂Loss/∂b ≈ -0.0412
注意,∂Loss/∂w2是正的,意思是w2增大一点,loss反而会增大,所以要减小w2;∂Loss/∂w1是负的,增大w1会降低loss。方向正好相反。
最后,用学习率 lr=0.1 做一步更新: w1_new = 0.3 - 0.1×(-0.0412) ≈ 0.3041 w2_new = -0.4 - 0.1×0.0824 ≈ -0.4082 b_new = 0.1 - 0.1×(-0.0412) ≈ 0.1041
你可以自己重新算一遍新前向,预测值会比0.7685更接近1,这就是一个最小但完整的学习闭环。这个例子虽然小,但反向传播的所有关键动作都齐了:从输出误差开始,逐层往回乘导数,最后落到每个权重上。真实的大模型只是把它从两三个变量放大到上亿个变量,思路完全一样。
3.3 工程里的反向传播:计算图、自动微分和显存
你训练大模型时,当然不用手写这些偏导。PyTorch、TensorFlow这类框架会把网络构建成一张计算图,你定义前向计算时,框架自动记录每一步操作,等到调用backward(),它按计算图从输出往输入自动求梯度。这就是自动微分。
这里有一个容易被忽视的点:为什么训练时显存比推理时大那么多?因为推理只需要前向,每算完一层,上一层的中间结果可以扔掉;训练时反向传播需要用到前向时算出的每一层激活值,它们都被保留在显存里。你可以把前向当成记账,每一笔流水都留着,反向传播时对着账本逐笔回算梯度。所以一个70B模型推理可能只需要几百GB显存,训练时常常是几倍于此,很大一部分就是这些中间激活值。
对应有两个常用工程技巧。一是梯度累积:训练时如果batch太大显存炸了,可以拆成几个小batch,分别算梯度但不立刻更新,把梯度累加,攒够一个大batch的量再统一更新。它解决的问题是:既不想降低batch size怕影响收敛质量,又放不下大batch。二是混合精度:把前向和反向计算用BF16做,速度更快、显存减半,但模型主权重和优化器状态仍保留FP32。直接用FP16训练大模型容易因数值范围太窄出现梯度下溢,BF16牺牲一些精度却保留大动态范围,训练鲁棒性好得多。
3.4 大模型本质上是放大了的反向传播
你可能有个疑问:标题写的是大模型原理,那反向传播在GPT、Llama这些模型里,跟上面这个两变量小网络有区别吗?
结构上没区别,规模上天差地别。Transformer把一个token序列拆成向量,经过embedding层、多头注意力、前馈网络、LayerNorm,最后接一个softmax输出每个词的概率。反向传播时,梯度就是沿着这条路径倒着流:先从softmax输出的loss开始,回传到最后一个Linear层,再到LayerNorm、残差块、注意力权重矩阵,一路传到embedding层。
值得一说的是残差连接在反向传播里的作用。它的设计初衷就是为了让梯度顺畅传导:残差支路是一个恒等映射,梯度经过它时几乎不打折,所以即使网络堆了上百层Transformer块,梯度也能完好回传到最底层。相比之下,早期那种纯顺序堆叠的深网络,梯度传到底层时早就衰减成0了,根本训不动。
所以你会发现,现在所谓大模型的“深”,主要深在层的数量,而层的连接方式处处为反向传播着想。业界做分布式训练,比如ZeRO、流水并行、张量并行,本质也不是改反向传播算法,而是把同一套前向加反向的计算,按参数、按层、按数据切到多张卡上并行做,再把梯度求和汇总。从这个角度看,理解了反向传播和梯度下降,大模型训练的地基你已经踩实了一半。
4. 预训练与微调:反向传播的两种玩法
4.1 预训练:在文本上反复推倒重来
现在把视野拉回大模型的实际训练流程。第一步是预训练,目标是让模型学会世界知识和语言规律。语言模型的任务很简单:给定前面的token,预测下一个token是什么,用交叉熵度量预测准不准。
这个任务听起来朴素,但规模很吓人。以LLaMA原始论文为例,训练数据是1.4万亿个token。每次取一批文本出来,前向算出每批的loss,反向传播算出梯度,更新一次参数;重复这个过程几万步、几个月,消耗上万块GPU的算力。
预训练里的反向传播反复在做同一件事:把“看完上一个词之后,模型对下一个词的预测分布”和“真实下一个词”之间的差异,一层层传回参数里去。你在网上看到“模型突然涌现某种能力”的报道,背后其实是几万名开发者用同一套反向传播,在不同数据子集上不停纠偏,让模型内部表示越来越接近真实语言统计。
一个容易被忽视的技术点是:预训练时的上下文长度会直接影响反向传播的显存占用。上下文越长,中间激活值规模越大,梯度回传路径也越长。所以很多训练框架会在长上下文上做专门优化,本质上都是在给反向传播的“记账本”减负。
4.2 微调:参数更新的不同策略
预训练做完,模型有了通用能力,接下来要做下游任务微调,让它更适配特定场景。微调时反向传播依然在工作,但策略上有几个区别。
全量微调是指让所有层的参数都参与训练、都按梯度更新。这个方案效果最完整,但成本高:显存要放下全部参数、梯度和优化器状态,对几十B级别的模型来说,普通开发者很难承受。
于是出现了参数高效微调,最典型的就是LoRA。LoRA的思路很有意思:它不修改原来的权重矩阵W,而是在旁边加两个小矩阵A和B,形成低秩分解。训练时冻结原权重W,只更新A和B。前向变成 h = Wx + BAx,反向传播的梯度也只针对B和A计算。
有人会问:只训练两个小矩阵,为什么效果能逼近全量微调?因为研究者发现,大模型微调时的参数变化量天然是低秩的——真正有用的调整方向就那么几个,不需要在几百万维空间里乱改,用一个小矩阵去表达这个偏移就够了。打个比方,你要重新装修一个房间,不用把整栋楼的墙都拆了,在指定位置做轻改装就行。
我实际跑LoRA的经验是:秩r取8到16通常就够,学习率可以比全量微调调高一点,5e-4到1e-3都行,因为要动的新参数少,不容易直接把模型搞崩。但LoRA也不是万能,如果下游任务和原任务差距极大,比如从通用聊天改成极其专业的领域,秩太小可能会不够用,需要加大秩或者干脆全量微调。
4.3 微调时常见的两个实战问题
微调最容易翻车的两个问题,都和反向传播的环境变化有关。
第一个是灾难性遗忘。模型适应新任务时,反向传播会把所有层朝着适合新数据的方向推,结果旧能力被“洗掉”。比如你把一个中文通用模型专门微调成客服助手,训完发现它连原来的创意写作能力都退化了。解决办法之一是混着旧数据一起训练,让梯度方向不至于完全偏向新任务;也可以用LoRA,它只改变低秩偏移,对原始权重影响小,遗忘现象会轻很多。
第二个是显存不够。微调的显存压力比推理大得多,很多人卡在这一步。我常用的三板斧是:用LoRA减少参与反向传播的参数;用梯度累积增大训练batch而不增加显存;用BF16混合精度跑。如果这三招用完还炸,那就得考虑冻结更多底层embedding层,只让高层和新增模块参与训练,因为底层特征更新的需求更小。
5. 训练中翻车现场:常见的梯度问题与排查思路
5.1 学习率不当的翻车姿势
不夸张地说,训练大模型九成的翻车都出在学习率上。我总结几种典型姿势,你可以对照自己的loss曲线:
第一种,loss完全不降,或者刚开始降了一点点就停在原地。多半是学习率太小,参数几乎没在挪;也可能是模型初始化和数据加载有问题。建议先调大一个量级试试。
第二种,loss大幅震荡,像心电图一样跳来跳去。学习率偏大,参数每一步都迈得太大,在山谷两侧反复横跳。先把学习率降到原来的十分之一,看震荡是否收敛。
第三种,loss突然变NaN,所有数值归零或变成无穷。通常是学习率过大导致梯度爆炸,加上部分激活值溢出。除了降学习率,还可以开梯度裁剪,把梯度的范数限制在某个值以内,比如1.0,防止一步更新冲过头。
我以前自己跑过一个小实验,同样模型同样数据,学习率从1e-4提高到3e-3,loss从稳定下降到直接NaN,中间只隔了不到100步。那次以后,我养成了每个新实验都先跑几十步探路的习惯,再正式开长训练。
5.2 梯度消失与爆炸:深网络的生死劫
梯度消失和爆炸是反向传播最经典的敌人。梯度消失指梯度传到浅层时变得极小,参数几乎不更新;梯度爆炸相反,传到某层时数值疯狂放大,一下子把参数炸飞。
为什么会出现这两种情况?因为反向传播本质是链式法则的连乘。如果每层的导数小于1,比如sigmoid导数最大才0.25,乘个几十层之后,梯度就成了10的负几十次方,浅层参数彻底学不动;如果某些层权重的谱范数大于1,乘起来又会指数放大。
Transformer能在超深结构下存活,主要靠三件套:一是残差连接,让恒等路径上的梯度不衰减;二是LayerNorm,把每层数据尺度稳定住;三是激活函数选ReLU、GELU这类导数不容易像sigmoid那样饱和的款式。三件配合下来,反向传播的梯度流才算有了高速公路。
如果你训练的模型遇到梯度异常,我建议打开训练日志里的梯度范数字段。正常情况它应该在1附近或略高一点;如果持续小于0.01,说明梯度消失;如果不断增大到几百上千,就要梯度裁剪了。现代框架里这两类监控都有现成接口,跑大模型训练时务必不要省这一步。
5.3 过拟合与欠拟合:从训练和验证loss看问题
不是所有训练问题都出在梯度本身,但很多和梯度相关的现象都可以从loss曲线判断。我习惯同时盯训练loss和验证loss两条线:
如果训练loss一直降,验证loss降不下去甚至回升,那就是过拟合。模型把训练集的样本特征背下来了,验证集上露馅。解决办法包括加大数据量、用weight decay、dropout、早停。其中weight decay在AdamW里已被单独处理,这也是它在大模型圈受欢迎的原因之一。
如果验证loss和训练loss一起高居不下,那是欠拟合,模型对任务的表达力不够或者训练配置太保守。这时候应考虑增大模型容量、降低正则化强度、把学习率往上探一点。警惕一种假象:训练loss还没降到位就急着加正则项,结果两个loss同时拐弯,那是把“没学会”误判成了“学会但过拟合”。
5.4 我最常用的四步排查法
最后分享一套我在大模型训练中反复使用的排查流程,简单但救命。
第一步,拿到新数据或新模型配置,先跑一个小batch的单步前向加反向,确认loss不是NaN,梯度量级正常。这一步能筛掉很多配置错误,比如维度不对、类型不匹配。
第二步,小规模跑500步左右,画loss曲线。重点看四个特征:降不降、降得稳不稳、有没有周期性尖刺、最终值离预期多远。这四条能区分出大部分问题根源。
第三步,如果loss曲线不对,查梯度范数。梯度为0或NaN,多半是前向数值范围坏了;梯度长期不动,可能是学习率过低或者某层初始化太差。我见过最诡异的一次是某个线性层初始化为全0,梯度在它那里彻底断掉,后面所有层全部白训。
第四步,锁定问题后,先调整单一变量,不要同时动学习率、batch size、模型结构和数据。很多人一上来同时改三四个东西,最后出了问题根本不知道哪步引起的。我自己的习惯是,先把学习率和batch size钉死,再调架构和数据,每次只动一个,训练问题立刻好排查大半。
这套流程看着基础,但真遇到问题的时候,按部就班走一遍比乱试高效得多。我自己现在跑实验,基本都会把这四步当作固定动作嵌进训练脚本里,省下的都是踩坑的时间。