为什么 AI、数学顶尖人才都绕不开数学分析?答案藏在微分方程里!
为什么很多人学了一辈子数学,却依然无法进入 AI 和现代科学的核心?因为真正重要的不是计算,而是数学分析训练出的抽象能力。微分方程把未知数从一个数字升级为函数,也让人类第一次拥有预测世界变化的数学语言。
"代数方程的未知数是数,微分方程的未知数是函数。" —— 一句话决定了 17 世纪之后整个分析学的走向。
写代码的人有一个天然优势可以拿来读懂微积分:高阶函数。微分方程之所以让中学生发懵,本质上是因为它的未知数不再是某个具体的数,而是一整条函数。这个跳跃,跟编程里"函数返回函数"是同一件事的两种说法。这一章和 Chapter 0、0.5、0.7 一起,构成本项目的"如何学"四块基石。
0.8.1 普通方程 vs 微分方程:求"数"还是求"函数"?
| 类型 | 例子 | 未知是什么 | 编程类比 |
|---|---|---|---|
| 代数方程 | \(x^2 - 5x + 6 = 0\) | 一个数 \(x\) | solve(...) 返回 float |
| 函数方程 | \(f(x+y) = f(x)+f(y)\) | 一条函数 \(f\) | solve(...) 返回 Callable |
| 微分方程 | \(\dfrac{dy}{dt} = -ky\) | 一条函数 \(y(t)\) | solve(...) 返回 Callable[[t], y] |
代数方程问的是 "哪个数让等式成立?" —— 答案是离散的几个根(一次方程 1 个,二次 2 个,五次 5 个)。 微分方程问的是 "哪条函数让等式成立?" —— 答案是一整条曲线,甚至是一个函数族,要靠初值条件从这个族里挑出唯一的那一条。
所以解一个微分方程,返回的不是数字,而是一个能在任意时刻被调用、给出系统状态的函数。这就是高阶编程里 Params → (t → State) 的形式:先吃下方程和初值,吐出一条函数,然后这条函数还要再被调用一次才给出具体的数。
# 代数方程:返回数
roots = np.roots([1, -5, 6]) # array([3., 2.])
# 微分方程:返回"函数"(数值上由 dense_output 的内插函数代理)
from scipy.integrate import solve_ivp
sol = solve_ivp(lambda t, y: -0.3 * y, (0, 10), [1.0], dense_output=True)
y = sol.sol # y 本身是一个函数:t → state
print(y(2.5)) # 像调用普通函数一样在任意时刻取值
print(y(7.1)) # 同一条解函数,问它任意时刻
同样这件事在 Clojure / Lisp 里更赤裸 —— Lisp 把"函数即值"作为一等公民,"返回函数"是默认动作而不是特例:
;; 代数方程:返回数(这里 roots 用任意数值库;概念是 Vector → Vector)
(roots [1 -5 6]) ; → [3.0 2.0]
;; 微分方程:solve 返回的就是 fn
(defn solve-ode [f y0 t-end dt]
(let [trajectory (vec (reductions ; reductions = scan:留下每一步快照
(fn [y t] (+ y (* (f t y) dt)))
y0
(range 0 t-end dt)))]
(fn [t] (nth trajectory (int (/ t dt)))))) ; ← 返回值是一个 fn
(def y (solve-ode (fn [t y] (* -0.3 y)) 1.0 10.0 0.01))
(y 2.5) ; 像普通函数一样调用
(y 7.1) ; 同一条解函数,任意时刻取值
一句话:代数世界里
solve返回float;微分世界里solve返回Callable/fn。 微积分把数学从"标量编程"升级到了"高阶编程" —— Lisp 程序员对此本能地熟悉。
0.8.2 ODE vs PDE:独立变量数的差异
承认了"未知是函数"以后,区分常微分方程(ODE, Ordinary Differential Equation)和偏微分方程(PDE, Partial Differential Equation)就只剩一个问题:这条未知函数有几个自变量?
| 类型 | 自变量数 | 例子 | 物理场景 | 编程签名 |
|---|---|---|---|---|
| ODE | 1 个(通常是时间 \(t\)) | \(\dfrac{dy}{dt} = -ky\) | 放射性衰变、单摆、RC 电路、银行复利 | y: t → State |
| PDE | ≥ 2 个(时间 + 空间) | \(\dfrac{\partial u}{\partial t} = \alpha\,\dfrac{\partial^2 u}{\partial x^2}\) | 蒸汽机金属杆散热(傅里叶 1807)、波传播、薛定谔方程 | u: (t, x) → State |
两套求导符号也忠实地反映了这件事:
- \(\dfrac{d}{dt}\)(直体 \(d\))—— 函数只有一个变量,"全导数",没什么好"偏"的。
- \(\dfrac{\partial}{\partial t}\)(卷曲 \(\partial\))—— 函数有多个变量,固定其他变量、只对 \(t\) 求导,所以叫"偏导数"。Python 里这正好是 functools.partial 干的事。
# ODE:状态在每个时刻是一个标量(或低维向量)
y = torch.zeros(N_steps) # shape: (T,)
# PDE:状态在每个时刻本身就是"整条空间上的函数"
u = torch.zeros(N_steps, N_grid) # shape: (T, X)
在 Clojure 里,"偏导符号 \(\partial / \partial t\)" 几乎是字面对应到 partial —— 两者都在做同一件事:冻住其他参数,把多元函数降成一元函数:
;; ODE:解是一元函数 t → state
(def y (fn [t] ...))
;; PDE:解是多元函数 (t, x) → state
(def u (fn [t x] ...))
;; 数学上的 ∂/∂t 就是"先 partial 再 D":
;; 1) 用 partial 把 x 冻住 ——把多元函数压成一元函数
;; 2) 再用 Chapter 0.9 的 D 算子求一元导数
(defn partial-t [u x0]
(D (fn [t] (u t x0)))) ; ∂u/∂t |_(·,x₀)
;; 同样可以冻住 t,研究"某个时刻的空间形状"
(def u-at-t0 (partial u 0.0)) ; 只剩 x → state,是一条空间曲线
PDE 比 ODE 难,是因为系统的"状态"在每个时刻不再是一个数,而是整个空间上的一条函数。求解 PDE 的本质是:让一条函数随时间演化成另一条函数 —— 这是"返回函数"的"返回函数",签名约等于 (x → State) → (x → State)。所以热方程、波动方程、薛定谔方程才会一上来就显得吓人 —— 它们是高阶函数的高阶函数。
0.8.3 "求解留给下一个" = 积分 = for 循环
中学课本喜欢把"解微分方程"包装成查表 / 配技巧 / 套公式,这其实在掩盖最关键的事实:绝大多数微分方程没有解析解,它们全部靠"把答案留给下一步"的递推来求解。
微分方程只告诉你变化率(\(\dot{y}\)),不直接给你状态(\(y\))。要拿到状态,就必须把变化率沿时间轴累加起来 —— 这就是积分,骨子里是一个 for 循环:
# 欧拉法:最朴素的"求解留给下一个"
y = y0
for t in range(N_steps):
dy = f(t, y) * dt # 方程告诉你:当前的变化率 × 一小步时间
y = y + dy # 留给下一步的,就是更新后的 y
history.append(y)
这段 Python for 循环换到 Clojure 里就消失成一行 reduce —— 因为 Lisp 把"沿序列累积"作为语言原语:
;; 欧拉法 = reduce —— 一行就是"积分"
(reduce
(fn [y t] (+ y (* (f t y) dt))) ; 一步更新规则(= 方程)
y0 ; 初值
(range 0 t-end dt)) ; 时间序列
;; → 末态 y(t-end)
;; reductions 留下每一步的快照(欧拉法的完整轨迹)
(reductions
(fn [y t] (+ y (* (f t y) dt)))
y0
(range 0 t-end dt))
;; → (y₀ y₁ y₂ … yₙ) —— 整条数值解函数
这就是为什么 Chapter 0.9 会把"积分 = reduce"当成口号 —— 微积分基本定理在 Lisp 里就是 reduce 和 D 互为左右逆。
这段代码(无论 Python 还是 Clojure)就是牛顿 1666 年在伍尔索普庄园里干的事的现代翻译。所谓"求解留给下一个",就是 y = y + dy 这一行 —— 当前状态决定下一刻状态,下一刻再决定再下一刻,无穷递推。在编程世界里这叫 fold / scan / unfold(Clojure 里就是 reduce / reductions);在数学世界里这叫积分;在物理世界里它就是因果律本身。
更现代的求解器(Runge-Kutta、辛积分器、神经 ODE)只是把 y + dy 这一步换成更精巧的更新规则,骨架还是同一个 reduce。编程里你已经熟悉的那个迭代循环,就是积分。
0.8.4 编程概念 ↔ 微分方程概念字典
| 通用编程世界 | Clojure / Lisp | 微分方程世界 |
|---|---|---|
高阶函数 A → (B → C) |
(fn [a] (fn [b] ...)) |
解算子 初值 → 解函数 y(t) |
Callable 作为返回值 |
fn 直接返回 fn |
解一个 ODE / PDE |
for 循环累加 |
reduce |
数值积分(欧拉、RK4 的末态) |
fold / scan |
reductions |
完整解轨迹 \(\{y_0, y_1, \dots\}\) |
| 闭包(捕获参数) | Lisp 一等闭包 | 含参数的解族 \(y(t;\,k, y_0)\) |
| 递归 / 不动点 | recur / Y 组合子 |
隐式格式、Picard 迭代 |
| 流(Stream)/ 生成器 | lazy-seq / iterate |
连续动力系统 |
functools.partial |
partial |
偏导数 \(\partial u / \partial t\)(冻住其他变量) |
函数复合 f ∘ g |
comp |
链式法则、流形上的拉回 |
多维张量 Tensor[T, X, Y] |
core.matrix / Neanderthal |
PDE 的离散化解 \(u(t, x, y)\) |
反向传播 loss.backward() |
autograd 库 | 伴随方程(adjoint equation) |
最后一行特别值得标记:深度学习的反向传播本质上是在解一条反向的 ODE(伴随方程)—— 神经 ODE(Chen et al. 2018)把这件事公开化了。Chapter 7 会把这条线接上。
0.8.5 可视化与代码
- 可视化:
- 一个 ODE 的"解族"动画:固定方程、变化初值,看到一整族函数曲线如何被同一个方程"挑出来"
- ODE vs PDE 并排:ODE 的解是一条曲线 \(y(t)\),PDE 的解是一张随时间演化的曲面 \(u(t,x)\)
- 欧拉法 vs RK4 vs 解析解的精度对比,看"求解留给下一个"如何被一步步精化
- 把
for t in steps: y = y + f(y)*dt这段循环和loss.backward()并排,揭示二者的同构 - 代码:
ch00_8_diffeq_as_higher_order/ solve_returns_function.py—— 用scipy.solve_ivp(dense_output=True)演示"返回值就是一个 callable"solver_returns_fn.clj—— 同一件事的 Clojure 版:solve-ode直接返回fn,把"函数返回函数"变成默认而非特例euler_as_for_loop.py—— 把欧拉法写成 10 行 for 循环,标注每一行对应数学里的什么euler_as_reduce.clj—— 用一行(reduce ...)写出欧拉法,对照 Python for 循环看它如何"消失"ode_vs_pde.py—— RC 衰变(ODE)和 1D 热传导(PDE)的并排数值仿真solution_family.py—— 同一方程不同初值的解族,看初值条件如何"挑出"唯一解partial_as_functools.py—— 用functools.partial类比偏导数:冻住其他变量后剩下的 1D 函数partial_as_partial.clj—— Clojure 的partial与数学符号 \(\partial\) 的字面对应adjoint_as_backprop.py—— 用torchdiffeq演示伴随方程 = 反向传播
元教学意义:本章不教任何新公式,它教如何用编程的眼睛看微分方程。读完后再看 Chapter 5(傅里叶 / 热方程)和 Chapter 7(神经 ODE / 扩散模型),它们其实在用同一句话描述自己:"我返回的是一个函数。"