HanLP — HMM隱馬爾可夫模型 -- 維特比(Viterbi)算法 --示例代碼 - Python

語料庫 => 標註 => 訓練得到三數組 => 歸一化算概率 => 生成模型文件

加載模型文件 => 標註 => 預測 => 維特比

可以對着這篇貼子看代碼

import pickle
from tqdm import tqdm
import numpy as np
import os


def make_label(text_str):
    """從單詞到label的轉換, 如: 今天 ----> BE  麻辣肥牛: ---> BMME  的 ---> S"""
    text_len = len(text_str)
    if text_len == 1:
        return "S"
    return "B" + "M" * (text_len - 2) + "E"  # 除了開頭是 B, 結尾是 E,中間都是M


def text_to_state(train_file, state_file):
    """ 將原始的語料庫轉換爲 對應的狀態文件 """
    if os.path.exists(state_file):  # 如果存在該文件, 就直接退出
        os.remove(state_file)
    # 打開文件並按行切分到  all_data 中 , all_data  是一個list
    all_data = open(train_file, "r", encoding="utf-8").read().split("\n")
    with open(state_file, "w", encoding="utf-8") as f:  # 代開寫入的文件
        for d_index, data in tqdm(enumerate(all_data)):  # 逐行 遍歷 , tqdm 是進度條提示 , data 是一篇文章, 有可能爲空
            if data:  # 如果 data 不爲空
                state_ = ""
                for w in data.split(" "):  # 當前 文章按照空格切分, w是文章中的一個詞語
                    if w:  # 如果 w 不爲空
                        state_ = state_ + make_label(w) + " "  # 製作單個詞語的label
                if d_index != len(all_data) - 1:  # 最後一行不要加 "\n" 其他行都加 "\n"
                    state_ = state_.strip() + "\n"  # 每一行都去掉 最後的空格
                f.write(state_)


# 定義 HMM類, 其實最關鍵的就是三大矩陣
class HMM:
    def __init__(self, file_text, file_state):
        self.all_states = open(file_state, "r", encoding="utf-8").read().split("\n")[:200]  # 按行獲取所有的狀態
        self.all_texts = open(file_text, "r", encoding="utf-8").read().split("\n")[:200]  # 按行獲取所有的文本
        self.states_to_index = {"B": 0, "M": 1, "S": 2, "E": 3}  # 給每個狀態定義一個索引, 以後可以根據狀態獲取索引
        self.index_to_states = ["B", "M", "S", "E"]  # 根據索引獲取對應狀態
        self.len_states = len(self.states_to_index)  # 狀態長度 : 這裏是4

        # 初始矩陣 : 1 * 4 , 對應的是 BMSE,
        self.init_matrix = np.zeros((self.len_states))
        # 轉移狀態矩陣:  4 * 4 ,
        self.transfer_matrix = np.zeros((self.len_states, self.len_states))

        # 發射矩陣, 使用的 2級 字典嵌套,
        # # 注意這裏初始化了一個  total 鍵 , 存儲當前狀態出現的總次數, 爲了後面的歸一化使用
        self.emit_matrix = {"B": {"total": 0}, "M": {"total": 0}, "S": {"total": 0}, "E": {"total": 0}}

    # 計算 初始矩陣,統計每行第一個字出現的頻次
    def cal_init_matrix(self, state):
        self.init_matrix[self.states_to_index[state[0]]] += 1  # BMSE 四種狀態, 對應狀態出現 1次 就 +1

    # 計算 轉移矩陣,當前狀態到下一狀態的概率
    def cal_transfer_matrix(self, states):
        sta_join = "".join(states)  # 狀態轉移 從當前狀態轉移到後一狀態, 即 從 sta1 每一元素轉移到 sta2 中
        sta1 = sta_join[:-1]
        sta2 = sta_join[1:]
        for s1, s2 in zip(sta1,
                          sta2):  # 同時遍歷 s1 , s2  -- (('B', 'E'), ('E', 'B'), ('B', 'E'), ('E', 'S'), ('S', 'B'), ('B', 'E'), ('E', 'S'))
            self.transfer_matrix[self.states_to_index[s1], self.states_to_index[s2]] += 1

    # 計算 發射矩陣,在特定狀態下,出現某個字的概率
    def cal_emit_matrix(self, words, states):
        """計算 發射矩陣,在特定狀態下,出現某個字的概率
        [
          '今天 天氣 真 不錯 。',
          '麻辣肥牛 好喫 !',
          '我 喜歡 喫 好喫 的 !'
        ]
        [
          'BE BE S BE S',
          'BMME BE S',
          'S BE S BE S S '
        ]
        {
          'B': {'total': 7, '今': 1, '天': 1, '不': 1, '麻': 1, '好': 2, '喜': 1},
          'M': {'total': 2, '辣': 1, '肥': 1},
          'S': {'total': 7, '真': 1, '。': 1, '!': 2, '我': 1, '喫': 1, '的': 1},
          'E': {'total': 7, '天': 1, '氣': 1, '錯': 1, '牛': 1, '喫': 2, '歡': 1}
        }
        """
        # print(tuple(zip("".join(words), "".join(states))))
        for word, state in zip("".join(words), "".join(states)):  # 先把words 和 states 拼接起來再遍歷, 因爲中間有空格
            self.emit_matrix[state][word] = self.emit_matrix[state].get(word, 0) + 1
            self.emit_matrix[state]["total"] += 1  # 注意這裏多添加了一個  total 鍵 , 存儲當前狀態出現的總次數, 爲了後面的歸一化使用

    # 將矩陣歸一化
    def normalize(self):
        self.init_matrix = self.init_matrix / np.sum(self.init_matrix)
        self.transfer_matrix = self.transfer_matrix / np.sum(self.transfer_matrix, axis=1, keepdims=True)
        self.emit_matrix = {
            state: {word: t / word_times["total"] * 1000 for word, t in word_times.items() if word != "total"} for
            state, word_times in
            self.emit_matrix.items()}

    # 訓練開始, 其實就是3個矩陣的求解過程
    def train(self, file_model):
        for words, states in tqdm(zip(self.all_texts, self.all_states)):  # 按行讀取文件, 調用3個矩陣的求解函數
            words = words.split(" ")  # 在文件中 都是按照空格切分的
            states = states.split(" ")
            self.cal_init_matrix(states[0])  # 初始矩陣,統計每行第一個字出現的頻次 [2. 0. 1. 0.]
            self.cal_transfer_matrix(states)  # 轉移矩陣,當前狀態到下一狀態的概率
            self.cal_emit_matrix(words, states)  # 發射矩陣,在特定狀態下,出現某個字的概率
        self.normalize()  # 矩陣求完之後進行歸一化
        pickle.dump([self.init_matrix, self.transfer_matrix, self.emit_matrix], open(file_model, "wb"))  # 保存參數


def viterbi_t(text, model_file):
    with open(model_file, 'rb') as file:
        loaded_matrix = pickle.load(file)

    states_to_index = {"B": 0, "M": 1, "S": 2, "E": 3}  # 給每個狀態定義一個索引, 以後可以根據狀態獲取索引
    states = ["B", "M", "S", "E"]
    start_p = loaded_matrix[0]
    trans_p = loaded_matrix[1]
    emit_p = loaded_matrix[2]
    V = [{}]  # 存路徑的概率
    path = {}  # 存路徑,用於最後根據 S、E 判斷增加空格
    # START 到第一個字的概率
    for y in states:
        # 初始矩陣 * 第一個字在 BMSE 中的概率 [{'B': 95.23809523809524, 'M': 0.0, 'S': 0.0, 'E': 0.0}]
        V[0][y] = start_p[states_to_index[y]] * emit_p[y].get(text[0], 0)
        path[y] = [y]  # {'B': ['B'], 'M': ['M'], 'S': ['S'], 'E': ['E']}
    # 從第二個字開始算 “今天” 找到它的最優路徑,從今到天,有16條路徑,4種狀態,每種狀態下假設一個最優的,再從四條裏面取最優
    for t in range(1, len(text)):
        V.append({})
        newpath = {}

        # 檢驗訓練的發射概率矩陣中是否有該字
        neverSeen = text[t] not in emit_p['S'].keys() and \
                    text[t] not in emit_p['M'].keys() and \
                    text[t] not in emit_p['E'].keys() and \
                    text[t] not in emit_p['B'].keys()
        # B M S E 分別計算四條路徑,然後取最大概率的,做爲最優路徑
        for y in states:
            emitP = emit_p[y].get(text[t], 0) if not neverSeen else 1.0  # 設置未知字單獨成詞,到發射矩陣中,找到字的概率
            temp = []
            # 根據前一個字,做邏輯處理
            for y0 in states:
                pre_val = V[t - 1][y0]
                if pre_val > 0:  # 排除掉前一個字爲0的路徑,前路徑概率 * 發射矩陣 * 轉移矩陣 , 轉移矩陣可以排隊掉一些無效的路徑,如 B->B
                    temp.append((pre_val * trans_p[states_to_index[y0], states_to_index[y]] * emitP, y0))
            (prob, state) = max(temp)  # 找出最大概率 及對應的狀態(y0)
            V[t][y] = prob  # 最大的概率 及 狀態,賦給 V 用於後面找出最大路徑
            newpath[y] = path[state] + [y]  # 前一個字的狀態 + 當前狀態
        path = newpath

    # 求最大概率的路徑,找出最後一個字的 B M S E 的最大值,說明它的路徑概率最大(4條路徑中找出最大的路徑)
    (prob, state) = max([(V[len(text) - 1][y], y) for y in states])

    result = ""  # 拼接結果
    for t, s in zip(text, path[state]):
        result += t
        if s == "S" or s == "E":  # 如果是 S 或者 E 就在後面添加空格
            result += " "
    return result


if __name__ == "__main__":
    model_file = "data/train_model.pkl"

    if not os.path.exists(model_file):
        # 模型不存在,就訓練模型
        train_file = "data/train_data.txt"
        state_file = "data/train_state.txt"
        text_to_state(train_file, state_file)
        hmm = HMM(train_file, state_file)
        hmm.train(model_file)

    # 預測
    text = "今天的天氣不錯"
    result = viterbi_t(text, model_file)
    print(result)

image

源代碼: https://gitee.com/VipSoft/VipPython/tree/master/hmm_viterbi
視頻:代碼講解 https://www.bilibili.com/video/BV1aP4y147gA?p=11

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