【NLP複習】NER:用HMM(隱馬)訓練,用維特比算法預測

一、命名實體識別(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="我在國務院門口經過"))

 

【參考資料】https://www.bilibili.com/video/BV1MJ411w7xR

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章