2021SC@SDUSC
2021SC@SDUSC
本文主要解决分词的另一块:未登陆词,也就是我们常说的新词。对于这些新词,我们前面所说的前缀词典中是不存在的,那么之前的分词方法自然就不能适用了。为了解决这一问题,jieba使用了隐马尔科夫(HMM)模型。在上上篇博客也曾经提到过。这篇文章会详细讲下发现新词的函数代码。
搜索引擎模式的分词方法在一些业务场景是需要的,但是它的进一步切分方法比较粗暴,这里可以修改代码做一些定制化的分词规则,比如切出4字词,切出重要的1字词,某些词不可切等定制化分词规则,以获取自己想要的分词结果。
jieba分词会首先调用函数cut(sentence),cut函数会先将输入句子进行解码,然后调用__cut函数进行处理。__cut函数就是jieba分词中实现HMM模型分词的主函数。__cut函数会首先调用viterbi算法,求出输入句子的隐藏状态,然后基于隐藏状态进行分词。
def __cut(sentence):
"""
jieba首先会调用函数cut(sentence),cut函数会先将输入的句子进行解码,
然后调用__cut()函数进行处理。该函数是实现HMM模型分词的主函数
__cut()函数首先调用viterbi算法, 求出输入句子的隐藏状态,然后基于隐藏状态分词
"""
global emit_P
# 通过viterbi算法求出隐藏状态序列
prob, pos_list = viterbi(sentence, 'BMES', start_P, trans_P, emit_P)
begin, nexti = 0, 0
# print pos_list, sentence
# 基于隐藏状态进行分词
for i, char in enumerate(sentence):
pos = pos_list[i]
# 如果字所处的位置是开始位置 Begin
if pos == 'B':
begin = i
# 如果字所处的位置是结束位置 END
elif pos == 'E':
# 这个子序列就一个分词
yield sentence[begin:i + 1]
nexti = i + 1
# 如果单独成字 Single
elif pos == 'S':
yield char
nexti = i + 1
# 剩余的直接作为一个分词,返回
if nexti < len(sentence):
yield sentence[nexti:]
关于viterbi算法的代码解释具体如下,在这里主要针对分词,所以只简单介绍一下。
def viterbi(obs, states, start_p, trans_p, emit_p):
"""
viterbi函数会先计算各个初始状态的对数概率值,
然后递推计算,每时刻某状态的对数概率值取决于
上一时刻的对数概率值、
上一时刻的状态到这一时刻的状态的转移概率、
这一时刻状态转移到当前的字的发射概率三部分组成。
"""
V = [{}] # tabular表示Viterbi变量,下标表示时间
path = {} # 记录从当前状态回退路径
# 时刻t=0,初始状态
for y in states: # init
V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)
path[y] = [y]
# 时刻t = 1,...,len(obs) - 1
for t in xrange(1, len(obs)):
V.append({})
newpath = {}
# 当前时刻所处的各种可能的状态
for y in states:
# 获得发射概率对数
em_p = emit_p[y].get(obs[t], MIN_FLOAT)
# 分别获得上一时刻的状态的概率对数、该状态到本时刻的状态的转移概率对数
# 以及本时刻的状态的发射概率
(prob, state) = max(
[(V[t - 1][y0] + trans_p[y0].get(y, MIN_FLOAT) + em_p, y0) for y0 in PrevStatus[y]])
V[t][y] = prob
# 将上一时刻最有的状态 + 这一时刻的状态
newpath[y] = path[state] + [y]
path = newpath
# 最后一个时刻
(prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')
#返回最大概率对数和最有路径
return (prob, path[state])