高中最容易被忽略的数学归纳法,其实就是大模型"逐字生成"的骨架
数学归纳法、奠基、归纳假设、归纳步、"假设 n=k 成立,证明 n=k+1"……高中学的时候觉得是个"证明套路", 考完只记得"多米诺骨牌"这个比喻。 因为老师只教你"用它证求和公式、证不等式",没告诉你两件事:
第一,数学归纳法的灵魂不是"一种证明技巧",是一台构造机器:只要定义好"起点"和"由前一步推出下一步的规则",就能覆盖无限多的情形。 第二,今天每一个大模型(LLaMA、Qwen、GPT、DeepSeek)逐字生成一句话的方式, 结构上就是数学归纳法:把"第一个词"当奠基、把"由前文推出下一个词"当归纳步,一步步递推出整句话。
这篇文章不背一个公式、不做一道归纳证明,全部用能跑起来的 PyTorch 代码, 把数学归纳法最核心的"奠基 + 归纳步"结构,一路接到大模型的自回归生成。 并且顺手回答四个灵魂拷问:
- 数学归纳法明明是"证明数列公式"用的,怎么会和大模型生成文字扯上关系?
- Transformer 号称"看全部前文",这算普通归纳还是"强归纳"?两者差在哪?
- 为什么大模型一步说错,后面常常越跑越歪、彻底崩掉?
- 每生成一个词都要重看全部前文,不是慢死了吗?它是怎么偷懒的?
0. 一句话主线
如果只能留一句话,那就是这句:
数学归纳法 = 奠基(起点)+ 归纳步(由前一步推出下一步)。自回归生成用的正是这套结构。
给定"第一个词"(奠基),再反复用"由前文推出下一个词"的规则(归纳步),就能递推出任意长的句子—— 这跟你用归纳法"由第 k 项推第 k+1 项、从而覆盖所有 n"是同一件事。
# 数学归纳法 = 奠基 + 归纳步。用它"构造"一个数列:
def build_sequence(n):
seq = [1] # 奠基:第一项 = 1
for k in range(1, n): # 归纳步:由前一项推出下一项
seq.append(seq[-1] + k + 1) # 规则:a(k+1) = a(k) + (k+1)
return seq
print(build_sequence(6)) # [1, 3, 6, 10, 15, 21] —— 三角形数
# 只定义了"第一项"和"由前推后的规则",就生成了任意长的数列 —— 这就是归纳法的构造力
记住这个画面:奠基放下第一块骨牌,归纳步保证"每一块都能推倒下一块",于是整排骨牌必然全倒。 下面所有东西都挂在"奠基 + 归纳步"这一个结构上。
1. 数学归纳法的真身:一台"由前推后"的构造机器
高中把归纳法包装成"证明题专用工具",让你以为它只能用来验证已知公式。 但它的本质是构造:奠基定义起点,归纳步定义"如何从已有的推出下一个",两者一凑,就覆盖了无限。 自回归生成,就是把"数列的项"换成"句子的词",其余结构原封不动:
import torch
import torch.nn.functional as F
torch.manual_seed(0)
V = 6
W = torch.randn(V, V) # 玩具"下一个词"规则(由当前词给出下一个词的打分)
def generate(start, n):
seq = [start] # 奠基:第一个词
for _ in range(n): # 归纳步:由前文推出下一个词
logits = W[seq[-1]]
seq.append(F.softmax(logits, dim=-1).argmax().item())
return seq
print("自回归生成:", generate(0, 8))
# 结构和上面的 build_sequence 一模一样:定义起点 + 由前推后的规则 = 生成任意长序列
看这两段代码的骨架——奠基一个起点 + 循环里由前一步推出下一步——完全一致。
大模型所谓的"自回归(autoregressive)",翻译成高中语言就是:"用数学归纳法的方式,一项一项地把句子递推出来。"
🤔 疑惑点一:Transformer"看全部前文",是普通归纳还是"强归纳"?
是强归纳。普通归纳里,第 k+1 步只用到第 k 步的结论(P(k) ⟹ P(k+1));强归纳里,第 k+1 步可以用到前面所有步的结论(P(1)…P(k) 一起 ⟹ P(k+1))。只看"上一个词"的模型(bigram、马尔可夫链)是普通归纳;而 Transformer 的注意力,预测下一个词时会回看前面的每一个词——这正是强归纳。这个区别,就是老式模型和 Transformer 在"记忆力"上的根本差距。
用代码把"普通归纳(只看上一个)"和"强归纳(看全部前文)"摆在一起:
import torch
import torch.nn.functional as F
torch.manual_seed(0)
seq = torch.randn(5, 4) # 已生成的 5 个词向量
# 普通归纳:预测下一个只看"上一个词"(bigram / 马尔可夫链)
last_only = seq[-1]
# 强归纳:预测下一个看"前面全部"(注意力对整段历史加权)
q = seq[-1]
attn = F.softmax(seq @ q / 4 ** 0.5, dim=-1) # 对全部历史算注意力权重
all_context = attn @ seq # 融合前文所有词
print("只看上一个词(普通归纳):", last_only.round(decimals=2).tolist())
print("看全部前文(强归纳) :", all_context.round(decimals=2).tolist())
# Transformer 走的是强归纳:第 n+1 步依赖 P(1)…P(n) 全部,而不只是 P(n)
普通归纳只攥着"上一个词"这一条信息,一旦需要"呼应很久之前说过的话"就无能为力; 强归纳(注意力)每一步都回看全部前文,所以 Transformer 能记住段首的主语、几十个词前立下的设定。 "看多远"这件事,本质就是"用普通归纳还是强归纳"。
2. 归纳链一环断,后面全歪:这就是"曝光偏差"
数学归纳法有个隐含的脆弱点:整条链依赖"每一步都对"。只要中间断了一环,"由前推后"的保证就失效,后面全部崩塌。 大模型逐词生成时也一样——一步说错,这个错词又成了下一步的"前文",误差会顺着归纳链滚雪球。用一个敏感数列演示:
# 逻辑斯蒂映射:一个对初值极度敏感的"归纳步",用来放大"一步之差"
def logistic(x0, n, r=3.9):
xs = [x0]
for _ in range(n):
xs.append(r * xs[-1] * (1 - xs[-1])) # 归纳步:由前一项推下一项
return xs
clean = logistic(0.400, 16)
broken = logistic(0.402, 16) # 起点只差 0.002
for i in [0, 4, 8, 12, 16]:
print(f"第{i:2d}步 正常={clean[i]:.3f} 出错后={broken[i]:.3f} 偏差={abs(clean[i]-broken[i]):.3f}")
# 起点仅差 0.002,十几步后偏差从 0.002 滚到 0.65 —— 一步错、归纳链断、后面全歪
起点只差 0.002,到第 16 步偏差已从 0.002 滚到 0.65,两条序列面目全非。这就是大模型的"曝光偏差(exposure bias)":
训练时它每一步看到的都是正确前文,但真实生成时,一旦某步选错词,错误就成了后续归纳步的输入,
沿着归纳链层层放大。 归纳法教给你的"每一步都必须成立",在这里变成了大模型最头疼的失败模式之一。
🤔 疑惑点二:每步都要重看全部前文,不是慢死了吗?它怎么偷懒的?
靠 KV-cache。强归纳每生成一个词都要回看前面所有词,如果每步都把全部前文从头重算一遍,长文本会慢得离谱。但归纳法有个天然的便利:前面那些步骤的结论(归纳假设)已经证过了,不用重证。KV-cache 就是把前文算过的中间结果(每个词的 Key、Value)缓存下来,新词来了只需补自己这一行,旧的直接复用。
用代码看"复用旧块"的等价性——这正是 KV-cache 省掉的重复计算:
import torch
torch.manual_seed(0)
seq = torch.randn(4, 8) # 已生成的 4 个词
new = torch.randn(1, 8) # 新来的第 5 个词
full = torch.cat([seq, new], dim=0)
recompute = full @ full.T # 不缓存:把 5 个词的全部两两关系重算一遍
cached_block = seq @ seq.T # 缓存:前 4 个词的关系上一轮就算过,直接复用
print("重算的旧块 == 缓存的旧块 ?",
torch.allclose(recompute[:4, :4], cached_block)) # True
# 前 n 项的"归纳假设"不必重证,缓存即可 —— 新词只需补自己这一行,这就是 KV-cache
True 说明:新词到来时,前文之间的关系一个字节都没变,重算纯属浪费。
KV-cache 把"已经证过的归纳假设"存起来复用,让强归纳既能"看全部前文"、又不至于慢到不可用。
归纳法里"前面步骤不用重证"的常识,在工程上就落成了这个让长文本生成变快的关键优化。
🎬 动手:一个最小自回归生成器,看"归纳链"如何一步步搭起来
把上面所有画面缝进一个能跑的最小自回归生成器,把每一次"归纳步"打印出来:
import torch
import torch.nn.functional as F
torch.manual_seed(0)
V = 8
W = torch.randn(V, V)
def generate(start, n):
seq = [start]
print(f"奠基:放下第 1 个词 = {start}")
for step in range(n):
logits = W[seq[-1]] # 归纳步:由前文推出下一个词
nxt = torch.multinomial(F.softmax(logits, dim=-1), 1).item()
seq.append(nxt)
print(f"归纳步 {step+1}:由前 {len(seq)-1} 个词 -> 推出第 {len(seq)} 个词 = {nxt}")
return seq
out = generate(0, 5)
print("最终序列:", out)
# 奠基 + 5 次归纳步 = 一条完整的归纳链,也就是一句"生成"出来的话
每一行都是一次归纳步:由已有的前文,推出下一个词,再把它接回前文,继续推。 这条不断延长的"归纳链",就是大模型写出的每一句话。 仓库里的动画脚本把它画了出来:
python induction_autoregression_visualization.py
左边你会看到一排"骨牌"逐个落下(逐词生成),每落下一块,都有一束箭头从它指回前面所有已生成的词—— 这就是"强归纳:看全部前文"的样子; 右边则演示"归纳链断裂":两条本该一样的序列,只在某一步被扰动了一点点, 之后就沿着归纳步越拉越远——一步错、后面全歪的曝光偏差,一目了然。
缝合:把所有画面接起来
回到开头,现在每个概念都有了画面和代码出处:
| 归纳法概念 | 高中怎么讲 | 这篇文章怎么看(画面) | 在大模型里是什么 |
|---|---|---|---|
| 奠基 | 证 n=1 成立 | 放下第一块骨牌(第 0、1 节) | 第一个词 / 起始符 |
| 归纳步 | n=k ⟹ n=k+1 | 由前文推出下一个词(第 1、2 节) | 自回归的每一步生成 |
| 强归纳 | 用 P(1)…P(k) | 注意力回看全部前文(疑惑点一) | Transformer 的长距记忆 |
| 链条依赖每步成立 | 有一步不成立就断 | 一环断、后面全歪(第 3 节) | 曝光偏差 / 错误累积 |
| 归纳假设不必重证 | 已证的直接用 | 缓存旧结果不重算(疑惑点二) | KV-cache |
三句话总结这篇文章:
数学归纳法的灵魂是"奠基 + 归纳步"——定义起点和"由前推后"的规则,就能构造无限(第 1 节); 自回归生成就是这套结构——第一个词是奠基、每次预测下一个词是归纳步,而注意力让它成了"看全部前文"的强归纳(第 2 节、疑惑点一); 归纳链的脆弱与高效也都继承了下来——一步错则后面全歪(曝光偏差),前项不必重证则可缓存(KV-cache)(第 3 节、疑惑点二)。
当年数学归纳法学得那么"套路化",不是因为它难,是因为没人告诉你: 那套"奠基 + 由前推后"的证明结构,最后长成了大模型逐字写出每一句话的骨架, 连它一步说错就崩、以及怎么缓存偷懒,都写在归纳法的老道理里。 现在把上面每段代码跑一遍,比高中刷十套归纳证明题都值。
备注(选题/标题): 这篇走的是"高中知识其实是 AI 基石"的钩子(和《对数》《向量》《sin 和 cos》同一路子)。 核心爽点是"自回归 = 数学归纳法"这个结构同构,且顺势带出两个真机制(强归纳↔注意力、归纳链断裂↔曝光偏差)。 可与《对数》《向量》《复数》拼成"注意力四件套",形成系列合力。 若并入"大学4年"主系列:《大学4年没让你真正搞懂的数学归纳法,被大模型的逐字生成讲透了》。