纠错是NLP中的一个看着不是很火但非常重要的部分,来聊聊pycorrector是怎么做的。

导读:大家好,我是机智的叉烧,这是我NLP.TM系列下的第33篇文章(部分文章还未更新到知乎中,微信公众号下有)。纠错其实在现实应用中非常重要的一个部分,在一个强NLP以来的项目(如搜索)发展至中期,纠错就会成为一个效果提升的新增长点,经过统计,在微博等新媒体领域中,文本出错概率在2%左右,在语音识别领域中,出错率最高可达8-10%(数据来自:https://zhuanlan.zhihu.com/p/159101860),从这个比例来看,如果能修正这些错误,对效果的提升无疑是巨大的,那么我们来看看,纠错任务是怎么做的。
更多文章欢迎关注:
我的专栏:数学·数据·计算机
我的公众号:CS的陋室

文章较长,懒人目录再现:

  • pycorrector简介
  • pycorrector的纠错思路
  • 混淆词典
  • 未登录词检测
  • 语言模型
  • 结果输出
  • 小结

pycorrector简介

pycorrector是非常基础的纠错模块工具,里面已经实现了一些非常通用的纠错方法,用里面的方法来做基线其实其实非常方便。

连接先放在这里:https://github.com/shibing624/pycorrector

他的使用方法其实也比较简单:

import pycorrectorcorrected_sent, detail = pycorrector.correct('少先队员因该为老人让坐')
print(corrected_sent, detail)

这是一个非常简单的官方case,详情还是可以去github里面去看看。

pycorrect的纠错思路

其实pycorrect里面造了很多飞机,不过实质上正式使用的还是非常经典的方法,来看看它的主函数具体思路是什么样的。

def correct(self, text, include_symbol=True, num_fragment=1, threshold=57, **kwargs):"""句子改错:param text: str, query 文本:param include_symbol: bool, 是否包含标点符号:param num_fragment: 纠错候选集分段数, 1 / (num_fragment + 1):param threshold: 语言模型纠错ppl阈值:param kwargs: ...:return: text (str)改正后的句子, list(wrong, right, begin_idx, end_idx)"""text_new = ''details = []self.check_corrector_initialized()# 编码统一,utf-8 to unicodetext = convert_to_unicode(text)# 长句切分为短句blocks = self.split_2_short_text(text, include_symbol=include_symbol)for blk, idx in blocks:maybe_errors = self.detect_short(blk, idx)for cur_item, begin_idx, end_idx, err_type in maybe_errors:# 纠错,逐个处理before_sent = blk[:(begin_idx - idx)]after_sent = blk[(end_idx - idx):]# 困惑集中指定的词,直接取结果if err_type == ErrorType.confusion:corrected_item = self.custom_confusion[cur_item]else:# 取得所有可能正确的词candidates = self.generate_items(cur_item, fragment=num_fragment)if not candidates:continuecorrected_item = self.get_lm_correct_item(cur_item, candidates, before_sent, after_sent,threshold=threshold)# outputif corrected_item != cur_item:blk = before_sent + corrected_item + after_sentdetail_word = [cur_item, corrected_item, begin_idx, end_idx]details.append(detail_word)text_new += blkdetails = sorted(details, key=operator.itemgetter(2))return text_new, details

这里面其实还是比较明确的:

  • 分句。一个长句分成多个断句。
  • 对每个短句进行错误检测detect_short
  • 错误点召回可能正确的词。
  • 召回后筛选最佳结果。

在这个框架下,来看看具体pycorrect的错误检测是怎么做的。

混淆词典

直接看源码:

# 自定义混淆集加入疑似错误词典
for confuse in self.custom_confusion:idx = sentence.find(confuse)if idx > -1:maybe_err = [confuse, idx + start_idx, idx + len(confuse) + start_idx, ErrorType.confusion]self._add_maybe_error_item(maybe_err, maybe_errors)

这块其实还是比较简单的,其实就是用户自定义了一个词典,这个词典作者叫做混淆词典,我更愿意叫做改写词典,遇到了key,就去找v,直接做这种改写。

不过个人感觉这种遍历整个整个词典然后find的方法复杂度可能比较高,如果是我我还是比较喜欢最大逆向匹配的方式来查字典。

未登录词检测

同样上代码:

if self.is_word_error_detect:# 切词tokens = self.tokenizer.tokenize(sentence)# 未登录词加入疑似错误词典for token, begin_idx, end_idx in tokens:# pass filter wordif self.is_filter_token(token):continue# pass in dictif token in self.word_freq:continuemaybe_err = [token, begin_idx + start_idx, end_idx + start_idx, ErrorType.word]self._add_maybe_error_item(maybe_err, maybe_errors)

注释其实还是非常友好的,其实就这几个步骤:

  • 切词。
  • 跳过特定词汇的检测。
  • 查字典看是否有低频词(未登录词)出现。
  • 结果整理。

首先就是切词,这里的切词是一个函数,我们也来看看他具体切词是怎么切的:

class Tokenizer(object):def __init__(self, dict_path='', custom_word_freq_dict=None, custom_confusion_dict=None):self.model = jiebaself.model.default_logger.setLevel(logging.ERROR)# 初始化大词典if os.path.exists(dict_path):self.model.set_dictionary(dict_path)# 加载用户自定义词典if custom_word_freq_dict:for w, f in custom_word_freq_dict.items():self.model.add_word(w, freq=f)# 加载混淆集词典、if custom_confusion_dict:for k, word in custom_confusion_dict.items():# 添加到分词器的自定义词典中self.model.add_word(k)self.model.add_word(word)def tokenize(self, unicode_sentence, mode="search"):"""切词并返回切词位置, search mode用于错误扩召回:param unicode_sentence: query:param mode: search, default, ngram:param HMM: enable HMM:return: (w, start, start + width) model='default'"""if mode == 'ngram':n = 2result_set = set()tokens = self.model.lcut(unicode_sentence)tokens_len = len(tokens)start = 0for i in range(0, tokens_len):w = tokens[i]width = len(w)result_set.add((w, start, start + width))for j in range(i, i + n):gram = "".join(tokens[i:j + 1])gram_width = len(gram)if i + j > tokens_len:breakresult_set.add((gram, start, start + gram_width))start += widthresults = list(result_set)result = sorted(results, key=lambda x: x[-1])else:result = list(self.model.tokenize(unicode_sentence, mode=mode))return result

看着很高端,稍微看看源码其实就可以发现用的是以jieba为基础的操作,只不过多了一种n-gram切词而已,其实就是切词以后按照n-gram拼装而已。

切完词后,就是过滤一些不需要检测的词汇,主要是一些数字之类的,来看看具体有哪些:

@staticmethod
def is_filter_token(token):result = False# pass blankif not token.strip():result = True# pass numif token.isdigit():result = True# pass alphaif is_alphabet_string(token.lower()):result = True# pass not chineseif not is_chinese_string(token):result = Truereturn result

  • 空字符串
  • 数字
  • 字母
  • 非中文

然后就是判断是否是低频词,这个就比较容易,他是构建了一个词典,直接判断是否在里面就好了。

语言模型

NLP领域最基础的东西就要数语言模型了,这里的假设其实是人输入的语言大都是常用的,如果出现了不太常用的东西,其实说明是有错的,带着这个假设,我们来看看利用这个方法是怎么判错的。

# 语言模型检测疑似错误字try:ngram_avg_scores = []for n in [2, 3]:scores = []for i in range(len(sentence) - n + 1):word = sentence[i:i + n]score = self.ngram_score(list(word))scores.append(score)if not scores:continue# 移动窗口补全得分for _ in range(n - 1):scores.insert(0, scores[0])scores.append(scores[-1])avg_scores = [sum(scores[i:i + n]) / len(scores[i:i + n]) for i in range(len(sentence))]ngram_avg_scores.append(avg_scores)if ngram_avg_scores:# 取拼接后的n-gram平均得分sent_scores = list(np.average(np.array(ngram_avg_scores), axis=0))# 取疑似错字信息for i in self._get_maybe_error_index(sent_scores):token = sentence[i]# pass filter wordif self.is_filter_token(token):continue# pass in stop word dictif token in self.stopwords:continue# token, begin_idx, end_idx, error_typemaybe_err = [token, i + start_idx, i + start_idx + 1,ErrorType.char]self._add_maybe_error_item(maybe_err, maybe_errors)except IndexError as ie:logger.warn("index error, sentence:" + sentence + str(ie))except Exception as e:logger.warn("detect error, sentence:" + sentence + str(e))

首先这个是基于字来判断的,所以不需要切词,直接把字符串一个一个的拼接成n-gram即可。

要分析整个句子中每个位点字合理,是需要看上下文的,这里分别采用了2-gram和3-gram进行了分析,分别计算了一个叫做ngram_score的东西,具体是这样的:

def ngram_score(self, chars):"""取n元文法得分:param chars: list, 以词或字切分:return:"""self.check_detector_initialized()return self.lm.score(' '.join(chars), bos=False, eos=False)

这里使用的是kenlm来训练的语言模型,然后用score进行得分计算,这个得分实质上就是分析这个句子组合产生的可能性,概率当然就是在$[0,1]$之间了,然后取对数,因此这个得分就是一个非正数了,越接近0,说明这个组合出现的可能性越大,越不可能有错了。

另外,为了保证整个句子的完整性,是需要padding的,代码里做了一个移动窗口的处理,直接看可能有些难懂,但是知道了padding,应该会好明白一些:

# 移动窗口补全得分
for _ in range(n - 1):scores.insert(0, scores[0])scores.append(scores[-1])

然后就对分数进行根据句子长度的均值计算,计算完之后分别保存了每个字的2-gram得分和3-gram得分,然后后续取了这两个分数的均值,这里的代码这么看:

avg_scores = [sum(scores[i:i + n]) / len(scores[i:i + n]) for i in range(len(sentence))]
ngram_avg_scores.append(avg_scores)if ngram_avg_scores:# 取拼接后的n-gram平均得分sent_scores = list(np.average(np.array(ngram_avg_scores), axis=0))

然后就会开始对这个分数进行分析,最终抽取可能有问题的位点,使用的函数就是_get_maybe_error_index

@staticmethod
def _get_maybe_error_index(scores, ratio=0.6745, threshold=2):"""取疑似错字的位置,通过平均绝对离差(MAD):param scores: np.array:param ratio: 正态分布表参数:param threshold: 阈值越小,得到疑似错别字越多:return: 全部疑似错误字的index: list"""result = []scores = np.array(scores)if len(scores.shape) == 1:scores = scores[:, None]median = np.median(scores, axis=0)  # get median of all scoresmargin_median = np.abs(scores - median).flatten()  # deviation from the median# 平均绝对离差值med_abs_deviation = np.median(margin_median)if med_abs_deviation == 0:return resulty_score = ratio * margin_median / med_abs_deviation# 打平scores = scores.flatten()maybe_error_indices = np.where((y_score > threshold) & (scores < median))# 取全部疑似错误字的indexresult = list(maybe_error_indices[0])return result

思路其实大概说了,就是基于平均离差来算,这其实就是常用异常检测的MAD。说白了就是整个句子,大部分情况是不会出错的,正常情况下打分就会在特定的一个范围内,但是出错的位置的打分会距离这个打分很远(可以理解为和常规语境和语言水平差别很大),我们需要把这几个打分比较远的对应位置提取出来。

另外这里蛮有意思的是,可以看到作者对numpy比较熟悉,可以看看里面这些操作。

结果输出

然后就是一些整理结果输出的操作了,基本的数据处理还是比较容易的,直接看看最终的输出格式吧

import pycorrectoridx_errors = pycorrector.detect('少先队员因该为老人让坐')
print(idx_errors)# 输出:[['因该', 4, 6, 'word'], ['坐', 10, 11, 'char']]

会把他定的位置和错误类型给指出来,最终只需要整理出这个格式就行。

小结

这里给大家介绍的是pycorrector内baseline的检测方法,让大家理解最基本的错误识别方式。

c 提示错误expected) before ; token_NLP.TM[33] | 纠错:pycorrector的错误检测相关推荐

  1. Webots:ERROR: “E:/**/wheelfinready.wbt”:33:23:错误:Expected ‘浮点值‘, found ‘[‘. {1‘?} {2:23:?}

    Webots:ERROR: "E:/**/wheelfinready.wbt":33:23:错误:Expected '浮点值', found '['. {1'?} {2:23:?} ...

  2. 共享文件时提示“将安全性信息应用到以下对象时发生错误”

    在给某文件夹设置用户权限时发生错误,提示"将安全性信息应用到以下对象时发生错误",点击继续其它子文件及文件夹依然如此. 故障如图: 解决方法: 1.右键打开文件夹的属性,在弹出选项 ...

  3. 安装VM虚拟机提示 尝试创建目录 C:\Public\documents\SharedVirtual Machines 时发生错误解决方法

    安装VM虚拟机提示 尝试创建目录 C:\Public\documents\SharedVirtual Machines 时发生错误解决方法 参考文章: (1)安装VM虚拟机提示 尝试创建目录 C:\P ...

  4. JAVA编写提示用户输入投资额_java(计算银行存款总额(要求输入错误时,提示重新输入))...

    [任务一]:编写一个简单 Java 程序,计算银行年存款的本息. 要求:程序运行后要求用户输入存款本金.年利率和存款年限,最后程序计算并输出相应年限后存款的金额.相应 的计算公式为:存款总额=本金 * ...

  5. Mac提示app损坏、Error,Mac电脑最常见错误的解决方案

    这篇文章蓝同学给大家分享一下Mac电脑上最常见错误的解决方案. 以下仅给出部分错误提示截图,类似的错误提示还有磁盘映像损坏.xxx.app有啥啥问题.... ①提示xxx.app已损坏,让你移到废纸篓 ...

  6. [C语言错误]expected declaration or statement at end of input)

    [C语言错误]expected declaration or statement at end of input 可能是缺少括号 可能是没有定义函数

  7. C#使用request.GetRequestStream() 提示“底层连接已关闭:发送时发生意外错误”的问题

    在使用HttpWebRequest的实例request请求网址时,在调用request.GetRequestStream()时提示 "底层连接已关闭:发送时发生意外错误"的问题 论 ...

  8. TIA博途下载PLC程序时提示“具有激活的TIS功能防止下载到设备”错误-处理办法

    TIA博途下载PLC程序时提示"具有激活的TIS功能防止下载到设备"错误-处理办法 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家.点击跳转到网站 ...

  9. php touch 无效,php提示 Warning: touch() [function.touch]: Utime failed: Permission denied in错误...

    在使用php程序时提示Warning: touch() [function.touch]: Utime failed: Permission denied in错误,下面一起来看看此问题的解决办法. ...

最新文章

  1. sudo apt-get update: 0% [正在等待报头]
  2. Mac 技术篇-Oracle数据库连接工具SQL Developer启用、关闭自动提交事务,设置自动commit
  3. build.gradle代码
  4. linux环境下安装mysql 8.0
  5. Vector源码分析
  6. Reuse library debug in Chrome - phase2 handle success response (2)
  7. 7-二进制,十进制,十六进制
  8. 设置ntpdate服务开机启动校验时间
  9. 涉密专用服务器审计系统,国产专用服务器主机审计
  10. mysql 远程 更改
  11. 找不到可安装的ISAM
  12. MongoDB 主从复制(主从集群 )
  13. windows datacenter 2012 R2 密钥
  14. Java经典程序编程50题(较适合初学者)
  15. 长江大学一键评教项目简要分析
  16. dedecms教程:织梦建站教程之如何为内容模型添加新字段?
  17. Python获取某平台主播照片, 实现颜值检测, 进行排名
  18. hdu 1429 胜利大逃亡(续)
  19. word2003文档转pdf预览加盖水印与套红
  20. 轮廓系数silhouette_score手动实现及使用总结

热门文章

  1. 利用linux mutt 发送邮件(在Shell脚本中使用比较方便)
  2. 穿透防火墙调用EJB--rmi-http在JBOSS中的应用
  3. Github上多人协作方式之一
  4. 初学QT遇到的“_on_OK_clicked(bool)未定义的引用”的问题,以及使用windows远程桌面登录树莓派
  5. linux下mysql数据的导出和导入
  6. (九)java多线程之CyclicBarrier
  7. Spring aop切面插入事物回滚
  8. 57-Insert Interval
  9. 在Spark上运行WordCount程序
  10. 完全自定义TabBar(八)