一、算法介绍
1、核心累积概率计算公式:P’(Wi)=P’(Wi-1) × P(Wi)
2、算法概述:
步骤:对一个带分词的字符串S,从左往右选出候选词W1,…Wn
计算每个候选词的概率值,并记录候选词的全部左邻词
计算候选词累积概率,选择累积概率最大的左邻接词为最佳左邻词
如果Wn是字符串的尾部并且累积概率最大,那么Wn作为终点词
从Wn开始,从右向左,输出对应词的最佳左邻词
3、伪代码:
3.1、获取候选词和左邻接词
初始化start下标,start为候选子串的开始下标
当start小于S的长度的时候,循环{
初始化end为start+1,得到候选子串s,end指向候选词的终止下标
当end小于或者等于S的长度的时候,循环{
if(s不在字典中){
if(s为一个字){在字典中为s设置一个很小的概率值}
else{退出这部分的循环}
}
将匹配到候选词加入对应的列表中。
一个候选词匹配完后接着匹配左邻接词,一候选词的开始下标start作为左;邻接词的终止下标left_near_end,向做匹配,循环直到左邻词开始下标left_near_start为0的时候终止.
}
}
3.2、选择最佳左邻词以及计算累积概率
For token in 候选词列表:
如果没有左邻词那直接候选词赋值累积概率,最佳左邻词设置为None
否则选出累积概率最大的左邻词为最佳左邻词,计算累积概率
3.3、获取分词的输出结果:
从下往上遍历end_index(即字符的终止下标)为S的长度的候选词,选择累积概率大的作为最后的词汇final_token,赋值给next_token,
从下往上遍历候选词列表,先将next_token存放在列表中,匹配到next_token后选择next_token的最佳左邻接词赋值给next_token,
循环直到next_token的最佳左邻接词(temp_best_left)为None
得到分词列表
字典数据格式:
先上代码!!
# -*- coding: utf-8 -*-
# @Time : 2019/10/15 21:39
# @Author : ChenHanWu
# @FileName: ProbSegmentation.py
# @Software: PyCharm
# 基于概率最大值的分词
import pandas as pd
import time
class Segmentation(object):
def __init__(self, origin_string, adjust_prob_times=10000):
self.dictionary = dict()
self.origin_string = origin_string
self.segment_word_list = []
self.sentence_dict = {"token": [], "left_near_token": [], "communicate_prob": [], "best_left_near_token": [],
"end_index": []}
self.adjust_prob_times = adjust_prob_times
def load_dictionary(self, dict_path):
print("load dictionary...")
data_df = pd.read_csv(dict_path, sep=',', encoding='gbk', header=None)
temp_prob_list = []
for temp_prob in data_df[2]:
temp_prob_list.append(float(temp_prob.replace("%", ""), ) * 0.01 * self.adjust_prob_times)
data_df[2] = temp_prob_list
# print(data_df.head(5),"\n",data_df.tail(5))
token_list = data_df[0].values
prob_list = data_df[2].values
for i, token in enumerate(token_list):
self.dictionary[token] = prob_list[i]
print("load dictionary finish...")
# 从下往上寻找出现的上一个目标token累积概率,比如一句话有“今天、今天你”这两个候选词,
# 那么“今天”对应的累积概率应该有多个,那么“你”的累积概率取决于第二个“今天”,而不是第一个
def find_last_token_communicate_prob(self, target_end, target_token):
# print(self.sentence_dict["token"])
# print(self.sentence_dict["communicate_prob"])
for j in range(target_end, -1, -1):
if self.sentence_dict["token"][j] == target_token:
return self.sentence_dict["communicate_prob"][j]
def segment_word(self, early_stop=True, patience=0):
start = 0
# 选择候选词和左邻词
while start < len(self.origin_string):
end = start + 1
patience_count = 0 # 将忍耐匹配不到的次数置为0
while end <= len(self.origin_string): # end最大要移动到最后一个字节的后一位,如长度为6的,那么end下标最大应该为6
temp_token = self.origin_string[start:end] # 截出词汇做判断
if temp_token not in self.dictionary.keys():
if len(temp_token) == 1: # 如果单个字在词典中都没能找到,那么就给定一个很小的概率值,然后继续进行左临词的匹配
self.dictionary[temp_token] = 0.0000001 * self.adjust_prob_times
else:
patience_count += 1
if early_stop and patience_count > patience: # 如果找不到的次数超过了限定的patience那么就停止
break
else:
continue # 直接跳过循环下面的其他操作
self.sentence_dict["token"].append(temp_token)
self.sentence_dict["end_index"].append(end)
left_near_start = start - 1 # 左邻近词汇从temp_token的起始下标往左匹配
left_near_end = start
temp_left_near_token_list = []
near_patience_count = 0
while left_near_start >= 0:
temp_left_near_token = self.origin_string[left_near_start:left_near_end]
if temp_left_near_token not in self.dictionary.keys():
near_patience_count += 1
if early_stop and near_patience_count > patience:
break
else:
temp_left_near_token_list.append(temp_left_near_token)
left_near_start -= 1 # 逐渐左移
self.sentence_dict["left_near_token"].append(temp_left_near_token_list) # 将左邻近词列表加紧来
end += 1
start += 1
# 开始选择最佳做邻词和计算累积概率
token_list = self.sentence_dict["token"]
for i, token in enumerate(token_list): # 遍历所有的候选词
left_near_token_list = self.sentence_dict["left_near_token"][i]
best_left_near_token = None
if len(left_near_token_list) == 0: # 如果没有左邻词
token_communicate_prob = self.dictionary[token]
else:
best_prob = 0
for left_near_token in left_near_token_list: # 遍历得到最佳左邻近词汇得到累计概率
# temp_token_prob = self.token_communicate_prob_dict[left_near_token]
temp_token_prob = self.find_last_token_communicate_prob(i - 1, left_near_token) # 从当前向前找到目标词汇的累积概率
if temp_token_prob > best_prob:
best_left_near_token = left_near_token
best_prob = temp_token_prob
token_communicate_prob = self.dictionary[token] * best_prob
# self.token_communicate_prob_dict[token] = token_communicate_prob # 在字典中记录对应的累积概率
self.sentence_dict["communicate_prob"].append(token_communicate_prob)
self.sentence_dict["best_left_near_token"].append(best_left_near_token)
def get_segment_result(self, verbose=1):
# 详细输出过程
if verbose == 1:
key_list = list(self.sentence_dict.keys())
mat = "{:20}\t{:20}\t{:20}\t{:20}\t{:20}"
print(mat.format(key_list[0], key_list[1], key_list[2] + "/" + str(self.adjust_prob_times), key_list[3],
key_list[4]))
for i in range(len(self.sentence_dict["token"])):
print(mat.format(str(self.sentence_dict[key_list[0]][i]), str(self.sentence_dict[key_list[1]][i]),
str(self.sentence_dict[key_list[2]][i]), str(self.sentence_dict[key_list[3]][i]),
str(self.sentence_dict[key_list[4]][i])))
final_token = None
best_prob = 0
for i in range(-1, -1 - len(self.sentence_dict["token"]), -1): # 从后向前搜索对应的最大累积概率的结束词汇
temp_prob = self.sentence_dict["communicate_prob"][i]
if temp_prob > best_prob:
best_prob = temp_prob
final_token = self.sentence_dict["token"][i]
if self.sentence_dict["end_index"][i] < len(self.origin_string): # 如果不是作为最后一个词的候选词,那么就停止了
break
if final_token is None: # 5e-324 后会越界,句子太长不行
print("错误!!!找不到尾部,可能小数计算越界了,建议调大adjust_prob_times参数")
else:
self.segment_word_list.append(final_token) # 将最后一个token最近列表中
next_token = final_token
for i in range(-1, -1 - len(self.sentence_dict["token"]), -1): # 从后向前搜索对应的最大累积概率的结束词汇
temp_token = self.sentence_dict["token"][i]
if next_token == temp_token:
# print(next_token)
temp_best_left = self.sentence_dict["best_left_near_token"][i]
if temp_best_left is None:
break
else:
self.segment_word_list.append(temp_best_left)
next_token = temp_best_left
else:
pass
# print(self.segment_word_list)
self.segment_word_list.reverse()
return "/".join(self.segment_word_list)
if __name__ == '__main__':
dictionary_data_path = "../data/WordFrequency.txt" # 字典路径
string1 = "今天天气真好,转载魑我今天真的很开心。" # 魑在字典中没有
# string1 = "在这一年中,中国的改革开放和现代化建设继续向前迈进。国民经济保持了“高增长、低通胀”的良好发展态势。农业生产再次获得好的收成,企业改革继续深化,人民生活进一步改善。对外经济技术合作与交流不断扩大。"
start_time = time.time()
segment = Segmentation(string1, adjust_prob_times=100) # 加载对象 adjust_prob_times调整概率倍数,防止分词序列过长导致小数位溢出 出现None,
segment.load_dictionary(dict_path=dictionary_data_path) # 加载字典
segment.segment_word(early_stop=True, patience=0)
# 将patience设置大一点或者关掉early_stop的话能够匹配到“幼儿园”,如果patience设置为0那么就匹配到“儿园”的时候停止了。
# 切词,设置early_stop=False的话,那么就每一次都匹配到最尾部
# 如果设置early_stop为True,那么就设置对应的patience,如果超过patience次数匹配不到词汇那么就停止匹配
result = segment.get_segment_result(verbose=1) # 获取分词结果,verbose设置为1输出详细过程
end_time = time.time()
print("result:", result)
print("耗时:", end_time - start_time)
二、实现要点
1、未登录字问题
当有字典中不存在的字出现的时候,应当为期赋予一个比较低的概率值后更新到字典中去,否则无法成功分词。比如“魑”在字典中就不存在。
2、获取字或者词的累积概率的时候必须自当前候选词token位置向上的候选词获取。比如一句话有“今天、今天你”这两个候选词, 那么“今天”对应的累积概率应该有多个,那么“你”的累积概率取决于第二个“今天”,而不是第一个。
3、early_stop问题
当子串在字典中查找不到时候,设置了patience,当匹配不大的次数超过patience的时候就break断开匹配,如果patience设置比较高那么就能匹配较长的词汇(在字典中存在的(比如:“广东外语外贸大学”)),代码中实现了这一点,可以设置early_stop为true的时候设置对应的patience值
4、获取最终结果
获取最终结果的时候,通过累计概率选定结尾后,必须自下向上的选择候选的最佳左邻词。比如有一句字“今天哈哈哈哈今天你好”,如果两个今天都出现在最佳左邻接词中,那么,如果从上往下选择会选到第一个今天后句子分词结果就结束了,中间的部分就被忽略掉了,所以必须自下晚上,匹配到第二个“今天”后继续匹配第二个“今天”对应的最佳左邻词。
5、句子过长
在输入句子过长的时候,可能会出现的情况是小数位溢出,所以必须设置参数提高概率的倍数,或者对概率值做对数运算转化,在此我做的是概率倍数提升,一般200长度以内的句子设置倍数为1000已经足够。
三、结果演示
匹配到最后一个句号后,不选选择最佳左邻词,从下往上跳选,直到最佳左邻词best_left_near_token为None 结束.
输入较长的测试文本,但文本过长时可能会出现小数点位置计算溢出,大概5e-324 后会越界,就应该调整对应的概率的倍数adjust_prob_times。
长句子匹配结果
result: 在/这/一/年/中/,/中国/的/改革/开放/和/现代/化/建设/继续/向前/迈进/。/国民/经济/保持/了/“/高/增长/、/低/通胀/”/的/良好/发展/态势/。/农业/生产/再次/获得/好/的/收成/,/企业/改革/继续/深化/,/人民/生活/进/一/步/改善/。/对外/经济/技术/合作/与/交流/不断/扩大/。
耗时: 0.1535935401916504
四、学习体会和问题
在算法构建过程中突然想到设置patience的问题,来提高匹配出来的分词结果的准确率,比如一些比较长的词汇(比如“广东外语外贸大学”,在匹配到“广东外”的时候就停止了),设置early_stop和对应的patience值可以提高准确率,又可以约束对应的匹配时间不会很长(如果不设置early_stop和patience的话那么就每一次都是整个句子遍历,时间开销太大)。遇到小数位溢出问题,尝试设置参数提高对应概率的倍数来动态调整,应对不同长度的句子的变化。