隐马尔科夫模型(HMM)是一个生成模型,求得是联合概率 p ( x , y ) = p ( x ∣ y ) p ( y ) p(x,y)=p(x|y)p(y) p(x,y)=p(x∣y)p(y),然后利用贝叶斯定理来求解后验概率 p ( y ∣ x ) p(y|x) p(y∣x):
p ( y ∣ x ) = p ( x ∣ y ) p ( y ) ∑ y ′ p ( x ∣ y ′ ) p ( y ′ ) p(y|x)=\frac{p(x|y)p(y)}{\sum_{y^{\prime}}p(x|y^{\prime})p(y^{\prime})} p(y∣x)=∑y′p(x∣y′)p(y′)p(x∣y)p(y)
它有三个基本的问题,给定一个观察序列 O = O 1 , O 2 , O 3 . . . O T O = O_{1},O_{2},O_{3}...O_{T} O=O1,O2,O3...OT和模型 μ = ( A , B , π ) \mu=(A,B,\pi) μ=(A,B,π),求:
HMM常用于解决序列标注问题,下面再说一下序列标注问题
序列标注问题的重点在于学习序列位置之间的关系,然后解码出最大概率标签路径。比如有K个可选标签,当输入序列长度为m时,那么就有 K m K^{m} Km条概率路径,序列标注问题是要从 K M K^{M} KM条概率路径中寻找到概率最大的那条路径。
NLP中常见的任务,如分词
,词性标注
,命名实体识别
都属于序列标注问题。
问题描述:小明每天会在朋友圈发一次自己当天的美食照片(但不发定位),如:
给定小明的5天朋友圈
Day | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
Food | 烤鸭 | 米粉 | 饭 | 烤鸭 | 饭 |
Place (BJ/YN) |
? | ? | ? | ? | ? |
根据小明朋友圈,猜小明当天在哪儿?
由HMM的基本知识我们知道,要计算最大路径得分需要知道状态样本有哪些,观测样本有哪些,状态转移概率矩阵A以及发射概率矩阵B,下面一一分析:
BJ
和YN
烤鸭
、米粉
和饭
# 状态的样本空间
state_set = ('BJ', 'YN') # 北京,云南
# 观测的样本空间
observation_set = ('烤鸭', '米粉', '饭') # 每天吃的东西
# 起始的状态概率
start_probability = {'BJ': 0.6, 'YN': 0.4} # 初始小明在北京或者在云南的概率
# 状态转移概率矩阵A:(可以画一个2×2的矩阵)
transition_probability = {
'BJ': {'BJ': 0.3, 'YN': 0.7}, # 今天在北京时,明天留在北京和去云南的概率
'YN': {'YN': 0.2, 'BJ': 0.8}, # 今天在云南时,明天去在北京和留云南的概率
}
# 状态->观测的发射概率矩阵B:(可以画一个2×3的矩阵)
emission_probability = {
'BJ': {'烤鸭': 0.5, '米粉': 0.2, '饭': 0.3}, # 在北京时,吃各种食物的概率
'YN': {'烤鸭': 0.1, '米粉': 0.5, '饭': 0.4}, # 在云南时,吃各种食物的概率
}
# 观测序列
# observation_seq = ["烤鸭", "米粉", "饭", "烤鸭", "饭"] # 把朋友圈5天动态存成列表形式
# observation_seq = ["烤鸭", "米粉", "饭", "烤鸭", "饭"] * 4 # 列表里的内容重复4遍
思路:
时间复杂度:O( m n m^n mn)
解释:对于n天里的任何一天,小明呆的地方都有m种可能。
def _all_sequence(unique_val_set, repeat):
"""
形参params:
unique_val_set: 一次实验,所有可能状态的集合(本实验只有"BJ","YN"两种可能)
repeat: 实验次数(本实验是指发朋友圈的天数)
实参return:
paths:一个列表,存储的每个元素是一个路径,列表长度是所有可能路径个数
exmaple:
if:
unique_val_set = ("A", "B")
repeat = 2
then:
paths = [["A", "A"],
["A", "B"],
["B", "A"],
["B", "B"]]
"""
# 用的是迭代法(iterative method),每次往上一轮的每个结果里加当前的数字nums[i]
paths = [] # 刚开始是一次实验也没做的结果
for i in range(repeat): # 从第1次做到第repeat次实验
last_paths = paths # 先把上次实验的结果赋值给last_paths
paths = [] # reset一下本轮实验结果的路径列表
if len(last_paths) == 0: # 第一次实验
for val in unique_val_set: # 只有两种可能,要么是'A',要么是'B'
a_path = [val] # 把[A]添加到a_path里
paths.append(a_path)
else: # 不是第一次实验,假设last_paths = [A,B]
for a_last_path in last_paths: # 取出上次实验后的每条路径last_paths=[A] or [B]
for val in unique_val_set: # 再把unique_val_set里每种可能[A]or[B]加到上一次实验的每个结果里
a_path = a_last_path + [val] # 例如val=[A],把[A]加到[A]里,把[A]加到[B]里; 又当val=[B],把[B]加到[A]里,把[B]加到[B]里;
paths.append(a_path) # paths = [[A,A],[B,A],[A,B],[B,B]]
del last_paths # 删除上次实验的结果
return paths
# 测试
paths = _all_sequence(("A", "B"), repeat = 3) # 所有可能的状态集合为"A"和"B"两种可能,一共发了3天的朋友圈
for path in paths:
print(path)
得到所有可能的路径后,计算每条路径的概率(得分),取最大值即可
如何计算路径得分?
e.g:
由观测状态`observation_seq = ["烤鸭", "米粉", "饭", "烤鸭", "饭"]`和所有路径`path = ["BJ", "BJ", "BJ", "BJ", "BJ"]`
可知得分为:P(第一天在北京) * P(在北京吃烤鸭) × P(从北京到北京) * P(在北京吃米粉) × P(从北京到北京) * P(在北京吃饭)...
即得分socre = P(day1 in "BJ") * P("BJ" eat "烤鸭") × P(day2 in "BJ" | day1 in "BJ") * P("BJ" eat "米粉") ... × P(day5 in "BJ" | day4 in "BJ") * P("BJ" eat "饭")
其中:
def guess_places(observation_seq, state_set, start_probability, transition_probability,
emission_probability):
"""
params:
observation_seq:观测序列。||一个列表,列表元素是朋友圈的一个状态 ["烤鸭", "米粉", "饭", "烤鸭", "饭"]
state_set:状态的样本空间。|| 朋友圈的所有可能的状态 ["BJ", "YN"]
start_probability:起始的状态概率。
transition_probability:状态转移概率
emission_probability:发射概率
return:
(paths, scores): 两个列表,对应所有可能的状态及其得分
example:
paths = [["A", "A"],
["A", "B"],
["B", "A"],
["B", "B"]]
scores = [0.5, 0.2, 0.1, 0.2]
解释:0.5对应["A", "A"],0.2对应["A", "B"],0.1对应["B", "A"],0.2对应["B", "B"]
"""
how_many_days = len(observation_seq) # 一共几天
paths = _all_sequence(state_set, how_many_days) # unique_set=state_set,repeat=days,得到所有可能的路径,从中找出概率最大的
# 计算每条路径得分,分为两步: 1. 状态转移概率的得分; 2.得到observation_seq的发射概率得分
scores = [] # 用于存放每条路径的得分
for path in paths: # 遍历2^{5}=32条路径里的每条路径
for i, place in enumerate(path): # i是下标,place=BJ or YN
if i == 0: # 如果是第一天的话要用起始概率矩阵而不用状态转移概率矩阵
score = start_probability[place] * \
emission_probability[place][observation_seq[i]] # P(第一天在北京)*P(在北京吃烤鸭)
else: # 从第二天开始
pre_place = path[i-1] # 得到前一天的地点
score = score * transition_probability[pre_place][place] * \
emission_probability[place][observation_seq[i]] # 得分=历史得分*状态转移得分*发射得分
# transition_probability[pre_place][place]: 按字典的键值对方法取值的,不是列表
scores.append(score)
return (paths, scores) # 返回"所有路径和所有得分"元祖,到时候用zip一组一组地取
import time
# 状态的样本空间
state_set = ('BJ', 'YN') # 北京,云南
# 观测的样本空间
observation_set = ('烤鸭', '米粉', '饭') # 每天吃的东西
# 起始的状态概率
start_probability = {'BJ': 0.6, 'YN': 0.4} # 小明 一般在北京或者在云南
# 状态转移概率
transition_probability = {
'BJ': {'BJ': 0.3, 'YN': 0.7}, # 今天在北京时,明天留在北京和去云南的概率
'YN': {'YN': 0.2, 'BJ': 0.8}, # 今天在云南时,明天去在北京和留云南的概率
}
# 状态->观测的发散概率
emission_probability = {
'BJ': {'烤鸭': 0.5, '米粉': 0.2, '饭': 0.3}, # 在北京时,吃各种食物的概率
'YN': {'烤鸭': 0.1, '米粉': 0.5, '饭': 0.4}, # 在云南时,吃各种食物的概率
}
observation_seq = ["烤鸭", "米粉", "饭", "烤鸭", "饭"] # 朋友圈5天动态
# observation_seq = ["烤鸭", "米粉", "饭", "烤鸭", "饭"]*2
start_time = time.time() # 开始时间
paths, scores = guess_places(observation_seq, state_set, start_probability,
transition_probability, emission_probability)
over_time = time.time() # 结束时间
import numpy as np
print("spend time: {}s".format(over_time - start_time))
print("max score:{}".format(max(scores))) # 返回最大的得分
print("the best path:{}".format(str(paths[np.argmax(scores)]))) # np.argmax(scores):返回得分最大路径的下标,再去paths里找
print("all paths number:{}".format(len(paths)))
# # 遍历所有路径,查看得分
# for path , score in zip(paths, scores):
# print("path = {}, score = {}".format(str(path), score))
def viterbi(observation_seq, states, start_p, trans_p, emit_p):
'''
形参params:
observation_seq: observation_set: 观测序列,即朋友圈动态
states: state_set: 隐藏状态:每一天有可能呆在的地点, BJ or YN
start_p: start_probability
trans_p: transition_probability
emit_p: emission_probability
return
result
举例example:
result {当前节点:(路径最高得分,来自的上一个节点)} = [{A:(0.1, None), B:(0.3, None)}, # T1时刻
{A:(0.04, B), B:(0.004, A)}, # T2时刻
{A:(0.0038, A), B:(0.009, B)}] # T3时刻
e.g:
A:(0.04, B): 当前节点A,最高得分是0.04;上一个节点B(0.04是从A走到A和从B走到A二者得分较大值)
'''
result = [{}] # 存放结果dp,每个元素是一个字典,字典的形式 cur_state:(score, pre_state)
# cur_state:当前状态; score:当前状态下的概率值; pre_state上一个状态
# 计算第一个时刻
for s in states: # ('BJ', 'YN'),例如: result[0]["BJ"], 第[0]维key = "BJ"的结果
result[0][s] = (start_p[s] * emit_p[s][observation_seq[0]], None) # 第[0]时刻状态s对应的value为: (起始状态的概率×发射概率, None).
# t代表下标index
for t in range(1, len(observation_seq)):
result.append({}) # 准备t时刻的结果存放字典,形式同上
# s代表字典的key,即当前的位置
for s in states: # 对于每一个t时刻状态s,获取t-1时刻每个状态s_pre的p,
# 结合由s_pre转化为s的转移概率trans_p[s_pre][s]
# 和s状态至obs的发散概率emit_p[s][observation_seq[t]]
# 计算t时刻s状态的最大概率,并记录该概率的来源状态s_pre
# max()内部比较的是一个tuple:(p,s_pre),max比较tuple内的第一个元素值
result[t][s] = max([(result[t-1][s_pre][0]* # result的第一维是哪一个时刻,s_pre指的是上一时刻的状态,第[0]维指的是得分,第[1]维是前一时刻的状态s_pre
trans_p[s_pre][s] * emit_p[s][observation_seq[t]], s_pre)
for s_pre in states]) # t-1时刻m个状态
return result
对于 r e s u l t [ t ] [ s ] result[t][s] result[t][s],举一个例子,当t = 3时,分别求从start转移到 t 3 t_3 t3 = A节点和 t 3 t_3 t3 = B节点这两个值
result[3][A] = max([(result[2][A][0]* trans_p[A][A] * emit_p[A][observation_seq[3]], A),(result[2][B][0]* trans_p[B][A] * emit_p[A][observation_seq[3]], B)]) # 即比较当前时刻为A时,上一时刻s_pre分别等于A或B的较大值
# 状态的样本空间
state_set = ('BJ', 'YN') # 北京,云南
# 观测的样本空间
observation_set = ('烤鸭', '米粉', '饭') # 每天吃的东西
# 起始的状态概率
start_probability = {'BJ': 0.6, 'YN': 0.4} # 小明一般在北京或者在云南
# 状态转移概率矩阵A
transition_probability = {
'BJ': {'BJ': 0.3, 'YN': 0.7}, # 今天在北京时,明天留在北京和去云南的概率
'YN': {'YN': 0.2, 'BJ': 0.8}, # 今天在云南时,明天去在北京和留云南的概率
}
# 状态->观测的发射概率矩阵B
emission_probability = {
'BJ': {'烤鸭': 0.5, '米粉': 0.2, '饭': 0.3}, # 在北京时,吃各种食物的概率
'YN': {'烤鸭': 0.1, '米粉': 0.5, '饭': 0.4}, # 在云南时,吃各种食物的概率
}
observation_seq = ["烤鸭", "米粉", "饭", "烤鸭", "饭"] # 朋友圈5天动态
# observation_seq = ["烤鸭", "米粉", "饭", "烤鸭", "饭"]*200
# 测试并输出结果
result = viterbi(observation_seq, state_set, start_probability,
transition_probability, emission_probability)
for t, res in enumerate(result):
print("起始状态S到 {} 时刻每个状态的最优路径得分 及对应的父节点:{}".format(t+1, res))
def _get_best_paths(result): # 回溯,找到全局最优解
"""
params:
result: like [{}], 每一个字典的形式是 state:(p, pre_state), p is path_score
"""
best_path = []
last_dict = result[-1] # 取到t5时刻的最终结果{'BJ':(0.00042336,'YN'),'YN':(0.0010584,'BJ')}
sorted_list = sorted(last_dict.items(), key = lambda last_dict:last_dict[1], reverse = True) # last_dict.items(): 返回(键:值)元祖数组
# 并按第[1]维排序,即(0.00042336,'YN')或(0.0010584,'BJ')
last_state, (final_score, pre_state) = sorted_list[0] # sorted_list[0] = 'YN':(0.0010584, 'BJ')
best_path.append(last_state) # 把'YN'压到best_path里
for i in range(len(result)-2, -1, -1): # 此时pre_state = 'BJ',拿到上一时刻t3中找pre_state
best_path.append(pre_state)
dict_ = result[i] # result[3] ={'BJ':(0.00378,'BJ'),'YN':(0.001764,'BJ')}
pre_state = dict_[pre_state][1] # 找到'BJ'对应的pre_state = 'BJ'
best_path.reverse() # 因为压入的顺序是从后往前,即从t5到t1压入的,所以最后还得倒过来
return best_path, final_score
测试
start_time = time.time()
# time.sleep(1)
result = viterbi(observation_seq, state_set, start_probability,
transition_probability, emission_probability)
best_path, final_score = _get_best_paths(result)
over_time = time.time()
print("spend time: {}s".format(over_time - start_time))
print("best path:{}".format(str(best_path)))
print("max score:{}".format(final_score))