第 7 课:第一个 MLP —— 教会神经网络算 XOR
第 7 课:第一个 MLP —— 教会神经网络算 XOR
代码位置:src/main.rs(
demo_xor函数) 前置知识:第 5 课 Linear / 激活函数、第 6 课 CrossEntropy / SGD
1. 本课要搞懂的问题
- XOR 为什么"看着简单却学不会"?什么是线性不可分?
- 为什么必须加隐藏层和非线性激活?
demo_xor的每一行代码在干什么?- 为什么 XOR 训练到 4/4 就能证明反向传播是对的?
2. XOR:简单到"一眼看穿",难到"一条直线搞不定"
异或(XOR):两个输入相同输出 0,不同输出 1。
| 输入 x1 | 输入 x2 | 输出 y |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
把四个点画在平面上(● = 输出 1,○ = 输出 0):
x2
1 ─ ●(0,1) ○(1,1)
│
0 ─ ○(0,0) ●(1,0)
└──────────────── x1
问题来了:能不能画一条直线,把 ● 和 ○ 分开?不能。 一条直线最多把平面分成两半,而两个 ● 在对角线上——无论直线怎么放,总有一侧同时混着 ● 和 ○。这就是线性不可分。
3. 为什么必须非线性:单层线性模型必败
- 第 2 课的线性回归只有一层:
pred = xW + b,它学出的边界本质是一条直线 - 对 XOR,直线永远分不开 → 单层模型最多只能对 3 个样本正确(3/4),永远到不了 4/4
- 解法:隐藏层 + 非线性激活(第 5 课学的 Tanh)。非线性可以把输入空间"折叠 / 扭曲",让原本线性不可分的点在高层变得线性可分
直觉(4 个样本为什么能学会):
- 隐藏层 4 个神经元 ≈ 学出 4 种"特征组合"
- Tanh 把每个特征压缩到 (-1, 1),给特征带来非线性
- 第二层再线性组合这些特征,就能画出"折线"边界,把四个点完美分开
4. 数据集与网络结构:2 → 4 → 2
数据集(4 个样本,一次全部喂入,batch = 4):
// 数据集:4 个样本
let x_data = Tensor::from_vec(
vec![0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0],
vec![4, 2],
);
let y_targets = vec![0usize, 1, 1, 0]; // XOR 真值表
x_data:[4, 2],四行正好是 (0,0)、(0,1)、(1,0)、(1,1)y_targets:对应输出 0、1、1、0
网络(两个 Linear 叠成 MLP):
let fc1 = Linear::new(2, 4, &mut rng); // 输入 2 维 → 隐藏层 4 维
let fc2 = Linear::new(4, 2, &mut rng); // 隐藏层 4 维 → 输出 2 维
结构图:
x [4,2] ─► fc1 (2→4) ─► Tanh ─► h [4,4] ─► fc2 (4→2) ─► logits [4,2]
- 输出层是 2 个神经元:第 0 个是"类别 0 的分数",第 1 个是"类别 1 的分数"
- 为什么不直接用 1 个输出(0 或 1)?因为配合 CrossEntropy 分类更自然,两个分数取 argmax 就是预测
收集参数 + 创建优化器:
let params: Vec<Tensor> = {
let mut ps = fc1.parameters();
ps.extend(fc2.parameters());
ps
};
let opt = SGD::new(0.5, params);
fc1.parameters()+fc2.parameters():共 2×4+4 + 4×2+2 = 22 个参数- 学习率 0.5:XOR 问题简单,步长可以大一点
- 随机种子固定为 42(
Rng::new(42)),每次运行初始化相同、结果可复现
5. 训练循环:1000 步
for step in 0..1000 {
// 前向:tanh(x @ W1 + b1) @ W2 + b2
let h = tanh(&fc1.forward(&x_data));
let logits = fc2.forward(&h);
let loss = cross_entropy_loss(&logits, &y_targets);
loss.backward();
opt.step();
opt.zero_grad();
if step % 200 == 0 {
println!(" step {:>4} | loss {:.4}", step, loss.data()[0]);
}
}
对应第 6 课的"四步舞":
| 步骤 | 代码 | 说明 |
|---|---|---|
| 前向 | fc1.forward(&x_data) → tanh → fc2.forward(&h) |
4 个样本一次算完 |
| 损失 | cross_entropy_loss(&logits, &y_targets) |
分类交叉熵 |
| 反向 | loss.backward() |
自动微分算出 22 个参数的梯度 |
| 更新 | opt.step() |
θ = θ − 0.5·g |
| 清零 | opt.zero_grad() |
防止梯度跨轮累加 |
观察 loss:每 200 步打印一次,趋势是单调下降并趋近 0(具体数值由种子 42 的初始化决定,不必纠结;刚初始化时接近随机猜,2 类问题约等于 −ln 0.5 ≈ 0.69)。
6. 验证:4/4 才算赢
训练完要"考试":
// 验证正确率
let h = tanh(&fc1.forward(&x_data));
let logits = fc2.forward(&h);
let data = logits.data();
let mut correct = 0;
for i in 0..4 {
let pred = if data[i * 2] > data[i * 2 + 1] { 0 } else { 1 };
if pred == y_targets[i] {
correct += 1;
}
}
println!(" 训练后正确率:{}/4(100% 说明反向传播正确)\n", correct);
- 对每个样本取 argmax:类别 0 的分数
data[i*2]大于类别 1 的分数data[i*2+1]→ 预测 0,否则预测 1 - 期望输出:
训练后正确率:4/4
7. 为什么 4/4 就能证明反向传播正确
推理链条:
- 参数是随机初始化的(种子 42),绝不是"碰巧答对";
- 梯度完全来自自动微分:任何一个运算(matmul / add / relu / softmax / log / sum ...)的反向实现出错,梯度就会偏一点,参数越走越偏,1000 步后必然累积成大错;
- XOR 没有捷径:它是"必须两层非线性"的问题,梯度要穿透 Tanh 的导数(
1 - t²)、两层 matmul 的转置路径、softmax 的雅可比路径——整条反向传播链路被真实训练"压测"了一遍; - 结果 100% 正确 + loss 单调下降,还同时验证了优化器(SGD 的
set_data写回)和训练循环(backward → step → zero_grad 的顺序)都是对的。
换句话说:XOR 训练收敛到 4/4,等于对"张量库 + 网络层 + 损失 + 优化器"整条链路做了一次端到端集成测试。
8. 运行
cargo run
输出第一段(loss 数值随种子确定,趋势如下):
=== 演示 1:MLP 学习 XOR(第 7 课)===
step 0 | loss ...
step 200 | loss ...
step 400 | loss ...
step 600 | loss ...
step 800 | loss ...
训练后正确率:4/4(100% 说明反向传播正确)
9. 动手练习
- 把隐藏层大小从 4 改成 2(
Linear::new(2, 2, ...)),观察还能不能到 4/4(提示:2 个隐藏神经元很可能不够)。 - 把
tanh换成relu或gelu,其他不变,观察收敛速度差异。 - 把学习率改成 0.05 和 5.0,对比 loss 下降曲线(太小慢、太大震荡)。
- 去掉
tanh(h = fc1.forward(&x_data)直接喂给 fc2),验证"没有非线性就学不会 XOR",观察正确率卡在 3/4。 - 把训练步数改成 100(可能还没收敛)和 10000(数据只有 4 个样本,看会不会过拟合),观察 loss 与正确率。
10. 本课总结
- XOR 线性不可分 → 必须隐藏层 + 非线性激活
demo_xor完整走通:数据集 → 网络 2→4→2 → 训练 1000 步 → 验证 4/4- 4/4 = 反向传播正确的强证据(端到端集成测试)
- 从现在起,我们有了一个能训练任意小分类问题的完整工具箱
- 下一课:给模型喂真正的文本——BPE 分词器
Rust 大语言模型 学习指南共 22 章
1从零用 Rust 实现大语言模型 —— 学习计划2第 1 课:张量 Tensor —— 一切的基础3第 2 课:自动微分 Autograd —— 让模型学会「自我修正」4第 3 课:张量运算扩展 —— 广播、归约、softmax、批量矩阵乘法5第 4 课:模块化重构 —— 项目结构分层与 Rc<RefCell> 架构6第 5 课:线性层与激活函数 —— 神经网络的「积木」7第 6 课:损失函数与优化器 —— 让模型知道「错在哪、怎么改」8第 7 课:第一个 MLP —— 教会神经网络算 XOR本篇9第 8 课:BPE 分词器 —— 让模型「读懂」文字10第 9 课:注意力机制 —— 让 token 互相「看」11第 10 课:多头注意力 —— 让模型「多角度」看世界12第 11 课:位置编码与归一化 —— 让序列带上「位置感」13第 12 课:完整 GPT 模型 —— 把积木拼成能预测下一个词的模型14第 13 课:训练循环 —— 让模型真正开始学习15第 14 课:数据加载 —— 文本如何变成训练样本16第 15 课:推理与采样 —— 让模型「创造性」地生成17第 16 课:训练小 GPT —— 看 loss 从 1.46 一路降到 0.1618第 17 课:AdamW 优化器 —— 给梯度下降装上「惯性」和「自适应步长」19第 18 课:KV Cache —— 让逐 token 生成不再重复计算20第 19 课:RoPE 旋转位置编码 —— 把「相对位置」揉进注意力21第 20 课:学习率调度与收尾 —— warmup、cosine decay 与全项目总结22第 21 课:GPU 加速训练与推理