前一章介紹了jieba分詞之前關於前綴詞典的構建,本章介紹jieba的主體:jieba.cut。
jieba分詞有三種模式:全模式、精確模式、搜索引擎模式。全模式和精確模式通過jieba.cut實現,搜索引擎模式對應cut_for_search,且三者均可以通過參數HMM決定是否使用新詞識別功能。官方例子:
# encoding=utf-8
import jieba
seg_list = jieba.cut("我來到北京清華大學", cut_all=True)
print("Full Mode: " + "/ ".join(seg_list)) # 全模式
# 【全模式】: 我/ 來到/ 北京/ 清華/ 清華大學/ 華大/ 大學
seg_list = jieba.cut("我來到北京清華大學", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list)) # 精確模式
# 【精確模式】: 我/ 來到/ 北京/ 清華大學
seg_list = jieba.cut("他來到了網易杭研大廈") # 默認是精確模式
print(", ".join(seg_list))
# 【新詞識別】:他, 來到, 了, 網易, 杭研, 大廈 (此處,“杭研”並沒有在詞典中,但是也被Viterbi算法識別出來了)
seg_list = jieba.cut_for_search("小明碩士畢業於中國科學院計算所,后在日本京都大學深造") # 搜索引擎模式
print(", ".join(seg_list))
# 【搜索引擎模式】: 小明, 碩士, 畢業, 於, 中國, 科學, 學院, 科學院, 中國科學院, 計算, 計算所, 后, 在, 日本, 京都, 大學, 日本京都大學, 深造
jieba.cut
def cut(self, sentence, cut_all=False, HMM=True):
'''
jieba分詞主函數,返回generator
參數:
- sentence: 待切分文本.
- cut_all: 切分模式. True 全模式, False 精確模式.
- HMM: 是否使用隱式馬爾科夫.
'''
sentence = strdecode(sentence) # sentence轉unicode
if cut_all:
# re_han_cut_all = re.compile("([\u4E00-\u9FD5]+)", re.U)
re_han = re_han_cut_all
# re_skip_cut_all = re.compile("[^a-zA-Z0-9+#\n]", re.U)
re_skip = re_skip_cut_all
else:
# re_han_default = re.compile("([\u4E00-\u9FD5a-zA-Z0-9+#&\._%]+)", re.U)
re_han = re_han_default
# re_skip_default = re.compile("(\r\n|\s)", re.U)
re_skip = re_skip_default
if cut_all:
cut_block = self.__cut_all # cut_all=True, HMM=True or False
elif HMM:
cut_block = self.__cut_DAG # cut_all=False, HMM=True
else:
cut_block = self.__cut_DAG_NO_HMM # cut_all=False, HMM=False
blocks = re_han.split(sentence)
for blk in blocks:
if not blk:
continue
if re_han.match(blk): # 符合re_han匹配的串
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
可以看出jieba.cut返回一個可迭代的generator,可以使用 for 循環來獲得分詞后得到的每一個詞語(也可以用jieba.lcut直接返回分詞list結果)。
- cut_all=True, HMM=_對應於全模式,即所有在詞典中出現的詞都會被切分出來,實現函數為__cut_all;
- cut_all=False, HMM=False對應於精確模式且不使用HMM;按Unigram語法模型找出聯合概率最大的分詞組合,實現函數為__cut_DAG;
- cut_all=False, HMM=True對應於精確模式且使用HMM;在聯合概率最大的分詞組合的基礎上,HMM識別未登錄詞,實現函數為__cut_DAG_NO_HMM。
嚴格來說,jieba.cut不能算是分詞主體,分詞結果實際在cut_block里。下面以精確模式(無新詞發現)為例具體講解:
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 = ''
通過這個函數,可以看出jieba分詞具體流程:構建有向無環圖-->計算最大概率路徑。
構建有向無環圖
有向無環圖,directed acyclic graphs,簡稱DAG,是一種圖的數據結構,顧名思義,就是沒有環的有向圖。
jieba采用了Python的dict結構,可以更方便的表示DAG。最終的DAG是以{k : [k , j , ..] , m : [m , p , q] , ...}的字典結構存儲,其中k和m為詞在文本sentence中的位置,k對應的列表存放的是文本中以k開始且詞sentence[k: j + 1]在前綴詞典中的 以k開始j結尾的詞的列表,即列表存放的是sentence中以k開始的可能的詞語的結束位置,這樣通過查找前綴詞典就可以得到詞。
get_DAG(self, sentence)函數進行對系統初始化完畢后,會構建有向無環圖。
def get_DAG(self, 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 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
例如:
text = '我來到北京清華大學'
print(jieba.get_DAG(text))
{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是用dict表示的,key為邊的起點,value為邊的終點集合,比如:上述例子中1 -> 2表示詞“來到”。
計算最大概率
將log(詞頻/總詞頻)作為有向無環圖邊的權值,並假設詞與詞之間相互獨立,從圖論的角度出發,將最大概率組合問題變成了最大路徑問題。即:
def calc(self, sentence, DAG, route):
N = len(sentence)
route[N] = (0, 0)
logtotal = log(self.total)
for idx in range(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])
Jieba用動態規划(DP)來求解最大路徑問題,假設用\(d_i\)標記源節點到節點i的最大路徑的值,則有
其中,\(w(j,i)\)表示詞詞\(c_{ij}\)的詞頻log值,\(w(i,i)\)表示字符\(c_i\)獨立成詞的詞頻log值。在求解上述式子時,需要知道所有節點i的前驅節點j;然后DAG中只有后繼結點list。在這里,作者巧妙地用到了一個trick——從尾節點m-1往前推算的最優解等價於從源節點0往后推算的。那么,用\(r_i\)標記節點i到尾節點的最大路徑的值,則