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。