基于概率最大化的分词算法

一、算法介绍

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的话那么就每一次都是整个句子遍历,时间开销太大)。遇到小数位溢出问题,尝试设置参数提高对应概率的倍数来动态调整,应对不同长度的句子的变化。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值