qinghuan
← 数学语言与概率基础 / 全部笔记

单元 04 / 数学语言与概率基础

P0 · 必会主线L0 · 零基础

导数、偏导与链式法则:追踪一次变化

先看输入稍变一点会怎样影响输出,再用两变量计算图理解导数、偏导和链式法则,并用差分核对。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能为简单复合函数手推两个偏导,并用完整程序与中心差分在光滑点核对结果。

学习理由、准备要求与知识点

为什么学它

训练要知道每个参数怎样影响损失。理解变化沿计算步骤怎样传递,才能查出梯度算错在哪一环。

先读这些内容

知识点清单

  • 平均变化率与导数
  • 导数单位
  • 偏导与固定其他变量
  • 梯度向量
  • 乘积法则
  • 链式法则
  • 计算图中的多条路径
  • 中心差分与步长误差

运行前先确认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 自动微分图。

experiment.py · python
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"]

按顺序看,每一步做了什么

  1. 列出中间变量与输入输出形状。
  2. 写出每个基本操作的局部导数。
  3. 沿依赖关系传播变化。
  4. 汇总同一变量的多路径贡献。
  5. 在光滑点用适当步长进行数值核对。

03对照实现代码

PyTorch v2.9.1:gradcheck 的数值方向导数 (新标签页)↗

PyTorch v2.9.1;torch/autograd/gradcheck.py · _compute_numerical_gradient

以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。

核心机制代码 · python
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 与其范数组织比较。若把方向乘十而仍期待同一导数数值,就混淆了方向导数的尺度。阅读函数签名时必须同时追踪传入扰动与分母的定义。

代码先调用谁,再调用谁

  1. gradcheck 选择输入与检查方向
  2. 保存原始输入
  3. 在负扰动和正扰动处求值
  4. 恢复输入并计算中心差分
  5. 更高层与自动梯度进行容差比较

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 节点保存该子表达式的数值,以及相对于两个原始输入的正确局部偏导。

时间 固定两个方向时 O(s),s 为基本操作数。额外空间 本固定表达式 O(1);一般表达式需保存或管理中间节点。

每个基本操作对两个方向执行常数次运算;扩展到 d 个输入方向的前向模式通常需要 O(sd) 工作量。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
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 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能为简单复合函数手推两个偏导,并用完整程序与中心差分在光滑点核对结果。

本篇重点加练

共享计算路径和函数组合决定梯度是否完整,重点训练结构重写后的等价性。

  • 链式法则
  • 计算图中的多条路径

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    模型耗时 T(b,w)=b²/w,其中 b 是样本数、w 是处理能力参数。解释两种偏导各固定什么、单位是什么,以及有限差分近似的适用区间。

    提交内容
    变量表与导数解释
    怎样算完成
    • 固定变量写明
    • 单位及定义域一致

    练到的知识点平均变化率与导数 · 导数单位 · 偏导与固定其他变量 · 梯度向量

  2. 作业 02代码推演

    对 f(x,y)=(xy+x)² 在 (x,y)=(2,-1/2) 画计算图并手算梯度,标注共享输入涉及的全部路径。

    提交内容
    计算图与手算梯度
    怎样算完成
    • 局部依赖完整
    • 共享路径未遗漏

    练到的知识点乘积法则 · 链式法则 · 计算图中的多条路径

  3. 作业 03原理问答

    同一函数写成 F(x)=(x+x)² 与 G(x)=4x²。分别画计算图并推导导数,解释为什么图节点数量不同仍须对应同一变化率。

    提交内容
    两张图与等价论证
    怎样算完成
    • 共享边全部标出
    • 表达式与导数的等价分开说明

    练到的知识点链式法则 · 计算图中的多条路径

  4. 作业 04代码推演

    对 x=ct 的重参数化,分别写损失 L(x) 对 x、对 t 的导数关系;选 L(x)=x³、c=0.01,检查两种梯度的单位与数值解释。

    提交内容
    变量关系图与推导
    怎样算完成
    • 求导变量明确
    • 重参数化常量不被遗漏

    练到的知识点链式法则

综合应用

  1. 作业 05编程实践

    为二元光滑函数写中心差分检查器,输出不同步长下的绝对和相对误差;至少比较 1e-2、1e-5、1e-9 三个步长。

    提交内容
    CPU 脚本与误差表
    怎样算完成
    • 参照导数独立
    • 零附近误差口径明确

    练到的知识点中心差分与步长误差

  2. 作业 06排错实验

    故意在一个复合函数导数中省略一个局部因子,用三个不同输入执行检验;记录哪些输入掩盖错误、哪些能够暴露错误。

    提交内容
    错误版本、测试与修复记录
    怎样算完成
    • 测试覆盖偶然相等情况
    • 定位依据来自计算记录

    练到的知识点偏导与固定其他变量 · 乘积法则 · 链式法则

  3. 作业 07面试场景

    优化目标含 abs(x) 与 max(0,x),同事要求数值差分在所有点都匹配一个唯一导数。制定验收规则并挑选需要单独处理的测试点。

    提交内容
    验收说明与测试集合
    怎样算完成
    • 可导性条件明确
    • 数值与数学边界分开

    练到的知识点平均变化率与导数 · 梯度向量 · 中心差分与步长误差

  4. 作业 08编程实践

    仅用 Python 标准库检验 h(x)=f(f(x)),其中 f(x)=x²+1;将内外两次 f 的中间值和局部导数分别记账,在五个自选输入核对解析结果与差分。

    提交内容
    计算账本、程序与误差记录
    怎样算完成
    • 两次调用身份不混用
    • 所有检验输入及步长公开

    练到的知识点链式法则 · 计算图中的多条路径

进阶挑战 · 掌握前置后选做

  1. 作业 09原理问答

    给定向量参数 θ 和标量损失 L,比较验证全部偏导与验证任意方向 v 的变化率能提供的证据;构造只用一个方向可能漏检的梯度错误。

    提交内容
    论证与二维反例
    怎样算完成
    • 检查覆盖范围准确
    • 反例满足给定定义

    练到的知识点梯度向量 · 链式法则 · 计算图中的多条路径

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 反例构造

    能否设计一个错误反向程序,在所有 x=0 的测试中通过,但在非零输入失败?要求错误来自遗漏共享路径,并说明怎样改变测试才能识别它。

    提交内容
    错误图、掩盖条件与检测输入
    怎样算完成
    • 错误机制与输入条件对应
    • 反例不只替换一个计算数字

    练到的知识点链式法则 · 计算图中的多条路径

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗