AI 工程基础体系 · 第 36/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

AI 微积分与自动微分:梯度、链式法则、Jacobian 和反向传播

机器学习中的参数更新,本质上是在回答一个问题:

如果把某个参数稍微改变,模型的损失会如何变化?

“变化多快、朝哪个方向变化”由导数、梯度和 Jacobian 描述;“如何高效地把损失对输出的影响传回每个参数”由链式法则和反向传播实现;“如何让程序自动计算这些导数”则由自动微分完成。

这几个概念经常一起出现,但它们不是同义词:

  • 导数描述一个标量函数在一点附近的局部变化率。
  • 梯度是标量函数对向量变量的导数,表示上升最快的方向。
  • Jacobian描述向量值函数对向量输入的全部一阶偏导数。
  • 链式法则说明复合函数的导数如何分解。
  • 反向传播是在计算图上按链式法则从输出向输入传播导数的过程。
  • 自动微分是利用程序中的运算结构,自动执行这些导数计算的方法。

1. 从导数到梯度

1.1 一元导数

对于标量函数

y=f(x),y=f(x),

xx 处的导数定义为

f(x)=limΔx0f(x+Δx)f(x)Δx,f'(x)=\lim_{\Delta x\to 0} \frac{f(x+\Delta x)-f(x)}{\Delta x},

前提是这个极限存在。

导数表示局部线性近似:

f(x+Δx)f(x)+f(x)Δx.f(x+\Delta x) \approx f(x)+f'(x)\Delta x.

例如:

f(x)=x2,f(x)=2x.f(x)=x^2, \qquad f'(x)=2x.

x=3x=3Δx=0.01\Delta x=0.01 时,

f(3.01)f(3)+230.01=9.06.f(3.01)\approx f(3)+2\cdot 3\cdot 0.01 =9.06.

真实值为 3.012=9.06013.01^2=9.0601,误差来自忽略了二阶项 (Δx)2(\Delta x)^2

机器学习中的“梯度下降”依赖的正是这个局部近似:如果损失函数 L(w)L(w) 对参数 ww 的导数为正,增大 ww 会使损失局部增大;如果导数为负,增大 ww 会使损失局部减小。


1.2 多元函数的偏导数

机器学习参数通常不是一个数,而是向量:

w=(w1,w2,,wn).\mathbf w=(w_1,w_2,\ldots,w_n).

标量损失函数可以写成

L(w)=L(w1,,wn).L(\mathbf w)=L(w_1,\ldots,w_n).

对第 ii 个参数的偏导数定义为

Lwi=limΔwi0L(w1,,wi+Δwi,,wn)L(w)Δwi.\frac{\partial L}{\partial w_i} = \lim_{\Delta w_i\to 0} \frac{ L(w_1,\ldots,w_i+\Delta w_i,\ldots,w_n)-L(\mathbf w) }{ \Delta w_i }.

计算偏导数时,其他变量保持不变。

把所有偏导数排列成向量,得到梯度:

wL=[Lw1Lw2Lwn].\nabla_{\mathbf w}L = \begin{bmatrix} \frac{\partial L}{\partial w_1}\\ \frac{\partial L}{\partial w_2}\\ \vdots\\ \frac{\partial L}{\partial w_n} \end{bmatrix}.

梯度的局部线性近似是

L(w+Δw)L(w)+wLTΔw.L(\mathbf w+\Delta\mathbf w) \approx L(\mathbf w) + \nabla_{\mathbf w}L^\mathsf T \Delta\mathbf w.

其中:

  • Δw\Delta\mathbf w 是参数变化;
  • wL\nabla_{\mathbf w}L 是梯度;
  • wLTΔw\nabla_{\mathbf w}L^\mathsf T\Delta\mathbf w 是损失的一阶变化量。

由 Cauchy–Schwarz 不等式可知,在固定步长 Δw\|\Delta\mathbf w\| 下,梯度方向使损失增加最快,因此负梯度方向使损失下降最快:

wnew=wηwL,\mathbf w_{\text{new}} = \mathbf w-\eta\nabla_{\mathbf w}L,

其中 η>0\eta>0 是学习率。

这不是说负梯度方向一定能让全局损失下降。它只保证在函数足够平滑、步长足够小时,一阶近似预测损失会下降。学习率过大时,二阶及更高阶项可能主导实际结果。


2. Jacobian:向量函数的一阶导数

2.1 定义与形状

设输入是 nn 维向量,输出是 mm 维向量:

y=f(x),xRn,yRm.\mathbf y=f(\mathbf x), \qquad \mathbf x\in\mathbb R^n, \quad \mathbf y\in\mathbb R^m.

Jacobian 矩阵定义为

Jf(x)=yx=[y1x1y1xnymx1ymxn].J_f(\mathbf x) = \frac{\partial \mathbf y}{\partial \mathbf x} = \begin{bmatrix} \frac{\partial y_1}{\partial x_1} & \cdots & \frac{\partial y_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \cdots & \frac{\partial y_m}{\partial x_n} \end{bmatrix}.

因此 Jacobian 的形状是

m×n.m\times n.

ii 行描述输出 yiy_i 对所有输入的偏导数;第 jj 列描述所有输出对输入 xjx_j 的敏感性。

局部线性近似可以写成

y(x+Δx)y(x)+Jf(x)Δx.\mathbf y(\mathbf x+\Delta\mathbf x) \approx \mathbf y(\mathbf x) + J_f(\mathbf x)\Delta\mathbf x.

这说明 Jacobian 是向量函数在一点附近的最佳一阶线性近似。


2.2 一个完整的 Jacobian 例子

y=f(x1,x2)=[y1y2]=[x12+x2x1x2].\mathbf y=f(x_1,x_2) = \begin{bmatrix} y_1\\ y_2 \end{bmatrix} = \begin{bmatrix} x_1^2+x_2\\ x_1x_2 \end{bmatrix}.

逐项求偏导:

y1x1=2x1,y1x2=1,\frac{\partial y_1}{\partial x_1}=2x_1, \qquad \frac{\partial y_1}{\partial x_2}=1,

y2x1=x2,y2x2=x1.\frac{\partial y_2}{\partial x_1}=x_2, \qquad \frac{\partial y_2}{\partial x_2}=x_1.

所以

Jf(x1,x2)=[2x11x2x1].J_f(x_1,x_2) = \begin{bmatrix} 2x_1 & 1\\ x_2 & x_1 \end{bmatrix}.

(x1,x2)=(2,3)(x_1,x_2)=(2,3) 处:

Jf(2,3)=[4132].J_f(2,3) = \begin{bmatrix} 4 & 1\\ 3 & 2 \end{bmatrix}.

如果输入变化为

Δx=[0.010.02],\Delta\mathbf x= \begin{bmatrix} 0.01\\ -0.02 \end{bmatrix},

则输出变化近似为

Δy[4132][0.010.02]=[0.020.01].\Delta\mathbf y \approx \begin{bmatrix} 4 & 1\\ 3 & 2 \end{bmatrix} \begin{bmatrix} 0.01\\ -0.02 \end{bmatrix} = \begin{bmatrix} 0.02\\ -0.01 \end{bmatrix}.

Jacobian 不一定在实际计算中显式构造。深度学习框架更常计算 Jacobian 与向量的乘积,因为完整 Jacobian 在高维模型中可能非常大。


2.3 标量输出时,Jacobian 与梯度的关系

如果输出只有一个标量:

y=f(x)R,y=f(\mathbf x)\in\mathbb R,

那么 Jacobian 是 1×n1\times n 的行向量:

Jf(x)=[yx1yxn].J_f(\mathbf x) = \begin{bmatrix} \frac{\partial y}{\partial x_1} & \cdots & \frac{\partial y}{\partial x_n} \end{bmatrix}.

梯度通常写成列向量:

xy=[yx1yxn].\nabla_{\mathbf x}y = \begin{bmatrix} \frac{\partial y}{\partial x_1}\\ \vdots\\ \frac{\partial y}{\partial x_n} \end{bmatrix}.

因此二者数值相同,但约定的方向不同:

Jf(x)=xyT.J_f(\mathbf x) = \nabla_{\mathbf x}y^\mathsf T.

实际编程时必须确认框架使用的是行向量还是列向量约定,否则矩阵乘法中的转置可能出错。


3. 链式法则:复合函数的导数分解

3.1 标量链式法则

如果

z=g(y),y=f(x),z=g(y), \qquad y=f(x),

那么

z=g(f(x))z=g(f(x))

的导数为

dzdx=dzdydydx.\frac{dz}{dx} = \frac{dz}{dy} \frac{dy}{dx}.

这个公式的关键不是“分数可以约掉”,而是复合函数的局部线性变化可以连续相乘:

ΔzdzdyΔy,ΔydydxΔx.\Delta z \approx \frac{dz}{dy}\Delta y, \qquad \Delta y \approx \frac{dy}{dx}\Delta x.

代入得到

ΔzdzdydydxΔx.\Delta z \approx \frac{dz}{dy} \frac{dy}{dx} \Delta x.

因此

dzdx=dzdydydx.\frac{dz}{dx} = \frac{dz}{dy} \frac{dy}{dx}.


3.2 多变量链式法则

y=f(x),z=g(y).\mathbf y=f(\mathbf x), \qquad \mathbf z=g(\mathbf y).

若两个函数在相关点可微,则

Jgf(x)=Jg(y)Jf(x).J_{g\circ f}(\mathbf x) = J_g(\mathbf y)J_f(\mathbf x).

矩阵乘法的维度正好对应中间变量:

JfRm×n,JgRk×m,J_f\in\mathbb R^{m\times n}, \qquad J_g\in\mathbb R^{k\times m},

所以

JgJfRk×n.J_gJ_f\in\mathbb R^{k\times n}.

这就是多变量链式法则。反向传播中所谓的“梯度沿图反向相乘”,本质上就是不断使用这个矩阵链式法则。


3.3 链式法则的完整算例

考虑函数

u=x2,v=siny,z=uv.u=x^2, \qquad v=\sin y, \qquad z=uv.

也就是

z=x2siny.z=x^2\sin y.

x=2,y=π2.x=2,\qquad y=\frac{\pi}{2}.

前向计算:

u=22=4,v=sinπ2=1,z=uv=4.u=2^2=4, \qquad v=\sin\frac{\pi}{2}=1, \qquad z=uv=4.

现在从 zz 反向求导。

xx

zx=zuux.\frac{\partial z}{\partial x} = \frac{\partial z}{\partial u} \frac{\partial u}{\partial x}.

因为

zu=v=1,ux=2x=4,\frac{\partial z}{\partial u}=v=1, \qquad \frac{\partial u}{\partial x}=2x=4,

所以

zx=14=4.\frac{\partial z}{\partial x}=1\cdot 4=4.

yy

zy=zvvy.\frac{\partial z}{\partial y} = \frac{\partial z}{\partial v} \frac{\partial v}{\partial y}.

因为

zv=u=4,vy=cosy=0,\frac{\partial z}{\partial v}=u=4, \qquad \frac{\partial v}{\partial y}=\cos y=0,

所以

zy=40=0.\frac{\partial z}{\partial y}=4\cdot 0=0.

这给出

(x,y)z=[40].\nabla_{(x,y)}z = \begin{bmatrix} 4\\ 0 \end{bmatrix}.

这里 yy 的梯度为零,并不意味着 zz 在所有位置都与 yy 无关;只说明在 y=π/2y=\pi/2 这个点,siny\sin y 的一阶变化率为零。


4. 计算图:把程序变成可微结构

自动微分通常会把表达式拆成一组基本运算节点。例如:

z=(x2+siny)2z=(x^2+\sin y)^2

可以拆成:

x ──平方──┐
          ├──加法──平方── z
y ──正弦──┘

更形式化地定义中间变量:

a=x2,b=siny,c=a+b,z=c2.a=x^2, \qquad b=\sin y, \qquad c=a+b, \qquad z=c^2.

前向计算按照输入到输出的顺序进行:

  1. 计算 aa
  2. 计算 bb
  3. 计算 cc
  4. 计算 zz

反向计算则从 zz 开始,使用每个节点的局部导数:

zc=2c,\frac{\partial z}{\partial c}=2c,

za=zcca=2c1,\frac{\partial z}{\partial a} = \frac{\partial z}{\partial c} \frac{\partial c}{\partial a} = 2c\cdot 1,

zx=zaax=2c2x,\frac{\partial z}{\partial x} = \frac{\partial z}{\partial a} \frac{\partial a}{\partial x} = 2c\cdot 2x,

zb=2c,\frac{\partial z}{\partial b}=2c,

zy=zbby=2ccosy.\frac{\partial z}{\partial y} = \frac{\partial z}{\partial b} \frac{\partial b}{\partial y} = 2c\cos y.

反向传播不需要重新展开整个函数,只需保存前向阶段所需的中间值,例如 xxccyy


5. 反向传播:链式法则的高效执行

5.1 为什么不能直接构造完整 Jacobian

设模型有 nn 个参数,损失是一个标量:

L=f(w),wRn.L=f(\mathbf w),\qquad \mathbf w\in\mathbb R^n.

我们只需要 nn 个数:

wL.\nabla_{\mathbf w}L.

如果模型中间激活是高维向量,直接为每个中间变量构造完整 Jacobian,可能产生巨大的矩阵。反向传播利用最终输出是标量这一点,从一个标量梯度开始传播,避免显式保存所有 Jacobian。

设一段计算为

h=f(x),L=g(h).\mathbf h=f(\mathbf x), \qquad L=g(\mathbf h).

反向传播计算

Lx=Lhhx.\frac{\partial L}{\partial\mathbf x} = \frac{\partial L}{\partial\mathbf h} \frac{\partial\mathbf h}{\partial\mathbf x}.

这里:

  • Lh\frac{\partial L}{\partial\mathbf h} 是来自后继节点的梯度;
  • hx\frac{\partial\mathbf h}{\partial\mathbf x} 是当前节点的 Jacobian;
  • 二者相乘是一个 向量-Jacobian 积,通常称为 VJP。

反向模式自动微分的核心就是高效计算这类 VJP。


5.2 分支汇合时必须累加梯度

考虑

z=x2+x.z=x^2+x.

变量 xx 通过两条路径影响 zz

a=x2,b=x,z=a+b.a=x^2,\qquad b=x,\qquad z=a+b.

反向传播时:

za=1,zb=1.\frac{\partial z}{\partial a}=1, \qquad \frac{\partial z}{\partial b}=1.

两条路径分别产生:

zxa=12x=2x,\frac{\partial z}{\partial x}\bigg|_{a} = 1\cdot 2x=2x,

zxb=11=1.\frac{\partial z}{\partial x}\bigg|_{b} = 1\cdot 1=1.

总梯度必须相加:

dzdx=2x+1.\frac{dz}{dx}=2x+1.

如果实现只保留其中一条路径,得到的梯度就会错误。深度学习框架中的梯度累积,正是由这种计算图汇合导致的。


5.3 线性层的反向传播

考虑一个批次中的线性层:

z=Wx+b,\mathbf z=\mathbf W\mathbf x+\mathbf b,

其中:

  • xRn\mathbf x\in\mathbb R^n 是输入;
  • WRm×n\mathbf W\in\mathbb R^{m\times n} 是权重;
  • bRm\mathbf b\in\mathbb R^m 是偏置;
  • zRm\mathbf z\in\mathbb R^m 是输出。

假设上游传回的梯度为

gz=LzRm.\mathbf g_z=\frac{\partial L}{\partial\mathbf z}\in\mathbb R^m.

根据微分:

dz=dWx+Wdx+db.d\mathbf z=d\mathbf W\,\mathbf x+\mathbf W\,d\mathbf x+d\mathbf b.

损失变化为:

dL=gzTdz.dL=\mathbf g_z^\mathsf T d\mathbf z.

代入:

dL=gzTdWx+gzTWdx+gzTdb.dL = \mathbf g_z^\mathsf T d\mathbf W\,\mathbf x + \mathbf g_z^\mathsf T\mathbf W\,d\mathbf x + \mathbf g_z^\mathsf T d\mathbf b.

整理可得:

LW=gzxT,\frac{\partial L}{\partial\mathbf W} = \mathbf g_z\mathbf x^\mathsf T,

Lx=WTgz,\frac{\partial L}{\partial\mathbf x} = \mathbf W^\mathsf T\mathbf g_z,

Lb=gz.\frac{\partial L}{\partial\mathbf b} = \mathbf g_z.

对于批次输入

Z=XWT+b,Z=XW^\mathsf T+b,

XX 的形状为 batch ×n\times n,则:

LW=GZTX,\frac{\partial L}{\partial W} = G_Z^\mathsf T X,

Lb=batchGZ.\frac{\partial L}{\partial b} = \sum_{\text{batch}}G_Z.

偏置梯度需要沿批次维度求和,因为同一个偏置被每个样本共享。


6. Softmax 交叉熵中的反向传播

分类模型常输出 logits:

z=(z1,,zK).\mathbf z=(z_1,\ldots,z_K).

Softmax 概率为

pi=ezij=1Kezj.p_i=\frac{e^{z_i}}{\sum_{j=1}^K e^{z_j}}.

给定 one-hot 标签 y\mathbf y,交叉熵损失为

L=i=1Kyilogpi.L=-\sum_{i=1}^K y_i\log p_i.

因为 yiy_i 只有真实类别位置为 1,所以也可写为:

L=logpt,L=-\log p_t,

其中 tt 是真实类别。

将 softmax 代入:

L=logeztjezj=zt+logjezj.L = -\log\frac{e^{z_t}}{\sum_j e^{z_j}} = -z_t+\log\sum_j e^{z_j}.

zkz_k 求导:

Lzk=1(k=t)+ezkjezj,\frac{\partial L}{\partial z_k} = -\mathbf 1(k=t) + \frac{e^{z_k}}{\sum_j e^{z_j}},

因此:

Lzk=pkyk\boxed{ \frac{\partial L}{\partial z_k}=p_k-y_k }

这是深度学习中极其重要的简化结果。

若最后一层是

z=Wh+b,\mathbf z=W\mathbf h+\mathbf b,

则先得到:

Lz=py,\frac{\partial L}{\partial\mathbf z} = \mathbf p-\mathbf y,

再使用线性层公式:

LW=(py)hT,\frac{\partial L}{\partial W} = (\mathbf p-\mathbf y)\mathbf h^\mathsf T,

Lb=py,\frac{\partial L}{\partial\mathbf b} = \mathbf p-\mathbf y,

Lh=WT(py).\frac{\partial L}{\partial\mathbf h} = W^\mathsf T(\mathbf p-\mathbf y).

这展示了一个完整的反向路径:

交叉熵损失
   ↓
softmax logits 梯度 p - y
   ↓
线性层的 W、b、h 梯度
   ↓
前一层网络

工程实现中通常直接使用框架提供的交叉熵损失,而不是先手动计算 softmax 再取对数。规范实现一般会使用数值稳定的 log-sum-exp 形式,避免 logits 很大或很小时出现溢出、下溢和 log(0)\log(0)


7. 自动微分是什么

自动微分(Automatic Differentiation,AD)不是:

  • 数值微分;
  • 符号代数系统;
  • 只适用于神经网络的特殊公式集合。

它把程序拆成基本运算,并对每个基本运算应用已知导数,再通过链式法则组合结果。

常见的三种方式如下。

7.1 数值微分

用有限差分近似:

f(x)f(x+ϵ)f(xϵ)2ϵ.f'(x) \approx \frac{f(x+\epsilon)-f(x-\epsilon)}{2\epsilon}.

优点是简单,不需要知道函数内部结构;缺点是:

  • ϵ\epsilon 太大,截断误差明显;
  • ϵ\epsilon 太小,浮点舍入误差明显;
  • 每个参数通常需要额外执行函数;
  • 复杂计算中误差难以定位;
  • 不能直接得到精确的程序级导数。

数值微分适合做小规模梯度检查,不适合作为训练大型模型的主要方法。

7.2 符号微分

符号系统将表达式变换为导数表达式。例如:

f(x)=x2sinxf(x)=x^2\sin x

可变换为:

f(x)=2xsinx+x2cosx.f'(x)=2x\sin x+x^2\cos x.

它可以生成可读的解析结果,但表达式可能发生严重膨胀;动态控制流、数据依赖的形状和复杂程序结构也会增加实现难度。

7.3 自动微分

自动微分不需要先把整个函数化简成一个巨大公式,而是在执行程序时记录或传播局部导数。

对于

f(x)=x2sinx,f(x)=x^2\sin x,

程序可以拆成:

a = x * x
b = sin(x)
y = a * b

每一步都有局部导数:

ax=2x,bx=cosx,ya=b,yb=a.\frac{\partial a}{\partial x}=2x, \qquad \frac{\partial b}{\partial x}=\cos x, \qquad \frac{\partial y}{\partial a}=b, \qquad \frac{\partial y}{\partial b}=a.

自动微分只需组合这些局部结果。


8. 前向模式与反向模式

设函数

f:RnRm.f:\mathbb R^n\rightarrow\mathbb R^m.

8.1 前向模式:JVP

前向模式从输入方向 v\mathbf v 出发,计算:

Jf(x)v.J_f(\mathbf x)\mathbf v.

这叫 Jacobian-vector product,简称 JVP。

它适合:

  • 输入维度 nn 较小;
  • 输出维度 mm 较大;
  • 需要一个输入方向上的敏感性;
  • 计算高阶导数或某些科学计算问题。

8.2 反向模式:VJP

反向模式从输出方向 u\mathbf u 出发,计算:

uTJf(x).\mathbf u^\mathsf T J_f(\mathbf x).

这叫 vector-Jacobian product,简称 VJP。

当模型有大量参数、但最终损失是一个标量时:

nm=1,n\gg m=1,

反向模式通常更合适,因为一次反向传播就能得到所有参数对标量损失的梯度。

这正是神经网络训练采用反向传播的主要原因,而不是因为“反向”在数学上更准确。对于向量输出,选择前向还是反向应由输入、输出维度和所需导数决定。


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.]])

这里的关键生命周期是:

  1. requires_grad=True:告诉框架需要跟踪该张量参与的可微运算;
  2. 执行前向计算:框架记录计算图或构造可用于反向的结构;
  3. z.backward():从标量 z 开始执行反向传播;
  4. x.grady.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.])

因为:

(y1+y2)x=[2x12x2].\frac{\partial (y_1+y_2)}{\partial x} = \begin{bmatrix} 2x_1\\ 2x_2 \end{bmatrix}.

更一般地,对向量输出 y\mathbf y 调用反向传播时,传入的 grad_outputs 就是外部向量 u\mathbf u,框架计算的是:

uTJf.\mathbf u^\mathsf T J_f.

也就是说,它执行的是 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.)

第二次结果是:

d(w2)dw+d(3w)dw=4+3=7.\frac{d(w^2)}{dw} + \frac{d(3w)}{dw} = 4+3=7.

因此典型训练循环需要在下一次反向传播前清零梯度:

optimizer.zero_grad()
loss.backward()
optimizer.step()

如果不清零,模型更新的将是多个批次梯度的累积。梯度累积有时是有意使用的,例如用多个小批次模拟更大的有效批次;但这时必须明确累积步数,并正确处理损失缩放和学习率。

还需要区分以下状态:

  • 参数值:优化器更新的模型权重;
  • 梯度值:当前或累计的 L/w\partial L/\partial w
  • 计算图:支持当前前向结果反向求导的结构;
  • 优化器状态:如 Adam 的一阶、二阶动量;
  • 模型模式:训练模式与推理模式;
  • 数据批次状态:当前样本、标签、掩码和有效 token 数。

这些状态混淆时,错误不一定立刻报错。例如梯度未清零通常能正常运行,但训练结果会悄悄改变。


11. detachno_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 定义为:

ReLU(x)=max(0,x).\operatorname{ReLU}(x)=\max(0,x).

x>0x>0 时:

ddxReLU(x)=1;\frac{d}{dx}\operatorname{ReLU}(x)=1;

x<0x<0 时:

ddxReLU(x)=0.\frac{d}{dx}\operatorname{ReLU}(x)=0.

但在 x=0x=0 处,左导数为 0,右导数为 1,因此经典导数不存在。

深度学习框架通常在这个点选择一个约定的次梯度或实现分支。由于连续分布下精确落到某个不可导点的概率通常很小,训练通常可以进行,但这不是“ReLU 在所有点都可导”的证明。

12.2 梯度为零不等于函数是常数

函数

f(x)=x3f(x)=x^3

x=0x=0 处导数为 0,但它显然不是常数函数。局部梯度为零只说明一阶变化率为零,不能单独说明全局没有变化。

同样,神经网络中出现零梯度可能表示:

  • 激活函数当前处于饱和区;
  • 参数处于局部平坦区域;
  • 梯度因数值下溢变成零;
  • 某个路径被 detach 或掩码切断;
  • 损失或标签处理错误。

必须结合激活值、梯度范数、损失规模和计算图检查。

12.3 反例:有限差分可能误导

f(x)=x2f(x)=x^2

x=108x=10^8 附近用单精度浮点数计算

f(x+ϵ)f(xϵ)2ϵ\frac{f(x+\epsilon)-f(x-\epsilon)}{2\epsilon}

时,两个函数值可能因浮点精度被舍入成相同或失去足够有效位,导致梯度估计不稳定。数值微分不是自动微分的等价实现;它更适合在小模型、较高精度和合理步长下进行梯度检查。


13. 梯度消失、梯度爆炸与链式乘积

深层网络的梯度是许多局部 Jacobian 的乘积:

Lh0=Lh1h1h0hkhk1.\frac{\partial L}{\partial \mathbf h_0} = \frac{\partial L}{\partial \mathbf h_1} \frac{\partial \mathbf h_1}{\partial \mathbf h_0} \cdots \frac{\partial \mathbf h_k}{\partial \mathbf h_{k-1}}.

如果这些矩阵的有效范数长期小于 1,乘积可能趋近于 0,形成梯度消失;如果长期大于 1,乘积可能快速增大,形成梯度爆炸。

在标量简化情形中,若每层局部导数约为 aa,经过 kk 层后梯度规模近似为:

ak.|a|^k.

a=0.5a=0.5k=20k=20 时:

0.5209.5×107.0.5^{20}\approx 9.5\times 10^{-7}.

a=2a=2k=20k=20 时:

220106.2^{20}\approx 10^6.

这解释了为什么初始化、归一化、残差连接、激活函数和优化器都会影响训练稳定性。残差连接增加了类似恒等映射的梯度路径,但不保证所有模型、所有输入和所有训练阶段都不会出现异常梯度。

诊断时可以记录:

  • 每层参数梯度的范数;
  • 激活值的均值、方差和极值;
  • 是否出现 NaN 或 Inf;
  • loss 是否突然变成 NaN;
  • 混合精度下 loss scaling 是否溢出;
  • 梯度裁剪前后的范数。

梯度裁剪可以限制更新规模,但它不能修复错误的损失、标签、掩码或计算图连接。


14. 梯度检查:用数值微分验证自动微分

对于小规模函数,可以用中心差分检查自动微分结果:

fxif(x+ϵei)f(xϵei)2ϵ.\frac{\partial f}{\partial x_i} \approx \frac{f(\mathbf x+\epsilon\mathbf e_i) - f(\mathbf x-\epsilon\mathbf e_i)} {2\epsilon}.

下面的示例检查

f(x,y)=x2siny.f(x,y)=x^2\sin y.

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())

在双精度和这个函数上,两者通常会非常接近。检查时要注意:

  1. 不要在参数很大或函数尺度极端时直接套用默认 ϵ\epsilon
  2. 尽量使用双精度;
  3. 比较相对误差和绝对误差;
  4. 避免包含随机算子、离散索引或不可导分支;
  5. 检查时不要让参数更新污染函数值;
  6. 对带 dropout、数据增强或并发非确定性的计算,先固定随机性或关闭随机行为。

梯度检查验证的是局部实现,不是训练一定收敛,也不是模型一定学到了正确目标。


15. 生成式 AI 中的自动微分

生成式模型的训练仍然遵循同一套机制。以语言模型为例:

tokenembeddingTransformer blocklogitscross-entropy loss.\text{token} \rightarrow \text{embedding} \rightarrow \text{Transformer block} \rightarrow \text{logits} \rightarrow \text{cross-entropy loss}.

反向传播会依次计算:

Llogits,Lattention,LMLP,Lembedding,Lparameters.\frac{\partial L}{\partial\text{logits}}, \quad \frac{\partial L}{\partial\text{attention}}, \quad \frac{\partial L}{\partial\text{MLP}}, \quad \frac{\partial L}{\partial\text{embedding}}, \quad \frac{\partial L}{\partial\text{parameters}}.

注意力中的 softmax、矩阵乘法、线性层、归一化和激活函数都可组成计算图。对生成模型的采样过程而言,推理时通常只需要前向计算;对监督微调、偏好优化或扩散模型训练,则需要对相应损失反向传播。

这也解释了训练和推理的成本差异:

  • 训练需要保存或重计算中间激活,并进行反向计算;
  • 推理通常不需要保存反向图;
  • 序列长度、批次大小、隐藏维度和梯度累积步数会共同影响显存与计算量;
  • 激活检查点通过“少保存、多重算”交换显存和计算时间;
  • 参数、梯度和优化器状态通常都需要额外存储。

对于长上下文生成式模型,自动微分的内存成本可能成为主要约束,而不仅仅是参数数量。


16. 生产系统中的梯度边界

梯度计算不是孤立的数学模块,而是训练系统中一条有状态的数据流:

flowchart LR
    D[训练数据与标签] --> F[前向计算]
    F --> L[损失]
    L --> B[反向传播]
    B --> G[参数梯度]
    G --> O[优化器状态与参数更新]
    O --> F
    V[验证数据] --> E[评测]
    O --> E

关键路径是:

  1. 训练数据进入模型;
  2. 前向计算生成预测;
  3. 损失函数比较预测和标签;
  4. 反向传播产生梯度;
  5. 优化器结合梯度和自身状态更新参数;
  6. 更新后的参数进入下一轮前向计算;
  7. 验证数据只用于评测,不应反向更新训练参数。

在权限设计上,训练任务需要读取训练数据、写入检查点和更新模型状态;评测任务通常只需要读取模型和验证数据。把生产用户输入、敏感数据或测试集错误地接入训练图,可能造成数据泄漏,而梯度本身还可能携带训练样本信息。权限边界应围绕数据集、检查点、实验记录和部署模型分别控制。

在成本控制上,需要区分:

  • 前向成本;
  • 反向成本;
  • 重新计算激活的成本;
  • 梯度累积导致的额外迭代成本;
  • 失败重试导致的重复计算;
  • 评测和训练是否共享昂贵的模型实例。

当训练任务失败时,不能只看最终 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. 从数学对象到训练更新

把整个过程合起来,一个监督学习步骤可以写成:

给定样本 (x,y)(\mathbf x,\mathbf y) 和参数 θ\theta

y^=fθ(x),\hat{\mathbf y}=f_\theta(\mathbf x),

L=(y^,y),L=\ell(\hat{\mathbf y},\mathbf y),

然后通过链式法则计算:

θL=Ly^y^θ.\nabla_\theta L = \frac{\partial L}{\partial \hat{\mathbf y}} \frac{\partial \hat{\mathbf y}}{\partial\theta}.

优化器再执行更新:

θt+1=Update(θt,θLt,optimizer statet).\theta_{t+1} = \operatorname{Update} (\theta_t,\nabla_\theta L_t,\text{optimizer state}_t).

其中自动微分负责的是梯度计算,不负责:

  • 选择损失函数;
  • 判断标签是否正确;
  • 选择学习率;
  • 保证数据没有泄漏;
  • 保证模型一定收敛;
  • 评估模型是否满足业务目标;
  • 控制谁可以访问训练数据和模型检查点。

因此,正确梯度只是训练系统成立的必要条件之一。模型、数据、评测、权限和成本必须作为同一个生产系统设计;但这些系统决策最终都建立在一个核心事实之上:

模型训练之所以可行,是因为复合计算可以拆成局部导数,并通过链式法则高效地组合;当输出是标量而参数很多时,反向模式自动微分能够以合理的成本得到完整参数梯度。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。