Pycorrector实现文本纠错
代码: https://github.com/shibing624/pycorrector
基于规则
基于Kenlm语言模型的错字纠正主要分为2个步骤:
- 错误纠正: 用候选集中的字词替换错误位置的字词后,通过语言模型计算混淆度,排序得到最优的纠正词
- 错误检测: 找出句子中疑似错误的字词和他们的位置
下面详细介绍每个步骤。
错误检测
主要检测4个方面的错误:
自定义混淆集
用户自定义的混淆集,若自定义的错误字词在问句中,将错误字词及其在问句中的位置加入疑似错误词典。
专名错误
对问句进行切分(原始代码以"字"为单位进行切分),再得到切分字词的ngram (n=1,2,3,4),对ngram进行去重和过滤;遍历每个ngram和存储的专名词,计算它们之间词的相似度,相似度取字形相似度和拼音相似度的最大值;若词相似度大于阈值且ngram与专名词不完全一致,将该ngram加入疑似错误词典。
字形相似度
- 两个词完全一致,相似度=1;两个词长度不想等,相似度=0
- 两个词长度相等,遍历每个位置的字,计算两个字的字形相似度;若有一对字的字形相似度小于阈值,则认为词的字形相似度为0,若所有字对的相似度都大于阈值,则取所有字相似度的平均作为词的字形相似度
字的字形相似度: 一个字为中文字符另一个不是,相似度=0;两个不是中文的相等字符,相似度=1;对于两个中文字符,得到它们的五笔输入序列(如"料"=nphspnnnhs, "枓"=hspnnnhs),相似度计算基于这两个序列的编辑距离。
score = 1.0 - edit_distance(char_stroke1, char_stroke2) # score: 字的字形相似度
##############################################################################
def edit_distance(str1, str2):
try:
# very fast
# http://stackoverflow.com/questions/14260126/how-python-levenshtein-ratio-is-computed
import Levenshtein
d = Levenshtein.distance(str1, str2) / float(max(len(str1), len(str2)))
except:
# https://docs.python.org/2/library/difflib.html
import difflib
d = 1.0 - difflib.SequenceMatcher(lambda x: x == " ", str1, str2).ratio()
return d
拼音相似度
- 两个词完全一致,相似度=1;两个词长度不想等,相似度=0
- 两个词长度相等,遍历每个位置的字,判断两个字是否为临近拼音;若有一对字不为临近拼音,则认为词的拼音相似度为0,若所有字对的拼音都为临近拼音,则取所有字的拼音相似度的平均作为词的拼音相似度。
判断是否为临近拼音:两个字的拼音长度相等,看作临近拼音;定义一个拼音混淆集,若两个字的拼音经过混淆集替换后相等,看作临近拼音;其他情况都不为临近拼音
confuse_dict = {
"l": "n", "zh": "z", "ch": "c", "sh": "s", "eng": "en", "ing": "in"}
for k, v in confuse_dict.items():
if char_pinyin1.replace(k, v) == char_pinyin2.replace(k, v):
return True
字的拼音相似度: 与<u>字的字形相似度</u>计算方法类似,只不过将五笔输入序列替换为拼音
词错误
对问句进行分词,过滤分词结果中的空格、数字、字母、和其他不是中文的字符,若剩下的分词结果有不在vocabulary中的(未登录词),则将这些词加入疑似错误词典。
字错误
基于语言模型检测疑似错字。遍历ngram,这里n=2,3。对于2gram,得到句子中的所有2gram和对应的ngram分数 (由语言模型计算得出),形成ngram分数列表,移动窗口补全得分(开头插入首个分数,结尾加入最后一个分数,3gram进行两遍这样的操作),再对每2个ngram分数取平均,得到2gram的得分;同理可得3gram的得分,再对每个位置2gram和3gram的得分取平均,得到最终的句子得分。
sentence = "x_1x_2x_3 ... x_m" # 句长=m
# 对于2gram, n=2
two_grams = ["x_1x_2", "x_2x_3", ... , "x_m-1x_m"]
two_gram_scores = [s_1, s_2, s_3, ..., s_k] # k=m-n+1
# 移动窗口补全得分
two_gram_scores = [s_1, s_1, s_2, s_3, ..., s_k-1, s_k, s_k]
# 对每n=2个分数取平均,使得len(two_gram_avg_scores)=m
two_gram_avg_scores = [(s_1+s_1)/2, (s_1+s_2)/2, (s_2+s_3)/2, ..., (s_k-1+s_k)/2, (s_k+s_k)/2]
# 同理可得three_gram_avg_scores, 长度也等于m
# 取每个位置2gram和3gram得分的平均,作为句子的最终得分; len(sent_scores)=m
sent_scores = (two_gram_avg_scores+three_gram_avg_scores)/2
得到句子得分后,根据平均绝对离差 (MAD median absolute deviation)得到句子中所有疑似错字的index
median = np.median(sent_scores, axis=0) # get median of all scores
margin_median = np.abs(sent_scores - median).flatten() # deviation from the median
med_abs_deviation = np.median(margin_median) # median absolute deviation
# 源代码中ratio=0.6745,指正态分布表参数
y_score = ratio * margin_median / med_abs_deviation
sent_scores = sent_scores.flatten()
# 源代码中threshold=2,阈值越小,得到疑似错别字越多
maybe_error_indices = np.where((y_score > threshold) & (sent_scores < median))
若疑似错字是中文且不为停用词,则将其加入疑似错误词典。
错误纠正
对于自定义混淆集和专名错误,对应的正确字词已经确定;而对于字错误和词错误,需要先找出所有可能正确的词。
字词错误候选召回
- 相同拼音召回
先对错误的词进行置换和替换,举例:
对于疑似错误词="因该",得到置换后的词"该因" (transpose),并词中的每个字符,分别替换 (replace) 为vocabulary中的任意字符,{c}+"该"和因+{c}集合(c指vocabulary中的任意字符);再取集合中的常用词作为候选。若候选词中和错误词的拼音相同,将这些候选词加入正确词候选集合中。 - 自定义混淆召回
若错误的词在自定义混淆集中,将对应的正确词加入到正确词候选结合中。 - 相似字召回
- 若错词为单字,得到该字的混淆字集合(与当前字具有相同拼音和相同字形的集合),加入正确词候选集合。
- 若错词有两个字,先得到第一个字的混淆字集合,与错词的第二个字组合;再得到第二个字的混淆字集合,与错词的第一个字组合;最后将第一个字的混淆字集合和第二个字的混淆字结合两两组合。将上面的所有组合词加入正确词候选集合。
- 若错词有多个字(大于两个),得到第二个字的混淆字集合,替换掉原始错字的第二个字;对错词中的第1到倒数第二个字做<u>相同拼音召回</u>,得到一个相同拼音集合,并与原始错词中的最后一个字做拼接;对错词中的第2到最后一个字做<u>相同拼音召回</u>,得到一个集合,并与原始错词中的第一个字做拼接。将上面的所有组合词加入正确词候选集合。
得到正确词候选集合后,先过滤掉其中不为中文的词,再根据词频对这些词排序。
语言模型纠错
用正确词候选集合中的正确词替换掉句子中的错词,并用语言模型计算新句子的混淆度 (perplexity),根据所有新句子的混淆度进行排序。
统计语言模型训练&纠错
kenlm的安装与使用
参考: https://blog.csdn.net/qq_33424313/article/details/120726582
https://blog.csdn.net/jclian91/article/details/119085472
数据预处理
数据采用的是客服机器人买家聊天语料,主要的问句预处理操作包括: 去除空格、字母转小写、去除表情符号、全角转半角等。
训练模型
- 语料
以char为单位: 每个句子中的char以空格隔开 -> text_chars.txt
太 宽 大 了 都 没 法 穿
好 吧
看错 了
以word为单位: 先 (用jieba_fast) 对句子进行分词,每个词以空格隔开 -> text_words.txt
太 宽大 了 都 没法 穿
好 吧
看错 了
同时,统计词频、生成vocabulary词汇表。
- 训练
cd kenlm/build/
# 以char为单位
bin/lmplz -o 3 --verbose_header --text text_chars.txt --arpa ./results/text_chars.arps -S 4G
# 以word为单位
bin/lmplz -o 3 --verbose_header --text text_words.txt --arpa ./results/text_words.arps -S 4G
# 参数-o表示ngram的数量# 模型压缩
# 以char为单位
bin/build_binary ./results/text_chars.arps ./results/text_chars.klm
# 以word为单位
bin/build_binary ./results/text_words.arps ./results/text_words.klm
规则纠错
用自己训练好的模型替换掉原来的模型,vocabulary (common_char_set.txt 数量>=10的字)和word_freq.txt (词频>=10的词) 也用我们自己的。proper_name.txt也是自定义的。
一些细节/改动
- 原始代码中jieba分词加载的词表是word_freq.txt,这里去掉这个逻辑,直接用jieba本身的词表,不然会有很多错误分词,因为用来训练的线上语料不是完全正确的,所以word_freq表中会有一些不太合理的词
- 专名错误检测中的阈值原本是固定的,这里给一些专名词特定的阈值,没有特别设置就用默认阈值 (这里设的0.9);原始代码中给ngram分词时是以"char"为单位,这里改成以"word"为单位,因为以"char"为单位的ngram会造成很多误纠
- 用text_chars.klm作为语言模型时,计算perplexity时要以"char"为单位分词;而用text_words.klm作为语言模型时,计算perplexity以"word"为单位进行分词
基于预训练语言模型: MacBERT4CSC
参考: https://github.com/shibing624/pycorrector/tree/master/pycorrector/macbert
训练语料
首先对数据进行预处理(参考【基于规则】中的文本预处理),利用同音字替换生成错字训练语料,并处理成和代码中相同的格式。
注意:
1)使用工具生成错误样本后,有可能输入和输出的句长不一致,要剔除这些样本,只保留前后句长一致的样本,否则模型训练时会报错
2)使用工具生成的错误样本,在模型训练语料中为original_text,而用来生成错误样本的原始样本在模型训练语料中为correct_text,不要弄反了
生成训练语料放到对应文件夹下就可以开始训练了,其他步骤都可参考上面的链接。