万能函数模拟器:大模型就是用微积分从数据里"积分"出来的函数 P
问一个最朴素的问题:GPT 到底是什么?
不同视角给出不同的答案——序列模型、Transformer、自回归生成器、压缩器、超级搜索引擎、可微分程序、神经-符号混合系统……这些都对,但最底层的那张图只有一张:
GPT 是一个用微积分(可微分编程)从海量离散文本数据里"反推"出来的函数 \(P(y \mid x)\)。
这里的"反推"两个字是字面意思——和 17 世纪牛顿用观测数据反推行星轨道、20 世纪气象台用观测数据反推大气方程是同一件事,只不过这次反推的不是连续轨迹,而是定义在词表上的条件概率分布族。
本文从"万能函数模拟器(Universal Function Simulator)"这个最古老的视角切入,把"为什么神经网络能模拟任何函数"、"为什么必须是微积分"、"离散文本怎么被一个函数捕获"、"矩阵和概率分别充当什么角色"——这四件事钉死在
model.py的具体行号上。读完之后,再看forward()(model.py:177-198),就只剩一句话:一段 124M 参数的函数 \(P_\theta\),在所有上下文 \(x\) 上输出下一个 token 的概率分布——这个 \(P_\theta\) 是被几十万次梯度下降"积分"出来的万能函数近似。
目录
- 一句话拼图:大模型 = 函数 P + 微积分反推
- 万能函数模拟器:通用近似定理的工程含义
- 万能函数思想的家谱:泰勒、傅立叶、神经网络
- 微积分的力量:从"算变化"到"反推原函数"
- 离散文本如何变成"找函数 P"的问题
- 可微分编程:把"找函数"变成"跑梯度下降"
- 矩阵就是参数化的万能函数:一行 nn.Linear 的全部秘密
- 概率视角:在概率单纯形上"积分"出一族函数
- 万能函数 P 的反推机器:cross-entropy + autograd + SGD
- CodeGPT 就是一个 124M 维参数空间里的万能函数实例
- 万能函数 P 的边界:能模拟什么,模拟不了什么
- 与本项目其他文档的联系
1. 一句话拼图:大模型 = 函数 P + 微积分反推
先把全文的结论摆出来。model.py:177-198 这段 forward:
# model.py:177
def forward(self, idx, targets=None):
...
tok_emb = self.transformer.wte(idx) # 离散 token id → 连续向量
pos_emb = self.transformer.wpe(pos)
x = self.transformer.drop(tok_emb + pos_emb)
for block in self.transformer.h: # 12 层 Block 复合映射
x = block(x)
x = self.transformer.ln_f(x)
logits = self.lm_head(x) # 连续向量 → 词表 logits
loss = F.cross_entropy(logits.view(-1, logits.size(-1)),
targets.view(-1), ignore_index=-1)
把它当一个函数来读:
P_θ : (token id 序列 x) ────────► (词表上的概率分布 P(y | x))
└────────────── 124M 参数 θ ──────────────┘
- \(x\) 是离散符号(token id 的序列);
- \(P_\theta(\cdot \mid x)\) 是一个长度 50304 的概率向量(参见 PROBABILITY_AS_AREA_MATRIX_AS_MAP.md §3);
- \(\theta\) 是被训练出来的 124M 个实数参数。
整个 GPT 训练干的事就一句话:
给定海量观测样本 \(\{(x_i, y_i)\}\),找一组参数 \(\theta\),让 \(P_\theta(y \mid x)\) 尽量贴住背后那个真实存在但未知的 \(P^*(y \mid x)\)。
"找参数 \(\theta\)" 这件事在数学上叫函数逼近,工程上叫训练。它的可行性靠两条腿撑着:
- 万能函数模拟器:足够宽 + 足够深的可微分程序可以逼近任意一个我们想要的函数 \(P^*\)(§2、§3)。
- 微积分:靠
loss.backward()自动求导 + SGD 一小步一小步走,从一个随机初始化的 \(\theta_0\) "积分"到一个好用的 \(\theta^*\)(§4、§6、§9)。
剩下所有节都是把这十几行代码展开来讲:万能函数模拟器为什么能成立、微积分凭什么搞定离散语言、矩阵和概率分别是这台机器里的哪个零件。
2. 万能函数模拟器:通用近似定理的工程含义
2.1 一个"过于强"的数学事实
1989 年,Cybenko 和 Hornik 各自证明了通用近似定理(Universal Approximation Theorem):
给定任意一个连续函数 \(f : \mathbb{R}^n \to \mathbb{R}^m\) 和任意精度 \(\varepsilon > 0\),存在一个单隐层前馈网络,使得它和 \(f\) 在任意紧集上的差距处处小于 \(\varepsilon\)。
翻译成工程语言:只要你给我足够多的神经元,我可以拟合任何连续函数到任意精度。
这是个看起来"过于强"的结论——它没有限制 \(f\) 是线性的、不要求 \(f\) 解析、不要求 \(f\) 有任何特殊结构。听起来像一台万能函数模拟器。
2.2 二维玩具版:50 行代码就能感受到
不必信教科书的证明,自己跑一次就能信。我们让一个单隐层 MLP 拟合 \(\sin(3x) + 0.3 \cos(7x)\) 这种带高频抖动的曲线:
import torch, torch.nn as nn
import torch.nn.functional as F
x = torch.linspace(-3, 3, 200).unsqueeze(1) # (200, 1)
y = torch.sin(3*x) + 0.3*torch.cos(7*x) # 目标:一条很抖的曲线
# 一个最简单的万能函数模拟器:单隐层 MLP
class TinyApproximator(nn.Module):
def __init__(self, hidden=128):
super().__init__()
self.fc1 = nn.Linear(1, hidden)
self.fc2 = nn.Linear(hidden, 1)
def forward(self, x):
return self.fc2(torch.tanh(self.fc1(x)))
net = TinyApproximator(hidden=128)
opt = torch.optim.Adam(net.parameters(), lr=1e-2)
for _ in range(2000):
opt.zero_grad()
loss = F.mse_loss(net(x), y) # ← 唯一的"目标"
loss.backward() # ← 唯一的"反推规则"
opt.step()
print(loss.item()) # ≈ 1e-4,曲线已经被压住
把 net(x) 和 y 画到一起,两条曲线几乎重合。没有人告诉网络这是 sin 和 cos,没有写任何三角公式——只是一段两行 nn.Linear 的可微分程序 + 一个 MSE loss + 反向传播。
这就是"万能函数模拟器"在最朴素意义下的样子:一段带 \(128\) 个可调旋钮的可微分程序,在被 2000 步梯度下降"扭"完之后,就成了 \(\sin(3x) + 0.3\cos(7x)\) 的良好近似。
2.3 三层升级:从拟合 sin 到拟合 \(P(y \mid x)\)
把上面那段代码做三处升级,就一路升到 CodeGPT:
| 升级 | TinyApproximator | CodeGPT |
|---|---|---|
| 输入维度 | 1 维实数 x |
1024 维 token id 序列(block_size=1024) |
| 输出对象 | 1 维实数 y |
50304 维概率向量(vocab_size=50304) |
| 中间层 | 单隐层 128 维 + tanh | 12 层 Block × 768 维 + GELU + Attention |
| 损失函数 | F.mse_loss(连续值用 L2) |
F.cross_entropy(离散类别) |
| 参数量 | ~400 个 | ~124M 个 |
但结构完全一致:一段可微分程序 + 一个 loss + 反向传播。区别只在维度大小和 loss 选哪种。
大模型的全部"魔法"——通用近似定理决定了它"理论上能做到",剩下的全是工程上的"实际让它做到"。
2.4 "存在"和"找得到"是两回事
通用近似定理有一个常被忽略的细节:它只说存在这样一个网络,没说怎么找到。
可以打个比方:定理告诉你"这片森林里一定有一棵能让你穿越的大树"——但没告诉你它长在哪、有多远、要砍掉多少藤蔓才到得了。
这就是为什么"万能函数模拟器"必须配上"微积分"才能跑起来——微积分告诉我们怎么从随机的 \(\theta_0\) 一步步走到那棵大树。这是 §4 的主题。
但在跳到微积分之前,还有一件事必须先讲清楚:"用一组简单函数搭出任意复杂函数"这个想法,不是 1989 年才被发明的。它是一条三百年的家谱——泰勒、傅立叶、小波、神经网络——同一主题在不同基函数上的反复回响。下一节先把这条家谱拉直,再回到微积分。
3. 万能函数思想的家谱:泰勒、傅立叶、神经网络
通用近似定理(UAT)只是"万能函数"思想链条的最新一环;前面还有泰勒级数、傅立叶级数、Weierstrass 逼近定理、小波分析。它们共享一个统一框架,只在"基函数选什么"和"系数怎么定"这两件事上有差异。把这条家谱拉直,神经网络的"承"与"新"就一目了然——它承接了三百年来"用简单基搭复杂函数"的传统,新意在于基函数本身能从数据学出来。
3.1 通用近似不是 1989 年才有的
数学史上有一条贯穿三百年的主线:
| 年代 | 谁 | 想法 | 用什么"基" |
|---|---|---|---|
| 1715 | Taylor | 任何光滑函数可以用多项式逼近 | \(\{1, x, x^2, x^3, \ldots\}\) |
| 1807 | Fourier | 任何周期信号可以用 sin/cos 叠加 | \(\{1, \sin nx, \cos nx\}\) |
| 1885 | Weierstrass | 闭区间上连续函数可以用多项式一致逼近 | \(\{1, x, x^2, \ldots\}\)(稠密性) |
| 1936 | Kolmogorov | \(n\) 元连续函数 = 一元函数嵌套 | 一元函数(为 UAT 埋伏笔) |
| 1989 | Cybenko / Hornik | 单隐层神经网络逼近任意连续函数 | \(\{\sigma(w^\top x + b)\}\)(可学习) |
| 1990s | Daubechies / Mallat | 小波分析:局部 + 多尺度逼近 | \(\{\psi_{j,k}(x)\}\)(自相似基) |
每一行都在说同一句话:"给一组简单基函数,任意复杂函数可以表示为它们的(无穷)线性组合"。但每一行选了不同的基——多项式、三角、神经元、小波。下面三节先看最老的两位玩家。
3.2 泰勒级数:多项式作为万能基
牛顿 / 莱布尼茨发明微积分之后,Taylor 在 1715 年给出第一种"万能函数"的写法:任何光滑函数 \(f\) 在 \(a\) 点附近,可以展开成
翻译成代码——用前 \(N\) 项的泰勒展开逼近 \(\sin x\):
import torch, math
def taylor_sin(x, N=10):
"""sin 的泰勒展开:x - x^3/3! + x^5/5! - x^7/7! + ..."""
result = torch.zeros_like(x)
for n in range(N):
sign = (-1)**n
result = result + sign * x**(2*n+1) / math.factorial(2*n+1)
return result
x = torch.linspace(-3, 3, 100)
y_true = torch.sin(x)
y_approx = taylor_sin(x, N=10)
print((y_true - y_approx).abs().max()) # ≈ 1e-5,几乎完美
注意几件事:
- 基函数预先定死:\(\{1, x, x^2, x^3, \ldots\}\) 这套多项式基,Taylor 没"学"出来,是数学规定好的;
- 系数 \(\frac{f^{(n)}(a)}{n!}\) 也是分析地算出来的——对 \(f\) 求 \(n\) 阶导,代入 \(a\);
- 局部展开:展开点 \(a\) 越远,逼近越差。
这是"万能函数"思想的第一个工程化样本——但它要求 \(f\) 必须已知且光滑,所以只能"算"不能"学"。
3.3 傅立叶级数:sin/cos 作为万能基
Fourier 在 1807 年提出更大胆的主张:任何周期信号都可以写成 sin 和 cos 的(无穷)叠加。这就是"一切信号都可以用 sin/cos 组合变形表示"——音频、图像、通信、量子力学的底层语言两百年来都建立在这一句话上。
用代码逼近一个方波看:
import torch, math
def fourier_square(x, N=20):
"""方波的傅立叶级数:sin(x) + sin(3x)/3 + sin(5x)/5 + ..."""
result = torch.zeros_like(x)
for n in range(1, 2*N, 2): # 只取奇数次谐波
result = result + torch.sin(n * x) / n
return result * (4 / math.pi)
x = torch.linspace(-math.pi, math.pi, 1000)
y = fourier_square(x, N=20)
# 把 y 画出来:已经是一个清晰的方波(只是边角有 Gibbs 振荡)
只用 20 个 sin 项,就能把方波这种"不连续、有跳跃"的奇形怪状信号近似得相当像样。连方波这种带跳跃的函数都能被光滑的 sin/cos 叠加出来——这是 Fourier 当年震惊整个数学界的发现。
把它推广到非周期信号就是傅立叶变换——在"时域"和"频域"之间互译:
import torch
signal = torch.sin(2*math.pi*5*torch.linspace(0,1,256)) \
+ 0.5*torch.sin(2*math.pi*12*torch.linspace(0,1,256))
spectrum = torch.fft.fft(signal) # 时域 → 频域:能看到 5Hz、12Hz 两个峰
recovered = torch.fft.ifft(spectrum) # 频域 → 时域:精确还原
任何信号 = 一族 sin/cos 的加权和——这是历史上第一个真正"万能"的信号模拟器。但和泰勒一样有限制:
- 基函数预先定死:\(\{\sin nx, \cos nx\}\) 这套基不是学出来的;
- 系数靠数学公式算:\(a_n = \frac{1}{\pi}\int f(x)\cos nx \, dx\);
- 基是"全局"的:每个 sin 占满整个 x 轴,信号的局部突变要靠很多高频项压住(就是 Gibbs 振荡的来源)。
第三点正是小波分析后来要补的——基函数应该"既看大尺度、又看小尺度",于是有了 \(\psi_{j,k}(x) = 2^{j/2}\psi(2^j x - k)\) 的多尺度自相似设计。但小波依然属于"基函数预先定死"的家族。
3.4 统一框架:万能函数 = 基 × 系数 的线性组合
把泰勒、傅立叶、小波、神经网络放到一张表上,结构惊人地一致:
| 万能函数 | 基函数 \(\phi_i(x)\) | 系数 \(c_i\) | 系数从哪来 |
|---|---|---|---|
| 泰勒级数 | \((x-a)^n / n!\) | \(f^{(n)}(a)\) | 解析求导 |
| 傅立叶级数 | \(\sin nx, \cos nx\) | \(a_n, b_n\) | 积分公式 |
| 小波分解 | \(\psi_{j,k}(x)\) | \(\langle f, \psi_{j,k}\rangle\) | 内积 |
| 单隐层 MLP | \(\sigma(w_i^\top x + b_i)\) | 输出层权重 \(W_{2,i}\) | 梯度下降学 |
所有万能函数都长成这个样子:
——线性组合,系数加权,基函数堆叠。"万能性"来自基函数的完备性(complete basis):基足够多,组合空间就足够大,能逼近的函数族就足够广。
写成 PyTorch,单隐层 MLP 的 forward 一行代码就是这个公式:
# §2 那段 TinyApproximator 的本质
def forward(x):
phi = torch.tanh(W1 @ x + b1) # N 个基函数 σ(w_i^T x + b_i) 同时算出来
return W2 @ phi # 加权求和:Σ c_i * φ_i(x)
W1 的每一行就是一个 \(w_i\)——决定了"第 \(i\) 个基函数长什么样";W2 那一行就是那一组系数 \(c_i\)。§2 那段拟合 \(\sin(3x) + 0.3\cos(7x)\) 的代码做的正是这件事——用 128 个 \(\tanh\) 基函数 + 128 个学出来的系数,逼近一条带高频抖动的曲线。
3.5 神经网络的关键飞跃:基函数本身被学出来
把上表最右一列再看一眼——前三行的系数都是用分析/积分算出来的,神经网络是用梯度下降学的。但这只是表面差异。更深的差异在基函数本身:
| 基函数 \(\phi_i\) | 是否可调 | |
|---|---|---|
| 泰勒 | \((x-a)^n\) | 死(无可调参数) |
| 傅立叶 | \(\sin nx, \cos nx\) | 死(频率 \(n\) 是固定整数) |
| 小波 | \(\psi_{j,k}(x) = 2^{j/2}\psi(2^j x - k)\) | 死(母小波 \(\psi\) 选定后只能平移缩放) |
| MLP | \(\sigma(w^\top x + b)\) | 活(\(w, b\) 自己学) |
这才是 1989 年 Cybenko/Hornik 的真正突破——基函数不再预先定死,而是和系数一起从数据里学出来。
直觉上的差别:
- 泰勒/傅立叶:像给你一套通用积木(每种形状预先做好),让你拼一个具体形状。形状越复杂,需要的积木越多;
- 神经网络:像给你一团橡皮泥——你可以现场捏出最适合手头任务的"积木形状",再用少量这种定制积木拼。
代码上看,这个"学基函数"的能力来自每一行 nn.Linear:
# model.py:36
self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias)
# 这里的 W 是可学习的——也就是"基函数 σ(W·x + b) 的 W 是 trainable"
每个 nn.Linear 都在为下一层的非线性激活准备"基函数的形状"。梯度下降每跑一步,都在调整这些 \(w_i\),让基函数族更适合当前任务。
这就是为什么神经网络比固定基万能函数更高效:
- 用傅立叶逼近 GitHub 上的 Python 代码——需要海量 sin/cos 项,因为 sin/cos 对"代码结构"完全不敏感;
- 用神经网络逼近同样的东西——基函数会自动演化成"代码 token 模式"、"括号嵌套"、"变量定义结构"这种任务相关的形状,效率高几个数量级。
§4(微积分反推) 把"基函数怎么被学"这件事的机制讲清楚——靠 loss.backward() 沿链式法则把误差信号传到每个 \(w_i\) 上,梯度下降按这个信号扭基函数。
3.6 现代回响:sin/cos 在 Transformer 里依然在工作
值得指出的是——Fourier 的 sin/cos 思想并没有被神经网络淘汰,反而在 Transformer 的关键位置上回归:
(a) 原版 Transformer 的位置编码就是 Fourier 基
2017 年 Vaswani 那篇 Transformer 论文里,位置编码不是学出来的,而是写死的 sin/cos:
import torch, math
def sinusoidal_pos_emb(T, d_model):
"""原版 Transformer 位置编码:用 d/2 个频率的 sin/cos 表示位置"""
pos = torch.arange(T).unsqueeze(1)
i = torch.arange(d_model // 2).unsqueeze(0)
angle = pos / (10000 ** (2*i / d_model))
return torch.cat([torch.sin(angle), torch.cos(angle)], dim=-1)
这就是 §3.3 那个傅立叶基的字面应用——把"序列位置"这个一维变量,展开成 \(d/2\) 个不同频率的 sin/cos。"位置"的所有有用特征(相邻、远距、周期模式)都藏在这些频率分量里。
CodeGPT 用的是可学习的位置编码 wpe = nn.Embedding(block_size, n_embd)(model.py:146),把 sin/cos 那一套换成自由参数——这是基函数从"死"到"活"的又一次替代。
(b) RoPE:让 sin/cos 直接旋转 Q 和 K
2021 年提出的 RoPE(Rotary Position Embedding)——本项目训练报告 docs/training-report-2026-03-21/ 里的 autoresearch 升级就采用了它——更进一步,把 sin/cos 嵌进 attention 内部:让 Q 和 K 在不同位置上以不同角度旋转。
import math, torch
def rope_pair(q_a, q_b, m, theta):
"""把 (q_a, q_b) 看作复平面上的向量,按角度 m·θ 旋转"""
c, s = math.cos(m*theta), math.sin(m*theta)
return q_a*c - q_b*s, q_a*s + q_b*c
这是 Fourier 思想做位置感知的极致写法——sin/cos 的旋转性质直接服务于"相对位置 = 两点旋转角差"这种几何直觉。点积 \(Q_m \cdot K_n\) 自动只依赖 \(m-n\) 这个相对距离,而不依赖绝对位置 \(m, n\)。
(c) Attention 本质上就是一种"在学出来的基上展开"
更宏观地看——attention 里 softmax(QK^T) @ V 的过程,Q 在 K 群里找匹配,按相似度加权 V——也可以理解为"在一组学出来的基(K 们)上展开 V"。这和 Fourier 把信号展开到频域、wavelet 把信号展开到时频域,精神上是同一件事:
| 基函数 | 展开系数 | 输出 | |
|---|---|---|---|
| Fourier | \(\sin nx, \cos nx\)(死的) | 积分得到的 \(a_n, b_n\) | \(\sum a_n \cos nx + b_n \sin nx\) |
| Wavelet | \(\psi_{j,k}\)(死的) | \(\langle f, \psi_{j,k}\rangle\) | \(\sum \langle f, \psi_{j,k}\rangle \psi_{j,k}\) |
| Attention | 每个位置的 K(活的) | \(\text{softmax}(QK^\top)\) | \(\sum \text{softmax}(QK^\top)_i \cdot V_i\) |
attention 不是"什么神奇的新机制",它就是学版本的 Fourier 展开——基(K)和系数(\(\text{softmax}(QK^\top)\))都从数据学(参见 GPT_AS_SUPER_SEARCH.md)。
(d) 一句话总结这一节
从泰勒到傅立叶到神经网络,"万能函数模拟器"是同一个主题的三种基。神经网络没有取代前两者——它在 sin/cos 这样的经典基不够用时,学一组任务相关的基接管;但在位置编码、attention、信号建模等场景里,Fourier 那套思想依然在 Transformer 内部静静工作。
理解了这条家谱,你就能看清:LLM 不是凭空出现的"AI 神迹"——它是数学家三百年来"用简单基函数搭复杂函数"传统的最新继承者,只是这次基函数自己能动了。
4. 微积分的力量:从"算变化"到"反推原函数"
DIFFERENTIABLE_PROGRAMMING.md §2 已经把微积分的来历讲过了:牛顿 / 莱布尼茨在 17 世纪发明微积分,让人类第一次能描述"瞬时变化"和"反推原函数"。本节只补充一个角度——为什么"反推原函数"恰好是大模型训练的精神原型。
3.1 微积分的两半:导数 + 积分
# 导数:已知位置 x(t),求每一时刻的速度 dx/dt
import torch
t = torch.linspace(0, 10, 1000, requires_grad=True)
x = torch.sin(t) # 位置
v = torch.autograd.grad(x.sum(), t)[0] # 速度 = dx/dt = cos(t)
# 积分:已知速度 v(t),反推原函数 x(t)
dt = (t[1] - t[0]).item()
x_reconstructed = torch.cumsum(v * dt, dim=0) # 积分恢复原函数
导数:把变化率算出来。 积分:从变化率反推回原函数。
牛顿-莱布尼茨基本定理告诉我们这两半是互逆的。这个对偶后来反复出现:
- 物理里:力 ↔ 位移(牛顿第二定律 \(F = m \ddot{x}\))
- 控制论里:误差 ↔ 控制信号
- ML 里:梯度 ↔ 参数更新
- GPT 里:loss 的梯度 ↔ 整个 \(P_\theta\) 的形状
3.2 "反推原函数"在科学史上的样板
牛顿的样板任务是:观测到的是一颗行星每晚的位置(采样点),背后存在一条连续轨迹 \(\vec r(t)\)(原函数)——能不能从前者反推后者?
牛顿的答案:写下 \(\vec F = m \ddot{\vec r}\) + 万有引力定律 → 解一个二阶微分方程。一旦给出初始条件,行星明晚的位置就由方程预测——人类第一次能用数学预测未来。
到了 20 世纪气象学:观测到的是上千个气象站的离散读数,背后存在大气的连续场 \(T(x,y,z,t)\)、\(p(x,y,z,t)\)、\(\vec v(x,y,z,t)\)(原函数族)——用 Navier-Stokes 方程 + 数值离散化反推。今天打开手机看明天的天气,就是这个反推流程在算。
到了 21 世纪大模型:观测到的是几万亿 token 的离散文本(采样点),背后存在某个真实的语言条件分布 \(P^*(y \mid x)\)(原函数)——用什么方程反推?
没有 Navier-Stokes 这么干净的方程。所以我们写一段神经网络当"通用方程模板"——这就是万能函数模拟器登场的理由。
天体物理里方程是人类发现的;天气方程是物理学家在 18-19 世纪推出来的;语言"方程"我们目前没找到——但通用近似定理保证:总有一个足够大的神经网络能近似它。剩下的就是让微积分把那个网络的参数"积分"出来。
3.3 三个"反推"的并列结构
| 反推任务 | 观测(采样点) | 待反推的原函数 | 反推机制 |
|---|---|---|---|
| 行星轨道 | 每晚天文观测 | 连续轨迹 \(\vec r(t)\) | \(\vec F = m \ddot{\vec r}\) + 万有引力定律 + 积分 |
| 明天天气 | 上千气象站读数 | 大气场 \(T, p, \vec v\) | Navier-Stokes + 网格离散化 + 数值积分 |
| 下一个 token | 几万亿 token 训练语料 | \(P^*(y \mid x)\) | 神经网络 + cross-entropy + SGD |
注意第三行的最后一格——它和前两行没有本质区别。SGD 就是大模型时代的"数值积分":
- 每次
loss.backward()算出梯度,等价于物理里"算出此刻的力"; - 每次
optimizer.step()走一小步,等价于"按这个力把粒子推一点"; - 几十万步累积下来,参数 \(\theta\) 从随机起点"积分"到一个能让 loss 很小的点——这就是从离散观测里反推出原函数 \(P^*\) 的全部数学。
3.4 链式法则:autograd 的核心一行
把"反推"的引擎拆开看,里面只有一条规则——链式法则:
用代码看更直白:
import torch
x = torch.tensor(2.0)
theta = torch.tensor(3.0, requires_grad=True)
# 一段三层组合的"程序"
h = theta * x # h = θ·x ∂h/∂θ = x = 2
y = h ** 2 # y = h² ∂y/∂h = 2h = 12
L = (y - 10) ** 2 # L = (y-10)² ∂L/∂y = 2(y-10) = 52
L.backward()
print(theta.grad) # = 52 * 12 * 2 = 1248,正好是链式法则连乘
loss.backward() 不是魔法——它就是把上面三步沿前向计算图反向连乘。每个原子操作(乘法、平方、ReLU、矩阵乘、softmax)都在 PyTorch 里登记了它的导数规则,autograd 把这些规则按图组装起来,像莱布尼茨当年用 dy/dx 在纸上推符号一样自动化。
大模型里那个 124M 维梯度向量 \(\nabla_\theta L\),本质上就是链式法则的工业化产物。你每次写
loss.backward(),调用的是一台沿莱布尼茨道路造了 300 年的"符号求导机器"。
5. 离散文本如何变成"找函数 P"的问题
牛顿的微积分活在连续空间——位置、速度、温度都是实数。但 token 是离散符号,词表里的原子,没有"半个 token"这种东西。
那微积分凭什么管得了语言?答案藏在三层"翻译"里。
4.1 第一层翻译:离散选择 → 概率分布
设词表有 \(V = 50304\) 个 token(CodeGPT 的设置,见 tokenizer.py:VOCAB_SIZE)。给定上下文 \(x\),下一个 token \(y\) 是 \(V\) 个离散选项之一——这个选择本身是离散的。
但"在 V 个选项上的概率分布"是连续的——它是一个长度 \(V\)、各分量 \(\in [0,1]\)、总和为 1 的向量。这种向量住在一个叫概率单纯形(probability simplex)的连续流形上:
# 一个 V 维概率向量就是单纯形里的一个点
probs = torch.tensor([0.7, 0.2, 0.05, 0.05])
probs.sum() # tensor(1.0)
(probs >= 0).all() # tensor(True)
所以"找一个最优的下一个 token"是离散决策问题,而"找一个最优的概率分布"是连续优化问题——后者可以做微积分。
4.2 第二层翻译:离散 id → 连续向量
把第一层翻译落到代码里——具体怎么把 token id 这种整数喂给可微分程序?答案:nn.Embedding。
# model.py:145
self.transformer.wte = nn.Embedding(config.vocab_size, config.n_embd)
# 形状:(50304, 768)
# model.py:183
tok_emb = self.transformer.wte(idx) # 离散 id (B, T) → 连续向量 (B, T, 768)
wte 是一张 \((50304, 768)\) 的大表。每个 token id 对应表里的一行——一个 768 维实向量。整张表的所有元素都是 requires_grad=True 的实数参数,可以被梯度下降调整(详情见 PROBABILITY_AS_AREA_MATRIX_AS_MAP.md §8)。
所以离散性只出现在最开头(id 进表)和最末尾(采样出 token)两次。中间所有运算——embedding 查表、attention、MLP、softmax——都在连续空间里跑:
离散 id ──[wte]──► 连续向量 ──[12 个 Block]──► 连续向量 ──[lm_head]──► 连续 logits ──[softmax]──► 连续概率 ──[multinomial]──► 离散 id
↑ ↓
└────────────────────── 整段可微分,梯度能一路反传 ──────────────────────────────────────────────────────────────┘
这是离散语言能被微积分捕获的工程秘密:把离散性挤到两头,中间留出一个完全可微的"管道"。管道里的所有参数都被梯度下降"扭"到位。
4.3 第三层翻译:找 P 是个带约束的连续优化问题
把前两层合起来——"反推 \(P^*(y \mid x)\)"这件事就被翻译成一个标准的连续优化问题:
# 伪代码:大模型训练的本质
def train():
theta = init_random_params(num_params=124_000_000) # 起点
for batch in dataset:
x, y_true = batch
p_theta = forward(x, theta) # 一个 50304 维概率向量
loss = -log(p_theta[y_true]) # 看正确答案那一格的"面积"
grad = autograd(loss, theta) # 链式法则反推梯度
theta = theta - lr * grad # 沿梯度走一小步
抽象一层看:
从离散观测 \(\{(x_i, y_i)\}\) 反推连续函数 \(P_\theta\) → 一个 124M 维空间里的最小化问题 → 用梯度下降数值地"积分"到最优点 \(\theta^*\)
这一节回答了开头的疑惑:"离散符号怎么会被微积分捕获?"——答案是:它不是直接被捕获,是先被翻译成"在概率单纯形上找一个最优函数"这个连续问题。Embedding 是翻译器,softmax 是翻译器,cross-entropy 是评价器,autograd 是反推机器。
6. 可微分编程:把"找函数"变成"跑梯度下降"
DIFFERENTIABLE_PROGRAMMING.md 已经详细讲过"深度学习就是可微分编程"。本节只补充一个新视角——可微分编程是怎么把"找函数 P"这件听起来很哲学的事,变成"写程序 + 跑梯度下降"这件很工程的事。
5.1 "找函数"这件事的三种范式
把"给一堆 \((x, y)\) 数据对,找一个函数 \(f\) 让 \(f(x) \approx y\)"这件事放在历史长河里看,三种范式各自的解法是:
| 范式 | 谁写函数 \(f\) | 怎么找参数 |
|---|---|---|
| 符号主义 | 人手写规则("if x is a verb, then ...") | 不需要"参数",规则就是答案 |
| 贝叶斯网络 | 人画图结构 + 写 CPT 模板 | 数据估计 CPT 数值(MLE / EM) |
| 可微分编程(深度学习) | 人写架构(多少层、用什么 op) | 梯度下降自动写出 124M 个参数 |
详情参见 SYMBOLIC_BAYES_NEURAL.md。深度学习的关键差异是:函数的"血肉"全部交给数据自动写。人只写函数的"骨架"——也就是 model.py 里那 400 行架构代码。
5.2 "万能函数模拟器"的代码实现:两件事
回到通用近似定理:它告诉我们"足够大的网络存在",但没告诉我们具体是什么样的网络。从工程角度,把它变成可执行的"万能函数模拟器"需要两件事:
(a) 一个表达力足够的架构模板。意思是:架构本身要能承载任意复杂的函数。CodeGPT 的整段 forward 就是这样一个模板:
# model.py:177-198
def forward(self, idx, targets=None):
tok_emb = self.transformer.wte(idx) # 离散 → 连续 embedding
pos_emb = self.transformer.wpe(pos)
x = self.transformer.drop(tok_emb + pos_emb)
for block in self.transformer.h: # 12 层 Block 复合
x = block(x)
x = self.transformer.ln_f(x)
logits = self.lm_head(x)
...
每个 nn.Linear、nn.Embedding、LayerNorm 都是带可调参数的零件。整套架构组合起来,理论上能逼近任意"上下文 → 下一个 token 概率分布"的映射。
(b) 一个能自动调节参数的反推机器**。意思是:给定一个 loss,要能自动算出每个参数应该往哪个方向调。这就是 loss.backward() + optimizer.step() 那一对:
# train.py 主循环简化版
logits, loss = model(X, Y) # 前向跑一遍架构,得到 loss
loss.backward() # autograd 沿前向图反推梯度
optimizer.step() # 按梯度更新每一个参数
optimizer.zero_grad()
(a) 是"万能函数模拟器"的本体——一段可微分程序。 (b) 是"万能函数模拟器"的反推机器——
autograd+optimizer。
5.3 训练 = 编译,推理 = 执行
这是可微分编程视角下最反直觉但最准确的类比:
传统编程: 人 ─[写代码]─► 程序 ─[执行]─► 结果
可微分编程: 数据 ─[训练]──► 参数化程序 ─[执行]─► 结果
↑ ↑
"编译"参数 "执行"forward
- 架构(人写的)相当于一段 C 程序的源码——决定了"程序结构";
- 参数(梯度下降写的)相当于编译后的二进制——决定了"程序的具体行为";
- 训练就是"编译过程"——把数据"编译"成参数;
- 推理就是"执行"——跑一遍带参数的程序。
所以"找万能函数 P"这件事在可微分编程里的精确说法是:
写一段架构(=源码),用数据梯度下降(=编译器)把它编译成 124M 个具体参数(=二进制),运行时就是一段确定性的函数 \(P_\theta\)。
5.4 "函数 P"的两种含义
需要澄清一个常见混淆——"找函数 P"里的 "P" 到底指什么?两个含义都对,但要分清:
| 含义 | 代码对应 |
|---|---|
| 函数体 \(P_\theta\)(输入 \(x\),输出概率向量) | 整段 forward()(model.py:177-198) |
| 函数值 \(P_\theta(y \mid x)\)(具体某个 \((x,y)\) 处的概率值) | F.softmax(logits, dim=-1)[..., y](model.py:301) |
训练的目标是函数体逼近函数体——让我们的 \(P_\theta\) 这个映射尽量接近未知的 \(P^*\) 这个映射。但训练时能直接看到的只是函数值——我们用 cross-entropy 看模型在每个观测样本 \((x_i, y_i)\) 处的函数值有多接近 1。
从函数值反推函数体——这正是"反推原函数"在大模型时代的具体形式。
7. 矩阵就是参数化的万能函数:一行 nn.Linear 的全部秘密
PROBABILITY_AS_AREA_MATRIX_AS_MAP.md 详细讲了"矩阵就是映射"。这一节专门补充另一个角度——为什么矩阵是"万能函数模拟器"的天然零件。
6.1 一行 nn.Linear = 一个可调旋钮库
fc = nn.Linear(in_features=768, out_features=3072, bias=False)
# 等价于: 一个 (3072, 768) 的可学习矩阵
# 等价于: 3072 × 768 = 2,359,296 个可调旋钮
nn.Linear 是可微分编程里最便宜的"通用零件":
- 它有大量参数(一行就是上百万个旋钮);
- 每个参数都可微(
∂(Wx)/∂W_{ij} = x_j); - 矩阵乘法处处可导,梯度能畅通反传;
- 高度并行(GPU SIMD 的母语)。
任何想得到的线性变换都能用矩阵实现:旋转、拉伸、投影、降维、升维……所以单看"线性",它已经覆盖了一个巨大的函数空间。
6.2 矩阵 + 非线性 = 万能函数
但只有线性还不够——堆叠多层线性等价于一层线性,没赚到(参见 DIFFERENTIABLE_PROGRAMMING.md §5)。
通用近似定理的真正配方是 "线性 + 非线性激活" 的交替堆叠。CodeGPT 的 MLP(model.py:76-90)就是最纯的样本:
class MLP(nn.Module):
def __init__(self, config):
super().__init__()
self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd) # 升维 768 → 3072
self.gelu = nn.GELU() # 非线性
self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd) # 降维 3072 → 768
def forward(self, x):
return self.c_proj(self.gelu(self.c_fc(x)))
这就是单隐层 MLP 的写法。理论上,只要 c_fc 升维得足够宽,这一个 MLP 就能逼近任意连续函数 \(\mathbb{R}^{768} \to \mathbb{R}^{768}\)——通用近似定理直接保证。
6.3 为什么 GPT 不只用单隐层?
通用近似定理保证"单隐层就够",但没说要多宽。要逼近一个复杂函数到给定精度,单隐层可能需要指数多个神经元——参数量爆炸到不可训。
工程上的妙手是:用多层 + 残差 把"指数宽"换成"线性深":
# model.py:186-187
for block in self.transformer.h: # 12 层
x = block(x)
每层不需要太宽(768 维),但 12 层复合起来表达力就上去了。从函数视角看:
每个 \(f_i\) 都是一个"小"的可微函数(attention + MLP + LayerNorm + residual);它们的复合才是最终的 \(P_\theta\)。这正是"深度学习"里"深度"两个字的工程意义——用层数换宽度,让万能函数模拟器实际能训得动。
6.4 一张矩阵清单:CodeGPT 用了多少个 nn.Linear?
model.py 里所有 nn.Linear 累加起来,就是这台万能函数模拟器的全部"线性零件":
| 位置 | 名称 | 形状 | 参数量 | 角色 |
|---|---|---|---|---|
| 入口 | wte(model.py:145) |
(50304, 768) | 38.6M | 离散 id → 连续向量 |
| 入口 | wpe(model.py:146) |
(1024, 768) | 0.79M | 位置 → 连续向量 |
| 每层 attn | c_attn(model.py:36) |
(768, 2304) | 1.77M | x → Q/K/V 三联体 |
| 每层 attn | c_proj(model.py:37) |
(768, 768) | 0.59M | 多头输出投影 |
| 每层 mlp | c_fc(model.py:80) |
(768, 3072) | 2.36M | MLP 升维 |
| 每层 mlp | c_proj(model.py:82) |
(3072, 768) | 2.36M | MLP 降维 |
| 出口 | lm_head(model.py:151) |
(768, 50304) | 与 wte 权重共享 |
把每层的参数量 × 12 加起来 + 入口 + 出口 ≈ 124M。这就是"一个 124M 维参数空间里的万能函数实例"的字面意思——124M 个可调旋钮被组织成约 80 个 nn.Linear,再被 GELU / softmax / residual 连接起来。
8. 概率视角:在概率单纯形上"积分"出一族函数
到目前为止我们说 \(P_\theta\) 是"一个函数"。但更精确的说法是:它是一族函数——给每个上下文 \(x\) 一个长度 50304 的概率向量。
7.1 概率单纯形:一族向量的几何家
一个长度 \(V\)、各分量 \(\in [0,1]\)、总和为 1 的向量,住在 \(\mathbb{R}^V\) 里的一个几何体上:
- \(V = 2\)(两类分类):单纯形是一条线段 \([0,1]\)(一个概率值 \(p\) 就够,另一维是 \(1-p\));
- \(V = 3\):单纯形是个二维三角形(三个分量在三角形顶点之一);
- \(V = 50304\)(CodeGPT):单纯形是一个 50303 维的"超三角形"。
# 一个 V 维点是否在单纯形上?
def is_on_simplex(p, eps=1e-6):
return (p >= 0).all() and abs(p.sum() - 1.0) < eps
probs = F.softmax(torch.randn(50304), dim=-1)
print(is_on_simplex(probs)) # True
\(P_\theta\) 干的事就是:每个输入上下文 \(x\) → 单纯形上的一个点。所有可能的上下文加起来,\(P_\theta\) 描绘了"上下文空间 → 单纯形"的一整张图——这就是"一族函数"或者说"函数族 / function family"。
7.2 "万能函数 P"的精确含义
把上节的几何观加进来,"万能函数 P"的完整意思是:
存在一个真实但未知的映射 \(P^* : \text{所有可能上下文} \to \text{概率单纯形}\),我们想用神经网络反推它。
这个 \(P^*\) 是"语言/代码本身的统计规律"——人类几千年写下来的每段文本背后都隐含着这样一个分布。它是不可枚举的(上下文是无穷的),但训练语料给了我们海量采样点。
CodeGPT 训练的本质是:
反推过程:
未知的真理 P* 可观测的样本(语料) 可训练的近似 P_θ
┌──────────┐ ┌──────────────────┐ ┌──────────────┐
│ 全部上下文 │ 采样 → │ (x_i, y_i) i=1..N │ 拟合 → │ 124M 参数的 │
│ → 单纯形 │ │ │ │ 神经网络 │
└──────────┘ └──────────────────┘ └──────────────┘
↑ │
└─────────── 训练完后,P_θ ≈ P* ────────────────────┘
每次 loss.backward() + optimizer.step(),整族函数 \(P_\theta\) 的形状在 124M 维参数空间里被"挪动了一小步"——挪到那些"实际观察到的 token 序列概率更高"的方向。
7.3 大模型里其实有四种概率分布
PROBABILITY_AS_AREA_MATRIX_AS_MAP.md §13 已经做过完整盘点。这里只把它们和"万能函数 P"对齐一遍:
| 分布 | 在哪 | 作用于 P 的哪个环节 |
|---|---|---|
| 类别分布(输出) | softmax(logits)(model.py:301) |
\(P_\theta(\cdot \mid x)\) 输出的那块面积——P 的"值域" |
| 高斯(内部装修) | 初始化、LayerNorm、GELU | \(P_\theta\) 的内部参数空间的几何约束 |
| 伯努利(开关) | Dropout、FIM 触发 | 训练时让 P 的轨迹有随机性,防过拟合 |
| 均匀(采样) | FIM 切点、shuffle、batch 选 | 让训练数据采样无偏 |
所以"找函数 P"实际上是在多种概率结构的协作下完成的:高斯负责内部装修,伯努利和均匀负责训练随机性,类别分布才是 P 的真实输出形态。
7.4 概率视角和万能函数视角的统一
最后一个角度——为什么 GPT 的输出非要是"概率分布"而不能直接吐 token?
因为离散选择不可微(不能对 argmax 求导)。而通用近似定理只对连续函数有效——你要让"万能函数模拟器"工作,就必须让输出是连续可微的。
- 离散选择 → 离散概率向量:把"选哪个"换成"在每个上的权重",离散变连续;
- 连续概率向量 → 万能函数模拟:现在 \(P_\theta\) 是连续映射,通用近似定理才能套上去;
- 训练完了再 multinomial 抽样:最终输出还是离散 token,回到符号世界。
"概率"在大模型里不是"诚实地表达不确定性"那种哲学姿态——它首先是数学上的必要妥协,让微积分和通用近似定理能在离散符号系统上工作。
9. 万能函数 P 的反推机器:cross-entropy + autograd + SGD
理论说完,看一下"反推机器"具体长什么样。三个零件:
8.1 零件 1:cross-entropy——"反推方向"的量化器
# model.py:192
loss = F.cross_entropy(logits.view(-1, logits.size(-1)),
targets.view(-1), ignore_index=-1)
这一行做的事:衡量当前 \(P_\theta\) 离真理 \(P^*\) 有多远。具体怎么衡量?看模型在"正确答案那一格"上铺了多少面积(详见 PROBABILITY_AS_AREA_MATRIX_AS_MAP.md §5):
为什么是 cross_entropy 而不是 MSE 或者别的?因为它在数学上等价于最大似然估计——而最大似然估计在统计学上就是"反推未知分布的最优方法"。在 KL 散度的语言下:
cross-entropy 是"反推原函数"这件事的标准答案——所有想从离散观测里反推概率分布的人,最终都会写出这一行。
8.2 零件 2:autograd——"反推路径"的自动计算器
loss.backward() # ← autograd 干活
这一行做的事:对 124M 个参数中的每一个,算出 "把它调大 0.0001,loss 会变化多少"。
它不是数值微分(不是真的去试探 0.0001),是符号微分:每个原子 op(nn.Linear、F.softmax、F.cross_entropy)都登记了自己的求导规则,autograd 沿着 forward 的计算图反向连乘——把链式法则机械化执行(详见 §4.4)。
124M 个梯度算完,等价于在 124M 维参数空间里给出了一个"反推方向向量" \(\nabla_\theta L\)——它指向"让 loss 增加最快"的方向,反方向就是"让 P_θ 更贴近 P*"的方向。
8.3 零件 3:SGD / Adam——"反推步进"的工程封装
optimizer.step() # 沿反方向走一小步
optimizer.zero_grad() # 清梯度,准备下一步
最朴素版本就一行:
with torch.no_grad():
for p in model.parameters():
p -= lr * p.grad
每一次 step 把 124M 维参数往"反梯度方向"挪动 lr(一般 \(10^{-4}\))那么大一小步。配合 Adam 这种自适应学习率优化器,可以让不同参数有不同的步长——具体见 train.py。
把这三件事拧在一起就是 train.py 的主循环:
# train.py 训练循环简化版
for batch in dataloader:
X, Y = batch
logits, loss = model(X, Y) # 前向:跑一遍 P_θ
loss.backward() # autograd:算反推方向
optimizer.step() # 走一小步
optimizer.zero_grad() # 清干净
几十万到几百万次循环之后,\(\theta\) 就从随机初始化"积分"到了一个让 loss 很小的点——这就是"万能函数模拟器" + "微积分反推机器"协作的全部内容。
8.4 一个工程细节:为什么必须用 mini-batch?
理论上"梯度下降"应该用整个数据集的梯度(叫"批梯度下降")。但实际中我们只用一个小 batch(CodeGPT 训练默认 batch_size=12 或 32)。原因:
- 算力不允许:用 100B token 数据集每步全计算一遍要算到天荒地老;
- 统计上够用:CLT 告诉我们 mini-batch 梯度是真实梯度的近似高斯样本,只要 batch 不太小,方向就接近正确(参见 PROBABILITY_AS_AREA_MATRIX_AS_MAP.md §13.3);
- 噪声有益:mini-batch 自带的随机性能帮助逃出鞍点和局部极小——梯度下降变成 Langevin 动力学(参见 PHYSICS_AND_DEEP_LEARNING.md)。
所以现代意义下的"反推机器"严格来说叫 SGD = 随机梯度下降 = 带噪声的微积分反推。噪声不是 bug,是 feature。
10. CodeGPT 就是一个 124M 维参数空间里的万能函数实例
把前面所有节合在一起,从"万能函数模拟器"视角看本项目,可以画出这张完整的对应图:
┌─────────────────────────────────────────────────────────────────────┐
│ 通用近似定理: 存在足够大的可微分程序能近似任意 P* │
│ ↓ │
│ CodeGPT.__init__ 写下了一个 124M 参数的可微分程序模板 │
│ 架构骨架: model.py:136-198 整个 CodeGPT 类 │
│ ↓ │
│ forward(idx, targets): 用当前参数 θ 跑一次 P_θ(y | x) │
│ model.py:177-198 │
│ ↓ │
│ cross_entropy: 衡量 P_θ 离观测样本 (x_i, y_i) 有多远 │
│ model.py:192 │
│ ↓ │
│ autograd (loss.backward): 链式法则算出 ∇_θ L │
│ ↓ │
│ optimizer.step: 沿 -∇_θ L 走一小步,把 P_θ 向 P* 推一点 │
│ ↓ │
│ 重复 N 次,得到训练好的 θ*。 P_{θ*} ≈ P* │
│ ↓ │
│ 推理(sample.py): 用 P_{θ*} 反复自回归生成下一个 token │
└─────────────────────────────────────────────────────────────────────┘
9.1 把项目代码逐行钉到这张图上
| 这张图里的角色 | CodeGPT 里的代码 |
|---|---|
| 万能函数模板(架构) | class CodeGPT(nn.Module)(model.py:136) |
| 离散 → 连续(embedding) | wte、wpe(model.py:145-146) |
| 多层复合(深度堆叠) | for block in self.transformer.h(model.py:186-187) |
| 万能函数的非线性 | nn.GELU()(model.py:81) |
| 可微的"查表"(attention) | model.py:51-73 整个 CausalSelfAttention.forward |
| 万能函数 → 概率分布 | F.softmax(model.py:301) |
| 衡量与真理的距离 | F.cross_entropy(model.py:192) |
| 反推方向自动计算 | loss.backward()(train.py 训练循环) |
| 沿反推方向走一步 | optimizer.step() |
| FIM: 给 P 添一个新维度的条件 | apply_fim_transform(tokenizer.py) |
| 推理时调控 P 的形状 | temperature/top-k/top-p(model.py:279-302) |
读 model.py 的时候不妨在脑子里把每一行翻译成"这是万能函数模拟器的哪个零件"——所有 400 行代码都能精准对位。
9.2 训练 = 在 124M 维空间里走一条曲线
把训练过程想象成在 124M 维参数空间里"走路":
- 起点 \(\theta_0\):纯随机的高斯初始化(
model.py:171-175); - 每一步:算出当前 batch 上的梯度,沿反方向迈 \(\text{lr} \cdot \|\nabla\|\) 这么远;
- 终点 \(\theta^*\):让 loss 在整个语料上很小的点;
- 整条路径就是一次"数值积分"——把"梯度场"沿着 SGD 的 Euler 步法积分出一条 trajectory。
数学上,SGD 是 Langevin 方程的数值离散化(参见 PHYSICS_AND_DEEP_LEARNING.md);几何上,残差网络是 ODE 的离散化(同篇)。所以整条训练曲线 = 数值积分,整条残差堆叠 = 数值积分——大模型从训练到推理,全是不同尺度上的"微积分操作"。
9.3 推理 = 万能函数 \(P_{\theta^*}\) 的 forward 执行
训练完之后,整个模型变成一个确定性的函数 \(P_{\theta^*}\)(除了 dropout 在 eval 模式下被关闭,行为完全可重现)。
每次推理就是一次纯 forward:
# sample.py 推理核心(简化)
while not done:
logits, _ = model(idx) # 跑一次 P_{θ*}
logits = logits[:, -1, :] / temperature # 取最后一个位置 + 调温度
probs = F.softmax(logits, dim=-1) # 50304 根柱子的面积
next_token = torch.multinomial(probs, 1) # 按面积比例抽样
idx = torch.cat([idx, next_token], dim=1)
每生成一个 token,就是让万能函数 \(P_{\theta^*}\) 在当前上下文上输出一个分布,从分布里抽一个。重复这一过程,从单个 token 自回归地长出整段代码——但每一次 forward 都是同一个 \(P_{\theta^*}\) 在不同上下文上的求值。
训练写权重,推理用权重——参见 SFT_RL_INFERENCE_MECHANICS.md,这一对偶在那里有更详细的展开。
11. 万能函数 P 的边界:能模拟什么,模拟不了什么
通用近似定理"在数学上"很强大,但万能函数模拟器在工程上仍然有边界。这一节做一次诚实清算。
10.1 通用近似定理的"小字条款"
把定理重新读一遍,注意四个限定词:
给定任意一个连续函数 \(f\) 和任意精度 \(\varepsilon\),存在一个单隐层前馈网络,使得它和 \(f\) 在任意紧集上差距小于 \(\varepsilon\)。
每个限定词都暗藏一个工程问题:
| 限定 | 暗示的问题 |
|---|---|
| 连续函数 | 真实的 \(P^*\) 是不是连续的?数据分布有断裂怎么办? |
| 任意精度 | 没说要多大的网络。要想 \(\varepsilon = 0.01\) 可能需要 10^9 参数,要 \(\varepsilon = 0.001\) 可能需要 10^{15} |
| 存在 | 不保证能找到。SGD 可能卡在局部极小或鞍点 |
| 紧集 | 训练分布之外没保证。这就是泛化和外推问题的根源 |
所以理论上的"万能"不等于工程上的"无所不能"。
10.2 万能函数模拟器做得到的
在足够数据 + 足够参数 + 充分训练的条件下,下列任务大模型能做得很好:
- 统计规律:哪些 token 在哪些上下文里高频出现——这是它最擅长的。
- 结构识别:括号匹配、缩进、代码 AST 形态——只要训练里见过类似模式,能很好泛化。
- 相似插值:训练里见过 \(f(x_1)\) 和 \(f(x_2)\),对于"语义上"在它们之间的 \(x\) 也能猜得不错。
- 多语言迁移:Python 的
for和 JS 的for在表示空间里会自动靠近(参见 GPT_AS_SUPER_SEARCH.md)。
10.3 万能函数模拟器做不到的
- 精确算术:万能函数模拟器是"软"的(softmax 是连续插值),不擅长精确的多位数乘法。Calculator-style tool use 是必要补丁。
- 可证明的逻辑链:长推理链的概率会衰减,且没有形式系统的保证。Lean / Coq / verifier 是必要补丁。
- 训练分布外的外推:通用近似定理只在紧集内保证。训练全是 Python 2,部署在 Rust 上必然失效——i.i.d. 假设破。
- 真实时间状态:模型里没有"实时时钟"或"外部世界"——它是一个纯函数,无副作用、无记忆。Tool use / RAG / 长上下文是必要补丁(参见 RAG_VS_SFT.md)。
10.4 为什么现代 LLM 系统都是"神经-符号混合"
把上面两小节合在一起,就直接推出当下的工程现实:
没有任何真实落地的 LLM 系统是"纯万能函数 P"——它都是 "\(P_\theta\) + 一堆符号代码(脚手架)" 的混合架构。
详情见 SYMBOLIC_BAYES_NEURAL.md 和 SFT_RL_INFERENCE_MECHANICS.md。简单说:
- \(P_\theta\) 负责"统计 + 插值":它擅长的部分;
- 符号脚手架(chat template、stop_tokens、tool dispatch、RAG retriever、verifier)负责"硬规则 + 外部世界":它做不到的部分。
CodeGPT 的"脚手架"目前还很简单——sample.py 里的 stop_tokens、temperature、FIM 模板——但已经满足这种混合架构的最小样本。
10.5 边界本身就是研究前沿
正因为"万能函数模拟器"有这些边界,整个 LLM 研究的下一波就是扩展这些边界:
- Tool use → 让函数 \(P\) 能调用外部计算;
- Long-context → 让 \(P\) 的输入空间变大;
- Test-time compute / o1 / R1 → 让 \(P\) 在推理时也用计算反推(不止训练时);
- MoE → 让 \(P\) 内部条件化路由(参见 SFT_FORGETTING_AND_MOE.md);
- Multi-modal → 让 \(P\) 接受文本之外的模态。
每一项都是给"万能函数模拟器"加一个新维度,让它能近似更广的 \(P^*\)。从这个意义上讲,通用近似定理 + 可微分编程 = 当代 AI 的元方法论——所有进展都可以理解为"换一种方式扩充这台万能函数模拟器"。
12. 与本项目其他文档的联系
这篇的位置是"最底层视角"——把大模型化简到一句话:"用微积分从数据里反推出来的函数 P"。其他文档从不同角度展开这一句话:
- 结构怎么演化:DEEP_DIVE.md 讲从 RNN 到 Transformer,万能函数模拟器的架构是怎么进化的。
- 目标为什么是 cross-entropy:COMPRESSION_IS_INTELLIGENCE.md 讲 cross-entropy = 压缩 = 智能。
- 可微分编程的全貌:DIFFERENTIABLE_PROGRAMMING.md 详细讲"为什么 PyTorch 是一门语言"。
- 概率和矩阵的几何直觉:PROBABILITY_AS_AREA_MATRIX_AS_MAP.md 详细讲"概率是面积、矩阵是映射"。
- 物理学的对应:PHYSICS_AND_DEEP_LEARNING.md 讲 SGD 是 Langevin 动力学、softmax 是玻尔兹曼分布。
- 和其他范式的对比:SYMBOLIC_BAYES_NEURAL.md 讲符号主义/贝叶斯/深度学习三种"找 P"的范式。
- 训练之后怎么落地:SFT_RL_INFERENCE_MECHANICS.md 讲"\(P_\theta\) + 脚手架"的工程现实。
- 找到 P 之后怎么用它检索:GPT_AS_SUPER_SEARCH.md 讲 attention = 在 P 内部做的 144 次高维搜索。
结语:站在万能函数模拟器肩膀上回看大模型
回到开头那一句话——
GPT 是一个用微积分(可微分编程)从海量离散文本数据里"反推"出来的函数 \(P(y \mid x)\)。
把这句话和本项目代码逐一对位:
# 函数 P 的本体
def forward(self, idx, targets=None): # model.py:177
...
# 反推机器
loss.backward() # autograd
optimizer.step() # SGD
# 衡量 P 离真理多远
F.cross_entropy(...) # model.py:192
# 离散 → 连续的翻译
self.transformer.wte(idx) # model.py:183
F.softmax(logits, dim=-1) # model.py:301
这五行加起来就是"万能函数 P + 微积分反推"的全部代码骨架。剩下所有的东西——多头注意力、残差连接、LayerNorm、FIM、temperature——都是这个骨架的精细化、工程化、定制化。
通用近似定理告诉我们这件事理论上能成;微积分告诉我们怎么把它做出来;离散数据 + 概率单纯形告诉我们离散符号怎么变成可微问题;
nn.Linear+nn.GELU告诉我们用什么零件搭起来;loss.backward()告诉我们怎么自动调零件。
理解了这条线,再看 model.py 的任意一行,都是这台"万能函数模拟器"的一个具体零件——零件简单,组合起来就是当代 AI 的全部。
CodeGPT 就是一个 124M 维参数空间里的一个万能函数实例——它由人写的架构(骨架)和梯度下降写的参数(血肉)合成,由微积分粘合,由数据塑形。这就是从万能函数模拟器视角看到的,大模型最朴素也最准确的样子。