NanoTransformer Courses

🏠 Back to Lab 📚 All Courses

Ch8: 多层感知机 (MLP) 从零手搓 — 用反向传播学会 99 乘法表

准确术语:Multi-Layer Perceptron (MLP),即多层感知机。也叫 Feedforward Neural Network(前馈神经网络)。 "深度学习"只是"层数多的 MLP/CNN/RNN 等"的统称,不是一个独立的概念。

为什么花了近 30 年

时间线

年份 事件 关键人物
1958 感知机 (Perceptron) — 单层线性分类器 Rosenblatt
1969 《Perceptrons》 — 证明单层感知机无法解决 XOR Minsky & Papert
1969-1986 神经网络寒冬 — 学术界几乎放弃
1986 反向传播 — 多层网络的训练算法 Rumelhart, Hinton, Williams
2006 深度信念网络 — 深层网络预训练 Hinton
2012 AlexNet — CNN + ReLU + GPU → ImageNet 冠军 Krizhevsky, Hinton

核心障碍:不是链式法则,是离散输出

链式法则(Chain Rule)是微积分的基础定理,1700 年代就有了。反向传播在数学上就是链式法则的应用。那为什么从 1958 到 1986 花了近 30 年?

Bengio 在 Quora 上的回答:

很多看似显而易见的想法只有在事后才变得显而易见。

在控制论中,很早就开始应用链式法则来解决多层非线性系统。 但在 80 年代早期,神经网络的输出是离散的,这样就无法用基于梯度的方法来优化了。

这时 Rumelhart 和 Hinton 想到,只要把输出做成平滑的 (sigmoid),就可以用链式法则来训练多层神经网络了。 刚好英伟达的显卡提供了 CUDA 的计算接口,也有了算力支撑。

所以这不仅仅是链式法则的问题,而是要跳出离散输出的框架,这种理念上的变革并不容易。

具体来说

激活函数演进

1958 感知机用的激活函数 — 阶跃函数 (Step Function):

    f(z) = 1  if z > 0
           0  if z ≤ 0

    导数 = 0(几乎处处)
    → 梯度无法传播 → 多层网络无法训练

1986 Rumelhart & Hinton 的关键突破 — 换成 Sigmoid:

    f(z) = 1 / (1 + e^(-z))

    导数 = f(z) * (1 - f(z))
    → 处处非零 → 链式法则能一路传到底层
    → 多层网络终于能训练了!

网络结构

MLP 网络结构

输入编码                隐藏层 1          隐藏层 2          输出层
a=7 → [0,0,...,1,0,0]
                    ┌─────────┐    ┌─────────┐    ┌──────────┐
concat → [20 维] ──→│W₁[20×64]│──→│W₂[64×64]│──→│W₃[64×82] │──→ Softmax → 预测
                    │+ b₁[64] │    │+ b₂[64] │    │+ b₃[82]  │
b=8 → [0,...,0,1,0] │  ReLU   │    │  ReLU   │    │          │
                    └─────────┘    └─────────┘    └──────────┘

参数量:20×64 + 64 + 64×64 + 64 + 64×82 + 82 = 10,834
对比 Ch0 Transformer: 18,304 参数(多了 69%)

为什么不需要 Attention

99 乘法表只有 100 个输入组合。MLP 的工作方式:

输入 (7, 8) → one-hot 拼接 [20维]
  ↓
W₁ 的某些行被强烈激活(检测到 "a=7" 且 "b=8" 的模式)
  ↓
ReLU 过滤掉不相关的神经元
  ↓
W₂ 进一步组合特征
  ↓
W₃ 的第 56 列被最强激活 → 输出 56

这就是一个可微分的查找表。不需要 token 之间的信息路由(Attention),因为所有信息已经在输入中拼接好了。


反向传播推导

反向传播全流程

前向传播(矩阵形式)

z₁ = X × W₁ + b₁       [N×20] × [20×64] = [N×64]
a₁ = ReLU(z₁)           [N×64]
z₂ = a₁ × W₂ + b₂      [N×64] × [64×64] = [N×64]
a₂ = ReLU(z₂)           [N×64]
z₃ = a₂ × W₃ + b₃      [N×64] × [64×82] = [N×82]
ŷ  = Softmax(z₃)        [N×82] — 预测概率

损失函数

L = -(1/N) × Σᵢ log(ŷᵢ[targetᵢ])    — 交叉熵

反向传播(链式法则逐层)

Step 1: 输出层梯度

∂L/∂z₃ = ŷ - one_hot(target)    [N×82]

为什么这么简洁?
  L = -log(ŷ_target)
  ∂L/∂z₃ᵢ = ŷᵢ - 𝟙{i=target}

  交叉熵 + Softmax 的导数恰好简化成这个形式。

Step 2: W₃, b₃ 的梯度

∂L/∂W₃ = a₂ᵀ × ∂L/∂z₃    [64×N]ᵀ × [N×82] = [64×82] ✓
∂L/∂b₃ = Σ ∂L/∂z₃         [82] ✓

Step 3: 梯度传到 a₂

∂L/∂a₂ = ∂L/∂z₃ × W₃ᵀ    [N×82] × [82×64] = [N×64] ✓

Step 4: 穿过激活函数(关键!)

∂L/∂z₂ = ∂L/∂a₂ ⊙ ReLU'(z₂)    [N×64] ⊙ [N×64] = [N×64]

ReLU'(z) = 1  if z > 0
            0  if z ≤ 0

如果这里用阶跃函数:
  Step'(z) = 0 几乎处处 → ∂L/∂z₂ = 0 → 梯度断裂!

这就是 Rosenblatt 的感知机无法训练多层的根本原因。

Step 5-6: 递推到 Layer 1(完全同构)

∂L/∂W₂ = a₁ᵀ × ∂L/∂z₂
∂L/∂b₂ = Σ ∂L/∂z₂
∂L/∂a₁ = ∂L/∂z₂ × W₂ᵀ
∂L/∂z₁ = ∂L/∂a₁ ⊙ ReLU'(z₁)
∂L/∂W₁ = Xᵀ × ∂L/∂z₁
∂L/∂b₁ = Σ ∂L/∂z₁

参数更新(SGD)

W ← W - lr × ∂L/∂W
b ← b - lr × ∂L/∂b

激活函数对比

激活函数 公式 导数 收敛 epoch 提出时间
Step (阶跃) 1 if z>0 else 0 0 (几乎处处) ∞ (无法训练) 1958
Sigmoid 1/(1+e⁻ᶻ) σ(1-σ) 1203 1986
ReLU max(0,z) 1 or 0 294 2012
GELU z·Φ(z) 复杂 588 2016

ReLU 最快的原因:正区间导数恒为 1,梯度无损传递,没有 sigmoid 的梯度消失问题。


实验结果

=== 梯度检查 ===
  W1  (20, 64)      max_rel_err = 0.00e+00  ✅
  b1  (64,)         max_rel_err = 1.72e-09  ✅
  W2  (64, 64)      max_rel_err = 1.14e-08  ✅
  b2  (64,)         max_rel_err = 8.58e-08  ✅
  W3  (64, 82)      max_rel_err = 1.92e-07  ✅
  b3  (82,)         max_rel_err = 2.69e-09  ✅

数值梯度 vs 解析梯度的相对误差 < 1e-4,验证反向传播实现正确。

=== ReLU 训练 ===
  Epoch   0: Loss 4.505, Acc  1%
  Epoch 100: Loss 2.250, Acc 47%
  Epoch 200: Loss 0.992, Acc 93%
  Epoch 294: Loss 0.387, Acc 100% 🎉

=== 示例预测 ===
  7×8=56  预测:56 (70.9%)  ✅
  3×4=12  预测:12 (60.1%)  ✅
  9×9=81  预测:81 (57.3%)  ✅
  0×5= 0  预测: 0 (99.1%)  ✅
  6×7=42  预测:42 (88.0%)  ✅

MLP vs Transformer 对比

MLP (本章) Transformer (Ch0)
参数量 10,834 18,304
收敛速度 294 epochs ~500 steps
能力 死记硬背 100 个答案 学到 token 之间的关系
输入方式 拼接 one-hot(位置无关) 序列输入(位置有关)
泛化性 ❌ 只能做 1 位数 ✅ 可扩展到多位数(Ch1-6)
本质 查找表 查找表 + 路由器

输入输出编码的选择:四种方案对比实验

一个自然的问题:输入必须 one-hot 吗?输出必须分类吗?

四种方案

方案 输入 输出 思路
A one-hot [20维] 分类 82类 (softmax+交叉熵) 模式匹配 → 查表
B one-hot [20维] 回归 1个数字 (MSE) 模式匹配 → 算数
C 原始 [a,b] 2维 分类 82类 (softmax+交叉熵) 曲面拟合 → 查表
D 原始 [a,b] 2维 回归 1个数字 (MSE) 曲面拟合 → 算数

实验结果(目标:100/100 = 100%,不能商量)

方案 能否 100% 收敛 epoch 参数量 速度对比
A one-hot + 分类 100% 294 10,834 基准
B one-hot + 回归 100% 14,123 8,385 慢 48×
C 2维 + 分类 100% 1,155 27,090 慢 4×
D 2维 + 回归 63% >20,000 16,769 不收敛

为什么分类比回归好

7×8=56,模型输出 54:

  分类 (交叉熵):P(54) 最高 → 完全错误,惩罚 = -log(P(56)) ≈ 巨大
  回归 (MSE):    输出 54 → 误差 = (54-56)² = 4 → "差一点",温和惩罚

核心区别:
  分类:54 和 100 都是"错误答案",惩罚一样重
  回归:54 比 100 "更接近正确",惩罚更轻

乘法要精确答案,不要"差不多" → 分类更合适

为什么 one-hot 比原始数字好

输入 a=7, b=8:

  原始 2维:网络看到 [7, 8],必须学会 f(7,8)=56 这个非线性曲面
            7 和 8 之间有大小关系,但乘法不是简单的线性关系
            网络需要用 ReLU 去逼近一个二元乘法函数

  one-hot 20维:网络看到 [0,0,0,0,0,0,0,1,0,0, 0,0,0,0,0,0,0,0,1,0]
               第 7 位和第 18 位亮了
               W₁ 的对应行被激活 → 直接匹配模式 → 查表输出

  类比:
    原始输入 = 给你两个数字,让你算乘法
    one-hot  = 给你 100 张卡片,每张写好答案,你只需要找对卡片

方案 D 为什么失败

2 维输入 + 回归是最难的组合: - 输入只有 2 个数字,信息密度最低 - 输出只有 1 个神经元,必须精确到整数 - MSE 损失对"差一点"不敏感 - 20000 epochs 后还有 37 个错误(包括 0×0=0 预测成 1)

结论:100% 硬指标下,分类(softmax + 交叉熵)是最可靠的路径。one-hot 输入进一步加速收敛。这也是为什么 Transformer 的输出层用的也是 softmax + 交叉熵。


运行

cd courses/ch8_mlp_from_scratch

# 完整流程(训练 + 评估 + 激活函数对比 + 可视化)
python3 run.py

# 只运行核心训练
python3 mlp_multiply.py

# 只生成激活函数对比图
python3 visualize.py

文件说明

文件 说明
mlp_multiply.py 核心代码:纯 numpy MLP,每行对应公式
run.py 一键入口:训练 + 评估 + 对比 + 可视化
visualize.py matplotlib 绘图:训练曲线、激活函数、权重热力图

扩展到多位数:MLP 的极限

如果要做 2 位数乘法(10-99 × 10-99):

1 位数 (本章) 2 位数 3 位数 4 位数
输入组合 100 8,100 810,000 81,000,000
输出类别 82 9,802 998,002 99,980,002
MLP 参数 10K 2.6M ~260M ~26B
训练数据 100 条全部 8,100 条全部 81 万条全部 8100 万条全部

每增加一位数,参数和数据量都增长 ~100 倍。4 位数需要 260 亿参数——比 GPT-3 还大,只为了背乘法表。

MLP 的输出层大小 = 所有可能答案的数量(指数增长)。 Transformer 的输出层大小 = 字符表大小(恒定 14)。


Transformer 之前:深度学习做算术的历史

MLP 做不了多位数,那 Transformer (2017) 发明之前呢?其实走了不少弯路:

年份 方法 做了什么 乘法结果
2014 LSTM (Zaremba & Sutskever) "Learning to Execute":LSTM 学执行简单程序 加法能做,乘法很差
2014 Neural Turing Machine (Graves) 给 RNN 加外部记忆条 能学 copy/sort,乘法不行
2015 Neural GPU (Kaiser & Sutskever) 专门为算术设计的并行架构 多位数乘法部分成功,但不鲁棒
2015 Stack RNN (Joulin & Mikolov) 给 RNN 加栈结构 简单模式可以,复杂算术不行
2017 Transformer (Vaswani et al.) 自注意力,并行处理全序列 架构突破
2022 Transformer + CoT 分步推理 多位数 100%

为什么 RNN/LSTM 做不好乘法

LSTM 处理 45×67= 的方式:

  时刻1: 读入 "4" → 更新隐藏状态 h₁
  时刻2: 读入 "5" → 更新 h₂ (h₁ 信息开始衰减)
  时刻3: 读入 "×" → 更新 h₃
  时刻4: 读入 "6" → 更新 h₄ (h₁ 信息进一步衰减)
  时刻5: 读入 "7" → 更新 h₅
  时刻6: 读入 "=" → 开始输出

  问题:到输出时,h₅ 要同时记住 4、5、6、7 和它们的位置
  全靠一个固定大小的向量 h → 信息瓶颈

Transformer 处理同样的输入:

  "=" 的 Attention 直接回看 "4"、"5"、"6"、"7"
  不需要经过中间状态 → 没有信息衰减
  所有 token 距离都是 1 → 并行直达

真正的突破:CoT,不仅是 Transformer

Transformer 单纯做乘法也不太行——直接预测 45×67=3015 仍然需要一步到位。

真正的突破是 2022 年的 Chain-of-Thought(就是 Ch1 做的):

不是让模型一步算出答案,而是让它展示推理过程:

45×67=S1:45×60=2700;S2:45×7=315;A1:2700+315=3015;Z3015

每一步只需要 1 位数运算 → MLP 级别的查表就够了
Attention 负责路由中间结果 → 把各步串起来

全课程技术演进线

MLP (1986):         能查表,不能推理                 ← 本章
  ↓
RNN/LSTM (2014):    能按顺序推理,但信息衰减
  ↓
Transformer (2017): 能并行访问全部历史,但单步推理有限  ← Ch0
  ↓
Transformer + CoT:  把复杂推理拆成简单步骤             ← Ch1
  ↓                 每步用 Attention 路由 + FFN 查表
Ch2-Ch6:            RoPE / KV Cache / FlashAttention / MLA+MoE
                    优化速度、压缩内存、扩展规模

核心认知

  1. 多层感知机的训练 = 链式法则的应用。数学上没有任何新东西,但把离散输出换成可微激活函数,是 1986 年的关键洞察
  2. MLP 是一个可微分的查找表:W₁ 的每行检测输入模式,W₃ 的对应列输出答案
  3. 分类(softmax + 交叉熵)是精确任务的最可靠方案:回归做不到 100%,分类+one-hot 最快收敛(294 epochs)
  4. 99 乘法表不需要 Transformer:只有 100 条事实,MLP 用更少参数就能 100% 记住
  5. 但 MLP 无法扩展:多位数乘法参数量指数增长。RNN 有信息衰减。Transformer + CoT 通过分步推理解决了这个问题
  6. ReLU 比 sigmoid 快 4 倍收敛:正区间梯度=1,没有梯度消失。这就是现代网络几乎都用 ReLU 系列的原因