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

1 概述

汉语自动分词是把没有明显分界标志的字串切分为词串。包括:标点符号、数字、数学符号、各种 标记、人名、地名、机构名等未登录词的识别。本篇博客使用Python编程语言实现基于概率最大化的中文分词算法。

2 理论描述

基于概率的自动分词算法
(1)基本思想:选择概率最大的分词路径作为最优结果
(2)利用字与字间、词与词间的同现频率作为分词的依据, 可以没有建立好的词典。需要大规模的训练文本, 用来训练模型参数。
(3)优点:不受应用领域的限制;
(4) 缺点:训练文本的选择将影响分词结果。

3 算法描述

(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的分词结。

4 详例描述

输入:结合成分子时
第一步:遍历整句话,找出候选词和在语料库中的词频

候选词 词频
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% 分子、子 分子

第四步:输出结果

结合/成/分子/时

5 实现代码

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))

6 总结

这里使用的词典格式是词,语料库中出现的次数,词频,大家可以根据实际字典修改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函数即可。

你可能感兴趣的:(中文分词,概率最大化,自然语言处理)