自然語言學習08-HMM(隱馬爾可夫模型)和 CRF(條件隨機場)

   HMM(隱馬爾可夫模型)和 CRF(條件隨機場)算法常常被用於分詞、句法分析、命名實體識別、詞性標註等。在命名實體、句法分析等領域 CRF 更勝一籌。

從貝葉斯定義理解生成式模型和判別式模型

生成式模型和判別式模型

生成式模型:估計的是聯合概率分佈,P(Y, X)=P(Y|X)*P(X),由聯合概率密度分佈 P(X,Y),然後求出條件概率分佈 P(Y|X) 作爲預測的模型,即生成模型公式爲:P(Y|X)= P(X,Y)/ P(X)。基本思想是首先建立樣本的聯合概率密度模型 P(X,Y),然後再得到後驗概率 P(Y|X),再利用它進行分類,其主要關心的是給定輸入 X 產生輸出 Y 的生成關係。

判別式模型:估計的是條件概率分佈, P(Y|X),是給定觀測變量 X 和目標變量 Y 的條件模型。由數據直接學習決策函數 Y=f(X) 或者條件概率分佈 P(Y|X) 作爲預測的模型,其主要關心的是對於給定的輸入 X,應該預測什麼樣的輸出 Y。

HMM 使用隱含變量生成可觀測狀態,其生成概率有標註集統計得到,是一個生成模型。其他常見的生成式模型有:Gaussian、 Naive Bayes、Mixtures of multinomials 等。

CRF 就像一個反向的隱馬爾可夫模型(HMM),通過可觀測狀態判別隱含變量,其概率亦通過標註集統計得來,是一個判別模型。其他常見的判別式模型有:K 近鄰法、感知機、決策樹、邏輯斯諦迴歸模型、最大熵模型、支持向量機、提升方法等。

動手實戰:基於 HMM 訓練自己的 Python 中文分詞器

模型介紹

HMM 模型是由一個“五元組”組成的集合:

  • StatusSet:狀態值集合,狀態值集合爲 (B, M, E, S),其中 B 爲詞的首個字,M 爲詞中間的字,E 爲詞語中最後一個字,S 爲單個字,B、M、E、S 每個狀態代表的是該字在詞語中的位置。

    舉個例子,對“中國的人工智能發展進入高潮階段”,分詞可以標註爲:“中B國E的S人B工E智B能E發B展E進B入E高B潮E階B段E”,最後的分詞結果爲:['中國', '的', '人工', '智能', '發展', '進入', '高潮', '階段']。

  • ObservedSet:觀察值集合,觀察值集合就是所有語料的漢字,甚至包括標點符號所組成的集合。

  • TransProbMatrix:轉移概率矩陣,狀態轉移概率矩陣的含義就是從狀態 X 轉移到狀態 Y 的概率,是一個4×4的矩陣,即 {B,E,M,S}×{B,E,M,S}。

  • EmitProbMatrix:發射概率矩陣,發射概率矩陣的每個元素都是一個條件概率,代表 P(Observed[i]|Status[j]) 概率。

  • InitStatus:初始狀態分佈,初始狀態概率分佈表示句子的第一個字屬於 {B,E,M,S} 這四種狀態的概率。

將 HMM 應用在分詞上,要解決的問題是:參數(ObservedSet、TransProbMatrix、EmitRobMatrix、InitStatus)已知的情況下,求解狀態值序列。

解決這個問題的最有名的方法是 Viterbi 算法。

語料準備

yj_trainCorpus_utf8.txt 整個語料大小 264M,包含1116903條數據,UTF-8 編碼,詞與詞之間用空格隔開,用來訓練分詞模型。

編碼實現

(1)預定義

首先引出庫,這兩個庫的作用是用來模型保存的:

    import pickle
    import json

接下來定義 HMM 中的狀態,初始化概率,以及中文停頓詞:

    STATES = {'B', 'M', 'E', 'S'}
    EPS = 0.0001
    #定義停頓標點
    seg_stop_words = {" ",",","。","“","”",'“', "?", "!", ":", "《", "》", "、", ";", "·", "‘ ", "’", "──", ",", ".", "?", "!", "`", "~", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "+", "=", "[", "]", "{", "}", '"', "'", "<", ">", "\\", "|" "\r", "\n","\t"}

(2)面向對象封裝成類

首先,將 HMM 模型封裝爲獨立的類 HMM_Model,下面先給出類的結構定義:

    class HMM_Model:
        def __init__(self):
            pass
        #初始化    
        def setup(self):
            pass
         #模型保存   
        def save(self, filename, code):
            pass
        #模型加載
        def load(self, filename, code):
            pass
        #模型訓練
        def do_train(self, observes, states):
            pass
        #HMM計算
        def get_prob(self):
            pass
        #模型預測
        def do_predict(self, sequence):
            pass

第一個方法 __init__() 是一種特殊的方法,被稱爲類的構造函數或初始化方法,當創建了這個類的實例時就會調用該方法,其中定義了數據結構和初始變量,實現如下:

    def __init__(self):
            self.trans_mat = {}  
            self.emit_mat = {} 
            self.init_vec = {}  
            self.state_count = {} 
            self.states = {}
            self.inited = False

其中的數據結構定義:

  • trans_mat:狀態轉移矩陣,trans_mat[state1][state2] 表示訓練集中由 state1 轉移到 state2 的次數。

  • emit_mat:觀測矩陣,emit_mat[state][char] 表示訓練集中單字 char 被標註爲 state 的次數。

  • init_vec:初始狀態分佈向量,init_vec[state] 表示狀態 state 在訓練集中出現的次數。

  • state_count:狀態統計向量,state_count[state]表示狀態 state 出現的次數。

  • word_set:詞集合,包含所有單詞。

第二個方法 setup(),初始化第一個方法中的數據結構,具體實現如下:

        #初始化數據結構    
        def setup(self):
            for state in self.states:
                # build trans_mat
                self.trans_mat[state] = {}
                for target in self.states:
                    self.trans_mat[state][target] = 0.0
                self.emit_mat[state] = {}
                self.init_vec[state] = 0
                self.state_count[state] = 0
            self.inited = True

第三個方法 save(),用來保存訓練好的模型,filename 指定模型名稱,默認模型名稱爲 hmm.json,這裏提供兩種格式的保存類型,JSON 或者 pickle 格式,通過參數 code 來決定,code 的值爲 code='json' 或者 code = 'pickle',默認爲 code='json',具體實現如下:

    #模型保存   
    def save(self, filename="hmm.json", code='json'):
        fw = open(filename, 'w', encoding='utf-8')
        data = {
            "trans_mat": self.trans_mat,
            "emit_mat": self.emit_mat,
            "init_vec": self.init_vec,
            "state_count": self.state_count
        }
        if code == "json":
            txt = json.dumps(data)
            txt = txt.encode('utf-8').decode('unicode-escape')
            fw.write(txt)
        elif code == "pickle":
            pickle.dump(data, fw)
        fw.close()

第四個方法 load(),與第三個 save() 方法對應,用來加載模型,filename 指定模型名稱,默認模型名稱爲 hmm.json,這裏提供兩種格式的保存類型,JSON 或者 pickle 格式,通過參數 code 來決定,code 的值爲 code='json' 或者 code = 'pickle',默認爲 code='json',具體實現如下:

    #模型加載
    def load(self, filename="hmm.json", code="json"):
        fr = open(filename, 'r', encoding='utf-8')
        if code == "json":
            txt = fr.read()
            model = json.loads(txt)
        elif code == "pickle":
            model = pickle.load(fr)
        self.trans_mat = model["trans_mat"]
        self.emit_mat = model["emit_mat"]
        self.init_vec = model["init_vec"]
        self.state_count = model["state_count"]
        self.inited = True
        fr.close()

第五個方法 do_train(),用來訓練模型,因爲使用的標註數據集, 因此可以使用更簡單的監督學習算法,訓練函數輸入觀測序列和狀態序列進行訓練, 依次更新各矩陣數據。類中維護的模型參數均爲頻數而非頻率, 這樣的設計使得模型可以進行在線訓練,使得模型隨時都可以接受新的訓練數據繼續訓練,不會丟失前次訓練的結果。具體實現如下:

    #模型訓練
    def do_train(self, observes, states):
        if not self.inited:
            self.setup()

        for i in range(len(states)):
            if i == 0:
                self.init_vec[states[0]] += 1
                self.state_count[states[0]] += 1
            else:
                self.trans_mat[states[i - 1]][states[i]] += 1
                self.state_count[states[i]] += 1
                if observes[i] not in self.emit_mat[states[i]]:
                    self.emit_mat[states[i]][observes[i]] = 1
                else:
                    self.emit_mat[states[i]][observes[i]] += 1

第六個方法 get_prob(),在進行預測前,需將數據結構的頻數轉換爲頻率,具體實現如下:
 

    #頻數轉頻率
    def get_prob(self):
        init_vec = {}
        trans_mat = {}
        emit_mat = {}
        default = max(self.state_count.values())  

        for key in self.init_vec:
            if self.state_count[key] != 0:
                init_vec[key] = float(self.init_vec[key]) / self.state_count[key]
            else:
                init_vec[key] = float(self.init_vec[key]) / default

        for key1 in self.trans_mat:
            trans_mat[key1] = {}
            for key2 in self.trans_mat[key1]:
                if self.state_count[key1] != 0:
                    trans_mat[key1][key2] = float(self.trans_mat[key1][key2]) / self.state_count[key1]
                else:
                    trans_mat[key1][key2] = float(self.trans_mat[key1][key2]) / default

        for key1 in self.emit_mat:
            emit_mat[key1] = {}
            for key2 in self.emit_mat[key1]:
                if self.state_count[key1] != 0:
                    emit_mat[key1][key2] = float(self.emit_mat[key1][key2]) / self.state_count[key1]
                else:
                    emit_mat[key1][key2] = float(self.emit_mat[key1][key2]) / default
        return init_vec, trans_mat, emit_mat

第七個方法 do_predict(),預測採用 Viterbi 算法求得最優路徑, 具體實現如下:

    #模型預測
    def do_predict(self, sequence):
        tab = [{}]
        path = {}
        init_vec, trans_mat, emit_mat = self.get_prob()

        # 初始化
        for state in self.states:
            tab[0][state] = init_vec[state] * emit_mat[state].get(sequence[0], EPS)
            path[state] = [state]

        # 創建動態搜索表
        for t in range(1, len(sequence)):
            tab.append({})
            new_path = {}
            for state1 in self.states:
                items = []
                for state2 in self.states:
                    if tab[t - 1][state2] == 0:
                        continue
                    prob = tab[t - 1][state2] * trans_mat[state2].get(state1, EPS) * emit_mat[state1].get(sequence[t], EPS)
                    items.append((prob, state2))
                best = max(items)  
                tab[t][state1] = best[0]
                new_path[state1] = path[best[1]] + [state1]
            path = new_path

        # 搜索最有路徑
        prob, state = max([(tab[len(sequence) - 1][state], state) for state in self.states])
        return path[state]

上面實現了類 HMM_Model 的7個方法,接下來我們來實現分詞器,這裏先定義兩個函數,這兩個函數是獨立的,不在類中。

(1)定義一個工具函數

對輸入的訓練語料中的每個詞進行標註,因爲訓練數據是空格隔開的,可以進行轉態標註,該方法用在訓練數據的標註,具體實現如下:

    def get_tags(src):
        tags = []
        if len(src) == 1:
            tags = ['S']
        elif len(src) == 2:
            tags = ['B', 'E']
        else:
            m_num = len(src) - 2
            tags.append('B')
            tags.extend(['M'] * m_num)
            tags.append('E')
        return tags

(2)定義一個工具函數

根據預測得到的標註序列將輸入的句子分割爲詞語列表,也就是預測得到的狀態序列,解析成一個 list 列表進行返回,具體實現如下:

    def cut_sent(src, tags):
        word_list = []
        start = -1
        started = False

        if len(tags) != len(src):
            return None

        if tags[-1] not in {'S', 'E'}:
            if tags[-2] in {'S', 'E'}:
                tags[-1] = 'S'  
            else:
                tags[-1] = 'E'  

        for i in range(len(tags)):
            if tags[i] == 'S':
                if started:
                    started = False
                    word_list.append(src[start:i])  
                word_list.append(src[i])
            elif tags[i] == 'B':
                if started:
                    word_list.append(src[start:i])  
                start = i
                started = True
            elif tags[i] == 'E':
                started = False
                word = src[start:i+1]
                word_list.append(word)
            elif tags[i] == 'M':
                continue
        return word_list

最後,我們來定義分詞器類 HMMSoyoger,繼承 HMM_Model 類並實現中文分詞器訓練、分詞功能,先給出 HMMSoyoger 類的結構定義:

    class HMMSoyoger(HMM_Model):
        def __init__(self, *args, **kwargs):
            pass
        #加載訓練數據
        def read_txt(self, filename):
            pass
        #模型訓練函數
        def train(self):
            pass
        #模型分詞預測
        def lcut(self, sentence):
            pass

第一個方法 init(),構造函數,定義了初始化變量,具體實現如下:

    def __init__(self, *args, **kwargs):
            super(HMMSoyoger, self).__init__(*args, **kwargs)
            self.states = STATES
            self.data = None

第二個方法 read_txt(),加載訓練語料,讀入文件爲 txt,並且 UTF-8 編碼,防止中文出現亂碼,具體實現如下:

    #加載語料
    def read_txt(self, filename):
            self.data = open(filename, 'r', encoding="utf-8")

第三個方法 train(),根據單詞生成觀測序列和狀態序列,並通過父類的 do_train() 方法進行訓練,具體實現如下:

    def train(self):
            if not self.inited:
                self.setup()

            for line in self.data:
                line = line.strip()
                if not line:
                    continue

               #觀測序列
                observes = []
                for i in range(len(line)):
                    if line[i] == " ":
                        continue
                    observes.append(line[i])

                #狀態序列
                words = line.split(" ")  

                states = []
                for word in words:
                    if word in seg_stop_words:
                        continue
                    states.extend(get_tags(word))
                #開始訓練
                if(len(observes) >= len(states)):
                    self.do_train(observes, states)
                else:
                    pass

第四個方法 lcut(),模型訓練好之後,通過該方法進行分詞測試,具體實現如下:

    def lcut(self, sentence):
            try:
                tags = self.do_predict(sentence)
                return cut_sent(sentence, tags)
            except:
                return sentence

通過上面兩個類和兩個方法,就完成了基於 HMM 的中文分詞器編碼,下面來進行模型訓練和測試。

訓練模型

首先實例化 HMMSoyoger 類,然後通過 read_txt() 方法加載語料,再通過 train() 進行在線訓練,如果訓練語料比較大,可能需要等待一點時間,具體實現如下:

    soyoger = HMMSoyoger()
    soyoger.read_txt("syj_trainCorpus_utf8.txt")
    soyoger.train()

模型測試

模型訓練完成之後,進行測試:

print(soyoger.lcut("中國的人工智能發展進入高潮階段。"))

得到結果爲:

················數據集還需豐富··········

基於 CRF 的開源中文分詞工具 Genius 實踐

Genius 是一個基於 CRF 的開源中文分詞工具,採用了 Wapiti 做訓練與序列標註

 

分詞

首先引入 Genius,然後對 text 文本進行分詞。

import genius
text = u"""中文自然語言處理是人工智能技術的一個重要分支。"""
seg_list = genius.seg_text(
    text,
    use_combine=True,
    use_pinyin_segment=True,
    use_tagging=True,
    use_break=True
)
print(' '.join([word.text for word in seg_list])

其中,genius.seg_text 函數接受5個參數,其中 text 是必填參數:

  • text 第一個參數爲需要分詞的字。
  • use_break 代表對分詞結構進行打斷處理,默認值 True。
  • use_combine 代表是否使用字典進行詞合併,默認值 False。
  • use_tagging 代表是否進行詞性標註,默認值 True。
  • use_pinyin_segment 代表是否對拼音進行分詞處理,默認值 True。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章