读完这篇,你能做什么
能为简单复合函数手推两个偏导,并用完整程序与中心差分在光滑点核对结果。
学习理由、准备要求与知识点
为什么学它
训练要知道每个参数怎样影响损失。理解变化沿计算步骤怎样传递,才能查出梯度算错在哪一环。
先读这些内容
知识点清单
- 平均变化率与导数
- 导数单位
- 偏导与固定其他变量
- 梯度向量
- 乘积法则
- 链式法则
- 计算图中的多条路径
- 中心差分与步长误差
运行前先确认Java 17;Python 3.14 CPU 教学实验,无外部数据、预训练权重、GPU 或多机训练。源代码基线与局限见源码模块。
01先弄懂它是什么
想知道“输入稍微变一点,输出会怎样”,就会用到导数。先计算平均变化率 (f(x+h)-f(x))/h;让变化量 h 越来越接近零,如果这个比值趋向一个确定值,就得到导数 f'(x)。例如 f(x)=x^2,在 x=3 附近增加很小的 h,输出大约增加 6h。因此这里的导数是六;函数值是九,表示的却是当前输出有多大。
读导数时,还要看变化的是什么、用什么单位。例如位置用米、时间用秒,位置对时间的导数就是速度,单位为米每秒。同样,损失对参数求导时,单位是损失单位除以参数单位。导数为负,只说明在当前位置稍微增加输入,输出会下降;它不表示输出本身是负数,也不保证输入增加很远之后还会继续下降。
多个输入时,偏导表示固定其他变量,只改变其中一个。f(x,y)=x*y 对 x 的偏导为 y,对 y 的偏导为 x;把两个偏导排成向量得到梯度 grad f=(df/dx,df/dy)。梯度形状与输入变量形状对应,后续神经网络的每个参数都需要自己的变化方向,不能把一个损失标量误当成只有一个梯度数值。
文中这几个词,先认一认
- 导数
- 输入在某一点附近稍微变化时,输出变化有多快。它描述的是附近的变化趋势。
- 偏导
- 函数有多个输入时,暂时固定其他输入,只看其中一个输入稍微变化会带来什么影响。
- 链式法则
- 一个计算接着另一个计算时,沿途结合每一步的导数,求出最初输入对最终输出的影响。
02看看它怎么工作
若 u=g(x)、z=h(u),则 dz/dx=(dz/du)*(du/dx)。直觉是第一步把输入变化放大或缩小一次,第二步再作用一次,所以沿单条链相乘。对 f(x)=(2x+1)^2,内部导数为二,外部对 u 的导数为 2u,合起来为 4(2x+1)。漏掉内部导数会得到貌似合理却错误的梯度。
当一条输入通过多条路径影响输出时,要把各路径贡献相加。设 u=x*y+sin(x),f=u^2,则 df/dx=2u*(y+cos(x)),df/dy=2u*x。x 同时进入乘法和正弦,两条贡献在加法节点汇合;y 只经过乘法路径。画计算图能清楚解释为什么有的地方相乘,有的地方相加。
乘积法则同样来自同时变化的两条路径:d(a*b)=b*da+a*db。如果 a 与 b 都依赖 x,不能只把其中一个当常数。Java 教学算法为每个中间量同时保存值、对 x 的偏导和对 y 的偏导,让加法、乘法和正弦各自传播局部变化。这是固定两个方向的前向微分模型,不是完整反向传播引擎。
中心差分 (f(x+h)-f(x-h))/(2h) 用两侧取样近似导数,适合检查推导。对于足够光滑的函数,这个近似公式本身的误差(截断误差)通常随 h 的平方减小,但浮点舍入会在 h 很小时被除法放大,所以不是 h 越小越好。若 x+h 在机器中已经等于 x,就没有实际改变输入值。
有些点本来就没有通常意义的导数。例如绝对值函数在零点左右的斜率不同,中心差分却可能刚好算出零,不能据此说零点导数存在。机器学习库可能为特定运算约定次梯度或边界值。核对时要避开不可导点,或按该运算的明确约定来检查。有限差分只提供某个位置附近的数值证据,不能证明所有输入下都正确。
先跑通一个最小例子
Python stdlib 实验比较解析偏导和中心差分,额外观察步长缩小时的误差变化。它不构建 PyTorch 自动微分图。
import math
def f(x,y):
return (x*y+math.sin(x))**2
def central(fn,x,h):
if not math.isfinite(h) or h<=0 or x+h==x or x-h==x:
raise ValueError("ineffective step")
return (fn(x+h)-fn(x-h))/(2*h) # 两侧独立求值,近似局部斜率。
x,y=0.7,-0.4
u=x*y+math.sin(x)
gx=2*u*(y+math.cos(x)); gy=2*u*x
assert math.isclose(central(lambda t:f(t,y),x,1e-5),gx,rel_tol=1e-7,abs_tol=1e-8)
assert math.isclose(central(lambda t:f(x,t),y,1e-5),gy,rel_tol=1e-7,abs_tol=1e-8)
assert f(0.,3.)==0.
assert math.isclose(central(lambda t:t*t,3.,1e-4),6.,rel_tol=1e-8)
e1=abs(central(math.sin,1.,1e-2)-math.cos(1.))
e2=abs(central(math.sin,1.,1e-3)-math.cos(1.))
assert e2<e1 # 本例位于光滑点且步长尚未小到舍入主导。
try:
central(math.sin,1.,0.)
raise AssertionError("invalid step accepted")
except ValueError:
pass
print("PASS 6: two partials, zero, square slope, step error, invalid step")
核对运行结果打印 PASS 6;两个偏导与中心差分在容差内一致,小步长示例误差更小,但不声称步长可无限缩小。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
X["x"] --> M["x 乘 y"]
Y["y"] --> M
X --> S["sin x"]
M --> U["相加得到 u"]
S --> U
U --> F["平方得到 f"]按顺序看,每一步做了什么
- 列出中间变量与输入输出形状。
- 写出每个基本操作的局部导数。
- 沿依赖关系传播变化。
- 汇总同一变量的多路径贡献。
- 在光滑点用适当步长进行数值核对。
03对照实现代码
PyTorch v2.9.1;torch/autograd/gradcheck.py · _compute_numerical_gradient
以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。
def directional_difference(fn,x,direction,step):
# 原创实数列表模型;方向长度应由调用者预先校验。
if step<=0:
raise ValueError("step")
lower=[a-step*v for a,v in zip(x,direction)]
upper=[a+step*v for a,v in zip(x,direction)]
return (fn(upper)-fn(lower))/(2*step) # 近似梯度与给定方向的点积。
# 不实现复数、稀疏张量、自动梯度比较或误差诊断。
PyTorch v2.9.1 的 _compute_numerical_gradient 保存输入副本,分别把输入设置到 orig-v 和 orig+v,调用待测函数,再恢复原输入。之后将两次输出之差除以两倍扰动范数,形成数值方向导数。代码明确采用两侧差分,而非只在一个方向作前向差分。
真实实现处理张量、稀疏表示及输出结构,并在附近输出之间进行额外检查。更高层 gradcheck 再把数值导数与自动微分结果比较;因此这个局部函数本身并不等于全部梯度检查。原创列表模型只演示实数方向扰动,省略这些兼容路径。
恢复原输入十分重要,因为数值检查要多次求值;若上一轮扰动残留,下一轮就不再围绕同一个基点。真实待测函数还应满足检查需要的确定性,随机采样、状态更新或原地修改可能让两次输出差异不再单纯来自输入扰动。
方向向量也需要说明规范。模型计算的是给定 direction 对应的变化率,并未自动把方向归一化;真实源码用扰动 v 与其范数组织比较。若把方向乘十而仍期待同一导数数值,就混淆了方向导数的尺度。阅读函数签名时必须同时追踪传入扰动与分母的定义。
代码先调用谁,再调用谁
- gradcheck 选择输入与检查方向
- 保存原始输入
- 在负扰动和正扰动处求值
- 恢复输入并计算中心差分
- 更高层与自动梯度进行容差比较
04动手写一个例子
双方向前向微分计算图
计算 f(x,y)=(x*y+sin(x))^2 的值和两个偏导;通过原创 Dual 值实现加法、乘法和正弦的链式规则,拒绝非有限中间值。
- 输入 1
x=0,y=3- 输出 1
f=0, dx=0, dy=0
- 输入 2
x=pi/2,y=0- 输出 2
f≈1, dx≈0, dy≈pi
每个 Dual 保存三个量:当前值 v、对原始 x 的变化率 dx、对原始 y 的变化率 dy。输入 x 初始化为 (x,1,0),输入 y 初始化为 (y,0,1)。常数的两个变化率为零,这些初始条件决定所有后续导数相对于谁计算。
加法把对应导数相加;乘法对每个方向使用 b.v*a.d+a.v*b.d;正弦把已有导数乘 cos(v)。将这些局部规则按照原表达式组合后,不需要再为整个表达式硬写一套导数公式,就能得到两个方向的结果。
正确性以计算图节点为归纳单位:若操作数保存的值与偏导正确,局部链式法则就保证新节点正确。程序的模型只支持固定两个输入方向,操作数较多时前向模式成本会随方向数增长;对大量参数和标量损失,后续反向模式往往更合适。
自测一部分使用可手算的零点和正弦峰值,另一部分用独立的解析公式或中心差分作对照,避免只把算法自身结果抄成预期值。计算中出现无穷或 NaN 会明确失败,提醒调用者重新检查尺度和函数定义,而不是把异常数值继续当作梯度更新。
为什么这样写是对的每个 Dual 节点保存该子表达式的数值,以及相对于两个原始输入的正确局部偏导。
每个基本操作对两个方向执行常数次运算;扩展到 d 个输入方向的前向模式通常需要 O(sd) 工作量。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
public class Main {
record D(double v,double dx,double dy){
D {if(!Double.isFinite(v)||!Double.isFinite(dx)||!Double.isFinite(dy))throw new IllegalArgumentException("finite");}
}
static D add(D a,D b){return new D(a.v+b.v,a.dx+b.dx,a.dy+b.dy);}
static D mul(D a,D b){
return new D(a.v*b.v,a.dx*b.v+a.v*b.dx,a.dy*b.v+a.v*b.dy); // 两条乘积路径贡献相加。
}
static D sin(D a){return new D(Math.sin(a.v),Math.cos(a.v)*a.dx,Math.cos(a.v)*a.dy);}
static D eval(double x,double y){
D a=new D(x,1,0),b=new D(y,0,1); // 两个独立输入的方向种子。
D u=add(mul(a,b),sin(a));
return mul(u,u); // 同一 u 出现两次,乘积规则自然累加两条路径。
}
static double value(double x,double y){double u=x*y+Math.sin(x);return u*u;}
static void near(double a,double b){if(!Double.isFinite(a)||Math.abs(a-b)>1e-6)throw new AssertionError(a+" != "+b);}
public static void main(String[] args){
D a=eval(1,2);double u=2+Math.sin(1);near(a.v,u*u);near(a.dx,2*u*(2+Math.cos(1)));near(a.dy,2*u);
D zero=eval(0,3);near(zero.v,0);near(zero.dx,0);near(zero.dy,0);
D peak=eval(Math.PI/2,0);near(peak.v,1);near(peak.dx,0);near(peak.dy,Math.PI);
double x=-0.5,y=1.2,h=1e-5;D d=eval(x,y);
near(d.dx,(value(x+h,y)-value(x-h,y))/(2*h));near(d.dy,(value(x,y+h)-value(x,y-h))/(2*h));
D t=new D(3,1,0);near(add(mul(t,t),t).dx,7);
try{eval(Double.NaN,1);throw new AssertionError();}catch(IllegalArgumentException expected){}
System.out.println("PASS 6");
}
}这些特殊情况也要试一试
- 零梯度可能是正常函数结构,不一定是程序失效。
- 不可导点不能靠差分值证明存在导数。
- 非有限输入及中间结果明确失败。
- 差分步长过小会失去有效扰动。
进阶选读 · 从前向到反向自动微分
场景标量损失依赖数百万参数。
做法保存计算图局部关系,从输出的一单位梯度反向累加向量雅可比积,避免为每个参数单独做前向扰动。
复杂度典型一次反向计算与前向计算同阶,存储或重算中间值有额外成本。
好处与代价反向模式适合少输出多输入,但需管理图生命周期、内存和原地修改。
05项目里怎么用
排查梯度太大或太小时,先确认损失到底怎么算。例如同一批数据,把损失从求和改成平均,梯度也会按比例缩小;学习率不变时,每次参数改动就会跟着变小。因此要先核对目标函数、样本权重,以及沿哪些维度求和或取平均,再判断是不是优化器出了问题。
梯度检查适合小规模、确定性、双精度输入的局部实验。关闭随机扰动或固定其行为,避免在检查时更新状态;对分段函数选择远离拐点的输入。测试通过说明这些检查点上的结果一致,不证明所有输入和硬件精度下都正确,更不能等同于模型已训练成功。
观察到很小或很大的梯度时,应沿计算图追踪局部因子。饱和激活可能缩小变化,重复乘较大因子可能放大变化,多个分支也可能互相抵消。先用小表达式验证机制,再在真实网络中检查统计分布和任务表现,避免仅凭一个梯度范数就决定修改整个模型。
容易踩的坑
- 把函数值和导数混淆。
- 链式法则漏乘内部导数。
- 同一变量多路径贡献只保留一条。
- 认为差分步长越小越准确。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 手工解析推导 | 可解释符号与单位 | 复杂图易漏项 | 小公式与理论检查 |
| 前向微分 | 规则局部且适合少输入 | 成本随输入方向数增长 | 少参数多输出 |
| 反向微分 | 适合标量损失大量参数 | 需要图和中间状态管理 | 神经网络训练 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01偏导时其他变量怎样处理?
展开参考答案与推导
把它们视为固定量,只观察目标变量的局部变化。这不同于所有变量一起变化的总变化率,方向导数需要同时给出各变量的变化方向。
02沿链相乘、分支相加如何理解?
展开参考答案与推导
单一路径上的局部放大率连续作用所以相乘,同一个输入经过不同路径影响输出时,总变化由各路径贡献叠加,因此相加。
03差分得到零能证明不可导点有导数吗?
展开参考答案与推导
不能。对称取样可能产生抵消,例如绝对值在零点的中心差分为零,但左右导数不同。导数存在性需按定义或算子约定判断。
换个情况,还会处理吗
01损失改成平均后梯度缩小怎么办?
展开参考答案与推导
先确认平均分母和批量大小,梯度随目标缩放是正常现象。再结合学习率与优化器调整更新幅度,不能直接把缩小说成自动微分错误。
02梯度检查每次结果都不同怎么办?
展开参考答案与推导
检查随机性、状态更新和输入原地变化,确保两次扰动求值围绕同一基点且函数行为可重复。固定种子并不自动消除所有状态副作用。
03小步长反而误差更大为何?
展开参考答案与推导
浮点相近数相减会丢失有效数字,差值再除以极小步长会放大舍入误差。需要在截断与舍入之间选择合理步长,而非无限缩小。
写代码试一试
01求简单复合函数导数。
- 输入 / 条件
f(x)=(2x+1)^2,x=1- 输出 / 预期
12
提示 先设内部变量。
展开参考答案与推导
内部值为三,外部导数为六,内部导数为二,两者相乘得到十二。漏掉内部二会错误得到六。
02求乘积两个偏导。
- 输入 / 条件
f=x*y,x=2,y=3- 输出 / 预期
dx=3,dy=2
提示 固定另一变量。
展开参考答案与推导
对 x 改变时 y 为常数三,对 y 改变时 x 为常数二;两个数构成梯度,不应把它们再随意相乘。
03检查双路径梯度。
- 输入 / 条件
f=x*x+x,x=3- 输出 / 预期
7
提示 平方中有两条 x 路径。
展开参考答案与推导
乘积法则为 x+x,再加线性项导数一,所以得到七。程序应对同一输入的所有路径累加,而非后写覆盖前写。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能为简单复合函数手推两个偏导,并用完整程序与中心差分在光滑点核对结果。
本篇重点加练
共享计算路径和函数组合决定梯度是否完整,重点训练结构重写后的等价性。
- 链式法则
- 计算图中的多条路径
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
模型耗时
T(b,w)=b²/w,其中 b 是样本数、w 是处理能力参数。解释两种偏导各固定什么、单位是什么,以及有限差分近似的适用区间。- 提交内容
- 变量表与导数解释
- 怎样算完成
- 固定变量写明
- 单位及定义域一致
练到的知识点平均变化率与导数 · 导数单位 · 偏导与固定其他变量 · 梯度向量
- 作业 02代码推演
对
f(x,y)=(xy+x)² 在 (x,y)=(2,-1/2) 画计算图并手算梯度,标注共享输入涉及的全部路径。- 提交内容
- 计算图与手算梯度
- 怎样算完成
- 局部依赖完整
- 共享路径未遗漏
练到的知识点乘积法则 · 链式法则 · 计算图中的多条路径
- 作业 03原理问答
同一函数写成
F(x)=(x+x)² 与G(x)=4x²。分别画计算图并推导导数,解释为什么图节点数量不同仍须对应同一变化率。- 提交内容
- 两张图与等价论证
- 怎样算完成
- 共享边全部标出
- 表达式与导数的等价分开说明
练到的知识点链式法则 · 计算图中的多条路径
- 作业 04代码推演
对 x=ct 的重参数化,分别写损失
L(x)对 x、对 t 的导数关系;选L(x)=x³、c=0.01,检查两种梯度的单位与数值解释。- 提交内容
- 变量关系图与推导
- 怎样算完成
- 求导变量明确
- 重参数化常量不被遗漏
练到的知识点链式法则
综合应用
- 作业 05编程实践
为二元光滑函数写中心差分检查器,输出不同步长下的绝对和相对误差;至少比较 1e-2、1e-5、1e-9 三个步长。
- 提交内容
- CPU 脚本与误差表
- 怎样算完成
- 参照导数独立
- 零附近误差口径明确
练到的知识点中心差分与步长误差
- 作业 06排错实验
故意在一个复合函数导数中省略一个局部因子,用三个不同输入执行检验;记录哪些输入掩盖错误、哪些能够暴露错误。
- 提交内容
- 错误版本、测试与修复记录
- 怎样算完成
- 测试覆盖偶然相等情况
- 定位依据来自计算记录
练到的知识点偏导与固定其他变量 · 乘积法则 · 链式法则
- 作业 07面试场景
优化目标含
abs(x)与max(0,x),同事要求数值差分在所有点都匹配一个唯一导数。制定验收规则并挑选需要单独处理的测试点。- 提交内容
- 验收说明与测试集合
- 怎样算完成
- 可导性条件明确
- 数值与数学边界分开
练到的知识点平均变化率与导数 · 梯度向量 · 中心差分与步长误差
- 作业 08编程实践
仅用 Python 标准库检验
h(x)=f(f(x)),其中f(x)=x²+1;将内外两次 f 的中间值和局部导数分别记账,在五个自选输入核对解析结果与差分。- 提交内容
- 计算账本、程序与误差记录
- 怎样算完成
- 两次调用身份不混用
- 所有检验输入及步长公开
练到的知识点链式法则 · 计算图中的多条路径
进阶挑战 · 掌握前置后选做
- 作业 09原理问答
给定向量参数 θ 和标量损失 L,比较验证全部偏导与验证任意方向 v 的变化率能提供的证据;构造只用一个方向可能漏检的梯度错误。
- 提交内容
- 论证与二维反例
- 怎样算完成
- 检查覆盖范围准确
- 反例满足给定定义
练到的知识点梯度向量 · 链式法则 · 计算图中的多条路径
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
能否设计一个错误反向程序,在所有 x=0 的测试中通过,但在非零输入失败?要求错误来自遗漏共享路径,并说明怎样改变测试才能识别它。
- 提交内容
- 错误图、掩盖条件与检测输入
- 怎样算完成
- 错误机制与输入条件对应
- 反例不只替换一个计算数字
练到的知识点链式法则 · 计算图中的多条路径
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
PyTorch v2.9.1:gradcheck 的数值方向导数 (新标签页)↗
阅读数值扰动、恢复输入和中心差分实现,区分它与完整自动微分引擎。
- 深入参考文档英文
MIT 单变量微积分 (新标签页)↗
阅读导数定义、乘积法则与链式法则。
- 深入参考文档英文
MIT 多变量微积分 (新标签页)↗
阅读偏导、梯度与方向导数。
- 深入参考文档英文
PyTorch 2.9 Gradcheck mechanics (新标签页)↗
了解数值检查与自动导数比较的条件和局限。