论文分享 -- NLP -- Neural machine Translation of Rare Words with Subword Units

2023-11-27 01:20

本文主要是介绍论文分享 -- NLP -- Neural machine Translation of Rare Words with Subword Units,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

博客内容将首发在微信公众号"跟我一起读论文啦啦",上面会定期分享机器学习、深度学习、数据挖掘、自然语言处理等高质量论文,欢迎关注!
在这里插入图片描述

本次总结的是一篇16年的关于NLP中分词操作的论文,论文链接Subword,参考的实现代码subword-nmt,许多论文方法(例如BERT等)都将该方法应用到分词处理上,相对于word-level和character-level,该方法取得了不错的效果。

动机和创新点

  • 机器翻译中,通常使用固定大小的词表,而在实际翻译场景中,应当是open-vocabulary。这就使得翻译数据集中的稀有词变得困难,不能生成词表中没出现的词。这就是OOV问题
  • 对于word-level 翻译模型,通常使用back-off dictionary(比如将source和target两两对应起来,使用OOV来表示,这样在翻译结果中出现OOV时,就用source所对应的target来代替)来处理OOV词汇。但是这样做是建立在source target中的词总是能一一对应的前提下,因为语言之间的形态合成程度不同,这种假设常常不成立;其次word-level 翻译模型不能生成模型没见过的词(不在词表中),对于这种情况,有论文提出直接从copy unknown 词到target words中,但是这种处理策略也仅仅限于一些实体名称类的词汇;同时为了节省计算时间和资源,词表大小通常被限制在30k-50k之间,所以词表空间是比较昂贵的,如果一些词义类似的词放到词表中,例如like,liked,liking等形态上类似的词均放到词表中,直观上感觉有些浪费。
  • 在实际翻译时,并不一定都是以word为基本单位进行翻译,可通过比单词更小的单位(subword)进行翻译,例如名称类的词(通过subword复制或音译)、复合词(形态上类似的词,前缀后缀相同,如run、runer、running等,可通过合成(run与er合成等)翻译)、同源词和外来词(通过subword语音和形态转换),以上称为透明翻译。直观上来看,有时相对于以一个word作为基本单位整体去翻译,这种通过subword来翻译则显得更有效率和意义。论文中提到,从德语数据集中分析,在100个稀有词(不在出现最频繁的5000个词中)中,大多数的词可以通过更小的subword units进行翻译。
  • 那么,如何将word切分成合适的subword呢?论文中提出了采用Byte pair encoding(BPE)压缩算法,首先以字符划分,然后再合并。也就是不断的以出现频次最高的2-gram进行合并操做,直到打到词表大小为止。这种以频次合并是比较符合常识的,例如像’er’,'ing,'ed’这样比较有意义的后缀,'e’和’r’同时出现的频次应该比较多。“ing”和“ed”类似道理。
  • 将稀有词划分成subword,能比较好的处理oov问题。例如训练集中大量出现“runner”和“thinking”,那按word-level词频来说,word-level词表中应该包含这两个词,此时词“running”只出现一次,则该词很有可能是oov,但是如果以subword切词,则subword词表中应该含有“run”,”er”,”think”,”ing”,那么对于”running”,其subword切词后为“run”,”ing”均在词表中。
  • 通过对稀有词进行适当的切分,得到subword units,机器翻译模型能更好的处理透明翻译,并且能生成一些unseen words。
  • 机器翻译模型通常都会用到注意力机制,在word-level模型中,模型每次只能计算在word级别上的注意力,我们希望模型可以在每一步学习中将注意力放在不同的subword上,显然这样更有意义和效率。

BPE

一个很简单的压缩算法。具体来说,分为以下几步:

  1. 将原始数据集划分成单词集合,再将每个单词划分成字符集,对于每个单词最后一个字符后面加上’-’(标记单词边界,以后用于字符恢复成单词),这样就得到了一个大的字符集合。
  2. 统计上面的得到的字符集合,统计每个单词内 2-gram字符出现频次,得到频次最高的2-gram字符,例如(‘A’,‘B’)连续出现的频率最高,则以“AB”替换所有单词内出现的(‘A‘,‘B’),然后将该新词添加到词表中。这里注意:该新词以后可以作为一个整体与单词内其他字符合并;替换或合并不跨单词边界
  3. 对步骤2循环进行多次,直到达到就得到我们预设的词表大小。最终的vocab_size = init_size(这里为0)+ merge_operation_count, merge_operation_count是模型唯一的超参数。
    在这里插入图片描述
    上图中,实际上将er这样常见且很有意义的后缀作为一个subword放入到词表中,对模型理解语言和节省词表空间、以及OOV问题很有帮助。

实现代码

这篇论文所提方法很简单,但是代码具体实现可以了解下,挺有趣的。这里参考的代码nmt-subword。代码中主要有以下两个重要文件代码。依次来看看。

learn_bpe.py

  1. 在原始数据集上,统计每个word出现的频率,然后得到每个单词的字符序列(末尾加’</w>'标记单词边界),和其出现频率,组成字典,然后按照频率进行倒排序,得到sorted_vocab。
vocab = get_vocabulary(infile, is_dict)
vocab = dict([(tuple(x[:-1])+(x[-1]+'</w>',) ,y) for (x,y) in vocab.items()])
sorted_vocab = sorted(vocab.items(), key=lambda x: x[1], reverse=True)
  1. 统计每个单词内2-gram出现的频率
def get_pair_statistics(vocab):"""Count frequency of all symbol pairs, and create index"""# data structure of pair frequenciesstats = defaultdict(int)#index from pairs to wordsindices = defaultdict(lambda: defaultdict(int))for i, (word, freq) in enumerate(vocab):prev_char = word[0]for char in word[1:]:stats[prev_char, char] += freqindices[prev_char, char][i] += 1prev_char = charreturn stats, indices
  1. 在预设的合并次数内,不断的合并出现频次最高的2-gram词,同时将得到的新词加入到词表中,同时更新与新词左右两边相连的2-gram频次。直到出现的最高频次低于我们预设min_frequency。
for i in range(num_symbols):if stats:most_frequent = max(stats, key=lambda x: (stats[x], x)) ## 统计出现的最高频次的2-gram# we probably missed the best pair because of pruning; go back to full statisticsif not stats or (i and stats[most_frequent] < threshold):prune_stats(stats, big_stats, threshold)stats = copy.deepcopy(big_stats)most_frequent = max(stats, key=lambda x: (stats[x], x))# threshold is inspired by Zipfian assumption, but should only affect speedthreshold = stats[most_frequent] * i/(i+10000.0)prune_stats(stats, big_stats, threshold)if stats[most_frequent] < min_frequency:sys.stderr.write('no pair has frequency >= {0}. Stopping\n'.format(min_frequency))breakif verbose:sys.stderr.write('pair {0}: {1} {2} -> {1}{2} (frequency {3})\n'.format(i, most_frequent[0], most_frequent[1], stats[most_frequent]))outfile.write('{0} {1}\n'.format(*most_frequent)) ## 将出现频次最高的2-gram存入到词表中changes = replace_pair(most_frequent, sorted_vocab, indices) ##  合并和替换出现频次最高的2-gramupdate_pair_statistics(most_frequent, changes, stats, indices) ## 更新与合并后新词左右两边相连的2-gram频次stats[most_frequent] = 0

这样就得到了2-gram词频表 BPE_codes

apply_bpe.py

在learn_bpe.py中,按照bpe算法得到了数据集的BPE_codes,那么将数据喂给模型之前,我们需要将输入数据按照bpe_codes进行编码,通俗来说就是按照BPE_codes里面的分词规则对输入数据进行分词罢了。

def segment_tokens(self, tokens):"""segment a sequence of tokens with BPE encoding"""output = []for word in tokens:# eliminate double spacesif not word:continuenew_word = [out for segment in self._isolate_glossaries(word)for out in encode( ## 下面会有encode方法介绍segment,   ## 需要进行编码的序列self.bpe_codes, ## learn_bpe得到的编码,进行了字典处理,第一个为pair 元祖,第一个为对应的索引self.bpe_codes_reverse, ## (pair[0] + pair[1], pair)self.vocab,self.separator,self.version,self.cache,self.glossaries)] ##  已有的一些词汇表,如城市名称,国家名称等,这些词不能再切分,并且要和其他词split开for item in new_word[:-1]:output.append(item + self.separator)output.append(new_word[-1])return output

再来看看encode方法,就是将序列内的2-ngram按照bpe_codes不断的合并替换。如果最终pair长度为1即没有合并的可能或者最大的pair也不再bpe_codes中,则停止循环。

def encode(orig, bpe_codes, bpe_codes_reverse, vocab, separator, version, cache, glossaries=None):"""Encode word based on list of BPE merge operations, which are applied consecutively"""if orig in cache:return cache[orig]if re.match('^({})$'.format('|'.join(glossaries)), orig):cache[orig] = (orig,)return (orig,)if version == (0, 1):word = tuple(orig) + ('</w>',)elif version == (0, 2): # more consistent handling of word-final segmentsword = tuple(orig[:-1]) + ( orig[-1] + '</w>',)else:raise NotImplementedErrorpairs = get_pairs(word)if not pairs:return origwhile True:bigram = min(pairs, key = lambda pair: bpe_codes.get(pair, float('inf')))if bigram not in bpe_codes:breakfirst, second = bigramnew_word = []i = 0while i < len(word):try:j = word.index(first, i)new_word.extend(word[i:j])i = jexcept:new_word.extend(word[i:])breakif word[i] == first and i < len(word)-1 and word[i+1] == second:new_word.append(first+second)i += 2else:new_word.append(word[i])i += 1new_word = tuple(new_word)word = new_wordif len(word) == 1:breakelse:pairs = get_pairs(word)# don't print end-of-word symbolsif word[-1] == '</w>':word = word[:-1]elif word[-1].endswith('</w>'):word = word[:-1] + (word[-1].replace('</w>',''),)if vocab: ## 这里的vocab是以一定阈值,统计得到的词表,过滤掉了低频词,以减少低词频影响。## 论文中讲到低频词可能是噪声## 这里结合过滤低频词后的词汇表。##因为过滤掉低词频,可能会出现oov问题,如出现oov问题,则将原词切分为更小的词。## 更小的词,就有可能在subword词表中。word = check_vocab_and_split(word, bpe_codes_reverse, vocab, separator)cache[orig] = wordreturn word

这样就完成了对输入数据的subword分词。

个人总结

  • 论文方法比较简单,在流程上输入数据预处理阶段
  • 相对于word-level,subword更有意义和效率
  • 这种subword方法可能对中文不太奏效,因为中文没有那种后缀之类的形式。

这篇关于论文分享 -- NLP -- Neural machine Translation of Rare Words with Subword Units的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/426697

相关文章

Python中常用的四种取整方式分享

《Python中常用的四种取整方式分享》在数据处理和数值计算中,取整操作是非常常见的需求,Python提供了多种取整方式,本文为大家整理了四种常用的方法,希望对大家有所帮助... 目录引言向零取整(Truncate)向下取整(Floor)向上取整(Ceil)四舍五入(Round)四种取整方式的对比综合示例应

linux进程D状态的解决思路分享

《linux进程D状态的解决思路分享》在Linux系统中,进程在内核模式下等待I/O完成时会进入不间断睡眠状态(D状态),这种状态下,进程无法通过普通方式被杀死,本文通过实验模拟了这种状态,并分析了如... 目录1. 问题描述2. 问题分析3. 实验模拟3.1 使用losetup创建一个卷作为pv的磁盘3.

MySQL8.2.0安装教程分享

《MySQL8.2.0安装教程分享》这篇文章详细介绍了如何在Windows系统上安装MySQL数据库软件,包括下载、安装、配置和设置环境变量的步骤... 目录mysql的安装图文1.python访问网址2javascript.点击3.进入Downloads向下滑动4.选择Community Server5.

CentOS系统Maven安装教程分享

《CentOS系统Maven安装教程分享》本文介绍了如何在CentOS系统中安装Maven,并提供了一个简单的实际应用案例,安装Maven需要先安装Java和设置环境变量,Maven可以自动管理项目的... 目录准备工作下载并安装Maven常见问题及解决方法实际应用案例总结Maven是一个流行的项目管理工具

10个Python自动化办公的脚本分享

《10个Python自动化办公的脚本分享》在日常办公中,我们常常会被繁琐、重复的任务占据大量时间,本文为大家分享了10个实用的Python自动化办公案例及源码,希望对大家有所帮助... 目录1. 批量处理 Excel 文件2. 自动发送邮件3. 批量重命名文件4. 数据清洗5. 生成 PPT6. 自动化测试

10个Python Excel自动化脚本分享

《10个PythonExcel自动化脚本分享》在数据处理和分析的过程中,Excel文件是我们日常工作中常见的格式,本文将分享10个实用的Excel自动化脚本,希望可以帮助大家更轻松地掌握这些技能... 目录1. Excel单元格批量填充2. 设置行高与列宽3. 根据条件删除行4. 创建新的Excel工作表5

Redis多种内存淘汰策略及配置技巧分享

《Redis多种内存淘汰策略及配置技巧分享》本文介绍了Redis内存满时的淘汰机制,包括内存淘汰机制的概念,Redis提供的8种淘汰策略(如noeviction、volatile-lru等)及其适用场... 目录前言一、什么是 Redis 的内存淘汰机制?二、Redis 内存淘汰策略1. pythonnoe

Golang操作DuckDB实战案例分享

《Golang操作DuckDB实战案例分享》DuckDB是一个嵌入式SQL数据库引擎,它与众所周知的SQLite非常相似,但它是为olap风格的工作负载设计的,DuckDB支持各种数据类型和SQL特性... 目录DuckDB的主要优点环境准备初始化表和数据查询单行或多行错误处理和事务完整代码最后总结Duck

将Python应用部署到生产环境的小技巧分享

《将Python应用部署到生产环境的小技巧分享》文章主要讲述了在将Python应用程序部署到生产环境之前,需要进行的准备工作和最佳实践,包括心态调整、代码审查、测试覆盖率提升、配置文件优化、日志记录完... 目录部署前夜:从开发到生产的心理准备与检查清单环境搭建:打造稳固的应用运行平台自动化流水线:让部署像

Python实现NLP的完整流程介绍

《Python实现NLP的完整流程介绍》这篇文章主要为大家详细介绍了Python实现NLP的完整流程,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 编程安装和导入必要的库2. 文本数据准备3. 文本预处理3.1 小写化3.2 分词(Tokenizatio