1. 程式人生 > >分詞:淺談中文分詞與jieba原始碼

分詞:淺談中文分詞與jieba原始碼

一、前言

1、什麼是中文分詞?

中文文字,從形式上看是由漢字、標點符號等組成的一個字串。由字組成詞,再組成句子、文章等。那麼分詞,就是按照一定的規則把字串重新組合成詞序列的過程。

2、為什麼要分詞?

(1)在中文裡面,詞是最小的能夠獨立活動的有意義的語言成分

(2)英文中單詞以空格作為自然分界,雖然也有短語劃分的問題。但中文詞沒有一個形式上的分界,相對而言難度大了許多

(3)分詞作為中文自然語言處理的基礎工作,質量的好壞對後面的工作影響很大

3、分詞的難點?

(1)歧義消解問題

輸入待切分句子:提高人民生活水平

可以切分輸出 :提高/人民/生活/水平

或者切分輸出:提/高人/民生/活水/平

可以看到,明顯第二個輸出為歧義切分。

(2)未登入詞識別

未登入詞指的是在已有的詞典中,或者訓練語料裡面沒有出現過的詞,分為實體名詞,專有名詞及新詞。

4、怎麼分詞?

(1)基於字典、詞庫匹配的分詞

機械分詞演算法,將待分的字串與一個充分大的機器詞典中的詞條進行匹配。

分為正向匹配和逆向匹配;最大長度匹配和最小長度匹配;單純分詞和分詞與標註過程相結合的一體化方法。所以常用的有:正向最大匹配,逆向最大匹配,最少切分法。

實際應用中,將機械分詞作為初分手段,再利用其他方法提高準確率。

(2)基於詞頻統計的分詞

統計分詞,是一種全切分方法。切分出待分語句中所有的詞,基於訓練語料詞表中每個詞出現的頻率,運用統計模型和決策演算法決定最優的切分結果。

(3)基於知識理解的分詞

主要基於句法、語法分析,並結合語義分析,通過對上下文內容所提供資訊的分析對詞進行定界。

這類方法試圖讓機器具有人類的理解能力,需要使用大量的語言知識和資訊,目前還處在試驗階段。

二、jieba分詞原始碼解析

jieba分詞,目前是python中文分詞方面比較好的工具。支援精確、全模式及搜尋引擎模式的分詞,具體可以請看jieba文件:https://github.com/fxsjy/jieba

在文件中,jieba列出了工具實現的演算法策略:

1)、基於字首詞典實現高效的詞圖掃描,生成句子中漢字所有可能成詞情況所構成的有向無環圖 (DAG)

2)、採用了動態規劃查詢最大概率路徑, 找出基於詞頻的最大切分組合

3)、對於未登入詞,採用了基於漢字成詞能力的 HMM 模型,使用了 Viterbi 演算法

接下來我們來看看,具體jieba是怎麼實現這些演算法的。

1、字首詞典

字首詞典,實際上可以認為是一個詞頻詞典(即:../jieba/dict.txt),具體實現參見程式碼中的Tokenizer.FREQ字典,讀取dict.txt檔案,之後轉化為{詞:頻率}的形式,如下:

    def gen_pfdict(self, f_name):
        lfreq = {} # 字典儲存  詞條:出現次數
        ltotal = 0 # 所有詞條的總的出現次數
        with open(f_name, 'rb') as f: # 開啟檔案 dict.txt 
            for lineno, line in enumerate(f, 1): # 行號,行
                try:
                    line = line.strip().decode('utf-8') # 解碼為Unicode
                    word, freq = line.split(' ')[:2] # 獲得詞條 及其出現次數
                    freq = int(freq)
                    lfreq[word] = freq
                    ltotal += freq
                    for ch in xrange(len(word)):# 處理word的字首
                        wfrag = word[:ch + 1]
                        if wfrag not in lfreq: # word字首不在lfreq則其出現頻次置0 
                            lfreq[wfrag] = 0
                except ValueError:
                    raise ValueError(
                        'invalid dictionary entry in %s at Line %s: %s' % (f_name, lineno, line))
        return lfreq, ltotal

2、有向無環圖DAG

DAG是基於前面得到的字首詞典進行構造的,基本思想是將待分語句進行全切分,將切分完的詞語列表與字首詞典進行比較,如果這個詞存在,就存下來,並用詞在字串中的位置索引進行替代,轉化為{key:list[i,j…], …}的字典結構,如下:

假設有句子,去北京大學玩,則對應的DAG為,{0 : [0], 1 : [1, 2, 4], 2 : [2], 3 : [3, 4], 4 : [4], 5 : [5]} 這樣一個字典。

具體意思是,0:[0]代表0位置,即‘去’在字首詞典中代表一個詞,同理1 : [1, 2, 4]代表‘北’,‘北京’,‘北京大學’。

3、基於詞頻的最大切分

我們擁有所有可能出現的詞及其對應的所有可能組成路徑(即DAG),那麼應該用什麼方法去找到一條最可能的路徑呢?

jieba中使用了動態規劃的方法,這裡簡單的理解就是對於一個長度為n的句子,由後往前遍歷。

假設最後一個詞出現的概率為N,倒數第二個詞出現的概率為N-1,那麼兩個詞在一起出現概率則為N(N-1),以此類推,直到到達第一個詞,計算出所有可能路徑中的最大概率路徑。這裡還需要理解兩個名詞,重疊子問題,最優子結構,最終得到最優的路徑,如下:

     #動態規劃,計算最大概率的切分組合
    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([ (概率對數,詞語末字位置) for x in DAG[idx] ])
           # 以idx:(概率對數最大值,詞語末字位置)鍵值對形式儲存在route中
           # route[x+1][0] 表示 詞路徑[x+1,N-1]的最大概率對數,
           # [x+1][0]即表示取句子x+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])

4、對於未登入詞實現的HMM模型

1、什麼是HMM?

隱藏馬爾科夫模型(Hidden Markov Model),關於時序的模型,描述由一個隱藏的馬爾科夫鏈隨機生成不可觀測的狀態隨機序列,再由各個狀態生成一個觀測而產生觀測隨機序列的過程。具體可以參照:52nlp HMM系列文章

2、隱馬爾科夫模型五元組?

隱馬爾科夫模型由初始狀態概率向量pi、狀態轉移概率矩陣A和觀測概率矩陣B決定,pi和A決定狀態序列I,B決定觀測序列O。

3、隱馬爾科夫模型基本假設?

(1)齊次馬爾科夫性,即假設隱藏的馬爾科夫鏈在任意時刻t的狀態只依賴於其前一時刻的狀態,與其他時刻的狀態及觀測無關,也與時刻t無關。(2)觀測獨立性假設,即假設任意時刻的觀測只依賴於該時刻的馬爾科夫鏈的狀態,與其他觀測狀態無關。

4、隱馬爾科夫基本問題?

(1)評估問題(概率計算問題) 即給定觀測序列 O=O1,O2,O3…Ot和模型引數λ=(A,B,pi),怎樣有效計算這一觀測序列出現的概率. (Forward-backward演算法) (2)解碼問題(預測問題) 即給定觀測序列 O=O1,O2,O3…Ot和模型引數λ=(A,B,pi),怎樣尋找滿足這種觀察序列意義上最優的隱含狀態序列S。 (viterbi演算法,近似演算法) (3)學習問題 即HMM的模型引數λ=(A,B,pi)未知,如何求出這3個引數以使觀測序列O=O1,O2,O3…Ot的概率儘可能的大. 

(極大似然估計的方法估計引數,Baum-Welch,EM演算法)

5、jieba中實現的模型

(1)觀測序列O

對於每一個待分的句子,都視為一個觀測序列,如:去北京大學玩,就是一個長度T為6的觀測序列

(2)狀態序列I

每一個觀測序列,都對應著相同長度的狀態序列。這裡將漢字按SBME進行標註,分別代表single(單獨成詞的字)、begin(一個詞語開始字)、middle(一個詞語中間的字)、end(一個詞語結束的字),如:

觀測序列:去 / 北京大學 / 玩

狀態序列:S / BMME / S

(3)初始概率分佈pi

對應jiaba/finalseg/prob_start.py檔案,如下:

P={'B': -0.26268660809250016,
 'E': -3.14e+100,
 'M': -3.14e+100,
 'S': -1.4652633398537678}
這裡可以看到,初始狀態只可能是B或者S,和實際相符。

(4)狀態轉移概率分佈A

對應jieba/finalseg/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}}
如P[‘B’][‘E’]代表的含義就是從狀態B轉移到狀態E的概率,由P[‘B’][‘E’] = -0.510825623765990,表示狀態B的下一個狀態是E的概率對數是-0.510825623765990。

(5)觀測概率分佈B

對應jieba/finalseg/prob_emit.py檔案,如下:

P={'B': {'\u4e00': -3.6544978750449433,
       '\u4e01': -8.125041941842026,
       '\u4e03': -7.817392401429855,
       '\u4e07': -6.3096425804013165,
       ...}
比如P[‘B’][‘\u4e00’]代表的含義就是’B’狀態下觀測的字為’\u4e00’(對應的漢字為’一’)的概率對數P[‘B’][‘\u4e00’] = -3.6544978750449433。

(6)這些概率分佈是怎麼來的?

隱馬爾科夫基本問題的第三個--學習問題,但這裡用到的方法是極大似然估計,具體請看:模型的資料是怎麼生成的?

1)初始概率分佈pi

統計所有訓練樣本,以狀態S、B、M、E為初始狀態的數量,之後分別除以訓練樣本總詞頻,就可以得到初始概率分佈

2)狀態轉移概率分佈A

同理,統計所有樣本中,從狀態S轉移到B的出現次數,再除以S出現的總次數,便得到由S轉移到B的概率分佈,其他可得

3)觀測概率分佈B

統計訓練資料中,狀態為j並觀測為k的頻數,除以訓練資料中狀態j出現的次數,其他同理可得

(7)jieba是怎麼利用HMM進行切詞的?

隱馬爾科夫基本的第二個--解碼問題(很形象,假設中文語句是狀態序列的某種形式的編碼,利用模型進行解碼),這裡用到了維位元演算法(可以認為是動態規劃)

關於演算法的細節部分請看原始碼:結巴分詞

6、其他

推薦另一個作者對於jieba的註釋程式碼,寫的很詳細:結巴分詞-註釋版