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.基础代码
- 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
- 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)批量处理所有目标序列
分别对它们进行填充等操作
这是处理成对数据时非常实用和高效的技巧! - nn.utils.rnn.pad_sequence 的作用
核心功能:将不同长度的序列填充到相同长度,组成一个整齐的Tensor。
为什么需要填充?在深度学习中,尤其是处理序列数据(如文本)时:
(1)GPU需要批处理(batch processing)来并行计算;
(2)批处理要求同一个batch内的所有样本形状相同,但自然语言句子长度是不同的;
参数详解:
nn.utils.rnn.pad_sequence(
sequences, # 要填充的序列列表/元组
batch_first=True, # 重要参数!
padding_value=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的损失
关键要点总结
- 训练模式设置:model.train() 启用dropout等训练特有的行为
- 教师强制:使用真实的前一个词作为解码器输入,提高训练稳定性
- 梯度管理:每个batch前清零梯度,防止累积
- Transformer流程:编码 → 解码 → 投影 → 计算损失
- 数据重塑:将3D输出转换为2D,适应交叉熵损失函数的输入要求
- 损失计算:使用带ignore_index的CrossEntropyLoss,忽略PAD token
- 优化步骤:反向传播计算梯度,优化器更新参数
- 进度监控:定期打印损失,监控训练进展
这个训练循环实现了标准的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}")
- import sacrebleu: 导入sacrebleu库,用于计算BLEU分数
- sacrebleu是标准的BLEU计算工具,解决了传统BLEU计算的许多问题(如tokenization不一致)
- 提供标准化、可复现的BLEU分数计算。

被折叠的 条评论
为什么被折叠?



