【一】詞典加載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分詞的核心理論基礎爲:統計和機率論。不知道基於深度學習的算法,是否能夠進行中文分詞。