第12天:NLP补充——HMM(隐马尔科夫模型)

时间:2022-07-24
本文章向大家介绍第12天:NLP补充——HMM(隐马尔科夫模型),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

马尔科夫链

  隐马尔科夫模型是自然语言处理中很重要的一种算法。在此之前,我们首先给大家介绍马尔科夫链。马尔科夫链,因安德烈·马尔科夫得名,是指数学中具有马尔科夫性质的离散事件的随机过程。在给定当前知识或信息的情况下,过去对于预测未来是无关的。每个状态的转移只依赖于之前的N个状态,这个过程被称为1个n阶的模型。其中n是影响转移状态的数目。最简单的马尔科夫过程就是一阶过程,每一个状态的转移只依赖于其之前的哪一个状态。用数学的表达式表示就是如下所示:

  假设天气服从马尔科夫链:

  转移矩阵

  那么,从今天开始,在遥远的未来的某天,晴阴的概率分布是什么?

  其实,不管是今天是晴天,还是阴天,其实很多天之后的晴阴分布收敛到一个固定分布。这个固定分布就是我们熟悉的稳态分布。因此,我们就为上面的一阶马尔科夫过程定义一以下三个部分:   1、状态:晴天、阴天   2、初始向量:定义系统在时间为0的时候的状态概率   3、状态转移矩阵:每种天气转换的概率。所有的能被这样描述的系统就是一个马尔科夫过程。   但是以上出现一个明显的缺陷,也就是前后关系的缺失,带来了信息的缺失:例如我们经常玩的股市,如果只是观测市场,我们只能知道当天的价格,成交量等信息,但是并不知道当前的股市是处于什么状态。在这种情况下我们有两个状态集合,一个是可以观察到的状态集合;另一个是一个隐藏的状态集合。我们希望能够找到一个算法可以根据股市价格的成交量状况和马尔科夫假设来预测股市的状况。基于上面我们提到的情况下,我们可以观察到状态序列和隐藏的状态序列是概率相关的。因此,我们可以将这种类型的过程建模为一个有隐藏的马尔科夫过程和一个与这个隐马尔科夫过程概率相关的并且可以观察到的状态集合,这就是我们今天要介绍的隐马尔科夫模型(HMM)。接下来为大家详细介绍隐马尔科夫模型。

隐马尔科夫模型

  隐马尔科夫模型是一种统计模型,用来描述一个含有隐含层未知参数的马尔科夫过程。但是其中最大的难点就是从可观察的参数中确定该过程的隐含参数,然后利用这些参数来作进一步分析。我们以日常的投骰子为例来解释隐马尔科夫模型:假设有三个不同的骰子(6面、4面、8面),每次先从三个骰子里面选择一个,每个骰子选中的概率为1/3,如下图所示,重复上述过程,得到一串数值[1,6,3,5,2,7]。这些可观测变量组成可观测状态链。同时,在隐马尔可夫模型中还有一条由隐变量组成的隐含状态链,在本例中即骰子的序列。比如得到这串数字骰子的序列可能为[D6, D8, D8, D6, D4, D8]。具体如下:

隐马尔可夫型示意图如下所示:

隐含状态转换如下图所示:

  接下来给大家介绍隐马尔科夫模型的三大问题:我们应用吴恩达教授中ppt来阐述这三大问题:

  上述中我们给出这三大问题的定义:隐马尔科夫模型是关于时序的概率模型,描述由一个隐藏的马尔可夫链随机生成不可观测的状态随机序列,再由各个状态生成一个可观测的随机序列的过程,隐藏的马尔可夫链随机生成的状态序列,称为状态序列。每个状态生成一个观测,而由此产生的观测随机序列,称为观测序列;序列的每个位置又可以看作是一个时刻。隐马尔可夫模型由初始的概率分布、状态转移概率分布以及观测概率分布确定。隐马尔可夫模型由初始状态概率向量C,状态转移概率矩阵A和观测概率矩阵B决定,C和A决定状态序列,B决定观测序列。   如果用我们上述的骰子来解释这三大问题的话即为:如果我们知道骰子有几种状态(隐含状态的数量),每种骰子的是什么(转换概率),根据骰子掷出的结果(可见状态链),我们想知道的是每次掷出的骰子都是哪种(隐含状态链)。当然我们也可以知道的是骰子有几种状态(隐含状态的数量)以及每次掷出的骰子都是哪种(隐含状态链),我们需要知道的是掷出这个结果的概率。最后一种就是我们知道骰子有几种状态(隐含状态的数量),但是不知道每种骰子的是什么(转换概率),观测到很多次掷骰子的结果(可见状态链),我们要反推出每种骰子是什么(转换概率)。这就是隐马尔科夫的三大问题,我们可以知二求一。   针对上面提到的三大问题,我们有对应的解法。针对概率计算问题我们用到的是前向后向算法。针对学习问题,我们用到的是维特比算法。针对最后的预测问题,我们用到的是Baum-Welch Algo算法。接下来我们分别介绍这几种方法。 1、概率计算问题——前向后向算法   在此之前我们应该首先给大家介绍遍历算法:

1、前向算法:

2、后向算法:

2、学习问题——Viterbi算法

3、预测问题——Baum-Welch算法

隐马尔科夫链的应用:词性标注

  接下来我们用HMM来进行词性标注。这里我们用NLTK自带的Brown词库进行学习。这里我们需要在进行试验之前首先安装nltk库,具体安装的过程请查看我的这篇博客。我们实验所用的环境就是我们之前在文章中提到的。接下来硬核程序员申请出战!!!!!三二一上代码: 1、我们导入需要的库

import nltk
import sys
from nltk.corpus import brown

2、进行预处理

  这里需要做的预处理是:给词们加上开始和结束符号。Brown里面的句子都是自己标注好了的,长这个样子:(I , NOUN), (LOVE, VERB), (YOU, NOUN)。那么,我们的开始符号也得跟他的格式符合,我们用:(START, START) (END, END) 来表示。

brown_tags_words = []
for sent in brown.tagged_sents():
    brown_tags_words.append(("START","START"))
    brown_tags_words.extend([(tag[:2], word) for (word, tag) in sent ])
    brown_tags_words.append(("END", "END")) 

3、词统计   接下来我们要把我们所有的词库中拥有的单词与tag之间的关系,做个大致的统计。也就是我们之前说过的:

P(wi | ti) = count(wi, ti) / count(ti)

  当然大家也可以自己一个个的loop全部的corpus,我们这里NLTK给了我们做统计的工具:

cfd_tagwords = nltk.ConditionalFreqDist(brown_tags_words)
cpd_tagwords = nltk.ConditionalProbDist(cfd_tagwords, nltk.MLEProbDist) 
print("The probability of an adjective (JJ) being 'new' is", cpd_tagwords["JJ"].prob("new"))
print("The probability of a verb (VB) being 'duck' is", cpd_tagwords["VB"].prob("duck"))

  接着进行第二个公式的计算,尽管这个公式与words并没有联系,因为它是属于隐层的马尔科夫链。因此我们选取所有的tag来。

P(ti | t{i-1}) = count(t{i-1}, ti) / count(t{i-1})

brown_tags = [tag for(tag, word) in brown_tags_words]
cfd_tags = nltk.ConditionalFreqDist(nltk.bigrams(brown_tags))
cpd_tags = nltk.ConditionalProbDist(cfd_tags, nltk.MLEProbDist)
print("If we have just seen 'DT', the probability of 'NN' is", cpd_tags["DT"].prob("NN"))
print( "If we have just seen 'VB', the probability of 'JJ' is", cpd_tags["VB"].prob("DT"))
print( "If we have just seen 'VB', the probability of 'NN' is", cpd_tags["VB"].prob("NN"))

  通过上述的代码我们发现了一些有意思的事情,那就是比如, 一句话,“I want to you”, 一套tag,“PP VB TO VB”。他们之间的匹配度有多高呢?其实就是:

P(START) * P(PP|START) * P(I | PP) * P(VB | PP) * P(want | VB) * P(TO | VB) * P(to | TO) * P(VB | TO) * P(race | VB) * P(END | VB)

prob_tagsequence = cpd_tags["START"].prob("PP") * cpd_tagwords["PP"].prob("I") * 
    cpd_tags["PP"].prob("VB") * cpd_tagwords["VB"].prob("want") * 
    cpd_tags["VB"].prob("TO") * cpd_tagwords["TO"].prob("to") * 
    cpd_tags["TO"].prob("VB") * cpd_tagwords["VB"].prob("race") * 
    cpd_tags["VB"].prob("END")
print( "The probability of the tag sequence 'START PP VB TO VB END' for 'I want to race' is:", prob_tagsequence)

4、Viterbi 的实现   首先,我们拿出所有独特的tags(也就是tags的全集),然后我们输入一个句子,接下来,开始维特比:从1循环到句子的总长N,记为i,每次都找出以tag X为最终节点,长度为i的tag链。另外,我们还需要一个回溯器:从1循环到句子的总长N,记为i。把所有tag X 前一个Tag记下来。

distinct_tags = set(brown_tags)
sentence = ["I", "want", "to", "you"]
strlen = len(sentence)
viterbi = []
backpointer = []
first_viterbi = {}
first_backpointer = {}
for tag in distinct_tags:
    if tag == "START":
        continue
    first_viterbi[tag] = cpd_tags["START"].prob(tag) * cpd_tagwords[tag].prob(sentence[0])
    first_backpointer[tag] = "START"
print(first_viterbi)
print(first_backpointer)

  以上,就是所有的第一个viterbi 和第一个回溯点。接下来,把上面的这些都存到Vitterbi和Backpointer两个变量里去,然后我们输出做好的tag。

viterbi.append(first_viterbi)
backpointer.append(first_backpointer)
currbest = max(first_viterbi.keys(), key= lambda tag: first_viterbi[tag])
print( "Word", "'" + sentence[0] + "'", "current best two-tag sequence:", first_backpointer[ currbest], currbest)

  接着我们进行loop

for wordindex in range(1, len(sentence)):
    this_viterbi = { }
    this_backpointer = { }
    prev_viterbi = viterbi[-1]
    
    for tag in distinct_tags:
        if tag == "START": continue
        best_previous = max(prev_viterbi.keys(),
                            key = lambda prevtag: 
            prev_viterbi[ prevtag ] * cpd_tags[prevtag].prob(tag) * cpd_tagwords[tag].prob(sentence[wordindex]))
        this_viterbi[ tag ] = prev_viterbi[ best_previous] * 
            cpd_tags[ best_previous ].prob(tag) * cpd_tagwords[ tag].prob(sentence[wordindex])
        this_backpointer[ tag ] = best_previous
    currbest = max(this_viterbi.keys(), key = lambda tag: this_viterbi[ tag ])
    print( "Word", "'" + sentence[ wordindex] + "'", "current best two-tag sequence:", this_backpointer[ currbest], currbest)
    viterbi.append(this_viterbi)
    backpointer.append(this_backpointer)

  最后就是找结束——END:回溯所有的回溯点,此时的最好的tag就是backpointer里面的current best,最后看我们训练的结果。

prev_viterbi = viterbi[-1]
best_previous = max(prev_viterbi.keys(),
                    key = lambda prevtag: prev_viterbi[ prevtag ] * cpd_tags[prevtag].prob("END"))
prob_tagsequence = prev_viterbi[ best_previous ] * cpd_tags[ best_previous].prob("END")
best_tagsequence = [ "END", best_previous ]
backpointer.reverse()
current_best_tag = best_previous
for bp in backpointer:
    best_tagsequence.append(bp[current_best_tag])
    current_best_tag = bp[current_best_tag]
best_tagsequence.reverse()
print("The sentence was:", end = " ")
for w in sentence:
    print(w, end = " ")
print("n")
print( "The best tag sequence is:", end = " ")
for t in best_tagsequence: print (t, end = " ")
print("n")
print( "The probability of the best tag sequence is:", prob_tagsequence)

总结

  本文详细介绍了在自然语言处理中最为常用的一种模型——隐马尔科夫模型,并且本文开头给大家介绍了马尔科夫链,并且讲解了马尔科夫链存在的问题,由此引出了隐马尔科夫模型,在HMM中,本文详细给大家讲解了其中的三大问题以及解决的方法,最后通过一个词性标注来将其应用,让读者更加对隐马尔科夫模型中的三大问题的求解过程有一个深刻的了解。此代码可以运行,大家装好环境,建议大家有时间就跑一遍,看看结果,对隐马尔科夫模型有进一步的了解。如果大家还是不明白建议看看此文章以及相应的维特比算法,讲的很详细,对大家的理解肯定有所帮助。