PYTHON 自然语言处理实战核心技术与算法-HMM算法进行分词

这是python 自然语言处理实战核心计算与算法那本书上3.3.2 HMM模型的代码,然后自己加了点备注。




# coding=utf-8
class HMM(object):
    def __init__(self):
        import os
        #后面计算的发射概率,转移概率,和状态的初始概率会放在这里
        self.model_file = r'E:\code\nlp\nlp3\hmm_model.pkl'
        #状态值的集合,我们这边就是知道其观测值来预测最大可能的状态值,凭借状态值来进行分词
        self.state_list=['B','M','E','S']
        #参数的加载,用于判断是否需要加载Model_file
        self.load_para = False


    # 用来加载已计算的中间结果,当需要重新训练的时候需要初始化清空结果
    def try_load_model(self,trained):
        if trained:
            import pickle
            with open(self.model_file, 'rb') as f:
                self.A_dic = pickle.load(f)
                self.B_dic = pickle.load(f)
                self.Pi_dic = pickle.load(f)
                self.load_para = True

        else:
            # 状态转移概率(状态->状态的条件概率(也就是在t时刻是这个状态下t+1时刻变成另一个状态的概率)
            self.A_dic = {}
            # 发射概率(状态->词语的条件概率(也就是这个状态下出现这个词语的概率)
            self.B_dic = {}
            # 状态的初始概率(在一个句子的第一个字是某个状态的概率)
            self.Pi_dic = {}
            self.load_para = False

    def train(self,path):
        # 重置几个概率矩阵
        self.try_load_model(False)

        # 统计状态出现次数,求p(o)(计算某个状态出现的次数)
        Count_dic = {}
        #初始化参数
        def init_parameters():
            #四个状态循环,每个状态都要初始化。
            #这边都是字典的形式,state相当于键值
            for state in self.state_list:
                self.A_dic[state]={s:0.0 for s in self.state_list}
                #因为A 字典是状态转移举证,是字典里面套字典出来的形式类似于:
                #{'B':{'B':0.0,'M':0.5,......},'M':{.......}
                #代表的意思是 B状态转移到B状态的概率是0,B状态转移到M状态的概率是0.5
                self.Pi_dic[state] =0.0
                self.B_dic[state] = {}
                #B举证也是字典里面套字典,不过套的字典不是状态而是字,所以这边直接先创建四个空字典就好
                Count_dic[state] =0
        #开始处理文本,将text 中的每个字的状态给标注出来
        def makeLabel(text):
            out_text =[]
            if len(text) ==1:#如果是text只有一个字的话就将其标注为s
                out_text.append('S')
            else:
                out_text +=['B']+['M']*(len(text)-2)+['E']
            return out_text
        init_parameters()
        line_num = -1
        words = set()#创建一个集合,其实这一句 和后面一句取或 我都觉得没必要 应该根本就没用到这个集合
        with open(path) as f:
            for line in f:#取一行出来
                line_num +=1
                line = line.strip()#移除字符串头尾的指定字符
                if not line:
                    continue#如果没有的话跳过这一个循环

                word_list = [i for i in line if i !=' ']
                words |= set(word_list)
                linelist = line.split()
                line_state = []
                for w in linelist:
                    line_state.extend(makeLabel(w))
                #将每个词中的每个字都存入line_state中,这边用extend 因为extend 可以将一个List 逐个存入
                assert len(word_list) == len(line_state)
                #lin_state 大概是这样line_state ['B', 'E', 'B', 'E', 'B', 'E', 'B', 'E', 's', 's', 's', 's', 's']
                for k ,v in enumerate(line_state):

                    Count_dic[v] +=1 #这个状态出现了一个加一
                    if k ==0:
                        self.Pi_dic[v] +=1 #如果是句子的第一个字,则这个字的状态的次数加1
                    else:
                        self.A_dic[line_state[k-1]][v] +=1
                        #从上一个状态转移到下一个状态的次数加一
                        self.B_dic[line_state[k]][word_list[k]] = self.B_dic[line_state[k]].get(word_list[k],0)+1.0
                        #这个状态下出现这个字的次数加一,后面get的意思是如果在B_dic下的第一个字典(关于状态)下套的字典(关于字)中
                        #有出现这个字,则取出这个字出现的次数再加1 如果没有的话则赋值为0 加一
            #循环完了之后计算概率值
            self.Pi_dic = {k:v *1.0/line_num for k ,v in self.Pi_dic.items()}
            self.A_dic = {k:{k1:v1/Count_dic[k]for k1,v1 in v.items()}for k ,v in self.A_dic.items()}
            #Count_dic[k] 这个状态总共出现的次数
            self.B_dic = {k:{k1:(v1+1)/Count_dic[k] for k1,v1 in v.items()}for k,v in self.B_dic.items()}
            #官方注释上面写这里是加一平滑,我不知道为啥.
            #序列化
            import pickle
            with open(self.model_file,'wb') as f:
                pickle.dump(self.A_dic,f)
                pickle.dump(self.B_dic,f)
                pickle.dump(self.Pi_dic,f)
            return self

    def viterbi(self, text, states, start_p, trans_p, emit_p):
        V = [{}]
        path = {}
        for y in states:
            #将四个状态都寻找一遍,然后得到第一个时刻,每个状态下得到第一个字 的概率
            V[0][y] = start_p[y] * emit_p[y].get(text[0], 0)
            path[y] = [y]
        for t in range(1, len(text)):
            V.append({})
            newpath = {}

            # 检验训练的发射概率矩阵中是否有该字
            #如果都没有的话,neverseen的值为True
            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:
                #这边加一个If 的意思是他可能在这个状态下没这个字,赋予0但是他可能在
                #另一个状态下有这个字,如果都没有的话将其看成单独成字
                emitP = emit_p[y].get(text[t], 0) if not neverSeen else 1.0  # 设置未知字单独成词
                (prob, state) = max(
                    [(V[t - 1][y0] * trans_p[y0].get(y, 0) *
                      emitP, y0)
                     for y0 in states if V[t - 1][y0] > 0])
                #取上一个时刻的四个状态值的概率*从这个状态转移到当前指定状态t*下一个状态下出现这个字的概率
                #的最大值,保存这个最大的概率值和上一个的状态。
                V[t][y] = prob
                newpath[y] = path[state] + [y]
                #这边循环完得到的是到当前时刻要达到这四个状态的最佳值(但是这四个状态不知道选哪个)
                #但是可以确定的是可以知道上一个状态选哪个了
                #更新newpath,让其还是四个键值的字典,字典里面是上一个确定的状态加上当前状态
            path = newpath

        #现在判断最后一个时刻应该选哪个
        #如果最后一个字在词中的概率大于是独立的概率(判断是不是标点符号吧?i guess)
        #则 直接让其在 词中 或者词尾中比较
        #直接比较存在v[上一个时刻][这个时刻的四个状态]谁比较大就可以了
        if emit_p['M'].get(text[-1], 0) > emit_p['S'].get(text[-1], 0):
            (prob, state) = max([(V[len(text) - 1][y], y) for y in ('E', 'M')])
        else:
            (prob, state) = max([(V[len(text) - 1][y], y) for y in states])

        #这里的path是一个四个键值的字典,state代表最后一个状态

        return (prob, path[state])


    def cut(self,text):
        import os
        if not self.load_para:
            self.try_load_model(os.path.exists(self.model_file))
        prob, pos_list = self.viterbi(text, self.state_list, self.Pi_dic, self.A_dic, self.B_dic)
        begin, next = 0, 0
        for i, char in enumerate(text):

            pos = pos_list[i]
            if pos == 'B':
                begin = i
            elif pos == 'E':
                yield text[begin: i + 1]
                next = i + 1
            elif pos == 'S':
                yield char
                next = i + 1
        if next < len(text):
            yield text[next:]

 #这边的yield 可以将每次的yiled的值用List 列表表示出来。

hmm = HMM()
hmm.train(r"E:/code/nlp/nlp3/hmm.txt")
text = '这是一个非常棒的方案!'
res = hmm.cut(text)
print(text)
print(str(list(res)))``

你可能感兴趣的:(nlp学习,NLP)