06. Seq2Seq模型
一、什么是 Seq2Seq 模型
传统的自然语言处理任务(如文本分类、序列标注)以静态输出为主,其目标是预测固定类别或标签。然而,现实中的许多应用需要模型动态生成新的序列,例如:
- 机器翻译:输入中文句子,输出对应的英文翻译。
- 文本摘要:输入长篇文章,生成简短的摘要。
- 问答系统:输入用户问题,生成自然语言回答。
- 对话系统:输入对话历史,生成连贯的写一条回复。
这些任务具有两个关键共同点:
- 输入和输出均为序列(如词、字符或字词序列)。
- 输入与输出序列长度动态可变(例如翻译任务中,中英文句子长句可能不同)
为了解决这类问题,研究者提供了 Seq2Seq(Sequence to Sequence,序列到序列)模型。Seq2Seq 模型由一个 编码器(Encoder)和 解码器(Decoder)构成。
- 编码器 主要由一个 循环神经网络(RNN / LSTM / GRU)构成,其任务是 将输入序列的语义信息提取并压缩为一个上下文向量。
- 解码器 主要也由一个 循环神经网络(RNN / LSTM / GRU)构成,其任务是 基于编码器传递的上下文向量,逐步生成目标序列。

我们在终端中输入 pip 指令安装 PyTorch。默认是从国外的主站上下载,因此,我们可能会遇到网络不好的情况导致下载失败。我们可以在 pip 指令后通过 -f 指定国内镜像源下载。
pip install torch torchvision -f https://mirrors.aliyun.com/pytorch-wheels/cu132
国内常用的 pip 下载源列表:
- 阿里云 https://mirrors.aliyun.com/pypi/simple
- 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple
- 中国科学技术大学 https://pypi.mirrors.ustc.edu.cn/simple
安装完 PyTorch 之后,我们可以通过如下代码查看 CUDA 是否可用:
import torch
print(f"PyTorch的版本:{torch.__version__}")
print(f"CUDA是否可用:{torch.cuda.is_available()}")
如果输出 CUDA 不可用,有可能你使用国内镜像只同步 PyPI 主站,不包含 PyTorch 官方 CUDA 专用包,
pip只会找到 CPU 版,找不到 CUDA 版。因此,我们还需要使用--extra-index-url https://download.pytorch.org/whl/cu132补充官方 CUDA 源 。
二、模型的训练和推理机制
2.1、训练机制
Seq2Seq 模型的训练目标,是在给定输入序列的条件下,逐步生成完整且准确的目标序列。下面以一个中 – 英机器翻译样本为例,说明训练过程的各个环节。这里,我们假设某个训练样本为:
- 中文输入:我喜欢你。
- 英文输出:I like you.
【1】、数据准备
为了让模型明确目标序列的起点和终点,我们通常在目标句前添加 <sos>(start of sequence),句末添加 <eos>(end of sequence)两个特殊标记。这两个特殊标记帮助模型学会从哪里开始生成,以及何时停止生成。
"I like you." → "<sos> I like you. <eos>"
【2】、前向传播
在模型处理输入序列时,编码器 的 循环神经网络 会依次接受每个 token 的输入,并在每个时间步更新隐藏状态。每个隐藏状态都携带了截止到当前位置为止的信息。
随着序列推进,信息不断积累,最终会 在最后一个时间步生成一个包含整句信息的隐藏状态。最后这个隐藏状态就会作为 上下文向量,传递给 解码器,用于知道后续的序列生成。

在生成开始时,解码器 的 循环神经网络 以 上下文向量 作为 初始隐藏状态,并接受一个特殊的 起始标记(<sos>,start of sentence)作为 第一个时间步的输入,用于 预测第一个 token。
随后,在每一个时间步,模型都会把 前一时刻的目标序列中真实的 token 作为为下一步输入,这种策略叫做 Teacher Forcing 策略。
生成过程会持续进行,直到模型生成了一个特殊的 结束标记(<eos>,end of sentence),表示句子生成完成。

使用 Teacher Forcing 策略带来了两个明显好处:
- 训练更快,误差不会累积。
- 梯度传播更稳定,有利于优化收敛。
为了增强编码器的理解能力,循环网络也可以采用 双向结构(结合前文和后文信息)或 多层结构(提取更深的语义信息)。
【3】、计算损失
解码器每一步输出一个 token 的概率分布,我们通过交叉熵损失函数衡量模型对真实词的预测质量。训练过程中,每一个时间步都会产生一个损失值。该样本的总损失,就是所有时间步的损失值逐步累加的结果。

【4】、反向传播
在 PyTorch 中,我们可以调用 loss.backward() 即可自动完成梯度的反向传播。系统会沿时间维度展开计算图,自动完成所有参数的梯度计算,无需手动推导,实现简洁高效。
2.2、推理机制
【1】、编码器处理
推理阶段的编码器处理流程与训练时完全一致。输入序列会经过分词、嵌入和循环神经网络的逐步处理,最终生成一个表示整句语义的上下文向量,该向量将作为解码器的初始隐藏状态,为生成过程提供语义基础。
【2】、解码器处理
解码器是推理过程的核心,其生成方式采用 自回归生成(Autoregressive Generation):每一步的输出会作为下一步的输入,逐步构造完整句子。

自回归生成流程如下:
- 解码器接收起始标记
<sos>,生成第一个词。 - 将上一步生成的词作为当前输入,再预测第二个词。
- 持续重复以上过程,直到模型生成
<eos>,或达到设定的最大生成步数。
每个时间步,解码器输出的是一个 词概率分布。我们需要从中选择一个具体词作为本时间步的输出,选择方式即为生成策略。常见策略包括:
- 贪心解码(Greedy Decoding):每一步都选择概率最高的词。它的过程简单高效,但容易陷入局部最优,生成不够多样。
- 束搜索(Beam Search):每一步保留多个候选词序列(如 beam size = 3),并在扩展后选择得分最高的完整句子。它基于全局考虑,生成质量高,但计算开销大。
起始标记和结束标记会在训练数据中显式添加,模型会在训练中学会何时开始、如何续写,以及何时结束,从而掌握完整的生成流程。
三、Seq2Seq 存在的问题
在上述 Seq2Seq 模型中,编码器会将整个源句压缩为一个固定长度的上下文向量,并将其作为解码器生成目标序列的唯一参考。这种 “压缩再解压” 的方式虽然结构简洁,但在实际任务中暴露出两个核心问题:
【1】、信息压缩困难,语义表达受限
对于编码器而言,用一个定长向量去表达任意复杂的句子,是一项非常困难的任务。尤其在面对长句时,信息很容易在压缩过程中丢失,导致语义表达不完整。这种 “信息瓶颈” 限制了模型在处理长文本或复杂语义结构时的表现。
【2】、缺乏动态感知,解码难以精准生成
解码器始终只能基于同一个上下文向量进行生成。但在实际生成过程中,不同位置的目标词,往往依赖源句中不同的关键信息:
- 生成主语时,可能更依赖源句的开头。
- 生成谓语或宾语时,可能需要参考句中或句末内容。
然而在固定表示下,解码器无法 “有选择地关注” 输入序列的不同部分,只能一视同仁地处理所有信息,从而降低了生成的准确性与灵活性。
四、中英翻译案例
我们可以从 阿里天池 中下载 中英翻译的数据集: https://tianchi.aliyun.com/dataset/174937 。
我们可以在终端中使用 pip 安装 pandas、sklearn 机器学习库和 ntlk 自然语言处理工具包。默认是从国外的主站上下载,因此,我们可能会遇到网络不好的情况导致下载失败。我们可以在 pip 指令后通过 -i 指定国内镜像源下载。
pip install scikit-learn nltk -i https://pypi.tuna.tsinghua.edu.cn/simple
数据预处理:
import os
DATA_DRIECTORY = os.path.join(os.getcwd(), "assets", "data")
MODEL_DRIECTORY = os.path.join(os.getcwd(), "assets", "models")
UNK_TOKEN = "<unk>"
PAD_TOKEN = "<pad>"
SOS_TOKEN = "<sos>"
EOS_TOKEN = "<eos>"
SPECIAL_TOKENS = [UNK_TOKEN, PAD_TOKEN, SOS_TOKEN, EOS_TOKEN]
import os
import pandas as pd
from nltk.tokenize import TreebankWordTokenizer
from sklearn.model_selection import train_test_split
def preprocess_data(file_path, test_size):
# 读取 TSV 格式的文件,
# sep 指定字段的分隔符,head=None 表示没有表头,
# secols 为要使用的列,可以使用索引,names 表示为列取的标签名
df = pd.read_csv(file_path, sep="\t", header=None, usecols=[0, 1], names=["en", "zh_cn"], encoding="utf-8").dropna()
train_df, test_df = train_test_split(df, test_size=test_size) # 划分训练集与测试集
# 构建中文词表,这里采用按字符切分
zh_cn_vacab_set = set([vacab for sentence in train_df["zh_cn"] for vacab in sentence if vacab.strip()])
zh_cn_vacab_list = SPECIAL_TOKENS + list(zh_cn_vacab_set) # 添加特殊标记
print("构建中文词表成功")
# 构建英文词表,基于正则匹配规则,将输入的英文句子拆分成包含单词和独立标点符号的词元列表(Token List)
tokenizer = TreebankWordTokenizer()
en_vacab_set = set([vacab for sentence in train_df["en"] for vacab in tokenizer.tokenize(sentence) if vacab.strip()])
en_vacab_list = SPECIAL_TOKENS + list(en_vacab_set) # 添加特殊标记
print("构建英文词表成功")
if not os.path.exists(DATA_DRIECTORY):
os.makedirs(DATA_DRIECTORY)
# 保存词表到文件中
file_path = os.path.join(DATA_DRIECTORY, "zh_cn_vocab.txt")
with open(file_path, "w", encoding="utf-8") as f:
f.write("\n".join(zh_cn_vacab_list))
print("保存中文词表到文件中成功")
file_path = os.path.join(DATA_DRIECTORY, "en_vocab.txt")
with open(file_path, "w", encoding="utf-8") as f:
f.write("\n".join(en_vacab_list))
print("保存英文词表到文件中成功")
# 构建词表到索引的映射
zh_cn_word2index = {word: index for index, word in enumerate(zh_cn_vacab_list)}
print("构建中文词表到索引的映射成功")
en_word2index = {word: index for index, word in enumerate(en_vacab_list)}
print("构建英文词表到索引的映射成功")
# 构建训练集
train_df["zh_cn"] = train_df["zh_cn"].apply(lambda sentence: get_word2index(zh_cn_word2index, list(sentence)))
train_df["en"] = train_df["en"].apply(lambda sentence: get_word2index(en_word2index, tokenizer.tokenize(sentence), True))
file_path = os.path.join(DATA_DRIECTORY, "translation_train.jsonl")
train_df.to_json(file_path, orient="records", lines=True) # 保存训练集
print("保存训练集数据成功")
# 构建测试集
test_df["zh_cn"] = test_df["zh_cn"].apply(lambda sentence: get_word2index(zh_cn_word2index, list(sentence)))
test_df["en"] = test_df["en"].apply(lambda sentence: get_word2index(en_word2index, tokenizer.tokenize(sentence), True))
# 保存测试集
file_path = os.path.join(DATA_DRIECTORY, "translation_test.jsonl")
test_df.to_json(file_path, orient="records", lines=True)
print("保存测试集数据成功")
def get_word2index(word2index, tokens, add_sos_eos=False):
if add_sos_eos:
tokens = [SOS_TOKEN] + tokens + [EOS_TOKEN]
return [word2index.get(token, SPECIAL_TOKENS.index(UNK_TOKEN)) for token in tokens]
if __name__ == "__main__":
test_size = 0.2
file_path = os.path.join(DATA_DRIECTORY, "cmn.txt")
preprocess_data(file_path, test_size)
创建数据集:
import torch
import pandas as pd
from torch.utils.data import Dataset
class TranslationDataset(Dataset):
def __init__(self, file_path):
self.data = pd.read_json(file_path, orient="records", lines=True).to_dict(orient="records")
def __len__(self):
return len(self.data)
def __getitem__(self, index):
index = min(max(index, 0), len(self.data) - 1) # 索引边界检查
# x.shape: [源序列长度]
x = torch.tensor(self.data[index]["zh_cn"], dtype=torch.long)
# y.shape: [目标序列长度]
y = torch.tensor(self.data[index]["en"], dtype=torch.long)
return x, y
创建模型:
import torch
from torch import nn
class TranslationEncoder(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers, padding_idx):
super().__init__()
# 词嵌入层 [词向量数(词表大小), 词向量维度, 填充值的索引]
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx)
# GRU 层 [输入维度(词向量维度), 隐藏层维度, 隐藏层层数, 是否将batch作为输入张量的第一个维度]
self.gru = nn.GRU(embedding_dim, hidden_dim, num_layers, batch_first=True)
def forward(self, x):
# x.shape: [批量大小, 序列长度]
# embed.shape: [批量大小, 序列长度, 词向量维度]
embed = self.embedding(x) # 获取单词的嵌入向量
# output 最后一层每个时间步的隐藏层状态
# output.shape: [批量大小, 序列长度, 隐藏层维度]
# hn 最后一个时间步的隐藏层状态(包含每一个层,每一个方向)
# hn.shape: [隐藏层层数, 批量大小, 隐藏层维度]
output, hn = self.gru(embed)
lengths = (x != self.embedding.padding_idx).sum(dim=1) # 获取每个样本真实token的长度
# last_hidden_state.shape: [批量大小, 隐藏层维度]
last_hidden_state = output[torch.arange(output.shape[0]), lengths - 1] # 获取每个样本最后一个时间步隐藏状态
return output, last_hidden_state
class TranslationDecoder(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers, padding_idx):
super().__init__()
# 词嵌入层 [词表的大小, 词向量维度, 填充值的索引]
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx)
# GRU 层 [输入维度(词向量维度), 隐藏层维度, 隐藏层层数, 是否将batch作为输入张量的第一个维度]
self.gru = nn.GRU(embedding_dim, hidden_dim, num_layers, batch_first=True)
# 输出层 [输入维度(隐藏层维度), 输出维度(英文词表大小)]
self.liner = nn.Linear(hidden_dim, vocab_size)
def forward(self, x, hx):
# x.shape: [批量大小, 序列长度(1)]
# hx.shape: [隐藏层层数(1), 批量大小, 隐藏层维度]
# embed.shape: [批量大小, 序列长度(1), 词向量维度]
embed = self.embedding(x) # 获取单词的嵌入向量
# output 最后一层每个时间步的隐藏层状态
# output.shape: [批量大小, 序列长度(1), 隐藏层维度]
# hn 最后一个时间步的隐藏层状态(包含每一个层,每一个方向)
# hn.shape: [隐藏层层数, 批量大小, 隐藏层维度]
output, hn = self.gru(embed, hx)
# Liner 层保留输入张量的所有维度(除最后一维外全部不变),仅将最后一维(特征维度)替换为 out_features
# output.shape: [批量大小, 序列长度(1), 输出维度(英文词表大小)]
output = self.liner(output)
return output, hn
class TranslationModel(nn.Module):
def __init__(self, vocab_sizes, embedding_dim, hidden_dim, num_layers, padding_idxs):
super().__init__()
self.encoder = TranslationEncoder(vocab_sizes[0], embedding_dim, hidden_dim, num_layers, padding_idxs[0])
self.decoder = TranslationDecoder(vocab_sizes[1], embedding_dim, hidden_dim, num_layers, padding_idxs[1])
模型单轮训练:
def model_train_one_epoch(model, device, data_loader, criterion, optimizer):
model.train() # 训练模式
total_loss = 0.0
for x, y in data_loader:
# x.shape: [批量大小, 源序列长度]
# y.shape: [批量大小, 目标序列长度]
x, y = x.to(device), y.to(device) # 指定设备
# 编码阶段
# context_vector.shape: [批量大小, 隐藏层维度]
encoder_outputs, context_vector = model.encoder(x)
# 解码阶段
decoder_inputs = y[:, : -1] # 获取解码器的输入列表
decoder_targets = y[:, 1 :] # 获取解码器的目标列表
# unsqueeze(dim) 方法在指定位置插入一个大小为 1 的新维度
# decoder_hidden.shape: [1(隐藏层层数), 批量大小, 隐藏层维度]
decoder_hidden = context_vector.unsqueeze(0) # 获取解码器的初始状态
decoder_outputs = [] # 解码器的输出列表
seq_len = decoder_inputs.shape[1] # 获取解码器的输入序列长度
for i in range(seq_len):
# decoder_inputs[:, i].shape: [批量大小]
# decoder_input.shape: [批量大小, 1(序列长度)]
decoder_input = decoder_inputs[:, i].unsqueeze(1) # 获取当前时间步的输入
# decoder_output.shape: [批量大小, 序列长度(1), 类别数(词汇表大小)]
decoder_output, decoder_hidden = model.decoder(decoder_input, decoder_hidden)
decoder_outputs.append(decoder_output)
# 计算损失
# decoder_targets.shape: [批量大小, 序列长度] -> [批量大小 * 序列长度]
decoder_targets = decoder_targets.reshape(-1)
# torch.cat() 就是把多个张量沿指定维度 "粘" 在一起,拼接后该维度的长度 = 各张量该维度长度之和,其它维度保持不变
# decoder_outputs: [tensor([批量大小, 序列长度(1), 类别数(词汇表大小)])] -> [批量大小, 各个序列长度维度长度之和, 类别数(词汇表大小)]
decoder_outputs = torch.cat(decoder_outputs, dim=1) # 将多个张量按指定维度拼接
# decoder_outputs.shape:[批量大小 * 序列长度, 类别数(词汇表大小)]
decoder_outputs = decoder_outputs.reshape(-1, decoder_outputs.shape[-1])
# CrossEntropyLoss() 多分类损失函数
# 如果目标值(y)的形状为 N,则预测值(output)的形状为 [N, 类别数]
loss = criterion(decoder_outputs, decoder_targets) # 计算损失
optimizer.zero_grad() # 清空梯度
loss.backward() # 反向传播
optimizer.step() # 优化器更新参数
# x.shape[0] 是当前批次的样本数量
# 乘以 x.shape[0] 是为了消除批次大小不一致(尤其是最后一个不完整批次)带来的统计偏差
total_loss += loss.item() * x.shape[0] # 计算当前批次的损失
# len(dataloader.dataset) 返回数据集的样本数量
# len(dataloader) 返回数据加载器的批次数量
return total_loss / len(data_loader.dataset) # 返回平均损失
训练模型:
EMBEDDING_DIM = 128
HIDDEN_DIM = 256
NUM_LAYERS = 1
BATCH_SIZE = 128
LEARNING_RATE = 1e-3
EPOCHS = 10
from torch.nn.utils.rnn import pad_sequence
def collate_fn(batch):
# batch 是一个二元组列表,[(x_tensor, y_tensor)]
x_tensors = [item[0] for item in batch]
y_tensors = [item[1] for item in batch]
# pad_sequence() 方法将多个长度不同的张量(序列)填充到相同长度,并堆叠成一个批处理张量
# padding_value 填充的值,<pad> 特殊标识的索引
x_tensors = pad_sequence(x_tensors, batch_first=True, padding_value=SPECIAL_TOKENS.index(PAD_TOKEN))
y_tensors = pad_sequence(y_tensors, batch_first=True, padding_value=SPECIAL_TOKENS.index(PAD_TOKEN))
return x_tensors, y_tensors
import os
import torch
from torch.utils.data import DataLoader
def model_train(model, device, data_loader, learning_rate, epochs, pad_token_index):
model.to(device) # 指定设备
# ignore_index 让损失函数忽略指定的目标类别索引,不参与损失计算
criterion = torch.nn.CrossEntropyLoss(ignore_index=pad_token_index) # 多元交叉熵损失函数
optimizer = torch.optim.Adam(model.parameters(), learning_rate) # 创建优化器
# 开始训练
for epoch in range(epochs):
print(f"开始第 {epoch + 1} 次训练")
loss = model_train_one_epoch(model, device, data_loader, criterion, optimizer)
print(f"epoch: {epoch + 1:0>2},loss: {loss:.4f}")
print("训练完成,开始保存模型")
if not os.path.exists(MODEL_DRIECTORY):
os.makedirs(MODEL_DRIECTORY)
file_path = os.path.join(MODEL_DRIECTORY, "translation_model_by_seq2seq.pth")
torch.save(model.state_dict(), file_path) # 保存模型
print("模型保存完成")
if __name__ == "__main__":
pad_token_index = SPECIAL_TOKENS.index(PAD_TOKEN)
padding_idxs = [pad_token_index] * 2
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 加载词表
file_path = os.path.join(DATA_DRIECTORY, "zh_cn_vocab.txt")
with open(file_path, "r", encoding="utf-8") as f:
zh_cn_vocab_list = [line.strip() for line in f.readlines()]
print("加载中文词表完成")
file_path = os.path.join(DATA_DRIECTORY, "en_vocab.txt")
with open(file_path, "r", encoding="utf-8") as f:
en_vocab_list = [line.strip() for line in f.readlines()]
vocab_sizes = [len(zh_cn_vocab_list), len(en_vocab_list)]
print("加载英文词表完成")
file_path = os.path.join(DATA_DRIECTORY, "translation_train.jsonl")
train_dataset = TranslationDataset(file_path) # 创建训练数据集
print("创建训练集成功")
# collate_fn 用来指定自定义的填充方式,如果一批数据中的序列长度不一样,会报错
train_data_loader = DataLoader(train_dataset, BATCH_SIZE, True, collate_fn=collate_fn) # 创建训练数据集加载器
print("创建训练数据集加载器成功")
model = TranslationModel(vocab_sizes, EMBEDDING_DIM, HIDDEN_DIM, NUM_LAYERS, padding_idxs) # 模型
print("创建模型成功")
model_train(model, device, train_data_loader, LEARNING_RATE, EPOCHS, pad_token_index)
在机器翻译任务中,BLEU(Bilingual Evaluation Understudy) 是一种常用的自动评估指标,用于衡量模型生成的翻译与人工参考译文之间的相似程度。其核心思想是:
- n-gram 匹配:统计预测译文中有多少 n-gram(词或短语)同时出现在参考译文中,用于衡量翻译内容的准确性。
- 精确率计算:将匹配到的 n-gram 数量除以预测译文中 n-gram 的总数,反映生成译文中 “正确部分” 的比例。
此外,BLEU 还引入长度惩罚机制,防止模型通过生成过短句子获得不合理的高分。最终得到的 BLEU 分数越高,说明生成译文与参考译文越接近。
在本案例中,我们可以使用 Python 的 NLTK 库 中的 bleu_score 模块,对模型在测试集上的翻译结果进行评估,主要参考 BLEU-4 的得分情况,作为翻译质量的衡量依据。
模型单批次预测:
import torch
def model_predict_one_batch(model, device, x, en_word2index, max_seq_len):
model.eval() # 评估模式
generated_token_indexes = [] # 存储每个时间步的预测概率最大的单词索引
with torch.no_grad(): # 禁用梯度计算
# 编码阶段
# context_vector.shape: [批量大小, 隐藏层维度]
encoder_outputs, context_vector = model.encoder(x)
# 解码阶段
# decoder_hidden.shape: [1(隐藏层层数), 批量大小, 隐藏层维度]
decoder_hidden = context_vector.unsqueeze(0) # 初始隐藏状态
batch_size = x.size(0) # 批量大小
# decoder_input.shape: [批量大小, 序列长度(1)]
# torch.full(张量, 值) 创建一个张量,所有元素都填充为指定值
decoder_input = torch.full((batch_size, 1), en_word2index[SOS_TOKEN], dtype=torch.long, device=device)
is_finished = torch.full([batch_size], False, device=device) # 记录每个样本是否生成完成
# 自回归生成
for i in range(max_seq_len):
# decoder_output.shape: [批量大小, 序列长度(1), 词汇表大小]
decoder_output, decoder_hidden = model.decoder(decoder_input, decoder_hidden) # 解码器前向传播
# next_token_indexes.shape: [批量大小, 序列长度(1)]
next_token_indexes = decoder_output.argmax(dim=-1) # 获取每个时间步的预测概率最大的单词索引
generated_token_indexes.append(next_token_indexes) # 存储每个时间步的预测概率最大的单词索引
decoder_input = next_token_indexes # 更新解码器的输入
# 判断是否生成了 <eos> 标记
is_finished |= (next_token_indexes.squeeze(1) == en_word2index[EOS_TOKEN])
# 如果所有样本都生成完成,跳出循环
if is_finished.all():
break
# 处理预测结果
# generated_tensor.shape: [批量大小, 各个序列长度维度长度之和]
generated_tensor = torch.cat(generated_token_indexes, dim=1) # 合并所有时间步的预测概率最大的单词索引
generated_list = generated_tensor.tolist()
# 去掉 <eos> 标记之后的 token id
for index, sentence in enumerate(generated_list):
if en_word2index[EOS_TOKEN] in sentence:
generated_list[index] = sentence[:sentence.index(en_word2index[EOS_TOKEN])]
return generated_list
模型评估:
MAX_SEQ_LEN = 100
import os
import torch
from torch.utils.data import DataLoader
from nltk.translate.bleu_score import corpus_bleu
def model_evaluate(model, device, data_loader, en_word2index, max_seq_len):
model.to(device) # 指定设备
model.eval() # 评估模式
# predictions: [[*, *, *], [*, *, *]]
predictions = []
# references: [[[*, *, *]], [[*, *, *]]]
references = []
for x, y in data_loader:
# x.shape: [批量大小, 源序列长度]
# y.shape: [批量大小, 目标序列长度]
x, y = x.to(device), y.to(device) # 指定设备
# targets: [[<sos>, *, *, <eos>, <pad>], [<sos>, *, *, <eos>, <pad>], ...]
targets = y.tolist()
results = model_predict_one_batch(model, device, x, en_word2index, max_seq_len)
predictions.extend(results)
references.extend([[target[1 : target.index(en_word2index[EOS_TOKEN])]] for target in targets])
accuracy = corpus_bleu(references, predictions)
print(f"准确率:{accuracy:.4f}")
if __name__ == "__main__":
pad_token_index = SPECIAL_TOKENS.index(PAD_TOKEN)
padding_idxs = [pad_token_index] * 2
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 加载词表
file_path = os.path.join(DATA_DRIECTORY, "zh_cn_vocab.txt")
with open(file_path, "r", encoding="utf-8") as f:
zh_cn_vocab_list = [line.strip() for line in f.readlines()]
print("加载中文词表完成")
file_path = os.path.join(DATA_DRIECTORY, "en_vocab.txt")
with open(file_path, "r", encoding="utf-8") as f:
en_vocab_list = [line.strip() for line in f.readlines()]
vocab_sizes = [len(zh_cn_vocab_list), len(en_vocab_list)]
print("加载英文词表完成")
# 构建词表索引
en_word2index = {word: index for index, word in enumerate(en_vocab_list)}
print("构建英文词表索引完成")
file_path = os.path.join(DATA_DRIECTORY, "translation_test.jsonl")
test_dataset = TranslationDataset(file_path) # 创建测试数据集
print("创建测试集成功")
test_data_loader = DataLoader(test_dataset, BATCH_SIZE, collate_fn=collate_fn) # 创建测试数据集加载器
print("创建测试数据集加载器成功")
model = TranslationModel(vocab_sizes, EMBEDDING_DIM, HIDDEN_DIM, NUM_LAYERS, padding_idxs).to(device)
print("创建模型成功")
file_path = os.path.join(MODEL_DRIECTORY, "translation_model_by_seq2seq.pth")
model.load_state_dict(torch.load(file_path)) # 加载模型参数
print("加载模型参数完成")
model_evaluate(model, device, test_data_loader, en_word2index, MAX_SEQ_LEN)
模型预测:
import os
import torch
from nltk.tokenize import TreebankWordDetokenizer
def model_predict(model, device, text, zh_cn_word2index, en_word2index, en_index2word, detokenizer, max_seq_len):
model.to(device) # 指定设备
tokens = list(text) # 切词
zh_cn_indexes = [zh_cn_word2index.get(token, SPECIAL_TOKENS.index(UNK_TOKEN)) for token in tokens] # 获取单词的索引
x = torch.tensor([zh_cn_indexes], dtype=torch.long, device=device) # 输入数据
results = model_predict_one_batch(model, device, x, en_word2index, max_seq_len)
tokens = [en_index2word[index] for index in results[0]]
return detokenizer.detokenize(tokens)
def model_run_predict(model, device, word2index, en_word2index, en_index2word, max_seq_len):
print("欢迎使用中英翻译模型,输入q或quit退出")
detokenizer = TreebankWordDetokenizer()
while True:
text = input("请输入中文: \n")
if text.strip() in ["q", "quit"]:
break
if text.strip() == "":
continue
print(f"输入的中文:")
print(text)
result = model_predict(model, device, text, word2index, en_word2index, en_index2word, detokenizer, max_seq_len)
print(f"翻译结果:")
print(result)
if __name__ == "__main__":
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
file_path = os.path.join(DATA_DRIECTORY, "zh_cn_vocab.txt")
with open(file_path, "r", encoding="utf-8") as f:
zh_cn_vocab_list = [line.strip() for line in f.readlines()]
print("加载中文词表完成")
file_path = os.path.join(DATA_DRIECTORY, "en_vocab.txt")
with open(file_path, "r", encoding="utf-8") as f:
en_vocab_list = [line.strip() for line in f.readlines()]
vocab_sizes = [len(zh_cn_vocab_list), len(en_vocab_list)]
print("加载英文词表完成")
vocab_sizes = [len(zh_cn_vocab_list), len(en_vocab_list)]
# 构建词表到索引的映射
zh_cn_word2index = {word: index for index, word in enumerate(zh_cn_vocab_list)}
print("构建中文词表到索引的映射")
en_word2index = {word: index for index, word in enumerate(en_vocab_list)}
print("构建英文词表到索引的映射")
# 构建索引到词表的映射
en_index2word = {index: word for index, word in enumerate(en_vocab_list)}
print("构建英文词引到词表的映射")
model = TranslationModel(vocab_sizes, EMBEDDING_DIM, HIDDEN_DIM, NUM_LAYERS, padding_idxs).to(device)
print("创建模型完成")
file_path = os.path.join(MODEL_DRIECTORY, "translation_model_by_seq2seq.pth")
model.load_state_dict(torch.load(file_path)) # 加载模型参数
print("加载模型参数完成")
model_run_predict(model, device, zh_cn_word2index, en_word2index, en_index2word, MAX_SEQ_LEN)

浙公网安备 33010602011771号