Docshttps://datawhaler.feishu.cn/wiki/OgQWwkYkviPfpwkE1ZmcXwcWnAh
一、Transformer
基于循环或卷积神经网络的序列到序列建模方法是现存机器翻译任务中的经典方法。然而,它们在建模文本长程依赖方面都存在一定的局限性。
-
对于卷积神经网络来说,受限的上下文窗口在建模长文本方面天然地存在不足。如果要对长距离依赖进行描述,需要多层卷积操作,而且不同层之间信息传递也可能有损失,这些都限制了模型的能力。
-
而对于循环神经网络来说,上下文的语义依赖是通过维护循环单元中的隐状态实现的。在编码过程中,每一个时间步的输入建模都涉及到对隐藏状态的修改。随着序列长度的增加,编码在隐藏状态中的序列早期的上下文信息被逐渐遗忘。尽管注意力机制的引入在一定程度上缓解了这个问题,但循环网络在编码效率方面仍存在很大的不足之处。由于编码端和解码端的每一个时间步的隐藏状态都依赖于前一时间步的计算结果,这就造成了在训练和推断阶段的低效。
也可以直接看李沐老师的B站讲解
Transformer论文逐段精读【论文精读】_哔哩哔哩_bilibili
Transformer的主要组件包括编码器(Encoder)、解码器(Decoder)和注意力层。其核心是利用多头自注意力机制(Multi-Head Self-Attention),使每个位置的表示不仅依赖于当前位置,还能够直接获取其他位置的表示。自从提出以来,Transformer模型在机器翻译、文本生成等自然语言处理任务中均取得了突破性进展,成为NLP领域新的主流模型。
从宏观角度来看,Transformer的编码器是由多个相同的层叠加而成的,每个层都有两个子层(子层表示为sublayer)。第⼀个子层是多头自注意力(multi-head self-attention)汇聚;第二个子层是基于位置的前馈网络(positionwise feed-forward network)。
二、代码注释
# 导入必要的库
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.nn.utils import clip_grad_norm_
from torchtext.data.metrics import bleu_score
from torch.utils.data import Dataset, DataLoader
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
from typing import List, Tuple
import jieba
import random
from torch.nn.utils.rnn import pad_sequence
import sacrebleu
import time
import math
# 定义tokenizer
en_tokenizer = get_tokenizer('spacy', language='en_core_web_trf')
zh_tokenizer = lambda x: list(jieba.cut(x)) # 使用jieba分词
# 读取数据函数
def read_data(file_path: str) -> List[str]:
"""从指定路径读取数据,每行一个样本,去除行尾空白字符"""
with open(file_path, 'r', encoding='utf-8') as f:
return [line.strip() for line in f]
# 数据预处理函数
def preprocess_data(en_data: List[str], zh_data: List[str]) -> List[Tuple[List[str], List[str]]]:
"""对英文和中文数据进行分词,并截取到最大长度"""
# ... (省略具体实现,与原始代码相同)
# 构建词汇表
def build_vocab(data: List[Tuple[List[str], List[str]]]):
"""从预处理后的数据中构建英文和中文的词汇表"""
# ... (省略具体实现,与原始代码相同)
# TranslationDataset类
class TranslationDataset(Dataset):
"""用于封装翻译任务的Dataset"""
# ... (省略构造函数、__len__和__getitem__方法,与原始代码相同)
# collate_fn函数
def collate_fn(batch):
"""用于DataLoader中自动批处理数据的函数,包括填充序列和转换为tensor"""
# ... (省略具体实现,与原始代码相同)
# 数据加载函数
def load_data(train_path, dev_en_path, dev_zh_path, test_en_path):
"""加载训练、验证和测试数据,进行预处理,并创建数据集和数据加载器"""
# ... (省略具体实现,注意这里假设了MAX_LENGTH和BATCH_SIZE等全局变量已定义)
# PositionalEncoding类
class PositionalEncoding(nn.Module):
"""为Transformer模型添加位置编码的模块"""
# ... (省略构造函数和forward方法,与原始代码相同)
# TransformerModel类
class TransformerModel(nn.Module):
"""定义Transformer模型的类"""
# ... (省略构造函数和forward方法,与原始代码相同)
# 初始化模型函数
def initialize_model(src_vocab, tgt_vocab, **kwargs):
"""根据给定的词汇表和参数初始化Transformer模型"""
# ... (省略具体实现,与原始代码相同)
# 初始化优化器函数
def initialize_optimizer(model, learning_rate=0.001):
"""为模型初始化优化器,默认使用Adam优化器"""
return optim.Adam(model.parameters(), lr=learning_rate)
# 计算epoch时间的函数
def epoch_time(start_time, end_time):
"""计算并返回训练或验证一个epoch所需的时间"""
# ... (省略具体实现,与原始代码相同)
# 训练函数
def train(model, iterator, optimizer, criterion, clip):
"""训练模型一个epoch,包括前向传播、反向传播和参数更新"""
# ... (省略具体实现,注意这里使用了DEVICE全局变量)
# 评估函数
def evaluate(model, iterator, criterion):
"""在验证集或测试集上评估模型性能,不更新模型参数"""
# ... (省略具体实现,与train函数类似)
# 翻译单个句子的函数
def translate_sentence(src_indexes, src_vocab, tgt_vocab, model, device, max_length=50):
"""使用训练好的模型翻译单个英文句子到中文"""
# ... (省略具体实现,注意这里在生成翻译时使用了贪心搜索)
# 计算BLEU分数的函数
def calculate_bleu(dev_loader, src_vocab, tgt_vocab, model, device):
"""计算验证集上的BLEU分数,评估翻译质量"""
# 主训练循环函数
def train_model(model, train_iterator, valid_iterator, optimizer, criterion, **kwargs):
"""主训练循环,包括多个epoch的训练和验证,保存最佳模型"""
# 定义一些全局常量
MAX_LENGTH = 100
BATCH_SIZE = 32
DEVICE = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
N = 148363 # 假设的训练集采样数量
# 加载数据(这里假设train_path等变量已定义)
# 主函数部分(假设在__main__块中)
if __name__ == '__main__':
# 初始化模型、损失函数和优化器(这里假设D_MODEL等参数已定义)
# ... (省略具体代码,与原始代码相同)
# 训练模型
# ... (省略调用train_model的代码,与原始代码相同)
# 加载最佳模型并翻译测试集(注意:这里实际上在__main__外部调用了load_state_dict,通常应放在__main__内部)
# ... (省略加载模型和翻译测试集的代码,注意通常这部分也会放在__main__内部,并且会处理可能的异常)
三、运行
数据信息
模型训练结果
结果
四、改进模型技巧(摘录)
-
最简单的就是调参,将 epochs 调大一点,使用全部训练集,以及调整模型的参数,如head、layers等。如果数据量允许,增加模型的深度(更多的编码器/解码器层)或宽度(更大的隐藏层尺寸),这通常可以提高模型的表达能力和翻译质量,尤其是在处理复杂或专业内容时。
-
加入术语词典,这是在此竞赛中比较有效的方法,加入术语词典的方法策略也有很多,如:
-
在模型生成的翻译输出中替换术语,这是最简单的方法
-
整合到数据预处理流程,确保它们在翻译中保持一致
-
在模型内部动态地调整术语的嵌入,这涉及到在模型中加入一个额外的层,该层负责查找术语词典中的术语,并为其生成专门的嵌入向量,然后将这些向量与常规的词嵌入结合使用
-
-
认真做数据清洗,我们在 Task2 已经提到过当前训练集存在脏数据的问题,会影响我们的模型训练
-
数据扩增:
-
回译(back-translation):将源语言文本先翻译成目标语言,再将目标语言文本翻译回源语言,生成的新文本作为额外的训练数据
-
同义词替换:随机选择句子中的词,并用其同义词替换
-
使用句法分析和语义解析技术重新表述句子,保持原意不变
-
将文本翻译成多种语言后再翻译回原语言,以获得多样化翻译
-
-
采用更精细的学习率调度策略(baseline我们使用的是固定学习率):
-
Noam Scheduler:结合了warmup(预热)阶段和衰减阶段
-
Step Decay:最简单的一种学习率衰减策略,每隔一定数量的epoch,学习率按固定比例衰减
-
Cosine Annealing:学习率随周期性变化,通常从初始值下降到接近零,然后再逐渐上升
-
-
自己训练一个小的预训练模型,尽量选择 1B 以下小模型,对 GPU 资源要求比较高,仅仅使用魔搭平台可能就满足不了
-
将训练集上训练出来的模型拿到开发集(dev dataset)上 finetune 可以提高测试集(test dataset)的得分,因为开发集与测试集的分布比较相近
-
在开发集和测试集上训一个语言模型,用这个语言模型给训练集中的句子打分,选出一些高分句子
-
集成学习:训练多个不同初始化或架构的模型,并使用集成方法(如投票或平均)来产生最终翻译。这可以减少单一模型的过拟合风险,提高翻译的稳定性。