vlambda博客
学习文章列表

HMM、Viterbi与中文分词应用

前言

在处理题库去重采用了关键词提取+simhash的办法。而提取关键词之前,需要先进行中文分词。一种基本方法是基于词库进行分词,但显然词库是不可能齐全的,这时,为了确认对于未被记入词库的词(未登录词)如别被处理,就需要有一定了解,才能准确应对意外的分词情况。本文为作者在进行题库去重过程中,对中文分词的学习经验总结,在此积累,以期为后来者参考学习、彼此交流。
   本篇介绍HMM(隐马尔可夫模型)和Viterbi(维特必)算法,主要面向对概率论不熟悉而希望对中文分词工具有更进一步了解的同学,期待为读者深入了解其他相关算法提供学习方式思路。同时,也期待专业人士能提供更进一步的意见或建议。
   同时,希望数学不好的同学对此也不必有恐惧心理。因为作者也不是对此特别熟悉,会结合个人的学习过程和实例,尽可能简单的介绍HMM、Viterbi,以及它们与中文分词三者之间的关系。
同时,希望数学不好的同学对此也不必有恐惧心理。因为作者也不是对此特别熟悉,会结合个人的学习过程和实例,尽可能简单的介绍HMM、Viterbi,以及它们与中文分词三者之间的关系。

正文

在正式介绍HMM之前,我们有必要先了解马尔可夫过程和马尔可夫链。

Markov process(马尔可夫过程)

一个性质:当一个随机过程在给定现在状态及所有过去状态情况下,其未来状态的条件概率分布仅依赖于当前状态;换句话说,在给定现在状态时,它与过去状态(即该过程的历史路径)是条件独立的,那么此随机过程即具有马尔可夫性质
一个定义:满足马尔可夫性质的过程称为马尔可夫过程

DTMC(离散马尔可夫链)

一个定义:时间和状态都是离散的马尔可夫过程。
一个重要特性:已知系统当前状态,则未来的演变不依赖于过去的演变。
一个泛化描述:假设,当前时间是T,则T+1等于某个值的概率,仅与T相关,而与[0, T-1]中的所有值都无关(一阶马尔可夫链)。

一个矩阵: 这个是其实是n步转移概率矩阵,详细讲解较为复杂,可以自行百度,不影响后文理解。

HMM、Viterbi与中文分词应用

如此,先记住什么是马尔可夫过程和DTMC,后续再了解什么是HMM(隐马尔可夫模型)就容易了。

HMM(隐含马尔可夫模型)

在正式介绍之前,我们先用一个句子作为实例。我们直接给定一个文本,按HMM给这个文本分词。
"我爱梅沙"

接下来我们按照HMM的过程进行分析。
  1. 对于句子中的字,我们定义起包含四种状态(隐含状态空间S):
    • B——begin。表示该字为词的起始字
    • M——middle。表示该字为词的中间字
    • E——end。表示词语中的结束字
    • S——single。表示单字成词
  2. 我们定义句子中,所有的字共同组成了一个观测值集合(观察状态),即:O = { 我, 爱, 梅, 沙 }。
  3. 当我们看到一个字(观察状态)时,它是BMES中某一个的概率,组成一个初始的概率矩阵(矩阵值已经基于 海量文本统计求得)。对于任何字,转移到BMES中的概率都是一致的,因此该矩阵是1x4的概率矩阵(π向量/初始概率矩阵),在这里,矩阵如下:
    { "B": -0.26268660809250016, "E": -3.14e+100, "M": -3.14e+100, "S": -1.4652633398537678}




  4. 已经存在一个概率矩阵,是当前字分别为BMES时,下一个字为BMES的概率(4x4的状态转移概率矩阵,矩阵中的值已经基于海量文本统计求得),即:HMM、Viterbi与中文分词应用,实际,这里很多情况概率为0,因此,实际矩阵如下:

    { "B": { "E": -0.510825623765990, "M": -0.916290731874155 }, "E": { "B": -0.5897149736854513, "S": -0.8085250474669937 }, "M": { "E": -0.33344856811948514, "M": -1.2603623820268226 }, "S": { "B": -0.7211965654669841, "S": -0.6658631448798212 }}


  5. 已经存在一个概率矩阵,每个字分别为BMES各个状态时的概率(发射概率矩阵/混淆矩阵,矩阵中的值已经基于 海量文本统计求得),即:{ Bij | i ∈ [0x4E00, 9FA5] , j ∈ (B, M, E, S) },在这里,发射矩阵如下:
        
          
          
        
     { "B": { "我": 1.4614045662995514, "爱": 1.968025153941063, "梅": 2.237194588185915, "沙": 1.983966134924789 }, "E": { "我": 2.8844153800921876e+101, "爱": 2.5467388205842573e+101, "梅": 2.5483227218706336e+101, "沙": 2.6413544826999272e+101 }, "M": { "我": 2.6778005616524882e+101, "爱": 2.2547330469174095e+101, "梅": 2.5528428570784386e+101, "沙": 2.321741847245321e+101 }, "S": { "我": 6.611019698336738, "爱": 11.146923368528606, "梅": 14.546547456418994, "沙": 13.526900849382743 } }

如此,我们得到了HMM的五元( S, O, Π, A, B )
  1. 隐状态 S:由马尔科夫过程描述,是隐状态集合

  2. 观察状态 O:是显示/直接观测而得的状态集合

  3. (隐含)状态转移矩阵 A:隐状态之间相互转移的概率矩阵

  4. 初始化概率向量 Π i) 每个观察状态,初始时分别为各个隐状态的概率集合。是一个 1xN(N是隐状态空间大小)的矩阵
  5. 混淆矩阵/发射概率矩阵 B:每个观察状态转移至各个隐状态的概率集合(发射概率)

    { "B": { "我": 1.4614045662995514, "爱": 1.968025153941063, "梅": 2.237194588185915, "沙": 1.983966134924789 }, "E": { "我": 2.8844153800921876e+101, "爱": 2.5467388205842573e+101, "梅": 2.5483227218706336e+101, "沙": 2.6413544826999272e+101 }, "M": { "我": 2.6778005616524882e+101, "爱": 2.2547330469174095e+101, "梅": 2.5528428570784386e+101, "沙": 2.321741847245321e+101 }, "S": { "我": 6.611019698336738, "爱": 11.146923368528606, "梅": 14.546547456418994, "沙": 13.526900849382743 }}


确定好模型各个部分,接下来就可以按步骤处理。大致思路是

  1. 计算初始状态
  2. 一次计算第二次、第三次、第四次的转移概率
  3. 找出最大概率路径(动态规划)


为什么上文中统计求得的概率结果都是负数?
这涉及到计算机中的 下溢问题,对于一般运算,下溢带来的误差并不明显,但在中文分词中,字库非常大 [0x4E00, 9FA5],而概率总是属于区间[0, 1]的,这时,一点点的下溢问题,都有可能导致整个模型计算错误。因此,推荐使用对数处理实际应用中的高精度概率计算问题。通过简单查看对数函数图像,我们即可理解为什么存储的概率都是负数了。但为什么是对数而不是其他函数?个人认为主要因素有以下两点,但或许未能描述清楚,欢迎同学补充:
  1. 浮点数乘除运算可以转为对数和差运算,减少溢出精度和误差累积:

    HMM、Viterbi与中文分词应用

  2. 对数在(-∞, +∞)中,值是(0, +∞)的,因此对于无限趋近于0的概率也能表示

  3. 对于任意的x ∈ [0, 1],x任何一点变化都会导致其对数结果变化明显



计算初始状态

按照上述HMM五元组,结合该公HMM、Viterbi与中文分词应用计算即可得。

{
   "B": {
       "我":1.1851974272161176,
       "爱":1.9983762718297673,
       "梅":2.388164278255412,
       "沙":2.5132210235744683 
  },
   "E":{
       "我":1.4167147493671037e+101,
       "爱":2.388740537292971e+101,
       "梅":2.854670014651611e+101,
       "沙":3.004155434998415e+101
  },
   "M":{
       "我":1.4167147493671037e+101,
       "爱":2.388740537292971e+101,
       "梅":2.854670014651611e+101,
       "沙":3.004155434998415e+101
  },
   "S":{
       "我":6.611019698336738,
       "爱":11.146923368528606,
       "梅":13.321157069582242,
       "沙":14.018722376196262
  }
}


计算状态转移概率矩阵

得到上述初始状态后,可以结合前文所述的HMM五元组,迭代求得如下关系图中所有弧的概率。t次迭代公式如下(这其实也可以泛化为马尔可夫n步转移矩阵迭代公式)。

HMM、Viterbi与中文分词应用

将弧的概率作为权值,寻找最大权值路径,即可求得。由于计算过程过于复杂,在这里不列出。经过代码测试,案例分词结果为『我/爱梅沙』。

HMM、Viterbi与中文分词应用

不是所有预测性问题都能使用HMM解决。实际上,HMM有三个重要假设(个人理解,三个假设即为满足这三个假设的问题,都可以使用HMM解决)。

  1. 马尔可夫假设——即上文所述一阶马尔可夫链,当前为某状态的概率仅与上一次的状态有关,而与更早的状态无关
    HMM、Viterbi与中文分词应用
  2. 不动性假设——状态与时间/时序无关

  3. 输出独立性假设——输出状态仅与当前状态有关,即当前输入状态得到确定的输出状态,这个过程不随更早的输入、输出或时间而被改变

Viterbi——HMM的优化实现

HMM有三个概率矩阵,对于UTF-8编码,中文字的数量也是庞大的。这时候,随着运算(待分词的句子长度)增加,混淆矩阵和状态转移概率矩阵都会指数增长,再基于动态规划求最大概率路径,这个过程的不论是时间复杂度还是空间复杂度都可能指数增长。这时,使用Viterbi算法就是为了解决这个问题——当满足某些约束条件时,我们不需要求解DAG中,所有弧的概率(权值)。约束条件如下(该约束条件为个人总结,不知是否准确。欢迎同学修正或补充):

  1. 求解网络中的最长路径

  2. 所有节点的选择仅受上一层最大概率节点和当前节点输入值影响

结合前文,关于HMM的三个假设,可以发现,Veterbi适用于求解HMM的最大概率路径。具体过程不再这里赘述,大体思路是初始概率计算与前文所述计算方式相同,后续所有状态的计算,都只取上一层计算中,最大概率节点。这是一个类似递归的过程,最后回溯可以得到分词结果。可以感受到,使用Viterbi算法后的主要效果是,省去了其他无关路径带来的计算量(个人认为这有点像贪心,但贪心得到的不一定是最优解,而Viterbi算法结果是最优解)。



很多人都用隐马尔科夫模型来回答viterbi算法,其实viterbi算法只是解决隐马第三个问题(求观察序列的最可能的标注序列)的一种实现方式。这个问题可以用于viterbi算法实现,也可以用其他方式实现(如穷举法);而viterbi算法可以用于解决隐马第三问题,也可以用于解决其他问题。所以千万不要把viterbi算法和隐马尔科夫模型等价了。


https://www.zhihu.com/question/20136144


viterbi算法其实就是多步骤每步多选择模型的最优选择问题,其在每一步的所有选择都保存了前续所有步骤到当前步骤当前选择的最小总代价(或者最大价值)以及当前代价的情况下前继步骤的选择。依次计算完所有步骤后,通过回溯的方法找到最优选择路径。符合这个模型的都可以用viterbi算法解决,隐马模型的第三问题刚好符合这个模型,所以才采用了viterbi算法

https://www.zhihu.com/question/20136144



为什么是Viterbi而不是拓扑排序?

个人理解是:拓扑排序解决的是单源点到多汇点的路径选择问题,而HMM中的网络结构是多源的——初始显状态空间,此时每种情况都是一个起点,因此,拓扑排序对于HMM的计算是不适用的。但我也认为这个解释不够合适,欢迎各位优秀同学纠正。

此外,看到带权、有向无环图两个关键字,很容易让人联想到AOE网。我们要明确,AOE网描述的是单源点;单汇点;带权;有向无环图。AOE网的关键路径选择过程在这里是不适用的,二者不可混淆。


结巴分词

通过上文所述,相信大家对未登录词的解析方式有了一定概念,更多的信息推荐直接阅读文末参考文献[1]。当然,这只是对于未登录词的常见处理方式。实际上,我们能获取很庞大的词典(ictclas提供包含3000万词的免费词库),也能基于已有数据和HMM统计一份自己的词典。对于结巴分词,会首先基于词典文件构造前缀词典;分词时,基于前缀词典完全切分句子,再以每个词的成词概率为权值,构造DAG(有向无环图);最后,用动态规划的办法从后向前遍历,查找最大权值路径。其中,未登录词的成词概率基于HMM模型,采用Viterbi算法计算。

关于这个基本过程,其实结巴分词的README文件中有描述。但对于不了解HMM的同学而言,看到『对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法』这句话可能会比较懵逼。结合上文所述,再回归到结巴分词的README文件中关于算法的说明及其具体代码中,思路可能会清晰很多。

此外,结巴分词关于DAG采用十字链表法(将每个词看做一条弧,词的起始字的位置为弧尾,结束字位置为弧头)。在结巴分词中,可以找到getDAG方法,尝试打印其返回值有助于更深刻了解这一存储结构和后续遍历过程。

引用 https://github.com/fxsjy/jieba/blob/master/README.md#算法

  • 基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG)

  • 采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合

  • 对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法

在上文,我们提及,关于隐含状态、隐含状态概率矩阵已经通过统计得到。实际上,除此之外,还有很多参数都是如此。具体可以参见结巴分词 issue#7 数据是如何生成的。需要注意的是,该issue的回答是2012年。于2013年(latest update),作者还更新出了 big 版词典。即目前数据来源比 issue #7 中作者回复的数据来源更全面。


垂直领域优化

在了解了 HMM 和结巴分词的基本过程后,我们可以明白,尽管结巴分词提供了 HMM 算法的实现,用于实现对未登录词的解析,但为了在生产环境中实现更高的 Precision 和 Recall ,我们有必要获取垂直领域的词库,并统计相应词频,并修改分词规则。以期提高对垂直领域(尤其是仅该领域特有的文本特性,如数学、物理、计算机编程等)文本处理的精确度。


小结

对于 HMM 在分词中的应用,个人理解为这是从概率统计的思路,简化中文分词问题。在学习过程中,我们不能认为是无理由或随机选择了 HMM 实现对中文未登录词的处理,这很容易导致后续学习的思路混乱。更合理的,可能是因为中文文本的特性与HMM的三个假设恰好符合,因此,选择了 HMM 。也即,是否有其他方法解决呢?也有,比如 LSTM[1]。

通过了解历史,我们可以知道,最初的自然语言处理是基于语法规则的(这也是为什么最早的时候,我们使用翻译工具都十分不通顺,几乎是简单的词到词的映射),随着计算机性能的提升和相关技术发展,基于概率统计的系统性能更理想。但我们不能因为『实践证明基于手工规则的分词系统在评测中不敌基于统计学习的分词系统』,就认为基于语法规则的系统是毫无前途的。实际上,目前有许多文献资料都有提及,未来如果要实现更精确的自然语言处理,基于语法规则与基于概率统计的方法相结合是必然的趋势(虽然没有具体样例,但是个人有印象,有系统使用语法规则对基于概率统计的分析结果做进一步纠错的处理方式,有兴趣的同学可以做进一步了解)。同时,个人建议感兴趣的同学,了解基本语言发展历史和中文分词的发展历程,有助于加深关于分词系统或其它NLP系统的原理和发展趋势,具体可以参考文末[2-3]。

由此前文可以理解,目前我们对于这一技术不能认为分词工具和基于分词工具的系统可以达到百分百正确。因而,在实际应用中,有必要基于实际业务设计测试用例并限定最低Precision、Recall、F-score。令系统完成所有测试用例,且各评分达到相应阈值即可认为通过测试。


参考

  1. 基于LSTM网络的序列标注中文分词法[J]. 计算机应用研究, 2017(5).

  2. 郑捷. nlp汉语自然语言处理原理与实践[M]. 北京: 电子工业出版社, 2017.1

  3. 黄昌宁, 赵海. 中文分词十年回顾[J]. 中文信息学报, 2007, 21(3):8-19.

  4. https://www.zhihu.com/question/20136144

  5. https://github.com/fxsjy/jieba

  6. http://www.52nlp.cn/hmm

  7. https://www.cnblogs.com/zhbzz2007/p/6092313.html

  8. 数据是如何生成的 - https://github.com/fxsjy/jieba/issues/7


============ 一条不太完美的分割线 ===========


👇️

点击阅读原文,访问我们的博客,获取更多文章