[机器学习40讲]33-序列化建模:隐马尔可夫模型.

复制代码
import numpy as np
import math
from hmmlearn.hmm import MultinomialHMM

model_man_derby = MultinomialHMM(n_components=2)
states = ["Home", "Away"]
observations = ["Win", "Lose", "Draw"]

initial_vector = np.array([0.5, 0.5])
model_man_derby.startprob_ = initial_vector
transition_matrix = np.array([[0.2, 0.8], [0.8, 0.2]])
model_man_derby.transmat_ = transition_matrix
emission_matrix = np.array([[0.4, 0.467, 0.133], [0.4, 0.4, 0.2]])
model_man_derby.emissionprob_ = emission_matrix

result = np.array([[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2]]).T
titles = ["WW", "WL", "WD", "LW", "LL", "LD", "DW", "DL", "DD"]
i = 0

for title in titles:
    logprob = model_man_derby.score(result[:, i].reshape(1, -1))
    print(title, ':', math.exp(logprob))
    i += 1

这段代码用 hmmlearn 的 MultinomialHMM 构建了一个"曼彻斯特德比"比赛结果模型,计算 9 种两场连赛结果组合的出现概率。


一、模型设定

复制代码
from hmmlearn.hmm import MultinomialHMM

model_man_derby = MultinomialHMM(n_components=2)
states = ["Home", "Away"]                       # 隐藏状态:主场 / 客场
observations = ["Win", "Lose", "Draw"]          # 观测值:胜 / 负 / 平
  • 2 个隐藏状态,3 种观测值,构成一个离散 HMM。

  • states、observations 只是给人看的名字,没有真正传给模型,模型内部并不使用它们。


二、参数手动赋值

1. 初始状态分布

复制代码
initial_vector = np.array([0.5, 0.5])
model_man_derby.startprob_ = initial_vector

第一场比赛处于 Home / Away 状态的概率各 0.5。

2. 状态转移矩阵

复制代码
transition_matrix = np.array([[0.2, 0.8],
                              [0.8, 0.2]])
model_man_derby.transmat_ = transition_matrix
  • Home → Home 0.2,Home → Away 0.8

  • Away → Home 0.8,Away → Away 0.2

即状态倾向于每场切换(主客交替)。

3. 发射矩阵(观测概率)

复制代码
emission_matrix = np.array([[0.4, 0.467, 0.133],   # Home 状态下:胜/负/平
                            [0.4, 0.4,  0.2  ]])   # Away 状态下:胜/负/平
model_man_derby.emissionprob_ = emission_matrix
  • 列顺序对应 observations = ["Win", "Lose", "Draw"],即第 0 列 Win、第 1 列 Lose、第 2 列 Draw。

  • Home 状态更容易负(0.467),Away 状态胜/负均衡(0.4/0.4)。


三、观测序列与标题

复制代码
result = np.array([[0, 0], [0, 1], [0, 2],
                   [1, 0], [1, 1], [1, 2],
                   [2, 0], [2, 1], [2, 2]]).T
titles = ["WW", "WL", "WD", "LW", "LL", "LD", "DW", "DL", "DD"]
  • result 形状是 2×9 ,每一列是一段长度为 2 的观测序列。

  • 列的含义:

列索引 观测序列 标题 含义
0 0,0 WW 两场都胜
1 0,1 WL 先胜后负
2 0,2 WD 先胜后平
3 1,0 LW 先负后胜
4 1,1 LL 两场都负
5 1,2 LD 先负后平
6 2,0 DW 先平后胜
7 2,1 DL 先平后负
8 2,2 DD 两场都平

编号 0/1/2 对应 Win/Lose/Draw,与发射矩阵列顺序一致,逻辑正确。


四、计算每种组合的概率

python

复制代码
i = 0
for title in titles:
    logprob = model_man_derby.score(result[:, i].reshape(1, -1))
    print(title, ':', math.exp(logprob))
    i += 1
  • result[:, i] 取出第 i 列,即长度 2 的观测序列。

  • .reshape(1, -1) 变成 (1, 2):hmmlearn 要求输入是 (n_samples, n_features),这里每个时间步 1 个特征。

  • score(X) 返回该序列的对数似然 log P(sequence)。

  • math.exp(logprob) 转回概率值。

⚠️ 关键隐患 :hmmlearn 的 MultinomialHMM 期望输入是计数向量 (每行是每个观测类别的出现次数,长度 = 发射矩阵列数)。这里把观测值 0/1/2 当作单个数字输入,形状是 (1, 2),只有 1 个特征,而模型发射矩阵有 3 列,会触发维度/语义不匹配。在新版 hmmlearn 中,这种输入通常直接报错或给出错误结果。正确用法见下文。


五、概率计算的数学原理

对单个长度 2 的序列 (o₁, o₂):

P(o1,o2)=∑s1,s2πs1 bs1(o1) as1s2 bs2(o2)P(o1​,o2​)=s1​,s2​∑​πs1​​bs1​​(o1​)as1​s2​​bs2​​(o2​)

即对所有状态路径求和,HMM 内部用前向算法(Forward)在 O(T·N²) 时间内完成。

示例:WW(观测 0,0)

  • 路径 Home→Home:0.5 × 0.4 × 0.2 × 0.4 = 0.016

  • 路径 Home→Away:0.5 × 0.4 × 0.8 × 0.4 = 0.064

  • 路径 Away→Home:0.5 × 0.4 × 0.8 × 0.4 = 0.064

  • 路径 Away→Away:0.5 × 0.4 × 0.2 × 0.4 = 0.016

  • 合计 0.16

示例:LD(观测 1,2)

  • H→H:0.5×0.467×0.2×0.133 ≈ 0.00621

  • H→A:0.5×0.467×0.8×0.2 = 0.03736

  • A→H:0.5×0.4 ×0.8×0.133 ≈ 0.02128

  • A→A:0.5×0.4 ×0.2×0.2 = 0.008

  • 合计 ≈ 0.07285

所有 9 种组合概率之和应接近 1(因为穷举了所有可能的两场结果),可用作校验。


六、主要问题与改进建议

# 问题 说明 建议
1 输入格式不符合 MultinomialHMM 语义 应传入计数向量,每行长度 = 发射矩阵列数(3) 见下方正确写法
2 观测编码 0/1/2 直接当特征 与"词袋/计数"模型语义冲突 用 one-hot / 计数形式
3 未检查概率归一化 发射矩阵每行和应为 1 Home: 0.4+0.467+0.133=1.0 ✓;Away: 0.4+0.4+0.2=1.0 ✓
4 states/observations 未使用 仅是注释作用 可作为映射字典使用
5 用 math.exp 而非 np.exp 单值无所谓,但建议统一 np.exp 更通用
6 循环用 i 手动索引 不够 Pythonic for i, title in enumerate(titles)
7 未验证模型参数合法性 hmmlearn 会做部分校验,但可提前 assert 加断言

七、修复后的正确写法

MultinomialHMM 的输入语义是:每一行是一个时间步的"计数向量" ,长度 = 发射矩阵列数(此处 3)。要把 [o₁, o₂] 这种类别序列编码为每步的 one-hot 计数:

复制代码
import numpy as np
import math
from hmmlearn.hmm import MultinomialHMM

model = MultinomialHMM(n_components=2)
model.startprob_ = np.array([0.5, 0.5])
model.transmat_   = np.array([[0.2, 0.8], [0.8, 0.2]])
model.emissionprob_ = np.array([[0.4, 0.467, 0.133],
                                [0.4, 0.4,   0.2  ]])

titles = ["WW","WL","WD","LW","LL","LD","DW","DL","DD"]
seqs = [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]]

def to_counts(seq, n_obs=3):
    """把类别序列编码成 (T, n_obs) 的 one-hot 计数矩阵"""
    out = np.zeros((len(seq), n_obs), dtype=int)
    for t, o in enumerate(seq):
        out[t, o] = 1
    return out

probs = []
for title, seq in zip(titles, seqs):
    X = to_counts(seq)
    logprob = model.score(X)
    p = math.exp(logprob)
    probs.append(p)
    print(f"{title}: {p:.6f}")

print("sum =", sum(probs))   # 应接近 1

运行后 9 个概率之和 ≈ 1,说明穷举完整、模型参数自洽。


八、小结

  • 代码意图正确:枚举两场比赛的 3×3=9 种结果组合,用 HMM 前向算法算每种组合的联合概率。

  • 数学逻辑正确:概率由初始分布、转移矩阵、发射矩阵按路径求和得到。

  • 主要缺陷 是 MultinomialHMM 的输入格式:类别编号 0/1/2 不能直接当特征输入,需要编码成计数/one-hot 向量,否则在新版 hmmlearn 中会报维度错误或得到无意义结果。

  • 参数本身自洽(每行和为 1),枚举完整,加上编码修正后即可正确输出 9 种组合的概率分布。

如果只是想要"两场独立比赛结果的组合概率",其实也可以不用 HMM,直接用马尔可夫链的前向概率 手算(如上文示例),结果与修正后的 HMM score 完全一致;HMM 的价值在于它能处理隐藏状态(主/客场倾向),并支持更长序列、参数学习(EM)等场景。

相关推荐
geats人山人海1 小时前
现代c++第四章 简单类的概念
1024程序员节
ctlover2 小时前
langchain 中间件
中间件·langchain·1024程序员节
Ivanqhz2 小时前
MoE 原理、TC/EC 路由、LSH-MoE 与负载均衡浅析
运维·网络·人工智能·深度学习·算法·机器学习·负载均衡
Be for thing3 小时前
PyTorch自动求导Autograd详解|requires_grad、backward、grad、梯度清零
pytorch·学习·1024程序员节
syagain_zsx3 小时前
ODB 与 MySQL:C++ 里的 ORM 实践(类即表、事务铁律与查询封装)
开发语言·数据库·c++·mysql·工具库·1024程序员节
小嘉丶学长3 小时前
Day08 · 字符串(二)
服务器·开发语言·python·学习·1024程序员节
方方洛3 小时前
ray教程-14-RayTune与超参搜索
人工智能·分布式·机器学习
4SAPI4 小时前
电商全链路内容自动化生成大模型 API 生产选型指南:4SAPI、硅基流动与 OpenRouter 的架构权衡
运维·人工智能·架构·自动化·1024程序员节