jieba分词中文分词:源码地址:https://github.com/fxsjy/jieba
其特点:
-
支持三种分词模式:
- 精确模式,试图将句子最精确地切开,适合文本分析;
- 全模式,把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义;
- 搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。
-
支持繁体分词
-
支持自定义词典
-
MIT 授权协议
jieba分词中用到的算法:
- 基于字典树结构实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图(DAG)
- 采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合
- 对于未登录词,采用了基于汉字成词能力的HMM模型,使用了Viterbi算法
一、基于字典树结构实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图(DAG)
gen_pfdict加载dict.txt生成字典树,lfreq存储dict.txt每个词出现了多少次,以及每个词的所有前缀,前缀的频数置为0,ltotal是所有词出现的总次数,得到的字典树存在FREQ中。
get_DAG函数是根据FREQ对于每个句子sentence生成一个有向无环图,图信息存在字典DAG中,其中DAG[pos]是一个列表[a, b, c...],pos从0到len(sentence) - 1,表示sentence[pos : a + 1],sentence[pos, b + 1]...这些单词出现在了dict中。
def gen_pfdict(f):
lfreq = {}
ltotal = 0
#f_name = resolve_filename(f)
f_name = f.split('/')[-1]
#既遍历索引,又遍历元素,且指定起始索引为 1
#对于遍历对象是文件而言,元素为每一行的文本
with open(f,'r',encoding = 'utf-8') as fr:
for lineno, line in enumerate(fr.readlines(), 1):
try:
#print(line.strip())
line = line.strip()
word, freq = line.split(' ')[:2]
freq = int(freq)
lfreq[word] = freq #将文件中的词和频率构成字典
ltotal += freq #ltotal是所有频率之和
#以下是将字典中词的拆分词也加入字典
#例如:不拘一格是在字典中的,然后将:不:0;不拘:0;不拘一:0;也加入字典中
for ch in range(len(word)):
wfrag = word[:ch + 1]
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
#根据字典树FREQ生成有向无环图
#举例实现过程:
#以frag变量的变化:但(在字典中) 但也(在上一个的基础上加一个字,判断是否在字典中,在,则继续,不在,则换下一个字开始,如也)
def get_DAG(FREQ, sentence):
#self.check_initialized()
DAG = {}
N = len(sentence)
for k in range(N):
tmplist = []
i = k
frag = sentence[k]
while i < N and frag in FREQ: #从第k个字起始,直到找到在字典(不是字典树,字典树含词的前缀)中的最长的词
if FREQ[frag]: #退出条件是:最长的词在加一个词,不在字典树中
tmplist.append(i)
i += 1
frag = sentence[k:i + 1]
if not tmplist: #若tmplist为空时(若该词不在字典中),则直接将该词在句中的序号直接放到tmplist
tmplist.append(k)
DAG[k] = tmplist
return DAG
二、采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合
#动态规划求解最大路径,其中route中的值是元组,元组的第一个元素是Rmaxi,而第二个元素则保存的切分的位置信息
"""
当sentence = '我知道你们很好'时,产生以下结果:
route:利用cal函数计算出来的
{0: (-33.64133614006646, 0),
1: (-28.433112642565263, 2),
2: (-27.243655624028406, 2),
3: (-21.18538549744644, 4),
4: (-20.529392523698085, 4),
5: (-13.244324151007824, 5),
6: (-6.476124447406034, 6),
7: (0, 0)}
__cut_DAG_NO_HMM函数则根据上述计算出的最佳路径,得到拆分结果:
例如:0-1,1-3,3-5,5-6,6-7,即:x(上一个的结尾,初始为0)-route[x][1]+1,
"""
def calc(FREQ, total, sentence, DAG, route):
N = len(sentence)
route[N] = (0, 0)
logtotal = log(total)
#log(FREQ.get(sentence[idx:x + 1]) or 1)获取句中任意两个相邻字的在字典中的频率,如果,该词不在,则词频为0
#route的形式是字典,值是元组形式,元组前一个是该词的频率的log形式-总log频率
#对于整个句子的最优路径Rmax和一个末端节点Wx,可能存在对个前驱Wi,Wj,Wk,设到达Wi,Wj,Wk的最大路径分别为Rmaxi,Rmaxj,Rmaxk,则:
#Rmax = max(Rmaxi,Rmaxj,Rmaxk)+weight(Wx)
#重复子问题是:从倒数第二个字往前遍历句子,遍历的字作为末端节点,不断求Rmax
for idx in range(N - 1, -1, -1):
route[idx] = max((log(FREQ.get(sentence[idx:x + 1]) or 1) -
logtotal+ route[x + 1][0] , x) for x in DAG[idx])
__cut_DAG_NO_HMM是不使用hmm的精确模式,首先调用calc,得到的route[i][1]中保存的是切分的位置信息,然后遍历输出切分方式。
#jieba字典字典中没有单个的26个字母,因此对于连续的英文,默认是不拆分,但与中文是分开的
#中文则按照动态规划求解的最大路径
def __cut_DAG_NO_HMM(FREQ,total,sentence):
DAG = get_DAG(FREQ,sentence)
route = {}
calc(FREQ, total, sentence, DAG, route)
x = 0
N = len(sentence)
buf = ''
while x < N:
y = route[x][1] + 1
l_word = sentence[x:y]
if re_eng.match(l_word) and len(l_word) == 1:
buf += l_word
x = y
else:
if buf:
yield buf
buf = ''
yield l_word
x = y
if buf:
yield buf
buf = ''
三、对于未登录词,采用了基于汉字成词能力的HMM模型,使用了Viterbi算法
__cut_DAG同时使用最大概率路径和hmm,对于利用动态规划计算出的最大概率切分后,用buf将连续的单字收集以及未登录词收集起来,再调用finalseg.cut利用hmm进行分词。
该函数封装在finalseg模块中,主要通过 __cut 函数来进行进一步的分词,代码如下:
def __cut(sentence):
global emit_P
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]
if pos == 'B':
begin = i
elif pos == 'E':
yield sentence[begin:i + 1]
nexti = i + 1
elif pos == 'S':
yield char
nexti = i + 1
if nexti < len(sentence):
yield sentence[nexti:]
其中,__cut()通过调用viterbi算法得到概率和path之后,对sentence进行分词。
viterbi算法代码如下:
#维特比算法,给定了模型参数和观察序列之后求隐藏状态序列,
#其中对于分词,观察序列就是句子本身,而隐藏序列就是一个由{B, M, E, S}组成的序列,B表示词的开始,M表示词的中间,E表示词的结尾,S表示单字成词。
#输入参数中obs是输入的观察序列,即句子本身,states表示隐藏状态的集合,即{B, M, E, S},
#start_p表示第一个字分别处于{B, M, E, S}这几个隐藏状态的概率,
#trans_p是状态转移矩阵,记录了隐藏状态之间的转化概率,emit_p是发射概率矩阵,表示从一个隐藏状态转移到一个观察状态的概率
def viterbi(obs, states, start_p, trans_p, emit_p):
V = [{}] #V是一个列表,V[i][j]表示对于子观察序列obs[0 ~ i],在第i个位置时隐藏状态为j的最大概率
path = {} #记录了状态转移的路径。
for y in states: # init
V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT) #计算初始状态概率,由于概率值做了对数化,所以乘号变成了加号
path[y] = [y]
for t in range(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
#求出最后一个字哪一种状态的对应概率最大,最后一个字只可能是两种情况:E(结尾)和S(独立词)
(prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')
return (prob, path[state])