汉语自动分词是把没有明显分界标志的字串切分为词串。包括:标点符号、数字、数学符号、各种 标记、人名、地名、机构名等未登录词的识别。本篇博客使用Python编程语言实现基于概率最大化的中文分词算法。
基于概率的自动分词算法
(1)基本思想:选择概率最大的分词路径作为最优结果
(2)利用字与字间、词与词间的同现频率作为分词的依据, 可以没有建立好的词典。需要大规模的训练文本, 用来训练模型参数。
(3)优点:不受应用领域的限制;
(4) 缺点:训练文本的选择将影响分词结果。
(1)对一个待分词的字串S,按照从左到右的顺序取出全部候选词 W 1 W_1 W1, W 2 W_2 W2,…, W i W_i Wi, W n W_n Wn;
(2)计算每个候选词的概率值P( W i W_i Wi ),记录每个候选词的全部左邻词;
(3) 计算每个候选词的累计概率,累计概率最大的候选词为最佳左邻词;
(4)如果当前词Wn是字串的尾词,且累计概率P’( W n W_n Wn)最大,则 是S的终点词;
(5)从 W n W_n Wn开始,按照从右到左顺序,依次将每个词的最佳左邻词输出,即S的分词结。
输入:结合成分子时
第一步:遍历整句话,找出候选词和在语料库中的词频
候选词 | 词频 |
---|---|
结 | 0.0037% |
结合 | 0.0353% |
合 | 0.0049% |
合成 | 0.0006% |
成 | 0.0423% |
成分 | 0.0023% |
分 | 0.0312% |
分子 | 0.0038% |
子 | 0.0010% |
时 | 0.1043% |
第二步:找出备选左邻词
候选词 | 备选左邻词 |
---|---|
结 | |
结合 | |
合 | 结 |
合成 | 结 |
成 | 结合、合 |
成分 | 结合、合 |
分 | 合成、成 |
分子 | 合成、成 |
子 | 成分、分 |
时 | 分子、子 |
第三步:计算累计概率,选出最佳左邻词
候选词 | 累计概率 | 备选左邻词 | 最佳左邻词 |
---|---|---|---|
结 | 0.0037% | ||
结合 | 0.0353% | ||
合 | 0.00001813% | 结 | 结 |
合成 | 0.00000222% | 结 | 结 |
成 | 0.00149319% | 结合、合 | 结合 |
成分 | 0.00008119% | 结合、合 | 结合 |
分 | 0.0000465875% | 合成、成 | 成 |
分子 | 0.00000567412% | 合成、成 | 成 |
子 | 0.00000008119% | 成分、分 | 成分 |
时 | 0.000000591811% | 分子、子 | 分子 |
第四步:输出结果
结合/成/分子/时
def load_dict():
"""
加载字典
:return:返回字典==>"词:频率"
"""
dic_file = open("WordFrequency.txt", 'r', encoding='utf-8')
freq_dict = {}
for l in dic_file:
freq_dict[l.split(',')[0]] = l.split(',')[2].strip()
dic_file.close()
return freq_dict
def find_word_in_dict(s):
"""
在字典中查找候选词
:param s: 输入句子
:return: 返回字典==>"词:词频|候选左邻词1/候选左邻词2"
"""
freq_dict = load_dict()
result = {}
for index in range(0, len(s)): # 遍历所有字
for wordLen in range(0, len(s) - index): # 遍历该字的所有可能词
seg_word = s[index:index + wordLen + 1]
if seg_word in freq_dict.keys():
# 找到候选词,找其左邻词
left_words = ''
for word_len in range(index, 0, -1): # 在之前的候选词列表里找左邻词(最大词长开始)
for k in result.keys():
if s[index - word_len:index] == k.split('-')[1]:
left_words += str(index - word_len) + '-' + s[index - word_len:index] + '/'
# 返回候选词及其语料库词频和候选左邻词
result[str(index) + '-' + seg_word] = freq_dict[seg_word] + '|' + left_words
return result
def cl_probability(words_dict):
"""
计算累加概率并选择最佳左邻词
:param words_dict: "词:词频|候选左邻词1/候选左邻词2"
:return:返回新字典==>"词:累计概率|最佳左邻词"
"""
for k, v in words_dict.items():
freq = v.split('|')[0][:-1]
left_words = v.split('|')[1]
if left_words == '':
continue
else:
left_word = left_words.split("/")
max_left_p = 0.0
which_word = ''
for num in range(0, len(left_word) - 1):
curr_left_word_p = float(words_dict[left_word[num]].split('|')[0][:-1])
if curr_left_word_p > max_left_p: # 比较当前左邻词的累计概率
max_left_p = curr_left_word_p
which_word = left_word[num]
curr_max_p = float(max_left_p) * float(freq)
# 用最大累计概率替换原来的词频,用最佳左邻词替换候选左邻词
words_dict[k] = v.replace(freq, str(curr_max_p)).replace(left_words, which_word)
return words_dict
def seg(sentence):
"""
接收输入,调用函数并输出
:param sentence:
:return: 分词后的句子
"""
words_dict = find_word_in_dict(sentence)
best_words_dict = cl_probability(words_dict)
seg_line = ''
keys = list(best_words_dict.keys())
key = keys[-1]
while key != '':
seg_line = key.split('-')[1] + '/ ' + seg_line
key = best_words_dict[key].split('|')[1]
return seg_line
if __name__ == '__main__':
print("概率最大化分词-请输入待切分句子:")
input_str = input()
print(seg(input_str))
这里使用的词典格式是词,语料库中出现的次数,词频
,大家可以根据实际字典修改loadDict函数。
和,10919,0.9737%
是,9847,0.8781%
“,7970,0.7107%
”,7943,0.7083%
一,7335,0.6541%
找左邻词要考虑子串重复的情况,这里想了很久,如不处理,在输入为分子结合成分子时
,分子分子分子
时会有Bug。因为字典的键值重复了,所以后面出现的分子
的值会把前面分子
的值覆盖掉,因此,我在写入键和左邻词的时候加上了首字的index使其具有唯一性。不过这样在后面的切片时要更为细心,多多调试,如果是Java的话估计我已经放弃了hh…
处理前:
{‘分’: ‘0.0312%|合成/成/’, ‘分子’: ‘0.0038%|合成/成/’, ‘子’: ‘0.0010%|成分/分/’, ‘结’: ‘0.0037%|分子/子/’, ‘结合’: ‘0.0353%|分子/子/’, ‘合’: ‘0.0049%|结/’, ‘合成’: ‘0.0006%|结/’, ‘成’: ‘0.0423%|结合/合/’, ‘成分’: ‘0.0023%|结合/合/’, ‘时’: ‘0.1043%|分子/子/’}
处理后:
{‘0-结’: ‘0.0037%|’, ‘0-结合’: ‘0.0353%|’, ‘1-合’: ‘0.0049%|0-结/’, ‘1-合成’: ‘0.0006%|0-结/’, ‘2-成’: ‘0.0423%|0-结合/1-合/’, ‘2-成分’: ‘0.0023%|0-结合/1-合/’, ‘3-分’: ‘0.0312%|1-合成/2-成/’, ‘3-分子’: ‘0.0038%|1-合成/2-成/’, ‘4-子’: ‘0.0010%|2-成分/3-分/’, ‘5-时’: ‘0.1043%|3-分子/4-子/’}
算法效率影响因素主要考虑如何寻找候选左邻词,考虑过从后往前遍历保留候选词,不过在寻找左邻词时开销比较大,索性在正序找候选词时一并将候选左邻词一并保存,之后通过更新字典值的方法计算累计概率。最后在计算的时候注意一下%的处理,str转float等一些语法的问题就可以啦。
最后一点要注意的是,在某些情况下,随着累乘次数的增加,累计概率的值可能会小于python的float的精度,这里给出的解决思路是用lg将累乘问题变成累加问题,只需要修改clProbabilty函数即可。