模型介紹
馬爾科夫假設: 假設模型的當前狀態僅僅依賴於前面的幾個狀態
一個馬爾科夫過程是狀態間的轉移僅依賴於前n個狀態的過程。這個過程被稱之為n階馬爾科夫模型,其中n是影響下一個狀態選擇的(前)n個狀態。最簡單的馬爾科夫過程是一階模型,它的狀態選擇僅與前一個狀態有關。這里要注意它與確定性系統並不相同,因為下一個狀態的選擇由相應的概率決定,並不是確定性的。
對於有M個狀態的一階馬爾科夫模型,共有\(M^2\)個狀態轉移,因為任何一個狀態都有可能是所有狀態的下一個轉移狀態。每一個狀態轉移都有一個概率值,稱為狀態轉移概率——這是從一個狀態轉移到另一個狀態的概率。所有的\(M^2\)個概率可以用一個狀態轉移矩陣表示。注意這些概率並不隨時間變化而不同——這是一個非常重要(但常常不符合實際)的假設。
以天氣系統為例,假設有狀態轉移矩陣:
要初始化這樣一個系統,我們需要確定起始日天氣的(或可能的)情況,定義其為一個初始概率向量,稱為pi向量:
也就是說,第一天為晴天的概率為1。現在我們定義一個一階馬爾科夫過程如下:
- 狀態:三個狀態——晴天,多雲,雨天。
- pi向量:定義系統初始化時每一個狀態的概率。
- 狀態轉移矩陣:給定前一天天氣情況下的當前天氣概率。
任何一個可以用這種方式描述的系統都是一個馬爾科夫過程。
在某些情況下,我們希望通過觀察到的狀態序列來預測某個隱藏的狀態序列:比如通過觀察水藻狀態來預測天氣,或者通過觀察聲帶振動來預測發音單詞等。在這種情況下,觀察到的狀態序列與隱藏過程有一定的概率關系。我們使用隱馬爾科夫模型對這樣的過程建模,這個模型包含了一個底層隱藏的隨時間改變的馬爾科夫過程,以及一個與隱藏狀態某種程度相關的可觀察到的狀態集合。
需要着重指出的是,隱藏狀態的數目與觀察狀態的數目可以是不同的。一個包含三個狀態的天氣系統(晴天、多雲、雨天)中,可以觀察到4種(或更少,或更多)的海藻濕潤情況(干、稍干、潮濕、濕潤等)
下圖顯示的是天氣例子中的隱藏狀態和觀察狀態:
隱藏狀態和觀察狀態之間的連接表示:在給定的馬爾科夫過程中,一個特定的隱藏狀態生成特定的觀察狀態的概率。‘進入’一個觀察狀態的所有概率之和為1.
除了定義了馬爾科夫過程的概率關系,我們還有另一個矩陣,定義為混淆矩陣(confusion matrix),它包含了給定一個隱藏狀態后得到的觀察狀態的概率。對於天氣例子,混淆矩陣是:
注意矩陣的每一行之和是1。
總結(Summary)
隱馬爾科夫模型(HMM)包含2組狀態集合和3組概率集合:
- 隱藏狀態 \(N\):一個系統的(真實)狀態,可以由一個馬爾科夫過程進行描述(例如,天氣)。
- 觀察狀態 \(M\):在這個過程中‘可視’的狀態(例如,海藻的濕度)。
- pi向量 \(\pi\):包含了(隱)模型在時間t=1時一個特殊的隱藏狀態的概率(初始概率)。
- 狀態轉移矩陣 \(A\):包含了一個隱藏狀態到另一個隱藏狀態的概率
- 混淆矩陣 \(B\):包含了給定隱馬爾科夫模型的某一個特殊的隱藏狀態,觀察到的某個觀察狀態的概率。
在狀態轉移矩陣及混淆矩陣中的每一個概率都是時間無關的——也就是說,當系統演化時這些矩陣並不隨時間改變。實際上,這是馬爾科夫模型關於真實世界最不現實的一個假設。
隱馬爾科夫模型通常解決的問題包括:
- 對於一個觀察序列匹配最可能的系統——評估,使用前向算法(forward algorithm)解決;
- 對於已生成的一個觀察序列,確定最可能的隱藏狀態序列——解碼,使用Viterbi 算法(Viterbi algorithm)解決;
- 對於已生成的觀察序列,決定最可能的模型參數——學習,使用前向-后向算法(forward-backward algorithm)解決。
NER與Viterbi算法
NER其實就是序列標注問題,那么HMM中的5個基本元素:\(\{N,M,A,B,π\}\)在序列標注問題中可理解為:
N: 隱藏狀態的有限集合。在NER任務中,是指每一個詞語背后的標注。
M: 觀察狀態的有限集合。在NER任務中,是指每一個詞語本身。
A: 狀態轉移矩陣。在NER任務中,是指某一個標注轉移到下一個標注的概率。
B: 混淆矩陣。在NER任務中,是指在某個標注下,生成某個詞的概率。
π: pi向量。在NER任務中,是指每一個標注的初始化概率。
以上的這些元素,都是可以從訓練語料集中統計出來的。最后,我們根據這些統計值,應用維特比(viterbi)算法,就可以算出詞語序列背后的標注序列了。
舉個例子:
假設有三個詞語(觀察狀態集合M):{'策划', '決定', '記錄'}
有兩種可能的標注(隱藏狀態集合N): {'動詞v.', '名詞n.'}
句子中第一個詞可能的詞性(Pi向量):{'動詞': 0.3, '名詞': 0.7}
從語料中統計得知:一個詞到下一個詞之間,詞性轉換的比例分布 = {
名詞->名詞: 0.3 ,
名詞->動詞: 0.7 ,
動詞->名詞: 0.6 ,
動詞->動詞: 0.4
}
由此可以列出相應的狀態轉移矩陣A
同樣,從語料中統計得知:在指定標注下,各詞語出現的比例 = {
名詞,策划:0.7 ,決定 :0.2 ,記錄: 0.1 ;
動詞,策划:0.1 ,決定 :0.5 ,記錄: 0.4
}
由此可以列出相應的混淆矩陣B
已知一個句子片段分詞后是:“策划 決定 記錄”
那么就有\(2^3\)種可能的標注序列: nnn, nnv, nvn, nvv, vnn, vnv, vvn, vvv
我們需要尋找這些標注序列中,統計概率最大的那一組。
Viterbi算法解決該問題的思路其實就是動態規划的思路:
n個詞概率最大的標注序列 = 前n-1詞概率最大的標注序列 + 最后1個詞概率最大的標注
按這個思路,我們來一起算一下:
P(句子第一個詞是名詞) = P(一般第一個詞是名詞) * P(名詞中出現'策划') = 0.70.7 = 0.49
P(句子第一個詞是動詞) = P(一般第一個詞是動詞) * P(動詞中出現'策划') = 0.30.1 = 0.03
接着,按照DP的思想:前二個詞概率最大的標注等於第一個詞概率最大的標注加上第二個詞概率最大的標注,由於第一個詞概率最大的標注我們已經算出來了,所以第二個詞的標注的計算過程:
P(句子第二個詞是名詞|第一個詞是名詞) = P(第一個詞是名詞) * P(名詞轉名詞) * P(名詞中出現'決定') = 0.490.30.2 = 0.0294
P(句子第二個詞是動詞|第一個詞是名詞) = P(第一個詞是名詞) * P(名詞轉動詞) * P(動詞中出現'決定') = 0.490.70.5 = 0.1715
所以前兩個詞概率最大的標注序列是:[名詞, 動詞]
同樣的,三個詞概率最大的標注序列 = 前兩個詞概率最大的標注序列 + 最后一個詞概率最大的標注
大家可以自己算一算,看看最后的結果是不是[名詞,動詞,動詞]呢?
代碼實踐
數據
數據樣例:
O:other, B:begin,M:middle,E:end,S:single word
1 O
9 O
9 O
8 O
年 O
9 O
月 O
馬 B-ORG
鋼 M-ORG
總 M-ORG
公 M-ORG
司 E-ORG
改 O
制 O
為 O
馬 B-ORG
鋼 M-ORG
( M-ORG
集 M-ORG
團 M-ORG
) M-ORG
控 M-ORG
股 M-ORG
有 M-ORG
限 M-ORG
公 M-ORG
司 E-ORG
, O
顧 S-NAME
先 O
生 O
出 O
任 O
總 B-TITLE
經 M-TITLE
理 E-TITLE
。 O
讀取數據:
word_lists = []
tag_lists = []
with open(filepath, 'r', encoding='utf-8') as f:
word_list = []
tag_list = []
for line in f:
if line != '\n': # 句子之間通過空行分隔
word, tag = line.strip('\n').split()
word_list.append(word)
tag_list.append(tag)
else:
word_lists.append(word_list)
tag_lists.append(tag_list)
word_list = []
tag_list = []
# 如果make_vocab為True,還需要返回word2id和tag2id
if make_vocab:
word2id = build_map(word_lists)
tag2id = build_map(tag_lists)
return word_lists, tag_lists, word2id, tag2id
else:
return word_lists, tag_lists
# 構建詞表,按每個詞的出現順序建立數字索引
def build_map(lists):
maps = {}
for list in lists:
for element in list:
if element not in maps:
maps[e] = len(maps) # 確保每個詞都有唯一的數字索引
return maps
模型
構建HMM模型:
import torch
class HMM(object):
def __init__(self, N, M):
"""Args:
N: 狀態數,這里對應存在的標注的種類
M: 觀測數,這里對應數據集中有多少不同的字
"""
self.N = N
self.M = M
# 狀態轉移概率矩陣 A[i][j]表示從i狀態轉移到j狀態的概率
self.A = torch.zeros(N, N)
# 觀測概率矩陣, B[i][j]表示i狀態下生成j觀測的概率
self.B = torch.zeros(N, M)
# 初始狀態概率 Pi[i]表示初始時刻為狀態i的概率
self.Pi = torch.zeros(N)
def train(self, word_lists, tag_lists, word2id, tag2id):
"""HMM的訓練,即根據訓練語料對模型參數進行估計,
因為我們有觀測序列以及其對應的狀態序列,所以我們
可以使用極大似然估計的方法來估計隱馬爾可夫模型的參數
參數:
word_lists: 列表,其中每個元素由字組成的列表,如 ['擔','任','科','員']
tag_lists: 列表,其中每個元素是由對應的標注組成的列表,如 ['O','O','B-TITLE', 'E-TITLE']
word2id: 將字映射為ID
tag2id: 字典,將標注映射為ID
"""
assert len(tag_lists) == len(word_lists)
# 估計轉移概率矩陣
for tag_list in tag_lists:
seq_len = len(tag_list)
for i in range(seq_len - 1):
current_tagid = tag2id[tag_list[i]]
next_tagid = tag2id[tag_list[i+1]]
self.A[current_tagid][next_tagid] += 1
# 問題:如果某元素沒有出現過,該位置為0,這在后續的計算中是不允許的
# 解決方法:我們將等於0的概率加上很小的數
self.A[self.A == 0.] = 1e-10
self.A = self.A / self.A.sum(dim=1, keepdim=True)
# 估計觀測概率矩陣
for tag_list, word_list in zip(tag_lists, word_lists):
assert len(tag_list) == len(word_list)
for tag, word in zip(tag_list, word_list):
tag_id = tag2id[tag]
word_id = word2id[word]
self.B[tag_id][word_id] += 1
self.B[self.B == 0.] = 1e-10
self.B = self.B / self.B.sum(dim=1, keepdim=True)
# 估計初始狀態概率
for tag_list in tag_lists:
init_tagid = tag2id[tag_list[0]]
self.Pi[init_tagid] += 1
self.Pi[self.Pi == 0.] = 1e-10
self.Pi = self.Pi / self.Pi.sum()
def test(self, word_lists, word2id, tag2id):
pred_tag_lists = []
for word_list in word_lists:
pred_tag_list = self.decoding(word_list, word2id, tag2id)
pred_tag_lists.append(pred_tag_list)
return pred_tag_lists
def decoding(self, word_list, word2id, tag2id):
"""
使用維特比算法對給定觀測序列求狀態序列, 這里就是對字組成的序列,求其對應的標注。
維特比算法實際是用動態規划解隱馬爾可夫模型預測問題,即用動態規划求概率最大路徑(最優路徑)
這時一條路徑對應着一個狀態序列
"""
# 問題:整條鏈很長的情況下,十分多的小概率相乘,最后可能造成下溢
# 解決辦法:采用對數概率,這樣源空間中的很小概率,就被映射到對數空間的大的負數
# 同時相乘操作也變成簡單的相加操作
A = torch.log(self.A)
B = torch.log(self.B)
Pi = torch.log(self.Pi)
# 初始化 維比特矩陣viterbi 它的維度為[狀態數, 序列長度]
# 其中viterbi[i, j]表示標注序列的第j個標注為i的所有單個序列(i_1, i_2, ..i_j)出現的概率最大值
seq_len = len(word_list)
viterbi = torch.zeros(self.N, seq_len)
# backpointer是跟viterbi一樣大小的矩陣
# backpointer[i, j]存儲的是 標注序列的第j個標注為i時,第j-1個標注的id
# 等解碼的時候,我們用backpointer進行回溯,以求出最優路徑
backpointer = torch.zeros(self.N, seq_len).long()
# self.Pi[i] 表示第一個字的標記為i的概率
# Bt[word_id]表示字為word_id的時候,對應各個標記的概率
# self.A.t()[tag_id]表示各個狀態轉移到tag_id對應的概率
# 所以第一步為
start_wordid = word2id.get(word_list[0], None)
Bt = B.t()
if start_wordid is None:
# 如果字不再字典里,則假設狀態的概率分布是均勻的
bt = torch.log(torch.ones(self.N) / self.N)
else:
bt = Bt[start_wordid]
viterbi[:, 0] = Pi + bt
backpointer[:, 0] = -1
# 遞推公式:
# viterbi[tag_id, step] = max(viterbi[:, step-1]* self.A.t()[tag_id] * Bt[word])
# 其中word是step時刻對應的字
# 由上述遞推公式求后續各步
for step in range(1, seq_len):
wordid = word2id.get(word_list[step], None)
# 處理字不在字典中的情況
# bt是在t時刻字為wordid時,狀態的概率分布
if wordid is None:
# 如果字不再字典里,則假設狀態的概率分布是均勻的
bt = torch.log(torch.ones(self.N) / self.N)
else:
bt = Bt[wordid] # 否則從觀測概率矩陣中取bt
for tag_id in range(len(tag2id)):
max_prob, max_id = torch.max(
viterbi[:, step-1] + A[:, tag_id],
dim=0
)
viterbi[tag_id, step] = max_prob + bt[tag_id]
backpointer[tag_id, step] = max_id
# 終止, t=seq_len 即 viterbi[:, seq_len]中的最大概率,就是最優路徑的概率
best_path_prob, best_path_pointer = torch.max(
viterbi[:, seq_len-1], dim=0
)
# 回溯,求最優路徑
best_path_pointer = best_path_pointer.item()
best_path = [best_path_pointer]
for back_step in range(seq_len-1, 0, -1):
best_path_pointer = backpointer[best_path_pointer, back_step]
best_path_pointer = best_path_pointer.item()
best_path.append(best_path_pointer)
# 將tag_id組成的序列轉化為tag
assert len(best_path) == len(word_list)
id2tag = dict((id_, tag) for tag, id_ in tag2id.items())
tag_list = [id2tag[id_] for id_ in reversed(best_path)]
return tag_list
訓練及測試
# 訓練HMM模型
train_word_lists, train_tag_lists = train_data
test_word_lists, test_tag_lists = test_data
hmm_model = HMM(len(tag2id), len(word2id))
hmm_model.train(train_word_lists,
train_tag_lists,
word2id,
tag2id)
save_model(hmm_model, "/content/drive/Shared drives/A/temp_file/hmm.pkl")
# 評估hmm模型
pred_tag_lists = hmm_model.test(test_word_lists,
word2id,
tag2id)
metrics = Metrics(test_tag_lists, pred_tag_lists, remove_O=remove_O)
metrics.report_scores()
metrics.report_confusion_matrix()
return pred_tag_lists
運行結果:
正在訓練評估HMM模型...
precision recall f1-score support
E-TITLE 0.9514 0.9637 0.9575 772
O 0.9568 0.9177 0.9369 5190
B-TITLE 0.8811 0.8925 0.8867 772
B-CONT 0.9655 1.0000 0.9825 28
B-EDU 0.9000 0.9643 0.9310 112
M-NAME 0.9459 0.8537 0.8974 82
B-RACE 1.0000 0.9286 0.9630 14
E-RACE 1.0000 0.9286 0.9630 14
M-ORG 0.9002 0.9327 0.9162 4325
M-LOC 0.5833 0.3333 0.4242 21
B-ORG 0.8422 0.8879 0.8644 553
E-PRO 0.6512 0.8485 0.7368 33
E-LOC 0.5000 0.5000 0.5000 6
M-EDU 0.9348 0.9609 0.9477 179
M-CONT 0.9815 1.0000 0.9907 53
E-NAME 0.9000 0.8036 0.8491 112
B-LOC 0.3333 0.3333 0.3333 6
E-EDU 0.9167 0.9821 0.9483 112
M-TITLE 0.9038 0.8751 0.8892 1922
E-CONT 0.9655 1.0000 0.9825 28
B-PRO 0.5581 0.7273 0.6316 33
M-PRO 0.4490 0.6471 0.5301 68
B-NAME 0.9800 0.8750 0.9245 112
E-ORG 0.8262 0.8680 0.8466 553
avg/total 0.9149 0.9122 0.9130 15100
Confusion Matrix:
E-TITLE O B-TITLE B-CONT B-EDU M-NAME B-RACE E-RACE M-ORG M-LOC B-ORG E-PRO E-LOC M-EDU M-CONT E-NAME B-LOC E-EDU M-TITLE E-CONT B-PRO M-PRO B-NAME E-ORG
E-TITLE 744 6 0 0 0 0 0 0 15 0 4 0 0 0 0 0 0 1 2 0 0 0 0 0
O 26 4763 26 0 1 0 0 0 204 0 37 4 0 1 0 2 0 2 78 0 3 12 0 30
B-TITLE 1 20 689 0 2 0 0 0 23 0 6 0 0 0 0 0 0 0 28 0 2 0 0 1
B-CONT 0 0 0 28 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
B-EDU 0 0 0 0 108 0 0 0 1 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0
M-NAME 0 3 0 0 0 70 0 0 3 0 0 0 0 0 0 6 0 0 0 0 0 0 0 0
B-RACE 0 1 0 0 0 0 13 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
E-RACE 0 1 0 0 0 0 0 13 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
M-ORG 4 70 17 0 3 2 0 0 4034 5 38 7 3 1 1 2 0 3 53 1 10 25 1 42
M-LOC 0 4 0 0 0 0 0 0 7 7 0 0 0 0 0 0 1 0 0 0 0 0 0 2
B-ORG 0 28 6 1 0 0 0 0 23 0 491 0 0 0 0 0 3 0 0 0 0 0 1 0
E-PRO 0 0 0 0 1 0 0 0 0 0 0 28 0 1 0 0 0 1 0 0 0 0 0 2
E-LOC 0 2 0 0 0 0 0 0 1 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0
M-EDU 0 1 0 0 0 0 0 0 0 0 0 0 0 172 0 0 0 0 0 0 1 4 0 1
M-CONT 0 0 0 0 0 0 0 0 0 0 0 0 0 0 53 0 0 0 0 0 0 0 0 0
E-NAME 0 16 0 0 0 2 0 0 0 0 0 0 0 0 0 90 0 0 0 0 0 0 0 3
B-LOC 0 1 0 0 0 0 0 0 0 0 3 0 0 0 0 0 2 0 0 0 0 0 0 0
E-EDU 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 110 0 0 0 0 0 0
M-TITLE 6 44 35 0 2 0 0 0 115 0 3 3 0 4 0 0 0 3 1682 0 1 7 0 17
E-CONT 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 28 0 0 0 0
B-PRO 0 0 0 0 1 0 0 0 5 0 0 0 0 0 0 0 0 0 0 0 24 3 0 0
M-PRO 0 0 0 0 1 0 0 0 18 0 0 0 0 1 0 0 0 0 0 0 1 44 0 3
B-NAME 0 8 0 0 0 0 0 0 2 0 1 0 0 0 0 0 0 0 0 0 0 0 98 0
E-ORG 1 10 9 0 1 0 0 0 30 0 0 0 0 0 0 0 0 0 18 0 1 3 0 480
【參考】