Datawhale AI 夏令营 讯飞机器翻译挑战赛学习笔记
AI初学者,第一次跑代码训练模型实战。有些个人想法可能存在问题,欢迎指正。
更新:2024.9.18,主要部分添加黄底标记,帮助回忆项目。无实质更新。
竞赛介绍
基于术语词典干预的机器翻译挑战赛选择以英文为源语言,中文为目标语言的机器翻译。本次大赛除英文到中文的双语数据,还提供英中对照的术语词典。参赛队伍需要基于提供的训练数据样本从多语言机器翻译模型的构建与训练,并基于测试集以及术语词典,提供最终的翻译结果。
数据集:
- 训练集:双语数据 - 中英14万余双语句对(用于运行学习算法)
- 开发集:英中1000双语句对(用于调整参数,选择特征,以及对学习算法作出其它决定。有时也称为留出交叉验证集(hold-out cross validation set)。)
- 测试集:英中1000双语句对(用于评估算法的性能,但不会据此改变学习算法或参数。提交分数)
- 术语词典:英中2226条
官方baselineTask01的代码模型是seq2seq,encoder和decoder都是GRU。
Task2添加了注意力机制和数据清洗。
Task3是Transformer。
配置环境
有几个包需要额外安装:
torchtext
:是一个用于自然语言处理(NLP)任务的库,它提供了丰富的功能,包括数据预处理、词汇构建、序列化和批处理等,特别适合于文本分类、情感分析、机器翻译等任务jieba
:是一个中文分词库,用于将中文文本切分成有意义的词语sacrebleu
:用于评估机器翻译质量的工具,主要通过计算BLEU(Bilingual Evaluation Understudy)得分来衡量生成文本与参考译文之间的相似度
!pip install torchtext
!pip install jieba
!pip install sacrebleu
spacy
:是一个强大的自然语言处理库,支持70+语言的分词与训练
这里,我们需要安装 spacy 用于英文的 tokenizer(分词,就是将句子、段落、文章这种长文本,分解为以字词为单位的数据结构,方便后续的处理分析工作)
需要注意的是,使用命令!python -m spacy download en_core_web_trf安装 en_core_web_sm 语言包非常的慢,经常会安装失败,这里我们可以离线安装(去github下载好)。由于en_core_web_sm 对 spacy 的版本有较强的依赖性,你可以使用 pip show spacy 命令在终端查看你的版本,可以看到我的是 3.7.5 版本的 spacy。
github上下好上传到服务器,离线安装(第三行)即可。
!pip install -U pip setuptools wheel -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install -U 'spacy[cuda12x,transformers,lookups]' -i https://pypi.tuna.tsinghua.edu.cn/simple
!pip install ../dataset/en_core_web_trf-3.7.3-py3-none-any.whl
其他注意事项
- 测试集只提供了英文数据,与原中文数据比对是在官方提交预测文本在后台比对的。
- 竞赛不允许使用预训练大模型。翻译效果采用BLEU-4评分。
- 竞赛使用魔搭平台GPU环境免费额度进行训练。
- 竞赛提供的代码已经实现了数据划分和模型搭建。如果不考虑上分基本是一键运行。但是task3默认代码运行产生的测试集输出译文如果数量远远不够–评分为0,那么记得修改测试数据集构造batchSize=1(因为测试集运行代码只跑了batch的第一个样本
src[0]
) - 魔搭只会保留代码文件如.ipynb文件,数据集最好保存在本地,每次启动新实例要上传。对于模型如果不想再从头训练,可以将模型.pt文件(
torch.save(model.state_dict(), save_path)
生成pt文件)download到本地,后续可以加载模型继续增加epoch训练。
基本流程
报名
→下载数据集和baseline代码
→个人优化修改等
→利用魔搭平台算力训练
→(开发集评估
→)测试集输出翻译结果到文件submit.txt
→下载输出文件并上传到官方测评分数
。
出分后打卡。
学习计划
NLP知识
自然语言处理(Natural Language Processing,NLP)是语言学与人工智能的分支,试图让计算机能够完成处理语言、理解语言和生成语言等任务。
大致可以将 NLP 任务 分为四类:
- 序列标注:比如中文分词,词性标注,命名实体识别,语义角色标注等都可以归入这一类问题。这类任务的共同点是句子中每个单词要求模型根据上下文都要给出一个分类类别;
- 分类任务:比如我们常见的文本分类,情感计算等都可以归入这一类。这类任务特点是不管文章有多长,总体给出一个分类类别即可;
- 句子关系判断:比如问答推理,语义改写,自然语言推理等任务都是这个模式,它的特点是给定两个句子,模型判断出两个句子是否具备某种语义关系;
- 生成式任务:比如机器翻译,文本摘要,写诗造句,看图说话等都属于这一类。它的特点是输入文本内容后,需要自主生成另外一段文字。
机器翻译
机器翻译(Machine Translation,简称MT)是自然语言处理领域的一个重要分支,其目标是将一种语言的文本自动转换为另一种语言的文本。机器翻译的发展可以追溯到20世纪50年代,经历了从基于规则的方法、统计方法到深度学习方法的演变过程。
seq2seq、encoder-decoder
深度学习中神经网络编码器-解码器模型:
通过循环网络(RNN)对源语言文本进行编码,并生成目标语言翻译结果的过程十分简单。然而,它仅仅使用一个定长的向量 hm编码整个源语言序列。这对于较短的源语言文本没有什么问题,但随着文本序列长度的逐渐加长,单一的一个向量 hm 可能不足以承载源语言序列当中的所有信息。
引入注意力机制的循环机器翻译架构与基于简单循环网络的机器翻译模型大体结构相似,均采用循环神经网络作为编码器与解码器的实现。关键的不同点在于注意力机制的引入使得不再需要把原始文本中的所有必要信息压缩到一个向量当中。引入注意力机制的循环神经网络机器翻译架构如图所示:
传统的 Seq2Seq 模型在解码阶段仅依赖于编码器产生的最后一个隐藏状态,这在处理长序列时效果不佳。注意力机制允许解码器在生成每个输出词时,关注编码器产生的所有中间状态,从而更好地利用源序列的信息。具体来说,给定源语言序列经过编码器输出的向量序列 h 1 , h 2 , h 3 , . . . , h m h_{1},h_{2},h_{3},...,h_{m} h1,h2,h3,...,hm,注意力机制旨在依据解码端翻译的需要,自适应地从这个向量序列中查找对应的信息。
Transformer
基于循环(上下文的语义依赖是通过维护循环单元中的隐状态实现的,早期的上下文信息容易被遗忘)或卷积(受限的上下文窗口)神经网络的序列到序列建模方法是现存机器翻译任务中的经典方法。然而,它们在建模文本长程依赖方面都存在一定的局限性。
太有名了,这里不多介绍。
Attention is all you need!(https://datawhaler.feishu.cn/wiki/OgQWwkYkviPfpwkE1ZmcXwcWnAh#ZH6cdmbkYoaf1Kx4KgDc5Tfunqe)
Transformer讲解
Transformer20题
翻译质量评价及评估指标BLEU
在机器翻译领域,BLEU(Bilingual Evaluation Understudy)是一种常用的自动评价指标,用于衡量计算机生成的翻译与一组参考译文之间的相似度。这个指标特别关注n-grams(连续的n个词)的精确匹配,可以被认为是对翻译准确性和流利度的一种统计估计。计算BLEU分数时,首先会统计生成文本中n-grams的频率,然后将这些频率与参考文本中的n-grams进行比较。如果生成的翻译中包含的n-grams与参考译文中出现的相同,则认为是匹配的。最终的BLEU分数是一个介于0到1之间的数值,其中1表示与参考译文完美匹配,而0则表示完全没有匹配。
BLEU-4 特别指的是在计算时考虑四元组(即连续四个词)的匹配情况。
BLEU 评估指标的特点:
- 优点:计算速度快、计算成本低、容易理解、与具体语言无关、和人类给的评估高度相关。
- 缺点:不考虑语言表达(语法)上的准确性;测评精度会受常用词的干扰;短译句的测评精度有时会较高;没有考虑同义词或相似表达的情况,可能会导致合理翻译被否定。
可以看出BLEU-4评分严格,我估计最终提交的分数也是1000条数据得分之和,所以一条数据的得分很低很低,自己从0训练这也很正常吧。
竞赛官方部分代码解释
审查数据,绝大多数文本序列长度都在100以内,所以MaxSeqLen = 100。多余截断,不足填充pad。
MAX_LENGTH = 100 # 最大句子长度
model.eval()
进入评估模式(不再dropout)。恢复训练记得使用model.train()
进入训练模式。
词嵌入后记得*sqrt(d_model)放缩。(具体原因不是很清楚,上文提到的Transformer20题中有一问,但问答好像不匹配)
# 构造数据集:文本序列 转为 词id序列
class TranslationDataset(Dataset):
def __init__(self, data: List[Tuple[List[str], List[str]]], en_vocab, zh_vocab):
self.data = data
self.en_vocab = en_vocab
self.zh_vocab = zh_vocab
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
# 数组越界错误,群友的建议(官方初始的样本个数大于真实样本个数,根据报错修改N=len(self.data)=148333即可)
if idx >= len(self.data):
raise IndexError(f'index {idx} out of range for data with length {len(self.data)}')
en, zh = self.data[idx]
# 句子开头结尾添加<bos>、<eos>标签。用于标识句子开头和结尾。
en_indices = [self.en_vocab['<bos>']] + [self.en_vocab[token] for token in en] + [self.en_vocab['<eos>']]
zh_indices = [self.zh_vocab['<bos>']] + [self.zh_vocab[token] for token in zh] + [self.zh_vocab['<eos>']]
return en_indices, zh_indices
# 训练好后测试集中翻译句子函数
def translate_sentence(src_indexes, src_vocab, tgt_vocab, model, device, max_length=50):
model.eval()
src_tensor = src_indexes.unsqueeze(0).to(device) # 添加批次维度
with torch.no_grad():
encoder_outputs = model.transformer.encoder(model.positional_encoding(model.src_embedding(src_tensor) * math.sqrt(model.d_model)))
# 从<bos>开始,依赖上一次token输出,循环输入到model中直到<eos>结束,这部分主要是tokenId
trg_indexes = [tgt_vocab['<bos>']]
for i in range(max_length):
trg_tensor = torch.LongTensor(trg_indexes).unsqueeze(0).to(device)
with torch.no_grad():
output = model(src_tensor, trg_tensor)
pred_token = output.argmax(2)[:, -1].item()
trg_indexes.append(pred_token)
if pred_token == tgt_vocab['<eos>']:
break
# 根据tokenId换成token词汇
trg_tokens = [tgt_vocab.get_itos()[i] for i in trg_indexes]
return trg_tokens[1:-1] # 移除<bos>和<eos>标记
上分
模型更改和调参
保持模型不变,可以考虑改变训练参数N(训练集数据个数)、epoch等。
task1中
baseline默认参数
代码默认训练阶段只取了1000条数据对,提交默认的训练结果评分只有0.2左右。
将N提高到1万条数据对进行训练,epoch保持十轮不变。训练时长为7分钟。分数提高到了1.0224。
teacherForcing机制
通过阅读源代码,了解到训练时采用了概率为0.5的teacherForcing和freeRunning混合的模式。
保持N=1万、epoch=10,修改训练阶段tearcherForcing的概率为0.85(即0.15的概率是freeRunning),评分又提高到了1.444,对比翻译结果文本也可明确得出,tearcherForcing效果会更好。
学习率策略
后来加入了余弦退火的学习率调整策略,分数下降了许多,毕竟训练数据太少模型简单,解释如下。
- 训练数据量较小,模型可能还有足够的学习空间,无需过度复杂的学习率调度策略。固定学习率可以让模型持续快速学习,不会被余弦退火过早地降低学习率所限制。
- 如果训练 Epoch 数量相对较少,模型可能还没有充分收敛,尚未到需要降低学习率的阶段。余弦退火在较少 Epoch 下可能过早地降低了学习率,阻碍了模型的快速学习。
- 如果模型本身复杂度较低,没有过拟合的风险,固定学习率可能就能很好地满足训练需求。相比之下,余弦退火的目的是为了防止过拟合,但在这种情况下可能反而不太必要。
task3中
学习率策略一开始使用Transformer本身推荐的Noam。但是效果并不好。(可能是代码写错了)
epoch=10,固定学习率时,后期loss下降变缓,所以可以尝试下学习率衰减。
数据清洗
通过输出训练样本发现许多中文语句中包含如(笑)(掌声)
类的词汇。可考虑清洗。但是效果并不好,可能不如保留此类词汇,所以测试集的译文中可能也带有此类词汇。本文最高分等都是采取了正则匹配删除了(..)
。
然后英文文本中去除特殊符号。
zh = re.sub(r'\(.*?\)', '', zh)# 删除中文文本中的`(笑)`等文本
en = re.sub(r'[a-zA-Z0-9.,!?]+', ' ', en)# 去除特殊符号等
...en.lower()...# 全部小写
术语词典
术语词典策略:
-
- 模型生成的翻译输出中替换术语,这是最简单的方法
- 整合到数据预处理流程,确保它们在翻译中保持一致
- 在模型内部动态地调整术语的嵌入,这涉及到在模型中加入一个额外的层,该层负责查找术语词典中的术语,并为其生成专门的嵌入向量,然后将这些向量与常规的词嵌入结合使用
其实经过比较,post_translate方式效果很差,甚至有时还不如不采用术语词典。因为他首先需要术语(人名等)翻译严格准确,而这在从0训练的模型中已经是比较难的了。
另外通过审查词典数据以及观察此方式下有术语词典参与的翻译结果,发现词典中有些“术语”会导致翻译结果更差,比如词典中有“One:一”
这种术语,然后译文中出现很多翻译错误:(seq2seq模型,训练集10000,epoch=10)
如果你想象One下,…
我认为我是One个很大的挑战的故事…
…我会把它放在One起。
初步想法,所以如果想使用这个最简单的策略(在翻译结果中检测并替换回原术语)
- 要保证数据量够大、训练数据质量高===>翻译准确。
- 如果数据量确实不够也不好构建高质量数据集,那么也可以考虑同义词匹配等策略。
- 词典数据清洗,像
One:一
这种术语映射应该是可以删除的。
可改进
每次训练,保存最佳模型和最新模型。download到本地方便后续增加epoch训练。逐渐增加epoch反复训练。
- 加入早停策略、试下StepDecay学习率策略。
采用lr衰减策略时也可以用以下代码查看当前学习率
print(f'Epoch {epoch + already_base_epoch+1}, Learning Rate: {optimizer.param_groups[0]["lr"]}')
- 加入权重衰减等正则化防止过拟合。
当前最高分数
模型参数如图:
- 13.9613——Transformer前30epoch,21~30学习率固定1e-4,取消术语词典。此分数是用的是第24epoch产生的最佳valLoss的模型。
- 13.1375——Transformer前20epoch,11~20学习率固定2e-4,取消术语词典。此分数是用的是第15epoch产生的最佳valLoss的模型。
- 10.2031——Transformer前10epoch,01~10固定学习率1e-4、译后术语词典。
增大epoch过程记录
看群友的意思,可能层数增益不大???,增加epoch还会有所提升。
群友发的三层50epoch比这效果好很多。就是不清楚群内大佬们都用的哪个学习率策略。
Tip:注意每次增加epoch训练前修改path等常量读取相应模型。如果是新实例那么还要pip安装一遍依赖库,而后续执行不再用安装了。
-
第11~20个epoch:保持参数(只把学习率从1e-4改到2e-4)训练,取消术语词典的译后替换。
可以发现第15轮的valLoss降到了最低,后面有升高,可能发生了过拟合。20epoch又有所降低。 -
第21~30个epoch,还抱有期望,根据11~20轮loss趋于平缓,这次降低了学习率但还是固定在1e-4,最终发现已经严重过拟合了。应该加上权重衰减试试的。(图中Epoch显示错误,应该是21~30的)
用最小Val. Loss的模型(第24epoch)去跑测试集,分数还是相较于第15轮的有点提升。
其他可参考上分技巧
调参
最简单的就是调参,将 epochs 调大一点,使用全部训练集,以及调整模型的参数,如head、layers等。如果数据量允许,增加模型的深度(更多的编码器/解码器层)或宽度(更大的隐藏层尺寸),这通常可以提高模型的表达能力和翻译质量,尤其是在处理复杂或专业内容时。
数据清洗
格式化
转换所有文本为小写,确保一致性;标准化日期、数字等格式。
分词
这里使用了使用jieba 对中文进行分词,使用spaCy对英文进行分词。
序列截断和填充
序列截断:限制输入序列的长度,过长的序列可能增加计算成本,同时也可能包含冗余信息。
序列填充:将所有序列填充至相同的长度,便于批量处理。通常使用标记填充。
添加特殊标记
序列开始和结束标记:在序列两端添加<SOS/BOS>(Sequence Start)和<EOS>(Sequence End)标记,帮助模型识别序列的起始和结束。
未知词标记:为不在词汇表中的词添加<UNK>(Unknown)标记,使模型能够处理未见过的词汇。
数据增强
- 回译(back-translation):将源语言文本先翻译成目标语言,再将目标语言文本翻译回源语言,生成的新文本作为额外的训练数据
- 同义词替换:随机选择句子中的词,并用其同义词替换,如Google的Synonyms或LUKE同义词替换模型。
- 使用句法分析和语义解析技术重新表述句子,保持原意不变
- 将文本翻译成多种语言后再翻译回原语言,以获得多样化翻译
- 利用大模型等工具按照样本生成同类样本扩增数据
- 使用文本摘要技术,如TextRank、LSA等,来生成摘要,然后将摘要添加到原始数据集中。
学习率
采用更精细的学习率调度策略(baseline我们使用的是固定学习率):
- Noam Scheduler:结合了warmup(预热)阶段和衰减阶段
- Step Decay:最简单的一种学习率衰减策略,每隔一定数量的epoch,学习率按固定比例衰减
- Cosine Annealing:学习率随周期性变化,通常从初始值下降到接近零,然后再逐渐上升
其他
- 数据过滤。训练翻译模型后(post-train-filter),进行打分,删除低分语句重新训练,分数也会有提升!
- 模型融合。
- 将训练集上训练出来的模型拿到开发集(dev dataset)上 finetune 可以提高测试集(test dataset)的得分,因为开发集与测试集的分布比较相近(官方大佬提了一嘴
- 集成学习:训练多个不同初始化或架构的模型,并使用集成方法(如投票或平均)来产生最终翻译。这可以减少单一模型的过拟合风险,提高翻译的稳定性。
- loss变化建议
train loss | test loss | 说明 | 措施 |
---|---|---|---|
不断下降 | 不断下降 | 网络仍在学习 | 最好的情况 |
不断下降 | 趋于不变 | 网络过拟合 | max pool或者正则化 |
趋于不变 | 不断下降 | 数据集100%有问题 | 检查dataset |
趋于不变 | 趋于不变 | 学习遇到瓶颈,需要减小学习率或批量数目 | 减少学习率 |
不断上升 | 不断上升 | 网络结构设计不当,训练超参数设置不当,数据集经过清洗等问题 | 最不好的情况 |
- 早停