一、命名實體識別(NER)是生成模型還判別模型?主流模型?
- 是生成模型:P(x|y),訓練過程根據tag預測token;而判別模型P(y|x)與之相反
- 基於LM的主流模型:BERT/LSTM+CRF
- 概率圖模型:HMM
二、何爲HMM?兩個假設和三種參數?如何訓練?有啥缺點?
- HMM就是由隱藏狀態(如實體類型)生成可觀測結果(本文)的過程
- 兩個假設:
- 【下圖藍色】t 時刻的實體標籤只與 t-1 時刻的標籤相關
- 【下圖粉色】觀測獨立假設,也就是t+1時刻的觀測結果文本只與t+1時刻的實體標籤有關
- 三種參數:
- 隱狀態轉移概率矩陣A(tag->tag 維度是:隱狀態個數*隱狀態個數,也就是實體類型數*實體類型數)
- 發射概率矩陣B(tag->char 維度是:實體類型數*詞表大小)
- 初試隱狀態矩陣π(每個tag在訓練集中首次出現概率,維度是 1*實體類型數)
- 缺點:某個點是全局最優路徑中的點,但他局部p值小所以就失去了全局最優解。於是用維特比算法。
三、維特比算法
- 動態規劃的思想解決HMM的局部最優問題
- 構造了兩個矩陣,第一個矩陣存轉移概率,第二個矩陣存最大概率對應前一步的索引(詳見參考視頻)
- 填完兩個矩陣後通過第二個矩陣回溯出最優路徑
import numpy as np
from utils import *
from tqdm import tqdm
class HMM_NER:
def __init__(self, char2idx_path, tag2idx_path):
# char -> idx
self.char2idx = load_dict(char2idx_path)
# tag -> idx
self.tag2idx = load_dict(tag2idx_path)
# idx -> tag (鍵值翻轉)
self.idx2tag = {v: k for k, v in self.tag2idx.items()}
# tag標記數量和vocab中char的數量
self.tag_size = len(self.tag2idx)
self.vocab_size = max([v for _, v in self.char2idx.items()]) + 1 # 最大的索引值
# 初始化A矩陣(tag->tag) B矩陣(tag->vocab) pi(初始值) 全爲0
self.transition = np.zeros([self.tag_size, self.tag_size])
self.emission = np.zeros([self.tag_size, self.vocab_size])
self.pi = np.zeros(self.tag_size)
# 偏置 防止log(0) or 乘以0
self.epsilon = 1e-8
def fit(self, train_dic_path):
"""
訓練HMM model
:param train_dic_path:
"""
print('開始訓練...')
train_dict = load_data(train_dic_path)
print('訓練集條數:', len(train_dict))
# 估計轉移概率矩陣A 初始概率矩陣pi 和 發射概率矩陣B 的參數
self.estimate_transition_and_initial_probs(train_dict)
self.estimate_emission_probs(train_dict)
# 取log防止計算結果下溢 註釋掉下面三行之後 每行和爲1
self.pi = np.log(self.pi)
self.transition = np.log(self.transition)
self.emission = np.log(self.emission)
print('========= 訓練結束的 pi transition emission ==========')
print(self.pi)
print(self.transition)
print(self.emission)
print('=====================================================')
print('訓練完成!!!')
def estimate_transition_and_initial_probs(self, train_dict):
"""
(tag的)轉移概率矩陣A 和初始pi的參數估計(bigram)二元模型
estimate p(Y_t+1 | Y_t)
:param train_dict:
:return:
"""
print("(tag的)轉移概率矩陣A 和初始pi的參數估計(bigram)二元模型...")
for dict in tqdm(train_dict):
# 遍歷每一條訓練數據
for i, tag in enumerate(dict["label"][:-1]):
if i == 0:
# 統計每條數據的起始標籤 看看數據集中7種標籤作爲起始的各有多少個
self.pi[self.tag2idx[tag]] += 1
# print(self.pi)
# 取當前跟下個tag的index
curr_tag = self.tag2idx[tag]
next_tag = self.tag2idx[dict["label"][i + 1]]
self.transition[curr_tag, next_tag] += 1
# 遍歷完所有訓練數據後計算分子/分母
self.transition[self.transition == 0] = self.epsilon # 分子
self.transition /= np.sum(self.transition, axis=1, keepdims=True) # 分子/分母 (行裏每個元素/按行求和)
self.pi[self.pi == 0] = self.epsilon
self.pi /= np.sum(self.pi)
print('ok')
def estimate_emission_probs(self, train_dict):
"""
發射概率矩陣(tag->vocab)參數估計
:param train_dict:
:return:
"""
print("發射概率矩陣(tag->vocab)參數估計...")
for dict in tqdm(train_dict):
# 遍歷每一條訓練數據
for char, tag in zip(dict["text"], dict["label"]):
# print(char, tag)
# print(self.char2idx[char])
# print(self.tag2idx[tag])
self.emission[self.tag2idx[tag], self.char2idx[char]] += 1
# 遍歷完所有訓練數據後計算分子/分母
self.emission[self.emission == 0] = self.epsilon # 分子
self.emission /= np.sum(self.emission, axis=1, keepdims=True) # 分子/分母 (行裏每個元素/按行求和)
print('ok')
def viterbi_decode(self, text):
"""
傳入text文本 使用維特比算法輸出最有可能的隱狀態路徑
:param text: str
:return:
"""
# 按字切分的序列長度
seq_len = len(text)
# 初始化T1和T2表格
T1_table = np.zeros([seq_len, self.tag_size]) # 保存tag1轉移到tag2轉移概率
T2_table = np.zeros([seq_len, self.tag_size]) # 保存這個位置是從哪個tag轉移過來的
# 求第1時刻的發射概率
start_p_Obs_State = self.get_p_Obs_State(text[0]) # 計算P(Obs_vocab|State_tag)
# 計算第一步初始概率, 填入表中
T1_table[0, :] = self.pi + start_p_Obs_State
T2_table[0, :] = np.nan
for i in range(1, seq_len):
# 維特比算法在每一時刻計算落到每一個隱狀態的最大概率和路徑
# 並把他們暫存起來
# 這裏用到了矩陣化計算方法, 詳見視頻教程
p_Obs_State = self.get_p_Obs_State(text[i])
p_Obs_State = np.expand_dims(p_Obs_State, axis=0)
prev_score = np.expand_dims(T1_table[i-1, :], axis=-1)
# 廣播算法, 發射概率和轉移概率廣播 + 轉移概率
curr_score = prev_score + self.transition + p_Obs_State
# 存入T1 T2中
T1_table[i, :] = np.max(curr_score, axis=0)
T2_table[i, :] = np.argmax(curr_score, axis=0)
# 回溯
best_tag_id = int(np.argmax(T1_table[-1, :]))
best_tags = [best_tag_id, ]
for i in range(seq_len-1, 0, -1):
best_tag_id = int(T2_table[i, best_tag_id])
best_tags.append(best_tag_id)
return list(reversed(best_tags))
def predict(self, text):
# 預測並打印出預測結果
# 維特比算法解碼
if len(text) == 0:
raise NotImplementedError("輸入文本爲空!")
best_tag_id = self.viterbi_decode(text)
self.print_func(text, best_tag_id)
def print_func(self, text, best_tags_id):
# 用來打印預測結果
for char, tag_id in zip(text, best_tags_id):
print(char+"_"+self.idx2tag[tag_id]+"|", end="")
def get_p_Obs_State(self, char):
# 計算P(Obs_vocab|State_tag)
char_idx = self.char2idx.get(char, 0) # get(char)是找char的索引
if char_idx == 0:
return np.log(np.ones(self.tag_size) / self.tag_size)
return np.ravel(self.emission[:, char_idx]) # 將多維數組降爲一維返回拷貝不影響之前的
if __name__ == '__main__':
hmm = HMM_NER(char2idx_path='dicts/char2idx.json', tag2idx_path='dicts/tag2idx.json')
hmm.fit(train_dic_path='corpus/train_data.txt')
print(hmm.predict(text="我在國務院門口經過"))