我上两个月参加了一个中文文本智能校对大赛,拿了17名,虽然没什么奖金但好歹也是自己solo下来的比赛吧,期间也学到了一些BERT应用的新视角和新的预训练方法,感觉还挺有趣的,所以在这里记录一下这期间学到的知识,分享一下自己的比赛过程,方案在此处:https://github.com/qftie/MiduCTC-competition 。这个赛题任务大概就是,选择网络文本作为输入,从中检测并纠正错误,实现中文文本校对系统。即给定一段文本,校对系统从中检测出错误字词、错误类型,并进行纠正。
任务定义
系统/模型的输入为原始序列\(X=(x1,x2,..,xn)\),输出为纠错后的序列 \(Y=(y1,y2,..,ym)\)X可能已经是完全正确的序列,所以X可能与Y相同。系统/模型需要支持多种粒度的序列,包括:字词、短语、句子、短文。
中文错误类型
一般包含三种,从字词到语义错误,难度依次递增
Soft-Masked BERT (ACL2020,字节跳动)
论文:Spelling Error Correction with Soft-Masked BERT
注意该模型只能处理输入序列和输出序列等长度的纠错场景!
模型简介:整个模型包括检错网络和改错网路:
- 检错网络是一个简单的Bi-GRU+MLP的网络,输出每个token是错字的概率
- 改错网络是BERT模型,创新点在于,BERT的输入是原始Token的embbeding和 [MASK]的embbeding的加权平均值,权重就是检错网络的概率,这也就是所谓的Soft-MASK,即 \(ei=pi∗e_{mask}+(1−p_i)∗e_i\) 。极端情况下,如果检错网络输出的错误概率是1,那么BERT的输入就是MASK的embedding,如果输出的错误概率是0,那么BERT的输入就是原始Token的embedding。
在训练方式上采用Multi-Task Learning的方式进行,\(L=λ·L_c+(1−λ)·L_d\),这里λ取值为0.8最佳,即更侧重于改错网络(Lc means correction)的学习。
模型结果:
该结果是句子级别的评价结果,Soft-MASK BERT在两个数据集上均达到了新的SOTA,相比仅使用BERT在F1上有2-3%的提升。
该模型处理错误的情况,主要有以下缺点,模型没有推理能力不能处理逻辑错误(语义错误),模型缺乏世界知识不能处理知识错误(地名等)
用MLM-phonetics纠错
2021ACL中文文本纠错论文:Correcting Chinese Spelling Errors with Phonetic Pre-training 论文笔记 - 知乎 (zhihu.com)
论文地址: paper
作者在论文中对比了MLM-base和MLM-phonetics的差异:
- MLM-base 遮盖了15%的词进行预测, MLM-phonetics 遮盖了20%的词进行预测。
- MLM-base 的遮盖策略基于以下3种:[MASK]标记替换(和BERT一致)、随机字符替换(Random Hanzi)、原词不变(Same)。且3种遮盖策略占比分别为: 80% 、10%、10%。MLM-phonetics的Mask策略基于以下3种:[MASK]标记替换(和BERT一致)、字音混淆词替换(Confused-Hanzi)、混淆字符的拼音替换(Noisy-pinyin)。且这3种遮盖策略分别占比为: 40%、30%、30%。
端到端文本纠错包括Detection Module和Correction Module2个部分,具体如下图所示,但官方没有发布预训练模型,paddle中提供了使用ernie1.0为backbone的模型:
GECToR
GECToR -- Grammatical Error Correction: Tag, Not Rewrite | Papers With Code
Seq2Edit模型简介:本文属于seq2edit模型,Seq2Edit模型只有Encoder,将GEC任务看作是一个序列标注任务,在每个Time-Step预测生成一个编辑动作。通过使用预测得到的编辑动作对源文本进行转化,我们便可以得到目标文本。属于一种序列标注模型,通过预先定义一些编辑动作,采用神经网络为句子的token打上编辑标签,从而进行语法纠错。
目前较为常用的Seq2Edit模型有PIE、GECToR等。以2019年Awasthi等人的并行迭代编辑(Parallel Iterative Edit, PIE)模型为例,它们使用的编辑动作有:复制、删除、增加、替换、变形等。其中,由于增加操作和替换操作需要在候选集中指定单词,所以实际上包含多种编辑操作。总体而言,Seq2Edit模型的编辑空间远远小于Seq2Seq模型的词汇空间,所以解码空间小了很多。此外,非自回归模型能够并行解码,速度优势巨大,比如GECToR 5次迭代比NMT beam-size为1还快接近一倍,并且是当前的sota。
Token级别的变换
原理
比较两个错误和正确句子的diff可以找到一系列编辑操作,从而把语法错误的句子变成语法正确的句子。为了给序列打标签,可以把编辑映射到某个token上认为是对这个token的操作。如果同一个token需要进行多个编辑操作,则需要采用迭代的方法给序列打标签。
比如上图的例子,红色的句子是语法错误的句子:”A ten years old boy go school”。
- 先经过一次序列打标签,找到了需要对ten和go进行操作,也就是把ten和years合并成ten-years,把go变成goes。注意:这里的用连字符”-“把两个词合并的操作定义在前面的Token上。
- 接着再进行一次序列打标签,发现需要对ten-years和goes进行操作,把ten-years变成ten-year然后与old合并,在goes后面增加to。
- 最后一次序列打标签在school后面增加句号”.”。
变换
上述的编辑操作被定义为对某个Token的变换(Transform),如果词典是5000的话,则总共包含4971个基本变换(Basic Transform)和29个g-变换。
基本变换
基本变化包括两类:与Token无关的和与Token相关的变换。与Token无关的包括\(KEEP(不做修改)、\)DELETE(删除当前token)。与token相关的有1167个\(APPEND_t1变换,也就是在当前Token后面可以插1167个常见词t1(5000个词并不是所以的词都可以被插入,因为有些词很少会遗漏);另外还有3802个\)REPLACE_t2,也就是把当前Token替换成t2。
g-变换
前面的替换只是把当前词换成另一个词,但是英语有很多时态和单复数的变化,如果把不同的形态的词都当成一个新的词,则词的数量会暴增,而且也不利于模型学习到这是一种时态的变化。所以这里定义了g-变换,也就是对当前Token进行特殊的变换。完整的g-变换包括:
- CASE类的变化包括字母大小写的纠错,比如$CASE_CAPITAL_1就是把第2(下标0开始)个字母变成对象,因此它会把iphone纠正为iPhone。
- MERGE把当前Token和下一个合并,包括MERGESPACE和MERGESPACE和MERGE_HYPHEN,分别是用空格和连字符”-“合并两个Token。
- SPLIT $SPLIT-HYPHEN把包含连字符的当前Token分开成两个
- NOUN_NUMBER把单数变成复数或者复数变成单数。
- VERB_FORM动词的时态变化,这是最复杂的,我们只看一个例子。比如VERB_FORM_VB_VBZ可以把go纠正成goes。时态变换使用了word forms提供的词典
预处理获得训练数据
我们的训练数据只是错误-正确的句对,没有我们要的VERB_FORM_VB_VBZ标签,因此需要有一个预处理的过程把句对变成Token上的变换标签。
1 token映射
把源句子(语法错误句子)的每一个Token映射为目标句子(语法正确句子)的零个(删除)、一个或者多个Token。比如”A ten years old boy go school”->”A ten-year-old boy goes to school.”会得到如下的映射:
A → A
ten → ten, -
years → year, -
old → old
boy → boy
go → goes, to
school → school, .
这是一种对齐算法,但是不能直接用基于连续块(Span)的对齐,因为这可能会把源句子的多个Token映射为目标句子的一个Token。我们要求每个Token有且仅有一个标签,所以这里使用了修改过的编辑距离的对齐算法。这个问题的形式化描述为:假设源句子为\(x_1,…,x_N\),目标句子为\(y_1,…,y_M\),对于源句子的每一个Token \(x_i(1≤i≤N)\),我们需要找到与之对齐的子序列\(y_{j_1},…,y_{j_2}\),其中\(1≤j_1≤j_2≤M\),使得修改后的编辑距离最小。这里的编辑距离的cost函数经过了修改,使得g-变换的代价为零。
2 找出token变换
通过前面的对齐,我们可以找到每个Token的变换,因为是一对多的,所以可能一个Token会有多个变换。比如上面的例子,会得到如下的变换:
[A → A] : $KEEP
[ten → ten, -]: $KEEP, $MERGE_HYPHEN
[years → year, -]: $NOUN_NUMBER_SINGULAR, $MERGE_HYPHEN
[old → old]: $KEEP
[boy → boy]: $KEEP
[go → goes, to]: $VERB_FORM_VB_VBZ, $APPEND_to
[school → school, .]: $KEEP, $APPEND_{.}
3 保留一个变换
只保留一个变换,因为一个Token只能有一个Tag。但是有读者可能会问,这样岂不是纠错没完全纠对?是的,所以这种算法需要多次的迭代纠错。最后的一个问题就是,多个变换保留哪个呢?论文说优先保留KEEP之外的,因为这个Tag太多了,训练数据足够。如果去掉KEEP还有多个,则保留第一个。所以最终得到的标签为:
[A → A] : $KEEP
[ten → ten, -]: $MERGE_HYPHEN
[years → year, -]: $NOUN_NUMBER_SINGULAR
[old → old]: $KEEP
[boy → boy]: $KEEP
[go → goes, to]: $VERB_FORM_VB_VBZ
[school → school, .]: $APPEND_{.}
模型结构
类似BERT的Transformer模型,加两个全连接层和一个softmax。根据不同的Pretraining模型选择不同的subword切分算法:RoBERTa使用BPE;BERT使用WordPiece;XLNet使用SentencePiece。因为我们需要在Token上而不是在subword进行Tag,因此我们只把每个Token的第一个subword的输出传给全连接层。
迭代纠错
前面介绍过,有的时候需要对一个Token进行多次纠错。比如前面的go先要变成goes,然后在后面增加to。因此我们的纠错算法需要进行多次,理论上会一直迭代直到没有发现新的错误。但是最后设置一个上限,因此论文做了如下统计:
基本上两次迭代就能达到比较好的效果,如果不在意纠错速度,可以到三次或者四次。
实验
3-stage training
本文中,训练分为三个阶段:在合成数据上的Pretraining;在错误-正确的句对上的fine-tuning;在同时包含错误-正确和正确-正确句对数据上的fine-tuning。
有第三步让模型看懂一些没有语法错误的句子是很重要的,实验也说明第三步使得结果好了很多;最后一行表示加上一些推理的trick,具体如下
推理的trick
- 给$KEEP增加一个bias
- 因为大部分的句子错误较少,而训练时错误的却居多,所以要给它加一个bias
- 增加最小的错误概率阈值
- 因为模型会尽量纠错,即使概率很少。这里增加一个句子基本的概率值,如果小于它则不纠错。
这两个值是使用验证集找到的。从上图的结果可以看出,使用了推理trick后效果提升不少。
预训练模型
RoBERTa和XLNet比较好,GPT-2和ALBERT较差,文章认为因为是生成模型
性能提升技术
重排序 TODO
与其它集成在模型内部的性能提升手段不同,重排序(Reranking)更像是模型预测完成之后的一个独立的阶段,所以它被称为一种后处理方法(post-training)。它的目的主要是为了解决:模型预测得分最高的结果,往往并不是最好的结果。
它的主要做法是:将GEC模型输出的N个最好的结果作为候选集,使用一些在GEC模型中无法被很好地覆盖但却又较为重要的特征,对这N个最好结果进行重新排序,选取得分最高的结果作为最终的预测结果。
通过使用重排序方法,我们可以引入丰富的语言学知识,考虑更多全局的特征,还能集成多个GEC模型的输出一起重排序。
常用的重排序特征有:1)语言模型得分;2)编辑距离特征;3)句法特征。
模型集成 TODO
模型集成(Model Ensemble)也是当下最为常用的性能提升手段之一,它的做法主要有:1)在Beam-Search解码阶段,将多个模型的输出取平均;2)在输出预测结果阶段,采用多模型投票的方式确定编辑操作等。
迭代纠正
同人类一样,机器对一个句子进行语法纠错往往也无法一次就找到所有的错误,所以,迭代纠正(Iterative Correction)的思想应运而生。这一方法的主要思想是:对一个含有语病的句子进行多轮纠错,直到评判句子正确程度的某种指标达到指定的阈值。比较典型的一个例子是微软亚洲研究院在2018年提出的Fluency Boosting模型。
修改损失函数
一种更直接的性能提升方式,是修改模型的损失函数。
例如:GEC任务中,输出结果的大多数Token与输入文本是相同的,并不重要,而那些产生了差异的Token理应受到更多的关注,所以我们应该提升这些产生差异的Token在损失函数中所占的权重,才能让模型更好地捕捉信息。
数据增强
人工生成的平行语料主要有两种使用方式:1)直接与真实数据集相合并,一起进行训练;2)先使用人工平行语料对模型进行预训练,再将预训练的模型使用真实数据集进行微调。由于人工数据的分布往往与真实数据不一致,所以将人工数据用于预训练阶段能够收获更好的性能,当下绝大多数基于神经网络的GEC模型都采用这一方式。
噪音生成
噪音生成的思想来自于预训练阶段常用的降噪自编码器(DAE)。例如:猿辅导研究院的Wei Zhao等人提出采用随机制造错误数据的方法来构建伪数据,具体流程如下:按照10%的概率随机删除一个词;按照10%的比例随机增加一个词;按照10%的比例随机替换一个词;对所有的词语序号增加一个正态分布,然后对增加正态分布后的词语序号进行重新排序后得到的句子作为错误语句。
噪音生成的具体做法有很多,目前比较好的方法是预先统计真实数据里各类型错误的分布及概率,再根据这一分布生成噪音,从而使人造数据尽可能地接近真实数据地情况。
通过将加入噪音的句子纠正回原本的句子,我们可以以一种无监督的方式对模型进行预训练,这种做法即为降噪自编码器,能有效提升模型性能。(这种方式有些类似PERT的做法,即打乱正常语序的句子而非【MASK】,让语言模型学会重新生成正确的句子)
比赛思路分享
模型
以GECToR作为baseline模型,我的方案主要是在赛题的baseline上进行更改,可参考GECToR论文和GECToR源代码
backbone则替换为了hfl/chinese-macbert-base
训练说明
该模型训练按GECToR的论文所述,尝试两个stage和三个stage的训练方法,由于验证下来两个stage显著优于只用伪数据训练,而三个stage相对两个stage提升不大,所以选择了两个stage的训练方式。
Stage1
第一个stage先在100w条样本的伪数据上进行训练,将训练得到的在preliminary_val.json
上效果最优的权重作为stage2的预训练权重。这里直接将第一个stage训练得到的权重等文件保存在pretrained_model/ch_macbert_base_epoch5,step1,testf1_39_41%,devf1_67_26%
,方便stage2的调用。
Stage2
第二个stage使用pretrained_model/ch_macbert_base_epoch5,step1,testf1_39_41%,devf1_67_26%
作为预训练权重,使用合并的初赛和决赛数据合并的data/final_train_fusion_stage2_3.json
数据集,分为十折来进行训练和验证,最后选取的是验证集表现最好的两组权重平均考虑其预测,生成最后得分Fscore=51.89
的提交文件。
调优和trick搜索
trick
在a榜b榜的提交过程中尝试了不同的trick均未有明显提升所以最后没有使用其他trick(尝试过的trick有迭代纠错、使用detect输出判断整句话是否有错,如果最大检错概率小于一定的阈值则认为该句没有出错直接跳过,测试记录可见提交结果记录文档)
backbone
在stage1尝试过roberta-base、macbert-base、pert-base、macbert-large,调优后发现macbert-base效果较好,个人觉得应该是因为macbert预训练就是使用了错字或者span替换等策略和gec中出现最多的错误类似,pert则是使用的语序打乱复原的预训练方式,可能对于乱序的错误的错误更有效果,也有考虑融合不同模型的优势,但由于时间问题没有尝试,但不清楚为什么large大模型反而效果更差,也许是因为没有足够的计算资源尝试lr调优
参考:
ERNIE for CSC:【的、地、得】傻傻分不清?救星来了! - 飞桨AI Studio (baidu.com)
(4 封私信 / 8 条消息) 目前NLP中文文本纠错(错别字检索,修改)有什么研究? - 知乎 (zhihu.com)
文本纠错的论文看这一篇就够了 - 知乎 (zhihu.com)
竞赛大神易显维:带你深度认知校对问题_哔哩哔哩_bilibili