大纲
- 1 jieba系统简介
- 2. jieba系统框架
- 3. jieba分词简介
- 4. 实例讲解
- 4.1 前缀词典构建
- 4.2 有向无环图构建
- 4.3 最大概率路径计算
- 5 源码分析
- 5.1 算法流程
- 5.2 前缀词典构建
- 5.3 有向无环图构建
- 5.4 最大概率路径计算
- 总结:
1 jieba系统简介
"结巴"中文分词:做最好的Python中文分词组件。
特点:
- 支持三种分词模式:精确模式,全模式,搜索引擎模式
- 支持繁体分词
- 支持自定义词典
- MIT授权协议
涉及算法:
- 基于前缀词典实现词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图(DAG),采用动态规划查找最大概率路径,找出基于词频的最大切分组合;
- 对于未登录词,采用了基于汉字成词能力的 HMM模型,采用Viterbi算法进行计算;
- 基于Viterbi算法的词性标注;
- 分别基于 TFIDF 和 TextRank 模型抽取关键词;
2. jieba系统框架
jieba分词系统,主要实现三个模块,
- 分词
- 词性标注
- 关键词抽取
其中,分词有三种模式,默认是精确模式,
- 精确模式,试图将句子最精确地切开,适合文本分析;
- 全模式,把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义;
- 搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词;
3. jieba分词 简介
jieba分词主要是基于统计词典,构造一个前缀词典;然后利用前缀词典对输入句子进行切分,得到所有的切分可能,根据切分位置,构造一个有向无环图(DAG);通过动态规划算法,计算得到最大概率路径,也就得到了最终的切分形式。
4. 实例讲解
以“去北京大学玩”为例,作为待分词的输入文本。
离线统计的词典形式如下,每一行有三列,第一列是词,第二列是词频,第三列是词性。
# dic.txt
...
北京大学 2053 nt
大学 20025 n
去 123402 v
玩 4207 v
北京 34488 ns
北 17860 ns
京 6583 ns
大 144099 a
学 17482 n
...
4.1 前缀词典构建
首先是基于统计词典构造前缀词典,如统计词典中的词“北京大学”的前缀分别是“北”、“北京”、“北京大”;词“大学”的前缀是“大”。
统计词典中所有的词形成的前缀词典如下所示,你也许会注意到“北京大”作为“北京大学”的前缀,但是它的词频却为0,这是为了便于后面有向无环图的构建。
# 前缀词典
...
北京大学 2053
北京大 0
大学 20025
去 123402
玩 4207
北京 34488
北 17860
京 6583
大 144099
学 17482
...
4.2 有向无环图构建
然后基于前缀词典,对输入文本进行切分,对于“去”,没有前缀,那么就只有一种划分方式;对于“北”,则有“北”、“北京”、“北京大学”三种划分方式;对于“京”,也只有一种划分方式;对于“大”,则有“大”、“大学”两种划分方式,依次类推,可以得到每个字开始的前缀词的划分方式。
在jieba分词中,对每个字都是通过在文本中的位置来标记的,因此可以构建一个以位置为key,相应划分的末尾位置构成的列表为value的映射,如下所示
0: [0]
1: [1,2,4]
2: [2]
3: [3,4]
4: [4]
5: [5]
对于0: [0],表示位置0对应的词,就是0 ~ 0,就是“去”;对于1: [1,2,4],表示位置1开始,在1,2,4位置都是词,就是1 ~ 1,1 ~ 2,1 ~ 4,即“北”,“北京”,“北京大学”这三个词。
对于每一种划分,都将相应的首尾位置相连,例如,对于位置1,可以将它与位置1、位置2、位置4相连接,最终构成一个有向无环图,如下所示
4.3 最大概率路径计算
在得到所有可能的切分方式构成的有向无环图后,我们发现从起点到终点存在多条路径,多条路径也就意味着存在多种分词结果,例如,
# 路径1
0 -> 1 -> 2 -> 3 -> 4 -> 5
# 分词结果1
去 / 北 / 京 / 大 / 学 / 玩
# 路径2
0 -> 1 , 2 -> 3 -> 4 -> 5
# 分词结果2
去 / 北京 / 大 / 学 / 玩
# 路径3
0 -> 1 , 2 -> 3 , 4 -> 5
# 分词结果3
去 / 北京 / 大学 / 玩
# 路径4
0 -> 1 , 2 , 3 , 4 -> 5
# 分词结果4
去 / 北京大学 / 玩
...
因此,我们需要计算最大概率路径,也即按照这种方式切分后的分词结果的概率最大。在计算最大概率路径时,jieba分词采用从后往前这种方式进行计算。
为什么采用从后往前这种方式计算呢?因为,我们这个有向无环图的方向是从前向后指向,对于一个节点,我们只知道这个节点会指向后面哪些节点,但是我们很难直接知道有哪些前面的节点会指向这个节点。
在采用动态规划计算最大概率路径时,每到达一个节点,它前面的节点到终点的最大路径概率已经计算出来。
5 源码分析
5.1 算法流程
jieba.__init__.py
中实现了jieba分词接口函数cut(self, sentence, cut_all=False, HMM=True)
。
jieba分词接口主入口函数,会首先将输入文本解码为Unicode编码,然后根据入参,选择不同的切分方式,本文主要以精确模式进行讲解,因此cut_all
和HMM
这两个入参均为默认值;
切分方式选择,
re_han = re_han_default
re_skip = re_skip_default
块切分方式选择,
cut_block = self.__cut_DAG
函数 __cut_DAG(self, sentence)
首先构建前缀词典,其次构建有向无环图,然后计算最大概率路径,最后基于最大概率路径进行分词,如果遇到未登录词,则调用HMM模型进行切分。
5.1 前缀词典构建
get_DAG(self, sentence)
函数会首先检查系统是否初始化,如果没有初始化,则进行初始化。在初始化的过程中,会构建前缀词典。
构建前缀词典的入口函数是gen_pfdict(self, f)
,解析离线统计词典文本文件,每一行分别对应着词、词频、词性,将词和词频提取出来,以词为key,以词频为value,加入到前缀词典中。对于每个词,再分别获取它的前缀词,如果前缀词已经存在于前缀词典中,则不处理;如果该前缀词不在前缀词典中,则将其词频置为0,便于后续构建有向无环图。
jieba分词中gen_pfdict
函数实现如下,
# f是离线统计的词典文件句柄
def gen_pfdict(self, f):
"""构建前缀词典"""
lfreq = {} # 初始化前缀词典
ltotal = 0
f_name = resolve_filename(f)
for lineno, line in enumerate(f, 1):
try:
line = line.strip().decode('utf-8') # 解析离线词典文本文件
word, freq = line.split(' ')[:2] # 词和对应的词频
freq = int(freq)
lfreq[word] = freq # 将词和词频加入到前缀词典
ltotal += freq
# 获取该词所有的前缀词
for ch in xrange(len(word)):
wfrag = word[:ch + 1]
# 如果该前缀词不在前缀词典中,则将对应词频设置为0, 如“北京大”
if wfrag not in lfreq:
lfreq[wfrag] = 0
except ValueError:
raise ValueError(
'invalid dictionary entry in %s at Line %s: %s' % (f_name, lineno, line))
f.close()
return lfreq, ltotal
5.2 有向无环图构建
有向无环图,directed acyclic graphs
,简称DAG,是一种图的数据结构.
因为DAG本身也是有向图,所以用邻接矩阵来表示是可行的,但是jieba采用了Python的dict
结构,可以更方便的表示DAG。
DAG是以
(不懂就看上面的4.2)
get_DAG(self, sentence)
函数进行对系统初始化完毕后,会构建有向无环图。
从前往后依次遍历文本的每个位置,对于位置k,首先形成一个片段,这个片段只包含位置k的字,然后就判断该片段是否在前缀词典中,
- 如果这个片段在前缀词典中,
1.1 如果词频大于0,就将这个位置追加到以为key的一个列表中;
1.2 如果词频等于0,如同“北京大”,则表明前缀词典存在这个前缀,但是统计词典并没有这个词,继续循环; - 如果这个片段不在前缀词典中,则表明这个片段已经超出统计词典中该词的范围,则终止循环;
- 然后该位置加1,然后就形成一个新的片段,该片段在文本的索引为
,继续判断这个新片段是否在前缀词典中。
jieba分词中get_DAG
函数实现如下,
def get_DAG(self, sentence):
"""构建有向无环图"""
# 检查系统是否已经初始化
self.check_initialized()
DAG = {} # DAG存储有向无环图的数据
N = len(sentence)
# 依次遍历文本中的每个位置
for k in xrange(N):
tmplist = []
i = k
frag = sentence[k] # 位置k形成的片段
# 判断片段是否在前缀词典中
# 如果片段不在前缀词典中,则跳出本循环,也即该片段已经超出统计词典中该词的长度
while i < N and frag in self.FREQ:
# 如果该片段的词频大于0, 将该片段加入到有向无环图中。 否则,继续循环
if self.FREQ[frag]:
tmplist.append(i)
# 片段末尾位置加1
i += 1
# 新的片段较旧的片段右边新增一个字
frag = sentence[k:i + 1]
if not tmplist:
tmplist.append(k)
DAG[k] = tmplist
return DAG
以“去北京大学玩”为例,最终形成的有向无环图为,
{0: [0], 1: [1,2,4], 2: [2], 3: [3,4], 4: [4], 5: [5]}
5.3 最大概率路径计算
5.2章节中构建出的有向无环图DAG的每个节点,都是带权的,对于在前缀词典里面的词语,其权重就是它的词频;
我们想要求得
如果需要使用动态规划求解,需要满足两个条件,
- 重复子问题
- 最优子结构
我们来分析一下最大概率路径问题,是否满足动态规划的两个条件。
(1)重复子问题
对于节点
- 任意通过
到达的路径的权重 = 该路径通过的路径权重 +的权重,也即
- 任意通过
到达的路径的权重 = 该路径通过的路径权重 +的权重,也即
即对于拥有公共前驱节点
(2)最优子结构
对于整个句子的最优路径
于是,问题转化为,求解
组成了最优子结构,子结构里面的最优解是全局的最优解的一部分。
状态转移方程为,
jieba分词中计算最大概率路径的主函数是calc(self, sentence, DAG, route)
,函数根据已经构建好的有向无环图计算最大概率路径。
函数是一个自底向上的动态规划问题,它从sentence的最后一个字(N-1)开始倒序遍历sentence的每个字(idx)的方式,计算子句
函数中,
jieba分词中calc
函数实现如下,
def calc(self, sentence, DAG, route):
"""计算最大概率路径"""
N = len(sentence)
route[N] = (0, 0) # 初始化末尾为0
logtotal = log(self.total) # 防止下溢
# 从后到前计算
for idx in xrange(N - 1, -1, -1):
route[idx] = max((log(self.FREQ.get(sentence[idx:x + 1]) or 1) -
logtotal + route[x + 1][0], x) for x in DAG[idx])
“去北京大学玩”
T代表总次数
1. idx=5 ,
2. idx=4 ,
3. idx=3 , x取3,
x取4,
后面大,取4,故
4. idx =2 ,
5.idx =1 , x取1,
x取2,
x取4,
最后一个最大,取4, 故
6.idx=0,
看这篇的4. 维特比算法
三三:文本预处理zhuanlan.zhihu.com(这里log加负号是为了转换为求最小值。)
构建有向无环图:
计算最大概率路径:
总结:
(1)jieba分词主要是基于统计词典,构造一个前缀词典;
(2)然后利用前缀词典对输入句子进行切分,得到所有的切分可能,根据切分位置,构造一个有向无环图(DAG);
(3)通过维特比算法,计算最大概率路径,也就得到了最终的切分形式。
over
第一次编辑(2020.7.13)
参考:
结巴分词1--结巴分词系统介绍 - 老顽童2007 - 博客园www.cnblogs.com 结巴分词2--基于前缀词典及动态规划实现分词 - 老顽童2007 - 博客园www.cnblogs.com