AI 工程基础体系 · 第 36/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。
AI 微积分与自动微分:梯度、链式法则、Jacobian 和反向传播
机器学习中的参数更新,本质上是在回答一个问题:
如果把某个参数稍微改变,模型的损失会如何变化?
“变化多快、朝哪个方向变化”由导数、梯度和 Jacobian 描述;“如何高效地把损失对输出的影响传回每个参数”由链式法则和反向传播实现;“如何让程序自动计算这些导数”则由自动微分完成。
这几个概念经常一起出现,但它们不是同义词:
- 导数描述一个标量函数在一点附近的局部变化率。
- 梯度是标量函数对向量变量的导数,表示上升最快的方向。
- Jacobian描述向量值函数对向量输入的全部一阶偏导数。
- 链式法则说明复合函数的导数如何分解。
- 反向传播是在计算图上按链式法则从输出向输入传播导数的过程。
- 自动微分是利用程序中的运算结构,自动执行这些导数计算的方法。
1. 从导数到梯度
1.1 一元导数
对于标量函数
在 处的导数定义为
前提是这个极限存在。
导数表示局部线性近似:
例如:
当 、 时,
真实值为 ,误差来自忽略了二阶项 。
机器学习中的“梯度下降”依赖的正是这个局部近似:如果损失函数 对参数 的导数为正,增大 会使损失局部增大;如果导数为负,增大 会使损失局部减小。
1.2 多元函数的偏导数
机器学习参数通常不是一个数,而是向量:
标量损失函数可以写成
对第 个参数的偏导数定义为
计算偏导数时,其他变量保持不变。
把所有偏导数排列成向量,得到梯度:
梯度的局部线性近似是
其中:
- 是参数变化;
- 是梯度;
- 是损失的一阶变化量。
由 Cauchy–Schwarz 不等式可知,在固定步长 下,梯度方向使损失增加最快,因此负梯度方向使损失下降最快:
其中 是学习率。
这不是说负梯度方向一定能让全局损失下降。它只保证在函数足够平滑、步长足够小时,一阶近似预测损失会下降。学习率过大时,二阶及更高阶项可能主导实际结果。
2. Jacobian:向量函数的一阶导数
2.1 定义与形状
设输入是 维向量,输出是 维向量:
Jacobian 矩阵定义为
因此 Jacobian 的形状是
第 行描述输出 对所有输入的偏导数;第 列描述所有输出对输入 的敏感性。
局部线性近似可以写成
这说明 Jacobian 是向量函数在一点附近的最佳一阶线性近似。
2.2 一个完整的 Jacobian 例子
令
逐项求偏导:
所以
在 处:
如果输入变化为
则输出变化近似为
Jacobian 不一定在实际计算中显式构造。深度学习框架更常计算 Jacobian 与向量的乘积,因为完整 Jacobian 在高维模型中可能非常大。
2.3 标量输出时,Jacobian 与梯度的关系
如果输出只有一个标量:
那么 Jacobian 是 的行向量:
梯度通常写成列向量:
因此二者数值相同,但约定的方向不同:
实际编程时必须确认框架使用的是行向量还是列向量约定,否则矩阵乘法中的转置可能出错。
3. 链式法则:复合函数的导数分解
3.1 标量链式法则
如果
那么
的导数为
这个公式的关键不是“分数可以约掉”,而是复合函数的局部线性变化可以连续相乘:
代入得到
因此
3.2 多变量链式法则
设
若两个函数在相关点可微,则
矩阵乘法的维度正好对应中间变量:
所以
这就是多变量链式法则。反向传播中所谓的“梯度沿图反向相乘”,本质上就是不断使用这个矩阵链式法则。
3.3 链式法则的完整算例
考虑函数
也就是
取
前向计算:
现在从 反向求导。
对 :
因为
所以
对 :
因为
所以
这给出
这里 的梯度为零,并不意味着 在所有位置都与 无关;只说明在 这个点, 的一阶变化率为零。
4. 计算图:把程序变成可微结构
自动微分通常会把表达式拆成一组基本运算节点。例如:
可以拆成:
x ──平方──┐
├──加法──平方── z
y ──正弦──┘
更形式化地定义中间变量:
前向计算按照输入到输出的顺序进行:
- 计算 ;
- 计算 ;
- 计算 ;
- 计算 。
反向计算则从 开始,使用每个节点的局部导数:
反向传播不需要重新展开整个函数,只需保存前向阶段所需的中间值,例如 、 和 。
5. 反向传播:链式法则的高效执行
5.1 为什么不能直接构造完整 Jacobian
设模型有 个参数,损失是一个标量:
我们只需要 个数:
如果模型中间激活是高维向量,直接为每个中间变量构造完整 Jacobian,可能产生巨大的矩阵。反向传播利用最终输出是标量这一点,从一个标量梯度开始传播,避免显式保存所有 Jacobian。
设一段计算为
反向传播计算
这里:
- 是来自后继节点的梯度;
- 是当前节点的 Jacobian;
- 二者相乘是一个 向量-Jacobian 积,通常称为 VJP。
反向模式自动微分的核心就是高效计算这类 VJP。
5.2 分支汇合时必须累加梯度
考虑
变量 通过两条路径影响 :
反向传播时:
两条路径分别产生:
总梯度必须相加:
如果实现只保留其中一条路径,得到的梯度就会错误。深度学习框架中的梯度累积,正是由这种计算图汇合导致的。
5.3 线性层的反向传播
考虑一个批次中的线性层:
其中:
- 是输入;
- 是权重;
- 是偏置;
- 是输出。
假设上游传回的梯度为
根据微分:
损失变化为:
代入:
整理可得:
对于批次输入
若 的形状为 batch ,则:
偏置梯度需要沿批次维度求和,因为同一个偏置被每个样本共享。
6. Softmax 交叉熵中的反向传播
分类模型常输出 logits:
Softmax 概率为
给定 one-hot 标签 ,交叉熵损失为
因为 只有真实类别位置为 1,所以也可写为:
其中 是真实类别。
将 softmax 代入:
对 求导:
因此:
这是深度学习中极其重要的简化结果。
若最后一层是
则先得到:
再使用线性层公式:
这展示了一个完整的反向路径:
交叉熵损失
↓
softmax logits 梯度 p - y
↓
线性层的 W、b、h 梯度
↓
前一层网络
工程实现中通常直接使用框架提供的交叉熵损失,而不是先手动计算 softmax 再取对数。规范实现一般会使用数值稳定的 log-sum-exp 形式,避免 logits 很大或很小时出现溢出、下溢和 。
7. 自动微分是什么
自动微分(Automatic Differentiation,AD)不是:
- 数值微分;
- 符号代数系统;
- 只适用于神经网络的特殊公式集合。
它把程序拆成基本运算,并对每个基本运算应用已知导数,再通过链式法则组合结果。
常见的三种方式如下。
7.1 数值微分
用有限差分近似:
优点是简单,不需要知道函数内部结构;缺点是:
- 太大,截断误差明显;
- 太小,浮点舍入误差明显;
- 每个参数通常需要额外执行函数;
- 复杂计算中误差难以定位;
- 不能直接得到精确的程序级导数。
数值微分适合做小规模梯度检查,不适合作为训练大型模型的主要方法。
7.2 符号微分
符号系统将表达式变换为导数表达式。例如:
可变换为:
它可以生成可读的解析结果,但表达式可能发生严重膨胀;动态控制流、数据依赖的形状和复杂程序结构也会增加实现难度。
7.3 自动微分
自动微分不需要先把整个函数化简成一个巨大公式,而是在执行程序时记录或传播局部导数。
对于
程序可以拆成:
a = x * x
b = sin(x)
y = a * b
每一步都有局部导数:
自动微分只需组合这些局部结果。
8. 前向模式与反向模式
设函数
8.1 前向模式:JVP
前向模式从输入方向 出发,计算:
这叫 Jacobian-vector product,简称 JVP。
它适合:
- 输入维度 较小;
- 输出维度 较大;
- 需要一个输入方向上的敏感性;
- 计算高阶导数或某些科学计算问题。
8.2 反向模式:VJP
反向模式从输出方向 出发,计算:
这叫 vector-Jacobian product,简称 VJP。
当模型有大量参数、但最终损失是一个标量时:
反向模式通常更合适,因为一次反向传播就能得到所有参数对标量损失的梯度。
这正是神经网络训练采用反向传播的主要原因,而不是因为“反向”在数学上更准确。对于向量输出,选择前向还是反向应由输入、输出维度和所需导数决定。
9. 用 PyTorch 验证梯度、Jacobian 和反向传播
下面的示例需要 Python 和 PyTorch。它先计算一个标量函数的梯度,再计算向量函数的 Jacobian。
import math
import torch
torch.set_default_dtype(torch.float64)
# 标量函数:
# u = x^2
# v = sin(y)
# z = u * v
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(math.pi / 2, requires_grad=True)
u = x ** 2
v = torch.sin(y)
z = u * v
# z 是标量,backward() 会计算 dz/dx 和 dz/dy
z.backward()
print("z =", z.item())
print("dz/dx =", x.grad.item())
print("dz/dy =", y.grad.item())
# 向量函数:
# f(x1, x2) = [x1^2 + x2, x1*x2]
def f(inputs):
x1, x2 = inputs
return torch.stack((x1 ** 2 + x2, x1 * x2))
inputs = torch.tensor([2.0, 3.0], requires_grad=True)
jacobian = torch.autograd.functional.jacobian(f, inputs)
print("f(inputs) =", f(inputs).detach())
print("Jacobian =\n", jacobian)
在常见 PyTorch 版本中,输出应接近:
z = 4.0
dz/dx = 4.0
dz/dy = 0.0
f(inputs) = tensor([7., 6.])
Jacobian =
tensor([[4., 1.],
[3., 2.]])
这里的关键生命周期是:
requires_grad=True:告诉框架需要跟踪该张量参与的可微运算;- 执行前向计算:框架记录计算图或构造可用于反向的结构;
z.backward():从标量z开始执行反向传播;x.grad和y.grad:读取累积到叶子张量上的梯度。
backward() 默认要求输出是标量。如果输出是向量,就必须提供外部梯度,或者对输出先聚合。例如:
x = torch.tensor([1.0, 2.0], requires_grad=True)
y = x ** 2
# 等价于对 y.sum() 求梯度
y.backward(torch.ones_like(y))
print(x.grad) # tensor([2., 4.])
因为:
更一般地,对向量输出 调用反向传播时,传入的 grad_outputs 就是外部向量 ,框架计算的是:
也就是说,它执行的是 VJP,而不是自动返回完整 Jacobian。
10. 梯度会累积:训练循环中的状态问题
在 PyTorch 等常见框架中,参数的 .grad 通常是累积的,而不是每次反向传播自动覆盖。
import torch
w = torch.tensor(2.0, requires_grad=True)
loss1 = w ** 2
loss1.backward()
print(w.grad) # tensor(4.)
loss2 = 3 * w
loss2.backward()
print(w.grad) # tensor(7.)
第二次结果是:
因此典型训练循环需要在下一次反向传播前清零梯度:
optimizer.zero_grad()
loss.backward()
optimizer.step()
如果不清零,模型更新的将是多个批次梯度的累积。梯度累积有时是有意使用的,例如用多个小批次模拟更大的有效批次;但这时必须明确累积步数,并正确处理损失缩放和学习率。
还需要区分以下状态:
- 参数值:优化器更新的模型权重;
- 梯度值:当前或累计的 ;
- 计算图:支持当前前向结果反向求导的结构;
- 优化器状态:如 Adam 的一阶、二阶动量;
- 模型模式:训练模式与推理模式;
- 数据批次状态:当前样本、标签、掩码和有效 token 数。
这些状态混淆时,错误不一定立刻报错。例如梯度未清零通常能正常运行,但训练结果会悄悄改变。
11. detach、no_grad 和反向图生命周期
推理阶段通常不需要梯度:
with torch.no_grad():
logits = model(inputs)
这样可以避免构建训练所需的计算图,降低内存和计算开销。
如果需要把一个张量从当前计算图中切断:
features = encoder(inputs)
features_for_other_system = features.detach()
detach() 返回的张量不再沿当前图向前面的运算传播梯度。它不是“冻结某个参数”的通用替代品,也不等于把张量复制到 CPU。
一个常见错误是过早调用 detach():
features = encoder(inputs).detach()
loss = head(features).sum()
loss.backward()
此时 loss 的梯度可以传给 head,但不能传回 encoder,因为连接已经被切断。
另一个常见错误是在同一计算图上多次反向传播。默认情况下,反向传播后中间缓存可能被释放,以节省内存。若确实需要对同一前向结果重复求导,通常需要重新执行前向,或在框架支持的情况下保留图;保留图会增加内存占用,不应无条件使用。
12. 不可导点、次梯度与实际网络
12.1 ReLU 的不可导点
ReLU 定义为:
当 时:
当 时:
但在 处,左导数为 0,右导数为 1,因此经典导数不存在。
深度学习框架通常在这个点选择一个约定的次梯度或实现分支。由于连续分布下精确落到某个不可导点的概率通常很小,训练通常可以进行,但这不是“ReLU 在所有点都可导”的证明。
12.2 梯度为零不等于函数是常数
函数
在 处导数为 0,但它显然不是常数函数。局部梯度为零只说明一阶变化率为零,不能单独说明全局没有变化。
同样,神经网络中出现零梯度可能表示:
- 激活函数当前处于饱和区;
- 参数处于局部平坦区域;
- 梯度因数值下溢变成零;
- 某个路径被
detach或掩码切断; - 损失或标签处理错误。
必须结合激活值、梯度范数、损失规模和计算图检查。
12.3 反例:有限差分可能误导
对
在 附近用单精度浮点数计算
时,两个函数值可能因浮点精度被舍入成相同或失去足够有效位,导致梯度估计不稳定。数值微分不是自动微分的等价实现;它更适合在小模型、较高精度和合理步长下进行梯度检查。
13. 梯度消失、梯度爆炸与链式乘积
深层网络的梯度是许多局部 Jacobian 的乘积:
如果这些矩阵的有效范数长期小于 1,乘积可能趋近于 0,形成梯度消失;如果长期大于 1,乘积可能快速增大,形成梯度爆炸。
在标量简化情形中,若每层局部导数约为 ,经过 层后梯度规模近似为:
当 、 时:
当 、 时:
这解释了为什么初始化、归一化、残差连接、激活函数和优化器都会影响训练稳定性。残差连接增加了类似恒等映射的梯度路径,但不保证所有模型、所有输入和所有训练阶段都不会出现异常梯度。
诊断时可以记录:
- 每层参数梯度的范数;
- 激活值的均值、方差和极值;
- 是否出现 NaN 或 Inf;
- loss 是否突然变成 NaN;
- 混合精度下 loss scaling 是否溢出;
- 梯度裁剪前后的范数。
梯度裁剪可以限制更新规模,但它不能修复错误的损失、标签、掩码或计算图连接。
14. 梯度检查:用数值微分验证自动微分
对于小规模函数,可以用中心差分检查自动微分结果:
下面的示例检查
import math
import torch
torch.set_default_dtype(torch.float64)
def f(x):
return x[0] ** 2 * torch.sin(x[1])
x = torch.tensor([2.0, 0.7], requires_grad=True)
# 自动微分
y = f(x)
y.backward()
autograd_grad = x.grad.detach().clone()
# 中心差分
epsilon = 1e-6
finite_diff = []
for i in range(len(x)):
plus = x.detach().clone()
minus = x.detach().clone()
plus[i] += epsilon
minus[i] -= epsilon
numerical = (f(plus) - f(minus)) / (2 * epsilon)
finite_diff.append(numerical.item())
finite_diff = torch.tensor(finite_diff)
print("autograd gradient:", autograd_grad)
print("finite difference:", finite_diff)
print("absolute error:", (autograd_grad - finite_diff).abs())
在双精度和这个函数上,两者通常会非常接近。检查时要注意:
- 不要在参数很大或函数尺度极端时直接套用默认 ;
- 尽量使用双精度;
- 比较相对误差和绝对误差;
- 避免包含随机算子、离散索引或不可导分支;
- 检查时不要让参数更新污染函数值;
- 对带 dropout、数据增强或并发非确定性的计算,先固定随机性或关闭随机行为。
梯度检查验证的是局部实现,不是训练一定收敛,也不是模型一定学到了正确目标。
15. 生成式 AI 中的自动微分
生成式模型的训练仍然遵循同一套机制。以语言模型为例:
反向传播会依次计算:
注意力中的 softmax、矩阵乘法、线性层、归一化和激活函数都可组成计算图。对生成模型的采样过程而言,推理时通常只需要前向计算;对监督微调、偏好优化或扩散模型训练,则需要对相应损失反向传播。
这也解释了训练和推理的成本差异:
- 训练需要保存或重计算中间激活,并进行反向计算;
- 推理通常不需要保存反向图;
- 序列长度、批次大小、隐藏维度和梯度累积步数会共同影响显存与计算量;
- 激活检查点通过“少保存、多重算”交换显存和计算时间;
- 参数、梯度和优化器状态通常都需要额外存储。
对于长上下文生成式模型,自动微分的内存成本可能成为主要约束,而不仅仅是参数数量。
16. 生产系统中的梯度边界
梯度计算不是孤立的数学模块,而是训练系统中一条有状态的数据流:
flowchart LR
D[训练数据与标签] --> F[前向计算]
F --> L[损失]
L --> B[反向传播]
B --> G[参数梯度]
G --> O[优化器状态与参数更新]
O --> F
V[验证数据] --> E[评测]
O --> E
关键路径是:
- 训练数据进入模型;
- 前向计算生成预测;
- 损失函数比较预测和标签;
- 反向传播产生梯度;
- 优化器结合梯度和自身状态更新参数;
- 更新后的参数进入下一轮前向计算;
- 验证数据只用于评测,不应反向更新训练参数。
在权限设计上,训练任务需要读取训练数据、写入检查点和更新模型状态;评测任务通常只需要读取模型和验证数据。把生产用户输入、敏感数据或测试集错误地接入训练图,可能造成数据泄漏,而梯度本身还可能携带训练样本信息。权限边界应围绕数据集、检查点、实验记录和部署模型分别控制。
在成本控制上,需要区分:
- 前向成本;
- 反向成本;
- 重新计算激活的成本;
- 梯度累积导致的额外迭代成本;
- 失败重试导致的重复计算;
- 评测和训练是否共享昂贵的模型实例。
当训练任务失败时,不能只看最终 loss。应保留足以定位问题的状态,例如数据批次标识、随机种子、模型检查点、梯度范数、学习率、混合精度溢出记录和异常层名称。否则即使重新运行成功,也难以判断是数据、数值稳定性、权限、资源不足还是代码变更导致的失败。
17. 常见误解与诊断路径
17.1 “反向传播就是把参数倒序相乘”
不准确。反向传播是在计算图上从输出向输入传播上游梯度,并在分支汇合处累加路径贡献。每个节点都要应用自己的局部 Jacobian;不同算子还可能涉及广播、求和、转置和共享参数。
17.2 “梯度越大,参数越重要”
梯度是局部变化率,不是绝对重要性。梯度大小还受参数单位、损失缩放、批次大小、正则化和网络参数化方式影响。比较不同层的梯度时,常需要同时观察参数范数、相对更新量和激活尺度。
17.3 “把学习率调小就能修复 NaN”
NaN 可能来自:
log(0);exp溢出;- 除以零;
- 输入中已有 NaN;
- 混合精度溢出;
- 非法标签;
- 梯度或优化器状态损坏。
减小学习率只能缓解部分更新过大的问题,不能修复数学域错误或输入数据错误。
17.4 “验证集也可以参与反向传播”
如果验证集梯度用于更新模型参数,它就不再是独立验证集,会产生评测泄漏。验证阶段应关闭参数更新;是否关闭梯度图取决于是否需要评测导数,但常规模型评测通常使用无梯度模式来降低资源消耗。
17.5 “完整 Jacobian 总是应该计算”
高维模型中完整 Jacobian 可能不可承受。应根据目标选择:
- 标量损失对参数的梯度:反向模式;
- 输入方向对输出的变化:JVP;
- 输出方向对输入的敏感性:VJP;
- 只有确实需要全部元素时才构造完整 Jacobian。
18. 从数学对象到训练更新
把整个过程合起来,一个监督学习步骤可以写成:
给定样本 和参数 :
然后通过链式法则计算:
优化器再执行更新:
其中自动微分负责的是梯度计算,不负责:
- 选择损失函数;
- 判断标签是否正确;
- 选择学习率;
- 保证数据没有泄漏;
- 保证模型一定收敛;
- 评估模型是否满足业务目标;
- 控制谁可以访问训练数据和模型检查点。
因此,正确梯度只是训练系统成立的必要条件之一。模型、数据、评测、权限和成本必须作为同一个生产系统设计;但这些系统决策最终都建立在一个核心事实之上:
模型训练之所以可行,是因为复合计算可以拆成局部导数,并通过链式法则高效地组合;当输出是标量而参数很多时,反向模式自动微分能够以合理的成本得到完整参数梯度。
系列导航与关联阅读
- 系列入口:AI 工程完整学习路线:从机器学习与 Transformer 到 RAG、Agent 和生产治理
- 上一篇:AI 概率统计:随机变量、分布、估计、贝叶斯与不确定性
- 下一篇:AI 优化基础:凸性、梯度下降、约束、鞍点与收敛判断
官方资料
本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论