基于HMM马尔可夫+维特比算法的中文分词

1.中文分词任务:
在这里插入图片描述
正向最大匹配:

  1. 从左到右将待切分句子的m个字符作为匹配字符,m为初始词典中最长词条的长度。
  2. 将字符与字典中元素进行匹配:
    若匹配成功,则将这个字符作为一个词切分出来。
    若匹配不成功,则将这个字符的最后一个字去掉,再进行匹配,重复上述过程,知道切分完整个文本为止。
    在这里插入图片描述

逆向最大匹配:

  1. 从右至左将待切分句子的m个字符作为匹配字符,m为初始词典中最长词条的长度。
  2. 将字符与字典中元素进行匹配:
    若匹配成功,则将这个字符作为一个词切分出来。
    若匹配不成功,则将这个字符的最后一个字去掉,再进行匹配,重复上述过程,知道切分完整个文本为止。

在这里插入图片描述双向最大匹配:
双向最大匹配法是将正向最大匹配法得到的分词结果和逆向最大匹配法的到的结果进行比较,从而决定正确的分词方法。
在这里插入图片描述
在这里插入图片描述

为什么要分词:

  • 为了更好的理解语义。
  • 为了更重要的 (人工智能)任务(命名实体识别 、 情感分析 、⽂本分类 、语义识别 … …)
  • 应⽤场景需要( 淘宝 、 百度 … …的引擎搜索 )

2.词料库(分词):
在这里插入图片描述
有个字都有⼀个(“隐形状态”)可以根据 语料库 得到 所有 标识

如:

  • B:词语开始
  • M:词语中间
  • E:词语结束
  • S:单独分词

中文分词就是为了得到状态:

   麻辣肥牛 真 好 吃  !
   B M M E S  B  E	S
   (这里的感叹号以及以后的训练词料库中的标点符号都算单独分词(S))

根据已知状态进行分词,即在"E"和"S"的后面输出空格即可:

   麻辣肥牛_真_好吃_!

分好的词 <-----> 每个字的状态
在这里插入图片描述
HMM分词训练 和 维特比算法预测:
在这里插入图片描述
在这里插入图片描述
例句(以下矩阵都以这三句为词料库):

	今天天气真不错!
	麻辣肥牛真好吃!
	我喜欢吃好吃的!

3.初始,转移,发射矩阵(训练):

  • 初始矩阵:统计每一排第一个字属于BEMS中哪一个状态,占比多少:

     今:B
     麻:B
     我:S
    

在这里插入图片描述

  • 转移矩阵:当前状态到下一个状态的概率

在这里插入图片描述

  • 发射矩阵:统计某种状态下,所有字出现的次数(概率)

在这里插入图片描述

(预测):

今天的天气不错----->>BESBEBE----->>今天_的_天气_不错
从以下总路径 4**7 中选择来最优路径即是最终结果
最后通过三个矩阵的计算得到概率最大的路径即为最优路径。(暴力求解)

在这里插入图片描述
在这里插入图片描述
由上图可知一句只有7个字的句子都会出现4**7这样不小的数字,可想而知如果是一篇五千字的论文,这个计算量会多么的庞大,因此提出了维特比算法。

  • 4.维特比算法:
    从众多的路径中,迅速选择出最优路径:

在这里插入图片描述
1-4中最优的路径不一定是全局最优,都成为备选,
比如想要到达B,有四条路径,必然有一条最优路径,保留下来,其余的全部删除。
就这样直到句子结束,每到达一个字会有四个路径,选择最优的 , 则可得到状态序列分词结束
在这里插入图片描述
最后通过三个矩阵的计算得到概率最大的路径即为最优路径。
5.代码实现:

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(file="all_train_text.txt"):  # 将原始的语料库转换为 对应的状态文件

    if os.path.exists("all_train_state.txt"):  # 如果存在该文件, 就直接退出
        return
    all_data = open(file, "r", encoding="utf-8").read().split("\n")  # 打开文件并按行切分到  all_data 中 , all_data  是一个list
    with open("all_train_state.txt", "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_)                                      # 写入文件, state_ 是一个字符串


# 定义 HMM类, 其实最关键的就是三大矩阵
class HMM:
    def __init__(self,file_text = "all_train_text.txt",file_state = "all_train_state.txt"):
        self.all_states = open(file_state, "r", encoding="utf-8").read().split("\n")   # 按行获取所有的状态
        self.all_texts = open(file_text, "r", encoding="utf-8").read().split("\n")     # 按行获取所有的文本
        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

        self.init_matrix = np.zeros((self.len_states))                                 # 初始矩阵 : 1 * 4 , 对应的是 BMSE
        self.transfer_matrix = np.zeros((self.len_states, self.len_states))            # 转移状态矩阵:  4 * 4 ,

        # 发射矩阵, 使用的 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
            self.transfer_matrix[self.states_to_index[s1],self.states_to_index[s2]] += 1

    # 计算发射矩阵
    def cal_emit_matrix(self, words, 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):
        if os.path.exists("three_matrix.pkl"):  # 如果已经存在参数了 就不训练了
                self.init_matrix, self.transfer_matrix, self.emit_matrix =  pickle.load(open("three_matrix.pkl","rb"))
                return
        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])     # 计算三大矩阵
            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("three_matrix.pkl", "wb")) # 保存参数

def viterbi_t( text, hmm):
    states = hmm.index_to_states
    emit_p = hmm.emit_matrix
    trans_p = hmm.transfer_matrix
    start_p = hmm.init_matrix
    V = [{}]
    path = {}
    for y in states:
        V[0][y] = start_p[hmm.states_to_index[y]] * emit_p[y].get(text[0], 0)
        path[y] = [y]
    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()
        for y in states:
            emitP = emit_p[y].get(text[t], 0) if not neverSeen else 1.0  # 设置未知字单独成词
            (prob, state) = max([(V[t - 1][y0] * trans_p[hmm.states_to_index[y0],hmm.states_to_index[y]] * emitP, y0)  for y0 in states if V[t - 1][y0] > 0])
            V[t][y] = prob
            newpath[y] = path[state] + [y]
        path = newpath


    (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__":
    text_to_state()
    text = "今天真快乐,所以我们一起打游戏吧"

    hmm = HMM()
    hmm.train()
    result = viterbi_t(text,hmm)

    print(result)
  • 11
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值