资讯详情

从数值微分到梯度下降:彻底搞懂反向传播与自动求导

📅 2026/9/9 15:20:49 | 华诺云谱 👁 阅读
从数值微分到梯度下降:彻底搞懂反向传播与自动求导
在深度学习这个圈子里待久了你会发现一个挺有意思的现象很多人用着 PyTorch、TensorFlow对loss.backward()倒背如流但你要是突然问他“梯度到底是怎么算出来的”他大概率会愣一下然后告诉你“框架自动求导呗”。自动求导确实强大它把我们从手动推导偏导数的泥潭里彻底解放了出来。但这也带来了一个问题当模型训练出现 NaN、梯度爆炸或者 loss 死活不下降时很多人只会两眼一抹黑地调学习率却不知道问题可能出在反向传播的源头。今天这篇文章不聊花哨的网络结构就回到一切的原点把“数值微分”和“梯度下降”这对基石彻底掰开揉碎。我会从最朴素的导数定义讲起告诉你数值微分到底怎么算、精度有多差它和真正在用的反向传播有什么关系又是怎么一步步演变成我们现在天天用的梯度下降算法的。看完这篇文章你不仅能彻底搞懂backward()背后发生的事还能学会一个被很多人忽略的实用技巧用数值微分来检查你手写的反向传播代码对不对。这篇文章适合刚刚学完神经网络基础、想要深入理解底层原理的初学者也适合那些用了很久框架但对内部机制一知半解的从业者。1. 从导数定义出发数值微分的本质与实现1.1 导数的数学定义与几何直觉我们先回到高等数学的课堂。函数f(x)在点x处的导数定义是这个极限f(x) lim(ε→0) [f(xε) - f(x)] / ε这个式子的几何意义再直观不过它表示的是函数曲线在某一点处的切线斜率。斜率越大说明函数在该点变化越剧烈斜率为正说明函数在增大斜率为负说明在减小。这就像你骑自行车爬坡坡度越陡你蹬得越费劲对应到函数上就是导数的绝对值越大。现在问题来了在实际写代码的时候我们面对的往往不是教科书上那些可以手推解析解的平滑函数而是动辄上亿参数的神经网络。要求出f(x)我们不可能真的让 ε 趋向于 0因为计算机里的浮点数是有精度极限的。于是我们只能用足够小的 ε 来近似逼近这个极限这种用有限步长近似计算导数的方法就叫做“数值微分”。数值微分最朴素的实现方式就是我们高中课本里见过的“割线斜率法”def numerical_diff(f, x, eps1e-4): return (f(x eps) - f(x)) / eps就这么三行代码一个最简单的数值微分就完成了。你把任意一个函数f和点x丢进去它就能告诉你这个点的近似导数。比如f(x) x²在x 3处它的解析解是2x 6。用上面这个函数算eps取1e-4得到的结果是6.000100000012066和真实值误差在1e-4量级。看起来还不错对吧但这个版本存在一个明显的数值缺陷后面我会专门讲。1.2 前向差分、中心差分与截断误差我刚才写的numerical_diff在数值分析里有个专门的术语叫“前向差分”forward difference。它的问题在于当你缩小 ε 时截断误差会变小但舍入误差会因为两个相近浮点数相减而急剧放大。这就陷入了一个两难ε 太大函数的高阶项带来的截断误差占主导ε 太小浮点数精度带来的舍入误差占主导。一个真正懂数值计算的人不会用前向差分因为存在一个精度对称的更好选择——中心差分central differencef(x) ≈ [f(x ε) - f(x - ε)] / (2ε)还是以f(x) x²在x 3处为例用中心差分计算结果是6.000000000039306误差直接降到了1e-11量级。为什么精度提升这么多因为泰勒展开式里前向差分的误差项是O(ε)而中心差分的误差项是O(ε²)。中心差分通过对称地取x ε和x - ε两点恰好把泰勒展开中的偶数阶项消掉了。教科书上没有骗我们中心差分确实比前向差分精度高出好几个数量级。理论上 ε 越小越好但实际并非如此。当 ε 小到1e-12以下时浮点数的舍入误差会开始起主导作用。因为x ε和x - ε这样两个在数值上非常接近的大数相减会丢失大量的有效数字这就是灾难性的“大数吃小数”问题。经过无数次实验我自己的经验是中心差分的 ε 取1e-4到1e-6是最稳的区间既能有效压制截断误差又不会触底浮点精度的天花板。这也是 PyTorch 官方梯度检查接口torch.autograd.gradcheck内部使用的步长范围。2. 从数值微分到解析求导为什么训练时不能用数值微分2.1 计算量和精度数值微分的两大硬伤可能你已经敏锐地意识到了既然数值微分这么简单几行代码就搞定了那为什么深度学习框架里没有一个用它来训练模型这里有两个致命伤。第一是精度问题。数值微分永远只是一个近似值不管你步长选得多精妙它和真正的解析导数之间始终存在一个微小的差距。反向传播计算的是解析公式推导出的真实梯度而数值微分算的是带误差的近似梯度。用近似梯度去更新参数就好比蒙着眼睛爬山每一步的方向都不是正对着山顶方向稍微偏一点整个训练过程就会变得极不稳定甚至无法收敛。第二是计算量的问题这才是真正让人望而却步的。假设你的神经网络有N个参数你用数值微分求梯度需要对每个参数θᵢ做两次前向传播f(θ ε)和f(θ - ε)。那算出全部参数的梯度总共要执行2N次完整的前向传播。一个像样的 ResNet-50 有 2500 万参数也就是要跑 5000 万次前向传播才能完成一次参数更新。这还是在没算反向传播的情况下就这个计算量用世界上最快的超算来跑也够喝一壶的。相比之下反向传播算法只需要一次前向传播加一次反向传播就能拿到所有参数的梯度计算效率是O(N)级别的每轮迭代的总耗时从 5000 万次前向变成了仅仅 2 次传播。2.2 反向传播链式法则与计算图的完美配合既然数值微分不中用那深度学习到底是怎么解决梯度计算问题的答案是反向传播本质就是高等数学里的链式法则配合上计算图这种数据结构。我们来回忆一下链式法则如果y f(u)u g(x)那么dy/dx (dy/du) * (du/dx)。神经网络就是这么一层层嵌套起来的复合函数输入经过第一层线性变换加激活函数输出作为第二层的输入如此层层堆叠最后得到一个损失值L。前向传播时计算图会按顺序记录每一步的中间结果。反向传播时我们从损失值L开始从图的最后往前逆向走利用链式法则逐步求出每一层参数的梯度。这个过程极其高效因为dy/du这种中间导数在反向传播过程中会被复用来计算更前面的梯度不会重复计算。这里要强调一个深度学习的本质认知反向传播不是一种新的数学它只是链式法则在计算图上的一种高效编排方式。很多初学者听到“反向传播”这个名字会以为是什么天外飞仙般的算法其实它骨子里还是我们大学一年级学过的微积分。理解了这一层你就再也不会对loss.backward()感到神秘了。2.3 数值微分在现代深度学习中的真正用途梯度检查Gradient Check看到这里你可能要问了既然数值微分既慢又不准那它是不是就该被扫进历史的垃圾堆了恰恰相反它在现代深度学习工程中仍然扮演着一个极其关键的守门员角色这就是梯度检查Gradient Check。什么场景需要梯度检查当你不满足于框架自带的操作自己手写了一个自定义的网络层、自定义的损失函数或者对某个算子实现了自定义的反向传播时你怎么验证你写的反向传播公式是对的你不可能靠肉眼盯着公式看这时候数值微分就是唯一可靠的裁判用数值微分算出某一层的近似梯度和你的自定义反向传播算出的梯度做对比。如果两者在给定的容差范围内一致说明你的反向传播实现是科学的如果对不上那你的代码里必定有 bug。我举一个我自己踩过的真实案例。有一次我实现了一个带 mask 的 Softmax 层前向传播时对某些位置的元素做了遮蔽处理但在写反向传播时忘记对梯度也施加同样的 mask导致被遮蔽位置的梯度没有归零。这种错误用肉眼根本看不出来loss 也在下降只不过下降得比预期慢很多。后来我用数值微分对整个就低层做了梯度检查发现梯度不一致的坐标正好全落在 masked 位置上问题当场就定位了。从那时起每次我写任何自定义层都会顺手跑一遍梯度检查这已经成了我写代码的一条铁律。PyTorch 里做梯度检查非常简单import torch from torch.autograd import gradcheck # 定义一个自定义的线性层 class CustomLinear(torch.autograd.Function): staticmethod def forward(ctx, x, weight, bias): ctx.save_for_backward(x, weight, bias) return x weight.T bias staticmethod def backward(ctx, grad_output): x, weight, bias ctx.saved_tensors grad_x grad_output weight grad_weight grad_output.T x grad_bias grad_output.sum(0) return grad_x, grad_weight, grad_bias # 输入必须是双精度并且 requires_gradTrue x torch.randn(3, 5, dtypetorch.float64, requires_gradTrue) weight torch.randn(4, 5, dtypetorch.float64, requires_gradTrue) bias torch.randn(4, dtypetorch.float64, requires_gradTrue) # 梯度检查标志True 说明反向传播实现没问题 print(gradcheck(CustomLinear.apply, (x, weight, bias), eps1e-6, atol1e-4))这里有几个实操细节值得说一下。第一gradcheck的输入必须是float64双精度类型因为数值微分本身有精度天花板用float32做梯度检查很容易因为舍入误差而误报错误。第二不同框架的容差默认值不一样PyTorch 一般用atol1e-4如果你写的算子数值范围特别大或特别小需要自行调整这个值。第三如果你的自定义层包含随机性比如 Dropout、随机采样梯度检查前务必把随机种子固定下来否则每次前向传播算出来的函数值都不同数值微分和反向传播对不上是很正常的。3. 梯度下降沿着最陡方向寻找山谷最低点3.1 梯度是方向导数最大的方向理解了梯度怎么算接下来就要解决怎么用的问题。在神经网络训练中我们的目标非常明确找到一组模型参数θ使得损失函数L(θ)的值尽可能小。但参数空间是高维的小则几千大则上亿我们不可能像高中求一元二次函数最小值那样直接令导数为零解方程。梯度下降算法的思路非常朴素既然函数在某个点沿梯度方向增长最快那沿梯度的反方向自然会下降最快。这个说法用数学来表达就是梯度向量的方向是方向导数最大的方向这就是“梯度”这个名字的来历。所以算法只干一件事在当前参数点上求出梯度然后沿着梯度的反方向迈出一步重复这个过程直到收敛。如果用把损失函数想象成一片连绵起伏的山脉你的参数是你在山脉上的坐标训练过程就是你在一片浓雾、看不清全局的山脉中寻找最低的盆地。你唯一能感知到的信息是脚底下的坡度于是你每一步都朝着最陡的下坡方向走。梯度下降不能保证你找到的是全球最低点它只能保证你找到的一定是某个方向的局部最低点。不过在深度学习中我们发现高维空间的局部极小值和全局最优之间的差距并没有想象中那么大这是另一个话题了这里先按下不表。3.2 学习率步长的艺术与生命线梯度下降更新参数的最基本公式只有一行theta theta - learning_rate * gradient这里的learning_rate学习率就是那只决定每一步迈多大步子的脚。这个超参数可以说是整个深度学习里最重要、最敏感的一个。学习率太大你每次跨的步子太大可能在最低点附近反复横跳甚至直接越过了山谷跳到了山脊另一侧导致 loss 不仅不下降反而飙升训练发散学习率太小每一步只挪动一丁点训练速度慢如蜗牛而且容易深陷在局部极小值里出不来。下面这组数据是我在一张简化损失函数上实测的对比学习率训练初期表现最终结果0.01loss 快速下降收敛速度极快在最小值附近来回震荡难以精确收敛0.001loss 稳定下降方向清晰顺利收敛逼近最小值0.0001loss 下降缓慢训练过程稳定收敛速度慢但最终能达到不错的结果0.1loss 剧烈震荡数值持续飙升完全发散几乎无法恢复这张表只想说明一个道理学习率是最需要“人肉调参”的超参数。深度学习框架永远不会替你决定用多大的学习率它把这当作你的责任。这里有一个非常实用的建议尤其对初学者在你没有把握的时候先从1e-3这种相对保守的值开始观察 loss 曲线的形状再决定调整方向。如果 loss 在稳步下降就说明当前学习率是合理的甚至可以酌情调大一点加速收敛如果 loss 疯涨立刻把学习率往小了调一般就是除以 10如果 loss 下降特别平稳但速度慢可能学习率偏小可以尝试乘以 2 或 3。训练模型的过程本质上是和这条曲线对话的过程学会了读曲线的语气你就算入门了。3.3 从批量梯度下降到小批量随机梯度下降梯度下降并不是只有一种形态。最早最原始的版本叫“批量梯度下降”Batch Gradient Descent它每更新一次参数就要在整个训练集上计算一遍梯度。这样做的优点是梯度方向非常准确缺点是计算量巨大而且放不进内存。训练集有 100 万张图片你每更新一步就要跑完这 100 万张图一轮迭代慢到令人绝望。于是出现了“随机梯度下降”Stochastic Gradient Descent, SGD它每取一个样本就立刻更新一次参数。这样参数更新频率高收敛速度快而且因为每次梯度都有随机噪声反而能帮模型跳出一些浅的局部极小值。但它的问题是梯度方向极其不稳定loss 曲线会像心电图一样剧烈跳动也增加了收敛到最优点后的震荡。最后大家做了一笔折中这就是如今最主流、几乎所有深度学习框架默认配置的版本“小批量随机梯度下降”Mini-batch SGD。它每次随机取一小批样本比如 32、64、128 张图在这批样本上计算一个平均梯度然后做一次参数更新。这一下子兼顾了两边的优点比全批量快得多比纯随机稳定得多。我见过太多人忽略“小批”两个字以为 SGD 就是随机梯度下降其实在现在的语境里说 SGD 基本默认就是指 mini-batch 版本的。这里还有一个小细节值得展开讲一下就是 batch size批量大小的选择。它不只是影响速度还会影响收敛质量。我个人的经验是batch size 太大会导致内存占用飙升而且可能收敛到尖锐的极小值泛化能力变差batch size 太小则梯度噪声太大收敛不稳定。一般来说32到128是一个比较普适的区间如果你的显存足够可以适当调大。不过近些年的研究也表明batch size 和泛化能力之间的耦合关系比大家想象中更复杂没有绝对的最优值还是要结合具体任务来实验。4. 从朴素 SGD 到现代优化器收敛稳定的工程艺术4.1 动量给梯度加上惯性朴素的 SGD 在面对复杂损失面时有一个痛点如果损失函数在某个方向的曲率远大于另一个方向梯度下降就会在这条陡峭的方向上来回震荡而沿平坦方向推进得极慢。一个形象的比喻是你把一个小球从沟壑纵横的山坡上滚下去小球在沟的横截面方向会来回弹跳几乎不往前走但如果你是让一个保龄球滚下去它会借助惯性碾过那些沟壑沿着整体的下坡方向一路冲下去。动量Momentum就是给梯度下降加惯性。它这样更新参数v momentum * v - learning_rate * gradient theta theta v这里v是累积的“速度”向量momentum一般取0.9左右。可以看到每一步的更新方向不再只是当前梯度还叠加了之前所有梯度的指数衰减累积。这样做最直接的好处是某一方向的梯度如果一直保持同一方向速度向量会让它越冲越快如果这个方向的梯度方向一直在变化比如在沟壑里来回震荡累积效果就会互相抵消从而自动抑制震荡。这真的是一个非常优雅的设计。我第一次把 SGD 换成带动量的 SGD 时明显感觉到训练 loss 曲线的毛刺少了很多收敛也变快了。当时的感觉是原来训练模型可以这么“顺滑”。4.2 自适应学习率Adam 和它的兄弟们动量解决的是“方向”的问题还有一个问题没有解决那就是“步长”在不同参数上应该是不同的。在大型网络里不同参数所处的损失面陡峭程度差异极大。有些参数在平坦区域梯度很小需要用大学习率才能推得动有些参数在陡峭区域梯度很大再用同样大的学习率就会震荡。于是自适应学习率的优化器应运而生其中的代表作就是 AdamAdaptive Moment Estimation。Adam 的核心思想是为每个参数维护一个基于梯度历史的“学习率缩放因子”。梯度大的参数给它配一个较小的有效学习率梯度小的参数给它配一个较大的有效学习率。这样就把参数更新的尺度统一到了一个更健康的范围。我自己的使用习惯一直很明确默认选择就是 Adam学习率设1e-3。它在大部分任务上表现都足够好基本不用怎么调参。而 SGD Momentum 需要更细致的学习率调节如果调好了在图像分类这类任务上的最终精度往往能比 Adam 更好一点。如果你想深入理解 Adam 和 SGD 在泛化能力上的差异可以从“尖锐最小值 vs 平坦最小值”这个角度去查一些资料这也是近几年优化器研究非常热门的方向。4.3 学习率调度先快后慢的艺术在深度学习训练中我们还需要让学习率在训练过程中动态变化。一个常见的策略是训练初期使用较大的学习率让参数快速接近一个较好的区域训练后期逐渐减小学习率让参数在局部最小值附近精细地“打磨”避免因为步长太大而跳过最优解。常用的学习率调度策略有Step Decay固定步长衰减每训练 N 个 epoch学习率乘以一个小于 1 的因子比如每 30 个 epoch 乘以 0.1。Cosine Annealing余弦退火学习率按照余弦函数从初始值退火到接近 0 的值这种策略在众多现代 Transformer 模型中被广泛使用。Warmup预热训练最初几千步学习率从 0 线性增长到预设值。这在大模型训练中几乎是标配因为训练初期参数都是随机初始化的前向传播的 loss 可能非常大梯度也很大如果直接上一个大学习率很容易一脚踩空把参数踢飞到无法恢复的境地。在 PyTorch 里现成的调度器可以直接用import torch.optim as optim optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max100, eta_min1e-6) for epoch in range(100): train_one_epoch(model, dataloader, optimizer) scheduler.step()4.4 训练不收敛怎么办我的排查步骤无论你是用最基础还是最复杂的优化器训练失败都是家常便饭。这些年我被各种妖魔鬼怪的 loss 曲线折磨过太多次逐渐总结出一套自己的排查流程分享给大家作为参考。首先看一眼 loss 是不是从一开始就在发呆完全不动。如果是大概率是学习率太小或者梯度传递在某一层断掉了比如自己写的自定义层反向传播出了问题。这时候我会把学习率调大三个数量级试跑几十步如果 loss 有明显的下降迹象就说明模型本身是通的再回头调回正常学习率。其次如果 loss 直接变成了 NaN这是最让人头大的。第一反应该去查数据有没有 NaN 混进输入标签有没有问题然后查模型结构有没有除以 0 的操作、有没有经过不稳定的指数函数导致溢出如果数据没问题那就要考虑是不是学习率太大导致梯度爆炸。排查的步骤就是先把学习率降到1e-6或者更低看能不能恢复正常数值如果能就说明是学习率的问题配合梯度裁剪来做。最后如果在训练初期一切正常但到训练后期 loss 开始来回震荡不再下降大概率是学习率太大了这时候需要让学习率调度器介入用一个更小的学习率继续打磨。这些排查方法不是我凭空想出来的是在一次又一次的通宵调试中用头发换来的教训。5. 数值微分的代码实战从零实现一个梯度检查工具为了让你真正掌握数值微分我打算带着你手写一个完全独立的梯度检查小工具。这个工具不依赖任何深度学习框架可以让你直观地看到数值微分、解析梯度和反向传播之间的关系说不定未来你还会在面试中遇到这道题。5.1 实战用 Python 手写梯度检查的完整过程首先我们定义一个非常简单的两层神经网络完全用 NumPy 实现不使用任何自动求导框架import numpy as np class SimpleNet: def __init__(self, input_dim, hidden_dim, output_dim): self.W1 np.random.randn(input_dim, hidden_dim) * 0.01 self.b1 np.zeros(hidden_dim) self.W2 np.random.randn(hidden_dim, output_dim) * 0.01 self.b2 np.zeros(output_dim) def forward(self, x): self.z1 x self.W1 self.b1 self.a1 np.maximum(0, self.z1) # ReLU self.z2 self.a1 self.W2 self.b2 return self.z2 def loss(self, x, y_true): y_pred self.forward(x) m y_true.shape[0] return (1 / (2 * m)) * np.sum((y_pred - y_true) ** 2) def backward(self, x, y_true): m y_true.shape[0] y_pred self.forward(x) dz2 (y_pred - y_true) / m dW2 self.a1.T dz2 db2 np.sum(dz2, axis0) da1 dz2 self.W2.T dz1 da1 * (self.z1 0) # ReLU 的导数 dW1 x.T dz1 db1 np.sum(dz1, axis0) return dW1, db1, dW2, db2这个网络的 backward 是我们手工推导的解析梯度。现在问题来了我们怎么能确定这个 backward 推得没错用数值微分来验证。方法非常简单就是对网络中每一个参数分别做一次微小的扰动记录 loss 的变化来近似该参数的梯度def numerical_gradient(net, x, y_true, eps1e-5): grads {} # 对 W1 的每个元素做数值梯度 W1_copy net.W1.copy() grad_W1 np.zeros_like(net.W1) for i in range(net.W1.shape[0]): for j in range(net.W1.shape[1]): # 中心差分 net.W1[i, j] W1_copy[i, j] eps loss_plus net.loss(x, y_true) net.W1[i, j] W1_copy[i, j] - eps loss_minus net.loss(x, y_true) grad_W1[i, j] (loss_plus - loss_minus) / (2 * eps) net.W1[i, j] W1_copy[i, j] # 对 b1、W2、b2 同理此处略去重复代码 grads[W1] grad_W1 return grads这个双重 for 循环跑起来会非常慢但这正是为了让你直观地体会到数值微分的计算开销仅仅是为了验证一个小网络的梯度就要做几百上千次前向传播如果用在真实的大模型上是绝对不可能接受的。所以它目前只适合在小规模、小模型上做一次性的正确性校验。最后我们验证一下数值梯度和反向传播梯度的误差np.random.seed(42) net SimpleNet(2, 3, 1) x np.random.randn(4, 2) y_true np.random.randn(4, 1) # 解析梯度 analytical_grads net.backward(x, y_true) # 数值梯度 numerical_grads numerical_gradient(net, x, y_true) # 计算相对误差 diff np.abs(analytical_grads[0] - numerical_grads[W1]) denom np.maximum(1e-8, np.abs(analytical_grads[0]) np.abs(numerical_grads[W1])) rel_error np.max(diff / denom) print(f相对误差: {rel_error:.2e})如果相对误差在1e-6这个量级或更小就说明我们的反向传播实现是科学的。一旦超过1e-3就说明 backward 有 bug需要逐层检查了。这里就是数值微分在现代深度学习中最黄金的价值它不是用来训练的而是用来验证的。5.2 梯度检查中的常见错误与避坑心得梯度检查这套方法论实操起来有不少坑以下是我总结的经验第一必须用双精度float64不要用float32。数值微分本身会损失精度单精度下的舍入误差足以让梯度检查产生误判。如果你发现两个梯度在1e-4量级附近波动但始终无法更小先检查一下精度是不是单精度。第二像 ReLU 这种在零点不可导的激活函数在梯度检查时极易出现问题。因为数值微分在ε这个小范围内穿越了“拐点”计算出的近似导数会和解析导数不一致。解决办法很简单数据预处理时手动调整样本尽量避免输入值落在零点附近或者用 Softplus 代替 ReLU 做插桩测试。第三如果你用的网络里包含 Dropout、Batch Normalization 这类行为依赖训练模式的层梯度检查一定把模型切到eval()模式。否则前向传播的随机性和数据分布变化会让对不上的。第四检查通过不代表万事大吉。我记得有一次我兴高采烈地通过了梯度检查、全套代码都跑通了后来换了网络结构才发现在一个特定条件下比如input_dim hidden_dim才会出现的 bug。所以建议你不只在一个输入尺寸上做检查多换几组不同的输入形状、不同数量的样本把不同路径都测一遍。你的验证覆盖越全面对梯度的信心就越足。6. 浮点精度与数值稳定性工程中的隐形敌人6.1 fp32、fp16 与混合精度到底在解决什么问题在实际模型训练和部署中精度问题不只是梯度检查时的障碍更是日常工程调试中绕不开的拦路虎。简单介绍几个常见的浮点格式fp32单精度32 位是深度学习的标准精度它提供大约 7 位有效十进制数字fp16半精度16 位能以一半的存储量进行更快的计算但有效数字只有约 3 位且表示范围非常窄bf16是另一类 16 位格式牺牲了有效数字来换取与fp32相同的指数范围主要用在需要大数值范围但不太在意尾数精度的场景tf32则是 NVIDIA Ampere 架构上的一种特殊格式专为加速训练中的矩阵乘法而设计它没有改变存储格式而是在计算时截断了输入精度。用生活中零钱来打比方fp32就像用支票记账精确到分fp16就像只用元为单位记账角分全舍掉几笔账目一多误差就大了bf16就像大整数记账数值能记到很大但小额变化全被抹平。在深度学习里你必须清楚什么时候该用哪种记账方式。近些年非常火的混合精度训练思路就是在用fp16做前向和反向传播计算加速的同时维护一份独立的fp32权重副本。这样既享受了fp16的速度优势又避免了梯度更新时因精度不足而出现“梯度下溢”的尴尬。工程上我只建议一个原则正式训练大型模型无脑启用混合精度但如果你刚入门在调试代码、排查 bug请老老实实开fp32慢慢跑否则你会同时被逻辑 bug 和数值 bug 一起折磨绝对怀疑人生。6.2 梯度爆炸与梯度消失为什么数值要稳定引入精度概念之后我们再回头看看训练中经常遇到的“梯度爆炸”和“梯度消失”问题。梯度爆炸的本质是链式法则在多层网络中的累乘效应如果每一层的梯度范数都大于 1经过几十层相乘梯度会变成天文数字哪怕学习率是1e-6更新一步也可能把模型参数全部踢飞。相反如果每一层的梯度范数都小于 1经过几十层相乘梯度会趋向于 0靠前的层几乎学不到任何东西网络实际上丧失了学习能力。最经典的应对手段包括梯度裁剪Gradient Clipping。它的做法很简单也很暴力算完梯度后检查一下梯度的总范数如果超过预设的阈值就等比例缩小梯度让它保持在安全范围。这本质上就像你在悬崖边走路时系了一根安全绳不管你走多快都不会真的摔下悬崖。另外一个做法就是初始化比如 He 初始化、Xavier 初始化这些初始化策略的核心思想都是为了调整初始权重尺度让前向传播和反向传播的“信号”在层间传递时保持在一个合理的范围不容易爆炸也不太容易消失。从数值微分的误差分析到反向传播的链式法则再到梯度下降的迭代更新再到工程中的精度问题这一条线下来其实核心就一句话深度学习的训练本质上是在跟数值误差和动力学稳定性做斗争。你越早理解这一点遇到实际问题时就越不会慌。7. 从基石到进阶打通深度学习的任督二脉最后想聊一点更宏观的感受。很多人觉得基础理论不重要会用框架就行。但我发现身边真正的强者没有一个不是把基础概念吃得透透的。我说的基础不是背公式而是能在不查资料的情况下徒手推出一个简单的两层网络的前向、反向过程能清楚地解释数值微分、解析梯度、反向传播三者之间的区别与联系。理解数值微分和梯度下降这条主线你还能自然而然地延伸到很多更高级的话题。比如学习率调度策略的基础就是梯度下降的步长控制优化器的演进历史完全就是基于朴素 SGD 的各种缺陷去修补理解梯度检查之后你就能更好地投入到自定义算子开发和模型部署优化中去而浮点精度这个话题可以直接连接到推理部署时的精馏量化操作。我带过不少新人发现一个现象挺有意思那些能快速定位训练问题的人往往不是最会写代码的而是最懂原理的。因为训练过程里 80% 的异常根源都能追溯到“梯度”这两个字上——梯度过大、梯度过小、梯度消失、梯度为 NaN、梯度方向错误。看懂梯度你就掌握了调试深度学习的核心武器。从数值微分到梯度下降这短短几个字浓缩了深度学习发展史上最关键的一段智慧。它能走到的深度取决于你对这段基础消化得有多透。就像盖房子底下的石头不起眼但恰恰是它决定了这栋大楼最终能盖多高。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。