结巴分词是一个受你们喜好的分词库,源码地址为github,今天咱们就跟进源码,看一下结巴分词的原理git
def cut(self, sentence, cut_all=False, HMM=True): ''' The main function that segments an entire sentence that contains Chinese characters into separated 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. '''
使用结巴分词的时候,有三种模式,这三种模式的进入条件分别为:github
if cut_all: cut_block = self.__cut_all elif HMM: cut_block = self.__cut_DAG else: cut_block = self.__cut_DAG_NO_HMM
首先咱们看一下这三种模式算法
__cut_all:app
__cut_DAG:函数
__cut_DAG_NO_HMM:spa
下面咱们就来分析一下这三种模式:
这三种模式有一个共同点,第一步都是先构造DAG,也就是构造有向无环图。
源码以下:code
def get_DAG(self, sentence): self.check_initialized() DAG = {} N = len(sentence) for k in xrange(N): tmplist = [] i = k frag = sentence[k] 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
若是sentence是'我来到北京清华大学‘,那么DAG为blog
{0: [0], 1: [1, 2], 2: [2], 3: [3, 4], 4: [4], 5: [5, 6, 8], 6: [6, 7], 7: [7, 8], 8: [8]}
直观上来看,DAG[5]=[5,6,8]的意思就是,以’清‘开头的话,分别以五、六、8结束时,能够是一个词语,即’清‘、’清华‘、’清华大学‘
get_DAG方法中,最重要的也就是self.FREQ了,它是怎么来的呢?ip
其实就是经过jieba目录下,dict.txt文件来产生的self.FREQ,方法以下:
dict.txt共有349046行,每一行格式为:unicode
一 217830 m 一一 1670 m 一一二 11 m 一一例 3 m 一一分 8 m 一一列举 34 i
第一部分为词语,第二部分为该词出现的频率,第三部分为该词的词性。
以读取’一一列举‘为例子,首先执行self.FREQ['一一列举']=34,而后会检查’一‘、’一一‘、’一一列‘、’一一列举‘以前是否在self.FREQ中存储过,若是以前存储过,则跳过,不然执行self.FREQ['一']=0,self.FREQ['一一']=0,self.FREQ['一一列']=0
因此self.FREQ中不止存储了正常的词语和它出现的次数,同时也存储了全部词语的前缀,并将前缀出现的次数设置为0,以和正常词语区别开。
好了,如今DAG这部分咱们介绍完了,而后咱们分开来介绍一下这三种模式:
源码以下:
def __cut_all(self, sentence): 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: for j in L: if j > k: yield sentence[k:j + 1] old_j = j
这个具体的遍历方式咱们就不细说了,你们自行看源码吧
def __cut_DAG(self, sentence): DAG = self.get_DAG(sentence) route = {} self.calc(sentence, DAG, route) ......
首先咱们先看一下self.calc方法
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])
这里使用了一个技巧,也就是log(a) + log(b) = log(ab),从而巧妙的避过了乘法,也就避免了溢出的风险。
其实calc函数就是实现了vertibi算法,不了解vertibi算法的同窗自行百度吧。
而后再贴上整个__cut_DAG的源码:
def __cut_DAG(self, sentence): DAG = self.get_DAG(sentence) route = {} 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: 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
其中,重点关注这一部分
if not self.FREQ.get(buf): recognized = finalseg.cut(buf) for t in recognized: yield t
何时会进入finalseg.cut(buf)呢?实际上,就是当遇到一些dict.txt中没出现的词的时候,才会进入这个函数:
在这个函数中,就是使用HMM的方法,对这些未识别成功的词进行标注,而后咱们来介绍一下项目中相关的内容:
其中,prob_start.py存储的是HMM的起始状态相关的信息,文件中的数字都通过log处理过:
P={'B': -0.26268660809250016, 'E': -3.14e+100, 'M': -3.14e+100, 'S': -1.4652633398537678}
B表明begin,E表明end,M表明middle,S表明single。因此在开始时,HMM的状态只多是S或者B,而E和M为负无穷
prob_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}}
prob_emit.py中存储的是在该状态下出现该汉字的几率,例如p('刘'|S)=-0.916
P={'B': {'\u4e00': -3.6544978750449433, '\u4e01': -8.125041941842026, '\u4e03': -7.817392401429855, '\u4e07': -6.3096425804013165, '\u4e08': -8.866689067453933, '\u4e09': -5.932085850549891, '\u4e0a': -5.739552583325728, '\u4e0b': -5.997089097239644, '\u4e0d': -4.274262055936421, '\u4e0e': -8.355569307500769, ......
经过这种方式,也就能够进行分词了。
‘我/ 来到/ 北京/ 清华大学’对应的状态应该为'SBEBEBMME'
其实__cut_DAG_NO_HMM和__cut_DAG的区别就是:对vertibi未成功切分的部分,__cut_DAG_NO_HMM没有使用HMM进行分词。源码以下:
def __cut_DAG_NO_HMM(self, sentence): DAG = self.get_DAG(sentence) route = {} self.calc(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 = ''