在构建了基于n-gram的纠错检错模型之后,我们自然不能放过如今大红大紫的神经网络,鉴于神经网络的灵活性和训练的耗时性,我们在方法尝试和模型训练上花了很多时间,期间走过不少弯路,也因工业界大佬进行交流走了捷径,总得来说,神经网络的表现虽然没有我预想的那么神奇,但也还是可以的。
本文介绍使用LSTM进行检错纠错的几个方案,需要注意的点以及和n-gram模型进行最终融合的尝试。
整体分为四个部分:
- 备选词生成(词级语言模型)
- 句子检错模型:
- 序列标注模型
- 字级语言模型
- tricks和坑
- 模型融合
需要提醒的是,鉴于神经网络的不稳定性以及数据分布的多样性,因此在本文提出的方法不一定适用于读者的数据,仅供参考。此外,为对项目进行保密,文中不会给出具体的准确率、F1数值,只会以好、差等程度词表明模型结果。
备选词生成(词级语言模型):
Part 0 : 介绍
我们首先使用LSTM构造的是词级语言模型,目的是为了取代从一对词语搭配中获取备选词的方法,而检错方法还是使用n-gram。 LSTM构造的语言模型和n-gram语言模型本源是一样的,都是通过前面的文本获取下一字或词的概率,只不过n-gram是基于统计的,只能人为设定有限的n,而LSTM能够综合一个词前面所有词的信息给出概率。
经过在项目通过各种角度使用语言模型,我总结出语言模型使用的三种思路:
- 检测句子流畅度
- 检测错误词语
- 生成备选词(句子)
这三个思路使用的语言模型的原理是一样的,只是使用的角度不同。检测句子流畅度是扫描整个句子,计算每个字/词的概率而后乘起来(鉴于字词概率都会远小于1,导致乘起来后值过小,一般语言模型都会采用log概率,即计算出概率后再取log),将乘出来的数成为句子流畅度。检测错误词语就是通过处理前面的文本,计算下一个已知的词的概率,若是概率低于阈值(阈值一般是人工设定),那么就可以标记成错误词。而只要将下一个词扩展成词表,对词表里每个词都求一个概率,然后取出概率最大的词,那么我们就可以做到备选词生成了,若是不断把概率最大的备选词填入原本句子中再输入语言模型,就可以做到句子生成了。
例如,对于一句话 "STA 我 今天 吃 苹果 了 END":
- 检测句子流畅度的话,就是计算 "STA"后是"我"的概率,【"STA", "我"】后面是"今天"的概率。。到 【"STA", "我", "今天", "吃", "苹果", "了"】后面是"END"的概率,而后将所有概率取log乘起来,得到句子流畅度分数。
- 检测错误词语,同样也是计算一遍所有词语的概率,而后将他们放到一起,根据认为设定的阈值筛出概率较低的词。
- 生成备选词,则是首先扫描到例如 "STA 我 今天 吃",而后计算词表中每个词的概率,即 若是将一个词接到前面扫描的部分的后面,那个词的概率,若词表有【"STA", "我", "今天", "吃", "苹果", "了", "END"】这么些词,那就是计算【"STA 我 今天 吃" + "STA"】的概率,【"STA 我 今天 吃" + "我"】的概率。。。,【"STA 我 今天 吃" + "END"】概率,每个加进去的词都会得到一个概率,若是取概率最大的词出来即可作为备选词了。
Part 1 : 模型模块详述
在这个部分,我梳理一下常用的用神经网络处理文本问题的流程,下图是在lstm后接入词表大小的全连接层的整体流程图:
整个过程分为以下五个部分(省略预处理过程):
- tokenizing
- indexing
- embedding
- lstm
- 下游任务
接下来我将对每个部分进行介绍。
1. tokenizing
tokenizing,从词义和上图中就可以知道,就是分词,而为什么单独将它拿出来,则是因为它具有独特的意义:
一:统计词数、词频。通过分词之后,我们能够清楚地了解语料中总共有多少词以及每个词的频率,那么我们就可以根据词频进行删减,例如删掉大量只出现几次的自造词、语气词或罕见词,以此节省计算内存并减少需要计算的参数;或者将频率过高的停用词替换成统一的标签,防止停用词对模型造成影响。
二:构造 "词"-> "索引"表。在分词的过程中工具还会构造 "词"-> "索引"表,这个表是做什么用的呢?要知道模型内部是不认字符的,只认数据,为了将词转换成模型可以处理的向量,就需要先将它们转成索引,而后通过 "索引"->"向量"矩阵直接查到对应的embedding向量,就可以成功地把词转换成向量了。而我们也可以构造反转表: "索引"->"词",就可以在最后得到概率最大的词的索引时查表得到词的字符表示。
三:选择粒度。在英文中,词之间以空格分离,所以英文处理就没有分词方面的困惑(应该很少有人会把句子按字母粒度分开吧?)。而中文因为没有空格区分,因此可能需要进行分词的操作,然而因为中文的多义性,分词的结果不一定能让人满意,有可能引入额外的错误使得模型跑偏,而字级粒度就可以很好地防止分词错误的问题了。但词级也同样有其优点,鉴于错误很可能产生散串,词级在检测散串上更有优势。选择哪种粒度就见仁见智了,不过选择的阶段就是在tokenizing部分进行的。
2. indexing
indexing要做的事情很简单,就是利用tokenizing阶段产生的 词-索引 表,把句子中的词替换成索引,而这个阶段也同样有其额外的功能,那就是padding。
padding这个技巧在文本处理中十分常见,含义就是填充,将句子通过填充达到一定长度。原因很简单:句子一般是不等长的,而矩阵要求每行元素等长,这个矛盾的解决那就是通过引入padding,通过在不等长的句子后面补数据(一般补0),从而把indexing后的句子补到统一的长度(称为max length),这就可以以batch进行训练了。当然,如果你每次只用一个句子训练,那就不用padding了,不过速度就可想而知了。那么有人可能会说:padding最起码会加长计算时间吧,如果是双向LSTM,还应该会影响训练效果吧,可以尽可能消除padding的影响吗?答案是可以的,起码在tensorflow中,就可以用dynamic rnn,通过sequence length参数将batch里每个句子的长度传进去,就可以实现每个句子不等长训练了(不过句子在传进去之前还是要padding成同一长度的)。
3. embedding
前面提到,模型不认字符,只认数字,那么我们就需要将字词转换成向量(至于为什么不是数字,那是因为单个数字所能保存的信息量太少,远不如多维向量能表示的信息多)。而这个向量,可以通过随机初始化而后自己训练,也可以借用其他人训练好的embedding再进行磨合(不过慎用,tricks和坑部分会谈到)。常用的embedding自然是word2vec了,不过近来不断有新的out-performing的embedding出现,如ELMO、Google的大杀器BERT。
4. lstm
这个阶段就是进行语言模型的建模了,LSTM每阶段有两个向量:hidden state和output,因hidden state中包含从开始到最后语言模型的记忆,所以我们选择使用final hidden state作为该阶段的输出物,使用hidden state实际上就是将前面所有词的信息(但有些信息会被模型遗忘)用一个向量表示(就是seq2seq的Encode阶段),而后就可以用这个浓缩的向量进行各种下游任务了。
5. 下游任务
下游任务的定义其实颇为灵活,若是对于训练embedding为主的模型,那么下游任务就是在embedding后的模型层,而对于我们的这个语言模型,下游模型层自然是lstm后的网络层,具体就是词表大小的全连接层。
虽然我们在前文讲到语言模型的三种用法的时候是先提到计算单个词语概率以此检测错误词语,再扩展到计算整个词表概率以此生成备选词,但三种用法的模型层都是一样的,都是词表大小的全连接层。对于检测错误词语的用法,只需要在词表所有的概率中取出要检测的词的概率即可。
Part 2 : 文本获取与处理
在这个模型中,我们使用的语料和上一篇文章(基于n-gram的纠错检错模型)一样:
- 网上获取的搜狗通用语料
- 金融新闻语料
- 人工纠正语句
语料处理过程的一部分也如前一篇文章那样,分词-数字转星号-长句切短句-加入STA、END标签。
而后我们统计了语料中的词频并将过低的词以UNK替换,并删去过高的词。
Part 3 : 模型构建及评价
模型训练工具包上,鉴于笔者当时是首次用神经网络处理实际项目问题,先前都是用tensorflow跟着教程或写写小demo,因此为了效率和准确使用了封装较好的keras,而keras当时的版本没有dynamic rnn,因此padding后的整个句子都会被lstm处理。
将模型在语料上满怀期待地进行训练过后,因为备选词生成没有标准答案,只能人工判断是否正确,于是我开始输入不完整的句子让模型给出备选词来检测模型结果,然而遗憾的是模型生成的基本都是牛头不对马嘴的词,或者是为了通用性用了很常见的词。尝试了数套参数和配置之后结果都差不多,我开始感觉到实际工业界和学术界似乎有些不同,但又不确定是否是我本身实现的问题,因此暂定备选词仍然采用依存句法提取的词语搭配,开始转为使用神经网络做句子检错。
Part 4:模型转移,从备选词生成到词级检错
上文提到,备选词生成和错误检测使用的模型是一样的,只是使用模型的方法不同。将词级模型从备选词生成转换成错误检测并没有花太大的功夫,在LSTM中从只取final hidden state变成每个time step的hidden step都要取,在原本给出的整个词表中根据概率取概率最大的词变成根据索引取特定词的概率。然而模型在检错方面的准确率,F1等指标表现很差,远不如n-gram的分数。鉴于经验不足,笔者在尝试了几套参数和配置之后开始尝试下一个模型,至于可能的表现不足的原因,笔者在后面会给出说明。
序列标注模型
Part 0 : 介绍
先前的使用词级语言模型的思路是师兄给出的,在尝试词级语言模型效果不理想之后,笔者提出可以将检错问题视作序列标注问题,正确的标0,错误的标1,将句子词或字序列输入模型后根据预测值得到错误字词。在阅读论文之后,笔者得知这也是文本检错的一个常规思路,也是很符合直觉的。
序列标注模型实际上就是个多分类模型,当问题是检错的时候就是个二分类模型,它和其它数据型分类模型的区别在于构建特征的时候需要考虑时序特性,就是利用LSTM将文本序列信息嵌入到特征向量中。
Part 1 : 模型模块简述
将序列模型的模块和语言模型的对比我们可以发现,两个模型还是有些区别的,区别在embedding后面的两个模块。
在lstm上序列标注模型使用了双向LSTM,在取hidden state的时候并不是仅取最后的hidden state,而是将每个阶段的hidden state均取出,至于forward和backward的hidden state是sum还是concat就见仁见智了。
最后的全连接层使用的是两个神经元,将每个time Step的hidden state作为特征预测这个词是否有错。
Part 2 : 训练集处理与构造
使用序列标注模型,训练集就不能像使用语言模型那样直接把整个句子丢进去就行,需要获取标注好的语料或者人为构造标注好的训练语料。至于该模型的训练集获取和构造,详见拼音型简单错误语料获取与处理一文。
Part 3 : 模型效果评价
模型训练既然不必多言,在实际跑出来之后进行测试,效果仍是惨不忍睹。而后笔者再搜罗各种检错纠错的论文阅读,从其中挖掘了使用序列标注模型时可以改进的方向和常用的方法,在正欲尝试改进时发生了一些事情(见"字级语言模型"Part 0),使得笔者留下序列标注改进这么个坑,构建新的模型去了。
Part 4:可扩展方向
bilstm-crf模型
经过阅读论文笔者发现,序列标注模型,尤其检错纠错的序列标注,最常用的是bi lstm-crf模型,通过lstm对每个字的每个类给出分数,将所有分数输入crf模型中,求解概率最大的标注序列。
加入词性特征和拼音特征
拼音特征的引入是为了更好地识别同音字错误,而词性特征的引入和语法正确性相关,笔者认为还跟错误产生的散串有关。散串理论认为当句子中有错误字、词时,分词后的句子会产生不协调的散串,这些散串以单字分开,它们和前后无法构成词因此独自成词,而它们本身单独成词很少见或者无法单独成词。根据这个理论,当句子中产生散串时,它们的词性序列就会很异常,从而被模型捕捉并识别。
字级语言模型
Part 0 : 介绍
在评估完一号版本的序列标注模型后,我有幸参加了MSRA-SCUT机器学习夏令营,并在夏令营最后一日前往CVTE参观。在参观途中,我有幸和CVTE的自然语言部门的技术负责人就语音识别后文本纠错领域进行一对一交流,在交流过程中他告诉我,经过他们在工业界的实验,效果最好的是使用双向LSTM的字级语言模型,而生成式模型也仅限于在学术论文中好看了,在实际工业界的效果并不好。
经过大佬的指点,我们又重回语言模型检错方案,只是这次使用字级粒度,在实验过程中,尽管在大方向上有了大佬的指导,但还是踩了不少坑,在后面的"tricks和坑"部分会详述。
Part 1:模型模块简述
字级语言模型的模块有些像前面两个模型模块的组合,在LSTM方面使用双向LSTM,取每个阶段的hidden state,然后把hidden state作为特征进行词表大小的概率预测,根据索引查找对应词的概率,并和阈值比较判定是否认为出错。
Part 2:训练集构造
训练集和词级类似,但为了尝试不同语料对模型的作用,笔者又获取了金融领域的保险推销对话、电商平台客服对话,并分别构造训练集用于构造不同的模型。
语料的处理方式也和前文词级语言模型提到的一样,区别在于不需要进行分词处理,直接一字一断,不过要注意不要把STA、END、UNK标签截断了。
Part 3:模型构建及评价
经过前两个模型实现的训练,笔者对神经网络实际应用开发有了进一步了解,这次字级语言模型就使用tensorflow1.2开发了,经过艰苦的debug以及尝试各种配置的过程,最终模型跑出来结果还不错,算是有了份可以见人的成果了,起码在检错效果上将网上大厂公布的语言模型和检错模型能比下去。
Tricks 和 坑
在这个部分,我将总结前面几个模型构建和训练过程中使用的一些tricks和踩过并意识到的坑(可能还有没意识到的)。
tricks
tensorflow使用方面的tricks 见文章 tensorflow 文本序列检错的tricks (0)
数据尽可能保存成文件
神经网络的训练本身就已经很耗时间了,我们不希望前期的数据处理再占用大量的时间,如果不保存成文件,那么模型每次在训练前都需要对语料进行处理,想想就耗时。
鉴于笔者采用的是tensorflow1.2,还不支持各种高级的DatasetAPI,因此各种数据以文本形式保存下来,其中涉及到的数据以及形式有:
- 词-索引表。保存了这个表,那么模型就不需要再将整个语料过一遍以获取完整词表,此外我们还可以根据词表大小进行词删减等操作。
- embedding。这个不必说,必定要保存的。
- padding后的句子索引序列。 在前期处理阶段就将语料转换成序列(此时最好保存成一份文件),而后对不等长序列进行padding,补到最大长度,保存补后的索引序列,那么模型之后就能够直接读入并构造矩阵了。而前面小括号内推荐额外保存不等长序列是为了之后想要修改最大长度或padding使用的数据会更方便。
大佬指点
在与CVTE的大佬交流后,笔者还获取了一些tricks,不论用没用到,在此分享出来:
- 神经网络和n-gram同样也仅能捕获句子的简单词语错误,因为短句子稍有些错就很难还原原本的意思。
- 句子是长是短没什么影响,按逗号句号把一个长句子划分成短句子的方向没问题。
- 用双向lstm,候选集词语挑选用搜索引擎的ranking技术。
- 加速训练方面:
- 富采样
- batch normalization
- 多显卡下用显卡并行加速训练
- 提高模型准确率的方法(比赛中常用的技巧):
- 先设置embedding不动而后训练模型参数,然后设置参数不变训练embedding,只进行一轮
坑
训练数据和测试数据同分布
训练数据的测试数据尽可能同分布,在文本中就是指词语的组合方式、用词、错误类型都差不多,例如都是电话中的对话,都是拼音类型为主的错误等等。
这在学术界看来再正常不过,毕竟数据集都是纯粹的,直接随机划分训练集验证集测试集即可。在工业界也简单,直接雇佣大量人手从要检测的大量语料中先纠一部分作为训练语料就行。然而苦的是夹在学术界和工业界之间的我们,既没有学术界那种那么纯粹而大量的数据,又没有工业界的财力可以先"人工"再"智能",于是只能人工纠正一小部分,再靠网络搜集尽可能相似分布的数据。然而经过实验表明,哪怕最好的相似数据,效果也不如人工纠正的那小部分数据。
embedding不要乱用
笔者之前很大一段时间都是直接使用其他人发布的已经预训练过的embedding,认为只需要在他们训练好的基础上针对我们的语料进行更新即可,然而现实给了我个下马威,在将预训练embedding换成随机初始化embedding之后,模型的准确率和F1瞬间上了一个台阶。在拥有足够数据的情况下,或者哪怕数据量不是那么够,也尽可能自己训练embedding吧,领域不匹配的话,其他人预训练好的embedding对你的模型来说就是毒药。
检错模型评价
在检错模型评价环节,笔者将对前面提到的三个模型进行对比,分析优劣并给出评价。
比较分为两个部分:
- n-gram VS lstm
- 序列标注模型 VS 语言模型
- 词级粒度 VS 字级粒度
n-gram VS lstm
在实际进行项目之前,笔者一直受到神经网络鼓吹者的影响,感觉神经网络仿佛已经万能了,传统机器学习方法都很过时,效果都远不如神经网络。然而,现实告诉我,方法在准确率上是和冷门热门无关的。现在转头一看,n-gram和lstm实际上各有优劣。
我们来看看n-gram和lstm相比的优势:
- n-gram在训练测试同分布的要求较低。使用搜狗通用大规模语料训练出来的n-gram语言模型表现已经不错了,而同样的大规模语料在lstm上却是惨不忍睹,因其和测试语料的分布不同,导致模型无法学习到需要学习的特征。
- n-gram在散串上的识别很强。同样是词级粒度,当文本中出现错误导致产生若干散串之后,n-gram能够很敏捷地察觉出来,然而LSTM较难做到。
- n-gram解释性强,原理清晰。这个自然不必多言,从理论到实际使用上经历许多年许多人的磨砺,这个语言模型早已身经百战。
然而LSTM也有其优势的,不然也无法红火到今日:
- 人工参与较n-gram少,毕竟神经网络最大的优势就在于特征自动学习,再加上如今各种高度抽象的深度学习工具包,只需要寥寥数行代码就可以构建一个神经网络。
- 语料数目要求不高。写到这里笔者内心其实也有些犹豫,毕竟神经网络本身要求数据量要足够大才能玩得转,然而经过实验,层数较少的神经网络在语料较少而分布接近的情况下也同样表现很好,而基于统计的n-gram若想要得到同样表现,就需要数倍数十倍于LSTM的语料用于训练了。
序列标注模型 VS 语言模型
首先用训练集进行对比:
- 序列标注模型语料需要使用标注好的数据
- 语言模型需要使用正确的数据
高下立判。序列标注模型需要标注好的数据才可以训练,对学术界而言自然不是问题,但对于工业界和夹在工业学术之间的我们显然不希望在构建数据上花费大量时间和精力。而语言模型只需要使用正确的句子就可以,而正确的句子在网络上到处都是,Wikipedia、百度百科、知乎什么的,一爬一大堆,虽然还是要注意分布相似性问题,但序列标注模型也同样需要考虑。因此语言模型在训练数据准备上无疑优于序列标注模型很多。
而后根据模型复杂度进行比较:
- 序列标注模型最后的全连接层只需要和类别数目相同的神经元,在本文中即两个神经元,分别代表正确和错误。
- 语言模型最后的全连接层需要词表大小的神经元。
在模型复杂度方面序列标注模型要占优势,毕竟词表少说也有上千上万个,需要更新的参数数目瞬间就多了几个量级。
最后将模型用途进行比较:
- 序列标注模型仅能用于标注。
- 语言模型可以用于检测句子流畅度、检测错误词、生成备选词。
用途一比,语言模型又大胜,序列标注模型仅专注于标注,而语言模型可以一模型多用,最后全连接层使用词表大小的神经元大大增加了语言模型的能力。
如此对比一看,序列标注模型或许更适合在学术界使用,毕竟他们拥有大量纯粹的数据,而语言模型更适合工业界使用超大量语料训练,得到一个核弹级多功能通用模型。
字级 VS 词级
在大佬的指点下我们从词级语言模型到字级语言模型,使用发现效果很不错之后,我们回头开始比较词级和字级的区别,分析为什么字级模型优于词级。
- 字级词表较词级小。显然,中文常用字就那么几千个,因此字级若再压缩的话可以将词表控制在两三千的规模。而词级因组合方式多样,再加上专有名词、成语等因素存在,导致常用词的词表都可以轻松达到上万规模。词表越大,需要更新和计算的参数越多,消耗的内存空间也越大,而词级的词表因规模大,更可能稀疏,致使效果不好。
- 词级需要分词,分词不总是完美的。在使用词级模型时,我们需要对语料进行分词处理,在分词工具上使用较为广泛使用的jieba分词,但分词工具总会有不完美的地方,尤其是作用在对话的文本中,其中包含大量重复和倒装的结构,导致模型在训练阶段就受到不少干扰。
然而,词级模型还是有它的优势的,在备选词生成上,字级模型生成备选词的效果就不如词级。
检错模型融合
在获取了n-gram词级语言模型和LSTM字级语言模型后,我们思考能否将它们融合起来以获取更好的效果。经过实验,我们将句子分别输入两个模型,将词语的分数标记给词中每个字,而后取两个模型给出分数的最高值,再和阈值比较高低判定是否可能出错。
句子纠错
在检错方面得到一定成果之后,我们开始结合检错和备选词生成进行句子纠错,其核心思想用一句话来描述就是:
用语言模型检错 -> 备选词替换 -> 语言模型检测句子流畅度,挑选流畅度最高的句子。
虽说一句话就能将核心思想描述了,但内部还是有不少细节的。
在语言模型方面采用的是融合之后的n-gram + 字级LSTM,备选词生成采用依存句法提取的词语搭配对,流畅度检验使用了百度训练的语言模型。
在使用语言模型检错之后,我们还需要将句子分词,获取检错模型认为错误的词语的前置词,而后用前置词根据词语搭配得到若干备选词,将备选词替换错误词后得到若干句子,包括原始句子和几个不同备选词替换后的句子,将这几个句子输入语言模型中获取句子流畅度,选择流畅度最高的句子作为最后输出。
最后使用语言模型检测句子流畅度这一操作,能够非常有效地把在检错阶段对全对的句子的误判消除,挑选备选词替换后的句子流畅度都不如原始的句子。这种召回机制能够有效防止正确句子经过模型之后反而变成错误的了。
感悟
这是我进入大学后历时最久也是最大的项目,在这个项目进行的几个月中,我从一个对机器学习只了解皮毛的大二半新生,变成对机器学习稍有了解的大三老油条。这个项目使我离实际工业界进了一步,使我明白了不能只看最新最火技术,传统技术同样有可取之处的道理,也使我对自然语言处理这个领域有进一步了解和兴趣,或许还要沿着这条路继续向下走。
更多实战文章欢迎关注微信公众号【AI实战派】
我的个人博客:Zedom1.top