| 参数 | 值 | 说明 |
|---|---|---|
| d_model | 256 | 嵌入维度 |
| n_heads | 8 | 注意力头数 |
| n_layers | 6 | Decoder 层数 |
| d_ff | 1024 | FFN 隐藏层 |
| vocab_size | 20 | 0-9 + 运算符 + 特殊token |
| 位置编码 | RoPE | 零参数,旋转式 |
| 总参数 | 4.74M | 比 Ch1 少 39,936 参数 |
标准 Transformer Decoder,唯一架构改动:删除 Learned Positional Embedding,在 Attention 内部用 RoPE 旋转 Q/K。
在 Ch1 中,我们的模型用的是 可学习位置编码 (Learned Positional Embedding):
self.pos_emb = nn.Embedding(max_len, d_model) # [64, 256] 查找表
# forward:
x = tok_emb(input_ids) + pos_emb(positions) # 直接相加!
每个位置 0, 1, 2, ... 都有一个独立的 256 维向量,和 token embedding 直接做加法。
模型学到的是"位置3的向量",但它真正需要的是"这两个 token 相隔多远"。
想象一下 CoT 中:
23*45=→S1: 3*5=15→S2: 3*4=12→ ...模型需要知道
3和5是"相邻的乘法因子"(相对关系),而不是"一个在位置7、一个在位置9"(绝对位置)。
pos_emb = nn.Embedding(64, 256) — 训练时只见过 64 个位置。
如果 CoT 推理链变长到 80 步?直接崩溃 —— 位置 65~79 根本没有对应向量。
tok_emb("3") + pos_emb(7) 把位置信息混进了语义向量。
到了 Attention 计算时,Q·K 既包含"这两个 token 语义相关吗?"也包含"位置编码加法残留的干扰",无法干净分离。
RoPE 不再把位置信息"加"到 embedding 上,而是在 Attention 计算时,旋转 Q 和 K 向量。
关键洞察: - 位置 m 的 Q 向量旋转 m·θ 角度 - 位置 n 的 K 向量旋转 n·θ 角度 - Q·K 点积时,旋转效果变成 (m-n)·θ —— 只取决于相对距离!
对于 d_model=256 的向量,RoPE 把每个 head 的 32 维向量拆成 16 个 2D 子空间。
每个 2D 子空间独立旋转,旋转角度为 m × θᵢ:
θᵢ = 1 / 10000^(2i/d_head)
这是 RoPE 最精妙的数学性质:
Q_m · K_n = R(mθ)q · R(nθ)k = q · R((m-n)θ) · k
旋转矩阵的正交性让点积只依赖角度差 (m-n)θ,即相对位置!
| 特性 | Learned PE (Ch1) | RoPE (Ch2) |
|---|---|---|
| 位置信息类型 | 绝对位置 | 相对位置 |
| 注入方式 | 加到 embedding 上 | 旋转 Q, K 向量 |
| 影响 V? | ✅ 是(污染了输入) | ❌ 否(只影响注意力分数) |
| 长度外推 | ❌ 不行,超出就崩 | ✅ 理论上无限外推 |
| 额外参数 | max_len × d_model | 零参数! |
| 代码位置 | Embedding 层 | Attention 内部 |
23*45 的 CoT 有 ~40 tokens,347*892 有 ~80+ tokens。Learned PE 需要预设 max_len 大到足够,浪费参数;RoPE 天然支持任意长度。
CoT 中 S1→S2→S3→... 每步引用上一步的结果。模型需要知道"上一步在哪"(相对位置),而不是"S2 在绝对位置 25"。
Learned PE 在 Ch1 中占 64×256 = 16,384 个参数。RoPE 零参数。对我们这种小模型来说,省下的参数量显著降低过拟合风险。
# Ch1: 加法式位置编码
- self.pos_emb = nn.Embedding(max_len, d_model)
- x = tok_emb + pos_emb
# Ch2: RoPE(在 Attention 内部应用)
+ # 不再需要 pos_emb!
+ # 在 Attention 中,计算 Q, K 之后:
+ Q = apply_rope(Q, positions) # 旋转 Q
+ K = apply_rope(K, positions) # 旋转 K
+ # V 不旋转!
+ scores = Q @ K^T / sqrt(d_head) # 点积自动编码相对位置
courses/ch2_rope/
├── README.md ← 中文完整文档 (原理 + 对比 + RoPE vs FFT)
├── README_en.md ← 英文完整文档
├── COMPARISON.md ← Ch1 vs Ch2 独立对比报告
├── ROPE_VS_FFT.md ← RoPE 与 FFT 相位旋转的数学联系
│
├── model.py ← 🧠 RoPE 模型 (核心改动在这里)
├── train.py ← 训练脚本 (Teacher Forcing + CosineLR)
├── generate_data.py ← CoT 乘法数据生成 (与 Ch1 格式相同)
├── eval_full.py ← 全量 9801 组合评估
├── infer.py ← 交互式推理
│
├── images/ ← 7 张 SVG 图解
│ ├── svg_learned_pe.svg 图1: Ch1 加法式位置编码原理
│ ├── svg_pe_problems.svg 图2: 加法编码的三大缺陷
│ ├── svg_rope_idea.svg 图3: RoPE 旋转核心思想
│ ├── svg_rope_multifreq.svg 图4: 多频率多分辨率旋转
│ ├── svg_rope_relative.svg 图5: 相对距离编码数学证明
│ ├── svg_rope_cot_value.svg 图6: RoPE 对 CoT 任务的价值
│ └── svg_rope_vs_doa.svg 图7: RoPE 与 DOA 波束形成对比
│
├── checkpoints_v3/ ← 最终模型 (全量 9801/9801 = 100%)
│ ├── best.pt 测试集首次 100% 时的快照
│ ├── latest.pt 最终训练完成的模型 ← 推荐使用
│ └── train_log.csv 训练日志 (epoch, loss, acc)
│
└── data/ ← 训练/测试数据
├── train.txt 5864 条训练样本 (乘法+加法+基本功)
├── test.txt 500 条测试样本
└── train_pairs.txt 3000 个乘法组合对
python generate_data.py --num_train 3000 --num_test 500 --num_add_practice 2000 --output_dir data
数据格式与 Ch1 完全相同,例如:
23*45=S1:23*40=920;S2:23*5=115;A1:920+115=5301;Z1035
python train.py \
--train_file data/train.txt \
--test_file data/test.txt \
--epochs 200 \
--batch_size 32 \
--d_model 256 --n_heads 8 --n_layers 6 --d_ff 1024 \
--max_len 64 \
--lr 3e-4 \
--ckpt_dir checkpoints
训练过程:
- 每 epoch 在 mini-batch 上随机抽 100 条评估 ans_acc
- 每 5 epochs 在完整 500 条测试集上评估
- 自动保存 best.pt(首次 100%)和 latest.pt(最新)
- 训练日志写入 checkpoints/train_log.csv
预期收敛: - ~25 epochs → 90% acc - ~42 epochs → 99% acc - ~47 epochs → 测试集 100%
python eval_full.py --checkpoint checkpoints/latest.pt
遍历 1~99 × 1~99 = 9801 个乘法组合,逐一自回归生成并验证最终答案。
输出示例:
🏆 最终结果: 9801/9801 = 100.00%
错误数: 0
🎉🎉🎉 完美! 全部 9801 个组合 100% 正确!
可加 --save_errors 保存错误详情到文件。
python infer.py --checkpoint checkpoints/latest.pt
输入任意乘法题(如 67*89),观察模型的完整 CoT 推理过程:
🔢 输入: 67*89=
🤖 输出: S1:67*80=5360;S2:67*9=603;A1:5360+603=3695;Z5963
✅ 正确! 67×89 = 5963
| Ch1 (Learned PE) | Ch2 (RoPE) | 差异 | |
|---|---|---|---|
| 总参数 | 4,784,128 | 4,744,192 | Ch2 少 39,936 |
| 位置编码参数 | 16,384 | 0 | ✅ RoPE 零参数 |
| 里程碑 | Ch1 (Learned PE) | Ch2 (RoPE) | 加速比 |
|---|---|---|---|
| 50% acc | Epoch 23 | Epoch 16 | 1.4× |
| 80% acc | Epoch 36 | Epoch 22 | 1.6× |
| 90% acc | Epoch 49 | Epoch 25 | 2.0× |
| 95% acc | Epoch 67 | Epoch 33 | 2.0× |
| 99% acc | Epoch 131 | Epoch 42 | 3.1× |
RoPE 收敛速度是 Learned PE 的 2~3 倍!
| Ch1 (Learned PE) | Ch2 (RoPE) | |
|---|---|---|
| 测试集 (500) | 100% ✅ | 100% ✅ |
| 全量 9801 | 100% ✅ | 100% ✅ (9801/9801) |
tok·pos 交叉项干扰,注意力学习更纯粹RoPE 的旋转编码和信号处理中的频域波束形成 (DOA) 使用的是完全相同的数学操作。
DOA 波束形成(声学相机):
// 时间延迟 → 相位角
float phase_shift = -2πf * Δt;
// 复向量旋转补偿
intensity_real += real(freq) * cos(phase_shift) - imag(freq) * sin(phase_shift);
intensity_imag += real(freq) * sin(phase_shift) + imag(freq) * cos(phase_shift);
RoPE 位置编码(Transformer):
# 位置 → 旋转角度
angle = m * θᵢ # θᵢ = 1/10000^(2i/d)
# 复向量旋转编码
x_complex = torch.view_as_complex(x)
x_rotated = x_complex * (cos(angle) + j*sin(angle))
| DOA 波束形成 | RoPE 位置编码 | |
|---|---|---|
| 输入 | 麦克风信号(复数) | Q/K 向量(两两配对成复数) |
| 旋转角度 | -2πf·Δt(补偿延迟) |
m·θᵢ(编码位置) |
| 数学操作 | signal × e^{-jΔφ} |
q × e^{jmθ} |
| 代码 | r*cos - i*sin, r*sin + i*cos |
完全一样! |
| 目的 | 消除绝对延迟 → 对齐信号 | 编码绝对位置 → 提取相对距离 |
傅里叶时移定理: f(t - Δt) ⟷ F(ω) · e^{-jωΔt}
时域的"平移/延迟" = 频域的"相位旋转"
DOA 是"旋回来消除差异",RoPE 是"旋过去编码差异"——方向相反,数学一样。
一句话:RoPE 就是 Transformer 的"频域波束形成"——用相位旋转把位置信息编码进向量,让注意力机制能像声学相机对齐信号一样,自动感知 token 之间的相对距离。
Learned PE: 位置 → 查表 → 加到token上 → 绝对位置,无法外推
RoPE: 位置 → 算角度 → 旋转Q,K → 相对位置,无限外推,零参数
一句话:RoPE 用旋转几何取代了加法,让位置信息在注意力计算中以"相对距离"的形式自然涌现,同时不增加任何可学习参数。收敛速度比 Learned PE 快 3 倍,全量 9801 测试 100% 正确。