中文分词技术(二):统计分词--基于隐马尔科夫模型

基于统计的中文分词主要思想是:利用字与字相邻出现的频率来反应成词的可靠度,统计语料中相邻共现的各个字的组合的频度,当组合频度高于某一个临界值时,我们便可认为此字组可能会构成一个词语。

基于统计的分词一般有两个步骤:

1)建立统计语言模型;

2)对句子进行单词划分,然后对划分结果进行概率计算,获得概率最大的分词方式。

一、语言模型

语言模型用概率论的术语来描述就是:为长度为m 的字符串确定其概率分布P(w_{1})(w_{1},w_{2},...w_{m}),其中w_{1}w_{m}依次表示文本中的各个词语。一般采用链式法则计算概率值,如下:

                                  P(w_{1})(w_{1},w_{2},...w_{m})=P(w_{1})P(w_{2}\mid w_{1})P(w_{3}\mid w_{1},w_{2})...P(w_{m}\mid w_{1},w_{2},...w_{m-1})

由于文本过长时,计算难度增大,所以提出了n元模型来降低计算难度。n元模型是值在估算条件概率时,忽略距离大于等于n的上文词的影响,因此上式可简化为:

                                              P(w_{i}\mid w_{1},w_{2},...,w_{i-1})\approx P(w_{i}\mid w_{i-(n-1)},...,w_{i-1})

当n=1时称为一元模型,在一元模型中,整个句子的概率等于各个词语概率的乘积。这样就完全损失了句中的词序信息。

当n=2时称为二元模型,当n=3时称为三元模型。当n>=2时,模型可以保留一定的词序信息,n越大,保留的词序信息越丰富,但计算成本也成指数级增长。

二、隐马尔科夫模型(HMM)

隐马尔科夫模型(HMM)是将分词作为字在字符串中的序列标注任务来实现的。基本思路是:每个字在构造一个特定的词语时都占据着一个特定的构词位置(即词位),规定每个字最多只能有四个构词位置:B(词首)、M(词中)、E(词尾)、S(单独成词)。

例如:中文/分词/是/文本处理/不可或缺/的/一步!

逐字标注:中/B文/E分/B词/E是/S文/B本/M处/M理/E不/B可/M或/M缺/E的/S一/B步/E!/S

用公式表示如下:

                                                          max=maxP(o_{1}o_{2}...o_{n}\mid \lambda _{1}\lambda _{2}...\lambda _{n})

其中,\lambda =\lambda _{1}\lambda _{2}...\lambda _{n}表示输入的句子,n是句子长度,\lambda _{i}表示字,o=o_{1}o_{2}...o_{n}代表输出的标签。

在分词时,o是B、E、M、S这四种标记,\lambda为句子中的每个字(包含标点等非中文字符)。

通过贝叶斯公式可得:

                                                     P(o\mid \lambda )=P(o,\lambda )/P(\lambda )=P( \lambda\mid o )P(o)/P(\lambda )

其中\lambda为给定的输入,P(\lambda )计算为常数,可以忽略,因此最大化P(o\mid \lambda )等价于最大化P(\lambda \mid o)P(o)

接下来要运用到隐马尔科夫模型的两个基本假设

1)观测独立性假设,即任意时刻的观测只依赖该时刻的状态:P(\lambda \mid o)=P(\lambda_{1} \mid o_{1})P(\lambda_{2} \mid o_{2})...P(\lambda_{n} \mid o_{n})

2)齐次马尔科夫假设,即当前输出只与上一个输出有关:P(o)=P(o_{1})P(o_{2}\mid o_{1})P(o_{3}\mid o_{2})...P(o_{n}\mid o_{n-1})

因此,P(\lambda \mid o)P(o)\sim P(\lambda _{1}\mid o_{1})P(o_{2}\mid o_{1})P(\lambda _{2}\mid o_{2})P(o_{3}\mid o_{2})...P(o_{n}\mid o_{n-1})P(\lambda _{n}\mid o_{n})

我们将P(\lambda _{k}\mid o_{k})称为发射概率,P(o_{k}\mid o_{k-1})称为状态转移概率,初始概率P(\pi ),\pi \in \left \{ B,M,E,S \right \}

求解maxP(\lambda \mid o)P(o)的常用方法是维特比算法。它是一种动态规划方法,核心思想是:如果最终的最优路径经过某个o_{i},那么从初始节点到o_{i-1}的路径必然也是一个最优路径,因为每一个节点o_{i}只会影响前后两个P(o_{i-1}\mid o_{i})P(o_{i}\mid o_{i+1})。根据递推的方法,在考虑每个o_{i}时只需要求出所有经过各o_{i-1}点的最优路径,然后再与当前o_{i}结合比较,找出最优路径。

下面通过python实现HMM,这里的语料格式是:每行一句话,每个词以空格分隔。

# coding=utf-8
#根据HMM来进行分词
class HMM():
    def __init__(self):
        self.pi_prob={}
        self.tran_state={}
        self.emit_prob={}
        self.state_set=['B','M','E','S']    #状态集合
    #根据训练样本来计算初始概率、状态转移概率、发射概率
    def calculate_prob(self):
        count_state = {}
        #初始化初始概率、状态转移概率、发射概率
        for state in self.state_set:
            self.pi_prob[state]=0.0
            self.tran_state[state]={s: 0.0 for s in self.state_set}
            self.emit_prob [state]={}
            count_state[state]=0.0
        #获取训练语料中各个字的词位标记
        def get_marks(w):
            out_text = []
            if len(w) == 1:
                out_text.append('S')
            else:
                out_text += ['B'] + ['M'] * (len(w) - 2) + ['E']
            #print(out_text)
            return out_text
        with open('train.txt',encoding='utf-8') as  f:
            line_num=0
            for line in f:
                line_num +=1
                if not line:
                    continue
                line=line.strip()
                word_list =[word for word in line if word !=' ']
                #print(word_list)
                state_list=[]
                line_list=line.split()
                for w in line_list:
                    state_list.extend(get_marks(w))
                #print(state_list)
                assert len(word_list) == len(state_list)        
               #断言,条件为True时正常运行,条件为false时,它先向stderr 打印一条出错信息,然后 
                通过调用 abort 来终止程序运行
                for k,v in enumerate(state_list ):   #迭代状态列表,K是索引,v是状态
                     count_state[v]+=1
                     if k==0:
                         self.pi_prob[v]+=1          
                         #每个句子第一个字的状态统计,用于计算初始状态概率
                     else:
                         self.tran_state[state_list[k-1]][v]+=1     #计算转移概率
                         self.emit_prob[state_list[k]][word_list[k]]=self.emit_prob[state_list[k]].get(word_list[k],0)+1.0   #计算发射概率
            #print(self.emit_prob)
            self.pi_prob={s:self.pi_prob [s]/line_num for s in self.state_set}
            print("初始概率:",self.pi_prob)
            self.tran_state ={k:{k1:(v1+1)/count_state[k] for k1,v1 in v.items()} for k,v in self.tran_state.items()}   #加1平滑
            print("转移概率:",self.tran_state)
            self.emit_prob ={k:{k1:v1/count_state[k] for k1,v1 in v.items()} for k,v in self.emit_prob.items()}
            print("发射概率:", self.emit_prob)
        return self
    #维特比算法(动态规划求最大路径)根据上述求得的三种概率计算输入文本的分词结果
    def viterb(self,text):
        path=[]
        v=[{}]
        si_gama=[{}]
        #当t=1时,计算t=1时刻各个状态产生各个观测结果的概率
        for state in self.state_set:
            si_gama[0][state]=self.pi_prob[state] * self.emit_prob[state].get(text[0],0)
        #print(si_gama[0])
        #当t>=2时,计算最大概率路径
        for t in range(1,len(text)):
            # 检验训练的发射概率矩阵中是否有该字
            neverSeen = text[t] not in self.emit_prob['S'].keys() and \
                        text[t] not in self.emit_prob['M'].keys() and \
                        text[t] not in self.emit_prob['E'].keys() and \
                        text[t] not in self.emit_prob['B'].keys()
            si_gama.append({})
            v.append({})
            for state in self.state_set:
                emit = self.emit_prob[state].get(text[t], 0) if not neverSeen else 1          #设置未知字单独成词
                (si_gama[t][state],j)=max([(si_gama[t-1][y0] * self.tran_state[y0].get(state,0) * emit,y0) \
                                       for y0 in self.state_set if (si_gama[t - 1][y0]) > 0])
                v[t][state]=j
        #根据最优路径概率获取最优路径终点,再逆向寻找其他节点
        p=max(si_gama[-1], key=si_gama[-1].get)
        path.append(p)
        for i in range(len(text) - 1, 0, -1):
            p2=v[i].get(p)
            p=p2
            path.append(p)
        path.reverse()
        print(path)
        return path

    #根据序列标注来展示分词结果
    def cut_text(self,text,path):
        s=''
        for i,p in enumerate(path):
            if p == 'B' or p=='M':
                s+=text[i]
            elif p =='E' or p=='S':
                s=s+text[i]+'/'+' '
        print("原文本:",text)
        print("分词结果:",s)

if __name__ =='__main__':
    model=HMM()
    model.calculate_prob()
    text="这是一年里股市的最高纪录"
    path=model.viterb( text)
    model.cut_text(text, path)

 

你可能感兴趣的:(中文分词技术(二):统计分词--基于隐马尔科夫模型)