Part9.第15章:Transformer--实现翻译模型源码

1.构建词典

如何利用BPE算法构建NLP模型的词典。这里我们就来实际构建一次。
首先你需要安装sentencepiece这个包,
然后运行下边代码来分别生成英文和中文的词典。

import sentencepiece as spm
spm.SentencePieceTrainer.Train('--input="data\\en2cn\\train_en.txt" --model_prefix=en_bpe --vocab_size=16000 --model_type=bpe --character_coverage=1.0 --unk_id=0 --pad_id=1 --bos_id=2 --eos_id=3')
spm.SentencePieceTrainer.Train('--input="data\\en2cn\\train_zh.txt" --model_prefix=zh_bpe --vocab_size=16000 --model_type=bpe --character_coverage=0.9995 --unk_id=0 --pad_id=1 --bos_id=2 --eos_id=3')

【参数说明】
–character_coverage参数是覆盖多少用字符集,因为英文单个字符有限,所以我们设置为1.0。但是中文有很多生僻字,所以我们设置为0.9995防止词表被大量生僻词占用。
vocab_size=16000参数是设置词表的大小,我们都设置为16000。
因为英语基本字符有限,中文基本字符较多,字符组合可能较多,需要分别统计频率,所以BPE生成中文词表过程会比较慢。这个过程可能需要几十分钟。建议你耐心等它生成完成。
如果你真的心急,可以通过下边的参数来采样一百万句子来生成词表。

--input_sentence_size=1000000 --shuffle_input_sentence=true

生成完之后,我们可以打开en_bpe.vocab来看一下英文词表:

<unk>   0
<pad>   0
<s> 0
</s>    0
▁t  -0
he  -1
▁a  -2
in  -3
ou  -4
re  -5
▁s  -6
▁w  -7
on  -8
▁the    -9
er  -10
at  -11
▁c  -12
▁m  -13
▁I  -14
▁b  -15
an  -16
it  -17
ing -18

中文词表:

<unk>   0
<pad>   0
<s> 0
</s>    0
▁我 -0
..  -1
▁你 -2
我们    -3
什么    -4
▁他 -5
一个    -6
知道    -7
... -8
▁我们   -9
他们    -10
▁但 -11
如果    -12
不是    -13
没有    -14
可以    -15
因为    -16
▁在 -17
你的    -18

词表里的每个词,我们叫做一个token,其中符号“▁”表示词开始的位置。后边的数字,分数越大(接近 0),表示该 token 在训练时越频繁或优先级越高;分数越小(负号越大),表示频率更低。
接下来我们使用我们训练出来的词典模型进行分词:

import sentencepiece as spm

sp_cn = spm.SentencePieceProcessor()
sp_cn.load('zh_bpe.model')
text = "今天天气非常好。"
eoncode_result = sp_cn.encode(text, out_type=int)
print("编码:", eoncode_result)
decode_result = sp_cn.decode(eoncode_result)
print("解码:", decode_result)

可以看到输出为:

编码: [387, 3205, 5241, 11821]
解码: 今天天气非常好。

其中:
“今天” 作为一个token,编码为387。
“天气” 作为一个token被编码为3205。
“非常好” 作为一个token被编码为5241。
“。” 作为一个token被编码为11821。

2.训练代码

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import sentencepiece as spm
#import os
#os.environ["CUDA_VISIBLE_DEVICES"] = "3"

from transformer import build_transformer
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 加载英文和中文的BPE分词模型
sp_en = spm.SentencePieceProcessor()
sp_en.load('en_bpe.model')
sp_cn = spm.SentencePieceProcessor()
sp_cn.load('zh_bpe.model')


# 将文本转换为token ID序列
def tokenize_en(text):
    return sp_en.encode(text, out_type=int)

def tokenize_cn(text):
    return sp_cn.encode(text, out_type=int)

# 中文和英文一致,取英文。
PAD_ID = sp_en.pad_id()  # 1
UNK_ID = sp_en.unk_id()  # 0
BOS_ID = sp_en.bos_id()  # 2
EOS_ID = sp_en.eos_id()  # 3


# ---------------------#
# 2. Dataset & DataLoader
# ---------------------#
class TranslationDataset(Dataset):
    ## 初始化方法,读取英文和中文训练文本。然后给每个句子前后增加<bos>和<eos>。 为了防止训练时显存不足,对于长度超过限制的
    ## 句子进行过滤。
    def __init__(self, src_file, trg_file, src_tokenizer, trg_tokenizer, max_len=100):
        with open(src_file, encoding='utf-8') as f:
            src_lines = f.read().splitlines()
        with open(trg_file, encoding='utf-8') as f:
            trg_lines = f.read().splitlines()
        assert len(src_lines) == len(trg_lines)
        self.pairs = []
        self.src_tokenizer = src_tokenizer
        self.trg_tokenizer = trg_tokenizer
        index = 0
        for src, trg in zip(src_lines, trg_lines):
            index += 1
            if index % 100000 == 0:
                print(index)
            # 每个句子前边增加<bos>后边增加<eos>
            src_ids = [BOS_ID] + self.src_tokenizer(src) + [EOS_ID]
            trg_ids = [BOS_ID] + self.trg_tokenizer(trg) + [EOS_ID]
            # 只保留输入和输出序列token数同时小于max_len的训练样本。
            if len(src_ids) <= max_len and len(trg_ids) <= max_len:
                self.pairs.append((src_ids, trg_ids))  # <-- 直接保存token id序列

    def __len__(self):
        return len(self.pairs)

    def __getitem__(self, idx):
    	"""
    	获取单个样本,转换为LongTensor
    	"""
        src_ids, trg_ids = self.pairs[idx]
        return torch.LongTensor(src_ids), torch.LongTensor(trg_ids)

    ## 对一个batch的输入和输出token序列,依照最长的序列长度,用<pad> token进行填充,确保一个batch的数据形状一致,组成一个tensor。
    @staticmethod
    def collate_fn(batch):
        src_batch, trg_batch = zip(*batch)
        src_lens = [len(x) for x in src_batch]
        trg_lens = [len(x) for x in trg_batch]
        ## 注意,Transformer里的tensor,设置batch_frist=True。
        src_pad = nn.utils.rnn.pad_sequence(src_batch, batch_first=True, padding_value=PAD_ID)
        trg_pad = nn.utils.rnn.pad_sequence(trg_batch, batch_first=True,padding_value=PAD_ID)
        return src_pad, trg_pad, src_lens, trg_lens

# === 数据集定义 ===
def create_mask(src, tgt, pad_idx):
    # mask <pad> token for encoder.
    src_mask = (src != pad_idx).unsqueeze(1).unsqueeze(2)  # (batch, 1, 1, src_len)
    # mask <pad> token for decoder.
    tgt_pad_mask = (tgt != pad_idx).unsqueeze(1).unsqueeze(2)  # (batch, 1, 1, tgt_len)

    tgt_len = tgt.size(1)
    # decoder mask 当前token后边的token。
    tgt_sub_mask = torch.tril(torch.ones((tgt_len, tgt_len), device=tgt.device)).bool()  # (tgt_len, tgt_len)
    # decoder 同时mask <pad> token, 以及当前token后边的token。
    tgt_mask = tgt_pad_mask & tgt_sub_mask  # (batch, 1, tgt_len, tgt_len)
    return src_mask, tgt_mask

def train(model, dataloader, optimizer, criterion, pad_idx):
    model.train()
    total_loss = 0
    step = 0
    log_loss = 0  # 用于每100步统计

    for src, tgt, src_lens, tgt_lens in dataloader:
        step += 1

        src = src.to(DEVICE)
        tgt = tgt.to(DEVICE)

        tgt_input = tgt[:, :-1]
        tgt_output = tgt[:, 1:]

        src_mask, tgt_mask = create_mask(src, tgt_input, pad_idx)

        optimizer.zero_grad()
        encoder_output = model.encode(src, src_mask)
        decoder_output = model.decode(encoder_output, src_mask, tgt_input, tgt_mask)
        output = model.project(decoder_output)

        output = output.reshape(-1, output.shape[-1])
        tgt_output = tgt_output.reshape(-1)

        loss = criterion(output, tgt_output)
        loss.backward()

        optimizer.step()

        total_loss += loss.item()
        log_loss += loss.item()

        if step % 100 == 0:
            avg_log_loss = log_loss / 100
            print(f"Step {step}: Avg Loss = {avg_log_loss:.4f}")
            log_loss = 0  # 重置每100步的loss计数

    return total_loss / len(dataloader)

def main():
    # 超参数
    SRC_VOCAB_SIZE = 5000   # 16000
    TGT_VOCAB_SIZE = 5000   # 16000
    SRC_SEQ_LEN = 128
    TGT_SEQ_LEN = 128
    BATCH_SIZE = 2
    NUM_EPOCHS = 10
    LR = 1e-4

    # 数据集加载
    train_dataset = TranslationDataset('data/en2cn/train_en.txt', 'data/en2cn/train_zh.txt',tokenize_en, tokenize_cn)
    train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=train_dataset.collate_fn)

    # 构建模型
    model = build_transformer(SRC_VOCAB_SIZE, TGT_VOCAB_SIZE, SRC_SEQ_LEN, TGT_SEQ_LEN).to(DEVICE)

    optimizer = optim.Adam(model.parameters(), lr=LR)
    criterion = nn.CrossEntropyLoss(ignore_index=PAD_ID)

    for epoch in range(NUM_EPOCHS):
        loss = train(model, train_dataloader, optimizer, criterion, PAD_ID)
        print(f"Epoch {epoch+1}/{NUM_EPOCHS} - Loss: {loss:.4f}")

        torch.save(model.state_dict(), "transformer.pt")

if __name__ == "__main__":
    main()

2.0.基础代码

  1. return torch.LongTensor(src_ids), torch.LongTensor(trg_ids), 这里为什么用longTensor,跟Tensor的区别在哪里?
    答:
    在你的代码中:
    LongTensor是必需的,因为token IDs是离散的索引值
    这些索引会用于:
    (1)词嵌入查找(需要整数索引)
    (2)交叉熵损失计算(需要整数标签)
    如果错误地使用FloatTensor,PyTorch会抛出类型错误
    虽然torch.tensor()可以自动推断,但显式指定LongTensor使意图更清晰,避免潜在的类型问题
# 默认Tensor通常是FloatTensor
x = torch.tensor([1, 2, 3])          # 类型:torch.FloatTensor
print(x.dtype)                        # torch.float32

# LongTensor显式指定
y = torch.LongTensor([1, 2, 3])      # 类型:torch.LongTensor
print(y.dtype)                        # torch.int64
  1. src_batch, trg_batch = zip(*batch),代码说明
    src_batch, trg_batch = zip(*batch) 是一个优雅的转置操作:
    输入:[(src1,trg1), (src2,trg2), (src3,trg3)]
    输出:(src1, src2, src3) 和 (trg1, trg2, trg3)
    这让我们能够:
    (1)批量处理所有源序列
    (2)批量处理所有目标序列
    分别对它们进行填充等操作
    这是处理成对数据时非常实用和高效的技巧!
  2. nn.utils.rnn.pad_sequence 的作用
    核心功能:将不同长度的序列填充到相同长度,组成一个整齐的Tensor。
    为什么需要填充?在深度学习中,尤其是处理序列数据(如文本)时:
    (1)GPU需要批处理(batch processing)来并行计算;
    (2)批处理要求同一个batch内的所有样本形状相同,但自然语言句子长度是不同的;
    参数详解:
nn.utils.rnn.pad_sequence(
    sequences,      # 要填充的序列列表/元组
    batch_first=True,   # 重要参数!
    padding_value=1     # 填充值
)
  1. src_mask = (src != pad_idx).unsqueeze(1).unsqueeze(2)
    编码器掩码 (src_mask)
# unsqueeze(1): 在维度1插入新维度
# 形状从 (batch=3, seq_len=5) -> (3, 1, 5)

# unsqueeze(2): 在维度2插入新维度  
# 形状从 (3, 1, 5) -> (3, 1, 1, 5)

为什么要这样做?
(1)因为多头注意力需要形状 (batch, num_heads, seq_len, seq_len) 或 (batch, 1, 1, seq_len)
(2)这样可以在后续计算中广播到合适的形状
(3)最终src_mask形状:(batch_size, 1, 1, src_seq_len);
5. torch.tril() 下三角矩阵

# 保留下三角部分(包括对角线),上三角设为0
tril_matrix = torch.tril(torch.ones((5, 5)))
# 结果:
tensor([[1., 0., 0., 0., 0.],
        [1., 1., 0., 0., 0.],
        [1., 1., 1., 0., 0.],
        [1., 1., 1., 1., 0.],
        [1., 1., 1., 1., 1.]])

在这里插入图片描述

2.1.train函数

1.函数定义和初始化

def train(model, dataloader, optimizer, criterion, pad_idx):
    model.train()  # 设置模型为训练模式
    total_loss = 0  # 累计整个epoch的损失
    step = 0  # 步数计数器
    log_loss = 0  # 用于每100步统计的临时损失
# 训练模式 vs 评估模式
model.train()   # 启用dropout、batch normalization的训练行为
model.eval()    # 禁用dropout、固定batch normalization的统计量

# 例子:
model = nn.Transformer()
model.train()   # 训练时:会有dropout,bn用当前batch统计
model.eval()    # 评估时:无dropout,bn用训练时累积的统计量

2.DataLoader返回的数据

for src, tgt, src_lens, tgt_lens in dataloader:
    step += 1
"""
# 从collate_fn返回的4个值:
# src: 源语言序列,形状 (batch_size, src_seq_len)
# tgt: 目标语言序列,形状 (batch_size, tgt_seq_len)
# src_lens: 每个源序列的实际长度列表
# tgt_lens: 每个目标序列的实际长度列表
"""
# 例如:batch_size=2
src = torch.tensor([[2, 10, 20, 3, 1], [2, 11, 3, 1, 1]])  # 形状(2,5)
tgt = torch.tensor([[2, 30, 40, 3, 1], [2, 31, 32, 33, 3]]) # 形状(2,5)
src_lens = [4, 3]  # 实际长度(不包括PAD)
tgt_lens = [4, 5]  # 实际长度

3.数据转移到设备

src = src.to(DEVICE)
tgt = tgt.to(DEVICE)

为什么需要转移设备?

# CPU和GPU内存是分开的
# 模型参数已经在DEVICE上(如GPU)
# 输入数据也需要移动到相同的设备
# 否则会报错:Tensor和模型不在同一设备

# 示例:如果模型在GPU,数据在CPU
# model.cuda()  # 模型在GPU
# data.cpu()    # 数据在CPU
# output = model(data)  # ❌ 报错:设备不匹配

4.Teacher Forcing(教师强制准备)

tgt_input = tgt[:, :-1]  # 去掉最后一个token
tgt_output = tgt[:, 1:]   # 去掉第一个token

原理如下:

原始目标序列 tgt: [<bos>, w1, w2, w3, <eos>]
tgt_input: [<bos>, w1, w2, w3]      ← 解码器输入
tgt_output: [w1, w2, w3, <eos>]     ← 解码器预测目标

训练过程:
1. 解码器输入: <bos>
   预测: w1(应该与tgt_output[0]=w1比较)
2. 解码器输入: <bos>, w1
   预测: w2(应该与tgt_output[1]=w2比较)
3. 解码器输入: <bos>, w1, w2
   预测: w3(应该与tgt_output[2]=w3比较)
4. 解码器输入: <bos>, w1, w2, w3
   预测: <eos>(应该与tgt_output[3]=<eos>比较)

5.创建注意力掩码

src_mask, tgt_mask = create_mask(src, tgt_input, pad_idx)

掩码形状:

# src_mask: (batch_size, 1, 1, src_seq_len)
# tgt_mask: (batch_size, 1, tgt_input_len, tgt_input_len)
# 注意:tgt_mask是基于tgt_input(不是原始tgt)创建的

6.梯度清零

optimizer.zero_grad()

为什么需要清零梯度?

"""
# PyTorch会累积梯度(对RNN有用)
# 但在标准训练中,每个batch应该独立
# 不清零会导致梯度累加,错误更新参数
"""
# 错误示例:
for batch in dataloader:
    loss = model(batch)
    loss.backward()  # 梯度累积
    # 第一次:grad = ∇L1
    # 第二次:grad = ∇L1 + ∇L2  ❌
    
# 正确做法:
optimizer.zero_grad()  # 清零梯度
loss.backward()        # 计算新梯度

7.Transformer前向传播

encoder_output = model.encode(src, src_mask)
decoder_output = model.decode(encoder_output, src_mask, tgt_input, tgt_mask)
output = model.project(decoder_output)

分步解释:

a) 编码器
encoder_output = model.encode(src, src_mask)
"""
# src: (batch_size, src_seq_len) 如(2,5)
# src_mask: (batch_size, 1, 1, src_seq_len)
# encoder_output: (batch_size, src_seq_len, d_model) 如(2,5,512)
# 编码器处理源语言,提取特征
"""
b) 解码器
decoder_output = model.decode(encoder_output, src_mask, tgt_input, tgt_mask)
"""
# encoder_output: 编码器输出
# src_mask: 编码器掩码(用于交叉注意力)
# tgt_input: (batch_size, tgt_input_len) 如(2,4)
# tgt_mask: (batch_size, 1, tgt_input_len, tgt_input_len)
# decoder_output: (batch_size, tgt_input_len, d_model) 如(2,4,512)
"""
c) 线性投影
output = model.project(decoder_output)
"""
# decoder_output: (batch_size, tgt_input_len, d_model)
# output: (batch_size, tgt_input_len, vocab_size) 如(2,4,5000)
# 将特征空间映射到词汇表空间
"""

8.reshape

output = output.reshape(-1, output.shape[-1])
tgt_output = tgt_output.reshape(-1)

为什么需要重塑?
数据重塑:将3D输出转换为2D,适应交叉熵损失函数的输入要求
重塑前:

"""
# output形状: (batch_size, seq_len, vocab_size) = (2, 4, 5000)
# tgt_output形状: (batch_size, seq_len) = (2, 4)
"""
# 示例数据:
output = torch.tensor([
    # batch 0
    [[0.1, 0.2, ...],  # 位置0,对5000个词的logits
     [0.3, 0.4, ...],  # 位置1
     [0.5, 0.6, ...],  # 位置2
     [0.7, 0.8, ...]], # 位置3
    # batch 1  
    [[0.9, 1.0, ...],
     [1.1, 1.2, ...],
     [1.3, 1.4, ...],
     [1.5, 1.6, ...]]
])  # 形状(2,4,5000)

tgt_output = torch.tensor([
    [30, 40, 50, 3],  # batch 0的目标token IDs
    [31, 32, 33, 3]   # batch 1的目标token IDs
])  # 形状(2,4)

重塑后:

# output.reshape(-1, output.shape[-1])
# -1表示自动计算维度,5000保持不变
# 新形状: (batch_size * seq_len, vocab_size) = (8, 5000)

# tgt_output.reshape(-1)
# 新形状: (batch_size * seq_len) = (8,)

# 重塑后的对应关系:
# output[0] 对应 batch0-位置0 的词表分布
# tgt_output[0] 对应 batch0-位置0 的真实token ID (30)
# output[1] 对应 batch0-位置1 的词表分布  
# tgt_output[1] 对应 batch0-位置1 的真实token ID (40)
# ...
# output[4] 对应 batch1-位置0 的词表分布
# tgt_output[4] 对应 batch1-位置0 的真实token ID (31)

9.计算损失

loss = criterion(output, tgt_output)

CrossEntropyLoss详解:

criterion = nn.CrossEntropyLoss(ignore_index=pad_idx)
"""
# output: (N, C) 其中 N = batch_size * seq_len, C = vocab_size
# tgt_output: (N,) 每个位置的真实token ID
# ignore_index=pad_idx: 忽略PAD token的损失

# 计算过程:
# 1. 对output的每一行做softmax,得到概率分布
# 2. 取tgt_output指定位置的概率
# 3. 计算负对数似然损失
# 公式: loss = -log(p[target])
"""
# 示例:
output_row = [0.1, 0.2, 0.7]  # 三个词的概率logits
target = 2  # 真实词是第2个(索引从0开始)
p = softmax(output_row) = [0.2, 0.3, 0.5]
loss = -log(0.5) = 0.6931

10.反向传播和优化

loss.backward()      # 计算梯度
optimizer.step()     # 更新参数

反向传播过程:

# loss.backward() 计算图中每个参数的梯度
# ∂loss/∂W1, ∂loss/∂W2, ...

# optimizer.step() 根据梯度更新参数
# W = W - lr * ∂loss/∂W

# 示例:Adam优化器更新规则更复杂,包括动量、自适应学习率

11.损失统计

total_loss += loss.item()
log_loss += loss.item()

.item() 的作用:

# loss是一个包含单个值的Tensor
loss = torch.tensor(2.5)  # requires_grad=True

# loss.item() 提取Python数值
loss_value = loss.item()  # 2.5 (float)

# 如果不使用.item()直接累加Tensor:
total_loss += loss  # ❌ 错误:会累积计算图,内存泄漏

12.定期打印损失

if step % 100 == 0:
    avg_log_loss = log_loss / 100
    print(f"Step {step}: Avg Loss = {avg_log_loss:.4f}")
    log_loss = 0  # 重置

监控训练进度:

# 每100步打印一次平均损失
# 帮助判断:
# 1. 损失是否在下降(学习正常)
# 2. 损失是否稳定(可能收敛)
# 3. 损失是否震荡(学习率可能太大)

13. 返回平均损失

return total_loss / len(dataloader)

计算整个epoch的平均损失:

# len(dataloader) = 数据集大小 / batch_size
# 例如:1000个样本,batch_size=32 → len(dataloader)=32

# total_loss是整个epoch的损失总和
# 除以批次数得到平均每个batch的损失

关键要点总结

  1. 训练模式设置:model.train() 启用dropout等训练特有的行为
  2. 教师强制:使用真实的前一个词作为解码器输入,提高训练稳定性
  3. 梯度管理:每个batch前清零梯度,防止累积
  4. Transformer流程:编码 → 解码 → 投影 → 计算损失
  5. 数据重塑:将3D输出转换为2D,适应交叉熵损失函数的输入要求
  6. 损失计算:使用带ignore_index的CrossEntropyLoss,忽略PAD token
  7. 优化步骤:反向传播计算梯度,优化器更新参数
  8. 进度监控:定期打印损失,监控训练进展

这个训练循环实现了标准的Transformer训练流程,是机器翻译等序列到序列任务的核心训练代码。

2.2.主函数

1.超参数

学习率 (Learning Rate)

LR = 1e-4  # 0.0001
"""
# Transformer通常使用较小的学习率
# 常见学习率:
# Adam优化器:1e-4 到 5e-4
# SGD优化器:0.1 到 0.01(有动量)
# 学习率调度:warmup + 衰减

# 学习率对训练的影响:
# 太大:震荡不收敛
# 太小:收敛速度慢
"""

2.DataLoader配置

train_dataloader = DataLoader(
    train_dataset, 
    batch_size=BATCH_SIZE, 
    shuffle=True, 
    collate_fn=train_dataset.collate_fn
)
  • batch_size=BATCH_SIZE:每个批次2个样本
  • shuffle=True:每个epoch打乱数据顺序
# shuffle的作用:
# 1. 防止模型学习到数据顺序
# 2. 提高泛化能力
# 3. 使梯度更新更稳定

# 注意:验证集通常不shuffle
  • collate_fn=train_dataset.collate_fn:自定义批处理函数
# 默认的collate_fn不能处理变长序列
# 需要自定义函数进行padding

# 如果没有自定义collate_fn:
# DataLoader会尝试堆叠不同长度的tensor,导致错误

3.Adam优化器

"""
# Adam优化器的优势:
# 1. 自适应学习率
# 2. 内置动量
# 3. 适合大多数深度学习任务
"""
# model.parameters() 返回模型的所有可训练参数
params = list(model.parameters())
print(f"模型总参数量: {sum(p.numel() for p in params)}")
"""
# Adam的其他参数(使用默认值):
# betas=(0.9, 0.999)   # 一阶和二阶矩估计的指数衰减率
# eps=1e-8             # 数值稳定性
# weight_decay=0       # L2正则化(权重衰减)
"""

4.模型保存

torch.save(model.state_dict(), "transformer.pt")
"""
# state_dict() vs 保存整个模型:
# 1. state_dict(): 只保存模型参数(推荐)
#    优点:文件小,兼容性好,可以加载到不同结构的模型
# 2. 保存整个模型: torch.save(model, "model.pth")
#    缺点:文件大,绑定到特定代码结构
"""
# 保存内容:
# model.state_dict() 返回一个字典:
state_dict = {
    'encoder.embed.weight': tensor(...),
    'encoder.layers.0.self_attn.q_proj.weight': tensor(...),
    'decoder.embed.weight': tensor(...),
    ...
}

# 加载模型:
model = build_transformer(...)  # 先构建相同结构的模型
model.load_state_dict(torch.load("transformer.pt"))
model.eval()  # 设置为评估模式

5.实际应用中的改进建议

<1>梯度裁剪 (Gradient Clipping)
# 防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# 在train函数中添加:
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()

原理:
a) 梯度范数计算

import torch

# 计算所有参数的梯度范数(L2范数)
def compute_gradient_norm(model):
    total_norm = 0.0
    for p in model.parameters():
        if p.grad is not None:
            param_norm = p.grad.data.norm(2)  # L2范数
            total_norm += param_norm.item() ** 2
    total_norm = total_norm ** 0.5
    return total_norm
"""
# L2范数计算公式:
# ||g||₂ = √(∑ᵢ gᵢ²)
"""

b) 裁剪规则

"""
# 1. 如果梯度范数 > max_norm:
#    缩放因子 = max_norm / 梯度范数
#    新梯度 = 原梯度 * 缩放因子

# 2. 如果梯度范数 <= max_norm:
#    保持梯度不变

# 数学表达:
# total_norm = ||g||₂
# if total_norm > max_norm:
#     g = g * (max_norm / total_norm)
"""
<2>学习率调度 (Learning Rate Scheduling)
# 1. Warmup(训练初期逐渐增加学习率)
def get_lr(step, d_model, warmup_steps=4000):
    return d_model**-0.5 * min(step**-0.5, step * warmup_steps**-1.5)

# 2. 按epoch衰减
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)

# 3. 根据验证损失调整
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, 
    mode='min', 
    factor=0.5, 
    patience=2
)
<3>混合精度训练 (Mixed Precision)
# 减少显存使用,加速训练
from torch.cuda.amp import autocast, GradScaler

scaler = GradScaler()

def train_with_amp(...):
    for ... in dataloader:
        with autocast():
            # 前向传播(自动使用半精度)
            output = model(...)
            loss = criterion(...)
        
        # 反向传播(自动调整精度)
        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()
        optimizer.zero_grad()
<4>早停 (Early Stopping)
# 防止过拟合
patience = 5
best_loss = float('inf')
no_improve_count = 0

for epoch in range(NUM_EPOCHS):
    train_loss = train(...)
    valid_loss = evaluate(...)
    
    if valid_loss < best_loss:
        best_loss = valid_loss
        no_improve_count = 0
        save_model(...)
    else:
        no_improve_count += 1
        
    if no_improve_count >= patience:
        print(f"早停! 连续{patience}个epoch验证损失没有改善")
        break

2.3.Transformer相关模块

本章节只展示部分前面未出现过的模块代码并进行说明

1.InputEmbeddings

作用:类实现了Transformer的输入嵌入层,将离散符号转换为适合神经网络处理的连续表示,并为后续的注意力机制做好准备。

class InputEmbeddings(nn.Module):

    def __init__(self, d_model: int, vocab_size: int) -> None:
        super().__init__()
        self.d_model = d_model
        self.vocab_size = vocab_size
        self.embedding = nn.Embedding(vocab_size, d_model)

    def forward(self, x):
        # (batch, seq_len) --> (batch, seq_len, d_model)
        return self.embedding(x) * math.sqrt(self.d_model)

InputEmbeddings 类的关键点:
(1)核心功能:
将离散的token IDs转换为连续的向量表示
应用√d_model缩放,与位置编码幅度匹配
(2)设计选择:
缩放因子√d_model:保持方差稳定,与位置编码兼容
初始化策略:影响训练稳定性和收敛速度
(3)实际应用:
通常是Transformer中参数量最大的部分(特别是大词汇表时)
需要与位置编码、层归一化等组件配合使用
可以应用权重压缩、量化等技术优化内存
(4)优化建议:

# 最佳实践:
# 1. 使用合适的初始化(normal或xavier)
# 2. 考虑词汇表大小对内存的影响
# 3. 对于大词汇表,考虑使用子词嵌入或压缩技术
# 4. 与位置编码正确组合

# 在Transformer中,输入嵌入层是:
# - 模型的第一层
# - 决定输入表示质量的关键
# - 需要仔细调优的部分

代码说明:
【1】nn.Embedding 层

# self.embedding = nn.Embedding(vocab_size, d_model)

# 相当于一个查找表:
# embedding_matrix = torch.randn(vocab_size, d_model)
# input_ids = [3, 10, 7]  # token IDs
# embeddings = embedding_matrix[input_ids]  # 形状(3, d_model)

# 参数:
# num_embeddings=vocab_size: 嵌入字典的大小
# embedding_dim=d_model: 每个嵌入向量的维度

【2】前向传播方法

def forward(self, x):
    # (batch, seq_len) --> (batch, seq_len, d_model)
    return self.embedding(x) * math.sqrt(self.d_model)

为什么乘以 √d_model?

# return self.embedding(x) * math.sqrt(self.d_model)

# 为什么要乘以√d_model?
# 这是Transformer论文中的设计选择,原因如下:

# 1. 缩放嵌入值,使其与位置编码的幅度匹配
# 位置编码的值范围在[-1, 1]之间
# 嵌入值经过缩放后也有相似的幅度

# 2. 保持方差稳定
# 假设嵌入权重用标准正态分布初始化 N(0,1)
# 那么嵌入值的方差 ≈ 1
# 乘以√d_model后,方差 ≈ d_model
# 这与位置编码的方差匹配

# 3. 数学推导:
# 设嵌入权重 W ∈ R^{vocab×d},初始化时 W_{ij} ∼ N(0,1)
# 对于一个token的嵌入向量 e = W[i,:]
# 则 Var(e_j) = 1
# 设嵌入缩放因子 α
# 缩放后:e'_j = α × e_j
# 我们希望 Var(e'_j) = d_model(与位置编码匹配)
# 所以:α² × Var(e_j) = d_model
#       α² × 1 = d_model
#       α = √d_model

2.ProjectionLayer

作用:看似简单的ProjectionLayer实际上是Transformer生成能力的核心,它将模型的"理解"转换为具体的"预测",是连接模型内部表示和实际输出的桥梁。

class ProjectionLayer(nn.Module):

    def __init__(self, d_model, vocab_size) -> None:
        super().__init__()
        self.proj = nn.Linear(d_model, vocab_size)

    def forward(self, x) -> None:
        # (batch, seq_len, d_model) --> (batch, seq_len, vocab_size)
        return self.proj(x)

代码说明:
【1】nn.Linear(d_model, vocab_size)

# 核心:线性变换层
# 权重矩阵形状: (vocab_size, d_model)
# 偏置向量形状: (vocab_size,)

# 数学上:
# 输入: x ∈ R^{batch×seq_len×d_model}
# 权重: W ∈ R^{vocab_size×d_model}
# 偏置: b ∈ R^{vocab_size}
# 输出: y = x·W^T + b ∈ R^{batch×seq_len×vocab_size}
# 参数量计算:
params = (d_model × vocab_size) + vocab_size

# 示例:d_model=512, vocab_size=50000
params = (512 × 50000) + 50000 = 25,600,000 + 50,000 = 25,650,000

ProjectionLayer的关键点:
(1)核心功能:
将解码器的高维表示映射到词汇表空间
输出每个位置每个词的得分(logits)
为后续的softmax和交叉熵损失做准备
(2)设计特点:
简单的线性层,但参数量可能很大
可以与词嵌入层进行权重绑定
合理的初始化很重要
(3)实际应用:
通常是Transformer中参数量最大的层之一
对于大词汇表,可能成为计算和内存瓶颈
需要仔细优化(梯度裁剪、混合精度等)
(4)优化建议:

# 最佳实践:
# 1. 使用权重绑定减少参数量
# 2. 应用合适的初始化(如Xavier)
# 3. 对于大词汇表考虑自适应softmax
# 4. 使用梯度裁剪防止梯度爆炸
# 5. 考虑混合精度训练节省内存

3.Transformer

作用:这个Transformer类是整个模型的核心骨架,它将所有组件优雅地组织在一起,提供了清晰的接口用于训练和推理,是现代NLP模型的经典设计模式。

class Transformer(nn.Module):

    def __init__(self, encoder: Encoder, decoder: Decoder, src_embed: InputEmbeddings, tgt_embed: InputEmbeddings,
                 src_pos: PositionalEncoding, tgt_pos: PositionalEncoding, projection_layer: ProjectionLayer) -> None:
        super().__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.tgt_embed = tgt_embed
        self.src_pos = src_pos
        self.tgt_pos = tgt_pos
        self.projection_layer = projection_layer

    def encode(self, src, src_mask):
        # (batch, seq_len, d_model)
        src = self.src_embed(src)
        src = self.src_pos(src)
        return self.encoder(src, src_mask)

    def decode(self, encoder_output: torch.Tensor, src_mask: torch.Tensor, tgt: torch.Tensor, tgt_mask: torch.Tensor):
        # (batch, seq_len, d_model)
        tgt = self.tgt_embed(tgt)
        tgt = self.tgt_pos(tgt)
        return self.decoder(tgt, encoder_output, src_mask, tgt_mask)

    def project(self, x):
        # (batch, seq_len, vocab_size)
        return self.projection_layer(x)

代码说明:
【1】编码方法 encode()

# 输入: src (batch_size, src_seq_len) - token IDs
# 输入: src_mask (batch_size, 1, 1, src_seq_len) - 注意力掩码

# 步骤1: 词嵌入
# src_embed: (batch, seq_len) → (batch, seq_len, d_model)
embedded_src = self.src_embed(src)

# 步骤2: 位置编码
# src_pos: (batch, seq_len, d_model) → (batch, seq_len, d_model)
pos_encoded_src = self.src_pos(embedded_src)

# 步骤3: 编码器处理
# encoder: (batch, seq_len, d_model) → (batch, seq_len, d_model)
encoder_output = self.encoder(pos_encoded_src, src_mask)

# 输出: encoder_output - 源语言的上下文表示
# 形状: (batch_size, src_seq_len, d_model)

可视化流程:

编码器流程:
src (token IDs)
    ↓
src_embed (词嵌入: token→vector)
    ↓
src_pos (位置编码: 添加位置信息)
    ↓
encoder (6层编码器: 提取特征)
    ↓
encoder_output (上下文表示)

【2】解码方法 decode()

# 输入参数:
# encoder_output: 编码器输出 (batch, src_seq_len, d_model)
# src_mask: 源语言掩码 (batch, 1, 1, src_seq_len)
# tgt: 目标语言输入 (batch, tgt_seq_len) - token IDs
# tgt_mask: 目标语言掩码 (batch, 1, tgt_seq_len, tgt_seq_len)

# 步骤1: 目标语言词嵌入
embedded_tgt = self.tgt_embed(tgt)  # (batch, tgt_seq_len, d_model)

# 步骤2: 目标语言位置编码
pos_encoded_tgt = self.tgt_pos(embedded_tgt)

# 步骤3: 解码器处理
# 输入: 目标序列 + 编码器输出 + 两个掩码
decoder_output = self.decoder(pos_encoded_tgt, encoder_output, src_mask, tgt_mask)

# 输出: decoder_output - 目标语言的上下文表示
# 形状: (batch_size, tgt_seq_len, d_model)

解码器内部流程:

解码器流程:
tgt (token IDs)
    ↓
tgt_embed (词嵌入)
    ↓
tgt_pos (位置编码)
    ↓
decoder (6层解码器: 
    ↓ 1. 掩码自注意力 (目标序列内部)
    ↓ 2. 交叉注意力 (关注编码器输出)
    ↓ 3. 前馈网络)
    ↓
decoder_output (目标语言上下文表示)

组件间的数据流动:

def visualize_data_flow():
    """可视化Transformer中的数据流动"""
    
    print("=== Transformer 数据流动可视化 ===\n")
    
    # 模拟数据流
    batch_size = 2
    src_seq_len = 10
    tgt_seq_len = 12
    d_model = 512
    vocab_size = 5000
    
    # 数据流图示
    flow = """
    训练阶段 (Teacher Forcing):
    
    源语言 (英文):
    src_ids (batch, {src_len}) 
        ↓ src_embed (词嵌入)
    src_embeddings (batch, {src_len}, {d_model})
        ↓ src_pos (位置编码)
    src_encoded (batch, {src_len}, {d_model})
        ↓ encoder (6层编码器)
    encoder_output (batch, {src_len}, {d_model})
    
    目标语言 (中文):
    tgt_ids (batch, {tgt_len})
        ↓ tgt_embed (词嵌入)
    tgt_embeddings (batch, {tgt_len}, {d_model})
        ↓ tgt_pos (位置编码)
    tgt_encoded (batch, {tgt_len}, {d_model})
        ↓ decoder (6层解码器) + encoder_output
    decoder_output (batch, {tgt_len}, {d_model})
        ↓ projection_layer (线性投影)
    logits (batch, {tgt_len}, {vocab})
    
    推理阶段 (Autoregressive Generation):
    
    1. 编码源序列 → encoder_output
    2. 初始: generated = [<bos>]
    3. while not <eos> and length < max_len:
        4. 解码当前序列 → decoder_output
        5. 投影 → logits
        6. 选择下一个token (greedy/sampling)
        7. 添加到generated
    8. 返回generated
    """.format(
        src_len=src_seq_len,
        tgt_len=tgt_seq_len,
        d_model=d_model,
        vocab=vocab_size
    )
    
    print(flow)

# 运行可视化
visualize_data_flow()

4.build_transformer

def build_transformer(src_vocab_size: int, tgt_vocab_size: int, src_seq_len: int, tgt_seq_len: int, d_model: int = 512,
                      N: int = 6, h: int = 8, dropout: float = 0.1, d_ff: int = 2048) -> Transformer:
    # 创建Embedding层
    src_embed = InputEmbeddings(d_model, src_vocab_size)
    tgt_embed = InputEmbeddings(d_model, tgt_vocab_size)

    # 创建位置编码层
    src_pos = PositionalEncoding(d_model, src_seq_len, dropout)
    tgt_pos = PositionalEncoding(d_model, tgt_seq_len, dropout)

    # 创建编码模块
    encoder_blocks = []
    for _ in range(N):
        encoder_self_attention_block = MultiHeadAttentionBlock(d_model, h, dropout)
        feed_forward_block = FeedForwardBlock(d_model, d_ff, dropout)
        encoder_block = EncoderBlock(d_model, encoder_self_attention_block, feed_forward_block, dropout)
        encoder_blocks.append(encoder_block)

    # 创建解码模块
    decoder_blocks = []
    for _ in range(N):
        decoder_self_attention_block = MultiHeadAttentionBlock(d_model, h, dropout)
        decoder_cross_attention_block = MultiHeadAttentionBlock(d_model, h, dropout)
        feed_forward_block = FeedForwardBlock(d_model, d_ff, dropout)
        decoder_block = DecoderBlock(d_model, decoder_self_attention_block, decoder_cross_attention_block,
                                     feed_forward_block, dropout)
        decoder_blocks.append(decoder_block)

    # 创建编码器和解码器
    encoder = Encoder(d_model, nn.ModuleList(encoder_blocks))
    decoder = Decoder(d_model, nn.ModuleList(decoder_blocks))

    # 创建输出映射层
    projection_layer = ProjectionLayer(d_model, tgt_vocab_size)

    # 创建Transformer
    transformer = Transformer(encoder, decoder, src_embed, tgt_embed, src_pos, tgt_pos, projection_layer)

    # 初始化参数
    for p in transformer.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform_(p)

    return transformer

代码说明:
【1】解码器与编码器的关键区别:

# 1. 两个注意力层:
decoder_self_attention_block    # 掩码自注意力(防止看到未来信息)
decoder_cross_attention_block   # 交叉注意力(关注编码器输出)

# 2. 掩码自注意力:
# 使用下三角掩码,确保每个位置只能看到自己和前面的位置
# 这是自回归生成的关键

# 3. 交叉注意力:
# Q来自解码器,K、V来自编码器输出
# 让解码器在生成每个词时关注源语言的相关部分

# 解码器块结构:
# 输入 → LayerNorm → 掩码自注意力 → 残差连接 →
# LayerNorm → 交叉注意力 → 残差连接 →
# LayerNorm → 前馈网络 → 残差连接 → 输出

【2】参数初始化

# 初始化参数
for p in transformer.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

Xavier初始化的原理:

# Xavier/Glorot初始化:
# 对于线性层,初始化权重为均匀分布:
# W ~ U(-√(6/(fan_in+fan_out)), √(6/(fan_in+fan_out)))

# 对于多头注意力中的Q、K、V投影:
# fan_in = d_model, fan_out = d_k = d_model/h
# 所以范围是:±√(6/(d_model + d_model/h))

# 作用:
# 1. 保持前向传播中激活值的方差
# 2. 保持反向传播中梯度的方差
# 3. 有助于深层网络训练

# 其他初始化选项:
# 1. Kaiming/He初始化:适合ReLU
# 2. 正态分布:N(0, 0.02)
# 3. 截断正态:限制范围防止过大值

2.4.推理代码

import torch
import sentencepiece as spm
from transformer import build_transformer  # adjust import path as needed

# ---------------------#
# 1. Load Tokenizers
# ---------------------#
sp_en = spm.SentencePieceProcessor()
sp_en.load('en_bpe.model')
sp_cn = spm.SentencePieceProcessor()
sp_cn.load('zh_bpe.model')

PAD_ID = sp_en.pad_id()  # 1
UNK_ID = sp_en.unk_id()  # 0
BOS_ID = sp_en.bos_id()  # 2
EOS_ID = sp_en.eos_id()  # 3

# ---------------------#
# 2. Load Trained Model
# ---------------------#
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# Model hyperparameters (must match training)
SRC_VOCAB_SIZE = 5000    # 16000
TGT_VOCAB_SIZE = 5000    # 16000
SRC_SEQ_LEN = 128
TGT_SEQ_LEN = 128

model = build_transformer(SRC_VOCAB_SIZE, TGT_VOCAB_SIZE, SRC_SEQ_LEN, TGT_SEQ_LEN).to(DEVICE)
model.load_state_dict(torch.load('transformer.pt', map_location=DEVICE))  # load your trained model
model.eval()


# ---------------------#
# 3. Translation Function (Greedy)
# ---------------------#
def create_mask(src, pad_idx):
    return (src != pad_idx).unsqueeze(1).unsqueeze(2)  # (1, 1, 1, src_len)


def translate_sentence(sentence, max_len=100):
    # 将英文句子转换为token IDs,例如: "Hello world" → [10, 20]
    tokens = [BOS_ID] + sp_en.encode(sentence, out_type=int) + [EOS_ID]
    src_tensor = torch.LongTensor(tokens).unsqueeze(0).to(DEVICE)  # [1, src_len],将Python列表转换为PyTorch张量
    src_mask = create_mask(src_tensor, PAD_ID) # 形状为 (1, 1, 1, src_len) 的布尔张量

    # 初始化为 [BOS_ID],表示翻译从<bos>开始
	# 这是自回归生成的标准做法
    trg_indices = [BOS_ID] 

    with torch.no_grad():  # 上下文管理器,禁用梯度计算;在推理阶段不需要计算梯度,节省内存和计算资源
        # Encode the source sentence
        encoder_output = model.encode(src_tensor, src_mask)

        # Generate translation token by token
        for _ in range(max_len):
            trg_tensor = torch.LongTensor(trg_indices).unsqueeze(0).to(DEVICE)  # [1, current_trg_len]

            # Create target mask
            trg_mask = torch.tril(torch.ones((len(trg_indices), len(trg_indices)), device=DEVICE)).bool()
            trg_mask = trg_mask.unsqueeze(0).unsqueeze(0)  # [1, 1, trg_len, trg_len]

            # Decode
            decoder_output = model.decode(encoder_output, src_mask, trg_tensor, trg_mask)
            output = model.project(decoder_output)

            # Get the last predicted token
            pred_token = output.argmax(2)[:, -1].item()
            trg_indices.append(pred_token)

            if pred_token == EOS_ID:
                break

    # Convert token IDs to text (skip <bos> and <eos>)
    translated = sp_cn.decode(trg_indices[1:-1])
    return translated


# ---------------------#
# 4. Interactive Test
# ---------------------#
if __name__ == '__main__':
    print("Transformer Translator (type 'quit' or 'exit' to end)")
    while True:
        src_sent = input("\nEnter English sentence: ")
        if src_sent.lower() in ['quit', 'exit']:
            break
        translation = translate_sentence(src_sent)
        print(f"Chinese Translation: {translation}")

1.创建掩码函数

def create_mask(src, pad_idx):
    return (src != pad_idx).unsqueeze(1).unsqueeze(2)  # (1, 1, 1, src_len)
# 函数功能:创建源语言序列的注意力掩码

# 参数:
# src: 源语言token IDs,形状 (batch_size, src_len)
# pad_idx: PAD token的ID

# 计算过程:
# 1. src != pad_idx: 创建布尔掩码,True表示非PAD位置
#    例如:src = [[2, 10, 3, 1, 1]],pad_idx=1
#          src != 1 → [[True, True, True, False, False]]
# 2. .unsqueeze(1): 在维度1增加一个维度
#    形状变为: (batch_size, 1, src_len)
# 3. .unsqueeze(2): 在维度2增加一个维度  
#    形状变为: (batch_size, 1, 1, src_len)

# 为什么需要这样的形状?
# 多头注意力需要形状 (batch, num_heads, seq_len, seq_len)
# 这里创建的是 (batch, 1, 1, src_len),可以在计算注意力时广播

2.翻译函数(贪婪解码)

<1>编码源句子
    with torch.no_grad():
        # Encode the source sentence
        encoder_output = model.encode(src_tensor, src_mask)
# with torch.no_grad(): 上下文管理器,禁用梯度计算
# 在推理阶段不需要计算梯度,节省内存和计算资源

# model.encode(src_tensor, src_mask): 调用Transformer的编码方法
# 内部流程:
# 1. src_embed: token IDs → 词嵌入向量
# 2. src_pos: 添加位置编码
# 3. encoder: 6层编码器处理
# 输出: encoder_output,形状 (1, src_len, d_model)
<2>自回归生成循环
        # Generate translation token by token
        for _ in range(max_len):
            trg_tensor = torch.LongTensor(trg_indices).unsqueeze(0).to(DEVICE)  # [1, current_trg_len]
# for _ in range(max_len): 循环最多max_len次
# 每次迭代生成一个token

# trg_tensor = torch.LongTensor(trg_indices).unsqueeze(0).to(DEVICE):
# 将当前生成的目标序列转换为张量
# 例如:第1次循环: trg_indices=[2] → tensor([[2]])
#       第2次循环: trg_indices=[2, 30] → tensor([[2, 30]])
#       第3次循环: trg_indices=[2, 30, 40] → tensor([[2, 30, 40]])
<3>创建目标掩码
# Create target mask
trg_mask = torch.tril(torch.ones((len(trg_indices), len(trg_indices)), device=DEVICE)).bool()
trg_mask = trg_mask.unsqueeze(0).unsqueeze(0)  # [1, 1, trg_len, trg_len]
# 步骤1: torch.ones((len(trg_indices), len(trg_indices))): 创建全1矩阵
#        例如当前生成长度=3: [[1, 1, 1],
#                            [1, 1, 1],
#                            [1, 1, 1]]

# 步骤2: torch.tril(...): 取矩阵的下三角部分(包括对角线)
#        得到: [[1, 0, 0],
#              [1, 1, 0],
#              [1, 1, 1]]

# 步骤3: .bool(): 转换为布尔类型
#        得到: [[True, False, False],
#              [True, True, False],
#              [True, True, True]]

# 步骤4: .unsqueeze(0).unsqueeze(0): 增加两个维度
#        形状变为: (1, 1, trg_len, trg_len)

# 为什么需要下三角掩码?
# 自回归生成中,每个位置只能看到自己和前面的位置
# 不能看到未来的信息(否则就是作弊了)
<4>解码和投影
# Decode
decoder_output = model.decode(encoder_output, src_mask, trg_tensor, trg_mask)
output = model.project(decoder_output)
# model.decode(...): 调用Transformer的解码方法
# 参数:
# 1. encoder_output: 编码器输出
# 2. src_mask: 源语言掩码
# 3. trg_tensor: 当前目标序列
# 4. trg_mask: 目标掩码(因果掩码)

# 解码器内部流程:
# 1. tgt_embed: 目标token IDs → 词嵌入向量
# 2. tgt_pos: 添加位置编码
# 3. decoder: 6层解码器处理(包含掩码自注意力和交叉注意力)

# model.project(decoder_output): 将解码器输出投影到词汇表空间
# 输出形状: (1, current_trg_len, tgt_vocab_size)
# 例如: (1, 3, 5000) 表示3个位置,每个位置5000个词的得分
<5>获取预测token
# Get the last predicted token
pred_token = output.argmax(2)[:, -1].item()
trg_indices.append(pred_token)
# output.argmax(2): 在词汇表维度(维度2)取argmax
# 得到每个位置预测的token ID
# 形状: (1, current_trg_len)

# output.argmax(2)[:, -1]: 取最后一个位置的预测
# 例如: output形状(1, 3, 5000) → argmax后得到(1, 3)
#      [:, -1] 取最后一列,得到第3个位置的预测

# .item(): 从张量中提取Python数值

# trg_indices.append(pred_token): 将预测的token添加到目标序列
# 下一次循环时,这个token会作为输入的一部分
<6>终止条件检查
if pred_token == EOS_ID:
    break
# if pred_token == EOS_ID: 检查是否生成<eos> token
# 如果生成<eos>,表示翻译完成,退出循环

# 如果没有生成<eos>,继续循环直到max_len
<7>转换为文本
    # Convert token IDs to text (skip <bos> and <eos>)
    translated = sp_cn.decode(trg_indices[1:-1])
    return translated
# trg_indices[1:-1]: 去掉开头的<bos>和结尾的<eos>
# 例如: [2, 30, 40, 50, 3] → [30, 40, 50]

# sp_cn.decode(...): 将中文token IDs转换回文本
# 例如: [30, 40, 50] → "机器学习"

# return translated: 返回翻译结果

3.完整工作流程图

用户输入英文句子
    ↓
英文分词 → token IDs
    ↓
添加<bos>和<eos>
    ↓
编码器处理 → 上下文表示
    ↓
初始化目标序列为[<bos>]
    ↓
循环生成(自回归):
    1. 解码当前序列
    2. 获取下一个token(贪婪选择)
    3. 添加到目标序列
    4. 检查是否<eos>
    ↓
去掉<bos>和<eos>
    ↓
中文token IDs → 中文文本
    ↓
输出翻译结果

2.5.BLEU评估代码

import sacrebleu
from inference import translate_sentence
# 读取验证集的英文原文和中文参考
with open('data/en2cn/valid_en.txt', 'r', encoding='utf-8') as f:
    src_sentences = [line.strip() for line in f.readlines()]

with open('data/en2cn/valid_zh.txt', 'r', encoding='utf-8') as f:
    ref_sentences = [line.strip() for line in f.readlines()]

# 检查长度是否匹配
assert len(src_sentences) == len(ref_sentences), "源语言和参考翻译句子数不匹配"

# 用模型生成翻译
hypotheses = []
for i, src in enumerate(src_sentences):
    print(f"Translating {i+1}/{len(src_sentences)}...")
    translation = translate_sentence(src)
    print(ref_sentences[i], translation)
    hypotheses.append(translation.strip())

# 计算 BLEU
bleu = sacrebleu.corpus_bleu(hypotheses, [ref_sentences], tokenize='zh')

print("\n========== BLEU Evaluation Result ==========")
print(f"BLEU Score: {bleu.score:.2f}")
  1. import sacrebleu: 导入sacrebleu库,用于计算BLEU分数
  2. sacrebleu是标准的BLEU计算工具,解决了传统BLEU计算的许多问题(如tokenization不一致)
  3. 提供标准化、可复现的BLEU分数计算。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值