viterbi算法

(仅作为个人的读书笔记)

**一、前言**

作者:岳席文
链接:https://www.zhihu.com/question/20136144/answer/154753703

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

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

**二、方法对比**

作者:UFO
链接:https://www.zhihu.com/question/20136144/answer/310197856

## 穷举法

我把所有可能路径都计算一遍,最优路径自然就出来了。

缺点:计算量太大。

## A*算法

我每一步只走最好走的路。

优点:计算快,而且这种贪心或者说启发式的算法,通常情况下,效果还是不错的。

缺点:“九阴白骨爪”比“九阴真经”速成,然而要是一开始就练“九阴白骨爪”,多半成不了绝顶高手(最优解)。

## beam search

我每一步只走最好走的前N条路。这里的N也叫Beam Width。

上图是一个Beam Width为2的Beam Search的剪枝示意图。每一层只保留2个最优的分支,其余分支都被剪掉了。

显然,Beam Width越大,找到最优解的概率越大,相应的计算复杂度也越大。因此,设置合适的Beam Width是一个工程中需要trade off的事情。

## Viterbi算法

这个动图可以助于理解,但是没法放了。那就下面这个图吧,也可以。

这里写图片描述

 

**三、举例理解**

作者:李大雷
链接:https://www.zhihu.com/question/20136144/answer/37291465

1.题目背景:

从前有个村儿,村里的人的身体情况只有两种可能:健康或者发烧。
假设这个村儿的人没有体温计或者百度这种神奇东西,他唯一判断他身体情况的途径就是到村头我的偶像金正月的小诊所询问。
月儿通过询问村民的感觉,判断她的病情,再假设村民只会回答正常、头晕或冷。
有一天村里奥巴驴就去月儿那去询问了。
第一天她告诉月儿她感觉正常。
第二天她告诉月儿感觉有点冷。
第三天她告诉月儿感觉有点头晕。
那么问题来了,月儿如何根据阿驴的描述的情况,推断出这三天中阿驴的一个身体状态呢?
为此月儿上百度搜 google ,一番狂搜,发现维特比算法正好能解决这个问题。月儿乐了。

2.已知情况:

隐含的身体状态 = { 健康 , 发烧 }

可观察的感觉状态 = { 正常 , 冷 , 头晕 }

月儿预判的阿驴身体状态的概率分布 = { 健康:0.6 , 发烧: 0.4 }

月儿认为的阿驴身体健康状态的转换概率分布 = {
健康->健康: 0.7 ,
健康->发烧: 0.3 ,
发烧->健康:0.4 ,
发烧->发烧: 0.6
}

月儿认为的在相应健康状况条件下,阿驴的感觉的概率分布 = {
健康,正常:0.5 ,冷 :0.4 ,头晕: 0.1 ;
发烧,正常:0.1 ,冷 :0.3 ,头晕: 0.6
}
阿驴连续三天的身体感觉依次是: 正常、冷、头晕 。

3.题目:

已知如上,求:阿驴这三天的身体健康状态变化的过程是怎么样的?

4.过程:

根据 Viterbi 理论,后一天的状态会依赖前一天的状态和当前的可观察的状态。那么只要根据第一天的正常状态依次推算找出到达第三天头晕状态的最大的概率,就可以知道这三天的身体变化情况。
 

1.初始情况:

  • P(健康) = 0.6,P(发烧)=0.4。

2.求第一天的身体情况:
计算在阿驴感觉正常的情况下最可能的身体状态。

  • P(今天健康) = P(正常|健康)*P(健康|初始情况) = 0.5 * 0.6 = 0.3
  • P(今天发烧) = P(正常|发烧)*P(发烧|初始情况) = 0.1 * 0.4 = 0.04

那么就可以认为第一天最可能的身体状态是:健康
3.求第二天的身体状况:
计算在阿驴感觉冷的情况下最可能的身体状态。
那么第二天有四种情况,由于第一天的发烧或者健康转换到第二天的发烧或者健康。

  • P(前一天发烧,今天发烧) = P(前一天发烧)*P(发烧->发烧)*P(冷|发烧) = 0.04 * 0.6 * 0.3 = 0.0072
  • P(前一天发烧,今天健康) = P(前一天发烧)*P(发烧->健康)*P(冷|健康) = 0.04 * 0.4 * 0.4 = 0.0064
  • P(前一天健康,今天健康) = P(前一天健康)*P(健康->健康)*P(冷|健康) = 0.3 * 0.7 * 0.4 = 0.084
  • P(前一天健康,今天发烧) = P(前一天健康)*P(健康->发烧)*P(冷|发烧) = 0.3 * 0.3 *0.3 = 0.027

那么可以认为,第二天最可能的状态是:健康
4.求第三天的身体状态:
计算在阿驴感觉头晕的情况下最可能的身体状态。

  • P(前一天发烧,今天发烧) = P(前一天发烧)*P(发烧->发烧)*P(头晕|发烧) = 0.027 * 0.6 * 0.6 = 0.00972
  • P(前一天发烧,今天健康) = P(前一天发烧)*P(发烧->健康)*P(头晕|健康) = 0.027 * 0.4 * 0.1 = 0.00108
  • P(前一天健康,今天健康) = P(前一天健康)*P(健康->健康)*P(头晕|健康) = 0.084 * 0.7 * 0.1 = 0.00588
  • P(前一天健康,今天发烧) = P(前一天健康)*P(健康->发烧)*P(头晕|发烧) = 0.084 * 0.3 *0.6 = 0.01512

那么可以认为:第三天最可能的状态是发烧

反推出结论:发烧(第三天)——>健康(第二天)——>健康(第一天)

看到网上有人说,有正向、逆向结果不一样的。我想那应该是以逆向的为准吧,因为是找整个的最优解,而不是局部的。

5.结论

根据如上计算。这样月儿断定,阿驴这三天身体变化的序列是:健康->健康->发烧。

**四、有关代码**

此例子也挺好理解,特别是关键代码注释部分

https://blog.csdn.net/zhangduan8785/article/details/80443650

def viterbi(obs, states, start_p, trans_p, emit_p):

  result_m = [{}] # 存放结果,每一个元素是一个字典,每一个字典的形式是 state:(p,pre_state)
                  # 其中state,p分别是当前状态下的概率值,pre_state表示该值由上一次的那个状态计算得到
  for s in states:  # 对于每一个状态
    result_m[0][s] = (E(start_p[s]*emit_p[s][obs[0]]),None) # 把第一个观测节点对应的各状态值计算出来

  for t in range(1,len(obs)):
    result_m.append({})  # 准备t时刻的结果存放字典,形式同上

    for s in states: # 对于每一个t时刻状态s,获取t-1时刻每个状态s0的p,结合由s0转化为s的转移概率和s状态至obs的发散概率
                     # 计算t时刻s状态的最大概率,并记录该概率的来源状态s0
                     # max()内部比较的是一个tuple:(p,s0),max比较tuple内的第一个元素值
      result_m[t][s] = max([(E(result_m[t-1][s0][0]*trans_p[s0][s]*emit_p[s][obs[t]]),s0) for s0 in states])
  return result_m    # 所有结果(包括最佳路径)都在这里,但直观的最佳路径还需要依此结果单独生成,在显示的时候生成

1.代码部分我自己的理解

维特比算法是将客体的一种描述转换成另一种描述,如这里所谓的“观测序列”(‘正常’,‘发冷’,‘发晕’)转换成这里所谓的“隐藏序列”(‘健康’,‘生病’)。为了方便理解,以上例子取一种说法,将“观测序列”说成“感官描述”,将“隐藏序列”说成“健康状态”

(1)第一个感官描述转换(为什么要单独拿出来,当然是由于没有健康状态的转移啊)

将第一个感官描述的各种可能的健康状态都列举出来。

没有健康状态转移,那就各个健康状态的影响因素就只有当前健康状态的占比和当前健康状态下此感官描述的占比(就是感受)

for s in states:  # 对于每一个状态
    result_m[0][s] = (E(start_p[s]*emit_p[s][obs[0]]),None) # 把第一个观测节点对应的各状态值计算出来

(2)后面的感官描述转换

对于每一个感官描述而言,当然也需要各种可能的健康状态都列举出来呀。

for t in range(1,len(obs)):
    result_m.append({})  # 准备t时刻的结果存放字典,形式同上

    for s in states:

由于不是第一个感官描述,列举的每一种健康状态会受到前一健康状态的影响。影响因素有:前一健康状态概率前一健康状态转移到当前健康状态的几率当前健康状态下当前感官描述的几率。有点晕了~_~

看着主要公式之有一个,很好理解。

result_m[t][s] = max([(E(result_m[t-1][s0][0]*trans_p[s0][s]*emit_p[s][obs[t]]),s0) for s0 in states])

当前感官描述下当前健康状态的几率(P) = max(前一健康状态概率 × 前一健康状态转移到当前健康状态的几率 × 当前健康状态下当前感官描述的几率)

(前一健康状态包括所有的健康状态,体现在for循环处)

此处可能有点绕,但确实是这么回事。代入实例,

今天发晕推出今天生病=max(昨天生病的概率×昨天生病且今天生病的概率×生病感觉发晕的概率,

                                                      昨天健康的概率×昨天健康且今天生病的概率×生病感觉发晕的概率)

今天发晕推出今天健康=max(昨天生病的概率×昨天生病且今天健康的概率×健康感觉发晕的概率,

                                                      昨天健康的概率×昨天健康且今天健康的概率×健康感觉发晕的概率)

简而言之,每个感官描述要去找是每个健康状态的概率,而每个健康状态要去前一时刻的所有健康状态中找到能使其最大的健康状态。

2.参数部分的分析

之前看到自然语言处理——分词的部分,使用HMM(viterbi)的过程理解的不是很透。(所以训练出来的参数到底是什么???)经过以上通过症状描述转换成健康状态的例子,照着葫芦画瓢。下面用‘有意见分歧’——>'BEMS'(‘观测序列’——>‘隐藏序列')为例。

观测序列:obs,隐藏序列:status,起始概率:start_p,转移概率:trans_p,发射概率:emit_p

(0)目标

‘有意见分歧’——>'BEMS'模式

(1)观测序列(原模式)

obs=('有','意','见','分','歧')

(2)隐藏序列(目标模式)

status=('B','E','M','S')

'B':词的开头 ,'E':词的结尾 ,'M':词的中间 ,'S':单个字的词  

(3)起始概率(隐藏序列,也即目标模式中各个状态的占比。如“健康”,“生病”各占比。)

start_p={'B':    ,'E':   ,'M':   ,'S':   }

(4)转移概率(隐藏序列,也即目标模式中各个状态相互转换的比率。如“健康”——"生病“的概率。)

其中可能的转移:B->M,B->E ; E->B,E->S ; M->M,M->E ; S->B,S->S

trans_p={'B':{'E':   ,'M':   },

                 'E':{'B':   ,'S':   },

                 'M':{'M':   ,'E':   },

                 'S':{'B':   ,'S':    } 

}

(5)发射概率(对应在”健康判断“例子中,可以说是隐藏序列中各隐藏序列元对应的各观测序列元的概率,也即目标模式中各模式对应的各原模式的概率。如”健康”状态对应:”正常“,”发晕“,”冷“的概率。此处延伸到分词时,应该是目标模式中各模式对应的所有可能的观测序列元的概率。换句话讲,就是'BEMS'模式中'B','E','M','S'分别对应的各种可能的中文字的概率。因为只有这样,才能在某状态的时候将该状态对应的某个字识别出来,发射出去。这一点体现在代码的emit_p[s][obs[t]]处。)

result_m[t][s] = max([(E(result_m[t-1][s0][0]*trans_p[s0][s]*emit_p[s][obs[t]]),s0) for s0 in states])

emit_p={'B':{'有':   ,'意':   ,'见':   ,'分':   ,'歧':   ...},

               'E':{'有':   ,'意':   ,'见':   ,'分':   ,'歧':   ...},

               'M':{'有':   ,'意':   ,'见':   ,'分':   ,'歧':   ...},

               'S':{'有':   ,'意':   ,'见':   ,'分':   ,'歧':   ...} 

}

如有错误欢迎指正 ^_^

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值