高中老师说"买菜用得上求根公式吗"——可这条公式,是整个大模型的祖宗

2026-06-25 · Steve Chan

上一篇《大学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.eignp.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 是它的多维推广。

为什么大模型不直接用 当激活函数,而要用 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         # 看"一堆小弯叠加" = 拟合任意曲线