一、先把整个项目看成一个流水线
你这个项目完整流程其实是:
数据准备 → 分词/BPE → 构造 batch → 建模 → 训练 → 验证 → 推理 → BLEU 评估
你面试或者自己理解时,一定先抓住这条线,不然会陷在某个函数里。
我先用最朴素的话说一遍:
假设输入一句德语:
ich liebe dich
模型最终想输出一句英语:
i love you
那在这个项目里,它不是直接看字符串,而是这样走的:
第一步:文本预处理
先把句子分成 token,比如:
- 德语:
["ich", "liebe", "dich"] - 英语:
["i", "love", "you"]
再变成数字 id,比如:
- 德语:
[15, 208, 77] - 英语:
[9, 51, 33]
第二步:加特殊符号
为了让模型知道句子开始和结束,会加:
[BOS]:开始[EOS]:结束[PAD]:补齐长度
比如 decoder 的输入常常是:
[BOS], i, love, you
而训练标签是:
i, love, you, [EOS]
这就叫 teacher forcing。
第三步:Encoder 编码源语言
德语句子进 Encoder,经过多层自注意力,得到每个位置的上下文表示。
第四步:Decoder 逐步生成目标语言
Decoder 一边看自己前面已经生成的英语词,一边看 Encoder 输出的德语表示,然后预测下一个英语词。
第五步:算损失,反向传播
用预测结果和真实英语标签比,算交叉熵损失,更新参数。
第六步:评估
训练完后,用 BLEU 分数评估翻译效果。
二、这个项目的核心目标是什么
这个项目本质上是在实现论文《Attention is All You Need》里的经典 Transformer 翻译模型,只不过做了工程化包装。
你可以把它理解成:
- Encoder:负责“读懂德语”
- Decoder:负责“翻译成英语”
- Attention:负责“在处理某个词时,去看哪些词更重要”
三、先讲数据部分:项目是怎么把文本变成可训练数据的
1. 数据集 LangPairDataset
你的 LangPairDataset 干的事情很简单:
- 读取德语文件
*_src.bpe - 读取英语文件
*_trg.bpe - 一行德语对应一行英语
- 过滤掉长度太长的句子
- 缓存成
.npy
它本质上是:
每次取一个样本,就返回
(source_sentence, target_sentence)
比如:
return self.src[index], self.trg[index]
也就是返回:
- 源句子(德语)
- 目标句子(英语)
为什么要过滤最大长度?
因为 Transformer 的计算复杂度跟序列长度关系很大,长度太长:
- 显存占用大
- 训练慢
- 注意力矩阵变大
所以这个项目设置了 max_length=128。
为什么要做缓存?
因为每次都重新读文件、重新过滤会很慢。缓存之后,下次直接加载处理好的数据。
你可以这么记:
Dataset 负责“把原始文本样本准备好”。
2. Tokenizer:把单词变成数字
Tokenizer 这块很关键。
它做两件事:
编码 encode
把词列表变成 id 列表,比如:
["i", "love", "you"] -> [1, 9, 51, 33, 3, 0, 0]
这里可能:
1是[BOS]3是[EOS]0是[PAD]
解码 decode
把 id 再变回句子。
3. 为什么 Encoder 和 Decoder 输入不一样?
这个地方很多人第一次学 Transformer 会糊涂。
你的代码里:
Encoder 输入
[BOS] src [EOS] [PAD]
Decoder 输入
[BOS] trg [PAD]
Decoder 标签
trg [EOS] [PAD]
为什么?
因为训练时 Decoder 是“拿前面的正确词,去预测下一个词”。
举例:
目标句子是:
i love you
那么:
decoder_inputs
[BOS] i love you
decoder_labels
i love you [EOS]
这样模型在每个位置学的是:
- 看
[BOS],预测i - 看
[BOS] i,预测love - 看
[BOS] i love,预测you - 看
[BOS] i love you,预测[EOS]
这就是机器翻译训练的标准做法。
四、为什么这个项目要专门设计 BatchSampler
这是你项目里一个很加分的点,因为说明你不只是会调包,还考虑了效率。
1. 普通 batch 的问题
如果一句话很短,一句话很长,强行放在一个 batch 里,就要 pad 到同样长度。
例如:
- 句子 A 长度 5
- 句子 B 长度 100
那 A 也得补到 100,很浪费。
2. 你的做法:按长度分组
TransformerBatchSampler 的思路是:
- 先按照源句长、目标句长排序
- 把长度接近的样本分到同一个 batch
- batch 大小不是固定“样本数”,而是尽量控制“总 token 数”
代码里这个判断很关键:
if max_len * (len(self._batch) + 1) > self._batch_size:
意思是:
当前 batch 的最大长度 × 样本个数,不能超过设定阈值
所以这里的 batch_size=4096,其实更像是 token budget,不是固定 4096 条样本。
这样做的好处
- 减少 padding
- 提高显存利用率
- 提高训练效率
你可以这样理解:
不是每车都装固定“人数”,而是尽量按“总体积”装货。
这个思路在 NLP 里很常见,也很有面试价值。
五、现在进入核心:Transformer 到底是什么
这里我用尽量“老师讲课”的方式,不直接堆公式。
1. Transformer 最核心的一句话
Transformer 的本质,就是让每个词在表示自己时,都能动态地参考句子里的其他词。
比如德语句子:
ich liebe dich
当模型处理 liebe 时,它不会只看这个词自己,还会看:
- 谁是主语:
ich - 谁是宾语:
dich
于是 liebe 的表示就不是孤立的,而是带上下文的。
这就是 attention 的意义。
2. 为什么不用 RNN,而用 Transformer?
传统 RNN/LSTM 是按顺序读:
- 先读第 1 个词
- 再读第 2 个词
- 再读第 3 个词
问题:
- 难并行
- 长距离依赖难学
Transformer 不一样:
- 一整句同时看
- 任意两个位置都能直接建立联系
- 更容易并行训练
所以它特别适合翻译任务。
六、Embedding:词向量 + 位置编码
在你代码里是 TransformerEmbedding。
1. 为什么需要词向量
因为模型不能直接处理单词字符串,必须先变成向量。
比如:
ich→ 一个 512 维向量love→ 一个 512 维向量
这部分是 word_embedding。
2. 为什么还要位置编码
注意力本身不带顺序概念。
也就是说,如果只看词向量,模型并不知道:
- 谁在前
- 谁在后
可语言里顺序非常重要。
比如:
dog bites manman bites dog
单词一样,顺序不同,意思完全不同。
所以需要加入 position embedding / positional encoding。
3. 你这里的位置编码怎么实现的
你代码里用的是经典的 正弦/余弦位置编码:
- 偶数维用
sin - 奇数维用
cos
好处是:
- 不需要学习参数
- 不同位置有不同模式
- 模型能感知相对/绝对位置
公式本身你不用死背,重点理解:
它给每个位置生成一个独特的“位置向量”,然后和词向量相加。
最终输入表示是:
词向量 + 位置向量
这就是:
embeds = word_embeds + pos_embeds
七、Attention 是这个项目最核心的思想
现在讲最关键的 MultiHeadAttention。
1. 先别看公式,先理解 Query / Key / Value
这是很多同学最难理解的地方,我给你一个“查资料”的类比。
你去图书馆找资料:
- Query:你现在想问的问题
- Key:每本书的标签
- Value:每本书真正的内容
流程是:
- 用你的 Query 和每本书的 Key 做匹配
- 匹配度越高,说明这本书越相关
- 再把相关书的 Value 按权重加权求和
- 得到你最终需要的信息
Attention 就是在干这件事。
2. 在句子里怎么理解?
比如句子:
The animal didn’t cross the street because it was tired.
处理 it 的时候,模型会看:
animalstreet
通过 attention,它会发现 it 更可能指向 animal。
也就是说:
当前词会“关注”对它最重要的其他词。
3. 代码里怎么实现的
你的代码流程是:
第一步:线性映射得到 Q、K、V
self.Wq
self.Wk
self.Wv
输入 hidden states 后,映射成:
- Q
- K
- V
第二步:分头
_split_heads
把 512 维拆成 8 个头,每头 64 维。
为什么要多头?
因为不同头可以关注不同关系:
- 一个头看主谓关系
- 一个头看位置关系
- 一个头看词义相关性
第三步:算注意力分数
qk_logits = torch.matmul(querys, keys.mT)
就是 Q 和 K 做点积。
第四步:缩放
qk_logits / sqrt(head_dim)
为什么缩放?
因为维度大时点积值可能很大,softmax 会过于尖锐,不利于训练稳定。
第五步:mask
qk_logits += attn_mask * -1e9
mask 的位置加一个很大的负数,softmax 后就接近 0。
第六步:softmax
attn_scores = F.softmax(...)
得到“每个词该关注谁”的概率分布。
第七步:加权求和
embeds = torch.matmul(attn_scores, values)
用注意力权重对 V 加权。
第八步:多头合并
最后把多头拼回去,再过一层线性层 Wo。
八、什么是 Self-Attention,什么是 Cross-Attention
这也是面试高频点。
1. Self-Attention:自己看自己这句话
在 Encoder 里:
- Query 来自源句子
- Key 来自源句子
- Value 来自源句子
即:
德语句子内部自己建模内部关系
比如处理德语某个词时,可以去看同一句里的其他德语词。
2. Cross-Attention:目标句子去看源句子
在 Decoder 里有一层特殊注意力:
- Query 来自当前 Decoder 状态
- Key 来自 Encoder 输出
- Value 来自 Encoder 输出
即:
生成英语时,去参考德语的编码结果
这就是翻译真正发生的地方。
你可以记成:
- Self-Attention:句内理解
- Cross-Attention:跨语言对齐
九、Transformer Block 为什么是这个结构
你的 TransformerBlock 结构很标准:
- Multi-Head Attention
- Add & LayerNorm
- Feed Forward Network
- Add & LayerNorm
如果是 Decoder block,还会多一个 Cross-Attention。
1. 残差连接为什么重要
你代码里像这样:
hidden_states + self.self_dropout(...)
这就是残差连接。
作用:
- 防止深层网络训练困难
- 保留原始信息
- 让梯度更容易传播
可以理解成:
新信息学得不好,也别把老信息完全丢掉。
2. LayerNorm 为什么重要
self.self_ln(...)
作用:
- 稳定训练
- 缓解数值波动
- 让不同层输出分布更平稳
3. FFN 是干什么的
nn.Linear -> ReLU -> nn.Linear
Attention 更像“信息交换”,FFN 更像“特征加工”。
可以理解成:
- Attention 负责“从别人那里拿信息”
- FFN 负责“把拿到的信息再深加工一下”
十、Encoder 和 Decoder 各自干了什么
1. Encoder
TransformerEncoder 就是堆叠多个 Encoder block。
你的参数里:
"num_encoder_layers": 6
也就是 6 层 Encoder。
它的任务是:
把德语句子编码成一串上下文化表示
输出的是每个源位置的 hidden state。
2. Decoder
TransformerDecoder 也是堆 6 层,但每层比 Encoder 多了一个 cross-attention。
它做三件事:
- 对已生成的目标词做 masked self-attention
- 看 Encoder 输出做 cross-attention
- 过 FFN
最终输出每个目标位置的 hidden state。
十一、为什么 Decoder 要做 mask
这个问题非常重要。
1. 训练时不能偷看未来
比如目标句子是:
i love you
当预测 love 时,模型只能看:
[BOS]i
不能偷看后面的 you。
否则训练时它就作弊了。
2. 你代码里的 look-ahead mask
generate_square_subsequent_mask
生成的是一个上三角遮罩。
意思是:
- 当前位置只能看自己和前面
- 不能看后面
这就保证了自回归生成的因果性。
3. 还有 padding mask
句子长短不同,需要 pad。
但 pad 只是占位,不是真实词,所以:
- attention 不能关注 pad
- loss 计算也不能把 pad 算进去
所以你的项目里有两类 mask:
attention mask
用于注意力里屏蔽不该看的位置
padding mask
用于损失函数里不计算 pad 的损失
这个你 notebook 里其实已经总结得很好。
十二、TransformerModel 整体怎么串起来
你可以把 TransformerModel.forward() 看成整个模型的总控室。
它做的事是:
1. 构造 mask
- encoder 的 padding mask
- decoder 的 look-ahead mask
- decoder 的 padding mask
- cross-attention mask
2. 做 encoder embedding
encoder_inputs_embeds = self.src_embedding(encoder_inputs)
3. 送入 encoder
encoder_outputs = self.encoder(...)
4. 做 decoder embedding
decoder_inputs_embeds = self.trg_embedding(decoder_inputs)
5. 送入 decoder
decoder_outputs = self.decoder(...)
6. 线性映射到词表
logits = self.linear(...)
最后得到:
[batch_size, trg_len, vocab_size]
它表示:
每个位置,对词表里每个词的预测分数。
比如第 3 个位置,它会给出:
i: 0.1love: 2.8you: 1.2- …
分数最高的词就是模型当前认为最可能的输出。
十三、为什么输出层要接 Linear
因为 Decoder 输出的是 hidden state,不是具体单词。
要变成“预测哪个词”,必须映射到词表大小:
hidden_size -> vocab_size
所以需要最后一个线性层。
如果开启 embedding sharing,还会把输出层和 embedding 权重共享,这样:
- 参数更少
- 有时泛化更好
你这里配置里是:
"share_embedding": False
说明当前实验没有共享。
十四、损失函数是怎么设计的
你的 CrossEntropyWithPadding 很标准,也很重要。
1. 为什么用交叉熵
翻译本质上是:
每个位置做一次多分类
因为每个位置都要从整个词表里选一个词。
所以自然用交叉熵。
2. 为什么要忽略 padding
因为 [PAD] 不是真实词,如果算进去会误导模型。
所以你做了:
padding_mask = 1 - padding_mask.reshape(-1)
loss = torch.mul(loss, padding_mask).sum() / padding_mask.sum()
意思就是:
只对真实 token 求平均损失。
3. 什么是 Label Smoothing
你的配置里:
"label_smoothing": 0.1
这也是面试高频点。
正常 one-hot 标签是:
- 正确类别概率 = 1
- 其他类别 = 0
Label smoothing 会把它稍微“抹平”一点,比如:
- 正确类别不是 1,而是 0.9 左右
- 其余类别分一点小概率
作用:
- 防止模型过度自信
- 提高泛化能力
- 对翻译任务经常有帮助
你可以简单讲成:
不让模型把答案看得太绝对,而是留一点不确定性。
十五、学习率为什么用 Noam Scheduler
这是 Transformer 经典设置。
你的 NoamDecayScheduler 是:
- 前期 warmup,学习率逐渐升高
- 后期再按步数衰减
公式不用死记,理解现象就够:
前期为什么升高?
模型刚开始训练很不稳定,直接大步更新容易炸,所以先慢慢热身。
后期为什么下降?
后面需要更细致地收敛,所以逐步减小学习率。
你可以概括成:
前期先试探着学,后期再稳稳地精调。
十六、训练循环在做什么
你的 training() 函数就是标准训练流程。
每个 batch:
- 取数据
- 前向传播
- 算 loss
loss.backward()optimizer.step()scheduler.step()- 每隔一段时间做验证
- 保存 checkpoint
这就是深度学习训练最核心的模板。
十七、这个项目中的“验证”和“测试”是什么意思
很多同学容易混。
训练集 train
用来更新参数
验证集 val
训练过程中评估模型效果,不更新参数
主要用于:
- 看模型有没有变好
- 调参
- 早停
- 保存最优模型
测试集 test
最终报告效果,只做最终检验
十八、推理阶段和训练阶段有什么区别
这个是理解 Transformer 项目必须明白的。
1. 训练阶段
训练时你把完整目标句子喂给 decoder。
因为有 teacher forcing,模型每一步都能看到正确前缀。
2. 推理阶段
推理时没有真实目标句子,只能自己一步一步生成:
- 先输入
[BOS] - 预测第一个词
- 把第一个词接到后面
- 再预测第二个词
- 一直循环,直到
[EOS]
这叫 自回归生成。
3. 你当前测试代码有个需要注意的点
你测试代码里这一段:
outputs = model(
encoder_inputs=encoder_inputs,
decoder_inputs=decoder_inputs,
encoder_inputs_mask=encoder_inputs_mask
)
preds = outputs.logits.argmax(dim=-1)
这里实际上更像是在做“带 teacher forcing 的预测”,不是严格意义上的自由生成。
因为你把 decoder_inputs 也传进去了,说明 decoder 看到了真实目标前缀。
所以这个结果更像:
- 测试集上的逐位置预测
- 不完全等于真实部署时的自由翻译
如果你要严格做翻译推理,应该调用你模型里的 infer(),让它自己一步一步生成。
这个点你在复试里讲出来,会显得你理解很细。
十九、BLEU 是什么,为什么用它
机器翻译里常用 BLEU。
它衡量的是:
模型翻译结果和参考答案,在 n-gram 层面有多接近
比如参考句子:
i love you
预测句子:
i like you
虽然不是完全一样,但有不少重合,BLEU 会给一定分数。
你代码里要注意的一点
你这里用了:
weights=(1, 0, 0, 0)
这实际上更像 BLEU-1,只看 1-gram,不是标准的 BLEU-4。
所以如果你在面试里说:
我在 notebook 里先实现了基于 unigram 的 BLEU 统计,作为一个简化版本;严格来说,标准机器翻译评估更常用 BLEU-4。
这会显得你很诚实,也很专业。
二十、这个项目最值得你真正理解的 8 个点
如果你想“充分理解”,我建议你把下面 8 点吃透。
1. 为什么要有 Encoder 和 Decoder
因为翻译是 seq2seq 任务:
- Encoder 负责编码源语言
- Decoder 负责生成目标语言
2. Self-Attention 到底在干什么
让每个位置动态关注句内其他位置,得到上下文表示。
3. Multi-Head 为什么有效
不同头学习不同关系,表达能力更强。
4. 为什么要位置编码
注意力本身不懂顺序,位置编码补充顺序信息。
5. 为什么 Decoder 要 mask
防止预测时偷看未来词,保证自回归生成。
6. Cross-Attention 为什么重要
让目标语言生成时参考源语言信息,这是翻译成立的关键。
7. 为什么 padding 不能参与 loss
pad 不是有效词,不应该影响训练。
8. 为什么要用 Noam + warmup
Transformer 对学习率比较敏感,这种策略更稳定。
二十一、你可以把整个模型想成一个现实类比
这个类比特别适合帮助你真正记住。
假设你在做“德语听写翻译”:
Encoder
像一个德语阅读理解老师,把整句德语读懂并做好笔记。
Decoder
像一个英语写作老师,根据:
- 自己已经写出的前文
- 德语老师给的笔记
一步一步写出英文。
Self-Attention
写某个词时,回头看上下文里哪些词最相关。
Cross-Attention
写英文时,去参考德语原句对应的信息。
Mask
写第 3 个词时,不能偷看第 4 个词。
二十二、你这个项目里有哪些亮点
从“项目理解”和“复试表达”角度看,亮点有这些:
1. 不是直接调用现成 Transformer,而是手写了核心模块
包括:
- Embedding
- Positional Encoding
- Multi-Head Attention
- Transformer Block
- Encoder / Decoder
- Mask
- Scheduler
这说明你是“理解原理后实现”,不是只会调库。
2. 有 batch sampler 优化
按 token 数动态分 batch,体现工程意识。
3. 有完整训练流程
包括:
- loss
- optimizer
- scheduler
- tensorboard
- checkpoint
4. 有推理和可视化意识
还加入了注意力热力图。
5. 有 BLEU 评估
说明你考虑了机器翻译的任务指标。
二十三、这个项目里你还要特别注意的几个细节
我也顺手帮你指出一些你后面可以继续完善的地方。
1. TransformerEmbedding.forward() 里有 print(position_ids)
训练时会大量打印,实际训练时应该删掉。
2. 测试部分更像 teacher-forcing evaluation
如果说“真正的翻译推理”,最好用自回归 infer()。
3. BLEU 这里目前更接近 BLEU-1
如果要更严谨,最好用 corpus-level BLEU 或标准 BLEU-4。
4. FFN 里用的是 ReLU
也可以考虑 GELU,但 ReLU 完全没问题。
5. 共享 embedding 的实验还可以展开
因为你 notebook 里提到了共享和不共享。
二十四、如果你要向老师讲这个项目,可以这样说
你可以直接用下面这段思路回答:
这个项目是一个基于 Transformer 的德语到英语机器翻译系统。整体流程包括数据加载、BPE 分词后的 token 编码、动态 batch 构造、Transformer 模型搭建、训练、验证以及基于 BLEU 的评估。
模型结构上,我手动实现了词嵌入和位置编码、多头注意力、前馈网络、残差连接和层归一化,并分别构建了 Encoder 和 Decoder。Encoder 主要通过 self-attention 对源语言句子进行上下文建模,Decoder 则在 masked self-attention 的基础上,通过 cross-attention 融合源语言信息,逐步生成目标语言。
训练时采用带 padding mask 的交叉熵损失,并结合 label smoothing 和 Noam 学习率调度提升训练稳定性。工程上,我还实现了按句长动态组 batch 的采样器,以减少 padding 带来的计算浪费。最后,在测试阶段使用 BLEU 指标对翻译结果进行评估,并支持注意力热力图可视化。
这段你基本背熟,复试就很稳。
二十五、最后我帮你做一个“最通俗版总结”
你可以把这个项目压缩成下面这几句话:
最通俗的一句话
这个项目就是教计算机把德语句子翻译成英语句子。
它是怎么做到的
- 先把单词变成数字
- 再让模型通过注意力机制理解句子里词和词之间的关系
- 用 Encoder 读懂德语
- 用 Decoder 一步步生成英语
- 用正确答案监督训练
- 最后用 BLEU 看翻译得好不好
Transformer 最核心是什么
不是按顺序死记,而是让每个词都能看全句里最重要的词。
这个项目最关键的代码模块
LangPairDataset:读数据Tokenizer:文本转 idTransformerBatchSampler:动态组 batchTransformerEmbedding:词向量 + 位置编码MultiHeadAttention:多头注意力TransformerBlock:attention + FFNTransformerModel:完整模型CrossEntropyWithPadding:损失NoamDecayScheduler:学习率调度training():训练循环