Skip to content

第 1 章 导数与计算图:程序怎样知道往哪儿改 ​

训练一个神经网络之前,可以先解决一个更小的问题:程序算出了一个不满意的数,输入该往哪个方向改?后端程序员习惯沿调用链查错。这里也要沿依赖关系往回走,不过传回去的是一个数:输出对某个输入的变化有多敏感。

本章只用标量,也就是单个实数。先把导数算对,再谈矩阵和 GPU,会少掉许多同时出现的概念。本章对应课程第一讲前半 notebook 的函数、差分、计算图和手工求导部分;本文重新组织推导,不是视频转录。固定版本 notebook

1.1 从一个能运行的函数开始 ​

设输入是实数 x,输出是实数 f(x):

f(x)=3x2−4x+5.

代入 x=3,得到 20。这个数告诉我们当前位置的函数值,却没有告诉我们稍微增大 x 会怎样。可以做两次计算:先算 f(x),再算 f(x+h)。其中 h 是一次很小的输入改动。两次输出之差除以输入之差,得到平均变化率:

f(x+h)−f(x)h.

把多项式展开,分子是 6xh+3h2−4h,所以商是 6x−4+3h。让 h 趋近 0,剩下 6x−4,这就是导数 f′(x)。在 x=3 处,导数是 14;在 x=2/3 处,导数是 0。后一个结果只说曲线此处局部平坦。对这个向上开口的二次函数,它恰好是最小值点;一般函数的零导数也可能对应最大值或其他平坦位置。

python
def f(x):
    return 3*x*x - 4*x + 5

x, h = 3.0, 1e-6
slope = (f(x + h) - f(x)) / h

这个近似称为前向差分。它不需要知道函数内部怎样写,代价是每个待检查方向都要重新运行函数。参数上百万时,用它训练模型不合适;用来检查少数梯度却很有价值。

为什么不让 h 越小越好?计算机保存的是有限精度数。两次几乎相等的函数值相减,会丢掉有效数字。h 太大,局部直线近似不准;h 太小,舍入误差被除法放大。本书的小型梯度测试使用 Python 双精度或 PyTorch float64,并通常从 10−6 左右开始检查,而不把某个步长当成适合所有函数的常量。

更好的对照是中心差分:

f′(x)≈f(x+h)−f(x−h)2h.

若函数足够光滑,左右展开中的偶次项相消,截断误差由前向差分的 O(h) 改善为 O(h2)。它仍有浮点误差,也不适合直接验证 ReLU 折点这样的不可导位置。

1.2 多个输入:每次只动一个 ​

现在把程序写成 d=ab+c。在 a=2,b=−3,c=10 时,d=4。改变 a 的时候保持 b,c 不变,得到偏导数:

∂d∂a=b=−3,∂d∂b=a=2,∂d∂c=1.

符号 “偏” 提醒我们,其他输入暂时固定。这些偏导数按输入顺序组成向量 (−3,2,1),叫梯度。每个分量都回答一个具体问题:只改这个输入一点点,输出大约改多少。

还可以接着计算 L=df,其中 f=−2,于是 L=−8。这里用大写 L 作为最终输出的名字;到训练部分它才具体表示损失。整个程序分成三步:

e=ab=−6,d=e+c=4,L=df=−8.
标量计算图:向右算数值,向左传导数
可切换原始尺寸,滚动查看细节

查看大图

尺寸读取中

适合窗口:完整显示图片,不放大超过原始尺寸。 按 Esc 关闭。

图里的箭头记录依赖:算出 e 要先有 a,b,算出 L 要先有 d,f。它是有向无环图。程序可以有循环,但一次具体前向计算展开后的这些运算节点不能循环依赖自己。

1.3 链式法则是一条局部乘法规则 ​

先看离终点最近的乘法 L=df。固定 f,d 每增加一点,L 约增加 f 倍那么多,因此 ∂L/∂d=−2。同理,∂L/∂f=d=4。

再往前一层,d=e+c。e 对 d 的局部影响是 1,d 对 L 的影响是 -2。两段接起来,e 对 L 的影响是 1×(−2)=−2。这就是链式法则:

∂L∂e=∂L∂d∂d∂e.

最后,e=ab。局部导数是 b=−3 和 a=2,乘上已经收到的 ∂L/∂e=−2,得到:

节点前向值对最终输出的导数
a26
b-3-4
c10-2
e=ab-6-2
d=e+c4-2
f-24
L=df-81

最后一行的 1 来自 ∂L/∂L=1,是反向计算的起点。不要把节点保存的梯度误读成 ∂e/∂a 这种局部导数。一个节点的 .grad 表示最终目标对它的导数;局部规则负责把这个数继续向前传播。

有分叉时,还要相加。例如 q=ab(a+b),a 会沿乘法和加法两条路径影响结果。令 u=ab,v=a+b,q=uv,则

∂q∂a=vb+u,∂q∂b=va+u.

在 a=−2,b=3 处,u=−6,v=1,结果分别为 -3 和 -8。遗漏任何一条路径都会错。下一章写引擎时,这个数学上的 “相加” 就是 +=。

1.4 梯度描述局部方向,不保证任意步长有效 ​

若目标是减小 L,设参数向量为 θ,学习率为正数 η,可以尝试

θnew=θ−η∇L.

由一阶近似,L(θ+Δθ)≈L(θ)+∇L⋅Δθ。代入 Δθ=−η∇L,变化近似为 −η∥∇L∥2,只要梯度不为零就是负数。但这个推导的前提是改动足够小;大步可能越过低点。

课程 notebook 的早期演示有一段沿正梯度方向改数值,用来观察输出增大。那与后面减小损失时使用负号并不冲突。另一个容易漏掉的条件是,必须先执行反向计算,否则初始梯度都是零,所谓更新根本没有移动。

交互实验:梯度下降与学习率 ​

训练实验室 · 梯度下降

方向对了,步子太大会怎样?

只训练一个参数 w,让它靠近目标 t。调节步长,再亲手更新一次,看损失是否真的下降。

L(w) = (w − t)² · g = 2(w − t) · w ← w − ηg。每一步都按公式重新计算。

01

选一种走法,比较结果

02

输入 · 步长与目标

决定沿负梯度走多远。0 表示停在原地。
训练开始前,参数所在的位置。
损失曲线的最低点;到达这里时梯度为零。
滑块与精确输入控制同一个参数
03

过程 · 沿曲线更新参数

已更新 0 / 16 次

从 w = 4 开始,损失为 16,梯度为 8。

曲线保留可读尺寸。窄屏可左右滑动,手动滚动会暂停播放;键盘聚焦曲线区域后,用左右方向键查看坐标与轨迹。

-4.72目标 04.72L(w),图顶约 22.2784w
曲线是损失,虚线连接更新轨迹,大圆点是当前位置。坐标范围随已经走过的点调整;判断是否发散请看数值,不能只看图的高度。

指标保留完整数字;如数字较长,可左右滑动,或聚焦数字后用方向键查看。

当前参数 w4目标 0
当前损失 L16离目标距离的平方
下次使用的梯度8g = 2(w − t)
误差倍数 1 − 2η0.6绝对值小于 1 才会缩短距离

这组参数说明什么?学习率在 0 与 0.5 之间:从同一侧逐步靠近目标。

逐步数值记录(已完成 0 次更新)

保留完整数值。窄屏可左右滑动;键盘聚焦记录区域后,用左右方向键查看。

从起点到当前步骤,显示值经过舍入
步数参数 w梯度损失
04816
04

解释 · 方向之外,还要看步长

为什么这个例子能判断是否收敛?

令误差 e = w − t,一次更新后 e 新 = (1 − 2η)e,损失因此乘以 (1 − 2η)²。对非零初始误差,0 < η < 1 时逐步收敛;η=1 保持距离;η>1 放大距离。起点已经等于目标时,梯度为零,任何允许的学习率都不移动。

这是一个可完整算清的标量二次函数,运行的是真实梯度下降规则。神经网络有许多参数,损失曲面也不同;这里的学习率分界不能直接套到网络训练。没有训练数据、随机小批量、动量或 Adam。JavaScript 使用有限精度浮点数,极小时可能出现舍入或下溢。

不操作也能读懂:负梯度给出局部下降方向;学习率决定走多远。把 η 改成 1.2,可以看到“方向正确但损失越走越大”。

依据:PyTorch:梯度与参数更新;收敛分界由本页的二次函数直接推导。外部文档核对于 2026-10-14。

1.5 把计算结果交给测试 ​

在项目根目录执行:

bash
source qa/environment/env.sh
python examples/part1/scalar_engine.py

这是本书真实运行的小实验,完整记录在 qa/part1/scalar.log。它核对了 L=(ab+c)f 的四个输入导数,最大中心差分误差为 1.497e-09;这比脚本设置的 10−8 误差上限小。误差上限是测试条件,输出误差是这次运行的观测,两者不要混淆。

练习与解答 ​

补充题:保持 a=2,b=−3,c=10,f=−2,只把 b 增加 0.001,不用运行程序,预测 L 的变化。提示:使用表里的 b 梯度。答案是 −4×0.001=−0.004;这里函数对单独的 b 恰好线性,所以不是仅仅近似。

官方 micrograd 练习第一部分要求对

f(a,b,c)=−a3+sin⁡(3b)−1/c+b2.5−a0.5

求解析梯度,再比较前向差分和中心差分。对本题实数定义域取 a>0,b>0,c≠0。答案为

(−3a2−12a,3cos⁡(3b)+2.5b1.5,1c2).

在 (2,3,4) 处,分别是 −12.3535533906,10.2569902711,0.0625。注意 sin(3b) 还要乘里面的导数 3,而 −1/c 求导后为正。完整可运行解答是 examples/part1/micrograd_exercises.py,会以同一个 h=10−5 检查两种差分误差。先自己实现,再运行答案更能发现漏掉哪条规则。官方练习 Colab

来源与边界 ​

正文依据第一讲两份 MIT notebook 的已读取单元编写,固定提交为 73c3fcc741f0ec104ca850b1fb0df90e7e8d4cde;后半 notebook 开头重复的导数演示合并在本章。官方练习已取得原始 notebook 并逐题核对。视频描述的章节时间戳用于范围校验,不代表取得了完整字幕。配套代码的改编来源和 MIT 授权保存在 examples/part1/NOTICE.txt。

保留各书来源与许可 · 示例运行条件以正文为准