中英翻译器之04 Transformer 翻译模型
中英翻译器之04 Transformer 翻译模型超通俗详解笔记
一、这个脚本到底是干嘛的?
一句话说清楚:这就是 AI 翻译官的大脑!
前面我们把中文和英文都变成了数字,又打包成了标准的批次。现在这个脚本定义了一个完整的 Transformer 模型,它能:
- 看懂输入的中文数字序列
- 理解中文句子的意思
- 一个词一个词地生成对应的英文数字序列
这是整个机器翻译项目最核心、最复杂的部分,也是 Transformer 架构的经典实现。
二、代码逐行大白话解释
2.1 开头导入工具
# 导入数学库:用来计算正弦余弦函数(位置编码要用)importmath# 导入PyTorch核心库importtorch# 导入神经网络模块:所有层都从这里来fromtorchimportnn# 导入配置文件:里面存了模型维度、头数、层数等超参数fromconfigimport*2.2 主模型类 TranslationModel
这是整个模型的外壳,采用了经典的 ** 编码器 - 解码器 (Encoder-Decoder)** 架构。
超级形象的比喻:
- 编码器 = 翻译官的耳朵和大脑:负责听懂中文,理解句子的意思
- 解码器 = 翻译官的嘴巴:负责把理解到的意思用英文说出来
- 整个过程:中文句子 → 编码器理解 → 解码器生成英文句子
# 自定义翻译模型类,继承自PyTorch的nn.Module(所有神经网络都必须继承它)classTranslationModel(nn.Module):# 初始化方法:定义模型的所有零件def__init__(self,src_vocab_size,tgt_vocab_size,src_padding_idx=0,tgt_padding_idx=0):# 调用父类的构造函数(PyTorch规定必须写)super().__init__()# 保存填充标记的ID(就是0),后面用来生成掩码self.src_padding_idx=src_padding_idx self.tgt_padding_idx=tgt_padding_idx零件 1:词嵌入层 (Embedding)
# ============ 1. 词嵌入层 ============# 中文词嵌入层:把中文数字ID变成有意义的向量self.src_embedding=nn.Embedding(num_embeddings=src_vocab_size,# 中文词表有多大embedding_dim=DIM_MODEL,# 每个词变成多少维的向量padding_idx=src_padding_idx# 告诉模型ID=0是填充,不用管它)# 英文词嵌入层:和中文一样,把英文数字ID变成向量self.tgt_embedding=nn.Embedding(num_embeddings=tgt_vocab_size,embedding_dim=DIM_MODEL,padding_idx=tgt_padding_idx)大白话解释:
- 之前我们把 “我” 变成了数字 4,但数字 4 本身没有任何意义
- 词嵌入层就是把每个数字变成一个有意义的长向量
- 比如:"我"→[0.1, 0.5, -0.3, …],"你"→[0.2, 0.4, -0.2, …]
- 意思相近的词,它们的向量也会很接近
padding_idx=0:告诉模型 ID=0 是我们用来填充的,不用学习它的向量,永远是 0
零件 2:位置编码层 (PositionEncoding)
# ============ 2. 位置编码层 ============# 给每个位置加上位置信息,告诉模型哪个词在前,哪个词在后self.pos_encoding=PositionEncoding(MAX_SEQ_LEN,DIM_MODEL)超级重要的大白话解释:
- Transformer 有个天生的缺陷:它不知道词的顺序!
- 对它来说,“我爱你” 和 “你爱我” 是完全一样的,因为它只看词,不看位置
- 所以我们必须手动给每个位置加上一个独特的 “位置标签”
- 位置编码层就是干这个的:给第 1 个词加标签 1,第 2 个词加标签 2,以此类推
- 这样模型就能区分不同位置的词了
零件 3:Transformer 主体
# ============ 3. Transformer主体 ============# 使用PyTorch官方写好的Transformer模型self.transformer=nn.Transformer(d_model=DIM_MODEL,# 模型的特征维度(和词嵌入维度一样)nhead=NUM_HEADS,# 多头注意力的头数(一般是8)num_encoder_layers=NUM_ENCODER_LAYERS,# 编码器有几层(一般是6)num_decoder_layers=NUM_DECODER_LAYERS,# 解码器有几层(一般是6)batch_first=True# 输入的第一个维度是批次大小(必须加!))大白话解释:
- 这就是 Transformer 的核心,里面包含了所有的注意力机制
- 多头注意力:让模型同时从多个角度关注句子中的词
- 比如翻译 “我吃苹果”,模型在翻译 “吃” 的时候,会同时关注 “我”(谁吃)和 “苹果”(吃什么)
batch_first=True:和之前 DataLoader 里的一样,保证输入形状是(批次大小, 序列长度)
零件 4:输出线性层
# ============ 4. 输出线性层 ============# 把模型的输出变成词表大小的向量,每个位置对应一个词的概率self.linear=nn.Linear(in_features=DIM_MODEL,out_features=tgt_vocab_size)大白话解释:
- 解码器输出的是一个 512 维的向量,但我们需要知道下一个词是什么
- 线性层就是把这个 512 维的向量,变成一个和英文词表一样大的向量
- 比如英文词表有 10000 个词,就输出一个 10000 维的向量
- 向量中最大的那个值对应的词,就是模型预测的下一个词
前向传播方法:数据在模型里的旅行路线
# 前向传播方法:定义数据从输入到输出的完整流程defforward(self,src_seq,tgt_seq,src_padding_mask,tgt_mask,tgt_padding_mask):# 第一步:中文句子经过编码器,变成上下文向量(理解了中文的意思)memory=self.encode(src_seq,src_padding_mask)# 第二步:根据上下文向量和已经生成的英文,预测下一个词output=self.decode(tgt_seq,memory,tgt_mask,tgt_padding_mask,src_padding_mask)returnoutput编码过程:听懂中文
# 编码过程:把中文句子变成上下文向量defencode(self,src_seq,src_padding_mask):# 1. 中文数字ID → 词嵌入向量embed=self.src_embedding(src_seq)# 2. 加上位置编码,告诉模型词的顺序src=self.pos_encoding(embed)# 3. 经过Transformer编码器,得到上下文向量# src_padding_mask告诉编码器哪些位置是填充的,不用管memory=self.transformer.encoder(src=src,src_key_padding_mask=src_padding_mask)returnmemory解码过程:说出英文
# 解码过程:根据上下文向量生成英文defdecode(self,tgt_seq,memory,tgt_mask,tgt_padding_mask,memory_padding_mask):# 1. 英文数字ID → 词嵌入向量embed=self.tgt_embedding(tgt_seq)# 2. 加上位置编码tgt=self.pos_encoding(embed)# 3. 经过Transformer解码器output=self.transformer.decoder(tgt=tgt,memory=memory,# 编码器输出的上下文向量tgt_mask=tgt_mask,# 因果掩码:不能看未来的词tgt_key_padding_mask=tgt_padding_mask,# 英文填充掩码memory_key_padding_mask=memory_padding_mask# 中文填充掩码)# 4. 经过线性层,变成词表大小的概率向量output=self.linear(output)returnoutput⚠️ 这里有个新手最容易困惑的点:tgt_seq是什么?
- 这是 ** 教师强制训练 (Teacher Forcing)** 的概念
- 训练的时候,我们不把模型上一步预测的词作为下一步的输入
- 而是直接把正确的英文句子作为输入,让模型学习
- 比如要翻译 “我爱你” 成 “I love you”:
- 输入给解码器的
tgt_seq是:[<sos>, I, love, you] - 模型需要输出的是:
[I, love, you, <eos>]
- 输入给解码器的
- 这样训练更快,更稳定
2.3 位置编码层 PositionEncoding
这是整个模型中最 “玄学” 也最巧妙的部分。
# 自定义位置编码层:给每个位置生成独特的位置标签classPositionEncoding(nn.Module):def__init__(self,max_len,d_model):super().__init__()# 创建一个空矩阵,形状是(最大序列长度, 模型维度)pe=torch.zeros(max_len,d_model)# 遍历每个位置forposinrange(max_len):# 遍历每个维度for_2iinrange(0,d_model,2):# 偶数维度用正弦函数pe[pos,_2i]=math.sin(pos/(10000**(_2i/d_model)))# 奇数维度用余弦函数pe[pos,_2i+1]=math.cos(pos/(10000**(_2i/d_model)))# 把这个矩阵注册为模型的缓冲区(不是可训练参数)self.register_buffer('pe',pe)defforward(self,x):# 截取和输入序列一样长的位置编码,加到词向量上returnx+self.pe[0:x.shape[1]]大白话解释:
为什么用正弦余弦函数?因为它有个神奇的性质:
- 任意两个位置 k 和 k+d 的位置编码,都可以通过线性变换得到
- 这样模型就能很容易地学习到词之间的相对位置关系
register_buffer是什么意思?- 这个位置编码是我们提前算好的,固定不变的,不需要模型学习
- 注册为缓冲区后,它会跟着模型一起保存和移动(CPU/GPU)
- 但不会被优化器更新
2.4 测试代码:验证模型能跑通
if__name__=='__main__':# 假设中文词表1000个词,英文词表1500个词src_vocab_size=1000tgt_vocab_size=1500# 创建模型实例model=TranslationModel(src_vocab_size,tgt_vocab_size)# 随机生成一个批次的中文数据:32个句子,每个句子20个词src_seq=torch.randint(src_vocab_size,(BATCH_SIZE,20))# 随机生成一个批次的英文数据:32个句子,每个句子17个词tgt_seq=torch.randint(tgt_vocab_size,(BATCH_SIZE,17))# 生成填充掩码:标记哪些位置是0(填充)src_padding_mask=(src_seq==0)tgt_padding_mask=(tgt_seq==0)# 生成因果掩码:防止解码器看到未来的词# 这是一个上三角矩阵,对角线以上都是True,表示这些位置被屏蔽tgt_mask=model.transformer.generate_square_subsequent_mask(tgt_seq.shape[1]).bool()# 执行前向传播output=model(src_seq,tgt_seq,src_padding_mask,tgt_mask,tgt_padding_mask)# 打印输出形状:应该是(32, 17, 1500)# 32个句子,每个句子17个词,每个词对应1500个词的概率print(output.shape)因果掩码的形象解释:
- 就像考试的时候,你只能看已经做过的题,不能看后面的题
- 模型在预测第 3 个词的时候,只能看第 1 和第 2 个词,不能看第 4、5… 个词
- 因果掩码就是一个上三角矩阵,把未来的位置都挡住了
三、特别重要的说明
整个模型的完整流程:
中文数字序列 → 中文词嵌入 → 加位置编码 → 编码器 → 上下文向量 ↓ 英文数字序列 → 英文词嵌入 → 加位置编码 → 解码器 → 线性层 → 英文概率序列三个掩码的作用:
掩码 作用 src_padding_mask告诉编码器哪些位置是填充的,不用关注 tgt_padding_mask告诉解码器哪些位置是填充的,不用关注 tgt_mask告诉解码器不能看未来的词,只能看已经生成的词 新手最容易踩的坑:
- 忘了加
batch_first=True:这会导致形状不匹配,非常难排查 - 因果掩码的形状不对:必须是
(T, T),T 是目标序列长度 - 填充掩码的类型不对:必须是布尔类型,True 表示被屏蔽
- 位置编码的维度和词嵌入维度不一样:必须完全相同
- 忘了加
为什么输出是 logits 而不是概率?
- 线性层输出的是 logits(原始得分),还没有经过 softmax
- 因为 PyTorch 的交叉熵损失函数内部已经包含了 softmax
- 这样做数值更稳定,计算更快
