NLP概率分词实验报告
完成日期:2018.11.24
GitHub:https://github.com/fyinh/NLPLearning_2-gram_segment
一、摘要
作为自然语言处理的分支,中文信息处理是指用计算机对中文进行处理,和大部分西方语言不同,书面汉语的词语之间没有明显的空格标记,句子是以字串的形式出现。因此对中文进行处理的第一步就是进行中文自动分词。中文分词是中文自然语言处理的一项基础性工作,也是中文信息处理的一个重要问题,只有不断提高中文分词算法的效率才能跟上信息爆炸增长的现状。因此本文将进行使用基于概率的算法对给定测试文本进行分词的实验。
二、理论描述
中文分词是指把没有明显分界标志的字串自动切分为词串。包括标点符号、数字、数学符号、各种标记、人名、地名、机构名等未登录词的识别。因此中文分词主要包括下面两个步骤:首先根据分词规范,建立机器词典,然后根据分词算法和机器词典,把字串切分成词串。目前根据所使用的知识资源不同分为基于规则的方法,基于统计的方法,以及两者结合的方法。
基于规则的方法一般都需要事先有人工建立好的分词词典和分词规则库。主要是基于字符串匹配的原理进行分词,往往以足够大的词表为依据,采用一定的处理策略将中文文本的字符串与词表中的词逐一匹配,如若成功,就认为该字串为词。主要有正向最大匹配法、逆向最大匹配法、双向匹配法、逐词遍历匹配法、设立切分标志法、正向最佳匹配法和逆向最佳匹配法等。而本次实验使用的方法就是双向匹配法。
而基于概率最大分词方法的实现是利用动态规划算法来实现的:即最有路径中的第i个词wi的累积概率等于它的左邻词wi-1的累积概率乘以wi 自身的概率。
三、算法描述
(1)对一个待分词的字串S,按照从左到右的顺序取出全部候选词w1,w2,。。。,wi,wn;
(2)计算每个候选词的概率值P(wi),记录每个候选词的全部左邻词;
(3)计算每个候选词的累积概率,累积概率最大的候选词为最佳左邻词;
(4)如果当前词wn是字串的尾词,且累积概率p’(wn)最大,则wn是S的终点词;
(5)从wn开始,按照从右到左顺序,依次将每个词的最佳左邻词输出,即S的分词结果。
四、算法说明
(1)是要获得词数组,在训练语料中,将所有的词都存在words[]数组中:当有两个以上的标点符号时,只保留一个。
# 获得词数组
def getWList():
f = open('corpus_for_ass2train.txt')
words = []
new_words = []
for line in f.readlines():
wordlist = line.split()
words.extend(wordlist)
for i in range(0, len(words)):
if is_other(words[i]):
if i!=0 and words[i-1] == 'S':
words[i] = '#'
else:
words[i] = 'S'
new_words.append(words[i])
else:
new_words.append(words[i])
f.close()
return new_words
(2)得到2-gram的字典:统计每个词与它后一个词的出现次数,存成字典的形式,同时也要统计自己出现的次数。
def get_dic(n, word_list):
word_dic = {}
count = 0
for i in range(len(word_list)):
if word_list[i] not in word_dic:
word_dic[word_list[i]] = {}
word_dic[word_list[i]][word_list[i]] = 1 + word_dic[word_list[i]].setdefault(word_list[i], 0)
print(word_dic[word_list[i]][word_list[i]])
if i != len(word_list) - 1:
word_dic[word_list[i]][word_list[i+1]] = 1 + word_dic[word_list[i]].setdefault(word_list[i+1], 0)
print(word_dic[word_list[i]][word_list[i+1]])
for key in word_dic.keys():
count += len(word_dic[key].keys()) - 1
return word_dic, count
(3)统计句子里的所有候选词:找到一个句子中所有可能被切分出来的词和其对应的起始位置和结束位置,用的是前向匹配的方法;在记录时,将所有可能的切分结果以[word,i,j]的形式保存,其中i代表word开始的位置,j代表word结束的位置,便于后续将句子重新连接起来。
# 得到一个句子的所有的词
def get_words(sen, max_length = 4):
all_words = []
for i in range(len(sen)):
all_words.append([sen[i],i,i])
for j in range(1, max_length + 1):
if i+j < len(sen):
if sen[i:i+j+1] in corpus:
print("aaa")
all_words.append([sen[i:i+j+1],i,i+j])
return all_words
(4)将词语重新连接起来组成句子,保存所有可能的切分结果:同时,在遍历每个新词时,在所有词中进行搜索,看有没有和该新词对应位置一致的词,如果有,比较他们俩的概率,并除去概率小的,以减小大量无用计算。
def get_sen_result(sen):
all_words = get_words(sen)
seg_result = []
i = 0
flag = 0
while (i < len(all_words)):
word = all_words[i]
if word[1] == 0 and word[2] != len(sen) - 1:
j = word[2] + 1
if j <= len(sen) - 1:
for word_later in all_words:
if word_later[1] == j:
word_new = word[0] + " " + word_later[0]
max_p = max_prob(word_new)
for word_old in all_words:
if word_old[1] == word[1] and word_old[2] == word_later[2] and word_old[0] != word_new:
if max_prob(word_old[0]) >= max_p:
max_p = max_prob(word_old[0])
else:
all_words.remove(word_old)
if word_old[1] > word[1]:
break
if max_p == max_prob(word_new) and [word_new, word[1], word_later[2]] not in all_words:
all_words.insert(flag,[word_new, word[1], word_later[2]])
elif word_later[1] > j:
break
all_words.remove(word)
i = flag
elif word[1] == 0 and word[2] == len(sen) - 1:
if word[0] not in seg_result:
seg_result.append(word[0])
flag += 1
i = flag
elif word[1] != 0:
break
return seg_result
(5)计算累积概率:采用了对数的方法来表示,防止概率太小了溢出,同时用了laplace平滑方法。
def max_prob(sen_one_result):
prob = 0
word_list = sen_one_result.split(' ')
word_list.insert(0,'#')
for i in range(1,len(word_list)):
if word_list[i-1] in word_dic:
denominator = word_dic[word_list[i-1]][word_list[i-1]]
else:denominator = 0
if denominator == 0:
numerator = 0
else:
numerator = word_dic[word_list[i-1]].get(word_list[i], 0)
p = math.log((numerator + 0.3)/(denominator + count * 0.3))
prob += p
return prob
(6)最后得出最优结果
def best_cut(sen):
seg_all_result = get_sen_result(sen)
best_prob = float('-Inf')
best_seg = ''
for seg_one_result in seg_all_result:
prob = max_prob(seg_one_result)
if prob > best_prob:
best_prob = prob
best_seg = seg_one_result
return best_seg
五、演示
(1)以“本报讯春节临近,全国各地积极开展走访慰问困难企业和特困职工的送温暖活动,并广泛动员社会各方面的力量。”为例:
可以看到分词结果为:“本报 讯 春节 临近 , 全国 各地 积极 开展 走访 慰问 困难 企业 和 特困 职工 的 送 温暖 活动 , 并 广泛 动员 社会 各方 面的 力量 。”
(2)读入corpus_for_ass2test.txt测试文件
六、总结
这个实验陆陆续续做了快一周才完成,而且一直打不出来,参考了一下网上的代码才能慢慢打出来。训练语料是在网上搜索的,两个文件:corpus_for_ass2test.txt 和corpus_for_ass2train.txt,分别用来训练和测试,在做这个实验的时候,遇到了很多困难:
1、首先是对字母、数字还有标点符号的处理,一开始没有考虑这个,拿到测试语料就开始分,结果把数字和字母都算进去了,得到的效果很差= =,之后才想到要把字母和数字分开还有标点符号分开,就是把字母和数字都当做标点符号来处理,然后把一个长句分成几个短句这样来处理,得到的效果比较好。
2、算出来的累积概率太小了,无法判断,一开始没有取对数,就直接相乘,然后怎么都出不了结果= =,然后检查了很久,才发现是溢出了,没办法比较,后来才想到取对数才能进行比较。
3、一开始在遍历生成候选词的时候,就只是生成候选词,并没有记录它们的开始和结束位置,然后在下面生成切分结果的时候,对于每一个候选词都需要重新遍历一遍句子来找到他们的位置,这样真的是太麻烦了= =,而且给程序增加了很多不必要的负担,后来在生成候选词数组的时候顺便记录了他们的位置,程序的运行速度提高了不少。
4、代码运行的速度很慢,打完代码之后发现代码运行的速度太慢了,然后想了很多办法怎么样去优化,可是都不能提高很多,后来参考了别人的代码找到了两种方法一个是在统计候选词的时候,借助栈的先进先出,使得更长匹配放在all_words结果的前面,可以明显改善最终结果和加快运算,可是我研究了很久都好像不太懂这个,所以我就没有用。然后还有还有一种方法是在遍历每一个新词时,在所有词中进行搜索,看有没有和该新词对应位置一致的词,如果有,比较他们俩的概率,并除去概率小的,以减小大量无用计算,我觉得这个可取,我就模仿了,发现运行的速度真的提高了很多!不过还是有点慢= =。
通过这次作业,我对于最大概率分词这个分词法的了解更深了,然后一些在听理论知识时候有些不懂的点在做这个作业时候都搞懂了,感觉自己收获很多!后续也会继续完善这个算法,让这个程序的运行速度更快,效果更好。