基于隐马尔科夫模型(HMM)的中文分词(CWS)(附代码以及注释)

我是一个刚入门的菜鸟,刚学习了HMM算法以及BiLSTM+CRF进行中文分词,记录下学习过程,欢迎大家讨论。

本文以模型使用为导向,介绍如何一步步使用HMM算法进行中文分词。

本文代码github地址:https://github.com/WhiteGive-Boy/CWS-Hmm_BiLSTM-CRF  

目录

1.HMM

1.1HMM简单介绍

1.2HMM参数解释

2.CWS

2.1.大体介绍

2.2.应用HMM进行分词

3.具体实现

3.1数据处理

3.2 参数计算

3.3 预测,进行分词


1.HMM

1.1HMM简单介绍

现在有两枚骰子,第一个骰子是正常的,1-6概率各为1/6;第二个骰子由于材料等原因各个数字概率不一,随便假设个数字,数字对应的概率如{1:0.1   2:0.1  3:0.1   4:0.1   5:0.1   6:0.5}。

我现在进行一系列投掷,每次可以选择两个骰子中的一个,我现在给定下一个时刻选择骰子的概率:

骰子1 骰子2
骰子1 0.3 0.7
骰子2 0.6 0.4

上表的意思与下面的假设一二相关,即我每次的骰子仅与上次骰子的类型相关,且转移概率是给定的。

(1)HMM的基本定义: HMM是用于描述由隐藏的状态序列和显性的观测序列组合而成的双重随机过程。投掷骰子的过程中,选择投掷哪个骰子就是隐藏的状态序列,这个序列是我们观测不到的。我们得到的数字就是观测序列,这个序列是我们能够观测到的。这两个序列都是随机序列。

(2)HMM的假设一:马尔科夫性假设。当前时刻的状态值,仅依赖于前一时刻的状态值,而不依赖于更早时刻的状态值。每次选择投掷哪一个骰子,会和前一天的骰子选择有关。

(3)HMM的假设二:齐次性假设。状态转移概率矩阵与时间无关。即所有时刻共享同一个状态转移矩阵。即当前骰子选择下一个骰子的概率是定的,全体骰子共享的。

(4)HMM的假设三:观测独立性假设。当前时刻的观察值,仅依赖于当前时刻的状态值。当前投掷出的数字仅与此刻选择哪个骰子有关。

(5)HMM的应用目的:通过可观测到的数据,预测不可观测到的数据。我们想通过投掷出的数字序列,猜测他投掷的骰子序列。

1.2HMM参数解释

状态值集合:隐藏状态可能的值的集合,如{骰子1.骰子2} ,设为N个

观测值集合:观测状态可能的值的集合,如{1,2,3,4,5,6},设为M个

状态值序列:一系列投掷中的骰子序列号,比如{骰子1,骰子2,骰子1,骰子1,骰子2......}

观测值序列:一系列投掷中的数字序列号,如{1,2,3,6,4,3,2....}

三个参数:A,B,π。

A:状态转移概率矩阵。表征转移概率,维度为N*N。

上例为:

骰子1 骰子2
骰子1 0.3 0.7
骰子2 0.6 0.4

B:观测概率矩阵。表征发射概率,维度为N*M。

1 2 3 4 5 6
骰子1 1/6 1/6 1/6 1/6 1/6 1/6
骰子2 0.1 0.1 0.1 0.1 0.1 0.5

π:初始状态概率向量。维度为N*1。即我初始选择哪个骰子开始投掷。

骰子1 骰子2
初始选择概率 0.5 0.5

2.CWS

2.1.大体介绍

中文分词,是东亚地区自然语言处理NLP所独有的一个任务,包括CJKV(中日韩越统一表意文字),即Unicode编码。这

是一种有别于字母文字(语言以空格间隔)的分词方法。这是汉文化圈独有的自然语言,人类智慧的结晶。当然,这种高级的语

言也带了高难度的挑战,那就是分词的挑战。

       不同于英文等语言,中文分词的难点在于 :

               1、中文分词标准不统一

               2、未登录问题

               3、歧义问题

               4、复合词的识别

               5、专有名词的识别

与英文相比,中文很多都是词组结合来表示一个意思,单个词可能不是最小的表意单位,需要多个词组合来表意。

2.2.应用HMM进行分词

状态值集合:{B:分词词首;M:分词词中;E:分词词尾;S:单个词分词}

观测值集合:每个观测值就是每个字。集合就是我们训练集各个字的合集

状态值序列:需要我们得到的结果{S,S,B,M,E,B,E,S,S,B,E}

观测值序列:一句话,一个sentence,如{我喜欢吃火锅}。我们的训练集就是很多sentence的集合

我们的算法要做的就是根据给定的数据集计算我们HMM的三个参数

A:状态转移概率矩阵。

B M E S
B
M
E
S

B:观测概率矩阵。

.....
B
M
E
S

π:初始状态概率向量。

B M E S
初始选择概率

HMM进行分词的思路很清晰,我们先将输入的数据处理后,跟据每个状态值和观测值填充我们的矩阵,最后根据转移矩阵进行预测,输出我们的分词结果。在预测时候需要用到维特比算法,算法核心跟dp很像,简单的来说,就是对每一个词,我们计算他作为每一个状态的可能的概率score,选择最大的一个进行记录,在计算概率的时候需要用到前一个词的score值,具体算法自行学习,很多大佬讲的很清晰,这里不展开了。

3.具体实现

3.1数据处理

数据选择:人民日报数据集,数据格式如下:

基于隐马尔科夫模型(HMM)的中文分词(CWS)(附代码以及注释)_第1张图片

每一个sentence我们根据以空格划分进行分词。

初步的数据处理我们需要将每句话转成我们的状态序列如{坚持 改革 ‘  开放 ,}这句话我们分为{B,E,B,E,S,B,E,S},当然你可以选择将中间的’号在读取时处理掉。

import codecs
from sklearn.model_selection import train_test_split#进行训练集和测试集划分
import pickle#进行参数保存


INPUT_DATA = "./RenMinData.txt_utf8"#数据集
SAVE_PATH="./datasave.pkl"#保存路径
id2tag = ['B','M','E','S'] #B:分词头部 M:分词词中 E:分词词尾 S:独立成词 id与状态值
tag2id={'B':0,'M':1,'E':2,'S':3}#状态值对应的id
word2id={}#每个汉字对应的id
id2word=[]#每个id对应的汉字

def getList(input_str):
    '''
    单个分词转换为tag序列
    :param input_str: 单个分词
    :return: tag序列
    '''
    outpout_str = []
    if len(input_str) == 1: #长度为1 单个字分词
        outpout_str.append(tag2id['S'])
    elif len(input_str) == 2:#长度为2 两个字分词,BE
        outpout_str = [tag2id['B'],tag2id['E']]
    else:#长度>=3 多个字分词 中间加length-2个M 首尾+BE
        M_num = len(input_str) -2
        M_list = [tag2id['M']] * M_num
        outpout_str.append(tag2id['B'])
        outpout_str.extend(M_list)
        outpout_str.append(tag2id['E'])
    return outpout_str


def handle_data():
    '''
    处理数据,并保存至savepath
    :return:
    '''
    x_data=[] #观测值序列集合
    y_data=[]#状态值序列集合
    wordnum=0
    line_num=0
    with open(INPUT_DATA,'r',encoding="utf-8") as ifp:
        for line in ifp:#对每一个sentence
            line_num =line_num+1
            line = line.strip()
            if not line:continue
            line_x = []
            for i in range(len(line)):
                if line[i] == " ":continue
                if(line[i] in id2word): #word与id对应进行记录
                    line_x.append(word2id[line[i]])
                else:
                    id2word.append(line[i])
                    word2id[line[i]]=wordnum
                    line_x.append(wordnum)
                    wordnum=wordnum+1
            x_data.append(line_x)

            lineArr = line.split(" ")
            line_y = []
            for item in lineArr:#对每一个分词进行状态值转换
                line_y.extend(getList(item))
            y_data.append(line_y)

    print(x_data[0])
    print([id2word[i] for i in x_data[0]])
    print(y_data[0])
    print([id2tag[i] for i in y_data[0]])
    x_train, x_test, y_train, y_test = train_test_split(x_data, y_data, test_size=0.2, random_state=43) #分为训练集和测试集
    with open(SAVE_PATH, 'wb') as outp: #保存
        pickle.dump(word2id, outp)
        pickle.dump(id2word, outp)
        pickle.dump(tag2id, outp)
        pickle.dump(id2tag, outp)
        pickle.dump(x_train, outp)
        pickle.dump(y_train, outp)
        pickle.dump(x_test, outp)
        pickle.dump(y_test, outp)

if __name__ == "__main__":
    handle_data()


3.2 参数计算

def init():
    '''
    参数初始化
    Trans = {}  #状态转移矩阵
    Emit = {}  #观测概率矩阵
    Count_dic = {} #每个状态的数量计数
    Start = {}  #初始概率矩阵

    '''
    for tag in tag2id:
        Trans[tag2id[tag]] = {}
        for tag2 in tag2id:
            Trans[tag2id[tag]][tag2id[tag2]] = 0.0
    for tag in tag2id:
        Start[tag2id[tag]] = 0.0
        Emit[tag2id[tag]] = {}
        Count_dic[tag2id[tag]] = 0
def train():
    '''
    根据输入的训练集进行各个数组的填充
    :return:
    '''
    for sentence, tags in zip(x_train, y_train):
        for i in range(len(tags)):
            if i == 0:
                Start[tags[0]] += 1
                Count_dic[tags[0]] += 1
            else:
                Trans[tags[i - 1]][tags[i]] += 1
                Count_dic[tags[i]] += 1
                if sentence[i] not in Emit[tags[i]] :
                    Emit[tags[i]][sentence[i]] = 0.0
                else:
                    Emit[tags[i]][sentence[i]] += 1

    for tag in Start:
        Start[tag] = Start[tag] * 1.0 / len(x_train)
    for tag in Trans:
        for tag1 in Trans[tag]:
            Trans[tag][tag1] = Trans[tag][tag1] / Count_dic[tag]

    for tag in Emit:
        for word in Emit[tag]:
            Emit[tag][word] = Emit[tag][word] / Count_dic[tag]
    print(Start)
    print(Trans)

3.3 预测,进行分词

最后模型效果使用fscore进行评分,分为precision和recall 准确率和召回率

def viterbi(sentence, tag_list):
    '''

    :param sentence:  输入的句子
    :param tag_list:  所有的tag
    :return: prob预测的最大的概率 bestpath 预测的tag序列
    '''
    V = [{}] #tabular
    path = {}
    backpointers = []
    for y in tag_list: #init
        V[0][y] = Start[y] * (Emit[y].get(sentence[0],0.00000001))
        path[y]=y
    backpointers.append(path)
    for t in range(1,len(sentence)):
        V.append({})
        newpath = {}
        path = {}
        for y in tag_list:
            (prob,state ) = max([(V[t-1][y0] * Trans[y0].get(y,0.00000001) * Emit[y].get(sentence[t],0.00000001) ,y0) for y0 in tag_list])
            V[t][y] =prob
            path[y]=state
        backpointers.append(path)
    (prob, state) = max([(V[len(sentence) - 1][y], y) for y in tag_list])
    best_path=[]
    best_path.append(state)
    for pathi in reversed(backpointers):
        state = pathi[state]
        best_path.append(state)
    best_path.pop()
    # Pop off the start tag (we dont want to return that to the caller)
    best_path.reverse()
    return (prob, best_path)

def test():
    '''
    计算Precision和Recall以及Fscore
    '''
    taglist=[tag2id[tag] for tag in tag2id]
    entityres = []#根据预测结果的分词序列
    entityall = []#根据真实结果的分词序列
    for sentence, tags in zip(x_test, y_test):
        #score, predict=viterbi(sentence,taglist,Start,Trans,Emit)
        score, predict = viterbi(sentence, taglist)
        entityres = calculate(sentence, predict, id2word, id2tag, entityres)
        entityall = calculate(sentence, tags, id2word, id2tag, entityall)

    rightpre = [i for i in entityres if i in entityall]#预测成功的分词序列
    if len(rightpre) != 0:
        precision = float(len(rightpre)) / len(entityres)
        recall = float(len(rightpre)) / len(entityall)
        print("precision: ", precision)
        print("recall: ", recall)
        print("fscore: ", (2 * precision * recall) / (precision + recall))
    else:
        print("precision: ", 0)
        print("recall: ", 0)
        print("fscore: ", 0)
def calculate(x,y,id2word,id2tag,res=[]):
    '''

    :param x: 输入的句子(转换后的ID序列)
    :param y: 标注tag序列
    :param id2word: id2word
    :param id2tag: id2tag
    :param res: 添加输入句子的词组划分 BME S
    :return: res
    '''
    entity=[]
    for j in range(len(x)):
        if id2tag[y[j]]=='B':
            entity=[id2word[x[j]]]
        elif id2tag[y[j]]=='M' and len(entity)!=0:
            entity.append(id2word[x[j]])
        elif id2tag[y[j]]=='E' and len(entity)!=0:
            entity.append(id2word[x[j]])
            res.append(entity)
            entity=[]
        elif id2tag[y[j]]=='S':
            entity=[id2word[x[j]]]
            res.append(entity)
            entity=[]
        else:
            entity=[]
    return res

3.4 模型结果:

HMM:
precision:0.87980196
recall:   0.84381022
fscore:   0.86143031

关于HMM的算法具体学习公示推导等可参考:一站式解决:隐马尔可夫模型(HMM)全过程推导及实现 - 知乎  NLP硬核入门-隐马尔科夫模型HMM - 知乎

你可能感兴趣的:(自然语言处理,深度学习,机器学习,python)