Transformer德英翻译项目介绍

一、先把整个项目看成一个流水线

你这个项目完整流程其实是:

数据准备 → 分词/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 man
  • man bites dog

单词一样,顺序不同,意思完全不同。

所以需要加入 position embedding / positional encoding


3. 你这里的位置编码怎么实现的

你代码里用的是经典的 正弦/余弦位置编码

  • 偶数维用 sin
  • 奇数维用 cos

好处是:

  • 不需要学习参数
  • 不同位置有不同模式
  • 模型能感知相对/绝对位置

公式本身你不用死背,重点理解:

它给每个位置生成一个独特的“位置向量”,然后和词向量相加。

最终输入表示是:

词向量 + 位置向量

这就是:

embeds = word_embeds + pos_embeds

七、Attention 是这个项目最核心的思想

现在讲最关键的 MultiHeadAttention


1. 先别看公式,先理解 Query / Key / Value

这是很多同学最难理解的地方,我给你一个“查资料”的类比。

你去图书馆找资料:

  • Query:你现在想问的问题
  • Key:每本书的标签
  • Value:每本书真正的内容

流程是:

  1. 用你的 Query 和每本书的 Key 做匹配
  2. 匹配度越高,说明这本书越相关
  3. 再把相关书的 Value 按权重加权求和
  4. 得到你最终需要的信息

Attention 就是在干这件事。


2. 在句子里怎么理解?

比如句子:

The animal didn’t cross the street because it was tired.

处理 it 的时候,模型会看:

  • animal
  • street

通过 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 结构很标准:

  1. Multi-Head Attention
  2. Add & LayerNorm
  3. Feed Forward Network
  4. 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。

它做三件事:

  1. 对已生成的目标词做 masked self-attention
  2. 看 Encoder 输出做 cross-attention
  3. 过 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.1
  • love: 2.8
  • you: 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:

  1. 取数据
  2. 前向传播
  3. 算 loss
  4. loss.backward()
  5. optimizer.step()
  6. scheduler.step()
  7. 每隔一段时间做验证
  8. 保存 checkpoint

这就是深度学习训练最核心的模板。


十七、这个项目中的“验证”和“测试”是什么意思

很多同学容易混。

训练集 train

用来更新参数

验证集 val

训练过程中评估模型效果,不更新参数

主要用于:

  • 看模型有没有变好
  • 调参
  • 早停
  • 保存最优模型

测试集 test

最终报告效果,只做最终检验


十八、推理阶段和训练阶段有什么区别

这个是理解 Transformer 项目必须明白的。


1. 训练阶段

训练时你把完整目标句子喂给 decoder。

因为有 teacher forcing,模型每一步都能看到正确前缀。


2. 推理阶段

推理时没有真实目标句子,只能自己一步一步生成:

  1. 先输入 [BOS]
  2. 预测第一个词
  3. 把第一个词接到后面
  4. 再预测第二个词
  5. 一直循环,直到 [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:文本转 id
  • TransformerBatchSampler:动态组 batch
  • TransformerEmbedding:词向量 + 位置编码
  • MultiHeadAttention:多头注意力
  • TransformerBlock:attention + FFN
  • TransformerModel:完整模型
  • CrossEntropyWithPadding:损失
  • NoamDecayScheduler:学习率调度
  • training():训练循环

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注