基于Transformer架构实现机器翻译
代码另见:https://github.com/xiaozhou-alt/Transformer_Machine-Translation
文章目录
一、项目介绍
本项目采用深度学习方法,具体使用 Transformer 架构实现英文到中文的机器翻译任务。Transformer 是一种基于自注意力机制的序列到序列(Seq2Seq)模型,专为高效处理长距离依赖和并行计算设计。其核心思想是通过多头注意力替代传统循环或卷积结构,实现全局上下文建模。主要由编码器(Encoder)和解码器(Decoder)组成,采用完全基于注意力机制的结构,摒弃了传统的循环神经网络。
对于 Transformer 架构感兴趣的小伙伴可以看这篇文章: Transformer模型详解,此处不再赘述
二、数据集介绍
本次使用的 AI Challenger 2017 英中机器翻译数据集是当前公开可用的最大规模、高质量的英中平行语料库之一,专为机器翻译任务设计。该数据集由创新工场旗下AI Challenger平台发布。其中,训练集合为12,904,955对,验证集合8000对,测试集A 8000条,测试集B 8000条。
数据集下载地址:https://challenger.ai/datasets/translation
此数据集中的数据来源有:专业翻译的书籍文献、双语新闻稿件、影视字幕文本、技术文档翻译、经过人工审核的公开双语资源。所有中文翻译均由专业翻译人员完成或经过严格审核,确保英中句子严格对齐,无错位现象。数据集格式如下图所示。
数据集优点:领域广泛,覆盖日常生活(32%)、新闻时事(28%)、科学技术(22%)、文学艺术(18%)等多个领域;句式丰富,包含简单句、疑问句、感叹句等多种句式;词汇量大,英文词汇量约50万,中文词汇量约35万;内容多样,英文部分包含口语化和正式书面语,中文部分采用标准现代汉语,符合中文表达习惯;大多数句子长度为5-30个单词/汉字,适合神经网络处理。
三、项目实现
1.项目文件夹结构
Transformer_Machine-Translation
├── test/ # 单元测试目录
│ ├── test_bleu.py # BLEU指标测试脚本
│ └── test_lr.py # 学习率测试脚本
├── transformer/ # Transformer模型核心实现
│ ├── __pycache__/ # Python编译缓存目录
│ │ └── __init__.py # 包初始化文件(编译版本)
│ ├── attention.py # 自注意力机制实现
│ ├── decoder.py # 解码器模块
│ ├── encoder.py # 编码器模块
│ ├── loss.py # 损失函数计算
│ ├── module.py # 基础模型组件
│ ├── optimizer.py # 优化器实现
│ ├── transformer.py # 完整模型架构
│ └── utils.py # 工具函数(模型相关)
├── bleu_score.py # BLEU分数计算
├── config.py # 项目配置文件(超参数/路径等)
├── convert_valid.py # 数据格式转换工具
├── data_gen.py # 数据集生成脚本
├── demo.py # 模型演示入口
├── export.py # 模型导出
├── extract.py # 特征提取工具
├── pre_process.py # 数据预处理
├── README.md
├── train.py # 主训练脚本
└── utils.py # 全局工具函数
2.数据预处理
1) 验证集数据集处理:将原始SGM格式的验证集文件转换为纯文本格式(convert_valid.py)
首先处理XML/SGM格式中的特殊字符,将原始文件中的 “&” 替换为 “XML实体&” ,防止XML解析错误。使用Python内置的 xml.etree.ElementTree 解析器处理 SGM 文件,定位所有标签然后提取每个标签内的文本内容,去除两端空白字符并添加换行,将提取的句子按行写入新文件从结构化标记语言中提取纯净的文本内容:
def convert(old, new):
print('old: ' + old)
print('new: ' + new)
with open(old, 'r', encoding='utf-8') as f:
data = f.readlines()
data = [line.replace(' & ', ' & ') for line in data]
with open(new, 'w', encoding='utf-8') as f:
f.writelines(data)
root = xml.etree.ElementTree.parse(new).getroot()
data = [elem.text.strip() + '\n' for elem in root.iter() if elem.tag == 'seg']
with open(new, 'w', encoding='utf-8') as file:
file.writelines(data)
2) 提取训练和验证样本:将原始文本转换为模型可处理格式(pre_process.py)
英文处理转换为小写,使用 NLTK 进行分词,应用 normalizeString 规范化处理,然后统计词频;中文使用 jieba 分词然后统计词频。保留最高频的 vocab_size-4 个词生成词表:
def process(file, lang='zh'):
print('processing {}...'.format(file))
with open(file, 'r', encoding='utf-8') as f:
data = f.readlines()
word_freq = Counter()
lengths = []
for line in tqdm(data):
sentence = line.strip()
if lang == 'en':
sentence_en = sentence.lower()
tokens = [normalizeString(s) for s in nltk.word_tokenize(sentence_en)]
word_freq.update(list(tokens))
vocab_size = n_src_vocab
else:
seg_list = jieba.cut(sentence.strip())
tokens = list(seg_list)
word_freq.update(list(tokens))
vocab_size = n_tgt_vocab
lengths.append(len(tokens))
words = word_freq.most_common(vocab_size - 4)
word_map = {k[0]: v + 4 for v, k in enumerate(words)}
word_map['<pad>'] = 0
word_map['<sos>'] = 1
word_map['<eos>'] = 2
word_map['<unk>'] = 3
print(len(word_map))
print(words[:100])
# n, bins, patches = plt.hist(lengths, 50, density=True, facecolor='g', alpha=0.75)
#
# plt.xlabel('Lengths')
# plt.ylabel('Probability')
# plt.title('Histogram of Lengths')
# plt.grid(True)
# plt.show()
word2idx = word_map
idx2char = {v: k for k, v in word2idx.items()}
return word2idx, idx2char
英文句子进行完整的小写转换、分词和规范化,转换为词ID序列;中文句子进行分词处理,添加开始(sos_id)和结束(eos_id)标记,转换为词ID序列。然后过滤过长的句子或包含未知词的句子。以此完成数据编码:
def get_data(in_file, out_file):
print('getting data {}->{}...'.format(in_file, out_file))
with open(in_file, 'r', encoding='utf-8') as file:
in_lines = file.readlines()
with open(out_file, 'r', encoding='utf-8') as file:
out_lines = file.readlines()
samples = []
for i in tqdm(range(len(in_lines))):
sentence_en = in_lines[i].strip().lower()
tokens = [normalizeString(s.strip()) for s in nltk.word_tokenize(sentence_en)]
in_data = encode_text(src_char2idx, tokens)
sentence_zh = out_lines[i].strip()
tokens = jieba.cut(sentence_zh.strip())
out_data = [sos_id] + encode_text(tgt_char2idx, tokens) + [eos_id]
if len(in_data) < maxlen_in and len(out_data) < maxlen_out and unk_id not in in_data and unk_id not in out_data:
samples.append({'in': in_data, 'out': out_data})
return samples
3) 数据集的构建和批量处理功能:
首先从上一步创建完成的 pkl 文件加载预处理好的数据和词表文件,并且记录加载时间用于性能监控:
logger.info('loading samples...')
start = time.time()
with open(data_file, 'rb') as file:
data = pickle.load(file)
elapsed = time.time() - start
logger.info('elapsed: {:.4f}'.format(elapsed))
然后将预处理后的数据封装为 PyTorch Dataset,自动将列表转换为 numpy 数组(int64类型),兼容PyTorch 张量。然后返回数字序列形式的源语言(英文)和目标语言(中文)句子对:
class AiChallenger2017Dataset(Dataset):
def __init__(self, split):
self.samples = data[split]
def __getitem__(self, i):
sample = self.samples[i]
src_text = sample['in']
tgt_text = sample['out']
return np.array(src_text, dtype=np.int64), np.array(tgt_text, np.int64)
def __len__(self):
return len(self.samples)
def main():
from utils import sequence_to_text
valid_dataset = AiChallenger2017Dataset('valid')
print(valid_dataset[0])
with open(vocab_file, 'rb') as file:
data = pickle.load(file)
src_idx2char = data['dict']['src_idx2char']
tgt_idx2char = data['dict']['tgt_idx2char']
src_text, tgt_text = valid_dataset[0]
src_text = sequence_to_text(src_text, src_idx2char)
src_text = ' '.join(src_text)
print('src_text: ' + src_text)
tgt_text = sequence_to_text(tgt_text, tgt_idx2char)
tgt_text = ''.join(tgt_text)
print('tgt_text: ' + tgt_text)
计算 batch 内最长序列,动态填充源语言序列和目标语言序列。源语言用 pad_id 填充,目标语言用 IGNORE_ID 填充。按源语言长度从长到短排序,提高 RNN 计算效率,减少计算浪费。返回处理后的 batch 数据包含:填充后的源语言序列、填充后的目标语言序列和原始源语言长度。从而完成从预处理数据到模型训练就绪格式的最后转换:
def pad_collate(batch):
max_input_len = float('-inf')
max_target_len = float('-inf')
for elem in batch:
src, tgt = elem
max_input_len = max_input_len if max_input_len > len(src) else len(src)
max_target_len = max_target_len if max_target_len > len(tgt) else len(tgt)
for i, elem in enumerate(batch):
src, tgt = elem
input_length = len(src)
padded_input = np.pad(src, (0, max_input_len - len(src)), 'constant', constant_values=pad_id)
padded_target = np.pad(tgt, (0, max_target_len - len(tgt)), 'constant', constant_values=IGNORE_ID)
batch[i] = (padded_input, padded_target, input_length)
# sort it by input lengths (long to short)
batch.sort(key=lambda x: x[2], reverse=True)
return default_collate(batch)
3.特征提取
此处仅进行特征提取原理的讲解,具体代码实现在Transformer架构定义中,感兴趣的同学可以自行研究
对于编码器而言,其通过自注意力机制动态捕捉序列中任意位置的关联,实现全局依赖建模。例如在机器翻译中,当输入句子 “a dog ate the food because it was hungry ” 时,编码器会通过计算每个词与其他词的关联权重,明确代词 “it ” 与 “dog ” 的高相关性(通过注意力分数),从而消除歧义。具体流程如下:
(1)输入嵌入与位置编码:词嵌入将单词转化为向量(如 “dog ” →[0.2, 0.7, …]),并叠加位置编码(如正弦函数生成的唯一位置标识),使模型感知序列顺序。例如输入 “I love Nature Language Process ” 会生成 5 个 512 维的向量,分别对应五个单词的位置信息。
(2)多头自注意力计算:每个头关注不同语义维度。例如在文本分类任务中,一个头可能捕捉语法结构(如主语-谓语关系),另一个头关注情感关键词(如 “excellent ” 与 “positive ” 的强关联)
(3)残差连接与层归一化:通过跳跃连接保留原始输入信息,避免梯度消失。例如在图像分类任务中,编码器通过残差传递底层纹理特征,同时通过自注意力提取高层语义
对于解码器而言,基于编码器输出的上下文信息,通过掩码自注意力机制逐步生成目标序列:掩码确保仅关注已生成部分(防止信息泄漏),并结合编码器-解码器注意力模块对齐源序列特征(如翻译任务中的语义映射)。其结构包含多层堆叠的注意力层和前馈网络,利用自回归机制逐词预测输出,并通过任务适配模块动态调整特征空间(如分类或生成任务)。解码器的核心优势在于融合局部生成与全局上下文,实现精准的序列重建(如文本生成或时间序列预测)
项目中使用的特征提取方式最大的优势在于:自注意力机制直接关联任意位置,高效捕捉长距离依赖,并且非序列化处理将大幅提升训练速度,适合大规模数据,以此提高语言类模型的输出效果,尤其是在语法方面的效果;而劣势在于机器翻译本身任务所需的语料库和数据集需求极大,小数据集场景下易过拟合,序列的长度计算涉及序列长度的平方,因此训练得到的模型在短语句上的效果可能会远远优于长语句。
4.模型选择与训练
1) 模型选择
编码器架构:6层编码器堆叠(由n_layers_enc=6参数控制),每层包含两个核心子层:多头自注意力机制(Multi-Head Self-Attention)和前馈神经网络(Position-wise Feed-Forward Network)。每个子层采用残差连接(Residual Connection)和层归一化(Layer Normalization)。
解码器架构:6层解码器堆叠(由n_layers_dec=6参数控制),每层包含三个核心子层:掩码多头自注意力机制(Masked Multi-Head Self-Attention)、编码器-解码器注意力机制和前馈神经网络,同样使用残差连接和层归一化。
多头自注意力(Multi-Head Attention)有以下三个特点:并行计算:将输入拆分为8个头,独立学习不同子空间特征;参数初始化:使用特定方差的正态分布初始化权重,保证训练稳定性;残差连接:输出与输入相加后通过LayerNorm。
2) 模型训练
(1)前向传播
输入序列(padded_input)经编码器提取全局特征,解码器结合目标序(padded_target)生成预测结果(pred),自注意力机制动态计算词间依赖关系,位置编(PositionalEncoding)保留序列顺序信息:
for i, (data) in enumerate(train_loader):
# Move to GPU, if available
padded_input, padded_target, input_lengths = data
padded_input = padded_input.to(device)
padded_target = padded_target.to(device)
input_lengths = input_lengths.to(device)
# Forward prop.
pred, gold = model(padded_input, input_lengths, padded_target)
loss, n_correct = cal_performance(pred, gold, smoothing=args.label_smoothing)
(2)反向传播与优化:
梯度裁剪(clip_gradient)限制梯度范数(grad_clip),防止梯度爆炸。优化器更新参数后,学习率根据步数动态衰减,平衡训练稳定性与收敛速度。
optimizer.zero_grad()
loss.backward()
clip_gradient(optimizer.optimizer, grad_clip)
optimizer.step()
elapsed = time.time() - start
start = time.time()
losses.update(loss.item())
times.update(elapsed)
机器翻译任务所需算力较高,且项目数据集较大,故使用魔搭社区ModelScope提供的GPU云平台环境( ≈ NVIDIA Geforce 3090)训练5个epoch,以下为训练过程输出:
损失值记录:损失值在训练5轮之后,整体呈下降趋势,但下降速度较慢,预计需训练50 epoch 左右能够达到较好的效果。
运行 export.py 将模型转化为 transformer.pt 模型文件,并运行 demo.py 随机选取的10个验证集数据,将词汇索引转换为中文字符,结果如下图所示:
不难看出,翻译效果一坨,大致结果还是不错的。
机器翻译本身是一个较为困难的任务,最大的难点我认为在于两点:
(1)翻译本身的困难:不同语言的语法习惯和句式组成往往差异较大,更不用说俚语和成语。
(2)算力的困难:语言作为心灵的窗户,囊括万千,这就导致想要较好的效果数据集的大小一定不会低于千万数量级,参数量与数据集较大将进一步增加训练时间,模型的训练成本非常高昂。
针对这两个问题,首先先看第二个问题,我决定即使增加训练时间也要采用较大数据集,最终验证梯度的确在缓慢下降,困于算力问题,只能进行极少的训练轮数。而对于第一个问题,在进行5轮训练之后,我们观察到对于重复出现次数较多的单词组合,如 “the man ”、“each day ”等模型已经能够有所区分,按照中文正常语序进行输出,相信随着训练的深入,语法习惯的问题将会得到较大的改善。
BLEU分数计算:
计算模型在验证集上的 BLEU 分数,衡量生成文本与参考文本的 n-gram 重叠度,用于客观评估模型性能。BLEU 分数公式如下:
其中,长度惩罚(BP)计算方法为:若生成文本长度(c)< 参考文本最短长度(r),则;否则 BP = 1。
评估过程中使用1-4-gram的默认等权重,即(w1=w2=w3=w4=0.25)。
得到评估指标如下:
由表可见,BLEU 分数(0.0为完全不匹配,1.0为完美匹配)均值仅为0.305,显然在不使用预训练模型且训练数据量有限,训练资源有限的情况下,模型的输出效果并不是非常理想,仅仅能做到词对词的翻译,很难考虑到联系上下文和整个句子的语法。
如果你喜欢我的文章,不妨给小周一个免费的点赞和关注吧!