高中老师说"买菜用得上求根公式吗"——可这条公式,是整个大模型的祖宗
上一篇《大学4年没讲明白的线性代数,被一段 PyTorch 代码讲透了》, 讲的是"线性代数是什么"。很多人留言问得更狠:这些东西到底是怎么一步步被逼出来的?
这篇就从根上讲——从初中的
y = ax + b、高中的求根公式开始, 一路讲到矩阵、行列式、特征值,最后落到一段大模型的代码里。起因是一句很多人都听过的话。高中老师一脸不屑:
"你买菜会用求根公式吗?"
言下之意:数学里有用的就是加减乘除,剩下的求根公式、矩阵、行列式都是考试用完就扔的废物。
这篇文章想替数学翻个案:求根公式恰恰是人类第一次意识到"有些方程能有公式"; 而整个线性代数、整个大模型,恰恰诞生于一个更深刻的发现——"有些方程没有公式"。 买菜确实用不上,但你手机里那个能聊天的大模型,骨子里全是它的子孙。
0. 一句话主线
如果只能留一句话:
一部代数史,就是"方程越来越解不动,于是人类发明了越来越强的工具"的历史。 一次方程一步解出 → 二次方程要套公式 → 多个方程要用矩阵 → 高次方程干脆没有公式,只能一步步逼近。 大模型,就站在这条链的最末端:它没有"一个公式解出最优权重",只能用梯度下降一步步爬。
这条链,七站:
| 站 | 方程 / 对象 | 逼出来的工具 | 在大模型里是什么 |
|---|---|---|---|
| ① | 一次 y = ax + b |
直接解 | nn.Linear、线性回归 |
| ② | 二次 ax²+bx+c=0 |
求根公式 / 配方法 → 二次型 | MSE、注意力分数、协方差 |
| ③ | 多元一次方程组 | 行列式 → 矩阵 | 权重矩阵、Ax=b |
| ④ | 高次(≥5 次) | 没有求根公式 → 迭代逼近 | 梯度下降(训练的本体) |
| ⑤ | 特征方程 det(A-λI)=0 |
特征值(③④合流) | PCA、谱、为什么只能迭代 |
| ⑥ | 曲线的次数 | 高次曲线 → 一堆"小弯"叠加 | sigmoid/tanh/ReLU、通用逼近 |
| ⑦ | 数列 / 递推 | 递推=矩阵幂,通项=特征值 | RNN、序列模型、梯度爆炸/消失 |
下面一站一站走,每站都有能跑的代码。
1. 第一站:一次方程 y = ax + b —— 简单到"一步解出",于是有了 nn.Linear
最开始,方程长这样(图里那位朋友的原话):
y = sx + b (s 是斜率 slope)
y = ax + b
要"解"它(求 ax + b = 0 的根),一步到位:
import torch
a, b = torch.tensor(2.0), torch.tensor(-6.0)
x = -b / a # 求根:移项、两边除以 a
print("根 x =", x.item()) # 3.0 —— 一步解出,没有任何悬念
一次方程的世界里,没有"解不出"这回事。也正因为简单,它成了机器学习的起点——
你在 PyTorch 里写的第一行 nn.Linear,本质就是一堆 y = ax + b 并排放在一起:
import torch
import torch.nn as nn
x = torch.tensor([[2.0, 3.0]]) # 一个样本,两个特征
layer = nn.Linear(2, 1) # 就是 y = w1·x1 + w2·x2 + b
W, b = layer.weight, layer.bias
print(torch.allclose(layer(x), x @ W.t() + b)) # True —— 还是那个 ax+b
nn.Linear 不神秘,它就是初中那条直线方程,升级成了向量版。
但只靠直线,你拟合不了任何弯曲的东西。世界是弯的,于是人类被迫往下走一站。
2. 第二站:二次方程与求根公式 —— "配方法"埋下了大模型里到处都是的"二次型"
二次方程 ax² + bx + c = 0,初中生都背得出那条求根公式:
import torch
def solve_quadratic(a, b, c):
disc = b * b - 4 * a * c # 判别式:决定有没有实根
sqrt_disc = torch.sqrt(torch.abs(disc))
if disc >= 0:
return ((-b + sqrt_disc) / (2 * a), (-b - sqrt_disc) / (2 * a))
return None # 判别式 < 0,曲线碰不到 x 轴
a, b, c = torch.tensor(1.0), torch.tensor(-5.0), torch.tensor(6.0)
print("两个根:", solve_quadratic(a, b, c)) # (3., 2.)
老师说"买菜用不上"——对,买菜确实用不上。但求根公式是怎么推出来的,才是真正值钱的东西。
它靠的是一招叫配方法(completing the square):把 ax²+bx+c 硬凑成"一个平方 + 一个常数"。
import torch
# y = x² - 4x + 5,配方:x²-4x+5 = (x-2)² + 1
x = torch.linspace(-1, 5, 7)
y_raw = x ** 2 - 4 * x + 5
y_done = (x - 2) ** 2 + 1
print(torch.allclose(y_raw, y_done)) # True —— 配方法只是换个写法
# 写成 (x-2)²+1 后,一眼看出:碗底在 x=2,最小值是 1
配方法做的事,就是把抛物线写成"碗"的标准形——一眼看出碗底(最小值)在哪。 而"求最小值"正是机器学习训练的全部目标(最小化 loss)。这不是巧合:
- 你写的
MSELoss,(pred - y)²,是个二次函数 → 它的图像就是一口碗 → 配方法找碗底 = 找最优解。 - 把它升到高维(向量版的平方),就是二次型
xᵀ A x——
import torch
x = torch.tensor([1.0, 2.0])
A = torch.tensor([[2.0, 0.5],
[0.5, 1.0]])
quad_form = x @ A @ x # xᵀ A x,"向量版的 ax²"
print("二次型的值:", quad_form.item()) # 一个数
这个 xᵀ A x 是深度学习里出镜率最高的形状之一:MSE 损失、L2 正则、协方差矩阵、
甚至注意力里的 Q·Kᵀ(每个词向量两两相乘),骨子里都是"二次"的影子。
先看清这三条曲线,再往下走—— 跑一下第一个可视化脚本:
python from_equation_to_matrix_visualization.py
四个子图对应这篇文章的四站: - ① 一次(直线)、二次(抛物线/碗)、三次(躺着的 S)画在一起, 你会亲眼看到"次数越高,曲线能弯的道数越多":一次 0 个弯,二次 1 个弯,三次 2 个弯(含一个拐点)。 - ② 把
x²-4x+5配方成(x-2)²+1,黑点标出碗底(2,1)——这就是 loss 最小值的几何原型。 - ③ 把三次曲线和 sigmoid / tanh 叠在一起,它们都有"拐点"、都呈 S 形(第 6 节细讲)。 - ④ 一个 3D 的碗,就是二次型xᵀ A x,也就是深度学习里那个 loss 地形——碗底就是最优权重。
判别式 b²-4ac 也别小看,它回答的是一个深刻的问题:这条抛物线到底碰不碰得到 x 轴(有没有解)?
import torch
for c in [4.0, 4.0, 6.0]: # 固定 a=1,b=-4,改 c
disc = (-4.0) ** 2 - 4 * 1.0 * c
print(f"c={c}: 判别式={disc:+.1f} ->",
"两个根" if disc > 0 else ("一个根" if disc == 0 else "无实根(碰不到x轴)"))
"方程有没有解"这件事,在二次还能用一个判别式说清楚。但越往高次走,"有没有解、能不能用公式解出来"就越来越难—— 这正是把人类推向矩阵、推向迭代法的那只手。 先解决"多个方程",再解决"高次方程"。
3. 第三站:从"一个方程"到"一堆方程" —— 线性代数真正的出生证明
现实里,未知数往往不止一个。两个未知数、两个方程:
2x + y = 5
x + 3y = 10
初中教你"消元法":把一个方程乘个系数,加到另一个上,消掉一个未知数。 但当方程有几十、几百个时,人脑消元就崩了。于是数学家做了一件事:把系数单独拎出来,排成一个方块——这就是矩阵。
import torch
A = torch.tensor([[2.0, 1.0], # 系数排成方块 = 矩阵
[1.0, 3.0]])
b = torch.tensor([5.0, 10.0]) # 右边的常数排成一列 = 向量
xy = torch.linalg.solve(A, b) # 一行解出整个方程组
print("x, y =", xy) # [1., 3.]
行列式:比矩阵更早出生的那个概念
有意思的是,行列式(determinant)比矩阵出现得还早。 17 世纪末, 日本的关孝和(1683)和德国的莱布尼茨(1693)几乎同时、独立地发现: 解线性方程组时,反复出现一个由系数交叉相乘、相减得到的数。1750 年克莱姆(Cramer) 把它整理成著名的克莱姆法则——用行列式直接写出方程组的解:
import torch
A = torch.tensor([[2.0, 1.0],
[1.0, 3.0]])
b = torch.tensor([5.0, 10.0])
D = torch.det(A) # 系数行列式
Dx = torch.det(torch.tensor([[5.0, 1.0], [10.0, 3.0]])) # 把第1列换成 b
Dy = torch.det(torch.tensor([[2.0, 5.0], [1.0, 10.0]])) # 把第2列换成 b
print("克莱姆法则:", (Dx / D).item(), (Dy / D).item()) # 1.0 3.0
print("和 solve 一致:", torch.linalg.solve(A, b)) # [1., 3.]
克莱姆法则里那个分母 D = det(A) 藏着一个要命的问题:如果 det(A) = 0,就除以 0 了——方程组没有唯一解。
几何上,det = 0 意味着两条直线平行或重合:
import torch
A = torch.tensor([[1.0, 2.0],
[2.0, 4.0]]) # 第二行正好是第一行的 2 倍
print("行列式:", torch.det(A).item()) # 0.0 —— 两个方程其实是同一条线,解不唯一
这正是上一篇讲的"
det = 0= 把空间拍扁、信息丢失、不可逆"的代数来源。 行列式最初不是为了算面积,而是为了回答"这个方程组到底有没有唯一解"。
矩阵这个词,1850 年才出现
真正把"矩阵"当成一个能加减乘、有自己代数的对象来研究的,是 19 世纪的
西尔维斯特(Sylvester,1850 年造出 "matrix" 这个词)和凯莱(Cayley,1858 年建立矩阵代数)。
在此之前,矩阵只是"方程组系数的速记表"。凯莱让它升级成了一台能作用在向量上的机器——
也就是上一篇反复说的"矩阵就是映射"。而高斯(Gauss)在做天文测量时打磨出的高斯消元法,
至今仍是 torch.linalg.solve 内部在做的事。
到这里,线性代数的核心零件——方程组、消元、行列式、矩阵——全部因为"要同时解一堆一次方程"而诞生。
而这堆零件,正是 nn.Linear 的本体:
import torch
import torch.nn as nn
layer = nn.Linear(3, 4)
x = torch.randn(2, 3)
# 前向传播 layer(x) 干的就是 "Ax+b":一次解出 4 个一次方程的值
print(torch.allclose(layer(x), x @ layer.weight.t() + layer.bias)) # True
每过一个
nn.Linear,就是让数据穿过一组Ax + b。一个 GPT 里有成百上千个这样的矩阵, 它们全是第三站这套"解一堆一次方程"工具的直系后代。
🤔 插一句:所以"线性代数"到底"线性"在哪?
很多人学完线代都没想过这个名字。"线性"就是"一次"——line(直线)。 线性代数研究的,全是一次的东西:一次方程、一次方程组、一次变换(矩阵映射)。它有两条铁律:
import torch
M = torch.tensor([[2.0, 1.0], [0.0, 3.0]])
u, v = torch.tensor([1.0, 0.0]), torch.tensor([0.0, 1.0])
# 铁律一:可加性 M(u+v) = M u + M v
print(torch.allclose(M @ (u + v), M @ u + M @ v)) # True
# 铁律二:齐次性 M(3u) = 3 (M u)
print(torch.allclose(M @ (3 * u), 3 * (M @ u))) # True
满足这两条的,才叫"线性"。而真实世界几乎没有纯线性的东西—— 所以大模型必须在一堆线性层(矩阵)中间,插进非线性的激活函数(第 6 站)。 线性代数提供"骨架",非线性提供"弯度"。这就是为什么这篇文章既要讲矩阵,也要讲二次、三次曲线。
4. 第四站:高次方程的滑铁卢 —— 没有求根公式,只能"一步步逼近"
现在回到图里那句最关键的话:
"矩阵的历史。高次方程没有求根公式,才引生出来。"
这句话点到了数学史上最震撼的一个转折,我们把它讲透。
二次有求根公式(古巴比伦人就会配方)。那三次、四次呢?
- 三次方程:16 世纪意大利人 del Ferro、Tartaglia,最后由卡尔达诺(Cardano,《大术》1545)公布了求根公式。
- 四次方程:卡尔达诺的学生费拉里(Ferrari)也解出来了,有公式。
于是所有人都相信:五次、六次……一定也有公式,只是还没找到。整整找了快 300 年。
直到 1824 年,挪威天才阿贝尔(Abel)证明了一件让人头皮发麻的事,几年后伽罗瓦(Galois) 用全新的"群论"给出了彻底的解释:
五次及以上的一般方程,不存在用加减乘除和开根号写出来的求根公式(阿贝尔-鲁菲尼定理)。
注意:不是"还没找到",而是数学上证明了"永远找不到"。用代码感受一下这种"解不出公式"的方程:
import numpy as np
# x⁵ - x - 1 = 0,一个没有根式解的五次方程
coeffs = [1, 0, 0, 0, -1, -1] # x⁵ + 0x⁴ + 0x³ + 0x² - x - 1
roots = np.roots(coeffs) # 注意:numpy 不是"套公式",是数值算出来的
print("数值求出的根:\n", roots)
# 你拿不到一个像 (-b±√…)/2a 那样的封闭表达式,只能得到一串近似小数
没有公式,怎么办?答案就一个字:逼近。 不去求那个不存在的公式,而是从一个猜测出发, 一步步往真正的根靠近。最经典的就是牛顿法——用切线和 x 轴的交点当作下一个更好的猜测:
import torch
def p(x): return x ** 5 - x - 1 # 没有求根公式的五次方程
def dp(x): return 5 * x ** 4 - 1 # 它的导数(切线斜率)
x = torch.tensor(1.5) # 随便一个初始猜测
for step in range(6):
x = x - p(x) / dp(x) # 沿切线挪到 x 轴交点 = 下一个猜测
print(f"第{step+1}步: x={x.item():.6f} 误差={p(x).item():+.2e}")
# x 飞快收敛到 ≈1.1673,误差趋近 0 —— 公式没有,逼近照样到达
请记住这个"不求公式、只求逼近"的思想——它就是深度学习训练的全部哲学。 神经网络也没有"一个公式解出最优权重",它干的事和牛顿法一模一样:从随机权重出发, 看一眼坡度(梯度),往下挪一小步,再挪一小步:
import torch
def loss(w): return (w - 2.0) ** 2 + 1.0 # 碗形 loss,最优在 w=2
def grad(w): return 2 * (w - 2.0) # 坡度
w, lr = torch.tensor(-3.0), 0.2
for step in range(6):
w = w - lr * grad(w) # optimizer.step() 的本体
print(f"第{step+1}步: w={w.item():.4f} loss={loss(w).item():.4f}")
# w 一步步从 -3 爬向 2 —— 和牛顿法逼近根,是同一个动作
跑第二个可视化脚本,把"逼近"这件事并排看清楚:
python no_formula_so_iterate_visualization.py
左右两幅图同步播放,讲的是同一个思想的两副面孔: - 左:五次方程
x⁵-x-1=0没有求根公式。红点从初始猜测出发,每一步沿切线滑到 x 轴交点, 一步步逼近真正的根——你能看见切线一次次"指向"答案。 - 右:碗形 loss 上的梯度下降。红点从远处沿坡一步步滚向碗底(最优权重)。 - 两边是同构的:左边"没有求根公式 → 沿切线迭代",右边"没有最优权重公式 → 沿梯度迭代"。 这就是大模型训练为什么是一个for循环,而不是一行公式。历史的另一条岔路:伽罗瓦为解释"五次为何无公式"而发明的群论, 后来长成了整个抽象代数,也是现代密码学、物理对称性的根基。一个"解不出的方程",开出两朵花: 一朵是迭代逼近(数值分析 → 梯度下降),一朵是群论(抽象代数)。
5. 第五站:高次方程 × 矩阵 = 特征值 —— 两条历史线在这里合流
第三站(矩阵)和第四站(高次方程无解)看似是两条独立的线。但它们会在一个点上狠狠撞在一起——特征值。
上一篇讲过,特征向量是"矩阵映射里方向不变、只被拉伸的那几根轴",拉伸倍数就是特征值 λ。 怎么求 λ?教科书给的方程是:
det(A − λI) = 0
对一个 n × n 的矩阵,把这个行列式展开,会得到一个关于 λ 的 n 次多项式(叫特征多项式)。
也就是说:求 n 阶矩阵的特征值 = 解一个 n 次方程。 用代码看它俩是一回事:
import numpy as np
A = np.array([[2.0, 1.0],
[1.0, 2.0]])
# 特征多项式 det(A-λI) = λ² - 4λ + 3 = 0,它的根就是特征值
char_poly = np.poly(A) # [1, -4, 3] 即 λ²-4λ+3
print("特征多项式系数:", char_poly)
print("解这个二次方程得特征值:", np.roots(char_poly)) # [3., 1.]
print("直接求特征值:", np.linalg.eigvals(A)) # [3., 1.] 一致
现在把第四站的炸弹接上:如果矩阵是 5×5 或更大,特征多项式就是 5 次或更高次——根据阿贝尔-鲁菲尼定理,它没有求根公式!
import numpy as np
np.random.seed(0)
A = np.random.randn(6, 6)
A = A + A.T # 6×6 对称矩阵
vals = np.linalg.eigvals(A)
print("6 阶矩阵的特征值(只能数值迭代算出来):\n", np.sort(vals))
# np.linalg.eigvals 内部用的是 QR 迭代算法,不是套公式 —— 因为根本没有公式可套
这件事的分量怎么强调都不过分:
torch.linalg.eig、np.linalg.eigvals这些函数,内部不可能是"套一个公式", 而必然是"一步步迭代逼近"(QR 算法、幂迭代等)。原因就是第四站那条定理: 5 次以上的方程没有求根公式。 矩阵越大,越只能靠迭代。
这也从根上解释了大模型为什么长这样:
- 大模型的权重,本质是在解一个巨大的、高度非线性的优化问题。
- 非线性方程组比"高次方程"还难,更加没有求根公式。
- 所以唯一的出路,就是第四站那个古老的智慧:从一个猜测出发,一步步逼近——这就是梯度下降、就是
optimizer.step()、就是训练。
图里那句"高次方程没有求根公式,才引生出矩阵",更精确的版本是: "高次方程无解"逼出了'迭代逼近'这套方法论,而矩阵的特征值问题又把矩阵和高次方程焊死在一起—— 最后这套'没有公式就迭代'的智慧,成了整个深度学习训练的灵魂。
6. 第六站:曲线的"次数"= 模型的"弯度" —— 从 x²、x³ 到激活函数
回到图里最有画面感的一句:
"二次 x² 是曲线,三次 x³ 是 softmax 的躺形状。"
这句话的直觉非常准。我们把它讲准确。方程的次数,决定了曲线能"弯"几道:
import torch
x = torch.linspace(-2, 2, 5)
print("一次 x :", (1.0 * x).tolist()) # 直的,0 个弯
print("二次 x² :", (x ** 2).tolist()) # 1 个弯(一口碗)
print("三次 x³ :", (x ** 3).tolist()) # 2 个弯,中间有个'拐点',呈躺着的 S
- 一次:直线,弯度为 0。
- 二次 x²:一口碗(抛物线),有 1 个弯——这是 loss 的形状。
- 三次 x³:有一个拐点(inflection point),曲线先朝下凹、过原点后朝上凸,
整体是一条躺着的 S。这个 S 形,正是
sigmoid/tanh的形状直觉,而softmax是它的多维推广。
为什么大模型不直接用 x³ 当激活函数,而要用 sigmoid/tanh/softmax?
因为高次多项式虽然能弯,但脾气太暴——稍微往两边走一点就冲到天上去(数值爆炸),还会剧烈震荡。
而 sigmoid/tanh 是"被驯服过的 S":两头平滑地压在 [0,1] 或 [-1,1] 里,温柔可控:
import torch
x = torch.linspace(-4, 4, 5)
print("三次 x³ (两头爆炸):", (x ** 3).tolist()) # [-64, -8, 0, 8, 64]
print("tanh (两头压平):", torch.tanh(x).round(decimals=3).tolist())
print("sigmoid(两头压平):", torch.sigmoid(x).round(decimals=3).tolist())
第一个可视化脚本的子图 ③ 把三者画在一起:三次曲线、sigmoid、tanh 都有那个"拐点"、都呈 S 形, 区别只在于 sigmoid/tanh 把两端温柔地压平了。所以你朋友说"三次是 softmax 的躺形状"——直觉完全对: 激活函数就是被驯服、被压平两端的高次曲线。
这也接上了上一篇和《从线性到非线性》的核心结论:
纯线性(一次)的层叠多少层还是直线(第三站那两条铁律保证的)。 要让模型"弯",必须插入非线性。而最朴素的"弯",就是二次、三次这些高次曲线给的灵感—— 工程上选了 sigmoid/tanh/ReLU 这些好用的版本,但思想源头,是"次数越高越能弯"。
6.1 为什么非要"能弯的函数"?因为一个弯是积木,弯+弯能拼出任意曲线
光说"要弯"还不够,得说清怎么用弯拼出复杂曲线。先看清一件事:一个 sigmoid / tanh / ReLU,就是"一个弯"。
import torch
x = torch.linspace(-4, 4, 9)
print("一个 tanh = 一道拐弯:", torch.tanh(x).round(decimals=2).tolist())
print("一个 ReLU = 一个折点:", torch.relu(x).tolist()) # 在 0 处折一下
单独一个弯,啥复杂曲线都拟合不了。但关键在于:每个弯可以被"平移、缩放、翻转"——
a · tanh(w·x + b) 里,w 控制弯多急、b 控制弯在哪、a 控制弯多高/朝哪。
把许多个位置不同、高矮不同、朝向不同的弯加起来,就能拼出任意形状。手动叠两个弯,先拼出一个"鼓包":
import torch
x = torch.linspace(-5, 5, 11)
# 一个朝上的弯 + 一个朝下的弯,错开位置叠加 = 一个局部鼓包
bump = torch.tanh(x + 2) - torch.tanh(x - 2)
print("两个弯叠出一个鼓包:", bump.round(decimals=2).tolist())
# 一个鼓包就是一块"局部砖头",砖头摆够多,任意曲线都能砌出来
这就是通用逼近定理(Universal Approximation) 的全部直觉:
线性层(第一/三站的 Wx+b)负责决定"每个弯放在哪、多急、多高",激活函数负责"真的弯下去",
再把一堆弯加权求和,就能逼近任何曲线。 跑第三个可视化脚本,亲眼看弯怎么拼成曲线:
python why_we_need_bends_visualization.py
- 上图:一个训练好的小网络(
Linear→tanh→Linear)里,10 个隐藏神经元各自贡献"一个弯"—— 你会看到 10 条位置、高矮、朝向都不同的 tanh 曲线。- 下图:把这 10 个弯加权求和,蓝线精准贴合一条又扭又绕的目标曲线
sin(2x)+0.5sin(5x); 作为对照,红色虚线是"纯线性(一次)的最佳拟合"——一根直线,一道弯都弯不出来,彻底躺平。这张图就是"为什么必须有 sigmoid/tanh/ReLU"最直接的回答:没有弯,模型就只是第一站那根直线。
用代码把"弯的叠加"写成最小训练循环,10 个弯就能拟合一条扭曲的曲线:
import torch
import torch.nn as nn
x = torch.linspace(-3, 3, 200).unsqueeze(1)
y = torch.sin(2 * x) + 0.5 * torch.sin(5 * x) # 又扭又绕的目标
net = nn.Sequential(nn.Linear(1, 10), nn.Tanh(), nn.Linear(10, 1)) # 10 个弯
opt = torch.optim.Adam(net.parameters(), lr=0.02)
for _ in range(3000):
loss = ((net(x) - y) ** 2).mean()
opt.zero_grad(); loss.backward(); opt.step()
print("10 个弯加权求和后的 loss:", round(loss.item(), 4)) # ≈ 0.001,拟合得很好
6.2 "复合函数"= 网络的深度:弯之上再弯
上面是"一层很多弯"(宽)。还有一条路是"复合多次"(深)——把弯过的结果再喂进下一层去弯:
import torch
import torch.nn as nn
# 复合:f3( 弯 f2( 弯 f1(x) ) ),每层在上一层已经弯过的基础上再弯一次
deep = nn.Sequential(
nn.Linear(1, 8), nn.Tanh(), # 第一次弯
nn.Linear(8, 8), nn.Tanh(), # 在已弯的基础上再弯
nn.Linear(8, 1), # 求和输出
)
x = torch.tensor([[1.5]])
print("复合函数 f∘g∘h 的输出:", deep(x).item())
数学上这就是复合函数 f(g(h(x))):高中学过的"内层函数 → 外层函数",
在神经网络里就是"一层叠一层"。浅而宽(一层很多弯)和深而窄(多层复合)都能逼近复杂函数,
但深的复合更省参数、更能表达"弯里套弯"的层次结构——这正是"深度"学习里"深度"二字的由来。
一句话收束第六站:二次、三次告诉我们"次数越高越能弯"; 而神经网络换了个更聪明的办法——不用一个高次多项式,而是把无数个"温柔的小弯"(sigmoid/tanh/ReLU) 加起来(宽)、叠起来(深),照样能弯出任意复杂的曲线,还不会像高次多项式那样数值爆炸。
7. 第七站:数列、递推与通项 —— "一步步往前推",其实就是矩阵幂
前六站讲的都是"函数"(给个 x 算个 y)。但还有一类东西从小学就缠着我们:数列——
1, 1, 2, 3, 5, 8, … 这种一个接一个的数。它和线性代数、和大模型的关系,深得吓人。
数列有两种写法,对应两种世界观:
- 通项公式:
a(n) = 直接用 n 算出来。例如等差数列a(n) = a1 + (n-1)d,一步到位(像第一站的"直接解")。 - 递推公式:
a(n) = 用前面几项算出来。例如斐波那契f(n) = f(n-1) + f(n-2),必须一步步往前推(像第四站的"迭代")。
# 斐波那契:递推公式,一步步往前推
f = [1, 1]
for n in range(2, 10):
f.append(f[n - 1] + f[n - 2]) # 每一项 = 前两项之和
print("递推算出的数列:", f) # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
7.1 递推 = 矩阵幂:把"往前推一步"打包成一次矩阵乘法
这里出现整篇文章最漂亮的一次"代数→线性代数"穿越。一个线性递推,可以原样写成一次矩阵乘法—— 把"当前状态"装进一个向量,"往前推一步"就是"乘一个矩阵":
import torch
# 状态向量 [f(n), f(n-1)],往前推一步 = 乘这个"转移矩阵"
M = torch.tensor([[1.0, 1.0], # 新的 f(n+1) = 1·f(n) + 1·f(n-1)
[1.0, 0.0]]) # 新的 f(n) = 1·f(n) + 0·f(n-1)
s = torch.tensor([1.0, 1.0]) # 初始 [f(1), f(0)]
for _ in range(8):
s = M @ s # 往前推一步 = 一次矩阵乘法
print("用矩阵推出的:", s.tolist()) # [55., 34.] —— 和上面递推完全一致
于是"推 n 步"就等于"矩阵的 n 次幂"——递推公式 = 矩阵幂 Mⁿ:
import torch
M = torch.tensor([[1.0, 1.0], [1.0, 0.0]])
Mn = torch.linalg.matrix_power(M, 9) # M 的 9 次方
s = Mn @ torch.tensor([1.0, 1.0])
print("一步到位 Mⁿ:", s.tolist()) # 同样推到斐波那契后面的项
7.2 从递推回到通项公式:靠的正是第五站的特征值
那"递推公式"能不能变回"通项公式"(一步算出第 n 项,不用循环)?能——而钥匙就是第五站的特征值。
把转移矩阵 M 对角化(求特征值),就能把 Mⁿ 写成"特征值的 n 次方",从而得到闭式通项:
import torch
M = torch.tensor([[1.0, 1.0], [1.0, 0.0]])
vals = torch.linalg.eigvalsh(M)
print("转移矩阵的特征值:", vals.tolist()) # [-0.618, 1.618]
# 1.618 正是黄金比例 φ = (1+√5)/2 !
那个 1.618 正是黄金比例 φ。斐波那契的通项公式(比内公式 Binet)f(n) = (φⁿ - ψⁿ)/√5,
里面的 φ、ψ 就是这两个特征值。而 φ 满足的方程 x² = x + 1,正是这个递推的特征方程——
它和第五站矩阵的特征多项式 det(M-λI)=0 是同一个东西。
两条历史线在这里又一次合流:数列的"特征方程" 和 矩阵的"特征多项式" 本就是一回事。 这也意味着:如果递推依赖前 5 项以上,特征方程就是 5 次以上——根据第四站的阿贝尔-鲁菲尼定理, 它的通项公式可能根本写不出来,只能老老实实一步步递推(迭代)。 数列也逃不过那条定理。
7.3 多项式求和:另一种"闭式 vs 累加"
你小学就背过 1+2+…+n = n(n+1)/2。这就是"累加(一步步加)"被压缩成了"闭式公式(一步算出)"——
和"递推 → 通项"是同一种愿望。验证一下:
import torch
n = 100
loop_sum = sum(range(1, n + 1)) # 一步步累加
formula = n * (n + 1) // 2 # 闭式公式,一步算出
print(loop_sum, formula) # 5050 5050 —— 一致
# 平方和也有闭式:1²+2²+…+n² = n(n+1)(2n+1)/6
print(sum(i*i for i in range(1, n+1)), n*(n+1)*(2*n+1)//6) # 一致
能压成闭式公式的,是少数幸运儿(低次、规整);大多数复杂的累加/递推,压不出公式,只能迭代。 这又一次呼应了全文主线:有公式是例外,没公式靠迭代才是常态。
7.4 递推 = RNN:大模型处理序列的老祖宗
最后把数列接到大模型上。"用前面的项推下一项"——这不就是语言模型在干的事吗? 给定前面的词,预测下一个词。最早的序列模型 RNN,状态更新公式几乎和斐波那契的矩阵递推一模一样:
import torch
import torch.nn as nn
# RNN:h(t) = tanh(W·h(t-1) + U·x(t)),一个带"弯"的递推(第六站的非线性 + 第七站的递推)
rnn = nn.RNNCell(input_size=4, hidden_size=8)
h = torch.zeros(8) # 初始状态
seq = torch.randn(5, 4) # 一个长度 5 的序列
for t in range(5):
h = rnn(seq[t], h) # 每读一个词,往前推一步(= 数列的递推)
print("读完整个序列后的状态:", h.shape) # torch.Size([8])
而第 7.2 节那个"特征值决定数列爆炸还是收敛"的结论,在这里有个有名的工程后果:
如果递推矩阵的特征值 |λ|>1,状态会随步数指数爆炸;|λ|<1 则指数衰减到 0——
这正是 RNN 训练里臭名昭著的梯度爆炸 / 梯度消失问题。后来的 LSTM、乃至 Transformer 用注意力
一步直连所有历史(而非一步步递推),就是为了绕开这个"特征值次方"带来的灾难。
一句话收束第七站:递推公式 = 矩阵幂,通项公式 = 对角化(特征值的次方); 数列的"特征方程"和矩阵的"特征多项式"是同一个东西;而"用前面推后面"这个最朴素的数列思想, 正是 RNN、乃至整个序列大模型的老祖宗——连它的老毛病(梯度爆炸/消失)都是特征值次方写在脸上的。
8. 终点:把整条历史,缝进一段大模型的代码
现在把整条链全部接起来。下面是一段极简但完整的注意力前向传播——大模型最核心的一块。 每一行都标注了它来自历史的哪一站:
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
tokens = torch.tensor([3, 7, 1]) # 三个离散的词 id
# ── 第一站之前:把离散符号变成连续向量(embedding,见上一篇"离散变可导")
emb = nn.Embedding(num_embeddings=100, embedding_dim=8)
x = emb(tokens) # (3, 8)
# ── 第①③站:一次方程 / 矩阵映射 Ax+b,把词向量投影成 Q、K、V
Wq, Wk, Wv = nn.Linear(8, 8), nn.Linear(8, 8), nn.Linear(8, 8)
Q, K, V = Wq(x), Wk(x), Wv(x) # 每个都是一组 "Ax+b"(第三站)
# ── 第②站:二次型 —— 每个词向量两两相乘,本质是 xᵀ(WqᵀWk)x 那类"二次"结构
scores = Q @ K.t() # (3,3) 相关度矩阵,向量版的"二次"
# ── 第⑥站:非线性曲线 —— softmax 是高次 S 形曲线的多维版,把分数压成概率
attn = F.softmax(scores, dim=-1) # 两头压平、归一化(第六站的"躺 S")
# ── 又一次第③站:矩阵映射,按相关度加权求和
out = attn @ V # (3, 8)
print("注意力输出形状:", out.shape)
# ── 第④⑤站:上面所有权重(Wq/Wk/Wv/emb)都没有"求根公式"可解,
# 只能用梯度下降一步步逼近 —— 这正是 abel-ruffini 那条定理的精神后裔
loss = out.pow(2).mean() # 随便一个二次 loss(又是第二站的"碗")
loss.backward() # 算坡度
print("emb 拿到的梯度形状:", emb.weight.grad.shape) # 准备沿坡挪一步(牛顿法的子孙)
这段几十行的代码,把整部代数史走了一遍:
离散符号变向量 → 矩阵映射(Ax+b,第①③站)→ 向量两两相乘的二次结构(第②站)→
softmax 这条躺 S 曲线(第⑥站)→ 最后用梯度下降迭代训练(因为没有求根公式,第④⑤站)。
9. 把整条链缝起来
回到开头那句"买菜用得上求根公式吗"。现在可以正面回答了:
| 历史这一站 | 当年解决的问题 | 留下的工具 | 今天在大模型里 |
|---|---|---|---|
| 一次方程 | 一个未知数 | 直接解 x=-b/a |
nn.Linear、线性回归(第 1 节) |
| 二次方程 | 弯一下的曲线 | 求根公式 / 配方法 → 二次型 | MSE、注意力分数、loss 碗(第 2、8 节) |
| 多元一次方程组 | 一堆未知数 | 行列式 → 矩阵 | 权重矩阵、Ax+b(第 3 节) |
| 高次方程(≥5) | 解不出来了 | 承认没有公式 → 迭代逼近 | 梯度下降、optimizer.step()(第 4 节) |
| 特征方程 | 矩阵的骨架方向 | 特征值(前两条合流) | PCA、谱、为什么只能迭代算(第 5 节) |
| 曲线的次数 | 世界是弯的 | 高次曲线 → 驯服成 S 形 | sigmoid/tanh/softmax 激活(第 6 节) |
| 弯的组合 | 直线弯不动 | 一堆"小弯"加权叠加/复合 | 通用逼近、网络的宽与深(第 6.1、6.2 节) |
| 数列 / 递推 | 一步步往前推 | 递推=矩阵幂,通项=特征值 | RNN、序列模型、梯度爆炸/消失(第 7 节) |
三句话总结这篇文章:
代数史是一部"方程越来越解不动"的历史:一次一步解出,二次要套公式,多元要用矩阵, 高次干脆没有公式(第 1–4 节)。
大模型站在这条链的最末端:它要解的优化问题比高次方程还难,没有任何"求根公式", 只能继承那个最古老的智慧——从一个猜测出发,沿着坡度一步步逼近(梯度下降,第 4、5 节); 而"把无数小弯叠起来逼近任意曲线"(第 6 节)、"用前面推后面"(第 7 节),是它另外两件看家本领。
而矩阵、二次型、激活函数、递推这些零件,全是被一个个"解不动的方程"逼出来的副产品—— 它们最后全都活在一段能跑起来的大模型代码里(第 8 节)。
所以下次再有人说"求根公式买菜用不上",你可以告诉他: 买菜确实用不上,但正是"有些方程能有公式、有些方程没有公式"这个分界, 催生了线性代数,催生了迭代逼近,最终催生了你手机里那个会聊天的大模型。 把上面每段代码都跑一遍,再跑两个可视化脚本,你就把这条三百年的链,亲手走了一遍。
python from_equation_to_matrix_visualization.py # 看"次数→弯度"和 loss 碗
python no_formula_so_iterate_visualization.py # 看"没有公式就迭代" = 梯度下降
python why_we_need_bends_visualization.py # 看"一堆小弯叠加" = 拟合任意曲线