查看原文
其他

隐马尔科夫模型(HMM)简介及Python实现

王海华 模型视角 2023-09-05

隐马尔科夫模型(Hidden Markov Model,简称HMM)是一种统计模型,用于描述一个含有隐状态的马尔科夫过程。其核心思想是,系统的状态是不直接观察到的,但对于这些状态的变化,会产生一系列的可观测的输出。这些输出的产生是依赖于当前的隐状态的

基本要素

HMM包含以下基本元素:

  1. 隐状态集合 和观测集合
  2. 初始状态摡率 ,它定义了系统开始时处于某一状态的概率。
  3. 状态转移概率 ,它定义了从一个状态转移到另一个状态的概率。
  4. 观测摡率 ,它定义了给定当前状态下,观测某一符号的概率。

数学上,HMM可以表示为五元组 。下面是上述概念的数学表达。

  • 状态转移概率矩阵 : 每一个元素 代表从状态 转移到状态 的概率。定义为:

其中 是时刻 的状态。

  • 观测概率䂓阵 : 每一个元素 代表在状态 时观测到 的概率。定义为:

其中 是时刻 的观测值。

  • 初始状态溉率矩阵 : 每一个元素 代表初始时刻处于状态 的概率。定义为:

三个基本问题

应用HMM的三个基本问题:

  1. 评估问题: 给定模型 和观测序列 ,计算在模型 下观测序列 出现的概率。解决方法: 前向-后向算法
  2. 解码问题: 给定模型 和观测序列 ,找出最有可能的状态序列。解决方法: Viterbi算法
  3. 学习问题: 给定观测序列 ,如何调整模型参数 以最大化观测序列 出现的概率。解决方法: Baum-Welch算法 (一种特殊的期望最大化算法)

前向算法

前向算法是一种动态规划方法,用于计算观测序列的概率。它定义了一个前向变量 ,表 示在时间 观测到 并且系统处于状态 的概率。递推公式为:

其中 是从状态 到状态 的转移概率, 是在状态 下观测到 的概率。

Viterbi算法

Viterbi算法也使用动态规划,但其目的是找到最有可能的隐藏状态序列。它定义了一个路径 变量 和一个前一个状态的指针。递推公式为:

Baum-Welch算法

Baum-Welch算法是一种期望最大化 (EM) 算法,用于找到使观测序列概率最大化的模型参数。HMM在很多领域都有应用,例如语音识别、自然语言处理、生物信息学等。

下图描述了一个隐马尔可夫模型 (HMM) 的问题,该模型涉及到天气和某人的日常活动。

  • 隐藏状态 (Hidden States):这些是模型中的隐变量,即我们不能直接观察到的变量。在这个例子中,隐藏状态是“Rainy”(雨天)和“Sunny”(晴天)。这意味着在某一天,天气可以是雨天或晴天,但我们不能直接观察到它。

  • 观察 (Observations):这些是我们可以直接观察到的变量或结果。在这里,观察是指某人的日常活动,可以是“walk”(散步)、“shop”(购物)或“clean”(打扫)。

  • 初始概率 (Start Probability):这描述了模型开始时天气状态的概率。在这个例子中,开始时雨天的概率是0.6,晴天的概率是0.4。

  • 转移概率 (Transition Probability):这描述了从当前状态转移到另一个状态的概率。例如,如果今天是雨天,那么明天依然是雨天的概率是0.7,而转为晴天的概率是0.3。同样,如果今天是晴天,那么明天变为雨天的概率是0.4,而继续是晴天的概率是0.6。

  • 发射概率 (Emission Probability):这描述了给定隐藏状态(天气)时,观察到某个活动的概率。例如,如果是雨天,那么该人散步的概率是0.1,购物的概率是0.4,打扫的概率是0.5。而如果是晴天,该人散步的概率是0.6,购物的概率是0.3,打扫的概率是0.1。

案例:天气状态

一个常见的HMM应用示例是“天气预测”问题。这个例子是为了解释HMM的基本概念,而不是真实的天气预测方法。

假设一个小城市的天气只有两种状态:晴天 (Sunny) 和雨天 (Rainy)。但我们不能直接观察到这些天气状态(假设我们在一个看不到光的室内)。我们只能通过一个朋友每天穿的衣服来推测天气。他有两种衣服:短袖 (Short-sleeved) 和长袖 (Long-sleeved)。这是一个HMM问题,因为天气状态是隐含的,我们只能观察到衣服。

模型参数:

  1. 状态集合: 'Sunny', 'Rainy'
  2. 观测集合: 'Short-sleeved', 'Long-sleeved'
  3. 初始概率: 'Sunny' : 0.9 , 'Rainy' : 0.1
  4. 状态转移概率矩阵:

观测概率矩阵:

Python实现

from hmmlearn import hmm
import numpy as np

# 定义模型参数
states = ["Sunny""Rainy"]
observations = ["Short-sleeved""Long-sleeved"]

initial_probabilities = np.array([0.90.1])

transition_probabilities = np.array([
    [0.80.2],
    [0.30.7]
])

emission_probabilities = np.array([
    [0.70.3],
    [0.40.6]
])

# 创建并训练HMM模型
model = hmm.MultinomialHMM(n_components=2, n_trials=1)  # 设置n_trials为1
model.startprob_ = initial_probabilities
model.transmat_ = transition_probabilities
model.emissionprob_ = emission_probabilities

# 假设我们观察到的连续三天的衣服是: ["Short-sleeved", "Long-sleeved", "Long-sleeved"]
# 将其转化为多项分布的计数
obs_seq = np.array([
    [10],  # Short-sleeved
    [01],  # Long-sleeved
    [01]   # Long-sleeved
])

# 使用Viterbi算法找到最可能的天气序列
logprob, state_seq = model.decode(obs_seq)

print("Observation sequence:", [observations[np.argmax(obs)] for obs in obs_seq])
print("Most likely weather sequence:", [states[i] for i in state_seq])

结果:

Observation sequence: ['Short-sleeved''Long-sleeved''Long-sleeved']
Most likely weather sequence: ['Sunny''Sunny''Sunny']

解释一下上述代码的主要步骤和内容:

  1. 导入所需的库hmmlearn库中的hmm模块,以及numpy库。这些库分别用于处理隐马尔可夫模型和数值操作。

  2. 定义模型参数:

  • 两个隐状态:Sunny和Rainy。
  • 两种观测:Short-sleeved和Long-sleeved。
  • 模型的初始概率、状态转移概率和发射概率。
  1. 初始化和设置HMM模型:创建了一个MultinomialHMM模型,该模型具有2个隐状态,并设置了n_trials为1。这意味着每次观测都基于单次试验。接着,我们为模型设置了初始概率、状态转移概率和发射概率。

  2. 观测数据:我们定义了一个观测序列,表示连续三天的衣服选择:Short-sleeved, Long-sleeved, Long-sleeved。这些观测被转换为多项分布的计数形式。

  3. 使用Viterbi算法解码:我们使用decode方法和Viterbi算法来找出给定观测序列下最有可能的隐状态序列。我们正在尝试观察到的衣服选择预测连续三天的天气,基于我们。

  4. 输出结果:最后,我们打印了观测到的衣服选择序列和预测的天气序列。

上述代码旨在解释如何使用hmmlearn库的MultinomialHMM模型来解决实际问题,即基于连续几天的衣服选择来预测这些天的天气。

hmmlearn库简介

hmmlearn 实现了隐马尔可夫模型 (Hidden Markov Models,简称 HMMs)。HMM 是一种生成概率模型,其中一系列可观测的变量由一系列内部隐藏状态生成。这些隐藏状态并不直接观察到。隐藏状态之间的转换被假定为(一阶)马尔可夫链的形式。它们可以通过起始概率向量和转移概率矩阵来指定。一个可观测值的发射概率可以是任何分布,其参数取决于当前的隐藏状态。HMM 完全由这些参数确定。

可用的模型:

  • hmm.CategoricalHMM: 有分类(离散)发射的隐马尔可夫模型。
  • hmm.GaussianHMM: 具有高斯发射的隐马尔可夫模型。
  • hmm.GMMHMM: 具有高斯混合发射的隐马尔可夫模型。
  • hmm.MultinomialHMM: 具有多项分布发射的隐马尔可夫模型。
  • hmm.PoissonHMM: 具有泊松发射的隐马尔可夫模型。
  • vhmm.VariationalCategoricalHMM: 使用变分推断训练的有分类(离散)发射的隐马尔可夫模型。
  • vhmm.VariationalGaussianHMM: 使用变分推断训练的具有多元高斯发射的隐马尔可夫模型。

构建 HMM 并生成样本:可以通过将上述参数传递给构造函数来构建一个 HMM 实例。然后,可以通过调用 sample() 从 HMM 生成样本。

监控收敛: EM 算法的迭代次数由 n_iter 参数上限。训练会继续进行,直到完成 n_iter 步或分数的变化低于指定的阈值 tol。注意,根据数据,EM 算法可能会或可能不会在给定的步骤中实现收敛。可以使用 monitor_ 属性来诊断收敛。

该库官网给出了一些有趣的例子,我们以地震解释为例,来说明该库的使用。

我们正在查看的是从1900年到2006年间,全球发生的所有7级及以上的地震数据。这个数据是由美国地质勘查局收集的。这段代码的主要目标是,通过考察地震的发生频率,看看我们是否能够区分出导致地震的不同的地壳构造过程。

import numpy as np
import matplotlib.pyplot as plt

from scipy.stats import poisson
from hmmlearn import hmm

# earthquake data from http://earthquake.usgs.gov/
earthquakes = np.array([
    1314810162632271832362422232218,
    2521211481114231817192022191326,
    13142224212226212324274131273526,
    28363921172217191534101522181520,
    15221916302729232016212125161815,
    1814101581561187181613121320,
    1516121815161315161111])

这里,我们有一个名为earthquakes的numpy数组,它包含了不同年份地震的数量。接下来对数据进行可视化:

fig, ax = plt.subplots()
ax.plot(earthquakes, ".-", ms=6, mfc="orange", alpha=0.7)
ax.set_xticks(range(0, earthquakes.size, 10))
ax.set_xticklabels(range(1906200710))
ax.set_xlabel('Year')
ax.set_ylabel('Count')
fig.show()

在这部分,我们创建了一个图形,展示了每年发生的地震数量。每个点表示一个年份,其高度表示那一年发生的地震数量。

接下来,我们尝试为地震数据拟合一个带有泊松发射的HMM。

scores = list()
models = list()
for n_components in range(15):
    for idx in range(10):  # ten different random starting states
        # define our hidden Markov model
        model = hmm.PoissonHMM(n_components=n_components, random_state=idx,
                               n_iter=10)
        model.fit(earthquakes[:, None])
        models.append(model)
        scores.append(model.score(earthquakes[:, None]))
        print(f'Converged: {model.monitor_.converged}\t\t'
              f'Score: {scores[-1]}')

# get the best model
model = models[np.argmax(scores)]
print(f'The best model had a score of {max(scores)} and '
      f'{model.n_components} components')

# use the Viterbi algorithm to predict the most likely sequence of states
# given the model
states = model.predict(earthquakes[:, None])

在上述代码中,我们尝试使用不同数量的隐藏状态(从1到4)来训练模型,并对每个隐藏状态数量尝试10次不同的初始化,以获取最佳的模型拟合。

# plot model states over time
fig, ax = plt.subplots()
ax.plot(model.lambdas_[states], ".-", ms=6, mfc="orange")
ax.plot(earthquakes)
ax.set_title('States compared to generated')
ax.set_xlabel('State')

选择最佳模型:

model = models[np.argmax(scores)]
print(f'The best model had a score of {max(scores)} and {model.n_components} components')

根据模型的得分,我们选择了得分最高的模型,并输出了其得分和隐藏状态数量。

states = model.predict(earthquakes[:, None])

使用Viterbi算法,我们预测了最有可能的隐藏状态序列。

结果可视化.。在接下来的代码中,我们将模型预测的状态与实际的地震数据进行了对比,并展示了地震分布与模型的等待时间参数值之间的关系。

fig, ax = plt.subplots()
ax.imshow(model.transmat_, aspect='auto', cmap='spring')
ax.set_title('Transition Matrix')
ax.set_xlabel('State To')
ax.set_ylabel('State From')
fig, ax = plt.subplots()
ax.imshow(model.transmat_, aspect='auto', cmap='spring')
ax.set_title('Transition Matrix')
ax.set_xlabel('State To')
ax.set_ylabel('State From')
# get probabilities for each state given the data, take the average
# to find the proportion of time in that state
prop_per_state = model.predict_proba(earthquakes[:, None]).mean(axis=0)

# earthquake counts to plot
bins = sorted(np.unique(earthquakes))

fig, ax = plt.subplots()
ax.hist(earthquakes, bins=bins, density=True)
ax.plot(bins, poisson.pmf(bins, model.lambdas_).T @ prop_per_state)
ax.set_title('Histogram of Earthquakes with Fitted Poisson States')
ax.set_xlabel('Number of Earthquakes')
ax.set_ylabel('Proportion')
plt.show()

参考资料:

【1】https://hmmlearn.readthedocs.io/en/latest/auto_examples/plot_poisson_hmm.html#sphx-glr-auto-examples-plot-poisson-hmm-py

【2】https://vitalflux.com/hidden-markov-models-concepts-explained-with-examples/


下面是笔者的新书《数学建模实战:手把手教你参加数学建模竞赛》感兴趣的读者可以购买。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存