马尔可夫假设:
有限历史假设:P(Xt+1=sk|X1,...,Xt) = P(Xt+1=sk|Xt)
时间不变假设:∀i∈{1, 2, ..., T}, ∀x, y∈S, P(xi=y|xi-1 = x) = P(y|x)
马尔可夫模型形式化表述:
MM: λ=(S, π, A)
- S: 状态集合
- π: 初始状态概率
- A: 状态间转移概率
隐马尔可夫模型形式化表述:
HMM: λ=(S, O, π, A, B)
- S: 状态集合
- O: 每个状态可能的输出集合
- π: 初始状态概率
- A: 状态间转移概率
- B: 观察值概率矩阵
HMM可分为两部分:
- Markov链:由π,A描述,输出为状态序列
- 随机过程:由B描述,产生输出为观测序列
HMM三个问题:
- 评估。给定模型 λ=(S, O, π, A, B),如何高效计算某一输出字符序列的概率P(O|λ)?
答案:向前算法,向后算法
- 解码。给定输出序列O和一个模型λ,如何确定产生这一序列概率的最大状态序列(q1, q2, ..., qT)?
答案:Viterbi算法
- 参数估计。给定一个观察序列O,如何调整模型λ参数,使得产生O的概率最大?
答案:Baum-Welch统计方法,EM算法(抽空整理一下EM算法)
评估问题和解码问题都可用动态规划解决,可以参考我博客中的Dynamic Time Warping这篇文章中动态规划算法,找到O的最大概率和找到最佳状态转移可以合并为一个问题。通过正向计算最大概率,然后通过回溯解决最佳状态转移。【这种方法只能找到一条最佳的状态转移】
但是在实际情况中,情况比较复杂,HMM的状态转移路径可能会涉及多条概率相同的路径,这样基于上一篇文章中提到的算法就不太容易处理回溯中遇到多条路径的问题了。
Viterbi算法
Viterbi算法也是动态规划,在解码问题中的DP公式为:
DP递推公式:δt(j) = max[δt-1(i) × aij × bj(ot)]
- δt(j):t 时刻沿一条状态路径q1, q2, ..., qt,且qt=j,即 t 时刻处于状态j,产生o1, o2, ..., ot的最大概率
- aij:从状态i到状态j的转移概率
- bj(ot):状态 j 输出ot的概率
Viterbi练习题:杭电OJ:Peter's Hobby
题目描述:根据输入的观测序列,找到最大可能的状态序列。这题动态规划和Viterbi都可以做。
算法思路:
- 找到初始状态选择概率;
- 递归寻找最优子结构,从前向后找到最佳的状态转移路径;每次递归仅保留最大概率的路径,同时回溯剪掉小概率路径;
- 输出状态路径
Python Viterbi实现:
from state import State
def transition_probability(prb, emission, last_state, current_state, trans_prb_table, emission_prb_table):
"""
当前状态概率 = 上一次最优概率 * 状态转移概率 * 序列发射概率
:param prb: 上一次最优概率
:param last_state: 上一次状态
:param emission: 当前时刻发射概率
:param current_state: 当前状态
:param trans_prb_table: 状态转移概率表
:param emission_prb_table: 状态发射字符概率
:return:
"""
if last_state is None:
return emission_prb_table[current_state][emission]
return prb * trans_prb_table[last_state][current_state] * emission_prb_table[current_state][emission]
def reverse(state_node):
"""
回溯删除不可行路径,将next—state从父节点的next列表中移除
:return:
"""
previous_node = state_node.previous
if previous_node is None:
# 如果到起始点则返回
return
previous_node.next.remove(state_node)
# 如果该状态没有子状态,则说明该状态可以从路径中移除,递归删除
if len(previous_node.next) == 0:
reverse(previous_node)
def search_state(last_state, order, transition_prb_table, emission_prb_table, days):
"""
搜索HMM状态网格,由于可能会存在多条概率相同的路径,因此要用列表存储
:param last_state: 上一次的状态列表
:param order: 时间序列索引
:param transition_prb_table:状态转移概率表
:param emission_prb_table: 状态发射字符概率表
:param days: 观测序列
:return:
"""
if order == len(days):
return last_state
humidity = days[order]
# 记录当前路径中最大的概率,用于回溯修剪路径
current_path_maximum_prb = -1
# 从前向后搜索最佳的状态结点
for state in last_state:
maximum_prb = -1
next_state = []
for current_state in transition_prb_table.keys():
# 判断转移到哪个状态输出目标天气的概率最大
prb = transition_probability(state.prb, humidity, state.state, current_state, transition_prb_table,
emission_prb_table)
if prb > maximum_prb:
maximum_prb = prb
next_state.clear()
next_state.append(State(current_state, maximum_prb, state))
elif prb == maximum_prb:
next_state.append(State(current_state, maximum_prb, state))
state.next += next_state
if maximum_prb > current_path_maximum_prb:
current_path_maximum_prb = maximum_prb
# 记录当前状态,用于下一次递归
current_states = []
# 保留概率高的子路径
for state in last_state:
for node in state.next:
if node.prb < current_path_maximum_prb:
state.next.remove(node)
else:
current_states.append(node)
# 如果当前状态没有向外继续转移,则递归删除路径
if len(state.next) == 0:
reverse(state)
# 递归寻找转移状态
return search_state(current_states, order + 1, transition_prb_table, emission_prb_table, days)
def init_data():
"""
初始化数据
:return:
"""
trans_prb_table = {'sunny': {'sunny': 0.5, 'cloudy': 0.375, 'rainy': 0.125},
'rainy': {'sunny': 0.25, 'cloudy': 0.375, 'rainy': 0.375},
'cloudy': {'sunny': 0.25, 'cloudy': 0.125, 'rainy': 0.625}}
emission_prb_table = {'sunny': {'dry': 0.6, 'dryish': 0.2, 'damp': 0.15, 'soggy': 0.05},
'rainy': {'dry': 0.05, 'dryish': 0.1, 'damp': 0.35, 'soggy': 0.5},
'cloudy': {'dry': 0.25, 'dryish': 0.3, 'damp': 0.2, 'soggy': 0.25}}
return trans_prb_table, emission_prb_table
def backwards(state_node):
"""
采用栈模拟,递归输出状态序列
:param state_node: 状态节点
:return:
"""
stack = [state_node.state]
# 如果当前状态节点的前趋节点为起始节点,则递归返回
if state_node.previous.prb == -1:
return [state_node.state]
stack += backwards(state_node.previous)
return stack
if __name__ == '__main__':
transition_prb_table, emission_prb_table = init_data()
days = ['dry', 'damp', 'soggy']
# 记录路径起点
start_point = State('start', -1, None)
# 初始化初始状态
maximum = -1
states = []
for state in transition_prb_table.keys():
prb = transition_probability(1, days[0], None, state, transition_prb_table, emission_prb_table)
if prb > maximum:
states.clear()
states.append(State(state, prb, start_point))
maximum = prb
elif prb == maximum:
states.append(State(state, prb, start_point))
start_point.next += states
end_states = search_state(states, 1, transition_prb_table, emission_prb_table, days)
# 采用栈模拟,输出多条概率最大的状态路径
for state in end_states:
state_seq = backwards(state)
# 状态出栈
while len(state_seq) != 0:
print(state_seq.pop())
Python代码是C++代码转过来的,没有改题目的输入要求,以上代码仅仅提供一个思路。以上代码考虑了存在多于一条路径的情况,因此比单一路径的代码(比如)要复杂一些。由于路径采用链表存储,同时也多了一步回溯剪枝。
源码