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∑πs1bs1(o1)as1s2bs2(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)等场景。