1. 从一通电话说起:隐马尔科夫模型到底在解决什么问题
隐马尔科夫模型(Hidden Markov Model,HMM)是一类用来描述「背后有隐藏状态、我们只能看到表面现象」的统计模型。它能做什么?简单说,就是根据你观察到的一串现象,反推出背后那串看不见的状态,或者算出某个观察序列出现的概率有多大。适合谁?做语音识别、中文分词、词性标注、基因序列分析,甚至量化交易信号推断的初学者,都会碰到它。
我第一次接触 HMM 的时候,被「隐藏状态」「观测序列」「转移概率」「发射概率」这几个词绕得头晕。后来发现,只要抓住一个核心:你看得见的东西,是由你看不见的东西决定的。比如你朋友每天告诉你他做了什么,但你不知道他那边的天气;天气就是隐藏状态,他做的活动就是观测值。这就是 HMM 最朴素的样子。
这篇文章我会用两个生活化例子——天气变化和掷骰子——把 HMM 拆开讲透。天气例子帮你理解状态转移,掷骰子例子帮你理解发射概率。然后给你一份可以直接复制运行的 Python 代码,用hmmlearn库跑一遍状态推断,看看模型能不能从「散步、购物、清理」这串活动里猜出天气。最后把初学者最容易踩的报错整理出来,对照着排查。
你不需要很深的数学背景,只要会一点 Python 基础,跟着敲一遍,就能建立直观理解。整篇的节奏是:先讲清楚概念,再给配置,再跑验证,最后排错。我们直接开始。
2. 天气与掷骰子:把隐藏状态和观测序列讲成人话
2.1 天气例子:隐藏状态与状态转移
假设你有个朋友 Bob 住在另一个城市,你们每天通电话。Bob 只做三件事:散步(walk)、购物(shop)、清理房间(clean)。他做什么,完全取决于当天天气。但你不知道他那边的天气,你只知道这个地区总体下雨偏多。
这里有两层东西:
- 隐藏状态:天气,只有「雨(Rainy)」和「晴(Sunny)」两种,你看不见。
- 观测值:Bob 的活动,你能听见。
天气不是随机跳的,它有自己的规律。如果今天下雨,明天大概率还是雨;如果今天晴,明天大概率还是晴。这种「今天状态影响明天状态」的规律,就是状态转移概率。用矩阵写出来:
| 今天\明天 | Rainy | Sunny |
|---|---|---|
| Rainy | 0.7 | 0.3 |
| Sunny | 0.4 | 0.6 |
读法是:今天下雨,明天继续下雨的概率是 0.7,转晴的概率是 0.3;今天晴,明天转雨的概率是 0.4,继续晴的概率是 0.6。每一行加起来必须是 1,这是概率的基本要求。
还有一个初始状态概率,表示第一天天气的猜测。比如{Rainy: 0.6, Sunny: 0.4},意思是这个地区平均下来下雨天多一些。
2.2 掷骰子例子:发射概率到底是什么
天气例子讲的是状态怎么变,但还没讲清楚「状态怎么决定观测值」。这时候掷骰子就派上用场了。
想象你面前有两个骰子,一个正常骰子(六面均匀),一个灌铅骰子(出 6 的概率特别高)。你背对着我,随机选一个骰子掷,只告诉我点数,不告诉我用的是哪个骰子。这里:
- 隐藏状态:你用的是正常骰子还是灌铅骰子。
- 观测值:掷出来的点数 1 到 6。
- 发射概率:在某个骰子状态下,掷出某个点数的概率。
正常骰子每个点数概率都是 1/6;灌铅骰子可能 6 的概率是 0.5,其他点数分摊剩下 0.5。你连续掷出一串「6, 6, 5, 6」,我就能推断你大概率一直在用灌铅骰子。这就是从观测序列反推隐藏状态的直觉。
回到天气例子,发射概率就是「在某种天气下,Bob 做某件事的概率」:
| 天气\活动 | walk | shop | clean |
|---|---|---|---|
| Rainy | 0.1 | 0.4 | 0.5 |
| Sunny | 0.6 | 0.3 | 0.1 |
下雨天 Bob 更可能在家清理房间(0.5),晴天更可能出去散步(0.6)。每一行加起来也是 1。
2.3 三大问题:HMM 到底要回答什么
把上面两套概率凑齐,一个 HMM 就定义完了,通常记作 λ = (A, B, π):
- A:状态转移概率矩阵
- B:发射概率矩阵
- π:初始状态概率分布
有了 λ,HMM 要回答三个经典问题:
- 评估问题:给定 λ 和观测序列 O,算 P(O|λ),也就是这串观测出现的总概率。用前向算法。
- 解码问题:给定 λ 和观测序列 O,找最可能的状态序列。用 Viterbi 算法。
- 学习问题:只给观测序列,反推 λ 的参数。用 Baum-Welch 算法。
天气例子里,Alice 和 Bob 通了三天电话,听到「散步、购物、清理」。她想知道:这串活动出现的总概率是多少(问题 1)?最可能对应的天气序列是「晴、雨、雨」还是别的(问题 2)?这两个问题,下面的代码会直接跑给你看。
3. 可复制配置:用 hmmlearn 搭一个天气 HMM
3.1 环境准备与依赖安装
先把环境弄干净。我建议用虚拟环境,避免和系统里的包打架:
python -m venv hmm_demo source hmm_demo/bin/activate # Windows 用 hmm_demo\Scripts\activate pip install hmmlearn numpyhmmlearn是 scikit-learn 风格的 HMM 库,API 很友好。装好后我们直接写配置。
3.2 用 JSON 描述模型参数
为了让你一眼看清参数结构,我先把天气 HMM 写成一份 JSON 配置。这份配置和后面 Python 代码里的数值完全一致,你可以直接存成hmm_config.json:
{ "states": ["Rainy", "Sunny"], "observations": ["walk", "shop", "clean"], "start_probability": { "Rainy": 0.6, "Sunny": 0.4 }, "transition_probability": { "Rainy": {"Rainy": 0.7, "Sunny": 0.3}, "Sunny": {"Rainy": 0.4, "Sunny": 0.6} }, "emission_probability": { "Rainy": {"walk": 0.1, "shop": 0.4, "clean": 0.5}, "Sunny": {"walk": 0.6, "shop": 0.3, "clean": 0.1} } }注意几个约束,写错会直接报错:
- 每个状态的转移概率之和为 1。
- 每个状态的发射概率之和为 1。
- 初始概率之和为 1。
3.3 转成 hmmlearn 的矩阵形式
hmmlearn不认字典,它要的是 numpy 数组。所以我们需要把上面的 JSON 映射成三个矩阵。下面这段代码把配置读进来并转换,你可以直接复制:
import json import numpy as np from hmmlearn import hmm with open("hmm_config.json", "r", encoding="utf-8") as f: cfg = json.load(f) states = cfg["states"] obs = cfg["observations"] # 初始概率向量,顺序必须和 states 一致 start_prob = np.array([cfg["start_probability"][s] for s in states]) # 转移矩阵 A[i][j] = 从状态 i 转到状态 j 的概率 trans_mat = np.array([ [cfg["transition_probability"][si][sj] for sj in states] for si in states ]) # 发射矩阵 B[i][k] = 状态 i 下观测到第 k 个符号的概率 emit_mat = np.array([ [cfg["emission_probability"][si][ok] for ok in obs] for si in states ]) print("start:", start_prob) print("trans:\n", trans_mat) print("emit:\n", emit_mat)运行后你会看到三个矩阵打印出来,形状分别是 (2,)、(2,2)、(2,3)。这一步是很多初学者卡住的地方——顺序必须和 states、observations 的列表顺序严格对应,否则概率全乱。
3.4 构建模型对象
有了矩阵,构建模型就三行:
model = hmm.CategoricalHMM(n_components=2, n_iter=100) model.startprob_ = start_prob model.transmat_ = trans_mat model.emissionprob_ = emit_mat这里用的是CategoricalHMM,因为我们的观测值是离散的符号(walk/shop/clean)。如果是连续值(比如语音特征),要用GaussianHMM。n_components=2对应两个隐藏状态。
4. 验证请求:跑一遍状态推断看结果
4.1 把观测序列编码成整数
hmmlearn的观测输入是整数索引,不是字符串。所以「散步、购物、清理」要映射成[0, 1, 2]:
obs_index = {name: i for i, name in enumerate(obs)} sequence = ["walk", "shop", "clean"] X = np.array([[obs_index[o]] for o in sequence]) print("观测序列编码:", X.ravel())输出应该是[0 1 2]。
4.2 计算观测序列概率(问题 1)
用score方法算 P(O|λ):
log_prob = model.score(X) print("观测序列的对数概率:", log_prob) print("观测序列概率:", np.exp(log_prob))score返回的是对数概率,因为直接算概率容易下溢。取exp就还原成普通概率。这个值告诉你:在给定天气模型下,「散步、购物、清理」这串活动出现的可能性有多大。
4.3 用 Viterbi 解码最可能天气(问题 2)
log_prob, state_seq = model.decode(X, algorithm="viterbi") weather_seq = [states[s] for s in state_seq] print("最可能的天气序列:", weather_seq) print("该路径的对数概率:", log_prob)跑出来你会看到类似['Sunny', 'Rainy', 'Rainy']的结果。为什么?因为第一天散步,晴天发射概率 0.6 远高于雨天 0.1,所以第一天判晴;第二天购物,雨天 0.4 略高于晴天 0.3,加上从晴转雨有 0.4 的转移概率,综合下来判雨;第三天清理,雨天 0.5 明显高于晴天 0.1,继续判雨。
4.4 用前向-后向算法看每步状态概率
Viterbi 只给一条最优路径,有时候你想知道每一步各个状态的概率分布,用predict_proba:
probs = model.predict_proba(X) for i, p in enumerate(probs): print(f"第{i+1}天 雨:{p[0]:.3f} 晴:{p[1]:.3f}")这样你能看到模型在每一天对「雨/晴」的置信度,比单一序列更有信息量。实测下来,第一天晴的概率会明显高,第二、三天雨的概率反超,和 Viterbi 结果一致。
4.5 换个观测序列再验证
把序列换成["clean", "clean", "clean"],重新跑一遍:
X2 = np.array([[obs_index[o]] for o in ["clean", "clean", "clean"]]) print("天气序列:", [states[s] for s in model.decode(X2)[1]])你会看到连续三天都判雨,因为雨天清理房间的概率 0.5 远高于晴天 0.1。这说明模型确实在按发射概率和转移概率做联合推断,不是瞎猜。
5. 本篇常见报错排查:401、local proxy failed、reading choices 等
初学者跑 HMM 代码时,报错往往不在算法本身,而在环境、依赖和数据格式。下面按真实报错逐条对照。
5.1 401 Unauthorized / API key 相关
如果你在调用某些云端推理接口时看到401 Unauthorized,通常是 Key 没带对或过期。检查请求头里的Authorization: Bearer <你的Key>是否完整,Key 有没有多余空格。如果你用的是 TaoToken 这类平台的 API,去控制台重新生成一个 Key,确认 Base URL 填的是https://taotoken.net/api,不要多加路径。401 的本质是身份没通过,和 HMM 算法无关,但很多人会误以为是模型问题。
5.2 local proxy failed / connection refused
这个报错一般出现在你本地起了代理但端口没通,或者环境变量HTTP_PROXY指向了一个不存在的地址。先检查:
echo $HTTP_PROXY echo $HTTPS_PROXY如果有值但你不确定是否可用,先unset HTTP_PROXY HTTPS_PROXY再跑代码。本地跑hmmlearn根本不需要网络,所以这个报错多半是你环境里残留了代理配置。清掉即可。
5.3 reading choices / 读取配置失败
如果你把配置写成 JSON 读取时报json.decoder.JSONDecodeError,检查两点:文件是不是 UTF-8 编码;字典里有没有多余的逗号。Python 的json不允许尾随逗号。另外确认hmm_config.json和脚本在同一目录,或者用绝对路径。
5.4 OAuth / token 过期
有些平台用 OAuth 流程拿短期 token,过期后会报invalid_token或token expired。解决办法是重新走一遍授权,或者改用长期 API Key。如果你在 Claude Code 这类工具里配置模型,需要同时填对三件套:Base URL、API Key、Model ID。缺一个都会认证失败。Base URL 用https://taotoken.net/api,Model ID 按平台文档填,Key 从控制台复制。
5.5 概率矩阵不合法
hmmlearn会校验startprob_、transmat_、emissionprob_每行之和是否为 1。如果报ValueError: rows of transmat_ must sum to 1,说明你的概率没归一化。检查 JSON 里每一行,手动加一遍。浮点误差可以用np.allclose验证:
print(np.allclose(trans_mat.sum(axis=1), 1.0)) print(np.allclose(emit_mat.sum(axis=1), 1.0))两个都应该是True。
5.6 观测值索引越界
如果报IndexError,多半是你的观测符号没在observations列表里。比如你写了"shopping"但列表里是"shop",映射时就找不到。打印obs_index确认映射关系,保证输入序列里的每个符号都有对应整数。
6. 继续深入:从天气例子到真实场景的接入路径
天气和掷骰子只是入口。真正让 HMM 发挥价值的地方,是那些「观测丰富但状态不可见」的场景:语音识别里音素是隐藏状态、声波是观测;中文分词里词性是隐藏状态、字是观测;量化里市场 regime 是隐藏状态、价格波动是观测。你把这篇文章的代码改一改参数,就能套到这些场景上。
如果你想把 HMM 推断接到自己的应用里,或者用大模型辅助生成状态转移的候选参数,可以走 API 方式调用。接入前先在控制台把 Key 建好,Base URL 用https://taotoken.net/api,模型 ID 按文档选。需要长期跑编码任务或 Agent 流程的,可以看 Coding Plan;只是临时验证模型输出的,用模型对话页面就够了。接入文档里有完整的请求示例,照着改参数即可。
我自己的习惯是:先用小规模观测序列在本地把 HMM 参数调通,确认 Viterbi 解码结果符合直觉,再把推断逻辑封装成函数接到线上。这样出问题时,能快速定位是参数问题还是接口问题。踩过的坑告诉我,概率矩阵的归一化和观测索引映射这两步最容易出错,跑之前先打印验证一遍,能省很多调试时间。