jieba分词原理:构建前缀词典,形成语料的有向无环图,动态规划求解最优路径

jieba分词的官方github的地址https://github.com/fxsjy/jieba,具体的实现代码其中也有,本文对实现过程进行简单的介绍。

结巴分词的原理主要分为4步:

  1. 根据词典构建前缀词典
  2. 通过分词的语料构建语料的有向无环图(DAG)
  3. 采用了动态规划思想,查找有向无环图最大概率路径, 找出基于词频的最大切分组合
  4. 对未登录词(词典中不存在的词),基于隐马尔可夫模型HMM,使用了 Viterbi 算法进行预测

jieba0.40版本以上提供的paddle版的分词是百度提供PaddlePaddle深度学习框架,训练序列标注(双向GRU)网络模型实现分词

一、前缀词典的构建:

1.词典介绍

官方提供了3个词典文件,如下图所示,dict.text.big和dict.text.small以及默认字典dict.txt,big的词汇量584429,small的词汇量109750,dict的词汇量349046;**词典文件的格式,**每行包含一个词,如:云愁雨怨 3 i ,第一列为词汇,第二列为词频,第三列为词性,中间以空格分开。
在这里插入图片描述
在这里插入图片描述

2.前缀词典

结巴前缀词实现原理:读取词典文件中的所有词,构建每个词的前缀词,词典中存在的前缀词,词频为原始词频,未登录词,词频设置为0;统计词频的母的是后期计算每条路径的概率使用。

前缀词的示例:

词汇:美团点评
词汇的前缀词:美 美团 美团点 美团点评

前缀词典构建的代码:

def pre_dict(path):
    """
    构建前缀词典
    :param path: 词典路径
    :return: word_dcit词典和total总的词频
    """
    word_dict = dict()
    # 统计词频
    total = 0
    file_obj = open(path, 'rb')
    for lineno, line in enumerate(file_obj, 1):
        line = line.strip().decode('utf-8')
        word, freq, _ = line.split()  # 获取词汇和词频
        word_dict[word] = int(freq)
        total += int(freq)
        n = len(word)
        # 构建每个词前缀词,未登录词词频设置为0
        for ch in range(n):
            pre_word = word[:ch + 1]
            if pre_word not in word_dict:
                word_dict[pre_word] = 0  # 未登录词词频为0
    return word_dict, total


if __name__ == '__main__':
    path = './dict.txt'
    word_dict, total = pre_dict(path)
    print(word_dict)
    print(total)

测试结果:默认词典dict.txt总的词频total为60101967,前缀词典部分的结果如下:对未登录词如龟龙、龟龙片的词频为0,登录词 龟鳖为字典原有的词频。

{'龟镜': 3, '龟鳖': 3, '龟鹤遐寿': 3, '龟鹤': 0, '龟鹤遐': 0, '龟龄鹤算': 3, '龟龄': 0, '龟龄鹤': 0, '龟龙片甲': 3, '龟龙': 0, '龟龙片': 0, '龟龙麟凤': 3, '龟龙麟': 0, '龠': 5, '龢': 732}

二、语料的有向无环图:

结巴实现原理:遍历分词语料每个字可能构成的所有词,在字典中存在的词都记录其下标,每个字可能存在多种存在的成词可能性,可以构成有向无环图,根据有向无环图计算每种成词概率,最大概率的路径为最佳的分词可能。
**jieba采用了dict结构表示dag,**最终的dag是以{k : [k ,j , …] , m : [m , p , q] , …}的字典结构存储,其中k和m为词在文本sentence中的位置,k对应的列表列表存放的是sentence中以k开始的可能的词语的结束位置,这样通过查找前缀词典就可以得到词。
示例:

 text = '我毕业于北京大学'  # 语料
 # 每个字的可能成词可能,即所谓的后缀词

在这里插入图片描述

 # 示例 :我 所有的后缀词 如上图所示
 ![# 我 我毕 我毕业 我毕业于 我毕业于北 我毕业于北京 我毕业于北京大 我毕业于北京学 
 # 如果:我毕 在词典中不存在,循环结束,就开始下一个字 毕 的所有后缀词的判断

有向无环图的代码:

def get_dag(text, word_dict):
    """
    有向无环图
    :param text: 语料
    :param word_dict: 前缀词典
    :return: 有向无环图的字典
    """
    dag_dict = dict()
    n = len(text)
    # 遍历文本的每个字
    for i in range(n):
        # 每个字可能构成词典中存在后缀词的下表列表
        temp_list = list()
        word = text[i]
        k = i
        # 构成每个字的后缀词
        # 判断词是否存在词典中
        while k < n and word in word_dict:
            # 判断该词词频大于0,加入列表中
            if word_dict[word]:
                temp_list.append(k)
            k += 1
            # 下一个词
            word = text[i:k + 1]
        # 如果temp_list列表为空,则后缀词不存在
        if not temp_list:
            temp_list.append(i)
        # 添加所有可能成词的索引到字典
        dag_dict[i] = temp_list
    return dag_dict


if __name__ == '__main__':
    path = './dict.txt'
    word_dict, total = pre_dict(path)
    text = '我毕业于北京大学'
    dag_dict = get_dag(text, word_dict)
    print(dag_dict)

有向无环图结果:

# 输出结果
{0: [0], 1: [1, 2], 2: [2], 3: [3], 4: [4, 5, 7], 5: [5], 6: [6, 7], 7: [7]}

对于结果中

# 所有可能的成词
0   我   我
1   毕   毕   毕业
2   业   业
3   于   于
4   北   北   北京  北京大学
5   京   京
6   大   大   大学
7   学   学

三、动态规划,最大概率路径:

实现原理:jieba分词中计算最大概率路径的主函数是calc(self, sentence, DAG, route),函数根据已经构建好的有向无环图计算最大概率路径。

函数是一个自底向上的动态规划问题,它从sentence的最后一个字(N-1)开始倒序遍历sentence的每个字(idx)的方式,计算子句sentence[idx ~ N-1]的概率对数得分。然后将概率对数得分最高的情况以(概率对数,词语最后一个位置)这样的元组保存在route中。
函数中,logtotal为构建前缀词频时所有的词频之和的对数值,这里的计算都是使用概率对数值,可以有效防止下溢问题

1.有向无环图构成可能的路径

在这里插入图片描述

# 根据有向无环图建立所有的可能路径
# 路径1
0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7
# 路径2
0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6,7
# 路径3
0 -> 1 -> 2 -> 3 -> 4,5,6,7
# 路径4
0 -> 1 -> 2 -> 3 -> 4,5 -> 6,7
# 路径5
0 -> 1 -> 2 -> 3 -> 4,5 -> 6 -> 7
# 路径6
0 -> 1,2 -> 3 -> 4 -> 5 -> 6 -> 7
# 路径7
0 -> 1,2 -> 3 -> 4 -> 5 -> 6,7
# 路径8
0 -> 1,2 -> 3 -> 4,5 -> 6 -> 7
# 路径9
0 -> 1,2 -> 3 -> 4,5 -> 6,7
# 路径10
0 -> 1,2 -> 3 -> 4,5,6,7

2. 动态规划计算

代码:

def calc(text, dag_dict, total, word_dict):
    # 新建字典
    route = {}
    N = len(text)
    # 初始化末尾为(0,0)
    route[N] = (0, 0)
    # 总的词频对数
    logtotal = log(total)
    # 从后向前计算
    for idx in range(N - 1, -1, -1):
        route[idx] = max((log(word_dict.get(text[idx:x + 1]) or 1) -
                          logtotal + route[x + 1][0], x) for x in dag_dict[idx])
    return route


if __name__ == '__main__':
    path = './dict.txt'
    word_dict, total = pre_dict(path)
    text = '我毕业于北京大学'
    dag_dict = get_dag(text, word_dict)
    res = calc(text, dag_dict, total, word_dict)
    print(res)

结果:

{8: (0, 0), 7: (-8.142626068614787, 7), 6: (-8.006816355818659, 7), 5: (-17.126123636106, 5), 4: (-10.284495710736286, 7), 3: (-16.62319546494139, 3), 2: (-26.111206957361826, 2), 1: (-25.83723584715709, 2), 0: (-31.045459344658283, 0)}
  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值