【NLP实战】文本纠错: pycorrector

Pycorrector实现文本纠错

代码: https://github.com/shibing624/pycorrector

基于规则

基于Kenlm语言模型的错字纠正主要分为2个步骤:

  1. 错误纠正: 用候选集中的字词替换错误位置的字词后,通过语言模型计算混淆度,排序得到最优的纠正词
  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也是自定义的。

一些细节/改动

  1. 原始代码中jieba分词加载的词表是word_freq.txt,这里去掉这个逻辑,直接用jieba本身的词表,不然会有很多错误分词,因为用来训练的线上语料不是完全正确的,所以word_freq表中会有一些不太合理的词
  2. 专名错误检测中的阈值原本是固定的,这里给一些专名词特定的阈值,没有特别设置就用默认阈值 (这里设的0.9);原始代码中给ngram分词时是以"char"为单位,这里改成以"word"为单位,因为以"char"为单位的ngram会造成很多误纠
  3. 用text_chars.klm作为语言模型时,计算perplexity时要以"char"为单位分词;而用text_words.klm作为语言模型时,计算perplexity以"word"为单位进行分词

基于预训练语言模型: MacBERT4CSC

参考: https://github.com/shibing624/pycorrector/tree/master/pycorrector/macbert

训练语料

首先对数据进行预处理(参考【基于规则】中的文本预处理),利用同音字替换生成错字训练语料,并处理成和代码中相同的格式。

同音字替换工具: https://github.com/dongrixinyu/JioNLP/wiki/%E6%95%B0%E6%8D%AE%E5%A2%9E%E5%BC%BA-%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3#%E5%90%8C%E9%9F%B3%E8%AF%8D%E6%9B%BF%E6%8D%A2

注意:
1)使用工具生成错误样本后,有可能输入和输出的句长不一致,要剔除这些样本,只保留前后句长一致的样本,否则模型训练时会报错
2)使用工具生成的错误样本,在模型训练语料中为original_text,而用来生成错误样本的原始样本在模型训练语料中为correct_text,不要弄反了

生成训练语料放到对应文件夹下就可以开始训练了,其他步骤都可参考上面的链接。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容