原文链接 Youdao’s Winning Solution to the NLPCC-2018 Task 2 Challenge: A Neural Machine Translation Approach to Chinese Grammatical Error Correction
官方技术博客 http://techblog.youdao.com/?p=1281
NLPCC 2018中国语法错误纠正任务(CGEC),旨在寻找发现和纠正非汉语母语者所写中文论文中语法错误的最佳解决方案。这篇文章描述了有道NLP团队在这次挑战中获得第一名的方法。总的来说,我们把这个问题看作是一个机器翻译(MT)任务。我们使用分阶段的方法,针对特定的错误设计特定的模块,包括拼写、语法等。该任务使用M2 [5]的得分来评估每个系统的性能,我们的最终解决方案实现了最高的召回率和F0.5。
关键词:Grammatical error correction · Machine translation
中文是全球使用最多的语言。随着经济全球化的趋势的发展,越来越多的非汉语母语学者开始学习中文。然而,中文也是世界上最古老最复杂的语言之一。她的拼写与语法和其他的语言有着极大的不同。例如,不像英语或者其他西方语言,中文没有不同的复数和时态形式。此外,汉语中的叠词比在英语中更常见。由于这些差异,非汉语母语学者很容宜在使用中文时范一些语法错误。汉语有效语法错误纠错(CGEC) 系统可以为学习者提供即时的反馈,在学习过程中具有重要的价值。
然而,相对于英文语法纠错,中文语法纠错的研究相对较少。相关资源也很少。NLPCC 2018的CGEC任务,为研究员提供了平台和数据来更深入的研究这个问题。她的目标是发现并纠正非以汉语为母语的人所写的中文论文中的语法错误。通过计算系统输出序列与黄金标准之间的重叠来评估性能。
有道的NLP团队一直在积极研究语言学习技术,这是该公司利用人工智能推动在线教育的更大努力的一部分。通过对这个问题的仔细分析,我们使用了一种三级方法来处理它:首先,我们从输入中删除所谓的“表面错误”(例如,拼写错误,稍后再详细说明)。然后,我们把语法纠错问题转化为一个机器翻译任务,并应用了一个Seq2Seq模型。我们使用不同的配置为第二阶段构建了几个模型。最后,将这些模型组合起来生成最终的输出。通过仔细的调优,我们的系统获得了最高的召回率和F0.5,在任务中排名第一。
本文描述了我们的解决方案。主要内容如下:第二节接入了任务,以及相应的数据格式。第三节综述了GEC方向已有的工作。第四节阐述了我们的整个系统是怎么运行的。第五节呈现了评估结果。第六节作了总结。
虽然汉语语法错误诊断(CGED)任务以及提出来几年了,但这是第一次将纠错加入了挑战。CGEC的任务是检测和纠正非汉语母语者所写的中文论文中的语法错误。该任务提供带标注的训练数据和未标记的测试数据。每个参与者都需要提交测试数据的正确的修改数据。训练数据包括汉语学习者写的句子和母语为汉语的人修改的更正句。需要注意的是,这些句子可能有0∼N更正后的结果。具体来说,训练数据中原句和更正句的分布情况如表1所示,典型的数据示例如表2所示。
该任务使用M2 [5]得分来评估每个系统的性能。它根据正确的编辑、gold编辑来评估短语级别的更正系统,并使用这些编辑来计算每个参与者的F0.5。
语法纠错(GEC)任务自2013-2014年的CoNLL共享任务以来受到越来越多的关注。大多数早期的GEC系统为不同的错误构建特定的分类器,并将这些分类器组合成一个混合系统[11]。之后,一些研究者开始将GEC视为一个翻译问题,并提出了基于统计机器翻译(SMT)模型的解决方案[2]。一些通过改进的SMT获得了相当好的结果[3]。近年来,随着深度学习的发展,神经机器翻译(NMT)已经成为机器翻译的新范式,在翻译质量上远远超过了SMT系统。Yuan和Briscoe[16]将NMT应用于GEC任务。具体来说,他们使用了一个经典的翻译模型框架:一个双向的RNN编码器和一个的attrntion-RNN解码器。为了解决未登录词(OOV)的问题,Ji[8]提出了一种基于混合NMT的GEC模型,将单词和字符级信息结合起来。Chollampatt等人[4]提出使用卷积神经网络来更好地通过注意力捕获局部上下文。
直到今年,对汉语语法错误问题的研究一直集中在诊断上,以汉语语法错误诊断的共享任务为先导。Zheng[17]和Xie[15]都将CGED视为一个序列标记问题。他们的解决方案结合了传统的条件随机域(CRF)和长短时记忆(LSTM)网络。
在本文中,我们把CGEC任务看作一个翻译问题。具体来说,我们的目标是让神经网络学习错误句与正确句之间的对应关系,并将错误句转化为正确句。然而,与传统的机器翻译任务不同,GEC中的源句包含大量的错误类型。这就是GEC问题的本质(否则就不需要执行更正)。因此,GEC并行语料库中的明显模式要稀疏得多,而且很难学习。另一方面,语法是语言的较高抽象层次,学习者容易犯的语法错误较少。传统汉语语法错误诊断(CGED)任务只有四种类型的语法错误:冗余词®,缺词(M)选词不当(S)选词混乱(W)[17]。因此,一旦表面错误(例如,拼写错误)被移除,模型学习识别它们就变得相对容易了。因此,我们使用了一个三阶段的方法:预处理阶段旨在删除大部分表面错误(例如,拼写和标点错误),识别和纠正语法错误的转换阶段,以及集成阶段(将上述两个阶段结合起来生成最终输出)。模块化阶段允许我们使用不同的模块针对它们的特定目标,并分别进行调整。这将更好的提高整体性能。
在这个任务中,除了NLPCC提供的训练数据外,我们还使用了两个公共数据集:
** Language Model(语言模型)**。Language Model(语言模型)是语法修正领域中常用的一种模型,因为它能够度量单词序列的概率。具体来说,语法正确的句子在语言模型中获得更高的概率,而语法错误或不常见的单词序列则会降低句子的概率。我们使用 Language Model 作为辅助模型来提供特性来对结果进行评分。我们使用的模型是基于字符的从互联网上抓取了2500万个中文句子,训练成 5-gram 的中文模型。
Similar Character Set。由于汉语是象形文字,所以造成拼写错误的原因与英语等按字母顺序排列的语言大不相同。例如,即使是母语人士也经常会混淆形状或发音相似的汉字。此外,由于汉语单词通常较短(2 ~ 4个字符),常用的字典和基于编辑距离的拼写纠正方法效果不佳。为此,我们为中文的拼写校正设计了一个具体的算法。具体来说,我们得到了相似的形状和相似的发音中文字符集(一般称为相似字)
集(SCS))来自SIGHAN 2013 CSC数据集[9,14]。以下是数据集的一些简单示例:
我们使用SCS生成候选的拼写纠正,使用 Language Model 选择最可能的一个。
NLPCC Data Processing训练一个机器翻译模型需要一个(srcSent, tgtSent)对集合形式的并行语料库,其中srcSent是源句,tgtSent是目标句。NLPCC 2018 CGEC共享任务提供了训练语料库,其中每个句子都伴有0个或多个更正后的目标句。原始数据包含约71万个句子。我们处理数据并生成122万对(srcSent、tgtSent),其中srcSent是一个可能包含语法错误的句子,tgtSent是修改后的结果。如果原语句没有错误,tgtSent 与 srcSent 保持相同。如果一个错误的句子有多个更正,就会生成多个对。接下来,我们使用基于字符的 5-gram Language Model 来过滤掉 srcSent 分数明显低于 tgtsend 分数的句子对。在数据清理步骤之后,数据大小减少到0.76万对。
# 读原始数据
datalist = open(r"../Data/CGEC/data.train", encoding='UTF-8').readlines()
dataline = [datalist[i].replace('\n', '').split('\t') for i in range(len(datalist))]
del datalist
# 处理成(srcSent, tgtSent)对集合
newdata = []
for i in range(len(dataline)):
if dataline[i][1] == '0':
newdata.append([dataline[i][2], dataline[i][2]])
else:
for j in range(3, len(dataline[i])):
newdata.append([dataline[i][2], dataline[i][j]])
del dataline
预处理阶段去除大部分表面错误的主要组件是拼写纠正模型。为此,我们使用一个简单的5-gram语言模型。长度为n的字符序列W的概率: P ( w 1 , w 2 , . . . , w n ) = p ( w 1 ) p ( w 2 ∣ w 1 ) . . . p ( w n ) p ( w n ∣ w 1 , w 2 , . . . , w n − 1 ) P(w_{1},w_{2},...,w_{n})=p(w_{1})p(w_{2}|w_{1})...p(w_{n})p(w_{n}|w_{1},w_{2},...,w_{n-1}) P(w1,w2,...,wn)=p(w1)p(w2∣w1)...p(wn)p(wn∣w1,w2,...,wn−1)
序列的困惑度定义为序列概率倒数的几何平均: P P ( W ) = ( w 1 , w 2 , . . . , w n ) − 1 n PP(W)=(w_{1},w_{2},...,w_{n})^{-\frac{1}{n}} PP(W)=(w1,w2,...,wn)−n1
我们将使用PP(W)作为语言模型分数。较高的PP(W)表示较不可能的句子。
为了纠正拼写错误,我们首先将句子x分成字符。对于x中的每个字符c,我们使用SCS生成候选替换字符集 S c S_{c} Sc。然后我们试着使用 S c S_{c} Sc中的所有 c ′ c' c′去替换c,在困惑度最低的句子(包括原文)中进行选择。
我们使用2500万句子级别的通用中文单语语料训练了一个基于字的5元语言模型。最后,对输入句子,我们首先将句子按字进行拆分,对其中每一个字,我们首先判断该字符是否在SCS中出现过,如果出现过,则生成字符的候选替换集合。然后对,我们使用来替换中的字符,以此生成新句,再以语言模型对句子和进行打分,根据得分变化来选择对应结果。
有道技术博客
5-gram 语言模型 语料使用的是 ted演讲稿中文版 可以选择使用自己的语料库
from nltk.lm import KneserNeyInterpolated
from nltk.lm.preprocessing import pad_both_ends, padded_everygram_pipeline
from nltk import ngrams
import zipfile
import lxml.etree
import re
import joblib
"""
获取xml中的有效文本 content 为保留内存 每一步 del 不需要的缓存
"""
with zipfile.ZipFile(r'D:\C\NLP\Data\ted_zh-cn-20160408.zip', 'r') as z:
doc = lxml.etree.parse(z.open('ted_zh-cn-20160408.xml', 'r'))
input_text = '\n'.join(doc.xpath('//content/text()')) # 获取标签下的文字
z.close()
del doc, z
input_text_noparens = re.sub(r'\([^)]*\)', '', input_text)
input_text_noparens = re.sub(r'([^)]*)', '', input_text_noparens)
sentences_strings_ted = []
for line in input_text_noparens.split('\n'):
m = re.match(r'^(?:(?P[^:]{,20}):)?(?P.*)$' , line)
sentences_strings_ted.extend(sent for sent in re.split('[。?!]', m.groupdict()['postcolon']) if sent)
del input_text_noparens, input_text
sentences_strings_ted = [re.sub(r'[^\w\s]', '', sent) for sent in sentences_strings_ted]
sentences_strings_ted = [re.sub(r'[a-zA-Z0-9]', '', sent) for sent in sentences_strings_ted]
sentences_strings_ted = filter(None, sentences_strings_ted)
data = ' '.join([re.sub(r'\s', '', sent) for sent in sentences_strings_ted]).split(' ')
datax = [' '.join(sent).split(' ') for sent in data]
del sentences_strings_ted, data
# 训练 5-gram
train, vocab = padded_everygram_pipeline(5, datax)
lm = KneserNeyInterpolated(5)
lm.fit(train, vocab)
del train, vocab, datax
# 困惑度测试
test = '我想带你们体验一下,我们所要实现的“信任”的感觉。'
sent_list = re.sub(r'[^\w\s]', '', test)
sent_list = ','.join(sent_list).split(',')
text = list(ngrams(pad_both_ends(sent_list, 5), 5))
entropy = lm.entropy(text) # 交叉熵
perplexity = lm.perplexity(text) # 困惑度
print('交叉熵:%f' % entropy, '困惑度:%f' % perplexity)
# 储存模型 ... 以下内容 内存不足跑不起来 去 Colaboratory 或者 kaggle 跑蹭谷歌服务器
# joblib.dump(lm, 'kn_5gram.pkl')
# # In[]
# # 测试储存的模型
# kn = joblib.load('kn_5gram.pkl')
#
# kn_entropy = kn.entropy(text) # 交叉熵
# kn_perplexity = kn.perplexity(text) # 困惑度
# print('KN交叉熵:%f' % kn_entropy, 'KN困惑度:%f' % kn_perplexity)
拼写纠错
"""
未完待续
"""