ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

反向传播与梯度下降:大模型训练的底层逻辑与实战指南

反向传播与梯度下降:大模型训练的底层逻辑与实战指南 1. 从大模型的“学习”说起为什么绕不开反传和梯度下降大模型这事儿火了这几年各种榜单、Demo、API天天刷屏但真要问一句“模型到底是怎么学会说话的”很多人反而卡住了。答案其实不玄乎大模型也好小模型也好底层驱动的核心就两个东西——反向传播Backpropagation和梯度下降Gradient Descent。这俩组合拳一个负责算出“参数该往哪个方向调”另一个负责“实际去调这一步”循环往复直到模型把损失函数压到足够低。这篇文章就是想把这两件事彻底讲透。我会从大模型这个巨型复合函数说起拆清楚反向传播里的链式法则到底在链什么再逐层讲透梯度下降家族SGD、动量、Adam、AdamW各自的脾气最后落到大模型训练和微调的实战现场梯度爆炸、Loss变成NaN、显存不够、该不该用梯度裁剪、LoRA为什么能省显存——这些坑我基本都踩过一轮写出来全是干货。适合谁看如果你是刚入门深度学习、想弄懂大模型训练原理的新手这篇能帮你把最关键的底层逻辑理顺如果你已经在跑模型了但偶尔被反传梯度搞迷糊或者调参全靠玄学那这篇也能帮你把“为什么这么调”补上。我不打算堆公式吓人但必要的推导会保留——因为不理解推导后面踩坑你都不知道坑在哪。2. 反向传播链式法则的工程艺术2.1 三步走前向、损失、反向反向传播不是什么高深魔法一句话总结就是利用链式法则从输出端开始逐层计算损失函数对每个参数的梯度。整个过程拆开就三步。第一步是前向传播。输入数据从输入层进经过每一层的线性变换矩阵乘法加偏置和非线性激活函数逐层往前传最后得到模型的预测输出。这就像你把一份原料投进一条流水线每个工位加工一下最后出来一个成品。第二步是计算损失。拿模型的预测输出和真实标签做对比算出一个数值这个数值就是损失。损失越大说明模型预测得越离谱。常见的损失函数包括均方误差MSE、交叉熵Cross Entropy等。对大模型来说用的基本都是交叉熵——因为它天然适合衡量“预测的概率分布和真实分布差多远”。第三步是反向传播。从损失这个“终点站”出发沿着刚才前向传播的路径往回走利用链式法则逐层求出每个权重和偏置对损失的梯度。这个梯度告诉你要让损失变小每个参数应该往哪个方向调、调多少。整个流程如下前向传播输入 → 各层计算 → 预测输出计算损失预测输出 vs 真实标签 → 损失值反向传播损失 → 逐层计算梯度 → 得到每个参数的偏导数也许你会有个疑问为什么要反着算不能正着算吗问题在于前向传播时每一层的输出依赖前面的层而损失函数是最终输出的函数。想求某个中间参数的偏导数就得把这个参数到损失之间所有路径上的导数乘起来。如果正着求每个参数都要从头到尾遍历一遍计算量是参数数量的好几倍而反着求每个中间结果只用算一次整条网络只需要一次前向一次反向就能拿到所有参数的梯度。大模型动辄几千亿参数要是没有反向传播这种“一次遍历搞定全局梯度”的算法训练成本会高出几个数量级。2.2 局部梯度模块化拆解的核心反向传播能高效的关键在于每个计算模块只需要关心两件事自己输出的梯度从哪来自己该怎么把梯度传回去。用行话说就是“局部梯度”。你可以把每个层想象成一个水管的接头。前向传播时水从前面流过来经过这个接头变个方向继续往前流反向传播时水流倒过来从后面传回来的“梯度信号”经过这个接头被换算成“对前面管道的压力反馈”继续往回传。每个接头只需要知道自己的“变向规则”完全不需要关心整条管路长什么样。这就是深度学习框架能够把各种层像积木一样拼起来的原因——每个模块只要能实现forward和backward两个方法就能无缝插入整个网络。PyTorch里的autograd做的就是这件事。你只管写forward逻辑框架会自动记录计算图backward的时候自动把梯度传到每个参数上。代码层面长这样import torch # 一个极简线性层 x torch.randn(64, 768, requires_gradTrue) # 输入 w torch.randn(768, 768, requires_gradTrue) # 权重 b torch.randn(1, 768, requires_gradTrue) # 偏置 # 前向 y x w b # 假设 loss 是 y 的均方误差此处省略目标值 loss y.mean() # 反向传播自动计算所有 requires_grad 参数的梯度 loss.backward() # 此时 w.grad / b.grad / x.grad 都被填上了梯度你注意到没有当w和b的shape几百万几千万时反传依然一次就能算完所有梯度。这种“模块化链式法则”的组合就是反向传播作为工程艺术的底气。2.3 手推一个最简单的例子光说不练假把式。我们来手动推一个最简单的情况单层线性网络 y wx b损失为 L (y - y_true)²。这里只有w和b两个参数。前向传播很简单代入即可。反向传播要算的是 ∂L/∂w 和 ∂L/∂b。链式法则拆两步先算 ∂L/∂y 2(y - y_true)再算 ∂y/∂w x∂y/∂b 1于是 ∂L/∂w ∂L/∂y · ∂y/∂w 2(y - y_true) · x同理 ∂L/∂b 2(y - y_true)你看整个过程中间变量y复用了一次所以反向传播比正向逐参数求导高效得多。到多层网络里每一层都重复类似操作只是中间变量变成了每层的激活输出。之前遇到过一个朋友把反向传播理解成“倒着跑一遍前向”其实不完全准确——倒着跑的是梯度表达式不是重新计算一遍输出。提示很多资料说“梯度反传”不要真以为是把数据倒过来再过一遍网络。反传的是“损失对各层输出的偏导数”是误差信号不是数据本身。这两个概念一旦混淆后面调试梯度的时候会非常痛苦。2.4 大模型里的反向传播更深的链条大模型的反向传播和上面这个小例子本质没区别只是链条更长更宽。比如一个百亿参数的Transformer前向就是embedding → 多层自注意力 → 前馈网络 → LayerNorm → 输出层反传就是沿着这条路一路把梯度从输出层传回embedding层。但是链条长了之后会出两个新问题一是梯度在传递过程中容易“越传越小”梯度消失或者“越传越大”梯度爆炸二是每一层都存了中间激活值显存开销极大。这也是为什么大模型训练几乎标配了混合精度减少显存、残差连接缓解梯度消失、LayerNorm稳定梯度分布这些技术。后面第4章我会专门讲这些坑。现在你可以这样理解反向传播本身是通用的“梯度计算引擎”但大模型这个引擎太大了必须搭配额外的减震设备和散热系统才能稳定跑起来。3. 梯度下降家族从SGD到AdamW3.1 最朴素的梯度下降学习率就是步长反向传播算出了梯度但“算出来”不等于“用得好”。梯度告诉你的只是方向——往哪个方向走损失函数下降最快。但走多远是学习率说了算。最朴素的梯度下降GD的更新规则是w_new w_old - lr · gradient这里lr就是学习率也叫步长。如果学习率设得太大参数更新一步跨太大可能直接越过最优点甚至在损失函数的陡峭区域来回震荡发散设得太小训练慢得让人怀疑人生。这个“步长”的把握是整个深度学习调参里最核心也最玄学的一部分。大模型训练里几乎不用全量梯度下降GD而用随机梯度下降SGD的变种。区别在于全量GD每次要拿整个数据集算一遍梯度在大模型场景下根本不可能——几千亿参数、几万亿token一次全量迭代可能跑几个月。实际做法是Mini-batch SGD每次随机抽一小批数据比如几百几千条样本算梯度用这个“抽样估计”的梯度去更新参数。这样做的好处是计算量可控一个batch的算力跟全量比几乎可以忽略引入了一定的随机噪声反而有助于跳出局部最优点配合学习率调度能在大规模数据上稳定收敛。但Mini-batch SGD一到大模型场景就露怯它对学习率太敏感而且在loss landscape比较“扭曲”的地方会来回震荡。于是就有了各种改良版。3.2 动量、自适应学习率为何大模型都用AdamW动量Momentum的思想很朴素如果当前梯度方向跟历史方向一致就大胆往前走如果方向总是变来变去就减小步伐。数学上就是维护一个“速度项”v让更新量带有之前几轮梯度的惯性v_new β · v_old gradientw_new w_old - lr · v_new这个β通常取0.9相当于把最近约10轮的梯度方向做了指数滑动平均。动量带来的效果立竿见影在峡谷状loss landscape里SGD会走“之字形”动量则能沿着峡谷方向平滑加速前进。再进一步自适应学习率方法如AdaGrad、RMSProp不再给所有参数同一个学习率而是根据每个参数的历史梯度大小自动调整它的学习率——梯度大的参数步子小一点梯度小的参数步子大一点。把动量和自适应学习率合体就有了2015年提出的Adam。Adam的更新公式稍微吓人但拆开看就是多维护了两个滑动平均一阶动量梯度的均值类似动量项和二阶动量梯度平方的均值代表梯度大小然后用二阶动量归一化学习率m_new β1 · m_old (1-β1) · gv_new β2 · v_old (1-β2) · g²w_new w_old - lr · m_new / (√v_new ε)Adam好用是真好用但它有个让研究者头疼的毛病它在训练后期容易和“权重衰减weight decay”打架。权重衰减是正则化手段目的是让参数别长太大抑制过拟合。经典实现里权重衰减是加在梯度里的Adam会把它跟梯度的历史信息混在一起当噪声平均掉导致正则化效果打折。AdamW就是把这个事掰扯清楚直接在参数更新时做解耦权重衰减不走优化器的梯度历史。就这一点区别让AdamW在预训练和微调大模型的时候收敛更稳、泛化更好。现在大模型圈子里提到优化器默认就是AdamW几乎没人再用普通Adam。优化器对比可以看这张表优化器核心机制大模型适用性典型场景SGD固定步长 可带动量较差收敛慢小模型、CV微调Adam自适应学习率 动量良好通用各类模型训练AdamWAdam 解耦权重衰减极佳标配LLM预训练、微调AdaFactor分块近似二阶动量省显存超大规模模型极少用了3.3 学习率调度warmup与余弦退火优化器选定了学习率怎么给又是另一个大坑。大模型训练目前最通用的方案是warmup 余弦退火。Warmup就是在训练最开始让学习率从一个很小的值线性或按步长慢慢涨到预设峰值。为什么不能上来就用大学习率因为模型刚开始权重是随机初始化的梯度方向噪声非常大一步迈太大容易把参数推到某个“烂区域”后面很难救回来。先小步走几百步让模型大致站稳了再加大步伐就能有效避开这个风险。这在视觉模型上是经验到LLM这里几乎成了铁律——你去看各大模型开源的训练配置几乎全有warmup。余弦退火是训练中后期的事学习率按余弦函数的形状慢慢降低最后趋近于0。这么做的好处是训练到后期参数已经接近最优区域用小学习率慢慢磨能更精细地找到谷底。这个“warmup再余弦下降”的组合用代码实现也不复杂from torch.optim.lr_scheduler import SequentialLR, LinearLR, CosineAnnealingLR total_steps 10000 warmup_steps 500 optimizer torch.optim.AdamW(model.parameters(), lr3e-4) scheduler SequentialLR( optimizer, schedulers[ LinearLR(optimizer, start_factor0.01, total_iterswarmup_steps), CosineAnnealingLR(optimizer, T_maxtotal_steps - warmup_steps) ], milestones[warmup_steps] )注意如果你是用Hugging Face的TrainerTrainer内置了get_scheduler方法可以直接选schedule_type为“warmup_cosine”。但无论如何warmup步数一般占总训练步数的1%~5%不要贪多。4. 大模型训练与微调实战梯度不稳定与显存的艺术4.1 梯度爆炸与消失为什么层数一深就难训反向传播链条一长梯度信号在逐层传递的过程中会不断被“缩放”。如果每层的缩放因子大于1多传几层梯度就会指数级增长这就是梯度爆炸如果每层的缩放因子小于1梯度就会指数级衰减这就是梯度消失。放到大模型场景里梯度爆炸最直观的后果就是Loss变成NaN或Inf——参数一步更新过大直接冲破了数值表示的边界。梯度消失更阴险看起来训练还在跑但前面的层参数几乎不动模型只能在最后几层“自娱自乐”整个训练白费。Transformer能有效缓解这个问题靠的是两样东西一是残差连接Residual Connection让梯度可以直接“抄近路”跳过某些层传回前面不经过中间的反复矩阵乘法缩放二是LayerNorm对每一层的激活值做归一化控制住信号尺度。这俩加上合理的初始化方法比如大模型常用的标准差初始化基本能让几十上百层的Transformer在训练初期保持梯度稳定。但注意这只是“稳定”不代表一定不会炸。实际训练中Loss突然变成NaN仍然很常见那一刻的心情我太懂了——对着一个跑了好几天的训练日志结果第2000步loss变成了nan真想砸电脑。别急第5章我专门写排查方法。4.2 混合精度训练与loss scaling大模型训练如果不做任何优化光是显存就不够看。一个70B模型参数本身用FP32存储就要280GB再加上梯度、优化器状态AdamW要存一阶动量和二阶动量和中间激活值单卡根本放不下。混合精度训练AMP是应对这个问题的核心技术之一。它的思路很直接前向和反向用FP16或BF16算但参数更新时用FP32。FP16的显存占用只有FP32的一半能显著降低显存压力而且半精度在支持Tensor Core的GPU上计算速度更快。但FP16有个天生缺陷可表示的数值范围比FP32小得多。梯度值如果太小会直接变成0什么也没传如果太大又会溢出变成Inf。于是有了loss scaling在反向传播前先把loss乘上一个缩放因子如1024或动态调整把梯度整体放大到FP16能表示的范围内等梯度算完再缩小回去。PyTorch的GradScaler就是干这个的from torch.cuda.amp import GradScaler, autocast scaler GradScaler() for batch in dataloader: optimizer.zero_grad() with autocast(): loss model(batch) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()大模型训练更推荐用BF16Brain Floating Point。BF16的指数位和FP32一样所以数值范围跟FP32基本一致不会出现FP16那种loss scaling的麻烦只是尾数精度低一些。现在新一代GPU对BF16支持都很好很多开源模型训练脚本默认就是BF16混合精度。4.3 梯度裁剪防止“一步跨太大”梯度裁剪Gradient Clipping是大模型训练的“安全气囊”。逻辑很简单如果梯度的范数超过了设定阈值就把它等比缩放到阈值以内防止单次更新过大。最常用的是全局范数裁剪PyTorch里一行代码torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)max_norm设多少我自己的习惯是预训练给1.0微调给0.5到1.0之间。之前试过给到5.0结果照样炸给到0.1又发现模型收敛明显变慢。这个值本质上控制的是“每一步信任梯度的程度”太保守会牺牲收敛速度太激进又失去了保护的意义。提示不要以为加了梯度裁剪就万事大吉。如果损失频繁出现NaN梯度裁剪救不回来——它只能限制梯度大小不能修复损坏的数值状态。那种情况多半是学习率太激进、数据里有异常样本或者是混合精度下FP16溢出了。4.4 显存与分布式ZeRO/DeepSpeed与LoRA大模型训练的另一个核心矛盾是显存。AdamW本身要存三份状态参数、一阶动量、二阶动量FP16下70B模型光优化器状态就是天文数字。于是有了各种“省显存”方案。DeepSpeed的ZeRO系列是把状态拆到多卡。ZeRO-1把优化器状态切分ZeRO-2把梯度切分ZeRO-3把参数也一起切分。这些方案配合梯度检查点Gradient Checkpointing用时间换空间不存中间激活而是反传时重新算能把千亿级模型的训练塞进一个集群。但说实话对于大多数刚接触大模型的人来说先别一上来就搞多卡分布式——单卡能跑的模型也能学到很多。这个前提是你选择微调方案时别太贪。如果只是想对现有大模型做适配LoRALow-Rank Adaptation几乎是首选方案。LoRA的核心洞察是微调时不用动全部参数只学一个低秩增量矩阵比如rank8或16训练时冻结原模型权重只更新那个小矩阵。这样可训练参数量可能只有总参数的0.1%左右显存占用大幅下降普通消费级显卡也能跑。LoRA和反向传播的关系很像“只对最后一段电路做反向传播”——因为冻结的权重不需要梯度梯度只需要流经低秩矩阵。这相当于自动砍掉了一大批参数的梯度计算训练速度和显存占用双重受益。5. 常见问题与排查技巧实录5.1 Loss变成NaN怎么办NaN是训练里最让人头大的问题现象是损失突然变成非数值然后一路“灾难性遗忘”。排查顺序我建议照这个来先看学习率换小一倍试试如果解决说明之前lr太激进。看输入数据检查有没有NaN/Inf有没有异常大或小的值特别是fp16下数值溢出高发。看梯度范数如果NaN之前梯度范数已经异常大就是梯度爆炸调整初始化或加梯度裁剪。看混合精度如果用了FP16尝试关掉或换BF16。再看权重初始化有些初始化方案在深层网络特别容易爆换用更保守的方案。曾经有一次我们训练一个多模态模型Loss稳定在1.5左右很正常结果一换数据增强方式第二天早上起来看日志第600步loss成了NaN。查了一个多小时才发现是数据管线里有个归一化步骤对某些新增强方式产生了0值前向传播里变成了log(0)。这种坑真的只有靠系统排查才找得到。5.2 模型不收敛/收敛慢怎么排查Loss一直在降但降得极慢或者Loss根本不降。常见原因有这么几类学习率太小或太大固定步数内观察loss曲线是震荡不止可能偏大还是走不动可能偏小。数据加载有问题训练样本顺序有模式性、类别极度不均衡、或者标签有噪声。模型里某些层初始化不当比如embedding层初始化方差太大。优化器状态异常用了AdamW但weight_decay设得太大比如0.1以上模型还没学好就被“瘦身”了。一开始别急着用花哨技巧先在几百步内做小规模实验把learning rate和batch size这对组合试出来往往就解决了大半问题。5.3 排查经验速查表现象大概率原因首选排查动作Loss突变为NaN学习率过高、梯度爆炸、fp16溢出降低lr开amp的dynamic loss scalingLoss不降数据预处理bug、模型初始化不当先用少量数据跑通一个过拟合测试训练慢得离谱batch太小、混合精度没开、显存不足导致swap开AMP增大batch检查GPU利用率微调效果差学习率太大破坏原模型、LoRA rank太小lr调低rank增大梯度范数NaN反传数值溢出加梯度裁剪并缩小lr5.4 实操心得小抄最后分享几条我自己实践下来最值得记住的经验。第一条每次改配置前先跑一个极小规模的sanity check。用一个batch的数据看loss能不能过拟合到非常低。如果连一个batch都过拟合不了那一定是代码逻辑有bug不是模型/超参问题。第二条保存checkpoint别偷懒。不光要存模型权重优化器状态、学习率调度器状态、当前epoch、随机种子全都要存。训练到一半机器出问题或者你想回退到某个效果更好的checkpoint缺一个都让你哭。第三条学习率是超参里的“总闸”。当我面对任何大模型微调问题第一反应永远是先检查lr。预训练大模型微调用全参数一般3e-5到1e-5就够用LoRA可以放宽到1e-4到3e-4。不要照搬小模型的经验跑到大模型上。第四条看loss曲线要分段看。训练初期loss快速下降是正常的中后期锯齿状波动也正常但阶段性的平均趋势必须稳定向上往下走。如果训练很久loss却“平”了多半是模型卡在了某个平台期此时不是硬熬而是需要改变学习率或数据分布。第五条梯度检查是调试模型的好工具。PyTorch里可以开torch.autograd.detect_anomaly()来定位是哪里产生NaN梯度。虽然会拖慢训练速度但在排查阶段真的能帮你节省一整天的命。遇到梯度问题上这招直接真相大白。我个人在实际操作中的体会是反向传播和梯度下降这两个概念看着是几十年前的老古董但大模型时代它们不仅没有被淘汰反而因为规模化把每一个细节问题都放大了无数倍。很多人觉得大模型训练靠的是“大力出奇迹”堆数据堆算力就够了。但真到了训练不稳定、loss不收敛、显存爆掉的时刻能救你的往往不是花哨新理论而是你对反向传播和梯度下降那些“基础概念”是否真吃透了。理解了梯度从哪来、往哪去、为什么会爆为什么会消失你在调参时才不会像个无头苍蝇。最后再送一个实用建议别怕手推公式。拿个三层小网络把反向传播自己推一遍你以后看任何大模型训练相关的论文都会感觉轻松得多。
返回列表