jieba源碼研讀筆記(五) - 分詞之全模式
前言
根據jieba文檔,jieba的分詞共包含三種模式,分別是:全模式、精確模式及搜索引擎模式。
其中的精確模式又分為不使用HMM兩種模式或使用HMM(在jieba中為默認模式)兩種。
所以分詞總共有四種模式可以使用。
筆者將依全模式→精確模式(不使用HMM)→精確模式(使用HMM發現新詞)→搜索引擎模式的順序來介紹這四種分詞模式。了解完四種分詞模式後,再繼續看作為分詞函數入口的cut
以及tokenize
函數。
本篇介紹全模式,對應的是__cut_all
函數。
jieba的README中關於全模式分詞的介紹:
基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG)
而建立DAG的工作就是由get_DAG
這個函數負責。
本篇將依序介紹get_DAG
函數及__cut_all
函數。
get_DAG函數
我們可以將句子(sentence
)想像成一個圖(一種數據結構),圖中的每個節點是句中的一個字。
現在我們手上有一個字典(self.FREQ
),我們希望能將上面這個句子中可以成詞的部份,用一個有向的edge連接起來。
get_DAG
這個函數的目的就是利用手上有的字典,將句子表示成一個有向無環圖(DAG
)。
在以下代碼中,DAG
是一個把詞首索引對應到一個list的字典。list裡的每個元素是所有可以與該詞首成詞的詞尾索引。
可以先滑到下面看一下完成後的DAG
,這樣在讀代碼時會比較有體會。
def get_DAG(self, sentence):
#這段代碼裡會用到self.FREQ,所以需要確保對象己經初始化
self.check_initialized()
DAG = {}
N = len(sentence)
#如果是使用Python3
#這裡的xrange將指向range(這是在_compat.py裡做的)
for k in xrange(N):
tmplist = []#用來儲存詞尾的索引
i = k #當前cursor的位置,用來找出詞尾
frag = sentence[k] #當前的字段,表示句子的第k個字到第i個字
#在gen_pfdict中為self.FREQ賦予了值(可以參考https://blog.csdn.net/keineahnung2345/article/details/86977785#gen_pfdict_133)
#self.FREQ中包含的是存在字典裡的詞,他們的詞頻由字典給出,
#另外對於self.FREQ中的所有詞,它還把這些詞的前n個字都加入self.FREQ,並將它們的詞頻設為0
while i < N and frag in self.FREQ:
#因為只有當句子的片段frag在self.FREQ裡面時,才會繼續尋找
#為了避免還沒看到詞尾就跳出迴圈,
#所以在gen_pfdict裡才會將詞的前n個字都加入self.FREQ的原因
#這裡檢查sentence裡的第k到第i個字(即frag)是否成詞(也就是檢查frag這個片段的詞頻是否大於0)
if self.FREQ[frag]:
#如果frag成詞的話,就把i加入tmplist裡,表示句中的第k個字到第i個字可以成詞
#實際上的字段frag是sentence[k:i+1]
tmplist.append(i)
#繼續看下一個字
i += 1
frag = sentence[k:i + 1]
if not tmplist:#如果沒有字可與sentence[k]成詞
#單字成詞
tmplist.append(k)
#DAG記錄的是sentence裡的第k個字可以跟句字裡的哪些字組成詞
DAG[k] = tmplist
return DAG
測試一下get_DAG
這個函數:
import jieba
jieba.get_DAG("我明天要去上海") #{0: [0], 1: [1, 2], 2: [2], 3: [3], 4: [4], 5: [5, 6], 6: [6]}
先把結果畫成圖:
可以從圖中看到,句中的每個字都能成詞。
並且句中第2到第3個字可成詞(明天
),且第6到第7個字也可成詞(上海
)。
要注意的是get_DAG
函數會將非漢字的字元都切成單字詞。因此在jieba源碼研讀筆記(六) - 分詞之精確模式(使用動態規劃)中,會需要re_eng
來對get_DAG
的結果進行後處理。
__cut_all函數
__cut_all
函數會利用get_DAG
函數建立的dag
來找出句中所有可以成詞的部份。
注意到以下分詞函數是在句中的某個字無法與其它字成詞時,才會輸出該單字詞。
def __cut_all(self, sentence):
#dag表示句中的第幾個字到第幾個字可以成詞
dag = self.get_DAG(sentence)
old_j = -1 #前一個詞的詞尾,初始值為0-1=-1
#dag是一個字典,鍵代表詞首索引,值則是一個list,裡面的每個元素代表能與鍵成詞的詞尾索引
#用在這裡的話,便是:從第k個字開始,到L中的第j個字都可以成詞
# iteritems定義於_compat.py
for k, L in iteritems(dag):
#從get_DAG裡可以知道,當L的長度為1時,表示第k個字單字成詞
#單字成詞在一般情況下不輸出
#只有當第k個字無法與之前的字組詞(不被前面的字所組的詞所包含)時才會輸出
if len(L) == 1 and k > old_j:
yield sentence[k:L[0] + 1] #這時的L[0]+1其實就是k+1
old_j = L[0]#詞尾設為k
else:
for j in L:
#如果有多字的詞,就略過單個字的詞
#(詞尾j=詞首k表示一個單字詞)
if j > k:
yield sentence[k:j + 1]
old_j = j#更新詞尾
使用以下敘述來調用__cut_all
函數:
jieba.lcut("我明天要去上海", cut_all=True) #['我', '明天', '要', '去', '上海']
jieba.lcut("世界卫生组织", cut_all=True) #['世界', '世界卫生', '世界卫生组织', '卫生', '卫生组织', '组织']
jieba.lcut("這本書是莫言的", cut_all=True) #['這', '本', '書', '是', '莫言', '的']
以下這個例子可以解釋if len(L) == 1 and k > old_j
所做的事:
jieba.lcut("這本書是莫言的", cut_all=True) #['這', '本', '書', '是', '莫言', '的']
句中的最後一個字的
無法與前面的任何一個字成詞,在這種情況下,__cut_all
才會輸出的
。
要注意的是__cut_all
函數並未對非漢字的字元做處理。因此在jieba源碼研讀筆記(八) - 分詞函數入口cut及tokenizer函數才需要一個re_han
來過濾出__cut_all
能處理的詞。