上一篇:(六)不确定的世界——讲概率怎么驾驭不确定性。 这一篇:讲计算机怎么处理"连续"的东西——因为机器只会加减乘除,不会求导。
耗时:读完约 50 分钟,动手约 2 小时。
开头:机器不会求导,但它能"假装"会
第一篇文章说过一句话:
数学是连续的、无限的、精确的;计算机是离散的、有限的、近似的。 这两者之间的翻译工作,就是计算机数学。
前六篇讲的大部分是"离散"那一侧——格点、计数、图、证明、复杂度、概率。这一篇第一次正面处理"连续"。
因为微积分研究的对象是连续变化:速度、斜率、面积、曲线的弯曲。而计算机根本不知道什么是连续。它只有加减乘除、只有有限位。
那怎么办?
不怎么办——近似。而且近似得非常好。
这一篇要讲清三件事:
- 导数的数值算法,会让机器"自伤"——算得越精细,误差反而越大(这条最反直觉)
- 泰勒展开:用多项式当"万能替身",计算机里的
sin、exp、log全是这么算的 - 梯度下降:AI 学习的全部机制,就是"沿着导数下坡走"
一、导数是什么(30 秒版)
不废话,一句话:
导数 = 变化率 = 那个点的斜率 = “如果从这继续走,会往哪走、走多快”。
数学定义是:
f'(x) = lim(h→0) [f(x+h) − f(x)] / h
关键在 lim h→0。 计算机做不到"趋近于 0"——它只能取一个很小的 h。
这一句就是整篇文章的引信。
二、第一个反直觉:h 越小,误差越大
拿这个公式直接写代码(这叫前向差商):
def 导数(f, x, h):
return (f(x+h) - f(x)) / h
直觉告诉你:h 越小越准。 因为数学定义里 h 是趋近于 0 的。
实测(f = sin(x),在 x=1 求导,真值 cos(1) = 0.54030230586813977):
| h | 算出的导数 | 绝对误差 |
|---|---|---|
| 1e-1 | 0.497363752535 | 4.29e-02 |
| 1e-2 | 0.536085981012 | 4.22e-03 |
| 1e-3 | 0.539881480360 | 4.21e-04 |
| 1e-4 | 0.540260231419 | 4.21e-05 |
| 1e-5 | 0.540298098506 | 4.21e-06 |
| 1e-6 | 0.540301885121 | 4.21e-07 |
| 1e-7 | 0.540302264040 | 4.18e-08 |
| 1e-8 | 0.540302302898 | 2.97e-09 ← 最准 |
| 1e-9 | 0.540302358409 | 5.25e-08 ← 变差了! |
| 1e-10 | 0.540302247387 | 5.85e-08 |
| 1e-11 | 0.540301137164 | 1.17e-06 |
| 1e-12 | 0.540345546085 | 4.32e-05 |
| 1e-13 | 0.539568389968 | 7.34e-04 |
| 1e-14 | 0.544009282066 | 3.71e-03 |
| 1e-15 | 0.555111512313 | 1.48e-02 |
| 1e-16 | 0.000000000000 | 5.40e-01 ← 彻底崩了 |
看那条曲线:误差先降、后升,呈 U 形。最优在 h ≈ 1e-8,最小误差 2.97e-9。
h 再小,误差反弹。h = 1e-16 时,导数算出来是 0——完全错了。
为什么会这样:两个误差在打架
这不是 bug,是两种误差的拉锯:
1. 截断误差(h 太大时)
数学上导数是极限,你取有限的 h,就是"割线"代替"切线"。误差大约是 h 的一半。 → 所以 h 越大,截断误差越大,这符合直觉:O(h)。
2. 舍入误差(h 太小时)
这是上一篇讲过的格点问题。f(x+h) − f(x) 这个减法,当 h 极小时,两个数几乎相等,相减之后有效位数全丢光。
具体看 h = 1e-16 那行:x+h 在 double 里根本就是 x(因为 1.0 附近的格点间距是 2.2e-16,加 1e-16 走不到下一格)。于是 f(x+h) − f(x) = 0,除出来就是 0。
这就是第一篇文章那个"格点"在求导上的回报。 你在两个巨大的、几乎相等的数之间做减法,剩下的全是噪声。
→ 舍入误差的量级是 ε/h(ε = 机器精度 2.2e-16)。h 越小,这个除法的放大越猛。
总误差 = 两个方向相反的误差之和
总误差 ≈ 截断误差 + 舍入误差
≈ (h/2)·|f''| + (ε/h)·|f|
一个随 h 涨,一个随 h 落。两者相加,就是 U 形。 最优点在两边相等的地方:
h ≈ √ε ≈ 1.5 × 10⁻⁸
实测最优 h = 1e-8,理论 sqrt(ε) = 1.5e-8。对上了。
这是整本专题最漂亮的一个例子:第一篇文章的"格点",在这里直接变成了"算得越精细反而越错"的根源。 离散和连续,在这里正面相撞。
三、中心差商:把精度提高一档
有办法缓解。改成中心差商(两边各取一点):
def 导数(f, x, h):
return (f(x+h) - f(x-h)) / (2*h)
为什么更好?因为 f(x+h) 和 f(x-h) 的一阶项互相抵消了,剩下的主项是 h²。
实测对比:
| h | 前向差商误差 | 中心差商误差 |
|---|---|---|
| 1e-1 | 4.29e-02 | 9.00e-04 |
| 1e-2 | 4.22e-03 | 9.01e-06 |
| 1e-3 | 4.21e-04 | 9.01e-08 |
| 1e-4 | 4.21e-05 | 9.00e-10 ← 最准 |
| 1e-5 | 4.21e-06 | 1.11e-11 ← 更准 |
| 1e-6 | 4.21e-07 | 2.77e-11 |
| 1e-7 | 4.18e-08 | 1.94e-10 |
| 1e-8 | 2.97e-09 | 2.58e-09 |
| 1e-9 | 5.25e-08 | 2.97e-09 |
同一个 h,中心差商精度高 4~5 个数量级。
而且注意最优点也变了:
前向差商最优 h ≈ √ε ≈ 1.5e-8
中心差商最优 h ≈ ε^(1/3) ≈ 6.0e-6
中心差商的最优 h 反而"更大"——因为它的截断误差掉得快,所以它更能容忍小 h 带来的舍入误差。
这一课叫"误差的量级分析"。 它告诉你:不是越小越好,是有一个最优点;而这个最优点在哪,是可以算出来的。
这是数值分析(Numerical Analysis)的核心思维方式。第一篇文章讲"格点",这一篇讲"在格点上怎么活得最好"。
四、泰勒展开:计算机里的 sin 是怎么算的
现在换个方向。计算机里的 sin、cos、exp、log,没有一个是真的"算"出来的——它们全是用加减乘除拼出来的。
工具就是泰勒展开:
sin(x) = x − x³/3! + x⁵/5! − x⁷/7! + ...
一个无限多项式,每一项都是加减乘除。
实测:要几项才够
sin(3) 的真值是 0.141120008059867:
| 项数 | 逼近值 | 误差 |
|---|---|---|
| 1 | 3.000000000000 | 2.86e+00 |
| 2 | −1.500000000000 | 1.64e+00 |
| 3 | 0.525000000000 | 3.84e-01 |
| 4 | 0.091071428571 | 5.01e-02 |
| 5 | 0.145312500000 | 4.19e-03 |
| 6 | 0.140874594156 | 2.45e-04 |
| 8 | 0.141119654341 | 3.54e-07 |
| 10 | 0.141120007859 | 2.01e-10 |
10 项,误差 10⁻¹⁰。 对 double 的 16 位精度来说,够用了。
但注意一个坑:离展开点越远,越难收敛
泰勒展开是在 x=0 附近展开的。那么 x=10 呢?
sin(10) = −0.5440211109
用 10 项泰勒: −16.8118501374 误差 1.63e+01 ← 完全错了
用 20 项泰勒: 误差 2.83e-09
在 x=10 处,10 项完全不够,要 20 项才行。
所以真实的
sin实现不是"直接展开到 x",而是先把 x 折叠到一个小范围(利用 sin 的周期性,把 x 缩到 [−π/4, π/4]),再展开。这就是"算法"和"数学"的区别:数学告诉你公式,算法告诉你怎么用有限算力把它用好。
这一条对所有数值库都成立:exp、log、pow、atan……全是"先归约到小区间,再展开"。
五、梯度下降:AI 学习的全部机制
现在到这一篇最重要的一节——因为 AI 的一切,都建在它上面。
一句话
想找一个函数的最低点?沿着导数(梯度)的反方向,一小步一小步走。
为什么?因为导数的方向就是"上升最快的方向"。所以反过来走,就是"下降最快"。
实测:学习率决定生死
用一个二维的碗:f(x,y) = x² + 10y²,最优点在 (0,0)。
注意 y 方向的梯度是 x 方向的 10 倍(这个差异叫"条件数",是真实问题里最常见的情况——有的方向陡,有的方向平)。
跑 50 步,看不同学习率:
| 学习率 | 最终 x | 最终 y | f 值 | 状态 |
|---|---|---|---|---|
| 0.001 | 9.05e-01 | 3.64e-01 | 2.15e+00 | 太慢,50 步没走完 |
| 0.01 | 3.64e-01 | 1.43e-05 | 1.33e-01 | 慢 |
| 0.05 | 5.15e-03 | 0.00e+00 | 2.66e-05 | 接近 |
| 0.09 | 4.91e-05 | 1.43e-05 | 4.44e-09 | ✅ 收敛 |
| 0.1 | 1.43e-05 | 1.00e+00 | 1.00e+01 | ❌ y 方向卡住不降 |
| 0.11 | 4.03e-06 | 9.10e+03 | 8.28e+08 | ❌ 炸了 |
| 0.2 | — | — | — | ❌ 发散 |
| 0.5 | — | — | — | ❌ 发散 |
注意 0.1 那一行,特别值得看:x 收敛到了 1.4e-05(很好),但 y 停在 1.0(完全没动)。
这不是"变慢",是"卡死"。
为什么卡死:一个可以算出来的临界值
f 对 y 求导 = 20y。梯度下降更新:
y ← y − lr · 20y = y(1 − 20·lr)
要让 y 收敛,必须 |1 − 20·lr| < 1:
−1 < 1 − 20·lr < 1
→ 0 < lr < 0.1
临界值正好是 0.1。 实测:lr=0.09 收敛,lr=0.1 卡死,lr=0.11 炸掉。全对。
这一条是深度学习里最实用的一个诊断: 学习率的上限由"最陡那个方向"决定,但收敛速度由"最平那个方向"决定。 所以在陡缓差异大的问题上(几乎所有真实问题),你被迫用小学习率,于是平的方向走得极慢——这就是训练慢的根本原因。
这不是"调参玄学",是可以算的。 条件数(最陡/最平的比值)越大,训练的困难就越大。
这就是 AI 在干的事
神经网络训练,本质就是:
- 定义一个损失函数(这个函数有多大,说明模型有多差)
- 算它的梯度(往哪个方向调参数会让损失下降)
- 沿反方向走一小步(学习率)
- 重复几百万次
就这四步。 没有别的。
而第 2 步"算梯度",在几十亿参数上怎么做?反向传播——本质是链式法则(微积分里最基本的那条规则)的高效实现。
所以: “AI 学会了"这件事,数学上就是"在几十亿维空间里,沿着梯度一直下坡,直到落进一个足够低的谷底”。 它不是"理解",是"下降"。
这一条价值很大——它同时解释了 AI 的能力和它的局限:
- 能力:任何能写成"最小化损失"的问题,都能用这套方法逼近
- 局限:它只能落进局部的谷底,不是全局最优;而且它不知道自己在算什么
六、五个"离散撞连续"的地方
这一篇讲的每一件事,都是"离散逼近连续"的具体案例。汇总一下:
| 你要的(连续) | 机器给的(离散) | 误差在哪 |
|---|---|---|
| 导数(极限) | 差商(h 有限) | 截断 O(h) + 舍入 O(ε/h) → U 形 |
| sin/exp(超越函数) | 多项式(有限项) | 展开阶数不够 → 必须先归约区间 |
| 最小值(精确) | 沿梯度迭代 | 学习率、条件数、局部最优 |
| 面积(积分) | 数值积分(有限采样) | 依赖被积函数的光滑度 |
| 曲线(连续形状) | 折线/贝塞尔(控制点) | 越少控制点越"棱角" |
每一行都是同一件事:把连续的东西切成离散的碎片,然后算清楚碎片带来的误差**。**
这就是"计算机数学"这四个字最实在的含义。 不是"数学在计算机里",是**“在离散的机器上,怎样把连续的数学算到够用”**。
七、把这一篇放回体系
七篇讲完,八层只剩最后一层:
| 层 | 状态 |
|---|---|
| 一 数怎么装进机器 | ✅ (一) |
| 二 离散结构 | ✅ (二) |
| 三 怎么保证对 | ✅ (三) |
| 四 什么能算 | ✅ (四) |
| 五 算得多快 | ✅ (五) |
| 六 不确定的世界 | ✅ (六) |
| 七 连续的近似 | ✅ 本篇:微积分 |
| 八 高维的表示(线代) | 下一篇,也是最后一篇 |
这一篇在整条线上的位置
第一篇文章说"数学连续、机器离散"。前六篇大多站在"离散"那一侧。这一篇是第一次面对面:
导数在数学里是一个极限,在机器里是一个除法。 这中间隔着一个 h,而 h 的取值不是自由选择——它被两个误差夹在中间。
而正是因为"自动计算导数"这条路走通了(反向传播),才有了今天的深度学习。上一篇讲概率(AI 的输出),这一篇讲微积分(AI 的学习)——两篇合起来,才是 AI 的全貌。
八、动手:六个实验
实验 1:亲眼看到"U 形误差曲线"
import math
def f(x): return math.sin(x)
真值 = math.cos(1.0)
print(f"真值 cos(1) = {真值:.17g}")
for k in range(1, 18):
h = 10**(-k)
误差 = abs((f(1.0+h)-f(1.0))/h - 真值)
print(f"h=1e-{k:<2d} 误差 {误差:.3e}")
你会看到误差从 4e-2 一路降到 3e-9,然后反弹回 5e-1。 亲眼见过一次,就再也不信"h 越小越准"了。
实验 2:前向 vs 中心
import math
def f(x): return math.sin(x)
t = math.cos(1.0)
for k in range(1, 13):
h = 10**(-k)
前 = abs((f(1+h)-f(1))/h - t)
中 = abs((f(1+h)-f(1-h))/(2*h) - t)
print(f"h=1e-{k:<2d} 前向 {前:.2e} 中心 {中:.2e}")
同样是 h,中心差商精度高 4~5 个数量级。 而代价只是多算一次 f。
实验 3:泰勒展开要几项
import math
def 泰勒sin(x, n):
return sum((-1)**k * x**(2*k+1) / math.factorial(2*k+1) for k in range(n))
print(f"sin(3) = {math.sin(3):.15f}")
for n in [1, 3, 5, 8, 10]:
print(f"{n:2d} 项: {泰勒sin(3.0, n):>18.12f} 误差 {abs(泰勒sin(3.0,n)-math.sin(3)):.2e}")
再试试 x=10,看 10 项为什么会崩。
实验 4:学习率决定生死
def 训练(学习率, 步数=50, 起点=(1.0, 1.0)):
x, y = 起点
for _ in range(步数):
x -= 学习率 * 2*x # f = x² + 10y²
y -= 学习率 * 20*y # 梯度
if abs(y) > 1e9: return "发散"
return f"x={x:.2e} y={y:.2e} f={x*x+10*y*y:.2e}"
for lr in [0.001, 0.01, 0.05, 0.09, 0.1, 0.11, 0.2]:
print(f"lr={lr:<6} {训练(lr)}")
盯着 lr=0.1 那一行:x 收敛了、y 卡住了。这就是"高条件数"的真实表现。
实验 5:算出临界学习率
# 更新式: y ← y(1 - lr*20),收敛要求 |1-20*lr| < 1
for lr in [0.09, 0.0999, 0.1, 0.1001, 0.11]:
factor = 1 - 20*lr
print(f"lr={lr:<8} 衰减因子 {factor:>8.4f} {'收敛' if abs(factor)<1 else '发散'}")
数字对上实测,你就能"算"出学习率了,不用试。
实验 6:梯度下降在真实损失函数上
import random
random.seed(1)
# 用梯度下降拟合 y = 3x + 2(线性回归)
xs = [i/10 for i in range(100)]
ys = [3*x + 2 + random.gauss(0, 0.1) for x in xs]
w, b, lr = 0.0, 0.0, 0.01
for _ in range(2000):
gw = sum((w*x + b - y)*x for x, y in zip(xs, ys)) / len(xs)
gb = sum((w*x + b - y) for x, y in zip(xs, ys)) / len(xs)
w -= lr*gw; b -= lr*gb
print(f"学出来: w={w:.3f} b={b:.3f} (真值 3.0 和 2.0)")
这就是机器学习最小最小的一个例子——和 GPT 训练,只是规模不同,机制完全一样。
九、带走的东西
一句话:
计算机不会求导,它用除法假装——而这个"假装"的代价是可以算出来的:h 太大是截断误差,h 太小是舍入误差,最优 h 在两者相等处。
三句话:
- h 越小越准,是错的。 实测最优 h ≈ 1e-8(理论 √ε ≈ 1.5e-8),再小误差反而飙升,h=1e-16 时直接算成 0。两个误差方向相反,总误差是 U 形。
- 计算机里的 sin/exp/log 全是多项式拼的。 泰勒展开 + 区间归约。x=3 要 10 项,x=10 就要 20 项——所以真实实现必须先把 x 缩小。
- 梯度下降 = AI 学习的全部机制。 学习率有硬上界(实测 lr<0.1,由最陡方向决定),训练快慢由最平方向决定。条件数越大越难训,这是可以算的,不是玄学。
一个最该记住的判断
AI 不是"理解",是"下降"。 定义一个损失函数,算梯度,沿反方向走一小步,重复几百万次。 这个机制既解释了它的能力(任何能写成最小化的问题都能逼近),也解释了它的局限(只能落进局部谷底,不知道自己算的是什么)。
下一篇(最后一篇)
(八)高维的表示:线性代数。
从一个问题开始:怎么让机器知道"这两句话意思相近"? 答案是把每个词变成一个几百维的向量,然后用夹角衡量相似。
然后是矩阵、特征值、SVD——以及为什么"注意力机制"本质是矩阵乘法。
八层走完,最后我会写一篇总纲,把八层串成一条线。
深水区
- 微积分入门:任何一本《Calculus》教材(Stewart 最常见)。重点是导数定义、链式法则、泰勒展开。
- 数值分析(这一篇的正统学科):Burden & Faires《Numerical Analysis》。第 4 章讲数值微分,就是这篇第二节的严谨版。
- 优化(梯度下降的正统学科):Boyd & Vandenberghe《Convex Optimization》——免费 PDF,讲清了为什么梯度下降会收敛、什么时候不会。
- AI 视角:Goodfellow《Deep Learning》第 4 章数值计算、第 8 章优化。讲的就是这篇的东西,用在几十亿参数上。
这些我不做页码引用——不同版本页码不同,写了就是骗你。按书名和章节找。
附:本篇所有数字的来源
全部本机跑出来:Ubuntu / Python 3.8 / x86-64(IEEE 754 double)。
数值求导误差(f=sin, x=1, 真值 0.54030230586813977):
- 前向差商 h=1e-1 → 1e-8:误差 4.29e-2 → 2.97e-9;h=1e-9 → 1e-16:误差反弹到 5.25e-8 → 5.40e-1
- 最优 h = 1e-8,最小误差 2.97e-9(理论 √ε ≈ 1.5e-8)
- 中心差商最优 h ≈ 1e-5,最小误差 1.11e-11(理论 ε^(1/3) ≈ 6.0e-6)
- 机器 epsilon = 2.220446049250313e-16
泰勒展开:
- sin(3) = 0.141120008059867;1/2/3/4/5/6/8/10 项误差 2.86/1.64/3.84e-1/5.01e-2/4.19e-3/2.45e-4/3.54e-7/2.01e-10
- sin(10) = −0.5440211109;10 项误差 1.63(完全错);20 项误差 2.83e-9
梯度下降(f = x²+10y²,起点 (1,1),50 步):
- lr=0.001/0.01/0.05/0.09 → f = 2.15/1.33e-1/2.66e-5/4.44e-9
- lr=0.1 → x 收敛但 y 卡在 1.0(f=1.00e+1)
- lr=0.11 → 发散(y=9.10e+3);lr=0.2/0.5 → 发散
- 理论临界值 lr < 2/20 = 0.1,实测吻合
你自己机器上跑出来的数字会略有不同(平台浮点单元、Python 版本)。差一点不影响结论——结论是结构性的。