Transformer学习笔记:从架构原理到从零手写实现
这是一篇从零梳理 Transformer 的学习笔记。前半部分讲清 Encoder–Decoder 总览、Embedding / 张量、Self-Attention、Multi-Head、FFN、残差与层归一化、位置编码、Decoder 与 Mask;后半部分落到我在 F:\transformer_practice 里从零手写的模块——从 Embedding 到 Encoder Block、分类模型,再到 Seq2Seq 训练与自回归推理。架构理解主要参考 CSDN 详解与论文 Attention Is All You Need,再结合自己的推导与代码注释。所有框架图引用与中文注释均为我在学习过程中整理。
前言
2017 年 Google 提出 Transformer 之后,Self-Attention 几乎成了 NLP(以及后来多模态)的默认底座。和 RNN 比,它最大的优势是可以并行计算整段序列;和 CNN 比,它用注意力直接建模任意距离的依赖。
这篇笔记按我自己的学习路径展开:
- 先把模型当成黑盒,建立 Encoder–Decoder 直觉
- 补齐 Embedding、张量形状这些前置概念
- 拆解 Self-Attention → Multi-Head → FFN → Add & Norm → Positional Encoding
- 搞清 Decoder、Cross-Attention 与两种 Mask
- 在
transformer_practice里从零手写并跑通:分类 + Seq2Seq 生成 - 对照完成度,按 A→I 分阶段路径推进:Mask 全链路 → 真实数据 → Tokenizer → Beam Search → 规模升级 → 小型 BERT/GPT/T5
架构学习主要参考:Transformer 模型详解(CSDN),再按自己的理解增删。
一、先把 Transformer 看成黑盒
1.1 输入一句话,输出另一句话
总的来看:把一个句子作为输入,Transformer 把它翻译 / 生成成另一段序列输出。中间发生了什么,先当黑盒。
1.2 本质仍是 Encoder–Decoder
把中间拆开:左边是编码组件(多层 Encoder 堆叠),右边是解码组件(相同层数的 Decoder 堆叠)。论文默认各 6 层,实际可改。
| 组件 | 子层 | 作用 |
|---|---|---|
| Encoder | Self-Attention + FFN | 读懂输入句,得到上下文表示 |
| Decoder | Masked Self-Attention + Cross-Attention + FFN | 看已生成内容 + 看 Encoder 输出,预测下一个 token |
一句话:Encoder 负责“理解输入是什么”,Decoder 负责“一步步写出答案”;Cross-Attention 把两边接起来。
整体架构图(也是本文封面):
我自己的串讲:
1 | 输入 x |
二、前置概念:Embedding 与张量
Transformer 不能直接处理汉字,只能处理数字。Embedding 的作用,就是把每个词变成一串数字。
2.1 Embedding:查表把词变成向量
例如“我爱学习”:
| 词 | 向量(论文里通常 512 维) |
|---|---|
| 我 | [0.2, 0.7, -0.1, …] |
| 爱 | [0.8, -0.2, 0.4, …] |
| 学习 | [0.3, 0.9, 0.6, …] |
表里的数字是训练学出来的特征。三个词拼起来就是形状 3×512 的矩阵:
- $L$:序列长度(几个 token)
- $d_{\text{model}}$:每个 token 用多少维表示(论文 = 512)
Embedding 只发生在最底层。 底部 Encoder 吃的是词向量;上面各层吃的是上一层 Encoder 的输出。
2.2 张量的本质:批量句子要补齐长度
现实里句子长短不一:
1 | 句子1:我 爱 你 → 3 个词 |
为了一次处理,常统一到固定长度(比如 L=5),短的补 [PAD]。若一次处理 $n$ 句,就是:
1 | (batch_size, sequence_length, embedding_dimension) |
这就是 PyTorch 里常见的三维 Tensor——多维数字数组。
三、Self-Attention:让机器知道 “it” 指谁
3.1 动机
句子:
The animal didn’t cross the street because it was too tired
人知道 it 指 animal,机器不容易。Self-Attention 让模型在编码某个词时,也能看句子里其他位置的词,把相关信息融进来。
3.2 公式:Scaled Dot-Product Attention
Self-Attention 里,Q、K、V 都来自同一输入 $X$。
3.3 逐步拆解(向量视角)
六步走完 Self-Attention
① 线性映射出 Q、K、V
每个词向量 $x_i$ 分别乘三个可学习矩阵 $W^Q, W^K, W^V$,得到 $q_i, k_i, v_i$。论文里词向量 512 维,单头 QKV 常取 64 维。
② 算注意力分数
编码 “Thinking” 时,用它的 $q_1$ 与每个位置的 $k_j$ 做点积:$q_1\cdot k_1,\ q_1\cdot k_2,\ \ldots$
③ 除以 $\sqrt{d_k}$
防止点积过大,让 Softmax 梯度更稳。
④ Softmax 归一化
分数变成和为 1 的正数权重——当前位置通常最高,但相关词也会分到权重。
⑤ 权重 × Value
权重大的位置,对应 $v$ 贡献大;权重小的几乎被忽略。
⑥ 加权求和
得到该位置 Self-Attention 的输出,再送进 FFN。
3.4 矩阵形式(真正写代码时用这个)
把所有词排成矩阵 $X$,一次算出:
再:
我在 attention.py 里用小例子验证过同一条链路:
1 | Q = W_Q(X); K = W_K(X); V = W_V(X) |
四、Multi-Head Attention:多个角度看关联
4.1 在干什么
Multi-Head = 同一份 $X$,用 h 套不同的 QKV 参数 做 h 次 Attention,再 Concat + Linear($W^O$)。
论文设定:$h=8$,$dk=d_v=d{\text{model}}/h=64$。
粗略流程:
1 | 同一个 X "Thinking Machines" |
本质:参数总量可控的前提下,把 QKV 映射到高维空间的不同子空间分别算 Attention,最后再拼回来。每个 head 看不同角度的关联(一个盯 animal,一个盯 tired),拼起来表示更丰富。
五、FFN、残差、层归一化
5.1 Position-wise FFN
一个 Encoder Layer 粗看是:
1 | 输入 X |
Position-wise:每个位置各自过同一个 FFN(共享 $W_1,b_1,W_2,b_2$),不是每个词一套网络。
论文:$d{\text{model}}=512$,$d{ff}=2048$,即:
先扩维再压回,是为了在更大特征空间里组合特征,同时保持主通道 512,方便残差相加、堆下一层。
5.2 Add & Norm
每个子层都是:
我把它读成:
1 | 1. SubLayer 学“新增信息” |
完整 Encoder Layer:
1 | 输入 X |
为了残差能直接相加,子层与嵌入的输出维度都要保持 $d_{\text{model}}=512$。
六、Positional Encoding:告诉模型“你站在哪”
Self-Attention 本身没有顺序(不像 RNN 一个一个扫)。所以要在进入 Encoder 之前,给每个位置加一个位置向量:
论文用固定的正弦 / 余弦:
维度与 Embedding 相同,可直接相加。
Embedding 告诉模型“你是谁”,Positional Encoding 告诉模型“你站在哪”。 PE 只在进入 Encoder 堆栈前加一次,不是 Attention 内部的东西。
我在 position.py 里按论文公式写过偶数维 sin、奇数维 cos,验证过形状仍是 [seq_len, d_model]。工程里部分脚本为了先跑通,还用过更简化的 sin(position)/cos(position)——思想一样,尺度细节以后可对齐论文。
七、Decoder 与 Mask
7.1 Decoder 在干什么
推理时 Decoder 自回归生成:
1 | 已生成的内容 → Decoder → 预测下一个 token → 拼进输入 → 继续 |
Decoder 每个 Layer 三块:
- Masked Self-Attention:只看已经生成的部分
- Encoder–Decoder Attention(Cross-Attention):Q 来自 Decoder,K/V 来自 Encoder 最终输出
- FFN
7.2 两种 Mask
Mask 的本质:在 Softmax 之前,把“不该看的位置”的分数改成极小数(如 $-\infty$),Softmax 后权重≈0。
| 类型 | 解决什么问题 | 做法 |
|---|---|---|
| Padding Mask | 补齐用的 PAD 没意义,不该被注意 | PAD 对应列 / 位置打成 $-\infty$ |
| Sequence / Causal Mask | Decoder 不能偷看未来词 | 上三角(未来位置)打成 $-\infty$,常用下三角 tril 保留可见区 |
因果掩码直觉:
1 | 生成第 3 个词时,只能看见第 1、2、3 个位置,看不见第 4、5… |
我在 mask.py 里写成:
1 | def create_causal_mask(seq_len, device): |
Attention 里配合:
1 | if mask is not None: |
7.3 最后的 Linear + Softmax
Decoder 输出仍是向量,要变成词:
1 | Decoder 输出向量 |
论文还有两个训练细节:Embedding 与最后 Linear 共享权重;Embedding 会乘 $\sqrt{d_{\text{model}}}$ 做尺度;子层与 Embedding+PE 后使用 Dropout=0.1,并配合 Label Smoothing=0.1。
八、从零实现:transformer_practice
理论串完后,我在 F:\transformer_practice 里不调用 HuggingFace 封装,用 PyTorch 从零搭模块。目标是理解完整架构,而不是追 SOTA。
8.1 项目结构
1 | transformer_practice/ |
8.2 Encoder Block:我写的核心前向
transformer.py 里的 TransformerBlock 对应“一层 Encoder”:
1 | class TransformerBlock(nn.Module): |
我对这段代码标的几个点
- 先 Linear 再按 head 拆分:等价于多套 $W_i^Q$ 的一种高效写法
transpose(1,2):让 head 维靠前,方便[B,H,L,L]的 score- mask 在 Softmax 前
masked_fill,与论文一致 - 当前演示规模是
d_model=4, heads=2,结构对齐论文,参数量远小于 512/8/6
8.3 分类模型:Encoder + Mean Pooling
1 | Token → Embedding → +PE → Encoder×N → mean(dim=序列) → Linear → 类别 |
train.py 已跑通,accuracy = 1.0(玩具数据),权重存 model.pth;inference.py 可加载预测。
8.4 Seq2Seq:Encoder–Decoder + 自回归
结构:
1 | Encoder tokens → Emb+PE → Encoder → H |
训练(seq2seq_train.py):
1 | loss: 4.8 → 0.08 |
推理(seq2seq_inference.py):从 [BOS] 起步循环:
1 | encoder: [5, 27, 91] |
这说明:在玩具设定下,Decoder 已经具备自回归生成能力。接下来的瓶颈不在“能不能生成”,而在 Mask 是否严谨、数据是否真实、规模是否接近论文。
8.5 当前完成度
| 模块 | 完成度 | 说明 |
|---|---|---|
| Embedding / PE / Attention / MHA | ██████████ 100% | 结构与公式对齐 |
| Encoder / Classifier / Seq2Seq 训练推理 | ██████████ 100% | 玩具数据已跑通 |
| Decoder | ████████░░ 80% | 主路径有,正式 Mask 集成仍可加强 |
| Mask | ████░░░░░░ 40% | mask.py 已写,需贯穿全链路 |
| 真实数据 / Tokenizer / GPT·BERT | ░░░░░░░░░░ 0% | 下一阶段 |
九、阶段性判断与后续具体路径
9.1 现阶段能下的结论
可以比较稳妥地写:
我已经从公式层面打通 Transformer 主路径,并在 PyTorch 中从零实现了 Encoder Block、分类头与 Encoder–Decoder Seq2Seq;在小规模模拟数据上,分类准确率与 Seq2Seq 拟合都验证了链路正确。距离论文级完整实现,主要缺口在 Mask 体系贯穿、真实语料与 Tokenizer、以及模型规模。
当前不是“推倒重来”,而是按缺口补齐。总路线一句话:
1 | Mask 全链路打通 |
原则和做 U-Net 对照实验时一样:一次只推进一个阶段、每阶段有可验证的验收标准,不要同时改 Mask、换数据集又升 d_model。
9.2 阶段总览(先看地图)
| 阶段 | 目标 | 主要改动文件 | 验收标准 |
|---|---|---|---|
| A | Mask 系统可用 | mask.py |
causal / padding 形状正确,可视化下三角与 PAD 列为 0 |
| B | Attention 统一吃 mask | transformer.py、decoder_layer.py |
Softmax 前 masked_fill;非法位置权重≈0 |
| C | Encoder/Decoder 全链路传 mask | encoder.py、decoder_stack.py、transformer_seq2seq.py |
训练/推理都传入 mask,PAD 不再影响表示 |
| D | 带 mask 的玩具 Seq2Seq 复训 | seq2seq_train.py、seq2seq_inference.py |
有 PAD 的 batch 仍能拟合;生成不偷看未来 |
| E | 真实数据管线 | 新建 dataset.py / collate |
DataLoader 出齐 src/tgt/mask |
| F | Tokenizer | 新建 tokenizer 相关 |
句子→ids→模型→ids→句子闭环 |
| G | Beam Search | 新建 beam_search.py 或扩推理脚本 |
同输入可出多候选并选最优 |
| H | 规模升级 | 配置与 FFN 维度 | 接近 512/8/6,能稳定训几个 epoch |
| I | 三条产品线分叉 | 新目录或新入口脚本 | 选定 BERT / GPT / T5 之一做出最小可跑 Demo |
下面按阶段写出具体怎么做。
9.3 阶段 A:把 Mask 做成可复用工具
要解决的问题
- Padding:短句补的
PAD不该被注意 - Causal:Decoder 第 $t$ 步不能看见 $t+1$ 及以后
建议文件:继续完善 mask.py(已有雏形)
1 | # 目标 API(保持简单、形状统一) |
形状约定(写进注释,后面少踩坑)
Attention score 形状一般是 [B, H, Lq, Lk],所以 mask 广播维建议:
| Mask | 建议形状 | 挡住谁 |
|---|---|---|
| Causal | [1,1,L,L] | 未来位置(上三角) |
| Padding(对 Key) | [B,1,1,Lk] | PAD 作为被关注对象 |
| 组合后 | [B,1,L,L] 或可广播到 score | Decoder Self-Attn |
约定:1=可见,0=屏蔽;在 score 上 masked_fill(mask==0, -inf)。
验收
- 打印
create_causal_mask(5),确认是下三角 1 tokens=[[5,27,0,0]]的 padding mask,最后两列全 0- 组合后:既不能看未来,也不能看 PAD
9.4 阶段 B:Attention 接口统一支持 mask
现状
transformer.py的 Block 已支持mask=None+masked_filldecoder_layer.py内部attention()也支持 mask- 但上层调用是否每次都传入、Encoder 是否向下传递,还不完整
要做的事
- 统一签名:所有算 Attention 的地方都是
attention(Q, K, V, mask=None) - Softmax 之前屏蔽;用
float("-inf")比极大负数更干净(Decoder 里已有写法,Encoder Block 可对齐) - 可选:对 attention weights 再 Dropout(论文子层做法)
涉及文件
1 | transformer.py ← Encoder Block |
验收
构造一个「故意把未来位置设为可见会泄题」的单元测试:加上 causal mask 后,第 1 行对第 3、4 列的权重必须≈0。
9.5 阶段 C:Encoder / Decoder 堆栈真正把 mask 传下去
这是当前工程里最关键的缺口之一。
现状问题(对照代码)
1 | # encoder.py 当前大致是: |
即使 Block 支持 mask,Encoder 不往下传等于没用。
目标调用链
1 | train / infer |
建议修改清单
| 文件 | 改动 |
|---|---|
encoder.py |
forward(self, x, mask=None),layer(x, mask=mask) |
decoder_stack.py |
确认每层都收到 self_mask / cross_mask |
transformer_seq2seq.py |
forward 已有 mask 参数则打通;训练时不要再传 None 敷衍 |
seq2seq_train.py |
batch 里构造 mask 再喂模型 |
seq2seq_inference.py |
逐步生成时,每步按当前长度重建 causal mask |
Cross-Attention 的 mask 通常是 Encoder 侧 padding mask(挡住源句 PAD),不是 Decoder 的因果掩码。因果掩码只给 Decoder 的 Self-Attention。
验收
- 源句带 PAD:Encoder 输出中,PAD 位置不应再被其它位置强烈关注(可打印 attention 权重做一次可视化)
- 推理时拉长已生成序列:每步 mask 尺寸随
decoder_tokens.shape[1]增长
9.6 阶段 D:用「带 PAD 的玩具数据」复训一轮
在上真实语料前,先用故意补 PAD 的假数据验证 Mask 真的生效。
做法
- 把现在定长、无 PAD 的
encoder_tokens改成不等长 +collate补 PAD target的 PAD 位置在 CrossEntropy 里用ignore_index=pad_id- 训练后检查:
- loss 仍能下降
- 推理生成序列合理
- (可选)把 causal mask 去掉做反例:训练会“作弊”,或生成异常——用来证明 mask 有用
建议命令习惯
1 | cd F:\transformer_practice |
每完成一阶段保留权重命名,例如 seq2seq_model_masked.pth,避免覆盖最早能跑通的 checkpoint。
9.7 阶段 E:真实 Seq2Seq 数据管线
目标任务(建议先选一个)
| 候选 | 输入 → 输出 | 为什么适合现阶段 |
|---|---|---|
| 英→中 / 中→英 小平行语料 | 翻译 | 最贴近原论文场景 |
| 文本摘要小样本 | 长 → 短 | 练 Encoder–Decoder |
| 拼写纠错 / 拼音→汉字 | 序列转换 | 数据好造 |
新建模块建议
1 | dataset.py |
训练脚本改造点
1 | DataLoader → 每个 batch 带齐 tensors + masks |
数据可以很小(几百~几千句)先跑通。先求 pipeline 正确,再求效果。
验收
- 一个 batch 里既能有短句也能有长句
ignore_index后 PAD 不贡献 loss- 验证集上能打印「源句 / 真值 / 预测」三条对照
9.8 阶段 F:Tokenizer(替换手写 token id)
现状:vocab_size=100 + 手工数字 id,只能验证结构。
路径(由易到难)
1 | 临时:空格分词 / 字符级 vocab |
建议闭环
1 | 原始句子 |
验收
- 任意一句中文/英文,encode→decode 可还原(或按 tokenizer 规则合理还原)
- 词表大小、UNK 比例心里有数
- 训练脚本不再出现「写死的
[5,27,91]」作为唯一数据源
9.9 阶段 G:推理从 argmax 升级到 Beam Search
现状(seq2seq_inference.py):每步取 argmax,贪心一条路走到底。
升级目标
1 | 每步保留 beam_size 条部分序列 |
建议新建 beam_search.py:
1 | def beam_search(model, src, bos, eos, beam_size=4, max_len=50): |
验收
- 同一
src,beam_size=1应退化成接近贪心 beam_size=4能给出不同于贪心的候选,并按分数排序- 仍遵守 causal mask(不能因搜多条就泄露未来)
9.10 阶段 H:模型规模向论文靠拢
当前演示配置 vs 论文
| 项 | 现在(练习) | 论文原版量级 |
|---|---|---|
d_model |
4 | 512 |
num_heads |
2 | 8 |
d_k |
2 | 64 |
d_ff |
8(示意) | 2048 |
num_layers |
2~6 | 6 |
| Dropout | 部分有 0.1 | 0.1 |
| 位置编码 | 有简化版 | sin/cos 公式版 |
建议升配节奏(仍一次动一处)
1 | ① d_model=64, heads=4, layers=2, d_ff=256 ← 先在真实小数据上稳 |
同时把 position.py / 模型内 PE 统一成论文 sin/cos(含 10000^{2i/d}),去掉仅 sin(pos) 的占位写法。
升规模后显存和训练时间会上去。可先 CPU 小配置验证正确性,再 GPU 训。学习率、warmup、Label Smoothing 也可在这一阶段按论文补上。
验收
- 配置改大后 forward 形状正确:
[B,L,512]贯穿 - 小语料上 loss 能下降(不要求立刻赶超预训练模型)
- 同一套 Mask/Tokenizer 代码无需推倒重来
9.11 阶段 I:三条最终方向(择一路做深)
Mask + 真实数据 + 推理稳定之后,再分叉——不要三线同时开:
方向 1:Encoder 系 ≈ 小型 BERT
- 只保留 Encoder Stack
- 任务:Masked Language Modeling(随机遮词预测)
- 下游:
[CLS]向量做分类 - 适合巩固双向 Self-Attention + Padding Mask
方向 2:Decoder 系 ≈ 小型 GPT
- 只保留 Decoder(或 Encoder Block + 强制 causal mask)
- 任务:Language Modeling(预测下一个 token)
- 下游:续写 / 对话玩具
- 适合巩固因果掩码与自回归训练
方向 3:Encoder–Decoder ≈ 小型 T5
- 保持现在的 Seq2Seq 骨架
- 任务:翻译 / 摘要 / 改写
- 最贴近当前
transformer_seq2seq.py的自然延伸
若与泡沫图像项目并行:CV 侧继续 Boundary / 实例分割;NLP 侧建议优先走 方向 3(T5 式 Seq2Seq),因为现有代码资产最多,迁移成本最低。想补「纯生成」再开 GPT 线。
9.12 推荐时间盒与里程碑(可按周执行)
1 | 第 1 周:阶段 A + B + C |
9.13 路径总图
1 | 【已完成】 |
十、总结
把整条学习线收个尾:
- 总览:Transformer = Encoder 堆栈 + Decoder 堆栈;并行算整句,靠注意力建长距离依赖
- 表示:Embedding 解决“词是谁”,PE 解决“词在哪”,合成 $X\in\mathbb{R}^{L\times d}$
- Self-Attention:$QK^T/\sqrt{d_k}$ → Softmax → 加权 $V$;矩阵化才能高效训练
- Multi-Head:多子空间看关联,Concat 后再 $W^O$
- FFN / 残差 / LN:交流后各自加工;
LayerNorm(x+SubLayer(x))让深层可训 - Decoder / Mask:因果掩码防未来泄露;Cross-Attention 用 Encoder 的 K、V
- 从零代码:
transformer_practice已跑通分类与 Seq2Seq 生成 - 后续路径:Mask 全链路 → 带 PAD 复训 → 真实数据 → Tokenizer → Beam Search → 规模升级 → BERT/GPT/T5 三选一做深
从“会背公式”到“能手写出带 mask 的 forward,并训练出能自回归吐 token 的模型”,再到“按验收标准一步步补到论文级工程”,这条线和学 U-Net 时一样——核心始终是:每个张量从哪来、变成什么形状、这一层到底在解决什么问题,以及下一步只改一个变量。
参考资料
- Vaswani A. et al. Attention Is All You Need (NeurIPS 2017)
- Transformer 模型详解(CSDN,架构主参考):https://blog.csdn.net/benzhujie1245com/article/details/117173090
- 个人整理:
transformer架构.md、Transformer 从零实现项目进度总结与后续计划.md - 手写工程:
F:\transformer_practice - 关联学习:U-Net / Attention U-Net 图像分割笔记(同系列,CV 侧注意力对照)


















