关于端到端文本生成中的稀疏词与未登录词问题的探讨

转自:http://www.infosec-wiki.com/?p=425981

介绍

本文将会探讨关于自然语言中生成式的序列到序列(Sequence2Sequence)相关模型中的稀疏词/未登录词问题。本文以Encoder-Decoder的端到端文本生成任务,神经机器翻译(NMT)或生成式对话(Chat-bot)为研究对象,并基于此进行探讨。

Encoder-Decoder 是一个十分适合于处理端到端文本生成的任务的框架,即由Encoder读入源文本序列Source Sequence,由Decoder生成目标文本序列 Target Sequence。Encoder-Decoder框架一经提出,遍迅速的普及开来,后续的绝大多数神经机器翻译、生成式对话都是基于这个框架来进行的。如果具体一点解释的话,比如说在英文到中文的机器翻译中, 原文本序列就是“Oh Indian Mi fans, are you ok?” 输出的文本序列就是“印度的米粉们,你们好吗?”, 从而达到一个翻译的效果。Encoder-Decoder 的学习过程就是尝试如何将源文本序列 一个目标文本序列。

如何表示文本

那么,既然Encoder-Decoder 要学习如何根据一个文本序列生成另一个文本序列,那么首先要处理的问题就是,如何去表示这些文本序列? 一般来说,我们提供给Encoder-Decoder的训练数据都是自然语言的形式,也就是真正切切的文字内容。很明显,计算机是无法直接理解这些自然语言的文字,通常都需要将这些文本重新表示,从而使得计算机能够理解。 最简单的一种方式就是以独热的方式表达(one-hot  encoding),就是构造一个词典  ,词典大小为,那么独热表示的方法就是针对对于每一个词,都用一个维度为的向量表示,这个向量只有这个词对应的词典索引位置处数值为1,其他地方数值都为0。 简单来说,独热表示简单地使用了一个唯一的位置去表示一个词语,那么对于一个文本序列,独热表示 就是一串对应的向量序列,一个向量表示一个词语,向量在序列中的位置对应文本中词语的位置。

很明显,独热表示这种方式只是解决了如何表示这个 问题,但是并没有做好。 近年来,利用Embedding Vector 表示是一种更为流行且有效的分布式表示方式(Distributed Representation)。在我们的场景中,所谓的Embedding,就是将词语用一个固定的低维实数稠密向量去表示。 具体来说,对于每一个词的Embedding Vector,其维度都为一个固定的值,Embedding 表示法不像独热表示法一样,表示向量的长度不会随着词典的数目增长而随之现行增长。另外,Embedding Vector的每一个维度的值,都是一个实数值,而不是独热表示那样的0或1。 独热表示法,对于一个长度为 的文本,需要占用 的大小, 而利用Embedding去表示的话,则只需要占用  的大小, 其中的大小是远小于的。 并且,除了占用的空间小这个优势以外,Embedding Vector本身还蕴含了词的一些语义信息,可以更好的表示这个词的内在信息,为NLP应用增添动力。 例如,在Embedding表示中,国王的Vector加上女人的Vector,应该正好接近女王的Vector,即 (国王)+(女人)=( 女王)。 和独热表示不一样的是 ,Embedding表示需要使用大量的语料来进行训练,才能得到每个词对应的Vector是什么,并且语料越多越丰富,效果越好。关于如何获得Embedding Vector有多种方法,比如Word2Vec,GLOVE或者和对应的模型直接共同训练等。

最早提出的Encoder-Decoder的时候,研究人员是使用RNN网络去分别实现Encoder和Decoder的,并且在后续的相关工作中,大部分Encoder和Decoder的实现也都是RNN为基础来进行的,比如说现在用的很多的 Encoder都是 多层双向LSTM/GRU。简单来说,这一类的框架,对于输入的文本序列,Encoder首选会根据Encoder自身的词表内容,将其转变为Embedding Vector序列,对应的,Decoder自身也有一个词表,Decoder也是根据这个词表一个一个的生成目标文本序列的词的,最后形成一个完整的句子。

至此,不知道大家在意到我们所介绍的内容中,文本的表示粒度都是词级别的(Word-Level)?也就是把一个文本划分为一个个的词,词作为表达文本的最小单位?比如说一个句子“今天早上你吃什么了?”,Word-Level表示就是首先将它分词后形成一个 的序列:“今天/早上/你/吃/什么/了/?”,然后将这个序列作为Encoder的输入的。Word-Level的表示方式也是绝大多数Encoder-Decoder模型所采用的的方式。在这种Word-Level的文本表示方法中,每一个词对应一个Embedding Vector,每一个Embedding Vector 都需要根据语料进行训练。具体来说,基于词级别的模型,首先需要构建一个词表,词表内包含个词,每个词是一个维的向量,对于一个已经分词好的文本序列,通过查这个词表的方式获得对应的Embedding表示。显然,这种方法存在一些问题。

有限的词表

一般来说,词表的总大小是固定的,那么如果一个文本中出现了一些新的词或者没有被加入到词典中的词(统称:未登录词),这个词表是无法给出对应的Embedding Vector的,这时候Word-Level词表的做法通常就是直接使用一个通配符UNK来进行替换,即所有不认识的词都用一个统一的符号来代替。使用词级别方式的相关模型,对于未登录词的出现,一般是不可避免的。与此同时,即便是词表中的词,由于长尾效应,往往存在大量稀疏词,这类词一般使用频率低、数量众多。 我们使用了维基百科的中文镜像,使用jieba对其中文语料进行了分词,统计了其出现的词语个数。结果显示,维基百科一共出现了 1.67 亿多频次的词,共计312万个独立的词,也就是说如果设计一个词表,完全覆盖维基百科,那么词表的大小需要有312万,并且其中可能有300万词都是稀疏词。 但是由于计算的复杂度和空间的消耗(特别是GPU显存)的限制,一般一个Encoder-Decoder 框架也就只能几万到十几万的大小的词表,312万明显是不现实的。除此以外,新词是被我们不断创造的,即便我们有一台超级的计算机能够 这312万词,在面对新词的时候也不得不把他当做未登录词来对待。

这里还有一个隐含的逻辑是,有大小限制的词表一般是按照词的重要性从高到低排序得到的,那么不在词表中的词,不但是未登录的词,它的重要性也不够,因此其实它也是稀疏词,不过本文并没有将未登录词看做稀疏词,本文的未登录词在意的是词的未登录的特性,稀疏词在意的是在词表中,但是稀疏的词,而并不是所有稀疏的词。

其中,未登录词的问题是直接让相关的模型变文盲,例如对话中,未登录词让Encoder读不懂用户说的是什么,让Decoder生成用户猜不透的UNK符号。而稀疏词,虽然不和未登录词一样完全不认识,但是如果一个稀疏词在语料库中只出现了几次,依赖于大量训练的Encoder-Decode框架即便记录这个词,也不能准确利用和生成这个词,这种半懂不懂的词多了后,可能反而拖累Encoder-Decoder的性能表现。未登录词的问题在于词表总会遇到不认识的词,稀疏词的问题在于理解不透,两个问题通常是同时出现的。

针对未登录词/稀疏词的问题,本文介绍的方法,大体上可以归纳为两类。第一类方法是基于非Word-Level的表示方法,能彻底消除这些未登录词且极大的缓解稀疏词问题,而另一类方法则是保持,且尽可能的减少这两类词所带来的影响,但可能模型本身并不是解决这些问题的。

上面我们说到的,现在我们用的多的都是用词级别的表示,并且让相应的模型(如Encoder-Decoder)也对应的工作在词级别行。 第一类方法的思路就是,既然基于词级别去表示文本会造成未登录词,那么我直接使用不会造成未登录词的方法不就可以了? 我们知道,在绝大部分的语言里面,词(Word)并不是最小的单位,词下面还有字/字母(Character),字是由字/字母组合而成的。 所以在这里,我们以字符级别为例(Character-Level)比如说在英文,所有单词都是由26个字母、10个数字和少量符号组合而成的,这些小单位的字符一般不超过128个(ASCII码),于是这类方法就利用了这个特性,使用字符来表示文本,并且让模型工作在字符级别。与此同时,由于字符的数目通常是远小于词的数目,在模型中,我们可以同时维护和学习所有字符的Embedding Vector. 这个方法可谓是非常巧妙,因为在一个语料中,无论这个词是多么稀疏、何时被创造的,其背后使用的字符都是恒定不变,使用字符去表示字符就直接解决了未登录问题。与此同时,使用字符之间几乎不存在稀疏问题,特别是对于英文这类语言,自然也没有某个字符因为过于稀疏而让模型理解困难。总体来说,使用字符来表示文本的话的大致优点有:

  • 由于字符的数量显著的少于词,并且一个语言的字符通常是恒定有限的,所以使用字符表示的方式,可以说彻底解决了未登录词这个问题。
  •  字符在类似于英文等语言中,不存在稀疏问题,而即便在中文中,稀疏的问题也会少了很多。

  •  词表占用的空间较小,模型体积可以做的比较小。这里提一下,所谓的词表并不是只能词级别使用的,本质上就是一个embedding矩阵和每个vector对应的是什么内容而已,所以无论下面是什么级别的表示,都统一使用这个概念。

  •  可以更好的适应卷积类的结构。

当然,凡事有利就有弊。使用字符去表示文本,相对于词级别的表示也引入了两个严重的问题:

 

  •   大幅度的加长了序列的长度:比如一个英文字符“How are you”,使用词级别去表示,这个序列长度就只是3,因为只有三个单词。但是如果使用字符级别去表示,序列长度就变成了11了,因为上面那个字符当中包含了11个字符(9个字母 2个空格,Character级别空格不可忽略)。由于字符的数目降低,序列的加长可能不会造成训练/推理时间的加长(因为单步复杂度降低),但是由于生成的序列变长了,由于一些类似长期依赖的问题出现,字符表示会造成生成的文本质量下降。
  •   使用字符表示文本,丢失了很多语法\语义\句法的信息。一个词本身承载了独特语义信息,承担了相应的语法句法角色,这些都是非常重要的,而如果使用字符级别去表示,则会丢失很多这方面的信息,同样的造成生成文本的质量下降。

对于上述的第二个问题,其影响生成的质量的大小,还是要具体语言具体分析的。比如在英文当中的词,词的序列都是原文本使用空格隔开的,隔出来的词都很准确。但是在中文中,由于我们说话并不加空格分词,再生成词序列的时候需要使用工具分词,这种分词无论如何也做不到绝对的准确,存在大量的噪音,这时候的如果使用字符表示中文,反而可以消除分词带来的噪音问题,并且因为中文一个词包含的字符数量并不多,Character-Level丢失的信息并不是特别多。

为了解决上述的1、2问题,并且保证能够彻底消除未登录词的情况下,有一些改进版本的方法,其核心思想也在于词表的构建。 比如在一个Word-Level Encoder-Decoder模型中,我们的词表大小构建到5W附近是一个非常不错的选择,保证速度的同时也不会有太多未登录词,但是如果切换到Character-Level的话,那么词表会瞬间少了个量级。于是我们可以利用这中间计算机的余力,去改进Character-Level的问题。

比如我们能够承受的词表上限大小是5W,而字符本身只有200个(假设的,英文差不多就是这个数字200好计算)的情况下,那么我们可以利用剩下的4.98万词表做一些什么事情:

  •   比如我们可以构建一些Subword Unit,所谓的Subword 就是一种介于Word和Character之间形式,Subword不是完整的一个词,但是又比Character要长,可以算一个二者的折中。例如BPE算法,BPE 能够根据语料中的字符的组合情况,在限定词表容量大小的前提下,构造出一个Subword词表,并且基于该Subword进行分词的语料,可以保证语料中不存在一个未登录词。
  •   Subword相对Character来说,主要的改进是降低了序列的长度,但是Subword毕竟是一种不太容易解释的符号,有时候总有种讲不通的感觉。所以这里也有另外一种折中的方法,就是在固定的词表大小中,首先放Character,比如上面的例子,我先把基本的200个字符放到词表,然后剩下的4.98万个放4.98个最常用的词。折中方法既解决了未登录词,也使得此表中的词都是可解释的。使用这种方式,通常需要在文本的读入和生成时做一些改进,并且比起上面的Subword来说,其实相关的研究工作反而不多。

另外,上面的方法依然能够彻底消除未登录词问题,降低序列长度,但是相应的,其解决稀疏词的能力会稍微差一些。

当然,未登录词也不是都那么可怕,由于长尾效应的存在,正常的词表都可以在一个合理的大小内容覆盖绝大部分的词,比如一般5W附近的词表可以保证一个语料库97+%的词都是登记在册的非未登录词,而剩下的系统不认识的未登录,都是“比稀疏词更稀疏的词”,并不是特别重要。此时对于对话也好、翻译也好,如果不知道这个词,找个其他词来冒名顶替一下也不是问题、或者通过其他手段避免他的生成,因为既然是稀疏词,那么肯定没那么重要。因此,这里说的第二类方法,只是缓解未登录词的出现几率,并且这些方法,有的方法是原作者刻意希望优化未登录词的出现,也可能是原作者没有提到,但其实也可以实现的。这一类方法的模式,通常是让我们的模型,能够使用更大范围的词表,从而降低未登录词的概率。而这类方法对于稀疏词的效果,则甚少关心,毕竟Char-Level可以大幅度解决稀疏的问题、Subword次之,Word-Level则连缓解都很难,大多只能靠足够的语料去解决这个问题。具体来说:

  •   Sampled Softmax: 这种方法加速了训练和解码的过程,降低了Encoder-Decoder计算中因为词表过而导致的相关开销,也就是降低了计算开销,从而让我们如何在内存允许的情况下词表尽可能的大。允许的词表大了,自然就能降低未登录词出现的几率。不能解决稀疏性问题。
  •   Dynamic Vocabulary:Dynamic Vocabulary动 态词表是AAAI 2018上的一个最新 ,其本身是希望针对每个需要生成的对话回复,都动态的创建一个专属的小词表,从而提升解码的速度和生成的质量。动态词表的构建方式 有趣,每一个动态词表其实都有包含一些一定会存在的词,这些词被称作功能词Function Words,顾名思义,就是比较重要的那些词,而剩下的功能词Content Words才是真正的动态预测出来的。其Content Words的预测方法,能够一定程度上的提升稀疏词被有效训练的概率,缓解了稀疏的问题。另外文章本身并不是针对未登录词, 但是其实我们可以应用这篇文章的思想,只要存储足够,总体上保留一个较大的词表,但是实际每次只使用一些特定小范围词表子集,降低相应的开销,也可以变相做到降低未登录词概率。

  •   低频词合并:之前关于未登录词的性质描述说过,很大一部分未登录词是一些稀疏词,也就是出现频率低的低频词。我们都知道经常使用的常用高频词的价值是远大于那些稀疏的低频词的,所以如果在表示这些词的时候,我们针对高低频词进行区别待遇,压缩低频词的表示,也可以一定程度的缓解未登录词的问题。比如说低频词的压缩方法,可以将几个低频稀疏词分箱到一起,Encoder-Decoder训练的时候,按照这个箱子训练,一来通过压缩词表大小以降低未登录词出现概率,二来低频词的弱弱抱团,知道抱团算法合理,可以使得他们受到训练的机会变多,从而缓解稀疏性问题。

 

 

结语

总而言之,针对Sequence2Sequence相关任务里面的未登录词问题,主要的解决方法大概的分为两类。 第一类是不使用Word-Level的表示方法,利用Character-Level/Subword-Level或者其混合的方式去解决未登录词问题。这一类方法通常能够彻底的解决未登录词问题并缓解稀疏词的问题,但因为丢失了词级别的信息,解决了未登录词问题后会引入一些新的问题。第二类方法则是保持以Word-Level表示不变,通过对模型的改进,使得词表的计算或者存储开销降低,从而缓解未登录词的问题。 这类方法并不能彻底解决未登录词问题,也很难缓解稀疏性的问题,但是其通常并不会和 第一类方法一样引入新的问题,实际的表现也很稳定很好。

其实,究竟要不要解决未登录词/稀疏词的问题仁者见仁,智者见智。比如说很多场景下,如果未登录词和稀疏词的 不是很要紧,那么可以把这问题放在一个较低的优先级去做,或者只是偏好性的选择一些顺手优化的模型(如第二类方法)就可以了。

参考文献

1、 Wu Y, Wu W, Yang D, et al. Neural Response Generation with Dynamic Vocabularies[J]. 2017.

2、 Wu S, Li Y, Wu Z. Low Frequency Words Compression in Neural Conversation System[M]// Neural Information Processing. 2017.

3、 Chung J, Cho K, Bengio Y. A Character-Level Decoder without Explicit Segmentation for Neural Machine Translation[J]. 2016.

4、 Sennrich R, Haddow B, Birch A. Neural Machine Translation of Rare Words with Subword Units[J]. Computer Science, 2015.

你可能感兴趣的:(关于端到端文本生成中的稀疏词与未登录词问题的探讨)