本文简单地讲解如何使用n-gram模型结合汉字拼音来做中文错别字纠错,而后介绍最短编辑距离在中文搜索纠错方面的应用;最后从依赖树入手讲解如何做文本长距离纠错(语法纠错),并从该方法中获得一种启示,利用依赖树的特色结合ESA算法来作同义词的查找。html
在中文错别字查错情景中,咱们判断一个句子是否合法能够经过计算它的几率来获得,假设一个句子S = {w1, w2, ..., wn},则问题能够转换成以下形式:java
P(S)被称为语言模型,即用来计算一个句子合法几率的模型。ios
可是使用上式会出现不少问题,参数空间过大,信息矩阵严重稀疏,这时就有了n-gram模型,它基于马尔科夫模型假设,一个词的出现几率仅依赖于该词的前1个词或前几个词,则有算法
(1)一个词的出现仅依赖于前1个词,即Bigram(2-gram):less
(2)一个词的出现仅依赖于前2个词,即Trigram(3-gram):jsp
当n-gram的n值越大时,对下一个词的约束力就越强,由于提供的信息越多,但同时模型就越复杂,问题越多,因此通常采用bigram或trigram。下面举一个简单的例子,说明n-gram的具体使用:函数
n-gram模型经过计算极大似然估计(Maximum Likelihood Estimate)构造语言模型,这是对训练数据的最佳估计,对于Bigram其计算公式以下:
性能
对于一个数据集,假设count(wi)统计以下(总共8493个单词):测试
而count(wi, wi-1)统计以下:ui
则Bigram几率矩阵计算以下:
句子“I want to eat Chinese food”成立的几率为:
P(I want to eat Chinese food) = P(I) * P(want|I) * P(to|want) * P(eat|to) * P(Chinese|eat) * P(food|Chinese)
= (2533/8493) * 0.33 * 0.66 * 0.28 * 0.021 * 0.52。
接下来咱们只须要训练肯定一个阀值,只要几率值≥阀值就认为句子合法。
为了不数据溢出、提升性能,一般会使用取log后使用加法运算替代乘法运算,即
log(p1*p2*p3*p4) = log(p1) + log(p2) + log(p3) + log(p4)
能够发现,上述例子中的矩阵存在0值,在语料库数据集中没有出现的词对咱们不能就简单地认为他们的几率为0,这时咱们采用拉普拉斯矩阵平滑,把0值改成1值,设置成该词对出现的几率极小,这样就比较合理。
有了上面例子,咱们能够拿n-gram模型来作选择题语法填空,固然也能够拿来纠错。中文文本的错别字存在局部性,即咱们只须要选取合理的滑动窗口来检查是否存在错别字,下面举一个例子:
咱们可使用n-gram模型检查到“穿”字打错了,这时咱们将“穿”字转换成拼音“chuan”,再从词典中查找“chuan”的候选词,一个一个试填,用n-gram检查,看是否合理。这就是n-gram模型结合汉字拼音来作中文文本错别字纠错了。汉字转拼音可使用Java库pinyin4j 。
import net.sourceforge.pinyin4j.PinyinHelper; import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat; import net.sourceforge.pinyin4j.format.HanyuPinyinToneType; import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination; public class Keyven { public static void main(String[] args) { HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat(); format.setToneType(HanyuPinyinToneType.WITHOUT_TONE); String str = "我爱天然语言处理,Keyven"; System.out.println(str); String[] pinyin = null; for (int i = 0; i < str.length(); ++i) { try { pinyin = PinyinHelper.toHanyuPinyinStringArray(str.charAt(i), format); } catch (BadHanyuPinyinOutputFormatCombination e) { e.printStackTrace(); } if (pinyin == null) { System.out.print(str.charAt(i)); } else { if (i != 0) { System.out.print(" "); } System.out.print(pinyin[0]); } } } }
固然,现实生活中也存在汉字拼音没打错,是词语选错了;或者n-gram检查合理但词语不存在,例如:
这时就用到最短编辑距离了,对于这种热搜词,咱们仅需记录n-Top,而后用最短编辑距离计算类似度,提供类似度最高的那个候选项就能够了。
编辑距离,又称Levenshtein距离,是指两个字串之间,由一个转成另外一个所需的最少编辑操做次数。许可的编辑操做包括将一个字符替换成另外一个字符,插入一个字符,删除一个字符。例如将kitten一字转成sitting:
sitten (k→s)
sittin (e→i)
sitting (→g)
俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。它是一种DP动态规划算法,在POJ或ACM算法书上有相似的题目。主要思想以下:
首先定义这样一个函数Edit(i, j),它表示第一个字符串的长度为i 的子串到第二个字符串的长度为j 的子串的编辑距离。显然能够有以下动态规划公式:
if (i == 0 且 j == 0),Edit(i, j) = 0
if (i == 0 且 j > 0),Edit(i, j) = j
if (i > 0 且j == 0),Edit(i, j) = i
if (i ≥ 1 且 j ≥ 1) ,Edit(i, j) = min{ Edit(i-1, j) + 1, Edit(i, j-1) + 1, Edit(i-1, j-1) + F(i, j) },
其中,当第一个字符串的第i 个字符不等于第二个字符串的第j 个字符时,F(i, j) = 1;不然,F(i, j) = 0。
#include <iostream> #include <string.h> using namespace std; #define min(a,b) (a<b?a:b) #define min3(a,b,c) (a<min(b,c)?a:min(b,c)) int main() { /* AGTCTGACGC AGTAAGTAGGC sailn failing */ char astr[100], bstr[100]; int dist[100][100]; memset(astr, '\0', sizeof(astr)); memset(bstr, '\0', sizeof(bstr)); memset(dist, 0, sizeof(dist)); gets(astr); gets(bstr); int alen = strlen(astr); int blen = strlen(bstr); for (int i = 0; i <= alen; i++) { dist[i][0] = i; } for (int i = 0; i <= blen; i++) { dist[0][i] = i; } for (int i = 1; i <= alen; i++) { for (int j = 1; j <= blen; j++) { int d = (astr[i - 1] != bstr[j - 1]) ? 1 : 0; dist[i][j] = min3(dist[i - 1][j] + 1, dist[i][j - 1] + 1, dist[i - 1][j - 1] + d); } } for (int i = 0; i <= alen; i++) { for (int j = 0; j <= blen; j++) { printf("%d ", dist[i][j]); } printf("\n"); } printf("%d\n", dist[alen][blen]); system("pause"); return 0; }
以前参加了中文语法错误诊断评测CGED(ACL-IJCNLP2015 workshop)比赛,我负责的是Selection 部分,咱们来看官方给出的样例(Redundant、Missing、Selection、Disorder分别对应4种语法错误):
比胜过程中有想到使用依存树来解决Selection(语法搭配错误)问题,语法搭配与其说是语法范畴,倒不如说是语义概念,例如“那个电影”咱们判断“个”错了是依据“电影”一词来判断,又如“吴先生是修理脚踏车的拿手”判断“拿手”错了是依据“是”一字,“拿手”是动词,怎么能采用“是+名词”结构呢?可是当时事情比较多各类手忙脚乱前途未卜,因此没作出来。后来上网查论文看到一篇《基于n-gram及依存分析的中文自动差错方法》,记得是2年前看到过的,当时对依存树还不理解因此没在乎论文的后半部,如今理解了,写东西也有个理论支撑,没想到想法好有缘分^_^。
词与词之间的搭配是看二者之间的语义关联强度,而依存树的边正能够用来体现这种语义关联度,若是一个句子存在Selection语法错误,那么建成依存树也应该存在一条边是不合理的,咱们能够用这条边来判断是否出现了语法错误。在上述论文中做者将其称之为用来做长距离的中文纠错,而n-gram则是短距离中文纠错。
至于怎样利用已有知识,创建领域知识库,咱们能够跑一遍正确的语料库数据集,统计那些语法正确的句子的依存树边... ...CGED那个比赛所给的训练集有点奇怪,这个也是致使比胜过程不理想没把依存树想法作出来的缘由。我从新从网上找来了几个测试样例(语言学专业的课件PPT),咱们来看一下再看如何拿依存树来作同义词聚类。利用依存树作Selection语法侦错是有了,但是还要纠错呢,怎么实现一种纠错算法呢,固然是同义词替换了,会产生Selection类错误通常都是同义词误用。我曾经拿HIT-IRLab-同义词词林(扩展版) 对比,效果不是很好,因此就有了后来的同义词聚类想法。
以前有接触过同义词聚类的论文,其中印象比较深入的一篇是《 Computing Semantic Relatedness using Wikipedia-based Explicit Semantic Analysis 》,也就是ESA(Explicit Semantic Analysis)算法。ESA的主要思想就是,将一个Wiki词条当作一个主题概念,而后将词条下的解释文本先用TF-IDF逆文档频率过滤分词,再用倒排索引创建成(word-Topic),这样就能够构造主题向量,咱们能够用这些主题向量来作语义类似度计算,完成同义词的查找。
可是这种工做对于我来讲有点难以完成,后来在看Selection平行语料库时,发现同样有意思的东西,就是上图中标成黄色的边,瞬间突发奇想,是否是能够拿这些依存边做为一个Topic,利用倒排索引创建主题向量,这样就能够造出一大堆丰富的原始特征,而后再找个算法做特征选择过滤,再完成同义词查找... ...
基于n-gram及依存分析的中文自动差错方法(马金山,刘挺,李生)
Computing Semantic Relatedness using Wikipedia-based Explicit Semantic Analysis