介绍
这篇文章主要对pycorrector默认使用规则的代码进行debug和理解,不论怎样,应先读下作者的readme,有个充分的理解先。
初始化工作
初始化主要做了三件事:
- 初始化一些词典,用于后面纠错用。
- 加载kenlm模型。
- 初始化jieba分词器。
1. 初始化一些词典等
- 加载常用的汉字列表、加载同音字列表、加载形似字
1 | check_corrector_initialized() |
- 转unicode
1 | # 编码统一,utf-8 to unicode |
- 长句分短句
额外插句:单从re_han
这里就可以看出,作者至少对jieba很熟悉。
1 | def split_2_short_text(text, include_symbol=False): |
2. 加载kenlm模型
关于kenlm,网上搜了下,除了纠错基本很少有人用到(而且还是针对pycorrector~),只有这篇文章说的还有点意思,而且看Github kenlm介绍,作者也是十分任性,只强调速度,没有强调用处。。。
简单来讲,kenlm是基于n-gram训练出来的一个预训练模型,它的更多用法可看Example。
3. 初始化jieba
加载词频
(这个我看了下,和jieba自带的那个dict.txt基本没关系,相当于作者自己训练了一个词频词典)
~~ * 自定义混淆集(空的,所以忽略这步)~~自定义切词词典
(默认是空,个人感觉可以把jieba那个dict.txt加进去,哈哈哈)一些特定词典
人名词典词频、place词典词频、停用词词典词频、将这些词典词频合并到一起
对于人名和place这种词典,不如使用现成了命名实体模型,这种词典的方式总之是无法完全枚举的。
1 | # 词、频数dict, TODO: 这里虽然有,但是貌似没有用到 |
错字识别
1. 基于word级别的错字识别
这部分使用jieba的search模式进行分词。
它的实现原理是:先使用hmm进行分词,比如少先队员因该为老人让坐
,它的分词结果是["少先队员", "因该", "为", "老人", "让", "坐"]
,然后对每个词再用2阶gram和3阶gram进行切分,在self.FREQ
中进行查找是否存在,得到的结果如下:
1 | ('队员', 2, 4) |
分完词后,按词粒度判断是否在词典里,符号,英文则跳过,否则则认为是可能错的。
到这里识别出因该
是可能错误的。
2. 基于kenlm级别的错字识别
取bigram和trigram,通过kenlm获取对应的score,然后求平均获取和句子长度一致的score。
比如:
1 | sent_scores = [-5.629326581954956, -6.566553155581156, -6.908517241477966, -7.255491574605306, -7.401519060134888, -7.489806890487671, -7.1438290278116865, -6.559153278668722, -6.858733296394348, -7.7903218269348145, -8.28114366531372] |
然后通过这个sent_scores
取判断哪些index是错的。
那作者是怎么判断的呢?
1 | def _get_maybe_error_index(scores, ratio=0.6745, threshold=2): |
- 按照百度百科平均绝对离差的定义:
平均绝对离差定义为各数据与平均值的离差的绝对值的平均数
,那作者这里的计算方式貌似就不一样了。 - 作者这里的计算方式不是求平均值,而是每个值减去中位数,然后再求中位数,这样做的好处更多是防止数据分布比较大,就比如大家的平均工资都很高~
- 作者接着使用两个比较,(1)ratio * np.abs(score - median) / 平均绝对离差
(2)scores 小于 中位数的,这地方看的迷迷糊糊,总有种凭经验的感觉。 - 获取对应的错字index。
至此获取到的可能错误列表是:
1 | [['因该', 4, 6, 'word'], ['坐', 10, 11, 'char']] |
纠错
1. 获取纠错候选集
假设当前输入word是因该
:
- 一、获取词粒度的候选集
- 获取相同拼音的(不包含声调)
_confusion_word_set
- 自定义混淆集
_confusion_custom_set
他这个获取相同拼音的写法就让我觉得emo,直接在self.known(自定义词典)里找长度相同,然后判断拼音一样不就得了~
自定义混淆集就是自定义一些经验进行。比如{“因该”: “应该”}这种,增大候选集。
- 二、获取基于字粒度的候选集
这地方分成三部分:
- 如果word的长度等于1。获取相同拼音的
same pinyin 加载同音的列表
,以及加载形似字same stroke 加载形似字
。 - 如果word的长度等于2。截取第一个字符,如
因
,然后获取相同拼音的same pinyin 加载同音的列表
,以及加载形似字same stroke 加载形似字
,然后和该
进行拼接,获取新的候选集。第二个字该
执行相同操作。 - 如果word的长度大于2。同理上述操作,只不过粒度不同(此处忽略)。
- 三、对候选集进行排序,以word_freq进行排序,然后只截取前K个候选集
2. 从候选集里面进行筛选
这个地方就有意思了,如何获取最正确的那个呢?看下面代码。
1 | def get_lm_correct_item(self, cur_item, candidates, before_sent, after_sent, threshold=57, cut_type='char'): |
核心的地方在self.ppl_score
那里,代码如下:
1 |
|
看作者注释,说的很明白了,如果这个句子越是流畅的,那么他的score就会更高。
1 | pprint(sorted_ppl_scores) |
最后一步,作者以score最高的那个加了一个threshold,如果得分在这个阈值内的,添加到候选的top_items里面。
如果当前的cur_item,即因该
不在这个候选集里,那么取第一个top_items
,如果在,那么就返回当前的cur_item。
这步的目的在于防止误判。
思考
- 对于时间日期、人名这种,个人感觉应该先用命名实体剔除掉。
- 自定义词典、形近词那里会是个问题,比如量少或者有歧义怎么解决,另外
因该
也是有可能作为一个单独的词,只是出现的可能性较小。 - 默认加载的是5-gram,关于这里为什么用5-gram没细研究。
- 关于字粒度纠错,那里我感觉真统计。。。
- 不过我喜欢纠错那里,方式简单直接。不过候选集那里可能会是个瓶颈。