中英翻译器之04 Transformer 翻译模型
·
中英翻译器之04 Transformer 翻译模型超通俗详解笔记
一、这个脚本到底是干嘛的?
一句话说清楚:这就是 AI 翻译官的大脑!
前面我们把中文和英文都变成了数字,又打包成了标准的批次。现在这个脚本定义了一个完整的 Transformer 模型,它能:
- 看懂输入的中文数字序列
- 理解中文句子的意思
- 一个词一个词地生成对应的英文数字序列
这是整个机器翻译项目最核心、最复杂的部分,也是 Transformer 架构的经典实现。
二、代码逐行大白话解释
2.1 开头导入工具
# 导入数学库:用来计算正弦余弦函数(位置编码要用)
import math
# 导入PyTorch核心库
import torch
# 导入神经网络模块:所有层都从这里来
from torch import nn
# 导入配置文件:里面存了模型维度、头数、层数等超参数
from config import *
2.2 主模型类 TranslationModel
这是整个模型的外壳,采用了经典的 ** 编码器 - 解码器 (Encoder-Decoder)** 架构。
超级形象的比喻:
- 编码器 = 翻译官的耳朵和大脑:负责听懂中文,理解句子的意思
- 解码器 = 翻译官的嘴巴:负责把理解到的意思用英文说出来
- 整个过程:中文句子 → 编码器理解 → 解码器生成英文句子
# 自定义翻译模型类,继承自PyTorch的nn.Module(所有神经网络都必须继承它)
class TranslationModel(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 维的向量
- 向量中最大的那个值对应的词,就是模型预测的下一个词
前向传播方法:数据在模型里的旅行路线
# 前向传播方法:定义数据从输入到输出的完整流程
def forward(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)
return output
编码过程:听懂中文
# 编码过程:把中文句子变成上下文向量
def encode(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)
return memory
解码过程:说出英文
# 解码过程:根据上下文向量生成英文
def decode(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)
return output
⚠️ 这里有个新手最容易困惑的点:tgt_seq是什么?
- 这是 ** 教师强制训练 (Teacher Forcing)** 的概念
- 训练的时候,我们不把模型上一步预测的词作为下一步的输入
- 而是直接把正确的英文句子作为输入,让模型学习
- 比如要翻译 “我爱你” 成 “I love you”:
- 输入给解码器的
tgt_seq是:[<sos>, I, love, you] - 模型需要输出的是:
[I, love, you, <eos>]
- 输入给解码器的
- 这样训练更快,更稳定
2.3 位置编码层 PositionEncoding
这是整个模型中最 “玄学” 也最巧妙的部分。
# 自定义位置编码层:给每个位置生成独特的位置标签
class PositionEncoding(nn.Module):
def __init__(self, max_len, d_model):
super().__init__()
# 创建一个空矩阵,形状是(最大序列长度, 模型维度)
pe = torch.zeros(max_len, d_model)
# 遍历每个位置
for pos in range(max_len):
# 遍历每个维度
for _2i in range(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)
def forward(self, x):
# 截取和输入序列一样长的位置编码,加到词向量上
return x + self.pe[0: x.shape[1]]
大白话解释:
-
为什么用正弦余弦函数?因为它有个神奇的性质:
- 任意两个位置 k 和 k+d 的位置编码,都可以通过线性变换得到
- 这样模型就能很容易地学习到词之间的相对位置关系
-
register_buffer是什么意思?- 这个位置编码是我们提前算好的,固定不变的,不需要模型学习
- 注册为缓冲区后,它会跟着模型一起保存和移动(CPU/GPU)
- 但不会被优化器更新
2.4 测试代码:验证模型能跑通
if __name__ == '__main__':
# 假设中文词表1000个词,英文词表1500个词
src_vocab_size = 1000
tgt_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
- 这样做数值更稳定,计算更快
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)