【一】词典加载python
利用jieba进行分词时,jieba会自动加载词典,这里jieba使用python中的字典数据结构进行字典数据的存储,其中key为word,value为frequency即词频。正则表达式
1. jieba中的词典以下:算法
jieba/dict.txt数据结构
X光 3 n X光线 3 n X射线 3 n γ射线 3 n T恤衫 3 n T型台 3 n
该词典每行一个词,每行数据分别为:词 词频 词性app
2. 词典的加载函数
jieba/_init_.py,源码解析以下:工具
# 加载词典 def gen_pfdict(self, f): # 定义字典,key = word, value = freq 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) # 记录词频与词的关系,这里直接采用dict存储,没有采用Trie树结构 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
在词典初始化时,会调用该接口,将词频表存储到变量FREQ中,后续进行分词时,会直接进行该词频表的查询。学习
调用点以下:指针
def initialize(self, dictionary=None): self.FREQ, self.total = self.gen_pfdict(self.get_dict_file())
【二】分词code
jieba进行分词时,使用词典的最大前缀匹配的方式。当使用精确匹配模型且启动HMM时,对于未登陆词(即词典中不存在的词),jieba会使用HMM模型对未登陆词进行分词,具体的算法是viterbi算法。
jieba分词的第一步是将待分配的文本,依据词典,生成动态图DAG。
1. 动态图的生成。
# 生成动态图 ''' 例如:sentence = '我爱北京天安门' id = 0 1 2 3 4 5 6 则 DAG = { 0:[0], 1:[1], 2:[2,3], 3:[3], 4:[4,5,6], 5:[5], 6:[6] } ''' def get_DAG(self, sentence): # 查看词典是否已经初始化,若没有,则加载词典,初始化词频表。 # 由此能够看出,jieba采用的是词典懒加载模式 self.check_initialized() # 使用字典结构存储动态图 DAG = {} # 获取待分词的句子的长度 N = len(sentence) # 从头至尾,逐字扫描 for k in xrange(N): tmplist = [] i = k # 获取句子中第k个位置的字 frag = sentence[k] # 若是该字在词频表中,则继续扫描,直到扫描出的词再也不词频表中为止 # 即jieba采用的最大前缀匹配法来搜索词 # 例如: 句子为: 我爱北京天安门,frag = 北,则该while循环会一直搜索到京, # 即搜索到北京,而tmplist里面会存储:北,北京两个词 while i < N and frag in self.FREQ: if self.FREQ[frag]: tmplist.append(i) i += 1 frag = sentence[k:i + 1] # 存储一个字到动态图中 if not tmplist: tmplist.append(k) DAG[k] = tmplist return DAG
2. 分词接口流程分析
# 分词接口 def cut(self, sentence, cut_all=False, HMM=True): ''' The main function that segments an entire sentence that contains Chinese characters into seperated words. Parameter: - sentence: The str(unicode) to be segmented. - cut_all: Model type. True for full pattern, False for accurate pattern. - HMM: Whether to use the Hidden Markov Model. ''' # 对待分词的序列进行编解码,解码成utf-8的格式 sentence = strdecode(sentence) # 依据是否使用全模式,肯定待使用的正则表达式式, # 结巴先使用正则表达式对待分割内容进行预处理,主要是去除标点符号 if cut_all: re_han = re_han_cut_all re_skip = re_skip_cut_all else: re_han = re_han_default re_skip = re_skip_default # 依据分割模式,设置分词接口,有点相似于函数指针的意思哈 if cut_all: # 全模式,使用__cut_all cut_block = self.__cut_all elif HMM: # 精确匹配模式,且使用HMM对未登陆词进行分割,则使用__cut_DAG cut_block = self.__cut_DAG else: # 精确匹配模式,但不使用HMM对未登陆词进行分割,则使用__cut_DAG_NO_HMM cut_block = self.__cut_DAG_NO_HMM # 使用正则表达式对待分词序列进行预处理 blocks = re_han.split(sentence) for blk in blocks: if not blk: continue if re_han.match(blk): # 调用分词接口进行分词 for word in cut_block(blk): yield word else: tmp = re_skip.split(blk) for x in tmp: if re_skip.match(x): yield x elif not cut_all: for xx in x: yield xx else: yield x
3. 全模式分词
def __cut_all(self, sentence): # 生成动态图 ''' 例如:sentence = '我爱北京天安门' id = 0 1 2 3 4 5 6 则 DAG = { 0:[0], 1:[1], 2:[2,3], 3:[3], 4:[4,5,6], 5:[5], 6:[6] } ''' dag = self.get_DAG(sentence) old_j = -1 # 扫描动态图 for k, L in iteritems(dag): # 若是只有一个字,且该字未在前面的词中出现过,则生成一个词,不然跳过 # 例如: ‘北京’已经成词了,再次扫描到‘京’时,须要跳过 if len(L) == 1 and k > old_j: yield sentence[k:L[0] + 1] old_j = L[0] else: #对于至少2个字的词,如 4:[4,5,6], 则分割为 天安,天安门 两个词 # 这符合jieba的全模式定义:尽可能细粒度的分词 for j in L: if j > k: yield sentence[k:j + 1] old_j = j
4. 精确模式分词,且使用HMM对未登陆词进行分割
def __cut_DAG(self, sentence): DAG = self.get_DAG(sentence) route = {} # 使用动态规划算法,选取几率最大的路径进行分词 ''' 例如:sentence = '我爱北京天安门' id = 0 1 2 3 4 5 6 则 DAG = { 0:[0], 1:[1], 2:[2,3], 3:[3], 4:[4,5,6], 5:[5], 6:[6] } ''' # 在进行 4:[4,5,6], 分词时,计算出成词的最大几率路径为4~6,即‘天安门’的几率大于‘天安’ self.calc(sentence, DAG, route) x = 0 buf = '' N = len(sentence) while x < N: y = route[x][1] + 1 l_word = sentence[x:y] if y - x == 1: buf += l_word else: if buf: if len(buf) == 1: yield buf buf = '' else: # 对于未登陆词,使用HMM模型进行分词 if not self.FREQ.get(buf): recognized = finalseg.cut(buf) for t in recognized: yield t else: for elem in buf: yield elem buf = '' yield l_word x = y if buf: if len(buf) == 1: yield buf elif not self.FREQ.get(buf): recognized = finalseg.cut(buf) for t in recognized: yield t else: for elem in buf: yield elem
5. 动态规划算法求解最大几率路径。
jieba在使用精确模式进行分词时,会将‘天安门’分割成‘天安门’,而不是全模式下的‘天安’和‘天安门’两个词,jieba时如何作到的呢?
其实,求解的核心在于‘天安门’的成词几率比‘天安’大。
5.1 先看看jieba的动态规划后的结果
''' 例如:输入的动态图以下 DAG = { 0:[0], 1:[1], 2:[2,3], 3:[3], 4:[4,5,6], 5:[5], 6:[6] } 则,返回值为: R = { 0:(f1,0), 1:(f2,1), 2:(f3,3), 3:(f3,3), 4:(f4,6), 5:(f5,5), 6:(f6,6) } 这个返回值是什么含义呢? 例如:0:(f1,0) ----> id从0到0成词几率最大,最大几率为f1 4:(f4,6), ----> id从4到6成词几率最大,最大几率为f4 依据返回值R,能够获得成词下标,0->0,1->1,2->3,4->6,即:我/爱/北京/天安门 ''' def calc(self, sentence, DAG, route): N = len(sentence) route[N] = (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])
5.2 动态规划算法
jieba使用的是1-gram模型,即 P(S) = P(W1)*P(W2)*....P(W3). 要求得P(S)取得最大值时,P(W1)/P(W2)..../P(Wn)依次取得什么值。
这里采用动态规划算法来求解该问题。即:argmax P(S) = argmax ( P(Wn)*P(Sn-1)) <=> argmax (log(P(Wn))+log(P(Sn-1)))
当使用jieba中的Route来存储最大几率路径时,即获得 R[i] = argmax(log(Wi/V) + R[i-1]),jieba中使用了一个小技巧,即倒着扫描,这与DAG中的list存储着后向节点有关系。即R[i] = argmax(log(Wi/V) + R[i+1]),因而便有了上述代码。
6. 关于未登陆词的HMM求解。
关于HMM介绍这里不作赘述,这里仅描述一下,jieba是怎么依据HMM模型进行分词的。
6.1 问题建模
jieba 使用 BMES对词进行建模。好比:我爱北京天安门,用BMES表示为:SSBEBME。只要拿到了SSBEBME这个字符串,就能够对“我爱北京天安门”进行分词,按照该字符串,分词结果为: 我/爱/北京/天安门。
那么问题来了,如何由“我爱北京天安门”获得“SSBEBME”这个字符串呢?
咱们能够将“我爱北京天安门”理解为观察结果,“SSBEBME”理解为隐藏的词的状态的迁移结果,即HMM中的隐式状态转移。那么问题就成了:如何求解:P(S|O)= P(隐式状态迁移序列|观测序列).
6.2 问题求解
依据贝叶斯公式: P(S|O) = P(S,O)/P(O) = P(O|S)P(S)/P(O)
结合HMM相关知识,进一步求解P(S|O) = P(St|Ot) = P(Ot|St)*P(St|St-1)/P(Ot),因为每个t时刻,P(Ot)都同样,能够去掉,所以:
P(St|Ot) = P(Ot|St)*P(St|St-1),其中 P(Ot|St)为发射几率,即HMM的参数Bij,P(St|St-1)是HMM的状态转移矩阵参数,即Aij。
另外还已知初始状态向量参数,所以能够求解出P(S|O)的最大值时的几率路径,也就是 “SSBEBME”。
6.3 jieba中的HMM参数值
初始状态几率在 jieba/finalseg/prop_start.py里面。
P={'B': -0.26268660809250016, 'E': -3.14e+100, 'M': -3.14e+100, 'S': -1.4652633398537678}
状态转移矩阵参数在 jieba/finalseg/prop_trans.py
P={'B': {'E': -0.510825623765990, 'M': -0.916290731874155}, 'E': {'B': -0.5897149736854513, 'S': -0.8085250474669937}, 'M': {'E': -0.33344856811948514, 'M': -1.2603623820268226}, 'S': {'B': -0.7211965654669841, 'S': -0.6658631448798212}}
发射几率在 jieba/finalseg/prop_emit.py
6.4 jieba中这些HMM的参数是怎么来的呢?
据jieba官方介绍,是采用人明日报的语料库 和另一个分词工具训练来的。总而言之,这些参数是依据特殊语料统计获得的。若是使用jieba的默认参数致使分词不理想时,应该考虑到从新训练本身的HMM参数。
6.5 jieba HMM源码解析
源码路径在 jieba/finalseg/_init_.py
主体代码流程以下:
def cut(sentence): # 先解码 sentence = strdecode(sentence) # 再按照正则表达式进行初步分割 blocks = re_han.split(sentence) for blk in blocks: if re_han.match(blk): # 依据HMM模型,对未登陆词进行分割 for word in __cut(blk): if word not in Force_Split_Words: yield word else: for c in word: yield c else: tmp = re_skip.split(blk) for x in tmp: if x: yield x
HMM分词以下:
def __cut(sentence): global emit_P # 使用viterbi算法进行HMM求解,即生成 BMES的状态迁移序列 prob, pos_list = viterbi(sentence, 'BMES', start_P, trans_P, emit_P) begin, nexti = 0, 0 # print pos_list, sentence # 依据 BMES的状态迁移序列,进行分词 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:]
这里使用了viterbi算法求解状态转移序列。关于viterbi算法,注意两点:该算法的推导时自顶向下,可是最终的计算是自底向上,体如今分词上,就是从前日后计算。求解的目的也是找到一条最大几率路径,所以也是动态规划算法。这里就不推导了。
【三】总结
jieba分词,对于登陆词,使用最大前缀匹配的方法,其中精确模式使用了动态规划算法来计算最大几率路径,进而获得最佳分词。对于未登陆词,jieba使用HMM模型来求解最佳分词。
两种方式,都用到了词典与模型的HMM模型参数。所以,对于某些分词场景,能够使用本身的词典和本身的语料库训练出来的HMM模型参数进行中文分词,进而提高分词准确性。
jieba分词的核心理论基础为:统计和几率论。不知道基于深度学习的算法,是否能够进行中文分词。