基于术语词典干预的机器翻译挑战赛
赛事概要
Transformer简介
基于循环或卷积神经网络的序列到序列建模方法是现存机器翻译任务中的经典方法。然而,它们在建模文本长程依赖方面都存在一定的局限性。
-
对于卷积神经网络来说,受限的上下文窗口在建模长文本方面天然地存在不足。如果要对长距离依赖进行描述,需要多层卷积操作,而且不同层之间信息传递也可能有损失,这些都限制了模型的能力。
-
而对于循环神经网络来说,上下文的语义依赖是通过维护循环单元中的隐状态实现的。在编码过程中,每一个时间步的输入建模都涉及到对隐藏状态的修改。随着序列长度的增加,编码在隐藏状态中的序列早期的上下文信息被逐渐遗忘。尽管注意力机制的引入在一定程度上缓解了这个问题,但循环网络在编码效率方面仍存在很大的不足之处。由于编码端和解码端的每一个时间步的隐藏状态都依赖于前一时间步的计算结果,这就造成了在训练和推断阶段的低效。
为了更好地描述文字序列,谷歌的研究人员在 2017 年提出了一种新的模型 Transformer,它摒弃了循环结构,并完全通过注意力机制完成对源语言序列和目标语言序列全局依赖的建模。在抽取每个单词的上下文特征时,Transformer 通过自注意力机制(self-attention)衡量上下文中每一个单词对当前单词的重要程度。
在这个过程当中没有任何的循环单元参与计算。这种高度可并行化的编码过程使得模型的运行变得十分高效。当前几乎大部分的大语言模型都是基于 Transformer 结构,本节以应用于机器翻译的基于Transformer 的编码器和解码器介绍该模型。
Transformer的主要组件包括编码器(Encoder)、解码器(Decoder)和注意力层。其核心是利用多头自注意力机制(Multi-Head Self-Attention),使每个位置的表示不仅依赖于当前位置,还能够直接获取其他位置的表示。自从提出以来,Transformer模型在机器翻译、文本生成等自然语言处理任务中均取得了突破性进展,成为NLP领域新的主流模型。
以下是Transformer模型的基本框架
其他内容不再赘述,更多的看附件的文档。
baseline的优化
1.数据的清洗
在源代码的基础上添加一个正则表达式(regex)过滤步骤来移除所有的括号及其内容。
# 数据预处理函数
def preprocess_data(en_data: List[str], zh_data: List[str]) -> List[Tuple[List[str], List[str]]]:
processed_data = []
for en, zh in zip(en_data, zh_data):
# 移除括号及其内容
en_cleaned = re.sub(r'\s*\([^)]*\)', '', en).strip().lower()
zh_cleaned = re.sub(r'\s*\([^)]*\)', '', zh).strip()
en_tokens = en_tokenizer(en_cleaned)[:MAX_LENGTH]
zh_tokens = zh_tokenizer(zh_cleaned)[:MAX_LENGTH]
if en_tokens and zh_tokens: # 确保两个序列都不为空
processed_data.append((en_tokens, zh_tokens))
return processed_data
这里的正则表达式 \s*\([^)]*\)
匹配任何由圆括号包围的文本,并且包括括号前后的任意空白字符。使用 re.sub
函数替换匹配到的模式为一个空字符串,从而移除它们。
如果需要处理中括号、大括号或其它类型的括号,可以相应地调整正则表达式。例如,要同时处理圆括号和中括号,可以使用这样的正则表达式:\s*[\(\[][^)\]]*\)[\)\]]
。但通常情况下,上述正则表达式应该足够应对大多数常见的括号情况。
ps.虽然在这里用了这种方法,但最后的结果还是出现了大量的脏数据,应该是方法不当,详见下文的结果展示
2.优化术语词典的后处理流程
## 存储成字典
#def load_dictionary(dict_path):
# term_dict = {}
# with open(dict_path, 'r', encoding='utf-8') as f:
# data = f.read()
# data = data.strip().split('\n')
# source_term = [line.split('\t')[0] for line in data]
# target_term = [line.split('\t')[1] for line in data]
# for i in range(len(source_term)):
# term_dict[source_term[i]] = target_term[i]
# return term_dict
# def post_process_translation(translation, term_dict):
# """ 使用术语词典进行后处理(这里的处理比较简单,加入更精细、高效的处理方式) """
#
# translated_words = [term_dict.get(word, word) for word in translation]
# return "".join(translated_words)
from collections import defaultdict
def load_dictionary(dict_path):
term_dict = {}
key_set = set() # 用于快速检查单词是否在词典中
with open(dict_path, 'r', encoding='utf-8') as f:
for line in f:
source, target = line.strip().split('\t')
term_dict[source] = target
key_set.add(source)
return term_dict, key_set
def post_process_translation(translation, term_dict, key_set):
""" 使用术语词典进行后处理,加入缓存机制和快速查找集合 """
cache = {} # 缓存已经查找到的结果
translated_words = []
for word in translation:
if word in cache:
translated_word = cache[word]
elif word in key_set:
translated_word = term_dict[word]
cache[word] = translated_word
else:
translated_word = word
cache[word] = word
translated_words.append(translated_word)
return "".join(translated_words)
上面注释掉的是优化前的部分,下面是优化后的部分。
在优化时主要考虑了这几个方面:
-
避免重复查找:对于大型词典,多次查找同一项可能会导致性能下降。我们可以考虑在第一次遇到某个词条时将其结果缓存起来,以减少后续的查找时间。
-
使用集合进行快速检查:如果词典非常大,使用字典键的成员资格测试可能不是最快的。我们可以维护一个单独的集合来存储词典中的所有键,这样可以更快地检查一个单词是否在词典中。
-
考虑多线程或多进程处理:如果翻译的文本很长,可以考虑使用多线程或多进程来加速处理,尤其是当系统有多个CPU核心时。
在这个版本中,load_dictionary
函数现在返回一个额外的 key_set
,它包含了词典中的所有源词条。post_process_translation
函数现在接受这个 key_set
和一个缓存字典 cache
来存储已处理过的单词,以避免重复查找。
请注意,如果翻译文本非常大,或者词典非常大,那么使用缓存可能需要更多的内存。
3.增加训练集的大小和epoch的数量
这是最简单也是最有效的方法,将训练数据取最大值,我将epochs定到15,保证了有效性
结果展示
可以看到任然存在大量的脏数据,但是总的看来比之前只会输出“的”的结果好上不少。
baseline附件:https://datawhaler.feishu.cn/wiki/OgQWwkYkviPfpwkE1ZmcXwcWnAh