一、机器翻译的发展历程:
1. 基于规则的机器翻译
利用语言学家编写的语法规则和词典进行翻译。这种方法需要对源语言和目标语言的语法和词汇有深入的理解,但��灵活性和适应性较差,难以处理复杂的语言结构和多义词问题。
2. 基于统计的机器翻译
随着计算机性能的提升和大规模平行语料库的出现,统计机器翻译开始兴起。这种方法通过分析大量双语文本,自动学习源语言和目标语言之间的对应关系,从而实现翻译。统计机器翻译在处理多义词和语言变异方面表现出更好的效果,但由于其依赖于大量训练数据,对于资源匮乏的语言支持不足。
3. 基于神经网络机器翻译
近年来,深度学习技术的快速发展推动了神经网络机器翻译(Neural Machine Translation,简称NMT)的兴起。NMT使用深度神经网络模型,如长短期记忆网络(LSTM)和 Transformer,能够自动学习源语言和目标语言之间的复杂映射关系,无需人工设计特征或规则。NMT在翻译质量、速度和适应性方面取得了显著进步,成为当前机器翻译领域的主流方法。
二、数据划分:
1. 训练集 (Training Set)
作用:训练集用于训练模型,使模型能够学习输入数据与输出结果之间的映射关系。模型会根据训练集中的样本调整其参数,以最小化预测误差。
目标:让模型在训练数据上尽可能地拟合好,学习到数据的内在规律。
2. 开发集/验证集 (Development/Validation Set)
作用:开发集用于在模型训练过程中调整超参数、选择模型架构以及防止过拟合。它作为独立于训练集的数据,用于评估模型在未见过的数据上的表现。
目标:通过在开发集上的性能评估,选择最佳的模型配置,避免模型在训练集上过度拟合,确保模型的泛化能力。
3. 测试集(Test Set)
作用:测试集用于最终评估模型的性能,是在模型训练和调参完全完成后,用来衡量模型实际应用效果的一组数据。它是最接近真实世界数据的评估标准。
目标:提供一个公正、无偏见的性能估计,反映模型在未知数据上的泛化能力。
三、赛题解析:
1. 赛事任务
基于术语词典干预的机器翻译挑战赛 选择以英文为源语言,中文为目标语言的机器翻译。本次大赛除英文到中文的双语数据,还提供英中对照的术语词典。参赛队伍需要基于提供的训练数据样本从多语言机器翻译模型的构建与训练,并基于测试集以及术语词典,提供最终的翻译结果
2. 赛题数据
-
训练集:双语数据 - 中英14万余双语句对
-
开发集:英中1000双语句对
-
测试集:英中1000双语句对
-
术语词典:英中2226条
四、baseline:
1. 安装相关库
diff
!pip install torchtext
2. 导入相关库
javascript
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torchtext.data.utils import get_tokenizer
from collections import Counter
import random
from torch.utils.data import Subset, DataLoader
import time
3. 定义数据集类
python
# 定义数据集类
# 修改TranslationDataset类以处理术语
class TranslationDataset(Dataset):
"""
用于翻译任务的数据集类,专门处理包含特定术语的数据
"""
def __init__(self, filename, terminology):
"""
初始化函数
参数:
filename: 包含翻译对的文件路径,每行格式为 "英文\t中文"
terminology: 术语词典,格式为 {"英文术语": "中文术语"}
"""
self.data = [] # 存储英文和中文句子对
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
en, zh = line.strip().split('\t')
self.data.append((en, zh))
self.terminology = terminology # 存储术语词典
# 创建词汇表,注意这里需要确保术语词典中的词也被包含在词汇表中
self.en_tokenizer = get_tokenizer('basic_english') # 使用预训练的英文分词器
self.zh_tokenizer = list # 中文使用字符级分词
en_vocab = Counter(self.terminology.keys()) # 初始化英文词汇表,确保术语在词汇表中
zh_vocab = Counter() # 初始化中文词汇表
# 统计词频
for en, zh in self.data:
en_vocab.update(self.en_tokenizer(en)) # 统计英文单词的频率
zh_vocab.update(self.zh_tokenizer(zh)) # 统计中文汉字的频率
# 添加特殊标记和高频词到词汇表
self.en_vocab = ['<pad>', '<sos>', '<eos>'] + list(self.terminology.keys()) + [word for word, _ in en_vocab.most_common(10000)]
self.zh_vocab = ['<pad>', '<sos>', '<eos>'] + [word for word, _ in zh_vocab.most_common(10000)]
# 创建词到索引的映射
self.en_word2idx = {word: idx for idx, word in enumerate(self.en_vocab)}
self.zh_word2idx = {word: idx for idx, word in enumerate(self.zh_vocab)}
def __len__(self):
"""
返回数据集的大小
"""
return len(self.data)
def __getitem__(self, idx):
"""
获取指定索引的数据
参数:
idx: 数据索引
返回:
en_tensor: 英文句子张量
zh_tensor: 中文句子张量
"""
en, zh = self.data[idx] # 获取指定索引的英文和中文句子对
en_tensor = torch.tensor([self.en_word2idx.get(word, self.en_word2idx['<sos>']) for word in self.en_tokenizer(en)] + [self.en_word2idx['<eos>']]) # 将英文句子转换为索引序列,并添加开始和结束标记
zh_tensor = torch.tensor([self.zh_word2idx.get(word, self.zh_word2idx['<sos>']) for word in self.zh_tokenizer(zh)] + [self.zh_word2idx['<eos>']]) # 将中文句子转换为索引序列,并添加开始和结束标记
return en_tensor, zh_tensor # 返回英文和中文句子张量
def collate_fn(batch):
"""
将多个样本组成一个批次数据
参数:
batch: 包含多个样本的列表,每个样本是__getitem__方法返回的元组
返回:
en_batch: 填充后的英文句子批次张量
zh_batch: 填充后的中文句子批次张量
"""
en_batch, zh_batch = [], [] # 初始化英文和中文句子批次列表
for en_item, zh_item in batch: # 遍历每个样本
en_batch.append(en_item) # 将英文句子添加到英文句子批次列表中
zh_batch.append(zh_item) # 将中文句子添加到中文句子批次列表中
# 对英文和中文序列分别进行填充
en_batch = nn.utils.rnn.pad_sequence(en_batch, padding_value=0, batch_first=True) # 对英文句子批次进行填充,使其长度一致
zh_batch = nn.utils.rnn.pad_sequence(zh_batch, padding_value=0, batch_first=True) # 对中文句子批次进行填充,使其长度一致
return en_batch, zh_batch # 返回填充后的英文和中文句子批次张量
4. 定义编码器、解码器和模型
python
import torch
import torch.nn as nn
import random
class Encoder(nn.Module):
"""编码器模块
"""
def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
"""
参数:
input_dim: 输入词汇表大小
emb_dim: 词嵌入维度
hid_dim: 隐藏状态维度
n_layers: GRU层数
dropout: dropout概率
"""
super().__init__()
self.embedding = nn.Embedding(input_dim, emb_dim) # 词嵌入层
self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True) # GRU层
self.dropout = nn.Dropout(dropout) # dropout层
def forward(self, src):
# src shape: [batch_size, src_len]
embedded = self.dropout(self.embedding(src))
# embedded shape: [batch_size, src_len, emb_dim]
outputs, hidden = self.rnn(embedded) # 调用GRU进行前向计算
# outputs shape: [batch_size, src_len, hid_dim] 所有时刻的隐藏状态
# hidden shape: [n_layers, batch_size, hid_dim] 最后一个时刻的隐藏状态
return outputs, hidden
class Decoder(nn.Module):
"""解码器模块
"""
def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout):
"""
参数:
output_dim: 输出词汇表大小
emb_dim: 词嵌入维度
hid_dim: 隐藏状态维度
n_layers: GRU层数
dropout: dropout概率
"""
super().__init__()
self.output_dim = output_dim
self.embedding = nn.Embedding(output_dim, emb_dim) # 词嵌入层
self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True) # GRU层
self.fc_out = nn.Linear(hid_dim, output_dim) # 线性层,用于输出预测
self.dropout = nn.Dropout(dropout) # dropout层
def forward(self, input, hidden):
# input shape: [batch_size, 1] 当前时刻的输入
# hidden shape: [n_layers, batch_size, hid_dim] 编码器最后一个时刻的隐藏状态
embedded = self.dropout(self.embedding(input))
# embedded shape: [batch_size, 1, emb_dim]
output, hidden = self.rnn(embedded, hidden) # 调用GRU进行前向计算
# output shape: [batch_size, 1, hid_dim] 当前时刻的隐藏状态
# hidden shape: [n_layers, batch_size, hid_dim]
prediction = self.fc_out(output.squeeze(1)) # 将GRU的输出转换为预测结果
# prediction shape: [batch_size, output_dim]
return prediction, hidden
class Seq2Seq(nn.Module):
"""seq2seq模型
"""
def __init__(self, encoder, decoder, device):
"""
参数:
encoder: 编码器实例
decoder: 解码器实例
device: 设备
"""
super().__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device
def forward(self, src, trg, teacher_forcing_ratio=0.5):
# src shape: [batch_size, src_len]
# trg shape: [batch_size, trg_len]
batch_size = src.shape[0]
trg_len = trg.shape[1]
trg_vocab_size = self.decoder.output_dim # 目标语言词汇表大小
outputs = torch.zeros(batch_size, trg_len, trg_vocab_size).to(self.device) # 初始化输出张量
_, hidden = self.encoder(src) # 调用编码器,获取最后一个时刻的隐藏状态
input = trg[:, 0].unsqueeze(1) # 获取目标语言的第一个词作为解码器的初始输入
for t in range(1, trg_len): # 遍历目标语言的每一个词
output, hidden = self.decoder(input, hidden) # 调用解码器进行预测
outputs[:, t, :] = output # 保存当前时刻的预测结果
teacher_force = random.random() < teacher_forcing_ratio # 决定是否使用teacher forcing
top1 = output.argmax(1) # 获取预测概率最高的词的索引
input = trg[:, t].unsqueeze(1) if teacher_force else top1.unsqueeze(1) # 如果使用teacher forcing,则将目标语言的下一个词作为输入,否则使用预测的词作为输入
return outputs # 返回所有时刻的预测结果
python
# 新增术语词典加载部分
def load_terminology_dictionary(dict_file):
"""
加载术语词典文件
参数:
dict_file: 术语词典文件路径,每行格式为 "英文术语\t中文术语"
返回:
terminology: 术语词典,格式为 {"英文术语": "中文术语"}
"""
terminology = {} # 初始化术语词典
with open(dict_file, 'r', encoding='utf-8') as f: # 打开术语词典文件
for line in f: # 遍历每一行
en_term, ch_term = line.strip().split('\t') # 分割英文术语和中文术语
terminology[en_term] = ch_term # 将英文术语作为键,中文术语作为值添加到词典中
return terminology # 返回术语词典
5. 模型的训练
python
def train(model, iterator, optimizer, criterion, clip):
"""
训练模型一个epoch
参数:
model: 模型实例
iterator: 数据迭代器
optimizer: 优化器
criterion: 损失函数
clip: 梯度裁剪阈值
返回:
epoch_loss / len(iterator): 平均损失值
"""
model.train() # 设置模型为训练模式
epoch_loss = 0 # 初始化epoch损失值
for i, (src, trg) in enumerate(iterator): # 遍历数据迭代器
# 将数据移动到设备上
src, trg = src.to(device), trg.to(device)
optimizer.zero_grad() # 清空梯度
output = model(src, trg) # 前向传播,获取模型输出
# 处理输出和目标张量,以便计算损失
output_dim = output.shape[-1]
output = output[:, 1:].contiguous().view(-1, output_dim) # 去掉开头<sos>标记,并将张量reshape为(batch_size * seq_len, output_dim)
trg = trg[:, 1:].contiguous().view(-1) # 去掉开头<sos>标记,并将张量reshape为(batch_size * seq_len)
loss = criterion(output, trg) # 计算损失
loss.backward() # 反向传播
torch.nn.utils.clip_grad_norm_(model.parameters(), clip) # 梯度裁剪
optimizer.step() # 更新模型参数
epoch_loss += loss.item() # 累加损失值
return epoch_loss / len(iterator) # 返回平均损失值
ini
# 主函数
If __name__ == '__main__':
Start_time = time.Time () # 开始计时
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
#terminology = load_terminology_dictionary('../dataset/en-zh.dic')
terminology = load_terminology_dictionary('../dataset/en-zh.dic')
# 加载数据
dataset = TranslationDataset('../dataset/train.txt',terminology = terminology)
# 选择数据集的前N个样本进行训练
N = 2000 #int(len(dataset) * 1) # 或者可以设置为数据集大小的一定比例,如 int(len(dataset) * 0.1),在这里修改N的数值可以修改使用的数据集的多少,扩充使用的数据集的大小可以提高训练的性能
subset_indices = list(range(N))
subset_dataset = Subset(dataset, subset_indices)
train_loader = DataLoader(subset_dataset, batch_size=32, shuffle=True, collate_fn=collate_fn)
# 定义模型参数
INPUT_DIM = len(dataset.en_vocab)
OUTPUT_DIM = len(dataset.zh_vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
HID_DIM = 512
N_LAYERS = 2
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5
# 初始化模型
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT)
model = Seq2Seq(enc, dec, device).to(device)
# 定义优化器和损失函数
optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss(ignore_index=dataset.zh_word2idx['<pad>'])
# 训练模型
N_EPOCHS = 50 #增加训练的轮数,提高训练效果
CLIP = 1
for epoch in range(N_EPOCHS):
train_loss = train(model, train_loader, optimizer, criterion, CLIP)
print(f'Epoch: {epoch+1:02} | Train Loss: {train_loss:.3f}')
# 在训练循环结束后保存模型
torch.save(model.state_dict(), './translation_model_GRU.pth')
end_time = time.time() # 结束计时
# 计算并打印运行时间
elapsed_time_minute = (end_time - start_time)/60
print(f"Total running time: {elapsed_time_minute:.2f} minutes")
6. 在开发集上进行模型评价
javascript
import torch
from sacrebleu. Metrics import BLEU
from typing import List
python
# 假设我们已经定义了TranslationDataset, Encoder, Decoder, Seq2Seq类
def load_sentences(file_path: str) -> List[str]:
"""
从文件中加载句子
参数:
file_path: 文件路径
返回:
List[str]: 句子列表
"""
with open(file_path, 'r', encoding='utf-8') as f:
return [line.strip() for line in f]
# 更新translate_sentence函数以考虑术语词典
def translate_sentence(sentence: str, model: Seq2Seq, dataset: TranslationDataset, terminology, device: torch.device, max_length: int = 50):
"""
翻译一个句子,并考虑术语词典
参数:
sentence: 待翻译的句子
model: 训练好的seq2seq模型
dataset: 数据集实例
terminology: 术语词典,格式为 {"英文术语": "中文术语"}
device: 设备
max_length: 最大翻译长度
返回:
str: 翻译后的句子
"""
model.eval() # 设置模型为评估模式
# 将句子转换为索引序列
tokens = dataset.en_tokenizer(sentence)
tensor = torch.LongTensor([dataset.en_word2idx.get(token, dataset.en_word2idx['<sos>']) for token in tokens]).unsqueeze(0).to(device) # [1, seq_len]
with torch.no_grad(): # 禁用梯度计算
# 使用编码器获取隐藏状态
_, hidden = model.encoder(tensor)
translated_tokens = [] # 初始化翻译后的词列表
input_token = torch.LongTensor([[dataset.zh_word2idx['<sos>']]]).to(device) # 初始化解码器的输入为<sos>标记
for _ in range(max_length): # 最多翻译max_length个词
# 使用解码器进行预测
output, hidden = model.decoder(input_token, hidden)
top_token = output.argmax(1) # 获取预测概率最高的词的索引
translated_token = dataset.zh_vocab[top_token.item()] # 将索引转换为词
if translated_token == '<eos>': # 如果遇到结束标记,则停止翻译
break
# 如果翻译的词在术语词典中,则使用术语词典中的词
if translated_token in terminology.values():
for en_term, ch_term in terminology.items():
if translated_token == ch_term: # 找到对应的英文术语
translated_token = en_term # 使用英文术语替换中文翻译
break
translated_tokens.append(translated_token) # 将翻译后的词添加到列表中
input_token = top_token.unsqueeze(1) # 将预测的词作为下一个时刻的输入
return ''.join(translated_tokens) # 返回翻译后的句子
python
def evaluate_bleu(model: Seq2Seq, dataset: TranslationDataset, src_file: str, ref_file: str, terminology, device: torch.device):
"""
使用BLEU指标评估模型的翻译质量
参数:
model: 训练好的seq2seq模型
dataset: 数据集实例
src_file: 源语言文件路径
ref_file: 参考译文文件路径
terminology: 术语词典,格式为 {"英文术语": "中文术语"}
device: 设备
返回:
score: BLEU分数
"""
model.eval() # 设置模型为评估模式
src_sentences = load_sentences(src_file) # 加载源语言句子
ref_sentences = load_sentences(ref_file) # 加载参考译文句子
translated_sentences = [] # 初始化翻译后的句子列表
for src in src_sentences: # 遍历源语言句子
translated = translate_sentence(src, model, dataset, terminology, device) # 翻译句子
translated_sentences.append(translated) # 将翻译后的句子添加到列表中
bleu = BLEU() # 初始化BLEU计算器
score = bleu.corpus_score(translated_sentences, [ref_sentences]) # 计算BLEU分数
return score # 返回BLEU分数
ini
# 主函数
if __name__ == '__main__':
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 选择设备
# 加载术语词典
terminology = load_terminology_dictionary('../dataset/en-zh.dic') # 从文件中加载术语词典
# 创建数据集实例时传递术语词典
dataset = TranslationDataset('../dataset/train.txt', terminology) # 创建训练数据集实例
# 定义模型参数
INPUT_DIM = len(dataset.en_vocab) # 输入词汇表大小
OUTPUT_DIM = len(dataset.zh_vocab) # 输出词汇表大小
ENC_EMB_DIM = 256 # 编码器词嵌入维度
DEC_EMB_DIM = 256 # 解码器词嵌入维度
HID_DIM = 512 # 隐藏状态维度
N_LAYERS = 2 # GRU层数
ENC_DROPOUT = 0.5 # 编码器dropout概率
DEC_DROPOUT = 0.5 # 解码器dropout概率
# 初始化模型
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT) # 初始化编码器
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT) # 初始化解码器
model = Seq2Seq(enc, dec, device).to(device) # 初始化seq2seq模型,并移动到设备上
# 加载训练好的模型
model.load_state_dict(torch.load('./translation_model_GRU.pth')) # 加载模型参数
# 评估BLEU分数
bleu_score = evaluate_bleu(model, dataset, '../dataset/dev_en.txt', '../dataset/dev_zh.txt', terminology = terminology, device = device) # 计算BLEU分数
print(f'BLEU-4 score: {bleu_score.score:.2f}') # 打印BLEU分数
7. 在测试集上进行推理
python
def inference(model: Seq2Seq, dataset: TranslationDataset, src_file: str, save_dir:str, terminology, device: torch.device):
"""
使用训练好的模型对源语言文件进行翻译,并将翻译结果保存到文件
参数:
model: 训练好的seq2seq模型
dataset: 数据集实例
src_file: 源语言文件路径
save_dir: 翻译结果保存路径
terminology: 术语词典,格式为 {"英文术语": "中文术语"}
device: 设备
"""
model.eval() # 设置模型为评估模式
src_sentences = load_sentences(src_file) # 加载源语言句子
translated_sentences = [] # 初始化翻译后的句子列表
for src in src_sentences: # 遍历源语言句子
translated = translate_sentence(src, model, dataset, terminology, device) # 翻译句子
#print(translated)
translated_sentences.append(translated) # 将翻译后的句子添加到列表中
#print(translated_sentences)
# 将列表元素连接成一个字符串,每个元素后换行
text = '\n'.join(translated_sentences)
# 打开一个文件,如果不存在则创建,'w'表示写模式
with open(save_dir, 'w', encoding='utf-8') as f:
# 将字符串写入文件
f.write(text)
#return translated_sentences
ini
# 主函数
if __name__ == '__main__':
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 选择设备
# 加载术语词典
terminology = load_terminology_dictionary('../dataset/en-zh.dic') # 从文件中加载术语词典
# 加载数据集和模型
dataset = TranslationDataset('../dataset/train.txt', terminology=terminology) # 创建训练数据集实例
# 定义模型参数
INPUT_DIM = len(dataset.en_vocab) # 输入词汇表大小
OUTPUT_DIM = len(dataset.zh_vocab) # 输出词汇表大小
ENC_EMB_DIM = 256 # 编码器词嵌入维度
DEC_EMB_DIM = 256 # 解码器词嵌入维度
HID_DIM = 512 # 隐藏状态维度
N_LAYERS = 2 # GRU层数
ENC_DROPOUT = 0.5 # 编码器dropout概率
DEC_DROPOUT = 0.5 # 解码器dropout概率
# 初始化模型
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT) # 初始化编码器
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT) # 初始化解码器
model = Seq2Seq(enc, dec, device).to(device) # 初始化seq2seq模型,并移动到设备上
# 加载训练好的模型
model.load_state_dict(torch.load('./translation_model_GRU.pth')) # 加载模型参数
# 指定保存路径并进行翻译
save_dir = '../dataset/submit.txt' # 翻译结果保存路径
inference(model, dataset, src_file="../dataset/test_en.txt", save_dir=save_dir, terminology=terminology, device=device) # 进行翻译并将结果保存到文件
print(f"翻译完成!文件已保存到{save_dir}")
8. baseline 的优化
对于 baseline,在模型的训练的时候,使用的数据仅为整体训练集的一小部分,同时训练的轮次也相对比较少,这导致模型拟合效果较差,因此,在进行优化的时候,可以改变使用的数据的大小 N 以及训练的次数 N_EPOCHS。