基于HMM的中文分词
隱馬爾可夫模型(HMM)在中文分詞中的應用
隱馬爾可夫模型的詳細解釋
隱馬爾可夫模型的一些范例介紹
隱馬爾可夫模型中有兩個序列,一個是狀態序列,另一個是觀測序列,其中狀態序列是隱藏的。用具體的例子來解釋。
? ? ? 假設一個房間內有N個裝有球的盒子,在這些盒子中分別有M種不同顏色的球,我根據某一個概率分布(初始概率分布,在中文分詞中就是一句話中第一個字符對應的狀態概率)隨機地選取一個初始的盒子,從中根據不同顏色的球的概率分布(發射概率,在中文分詞中就是每一個字對應的狀態概率),隨機選擇出一個球,然后我將這個球拿給房間外的你看。這個時候你能看到的就只是不同顏色的球的序列(觀測序列),我選擇的盒子的序列(狀態序列)你是不知道的。
訓練階段參數估計
一個HMM可以由下面幾個部分組成:
(1)模型中的狀態數量N(上例中盒子的數量)
在中文分詞中一般只有4個狀態:STATES={‘B’,‘M’,‘E’,‘S’},例如:小明是中國人,對應的狀態序列就是:B,E,S,B,M,E
(2)從每個狀態可能輸出的不同符號的數目M(上例中不同顏色球的個數)
在中文分詞中就是對應每一個字符
?
在中文分詞中就是狀態序列STATES={‘B’,‘M’,‘E’,‘S’}的轉移概率,這個狀態概率矩陣是在訓練階段參數估計中得到。在中文分詞中狀態轉移矩陣是一個4*4的矩陣,我在實驗中是通過統計訓練數據中狀態轉移的頻數確定矩陣,為了確保,需要對頻數矩陣除以對應每一行狀態的統計數,即A[key0][key1]/count(key0),為了保證數據的精度,我這里取了對數,并且以字典輸出方便查看。
{'B': {'B': -3.14e+100, 'M':-1.9594399657636383, 'E': -0.15191340104766693, 'S': -3.14e+100},
'M': {'B': -3.14e+100, 'M':-1.0983633205740504, 'E': -0.40558961540346977, 'S': -3.14e+100},
'E': {'B':-0.78182902092047468, 'M': -3.14e+100, 'E': -3.14e+100, 'S':-0.62312924475978682},
'S': {'B': -0.74289298827998818,'M': -3.14e+100, 'E': -3.14e+100, 'S': -0.81330579119502522}}
可以看到對應狀態‘B’后面只能接‘M’和‘E’;狀態‘M’后面只能接‘M’和‘E’;狀態‘E’后面只能接‘B’和‘S’;狀態‘S’后面只能接‘B’和‘S’。
?
在中文分詞中發射概率指的是每一個字符對應狀態序列STATES={‘B’,‘M’,‘E’,‘S’}中每一個狀態的概率,通過對訓練集每個字符對應狀態的頻數統計得到。我是通過字典的形式保存,就可能出現某個字符在某一個狀態沒有頻數,導致后面矩陣計算錯誤。為保證每個字符都在STATES的字典中,在構建發射概率矩陣是先初始化。
?
在中文分詞初始狀態概率指的是每一句話第一個字符的對應狀態概率。在我實驗中通過訓練的得到的初始狀態概率分布為,
{'B': -0.48164247830489765,'M': -3.14e+100, 'E': -3.14e+100, 'S': -0.96172723110752845}
可以看到第一個字符的初始狀態只能是‘B’和‘S’。
這個時候我們就完成了訓練階段的參數估計,得到了三個概率矩陣,分別是:
TransProbMatrix: 轉移概率矩陣(array_A)
EmitProbMatrix: 發射概率矩陣(array_B)
InitStatus: 初始狀態分布(array_pi)
測試階段Viterbi算法
在這里我們要做的是對測試數據進行分詞,首先要做的就是對測試數據中每一個字符進行狀態標注,這里使用的是Viterbi算法進行標注。
給一個測試語句‘小明是中國人’,首先標注第一個字‘小’,由于是第一個字,這里只需要用到初始狀態概率矩陣和發射矩陣,
初始狀態概率矩陣:
{'B': -0.48164247830489765,'M': -3.14e+100, 'E': -3.14e+100, 'S': -0.96172723110752845}
發射概率矩陣:
{‘B’:{‘小’:-6.04291273032},‘M’:{‘小’:-6.50192547556},‘E’:{‘小’:-7.88369576318},‘S’:{‘小’:-6.60403172082}}
然后標注概率矩陣tab為:
for state in STATES:tab[0][state] = array_pi[state] + array_b[state]['小']這里我對矩陣取對數,所有只用‘+’即可,則得到第一個字‘小’的標注概率:
tab[0][‘B’]=-6.52455520862
tab [0][‘M’]=-3.14e+100
tab [0][‘E’]=-3.14e+100
tab [0][‘S’]=-7.56575895193
這里可以看到path[0][‘B’]> path[0][‘S’],則這句語句第一個字‘小’標注為‘B’
然后計算第二個字明,這時就需要狀態轉移概率矩陣array_A。這里有兩個迭代過程,對于‘明’每一個狀態state0都需要計算前一個字‘小’可能的每一個狀態state1的概率prob,然后取max,
for state0 in STATES:for state1 in STATES:prob=tab[i-1][state1]+array_a[state1][state0]+array_b[state0][sentence[i]]這實際上就是一個動態規劃最優路徑的問題,對前一個字‘小’的每個狀態state根據狀態轉移矩陣array_a和‘小’的標注概率tab,再加上這個字‘明’的發射概率array_b,得到‘明’這個字的標記概率
tab[1][‘B’]= -14.742836926340019
tab [1][‘M’]=-14.836146344717278
tab [1][‘E’]= -12.832943424756692
tab [1][‘S’]=-17.961024112859207
這里可以看到tab [1][‘E’]最大,這個就是‘明’這個字的標注為‘E’。然后對測試數據依次進行這個操作。
在測試階段使用Viterbi標注時需要注意:若測試集中出現某一個字符,但這個字符在訓練集中未出現,這個時候發射概率是沒有的,這里需要做平滑處理,我這里采用的方式是:將所有未在訓練集中出現的字符統一發射概率為發射概率矩陣的中位數。
在這里測試數據的標注已經得到,最后一步需要做的就是根據標注的狀態進行分詞。這里有幾種情況:
(1)測試數據只有一個字符,直接輸出
(2)測試數據標注的最后一個字符的狀態不是‘S’和‘E’,這里需要進行修改
if tag[-1] == 'B' or tag[-1] == 'M': #最后一個字狀態不是'S'或'E'則修改if tag[-2] == 'B' or tag[-2] == 'M':tag[-1] = 'S'else:tag[-1] = 'E'最后整個分詞器就已經實現,這里需要對最后的分詞結果進行測試,測試資源
這里寫一下score腳本的使用方法,
(1)這是一個perl的腳本,在windows系統中首先要下載一個ActivePerl的解釋器,配置環境變量。
(2)然后score需要GNU diffutils的支持,下載地址
(3)然后需要修改score腳本中的語句,指定diff的安裝目錄$diff,$tmp1和$tmp2分別指定黃金標準分詞文件和測試集切分文件。
?
我這里指定了tmp的路徑,但是評分腳本“score”是用來比較兩個分詞文件的,需要三個參數:
1. 訓練集詞表(The training setword list)
2. “黃金”標準分詞文件(The gold standard segmentation)
3. 測試集的切分文件(The segmentedtest file)
我在cmd中的命令輸入為:
perl score CTB_training_words.utf8CTB_test_gold.utf8 output.txt
最后的分詞結果為
實驗代碼:
GitHub鏈接:https://github.com/CQUPT-Wan/HMMwordseg.git
這個項目里面有我實現HMM中文分詞的訓練集,測試集和黃金標準分詞文件
import pandas as pd import codecs from numpy import * import numpy as np import sys import re STATES = ['B', 'M', 'E', 'S'] array_A = {} #狀態轉移概率矩陣 array_B = {} #發射概率矩陣 array_E = {} #測試集存在的字符,但在訓練集中不存在,發射概率矩陣 array_Pi = {} #初始狀態分布 word_set = set() #訓練數據集中所有字的集合 count_dic = {} #‘B,M,E,S’每個狀態在訓練集中出現的次數 line_num = 0 #訓練集語句數量#初始化所有概率矩陣 def Init_Array():for state0 in STATES:array_A[state0] = {}for state1 in STATES:array_A[state0][state1] = 0.0for state in STATES:array_Pi[state] = 0.0array_B[state] = {}array_E = {}count_dic[state] = 0#對訓練集獲取狀態標簽 def get_tag(word):tag = []if len(word) == 1:tag = ['S']elif len(word) == 2:tag = ['B', 'E']else:num = len(word) - 2tag.append('B')tag.extend(['M'] * num)tag.append('E')return tag#將參數估計的概率取對數,對概率0取無窮小-3.14e+100 def Prob_Array():for key in array_Pi:if array_Pi[key] == 0:array_Pi[key] = -3.14e+100else:array_Pi[key] = log(array_Pi[key] / line_num)for key0 in array_A:for key1 in array_A[key0]:if array_A[key0][key1] == 0.0:array_A[key0][key1] = -3.14e+100else:array_A[key0][key1] = log(array_A[key0][key1] / count_dic[key0])# print(array_A)for key in array_B:for word in array_B[key]:if array_B[key][word] == 0.0:array_B[key][word] = -3.14e+100else:array_B[key][word] = log(array_B[key][word] /count_dic[key])#將字典轉換成數組 def Dic_Array(array_b):tmp = np.empty((4,len(array_b['B'])))for i in range(4):for j in range(len(array_b['B'])):tmp[i][j] = array_b[STATES[i]][list(word_set)[j]]return tmp#判斷一個字最大發射概率的狀態 def dist_tag():array_E['B']['begin'] = 0array_E['M']['begin'] = -3.14e+100array_E['E']['begin'] = -3.14e+100array_E['S']['begin'] = -3.14e+100array_E['B']['end'] = -3.14e+100array_E['M']['end'] = -3.14e+100array_E['E']['end'] = 0array_E['S']['end'] = -3.14e+100def dist_word(word0,word1,word2,array_b):if dist_tag(word0,array_b) == 'S':array_E['B'][word1] = 0array_E['M'][word1] = -3.14e+100array_E['E'][word1] = -3.14e+100array_E['S'][word1] = -3.14e+100return#Viterbi算法求測試集最優狀態序列 def Viterbi(sentence,array_pi,array_a,array_b):tab = [{}] #動態規劃表path = {}if sentence[0] not in array_b['B']:for state in STATES:if state == 'S':array_b[state][sentence[0]] = 0else:array_b[state][sentence[0]] = -3.14e+100for state in STATES:tab[0][state] = array_pi[state] + array_b[state][sentence[0]]# print(tab[0][state])#tab[t][state]表示時刻t到達state狀態的所有路徑中,概率最大路徑的概率值path[state] = [state]for i in range(1,len(sentence)):tab.append({})new_path = {}# if sentence[i] not in array_b['B']:# print(sentence[i-1],sentence[i])for state in STATES:if state == 'B':array_b[state]['begin'] = 0else:array_b[state]['begin'] = -3.14e+100for state in STATES:if state == 'E':array_b[state]['end'] = 0else:array_b[state]['end'] = -3.14e+100for state0 in STATES:items = []# if sentence[i] not in word_set:# array_b[state0][sentence[i]] = -3.14e+100# if sentence[i] not in array_b[state0]:# array_b[state0][sentence[i]] = -3.14e+100# print(sentence[i] + state0)# print(array_b[state0][sentence[i]])for state1 in STATES:# if tab[i-1][state1] == -3.14e+100:# continue# else:if sentence[i] not in array_b[state0]: #所有在測試集出現但沒有在訓練集中出現的字符if sentence[i-1] not in array_b[state0]:prob = tab[i - 1][state1] + array_a[state1][state0] + array_b[state0]['end']else:prob = tab[i - 1][state1] + array_a[state1][state0] + array_b[state0]['begin']# print(sentence[i])# prob = tab[i-1][state1] + array_a[state1][state0] + array_b[state0]['other']else:prob = tab[i-1][state1] + array_a[state1][state0] + array_b[state0][sentence[i]] #計算每個字符對應STATES的概率 # print(prob)items.append((prob,state1))# print(sentence[i] + state0)# print(array_b[state0][sentence[i]])# print(sentence[i])# print(items)best = max(items) #bset:(prob,state)# print(best)tab[i][state0] = best[0]# print(tab[i][state0])new_path[state0] = path[best[1]] + [state0]path = new_pathprob, state = max([(tab[len(sentence) - 1][state], state) for state in STATES])return path[state]#根據狀態序列進行分詞 def tag_seg(sentence,tag):word_list = []start = -1started = Falseif len(tag) != len(sentence):return Noneif len(tag) == 1:word_list.append(sentence[0]) #語句只有一個字,直接輸出else:if tag[-1] == 'B' or tag[-1] == 'M': #最后一個字狀態不是'S'或'E'則修改if tag[-2] == 'B' or tag[-2] == 'M':tag[-1] = 'E'else:tag[-1] = 'S'for i in range(len(tag)):if tag[i] == 'S':if started:started = Falseword_list.append(sentence[start:i])word_list.append(sentence[i])elif tag[i] == 'B':if started:word_list.append(sentence[start:i])start = istarted = Trueelif tag[i] == 'E':started = Falseword = sentence[start:i + 1]word_list.append(word)elif tag[i] == 'M':continuereturn word_listif __name__ == '__main__':trainset = open('CTBtrainingset.txt', encoding='utf-8') #讀取訓練集testset = open('CTBtestingset.txt', encoding='utf-8') #讀取測試集# trainlist = []Init_Array()for line in trainset:line = line.strip()# trainlist.append(line)line_num += 1word_list = []for k in range(len(line)):if line[k] == ' ':continueword_list.append(line[k])# print(word_list)word_set = word_set | set(word_list) #訓練集所有字的集合line = line.split(' ')# print(line)line_state = [] #這句話的狀態序列for i in line:line_state.extend(get_tag(i))# print(line_state)array_Pi[line_state[0]] += 1 # array_Pi用于計算初始狀態分布概率for j in range(len(line_state)-1):# count_dic[line_state[j]] += 1 #記錄每一個狀態的出現次數array_A[line_state[j]][line_state[j+1]] += 1 #array_A計算狀態轉移概率for p in range(len(line_state)):count_dic[line_state[p]] += 1 # 記錄每一個狀態的出現次數for state in STATES:if word_list[p] not in array_B[state]:array_B[state][word_list[p]] = 0.0 #保證每個字都在STATES的字典中# if word_list[p] not in array_B[line_state[p]]:# # print(word_list[p])# array_B[line_state[p]][word_list[p]] = 0# else:array_B[line_state[p]][word_list[p]] += 1 # array_B用于計算發射概率Prob_Array() #對概率取對數保證精度print('參數估計結果')print('初始狀態分布')print(array_Pi)print('狀態轉移矩陣')print(array_A)print('發射矩陣')print(array_B)output = ''for line in testset:line = line.strip()tag = Viterbi(line, array_Pi, array_A, array_B)# print(tag)seg = tag_seg(line, tag)# print(seg)list = ''for i in range(len(seg)):list = list + seg[i] + ' '# print(list)output = output + list + '\n'print(output)outputfile = open('output.txt', mode='w', encoding='utf-8')outputfile.write(output)參考鏈接:
http://www.leexiang.com/hidden-markov-model
http://www.52nlp.cn/category/hidden-markov-model
最后如果轉載,麻煩留個本文的鏈接,因為如果讀者或我自己發現文章有錯誤,我會在這里更正,留個本文的鏈接,防止我暫時的疏漏耽誤了他人寶貴的時間。
總結
以上是生活随笔為你收集整理的基于HMM的中文分词的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 某只羊的学习指南
- 下一篇: Excel转换成Json工具