目录
本文章讲会讲解如何用python代码实现单词拼写错误的检查与纠正,并提供了完整的实现代码,我也将会给大家讲解实现的原理。
完整可运行代码:wm/拼写纠错
代码中包含了三套代码,分别是
单个单词纠错:我们已知这个是错误的,将其纠正成概率最大的单词
文章纠错:自动判断单词是否错误,如果判断拼写错误,将其纠正成最有可能是用户想要的单词。
拼写纠错:上面两套代码虽然也能实现纠错功能,但是原理上是做了简化的,这套代码讲最完整的实现纠错原理
1. 应用领域举例
为什么我要把具体应用的领域列举出来呢,主要是为大家更通俗理解本算法的功能,对本算法实现的目标有一个大致的了解。首先可以思考一下word的单词纠错功能,当我们将study输入成了studu后单词下方将会出现红色的波浪线,这就是单词检查功能,当我们知道这个单词打错后或者对应的拼音输入后有个字母确实没打对但是手机输入法却给了一个我们想要的答案,或者我们单词打错后系统大概率知道我们想要的单词,这就是单词纠错功能。这是非常具体的领域。这一节我们将从单词纠错与检查角度给大家解读一下这种功能的实现方案。
其实单词拼写检查功能是非常好实现的,无非就是检索语料库查找我们拼写的单词是否存在其中,我们本文章主要还是集中在如何找到用户希望拼写的单词上,有了这一原理根据错误拼音找到用户希望的词句也是轻而易举,不过这里不会提供。
我们会从单词纠错的原理开始开始讲解!也就是我们已知单词拼写错误了,然后猜测用户到底想拼写什么单词的原理讲解,然后结合单词检查对一篇文章进行单词改错。
2. 理解概念
首先要理解什么是:编辑距离
意思就是需要几次元操作才能得到想要的单词
元操作:1.替换 2.删除 3.插入 4.对换
举例:
- 'act' 与 'atc' 的编辑距离为1,因为我们需要对atc进行t和c一次交换就得到了act
- 'study'与 'stedys ' 的编辑距离为2,因为我们需要操作两次stedys才能得到study
3. 算法思想
通过编辑距离我们得到了一个量化标准,到底我们拼写的错误单词和另一个单词之间的距离有多远也就是编辑距离。然后我们就可以给出编辑距离最低的单词给到用户了,这个就是猜测用户最希望输入的单词了。那如何获取和输入错误单词编辑距离最低的单词呢?
3.1. 第一种:(笨方法)
将语料库的单词循环一遍,计算语料库里所有的单词和当前单词的编辑距离,然后返回编辑距离最小的单词,缺点很明显,语料库庞大,计算量巨大。
3.2. 第二种:
生成编辑距离1、2的单词:
用户输入单词,对错误单词进行元操作(替换 删除 插入 对换)操作,生成编辑距离为1的单词,在此基础上再进行操作生成编辑距离为2的单词,然后进行过滤选出最合适的字符串。
也就是说将错误单词studu生成编辑距离为1的备选单词study、astudu、studi.......然后根据编辑距离为1的单词生成编辑距离为2的单词。这样就生成了备选单词,但是备选单词那个的概率是最高的呢?用户最希望打出的呢?下面就介绍过滤单词:
过滤单词:
目的:给定一个字符串s假设是appl,要找出最有可能正确的字符串s假设是apple
贝叶斯定理:
P(c|s) = P(s|c)*P(c) / P(s)
翻译为:P(c|s) = P(s|c)*P(c) / P(s) 在用户希望写s也就是apple的情况下,不小心发生了事件c也就是写成了appl的概率是多少
如何处理?
因为我们最终需要一个数值,并且这个数值只做各个单词之间比较,并且P(s)对于我们来说是个常量,因为是用户现在给的概率就是1,所以我们只需要知道P(s|c)*P(c) 就可以了
P(s|c):对于一个正确的字符串c也就是apple,有多少人写成了s也就是appl
p(c):语料库中出现apple的概率是多少
通过语料库我们可以统计出:用户将apple输成appl或者applv的概率分别是多少,也就是P(s|c)*p(c)
总结:
我们将贝叶斯定理思想引入后发现最终我们只需进行一定的统计就可以得到我们想要的概率,我们将计算概率问题变成了一个统计问题从而将问题简单化。
4. 代码实现
下面的所有代码我已经放入我的gitee仓库中,代码可直接运行,有需要的可以自行下载
仓库地址:wm/拼写纠错
单词纠错分为以下几个步骤:
- 读取word.txt
- 利用re将文本分词,提取出单词
- 建立字典语料库,键为单词本身,值为出现的频率,字典为弹性字典,只要查询此单词,都会创建默认值为1的此单词
- 先把这个检测单词进行检测,如果这个单词在语料库中优,说明不是错误单词,那么直接返回,否则对输入的错误词进行操作,生成经过(替换 删除 插入 对换)操作后的和原词编辑距离为1的单词们,再次在编辑距离为1的词的基础上生成编辑距离为2的单词们
- 对这些编辑距离为1和2的单词在语料库中匹配,并且找出语料库中能匹配到的值最大的的单词,并返回
文章改错就是在单词纠错基础上加上了判断每个单词是否拼写错误的检查。
4.1、4.2的已知错误单词纠错算法是上述方法的简易版本!!!!!!!!!!!只是一个粗略统计,将单词统计出来并排序,并没有真正实现贝叶斯算法,但是也够用,4.3为更加精准的贝叶斯方式
4.1. 已知单词拼写错误进行纠错代码实现
# -- coding: utf-8 --
# 本代码旨在阐述单词纠错基本原理(加个for循环就是文本纠错,如果是中文需要先分词,本文使用re正则找单词)
import re, collections
# 读取数据
word_collection = open('./words.txt').read()
# 匹配出单词(而不是字母)并将单词小写
def word_lower(word):
return re.findall('[a-z]+', word)
# 将单词装入字典,形成词袋子
def word_bag(word):
# 没去重的单词
wordOld = word
# 去掉重复的单词
word = list(set(word))
# 初始化字典,collections.defaultdict(lambda :1)={'':1,'':1,'':1........}
word_dic = collections.defaultdict(lambda: 1)
# 更新字典
for i in wordOld:
word_dic[i] += 1
return word_dic
# 获得词袋子
word_dic = word_bag(word_lower(word_collection))
# 修改编辑距离为1的错词
def editis1(wrong_word):
# 错词的长度
n = len(wrong_word)
# 定义丢掉一个字母后的所有可能结果
s1 = [wrong_word[0:i] + wrong_word[i + 1:] for i in range(n)]
# 定义丢互换相邻字母后的所有可能结果
s2 = [wrong_word[0:i] + wrong_word[i + 1] + wrong_word[i] + wrong_word[i + 2:] for i in range(n - 1)]
# 定义替换一个字母后的所有可能结果
all_alpha = 'abcdefghijklmnopqrstuvwxyz'
s3 = [wrong_word[0:i] + c + wrong_word[i + 1:] for i in range(n) for c in all_alpha]
# 定义增加一个字母后的所有可能结果
s4 = [wrong_word[0:i] + c + wrong_word[i:] for i in range(n) for c in all_alpha]
# 将编辑距离为1的所有可能结果结合起来并去重
editis1_word = set(s1 + s2 + s3 + s4)
# 去除原词
editis1_word.remove(wrong_word)
# 将所有可能结果匹配词袋子,找到是单词的字母组合
editis1_word = {w for w in editis1_word if w in word_dic}
return editis1_word
# 修改编辑距离为2的错词
def editis2(wrong_word):
# 获得编辑距离为1的所有单词
word1 = editis1(wrong_word)
# 获得编辑距离为2的所有单词,调用editis1时已去掉原词,所以这里不用remove
editis2_word = set(e2 for e2 in editis1(word1))
# 将所有可能结果匹配词袋子,找到是单词的字母组合
editis2_word = {w for w in editis2_word if w in word_dic}
return editis2_word
# 纠错
def correct(wrong_word):
# 如果单词拼写正确那么就不需要修改了
if wrong_word not in word_dic:
# 获得编辑距离1和2的单词
word_prob = editis1(wrong_word) or editis2(wrong_word)
# 选取靠前的单词,max选取最大值,但是如果值相同(都是2),会选取靠前的单词
result = max(word_prob, key=lambda w: word_dic[w])
return result
# 如果在词袋子中,或编辑距离大于3(这个单词错的有点离谱)
else:
return wrong_word
if __name__ == '__main__':
s = "todau"
print('对', s, '纠错的结果:', correct(s))
4.2. 文章纠错代码实现
import docx
from nltk import sent_tokenize, word_tokenize
from spelling_correcter import correct_text_generic
from docx.shared import RGBColor
# 文档中修改的单词个数
COUNT_CORRECT = 0
# 获取文档对象
file = docx.Document("./修改前的文章.docx")
# print("段落数:"+str(len(file.paragraphs)))
punkt_list = r",.?\"'!()/\\-<>:@#$%^&*~"
document = docx.Document() # word文档句柄
def write_correct_paragraph(i):
global COUNT_CORRECT
# 每一段的内容
paragraph = file.paragraphs[i].text.strip()
# 进行句子划分
sentences = sent_tokenize(text=paragraph)
# 词语划分
words_list = [word_tokenize(sentence) for sentence in sentences]
p = document.add_paragraph(' ' * 7) # 段落句柄
for word_list in words_list:
for word in word_list:
# 每一句话第一个单词的第一个字母大写,并空两格
if word_list.index(word) == 0 and words_list.index(word_list) == 0:
if word not in punkt_list:
p.add_run(' ')
# 修改单词,如果单词正确,则返回原单词
correct_word = correct_text_generic(word)
# 如果该单词有修改,则颜色为红色
if correct_word != word:
colored_word = p.add_run(correct_word[0].upper() + correct_word[1:])
font = colored_word.font
font.color.rgb = RGBColor(0x00, 0x00, 0xFF)
COUNT_CORRECT += 1
else:
p.add_run(correct_word[0].upper() + correct_word[1:])
else:
p.add_run(word)
else:
p.add_run(' ')
# 修改单词,如果单词正确,则返回原单词
correct_word = correct_text_generic(word)
if word not in punkt_list:
# 如果该单词有修改,则颜色为红色
if correct_word != word:
colored_word = p.add_run(correct_word)
font = colored_word.font
font.color.rgb = RGBColor(0xFF, 0x00, 0x00)
COUNT_CORRECT += 1
else:
p.add_run(correct_word)
else:
p.add_run(word)
for i in range(len(file.paragraphs)):
write_correct_paragraph(i)
document.save('./修改后的文章.docx')
print('修改并保存文件完毕!')
print('一共修改了%d处。' % COUNT_CORRECT)
# -*- coding: utf-8 -*-
import re, collections
def tokens(text):
"""从语料库中获取所有单词"""
return re.findall('[a-z]+', text.lower())
with open('./words.txt', 'r') as f:
WORDS = tokens(f.read())
WORD_COUNTS = collections.Counter(WORDS)
def known(words):
"""返回在WORD_COUNTS字典里的单词子集"""
return {w for w in words if w in WORD_COUNTS}
def edits0(word):
"""返回输入中编辑距离为0的字符串"""
return {word}
def edits1(word):
"""返回输入中编辑距离为1的字符串"""
alphabet = ''.join([chr(ord('a') + i) for i in range(26)])
def splits(word):
"""返回由输入字组成的所有可能对的列表。"""
return [(word[:i], word[i:])
for i in range(len(word) + 1)]
pairs = splits(word)
deletes = [a + b[1:] for (a, b) in pairs if b]
transposes = [a + b[1] + b[0] + b[2:] for (a, b) in pairs if len(b) > 1]
replaces = [a + c + b[1:] for (a, b) in pairs for c in alphabet if b]
inserts = [a + c + b for (a, b) in pairs for c in alphabet]
return set(deletes + transposes + replaces + inserts)
def edits2(word):
"""返回输入中编辑距离为2的字符串"""
return {e2 for e1 in edits1(word) for e2 in edits1(e1)}
def correct(word):
"""获取输入单词的最佳正确拼写"""
# Priority is for edit distance 0, then 1, then 2
# else defaults to the input word itself.
candidates = (known(edits0(word)) or
known(edits1(word)) or
known(edits2(word)) or
{word})
return max(candidates, key=WORD_COUNTS.get)
def correct_match(match):
"""在匹配中拼写正确的单词,并保留适当的大写/小写/标题大小写。"""
word = match.group()
def case_of(text):
"""返回适合文本的大小写函数:upper、lower、title或just str.:"""
return (str.upper if text.isupper() else # Python isupper() 方法检测字符串中所有的字母是否都为大写。
str.lower if text.islower() else
str.title if text.istitle() else
str)
# 原本是return case_of(word)correct(word.lower()),意思和下面的式子是一样的
return case_of(word)(correct(word.lower()))
def correct_text_generic(text):
"""更正文本中的所有单词,并返回更正后的文本。"""
# re.sub():匹配替换为选择的文本:从text中正则表达式匹配[a-zA-Z],改成correct_match
return re.sub('[a-zA-Z]+', correct_match, text)
4.3. 拼写纠错代码实现
import numpy as np
import re
import pandas as pd
# --------------------------Part0:构建词库---------------------------
# 构建词库
word_dic = []
# 通过迭代器访问: for word in f
# 用列表生成式直接将数据加入到一个空的列表中去
with open('./vocab.txt', 'r') as f:
word_dic = set([word.rstrip() for word in f])
# --------------------------Part1:生成所有的候选集合---------------------------
import string
def generate_candidates(word=''):
"""
word: 给定的错误输入
返回的是所有的候选集合
生成编辑距离为1的单词
1、insert
2、delete
3、replace
"""
# string.ascii_lowercase: 所有的小写字母
letters = ''.join([word for word in string.ascii_lowercase])
# 将单词分割成一个元组,把所有的可能性添加到一个列表中去。
# [('', 'abcd'), ('a', 'bcd'), ('ab', 'cd'), ('abc', 'd'), ('abcd', '')]
splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]
# 遍历字母,遍历所有的分割,把他们组合起来
# 插入到所有可能的位置
inserts = [L + i + R for L, R in splits for i in letters]
# delete
# 每次都是删除R的第一个元素(如果R存在的话)
deletes = [L + R[1:] for L, R in splits if R]
# replace
# 替换嘛。就是插入和删除的合体。
replaces = [L + i + R[1:] for L, R in splits if R for i in letters]
return set(inserts + deletes + replaces)
def generate_edit_two(word=''):
"""
给定一个字符串,生成编辑距离不大于2的字符串。
"""
# # 第一步,先生成编辑距离为1的候选集合。
# edit_one = generate_candidates(word)
# # 第二部,遍历编辑距离为1的候选集合,对每个元素都再次使用函数
# all_lis = []
# for i in edit_one:
# all_lis.extend(generate_candidates(i))
# 上边的方法也可以直接写成一个列表生成式
return set([j for i in generate_candidates(word) for j in generate_candidates(i)])
# --------------------------Part2:读取语料库,为构建语言模型准备-----------------------
# shift+tab 来调出函数的具体说明
# 读取一些句子,为了构建语言模型做准备。
# 从nltk中导入路透社语料库
# 路透社语料库
import nltk
from nltk.corpus import reuters
# 输出语料库包含的类别
categories = reuters.categories()
# corpus:包含许多句子的集合。
# 每个句子是列表形式:['ASIAN', 'EXPORTERS', 'FEAR', 'DAMAGE']
corpus = reuters.sents(categories=categories)
# --------------------------Part3:构建语言模型,Bigram-----------------------
# term_count: 代表所有字符以及其个数组成的一个字典。(单个字符)
term_count = {}
# bigram_count:双字符字典
bigram_count = {}
for doc in corpus:
# 每一个句子都加上起始符
doc = ['<s>'] + doc
# 遍历每一个句子的每一个字符,并将其个数记载入term_count字典里。
for i in range(len(doc) - 1):
# term: 当前字符
term = doc[i]
# bigram:当前字符以及后一个字符组成的列表
bigram = doc[i:i + 2]
if term in term_count:
term_count[term] += 1
else:
term_count[term] = 1
# 把bigram变换成一个字符串。
bigram = ' '.join(bigram)
if bigram in bigram_count:
bigram_count[bigram] += 1
else:
bigram_count[bigram] = 1
# --------------------------Part4:构建每个单词的错误单词输入概率的词典。-----------------------
# 用户通常输入错的概率 - channel probability
channel_prob = {}
# 打开拼写纠错记事本
with open("./spell-errors.txt", 'r', encoding='utf8') as f:
# 遍历每一行
for line in f:
# 用冒号来进行分割
# raining: rainning, raning变为['raining', ' rainning, raning\n']
temp = line.split(":")
# 正确的单词是列表里的第一个字符串并且去除掉前后空格
correct = temp[0].strip()
# 错误的单词是列表里的第二个字符串并且以逗号分隔开的几个单词。
mistakes = [sub_mis.strip() for sub_mis in temp[1].strip().split(",")]
# 将每一个单词和他的每个错误单词的比例组成一个键值对。
# 键是正确单词,值是一个花括号。
channel_prob[correct] = {}
for mis in mistakes:
# 嵌套词典
# 值是该错误单词占所有错误单词的比例
channel_prob[correct][mis] = 1.0 / len(mistakes)
# 最终结果如下
# {'raining': {'rainning': 0.5, 'raning': 0.5}}
# ---------------------------------Part5:使用测试数据来进行拼写纠错-----------------------------------
V = len(term_count)
# 打开测试数据
with open("./testdata.txt", 'r', encoding='utf8') as f:
# 遍历每一行
for line in f:
# 去掉每一行右边的空格。并且以制表符来分割整个句子
items = line.rstrip().split('\t')
# items:
# ['1', '1', 'They told Reuter correspondents in Asian capitals a U.S.
# Move against Japan might boost protectionst sentiment in the U.S. And lead to curbs on
# American imports of their products.']
# 把\.去掉,每个句子刚好在items的下标为2的位置。
line = re.sub('\\.', '', items[2])
# 去掉逗号,并且分割句子为每一个单词,返回列表
line = re.sub(',', '', line).split()
# line:['They', 'told', 'Reuter', 'correspondents', 'in', 'Asian',
# 'capitals', 'a', 'US', 'Move', 'against', 'Japan', 'might', 'boost', 'protectionst',
# 'sentiment', 'in', 'the', 'US', 'And', 'lead', 'to', 'curbs', 'on', 'American', 'imports', 'of', 'their', 'products']
# 遍历词语列表
for word in line:
# 去除每一个单词前后的逗号和句号。
word = word.strip('.')
word = word.strip(',')
# 如果这个单词不在词库中。
# 就要把这个单词替换成正确的单词
if word not in word_dic:
# Step1: 生成所有的(valid)候选集合
candidates_one = generate_candidates(word)
# 把生成的所有在词库中的单词拿出来。
candidates = [word for word in candidates_one if word in word_dic]
# 一种方式: if candidate = [], 多生成几个candidates, 比如生成编辑距离不大于2的
# TODO : 根据条件生成更多的候选集合
# 如果candidates为空的话,则接着生成编辑距离为2的。
if len(candidates) < 1:
candidates_two = generate_edit_two(word)
candidates = [word for word in candidates_two if word in word_dic]
if len(candidates) < 1:
continue
probs = []
# 计算所有候选单词的分数。
# score = p(correct)*p(mistake|correct)
# = log p(correct) + log p(mistake|correct)
# log p(mistake|correct)= log(p(correct/mistake)*p(mistake)/p(correct))
# 遍历候选词汇
# 返回score最大的candidate
# score既考虑了单个单词的概率,也考虑了与前边单词组合的概率。
for candi in candidates:
prob = 0
# a. 计算channel probability
# 如果候选词在channel_prob字典中,并且错误单词刚好在候选词对应的值处。
if candi in channel_prob and word in channel_prob[candi]:
prob += np.log(channel_prob[candi][word])
else:
prob += np.log(0.00001)
# b. 计算语言模型的概率
sentence = re.sub('.', '', items[2])
# 得到单词在原来句子中的索引
idx = re.sub(',', '', sentence).split().index(word)
#
# items:
# ['1', '1', 'They told Reuter correspondents in Asian capitals a U.S.
# Move against Japan might boost protectionst sentiment in the U.S. And lead to curbs on
# American imports of their products.']
# 把当前单词和他的前一个单词拼接到一起。
bigram_1 = ' '.join([items[2].split()[idx - 1], candi])
# 如果bigram_1在双字符词典里,并且前一个单词也在词典里
if bigram_1 in bigram_count and items[2].split()[idx - 1] in term_count:
prob += np.log((bigram_count[bigram_1] + 1.0) / (
term_count[items[2].split()[idx - 1]] + V))
else:
prob += np.log(1.0 / V)
# TODO: 也要考虑当前 [word, post_word]
# prob += np.log(bigram概率)
if idx + 1 < len(items[2].split()):
bigram_2 = ' '.join([candi, items[2].split()[idx + 1]])
if bigram_2 in bigram_count and candi in term_count:
prob += np.log((bigram_count[bigram_2] + 1.0) / (
term_count[candi] + V))
else:
prob += np.log(1.0 / V)
# 所有候选单词的分数都添加到probs列表里。
probs.append(prob)
#
print(probs)
if probs:
# 得到probs列表候选单词里最大的分数,把索引拿出来
max_idx = probs.index(max(probs))
# 该索引同时也对应着候选集合里的正确单词,输出错误单词和正确单词。
print(word, candidates[max_idx])
else:
print("False")
4.4. 参考资料url:
课时1:贪心学院 自然语言处理训练营 NLP 全程_哔哩哔哩_bilibili b站视频p26、p27
文本纠错之单词纠错子代码详解_文本纠错 算法代码-CSDN博客