使用语言模型对句子中的错别字进行判别的几个步骤:
- 使用kenlm训练自己的语言模型
- 对测试集中存在敏感错别字的句子中的错别字进行替换,得到一个新的句子。
- 导入训练好的语言模型,分别对原来的句子和替换后新得到的句子进行打分。
- 如果新得到的句子的分数比原来的句子高,就说明原来的句子出现了问题,该单词应该修改为需要替换后的那个单词。
实验目的:
通过相应的英文句子样本的训练集来自己训练语言模型,并使用自己训练得到的语言模型来判别测试集中的句子是否存在 a/an 的使用错误(比如, I have a apple是错误的; I have an apple 是正确的)。
实验环境:
- Python 3.7.4 (Anaconda3)
- macOS 10.14.4
实验数据:
- 语言模型训练英文语料(训练语料)
- 十万句待检查的英语测试样本(test_set_public)
实验步骤:
1. 观察实验数据
训练语料:
![0f4b8d926a1cb1c595cffc9db40772a6.png](https://i-blog.csdnimg.cn/blog_migrate/41baf672fe16035bd548248ce40a4a9c.jpeg)
对训练语料进行观察,我们发现,训练语料是一个包含了很多的 a/an 搭配句子的文本信息。总共有 2605776 条文本信息,总共的文件大小为 429.7MB,那么在我们 2G 内存的个人 PC 上应该是能够使用 kenlm 模型对这个语料进行训练。
测试集数据:
在对训练集的数据有了大致的了解,之后我们需要对测试集的数据进行观察。
![045646914a90cb5bcfb4a47b181f513c.png](https://i-blog.csdnimg.cn/blog_migrate/a24cc99e8ee3fc24a8f4d81e72111066.jpeg)
观察发现,测试集的数据是类似于训练集的总计十万句的含有 a/an 搭配的英文句子。在文件中按行进行表示。
2. 使用 kenlm 对训练集数据进行训练得到相应的语言模型
kenlm 的下载及安装说明:kenlm . code . Kenneth Heafield 。在这篇官方文档中,作者详细的给出了安装的步骤:
wget -O - https://kheafield.com/code/kenlm.tar.gz | tar xz
mkdir kenlm/build
cd kenlm/build
cmake ..
make -j2
注:
- 如果 cmake 报错的话,可能是没有安装 boost 或者其它 kenlm 的依赖工具,具体可以见官方文档中的使用说明,boost 的安装命令为:
brew install boost
- windows 下好像有点小问题,wget 就一直报错,如果大家成功了麻烦发个 windows 下 kenlm 安装教程给我,多谢。
使用 kenlm 根据我们的训练集语料训练出自己的语言模型:kenlm . code . Kenneth Heafield 。同样在这篇官方文档中,作者给出了使用训练集训练语言模型的方法:
bin/lmplz -o 5 <text > text.arpa
-o Required, Order of the language model to estimate
-o 5 代表使用5ngram
.arpa kenlm 指定训练得到的文件格式为.arpa格式
由于 a/an 的搭配仅仅与其后面的那个单词(是否为元音开头)有关,所以我们只需要训练一个 bigram 的模型就可以实现我们所要的功能了。训练命令为:
bin/lmplz -o 2 <./训练数据> lm.arpa
对训练得到的文件进行压缩:将arpa文件转换为binary文件,这样可以对arpa文件进行压缩,提高后续在python中加载的速度。针对我们训练的到的 lm.arpa 文件其转换命令为:
bin/build_binary -s lm.arpa lm.bin
最终得到文件 lm.bin
KenLM Python 模块的安装:
pip install https://github.com/kpu/kenlm/archive/master.zip
KenLM Python 模块的基本操作:
import kenlm
## 将文件导入到 kenlm 语言模型中
model = kenlm.LanguageModel("./lm.bin")
## 使用语言模型对句子进行打分
## bos=True, eos=True 属性,让 score 返回输入字符串的 log10 概率,即得分越高,句子的组合方式越好
model.score(sentence, bos=True, eos=True)
![9e3e846407df2173334a41bfb504698d.png](https://i-blog.csdnimg.cn/blog_migrate/c1aa8d34c912a034e305494646a3026f.png)
KenLM Python 用法官网说明地址:kpu/kenlm
![f62fa9943f236e9928287e2c12e7ad73.png](https://i-blog.csdnimg.cn/blog_migrate/88a73c49a0f50d342df68624d7613ab9.png)
3. 导入测试集文件
对于测试文件,由于它是按行存储,所以我们可以按行对句子进行取出,并进行判断。其大致思路为:
## 记录测试集文件的存储位置
## 定义一个文件读取方法
## 按行读取测试集数据
## 判断该行是否含有 a/an 的单独字符子串
## 统计 a/an 单独字符子串在这个字符串中的数量 n
## 构建长度为 n 的 a/an 两个字符的排列组合方式
## 对原有句子中的 a/an 进行替换,得到各种不同排列组合下的新句子
## 对各个组合下的新句子运用语言模型进行打分,得到最高分
## 判断最高分的句子是否为原句子
## 不是,就输出相应的修改建议
我定义的按行读取文件的方法如下:
#按行读取文件,返回文件的行字符串列表
def read_file(file_name):
fp = open(file_name, "r")
content_lines = fp.readlines()
fp.close()
#去除行末的换行符
for i in range(len(content_lines)):
content_lines[i] = content_lines[i].rstrip("n")
return content_lines
按行处理输入文本的方法如下:
lines = read_file(input_file_name)
for line in lines:
pass
4. 观察测试集文件中是否含有 a/an ,并进行枚举替换
这一步,我们首先要做的就是判断句子中有没有 a/an。如果存在的话,为了之后能够实现美军替换,那么我们就需要统计它含有都少个 a 和 an 的单独子串。为了确认我们得到的是 a 和 an 的单独子串,而不是像 "apple" 或者 "can" 这样的一个完整的单词中的一部分子串,所以我们需要匹配的是 " a " 和 " an ",在需要匹配的子串前后都加上一个空格,以词的方式进行匹配。
if " a " in line or " an " in line:
count = Counter(nltk.word_tokenize(line))["a"] + Counter(nltk.word_tokenize(line))["an"]
这样子,我们就得到的这个句子中含有的需要插入排列组合结果的插入单词个数。那么如何将a 和 an 的组合结果插入进去呢?
先想一下,如果我这个句子中含有 3 个 a 和 1 个 an,那么我需要插入的个数就是 4 个空。排列组合的结果就是如下十六种:
[('a', 'a', 'a', 'a'),
('a', 'a', 'a', 'an'),
('a', 'a', 'an', 'a'),
('a', 'a', 'an', 'an'),
('a', 'an', 'a', 'a'),
('a', 'an', 'a', 'an'),
('a', 'an', 'an', 'a'),
('a', 'an', 'an', 'an'),
('an', 'a', 'a', 'a'),
('an', 'a', 'a', 'an'),
('an', 'a', 'an', 'a'),
('an', 'a', 'an', 'an'),
('an', 'an', 'a', 'a'),
('an', 'an', 'a', 'an'),
('an', 'an', 'an', 'a'),
('an', 'an', 'an', 'an')]
类似于数学里的插空游戏,我们就可以用一个占位符来代替原来句子上的 a 和 an。同时,为了方便使用字符串的方式对该占位符所在位置根据a/an的排列组合方式进行插值,可以直接使用"%s"作为占位符。
然后的事情就简单了,先用a/an的排列方式来替换掉字符串中的%s占位符,形成a/an在句子结构中的全排列列表,然后将多种组合的句子列表返回给得分比较函数,比较出最高得分。判断这个最高得分的句子是不是原句子。
这里要说一下,在我们后续的实验过程中,如果仅仅如此简单的设置,我们会发现很多的TypeError的错误,这是因为a/an在句子中的格式有如下几种:(我用下划线代替空格)
- _a_(a/an在句子中间)
- 'a_(a/an的前面不是空格而是单引号)
- a_(a/an在句子开头,前面没有空格)
- _a_a_(存在连续的两个a/an的排列方式)
这些,都可以在进行字符串的全排列组合方式插入时出现TypeError时,对其错误进行捕捉,打印观察,后续再对我们的正则表达式进行完善实现。
另外,该处的测试集还有一类问题,就是对于类似于qal'a的错误切词,这个错误是由于nltk提供的英文切词工具的错误引起的,我们可以观察一下。
下面是,我对正则表达式进行进一步完善之后,捕捉到的TypeError的情况,总共是三个句子报错:
![d83cbeeef3b8a13ffe5a2a947a2d6504.png](https://i-blog.csdnimg.cn/blog_migrate/5bc36d8a9efb0655bb758a42c146562e.jpeg)
打印出这三个句子的原型、正则过滤后的样子、以及使用nltk英文切词工具得到的分词样式,如下:
![3a24eff4915168675173ae2966c2c4a8.png](https://i-blog.csdnimg.cn/blog_migrate/eab200edda383c902bc0f630d892cac2.jpeg)
我们可以发现,错误就是在于英文切词工具对于qal'a和shi'a之类的单词的错误切分。可以进一步的细致观察来验证一下,即打印出nltk的分词结果:
![3b72887dcee137c2b31786c406a401d9.png](https://i-blog.csdnimg.cn/blog_migrate/2f30f3b78869f469d965f2fb3ebd0a23.png)
![b2c4e9f31789be78f2534d0a100923e7.png](https://i-blog.csdnimg.cn/blog_migrate/4804574ec134edf977059b142c0c0b02.jpeg)
那么,对于这类错误,由分词工具产生的,且占比较少的3/100000,可以直接使用try捕捉过滤的方法,来直接过滤掉这些错误,对这些问题句子不作为。
下面上代码:
#对句子中的a/an进行全排列组合,返回全排列后的所有组合字符串列表
def change_a_an(line):
new_lines = []
if "a" in line or "an" in line:
#获取a/an的总数量
a_an_counter = Counter(nltk.word_tokenize(line))
a_an_num = a_an_counter["a"] + a_an_counter["an"]
#对字符串中的百分号进行出来,反正在后面的字符串处理中进行转制,从而导致字符串参数传入个数不匹配
percentage_regex = re.compile(r"%")
new_line = percentage_regex.sub(r"%%", line)
#对字符串行按照a/an进行切分
a_and_an_regex = re.compile(r"""
sasas |
sasans |
sansas |
sansans
""", re.VERBOSE)
new_line = a_and_an_regex.sub(r" %s %s ", new_line)
a_an_regex = re.compile(r"sas|sans")
new_line = a_an_regex.sub(r" %s ", new_line)
a_an_regex_front = re.compile(r"^as|^ans")
new_line = a_an_regex_front.sub(r"%s ", new_line)
a_an_regex_quotatio = re.compile(r"([^a-zA-Z]'as)|([^a-zA-Z]'ans)")
new_line = a_an_regex_quotatio.sub(r"'%s ", new_line)
#长度为a_an_num的a/an的排列组合方式的枚举
a_an_form = list(product(("a", "an"), repeat=a_an_num))
#按照排列组合枚举对字符串列表进行组合,形成新的句子
for form in a_an_form:
new_lines += [new_line % form]
return new_lines
for i in range(len(lines)):
line = lines[i]
try:
new_lines = change_a_an(line)
except TypeError:
continue
5. 分别对替换得到新句子进行打分,判断得分最高项是否为原句子。
最后就是得分比较的问题,没有太多的难度,就直接上代码了:
#得分判断
line_best = line
line_best_score = model.score(line, bos=True, eos=True)
for new_line in new_lines:
if model.score(new_line, bos=True, eos=True) > line_best_score:
line_best = new_line
line_best_score = model.score(new_line, bos=True, eos=True)
if line_best != line:
output_file.write("%s. " % (i+1) + line + "n")
output_file.write("【Wrong~】")
output_file.write("=> " + line_best + "nn")
wrong_line_num += 1
else:
output_file.write("%s. " % (i+1) + line + "n")
output_file.write("【Correct!】nn")
6. 观察最终结果
由于最终我们得到的需要进行修改矫正的句子数量很大, 所以我把结果保存到 了文件中,最终的输出文件内容如下:
![3ea0ead252afd91d164446e4bfdf07b4.png](https://i-blog.csdnimg.cn/blog_migrate/6173d384b987b5bb6f7ec86970c78919.jpeg)
最终总共判别出了 48193 组需要修改的数据。并且通过人工抽样对输出需要修改的文件中的修改建议进行检查,发现我们通过语言模型得到的修改建议结果合理。
实现代码:
# encoding=utf-8
import kenlm
from collections import Counter
import nltk
import re
from itertools import product
input_file_name = "./test_set_public"
output_file_name = "./output_file.txt"
#按行读取文件,返回文件的行字符串列表
def read_file(file_name):
fp = open(file_name, "r")
content_lines = fp.readlines()
fp.close()
#去除行末的换行符
for i in range(len(content_lines)):
content_lines[i] = content_lines[i].rstrip("n")
return content_lines
#对句子中的a/an进行全排列组合,返回全排列后的所有组合字符串列表
def change_a_an(line):
new_lines = []
if "a" in line or "an" in line:
#获取a/an的总数量
a_an_counter = Counter(nltk.word_tokenize(line))
a_an_num = a_an_counter["a"] + a_an_counter["an"]
#对字符串中的百分号进行出来,反正在后面的字符串处理中进行转制,从而导致字符串参数传入个数不匹配
percentage_regex = re.compile(r"%")
new_line = percentage_regex.sub(r"%%", line)
#对字符串行按照a/an进行切分
a_and_an_regex = re.compile(r"""
sasas |
sasans |
sansas |
sansans
""", re.VERBOSE)
new_line = a_and_an_regex.sub(r" %s %s ", new_line)
a_an_regex = re.compile(r"sas|sans")
new_line = a_an_regex.sub(r" %s ", new_line)
a_an_regex_front = re.compile(r"^as|^ans")
new_line = a_an_regex_front.sub(r"%s ", new_line)
a_an_regex_quotatio = re.compile(r"([^a-zA-Z]'as)|([^a-zA-Z]'ans)")
new_line = a_an_regex_quotatio.sub(r"'%s ", new_line)
#长度为a_an_num的a/an的排列组合方式的枚举
a_an_form = list(product(("a", "an"), repeat=a_an_num))
#按照排列组合枚举对字符串列表进行组合,形成新的句子
for form in a_an_form:
new_lines += [new_line % form]
return new_lines
#主函数
if __name__ == "__main__":
lines = read_file(input_file_name)
output_file = open(output_file_name, "w")
wrong_line_num = 0
model = kenlm.LanguageModel("./lm.bin")
for i in range(len(lines)):
line = lines[i]
try:
new_lines = change_a_an(line)
except TypeError:
continue
#得分判断
line_best = line
line_best_score = model.score(line, bos=True, eos=True)
for new_line in new_lines:
if model.score(new_line, bos=True, eos=True) > line_best_score:
line_best = new_line
line_best_score = model.score(new_line, bos=True, eos=True)
if line_best != line:
output_file.write("%s. " % (i+1) + line + "n")
output_file.write("【Wrong~】")
output_file.write("=> " + line_best + "nn")
wrong_line_num += 1
else:
output_file.write("%s. " % (i+1) + line + "n")
output_file.write("【Correct!】nn")
print("The total number of sentences that need to be corrected is: " + str(wrong_line_num))
output_file.close()
另外,有关本次实验的数据集和实现代码我已经上传到了 GitHub:shaonianruntu/Judgment-Of-A_An-Mismatch
推荐阅读:
- kenlm . code . Kenneth Heafield
- Python 学习笔记 (6)-- 读写文件
- 【自然语言处理入门】02:Kenlm语料库的制作与模型的训练 - CSDN博客