上一篇:(六)不确定的世界——讲概率怎么驾驭不确定性。 这一篇:讲计算机怎么处理"连续"的东西——因为机器只会加减乘除,不会求导。

耗时:读完约 50 分钟,动手约 2 小时。


开头:机器不会求导,但它能"假装"会

第一篇文章说过一句话:

数学是连续的、无限的、精确的;计算机是离散的、有限的、近似的。 这两者之间的翻译工作,就是计算机数学。

前六篇讲的大部分是"离散"那一侧——格点、计数、图、证明、复杂度、概率。这一篇第一次正面处理"连续"。

因为微积分研究的对象是连续变化:速度、斜率、面积、曲线的弯曲。而计算机根本不知道什么是连续。它只有加减乘除、只有有限位。

那怎么办?

不怎么办——近似。而且近似得非常好。

这一篇要讲清三件事:

  1. 导数的数值算法,会让机器"自伤"——算得越精细,误差反而越大(这条最反直觉)
  2. 泰勒展开:用多项式当"万能替身",计算机里的 sin、exp、log 全是这么算的
  3. 梯度下降: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-10.4973637525354.29e-02
1e-20.5360859810124.22e-03
1e-30.5398814803604.21e-04
1e-40.5402602314194.21e-05
1e-50.5402980985064.21e-06
1e-60.5403018851214.21e-07
1e-70.5403022640404.18e-08
1e-80.5403023028982.97e-09 ← 最准
1e-90.5403023584095.25e-08 ← 变差了!
1e-100.5403022473875.85e-08
1e-110.5403011371641.17e-06
1e-120.5403455460854.32e-05
1e-130.5395683899687.34e-04
1e-140.5440092820663.71e-03
1e-150.5551115123131.48e-02
1e-160.0000000000005.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-14.29e-029.00e-04
1e-24.22e-039.01e-06
1e-34.21e-049.01e-08
1e-44.21e-059.00e-10 ← 最准
1e-54.21e-061.11e-11 ← 更准
1e-64.21e-072.77e-11
1e-74.18e-081.94e-10
1e-82.97e-092.58e-09
1e-95.25e-082.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:

项数逼近值误差
13.0000000000002.86e+00
2−1.5000000000001.64e+00
30.5250000000003.84e-01
40.0910714285715.01e-02
50.1453125000004.19e-03
60.1408745941562.45e-04
80.1411196543413.54e-07
100.1411200078592.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最终 yf 值状态
0.0019.05e-013.64e-012.15e+00太慢,50 步没走完
0.013.64e-011.43e-051.33e-01慢
0.055.15e-030.00e+002.66e-05接近
0.094.91e-051.43e-054.44e-09✅ 收敛
0.11.43e-051.00e+001.00e+01❌ y 方向卡住不降
0.114.03e-069.10e+038.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 在干的事

神经网络训练,本质就是:

  1. 定义一个损失函数(这个函数有多大,说明模型有多差)
  2. 算它的梯度(往哪个方向调参数会让损失下降)
  3. 沿反方向走一小步(学习率)
  4. 重复几百万次

就这四步。 没有别的。

而第 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 在两者相等处。

三句话:

  1. h 越小越准,是错的。 实测最优 h ≈ 1e-8(理论 √ε ≈ 1.5e-8),再小误差反而飙升,h=1e-16 时直接算成 0。两个误差方向相反,总误差是 U 形。
  2. 计算机里的 sin/exp/log 全是多项式拼的。 泰勒展开 + 区间归约。x=3 要 10 项,x=10 就要 20 项——所以真实实现必须先把 x 缩小。
  3. 梯度下降 = 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 版本)。差一点不影响结论——结论是结构性的。