jieba分词的官方github的地址:https://github.com/fxsjy/jieba,具体的实现代码其中也有,本文对实现过程进行简单的介绍。
结巴分词的原理主要分为4步:
- 根据词典构建前缀词典
- 通过分词的语料构建语料的有向无环图(DAG)
- 采用了动态规划思想,查找有向无环图最大概率路径, 找出基于词频的最大切分组合
- 对未登录词(词典中不存在的词),基于隐马尔可夫模型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)}