ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型训练原理:反向传播与梯度下降的实战指南

大模型训练原理:反向传播与梯度下降的实战指南 你是不是也有这种感觉聊大模型的时候别人张口就是Loss曲线、梯度裁剪、LoRA微调一套一套的听起来很专业但问到最底层——模型到底是怎么一点点变聪明的很多人就卡在“反向传播”和“梯度下降”这两个词上。说白了大模型再大本质也就是一个参数很多很多的函数。训练过程里梯度下降负责回答“往哪个方向调整能让错误变小”反向传播负责回答“每一个参数对最终错误有多大贡献”。一个管方向一个管算账配合起来模型才能在几万亿次迭代里真正学到东西。这篇文章不搞那种贴满公式、看得人头皮发麻的推导。我会先讲清楚概念再给一个完整的手算示例最后说说我在预训练和微调实战里踩过的坑包括学习率怎么选、梯度消失和爆炸怎么排查。适合两类人看一是刚入门大模型、想搞懂基础原理的二是已经在跑训练但总被各种玄学问题折磨的。1. 先弄清大模型是怎么“学会”的训练到底在做什么1.1 模型的本质是一堆参数训练就是调参数先说一个反常识的事实大模型不是像数据库一样存了一堆语料。训练完成之后你问它“11等于几”它不是去查“112”这条记录而是把它见过的大量文本的统计规律压缩进了几百上千亿个小数里。这些小数就是模型的参数也叫权重。你打开任何一个开源模型的权重文件比如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之间其他保持beta10.9、beta20.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 手算示例一个最小规模的网络光讲理论容易飘我来手算一个具体例子。假设一个最简单的网络输入两个数 x11、x2-2真实目标 y1。模型是一个线性变换加sigmoid激活z w1 × x1 w2 × x2 b a sigmoid(z)初始权重随便设w10.3、w2-0.4、b0.1。损失函数用简化版MSELoss 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减自身)最大的地方在z0附近两边会迅速趋近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。方向正好相反。最后用学习率 lr0.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-3loss从稳定下降到直接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钉死再调架构和数据每次只动一个训练问题立刻好排查大半。这套流程看着基础但真遇到问题的时候按部就班走一遍比乱试高效得多。我自己现在跑实验基本都会把这四步当作固定动作嵌进训练脚本里省下的都是踩坑的时间。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进