jieba源碼研讀筆記(五) - 分詞之全模式

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)想像成一個圖(一種數據結構),圖中的每個節點是句中的一個字。
DAG w/o link
現在我們手上有一個字典(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]}

先把結果畫成圖:
DAG
可以從圖中看到,句中的每個字都能成詞。
並且句中第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能處理的詞。

參考連結

jieba文檔

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值