1. 程式人生 > >基於特定語料庫生成HMM轉移概率分佈和發射概率分佈用於詞性標註 Python

基於特定語料庫生成HMM轉移概率分佈和發射概率分佈用於詞性標註 Python

上篇文章我們以Brown語料庫中的一個特例講解了HMM和Viterbi演算法。
那麼如何使用特定語料庫通過HMM演算法進行詞性標註呢?我們可以從HMM的五元組入手。

大致步驟:

  1. 得到語料庫中詞性標註種類和個數==>得到隱序列;
  2. 對輸入的句子進行分詞==>得到觀測序列;
  3. 對每個詞性標註,計算該詞性出現時的前一個詞性的次數/該詞性出現的總次數==>得到轉移概率矩陣;
  4. 對每個觀測狀態,計算該狀態不同詞性出現的次數/該觀測狀態出現的總次數==>得到發射概率矩陣;
  5. 計算各詞性初始概率:nc(postag)nsum\frac{n_{c(pos-tag)}}{n_{sum}},某一個詞性出現的次數/語料庫總詞性標記數;

在實際操作中某些小細節是需要引起我們關注的。

1 如何做資料平滑?

我們要考慮並處理髮射矩陣和轉移矩陣中條件概率為0而出現整條路徑概率為0情況,因此我們需要做資料平滑。下面以他是創業大賽國賽金獎?為例展開分析:

資料平滑的方法有很多種,一開始我使用的是加一法,在測試的時候我發現無論輸入什麼句子,詞性標註都是Rg。

他/Rg 是/Rg 創業/Rg 大賽/Rg 國/Rg 賽/Rg 金獎/Rg ?/Rg

平滑方法改進後:

他/r 是/v 創業/vn 大賽/vn 國/n 賽/vn 金獎/n ?/w

為什麼會出現這樣的情況呢?

我們回到語料庫可以發現總詞性種類有44種,而Rg只出現了10次,如果我們使用加一法平滑資料,對資料的概率影響還是比較大的,因此加一法在該語料庫中的表現上效果不是特別理想。我們可以使用Laplace平滑方法

,並使引數p足夠小,對平滑方法進行改進,從而更好地彌補語料庫資料量的不足。其他資料平滑方法還有Kneser-Ney、Katz等,在宗成慶的《統計自然語言處理》中有所提及。

# 加一法平滑方法
for row in range(transaction_matrix.shape[0]):
	transaction_matrix[row] += 1
	transaction_matrix[row] /= np.sum(transaction_matrix[row])	
for row in range(emission_matrix.shape[0]):
    emission_matrix[row] +=
1 emission_matrix[row] /= tags_num[tags_list[row]] + emission_matrix.shape[1] # Laplace平滑方法 l=1,p=1e-16 # 這裡也可以看做使用一個極小的數字如1e-16代替0,分母+1為避免分母為0的情況出現 for row in range(transaction_matrix.shape[0]): n = np.sum(transaction_matrix[row]) transaction_matrix[row] += 1e-16 transaction_matrix[row] /= n + 1 for row in range(emission_matrix.shape[0]): emission_matrix[row] += 1e-16 emission_matrix[row] /= tags_num[tags_list[row]] + 1

2 未登入詞的詞性?

這裡使用的分詞詞典是基於該語料庫的,所以分割結果都能找到對應的詞性標註,所以不存在未登入詞未知詞性的情況。

但是,如果我們使用別的分詞詞典或者分詞工具呢?那就會出現未登入詞的情況,這時候每種詞性的發射概率都相同,該未登入詞的詞性就跟前後的詞性有關了,不過對未登入詞測試結果並不是太好。

最大概率分詞:
今天/t 和/p 明天/t 我/r 不/d 用/v 再/d 做/v 自然/a 語言/n 處理/vn 作業/n 了/y !/w

其他分詞:(含未登入詞-自然語言處理)
今天/t 和/p 明天/t 我/r 不用/d 再/d 做/v 自然語言處理/vd 作業/v 了/y 。/w

3 語料庫的影響?

先看一下這個例子:

我/r 喜歡/v 學習/v 。/w

我們再看看詞典,學習有/v,/vn這兩種詞性,但是在這個例子中應該是vn才對。這是什麼情況呢?通過打印發射概率矩陣發現,學習的vn和v這兩個發射概率竟然差不多?問題可能出在這裡。

我們可以試著將語料庫中學習/v全部替換成學習/vn。這時候,我們的標註結果變成了:

我/r 喜歡/v 學習/vn 。/w

分析發現,出現這樣的標註差異應該是語料庫本身造成的,有趣的是我們使用LTP詞性標註測試該例子時也是第一種結果。

4 轉移概率矩陣和發射概率矩陣的儲存?

資料結構使用numpy.float64,保證有足夠小的精度儘量防止溢位,能夠小到1e-308。

轉移概率矩陣大小為44*44,發射概率矩陣大小為44*分詞詞串數目,兩者都是在程式執行時生成。

若在大規模詞性標註情況下建議儲存44*4444*60366(語料庫總詞數)大小的轉移概率矩陣和發射概率矩陣到本地檔案中,需要時直接一次讀取該檔案存入記憶體而不需動態生成,提高效率。

5 其他問題

語料庫中有[香港/ns 特區/n 基本法/n 推廣/vn 督導/vn 委員會/n]nt這樣的組合標記,為了處理方便,這裡特別處理這樣的詞性標註,在實際應用中需要注意處理這種情況。

在計算轉移概率時是以句子為單位進行處理的,因此,對於一段文字建議按標點分成幾個句子處理,一來能提高詞性標註的準確性,二來防止因句子長度過長出現下溢現象

6 實現程式碼

import numpy as np
import hmm_viterbi
import max_probability_seg


def cal_hmm_matrix(observation):
    # 得到所有標籤
    word_pos_file = open('ChineseDic.txt').readlines()
    tags_num = {}
    for line in word_pos_file:
        word_tags = line.strip().split(',')[1:]
        for tag in word_tags:
            if tag not in tags_num.keys():
                tags_num[tag] = 0
    tags_list = list(tags_num.keys())

    # 轉移矩陣、發射矩陣
    transaction_matrix = np.zeros((len(tags_list), len(tags_list)), dtype=float)
    emission_matrix = np.zeros((len(tags_list), len(observation)), dtype=float)

    # 計算轉移矩陣和發射矩陣
    word_file = open('199801.txt').readlines()
    for line in word_file:
        if line.strip() != '':
            word_pos_list = line.strip().split('  ')
            for i in range(1, len(word_pos_list)):
                tag = word_pos_list[i].split('/')[1]
                pre_tag = word_pos_list[i - 1].split('/')[1]
                try:
                    transaction_matrix[tags_list.index(pre_tag)][tags_list.index(tag)] += 1
                    tags_num[tag] += 1
                except ValueError:
                    if ']' in tag:
                        tag = tag.split(']')[0]
                    else:
                        pre_tag = tag.split(']')[0]
                    transaction_matrix[tags_list.index(pre_tag)][tags_list.index(tag)] += 1
                    tags_num[tag] += 1

            for o in observation:
                # 注意' 我/','我/'的區別
                if ' ' + o in line:
                    pos_tag = line.strip().split(o)[1].split('  ')[0].strip('/')
                    if ']' in pos_tag:
                        pos_tag = pos_tag.split(']')[0]
                    emission_matrix[tags_list.index(pos_tag)][observation.index(o)] += 1

    for row in range(transaction_matrix.shape[0]):
        n = np.sum(transaction_matrix[row])
        transaction_matrix[row] += 1e-16
        transaction_matrix[row] /= n + 1

    for row in range(emission_matrix.shape[0]):
        emission_matrix[row] += 1e-16
        emission_matrix[row] /= tags_num[tags_list[row]] + 1

    times_sum = sum(tags_num.values())
    for item in tags_num.keys():
        tags_num[item] = tags_num[item] / times_sum

    # 返回隱狀態,初始概率,轉移概率,發射矩陣概率
    return tags_list, list(tags_num.values()), transaction_matrix, emission_matrix


if __name__ == '__main__':

    input_str = "今天和明天我彈琴。"
    obs = max_probability_seg.seg(input_str).strip().split(' ')
    hid, init_p, trans_p, emit_p = cal_hmm_matrix(obs)

    result = hmm_viterbi.viterbi(len(obs), len(hid), init_p, trans_p, emit_p)
    
    tag_line = ''
    for k in range(len(result)):
        tag_line += obs[k] + hid[int(result[k])] + ' '
    print(tag_line)