中英翻译器之04 Transformer 翻译模型超通俗详解笔记

一、这个脚本到底是干嘛的?

一句话说清楚:这就是 AI 翻译官的大脑!

前面我们把中文和英文都变成了数字,又打包成了标准的批次。现在这个脚本定义了一个完整的 Transformer 模型,它能:

  1. 看懂输入的中文数字序列
  2. 理解中文句子的意思
  3. 一个词一个词地生成对应的英文数字序列

这是整个机器翻译项目最核心、最复杂的部分,也是 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… 个词
  • 因果掩码就是一个上三角矩阵,把未来的位置都挡住了

三、特别重要的说明

  1. 整个模型的完整流程

    中文数字序列 → 中文词嵌入 → 加位置编码 → 编码器 → 上下文向量
                                                          ↓
    英文数字序列 → 英文词嵌入 → 加位置编码 → 解码器 → 线性层 → 英文概率序列
    
  2. 三个掩码的作用

    掩码 作用
    src_padding_mask 告诉编码器哪些位置是填充的,不用关注
    tgt_padding_mask 告诉解码器哪些位置是填充的,不用关注
    tgt_mask 告诉解码器不能看未来的词,只能看已经生成的词
  3. 新手最容易踩的坑

    • 忘了加batch_first=True:这会导致形状不匹配,非常难排查
    • 因果掩码的形状不对:必须是(T, T),T 是目标序列长度
    • 填充掩码的类型不对:必须是布尔类型,True 表示被屏蔽
    • 位置编码的维度和词嵌入维度不一样:必须完全相同
  4. 为什么输出是 logits 而不是概率?

    • 线性层输出的是 logits(原始得分),还没有经过 softmax
    • 因为 PyTorch 的交叉熵损失函数内部已经包含了 softmax
    • 这样做数值更稳定,计算更快
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐