1. 从理论到实践为什么选择hmmlearn如果你对时间序列分析、语音识别或者自然语言处理有过一些了解那你大概率听说过隐马尔可夫模型。这个听起来有点玄乎的名字其实是一个特别强大的统计模型专门用来处理那些“表面现象背后有隐藏规律”的数据。比如我们每天看到的天气晴天、雨天是“观测”而背后影响天气的大气状态高压、低压就是“隐藏状态”。HMM就是帮我们通过观测到的天气去推测背后那些看不见的大气状态。听起来很酷但自己从头实现一个HMM光是前向-后向算法、维特比算法就能让人头大。别担心这就是hmmlearn库存在的意义。它是一个专门为Python打造的HMM工具包把那些复杂的数学计算都封装好了我们只需要关心怎么用数据去“喂”它以及怎么解读结果。我用了这么多年最大的感受就是它把HMM的门槛降到了极低让算法工程师能更专注于业务逻辑和模型调优而不是纠结于矩阵运算的细节。hmmlearn主要提供了三种模型对应三种不同的数据“口味”GaussianHMM当你观测到的数据是连续的数值比如温度、股价、声音频率而且你觉得每个隐藏状态下的观测值大致符合一个高斯分布也就是正态分布就用它。这是最常用、最基础的一个。GMMHMM这是GaussianHMM的“升级版”。有时候一个隐藏状态下的数据可能不是单一的高斯分布能描述的它可能是由好几个不同的“子模式”混合而成。比如描述“繁忙时段”的交通流量可能混合了“早高峰”和“晚高峰”两种模式。GMMHMM就是用高斯混合模型来刻画每个状态拟合能力更强。MultinomialHMM当你观测到的是离散的类别比如掷骰子的点数1到6、文本中的词语、或者用户的行为标签点击、购买、收藏这个模型就是你的菜。它用多项分布来描述每个隐藏状态下产生各个观测类别的概率。简单来说选哪个模型就看你的数据是连续的还是离散的以及数据分布简不简单。接下来我们就用两个最接地气的例子——天气预测和股价分析手把手带你玩转hmmlearn。2. 实战入门用GaussianHMM预测晴雨我们先从一个最直观的例子开始通过历史温度数据来推测每天的隐藏天气状态晴天或雨天。这个例子完美契合GaussianHMM的假设我们观测到的是连续的温度值而每个隐藏天气状态晴/雨下的温度应该服从一个高斯分布晴天温度高且波动大雨天温度低且波动小。2.1 环境搭建与数据模拟首先确保你的Python环境里已经装好了必要的库。打开你的终端或命令行一行命令搞定pip install numpy matplotlib hmmlearn接下来我们来模拟一些数据。在真实项目中你当然是用真实的历史温度和天气数据。但为了演示我们先自己生成一份带“隐藏规律”的数据。import numpy as np from hmmlearn import hmm import matplotlib.pyplot as plt # 设定随机种子确保每次运行结果一致 np.random.seed(42) # 模拟参数设定 n_samples 500 # 总共500天的数据 # 假设晴天温度均值为25度标准差为3度雨天均值为18度标准差为2度 mean_sunny, std_sunny 25, 3 mean_rainy, std_rainy 18, 2 # 生成隐藏状态序列我们假设真实世界是250天晴天接250天雨天然后打乱顺序模拟天气的随机变化 true_states np.array([0] * 250 [1] * 250) np.random.shuffle(true_states) # 根据隐藏状态生成观测温度 temperatures [] for state in true_states: if state 0: # 晴天 temp np.random.normal(mean_sunny, std_sunny) else: # 雨天 temp np.random.normal(mean_rainy, std_rainy) temperatures.append(temp) # 将数据整理成hmmlearn需要的格式(n_samples, n_features) # 这里n_features1因为我们只用温度这一个观测特征 observations np.array(temperatures).reshape(-1, 1)数据准备好了它长这样一个500行的数组每一行代表一天的温度。我们心里知道前250个数据点大致是晴天模式后250个是雨天模式但模型是不知道的它需要自己从数据里学出这个规律。2.2 模型训练与状态解码现在请出我们的主角GaussianHMM。创建模型时有几个关键参数需要你心里有数n_components这是最重要的参数代表你认为数据背后有多少个隐藏状态。这里我们假设是2晴和雨。covariance_type协方差矩阵的类型。full表示每个状态有自己的完整协方差矩阵最灵活diag假设特征间独立只计算对角线上的方差tied是所有状态共享同一个协方差矩阵spherical是每个状态的特征方差都相同。对于单特征数据后三者区别不大。我们先用full。n_iter训练的最大迭代次数避免EM算法无限循环下去。# 创建并训练模型 model hmm.GaussianHMM(n_components2, covariance_typefull, n_iter100) model.fit(observations) # 这一步就是模型学习的过程 print(训练完成) print(f模型收敛所需的迭代次数{model.monitor_.iter})训练完成后模型就“学会”了。我们可以用它来对我们自己的数据或者新的数据进行“解码”即推测每一天最可能的隐藏状态是什么。# 预测隐藏状态序列 predicted_states model.predict(observations) # 看看模型学到的参数 print(\n模型学到的每个状态的温度均值) print(model.means_) print(\n模型学到的每个状态的温度方差) print(model.covars_) print(\n状态转移概率矩阵A矩阵) print(model.transmat_)运行后你可能会看到类似这样的输出模型学到的每个状态的温度均值 [[17.95] [24.89]] 模型学到的每个状态的温度方差 [[[3.82]] [[8.65]]]看模型成功地把两个状态区分开了它识别出一个状态我们解读为“雨天”均值在18度左右方差较小另一个状态“晴天”均值在25度左右但方差更大这和我们的预设是吻合的。transmat_矩阵则告诉我们比如今天如果是状态0雨天明天仍然是雨天的概率有多大变成晴天的概率又有多大。这个矩阵反映了天气变化的“惯性”。2.3 结果可视化与解读光看数字不够直观我们把原始数据、真实状态我们模拟时知道的和模型预测的状态画在一张图上高下立判。# 可视化结果 fig, axes plt.subplots(2, 1, figsize(14, 10)) # 子图1温度观测序列与预测状态 ax1 axes[0] ax1.plot(observations, colorlightgray, linewidth0.8, label温度观测值) # 用散点颜色表示预测的状态 scatter1 ax1.scatter(range(n_samples), observations, cpredicted_states, s15, cmapviridis, alpha0.6, label预测状态 (HMM)) ax1.set_title(GaussianHMM 天气状态预测, fontsize14) ax1.set_xlabel(天数, fontsize12) ax1.set_ylabel(温度 (°C), fontsize12) ax1.legend() # 添加一个颜色条说明状态 cbar1 plt.colorbar(scatter1, axax1) cbar1.set_label(预测的隐藏状态 (0/1), rotation270, labelpad15) # 子图2对比真实状态与预测状态 ax2 axes[1] # 绘制真实状态我们模拟时知道的 ax2.plot(range(n_samples), true_states 0.1, o, markersize4, label真实状态 (模拟), alpha0.7) # 绘制预测状态 ax2.plot(range(n_samples), predicted_states - 0.1, x, markersize4, label预测状态 (HMM), alpha0.7) ax2.set_yticks([0, 1]) ax2.set_yticklabels([状态 0 (推测为雨天), 状态 1 (推测为晴天)]) ax2.set_title(真实状态 vs. 模型预测状态, fontsize14) ax2.set_xlabel(天数, fontsize12) ax2.set_ylabel(状态, fontsize12) ax2.legend(locupper right) ax2.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.show()这张图会非常清晰地展示模型的威力。第一张图里颜色点会大致将高温点和低温点区分成两类。第二张图里如果模型的预测状态曲线和真实状态曲线重合度很高只在小部分波动剧烈的地方有分歧那就说明模型学得很好。在实际应用中我们往往没有“真实状态”可以对比所以第一张图结合模型参数均值、方差的解读就至关重要。你需要根据业务知识来判断模型找到的这两个状态是否对应你心目中“晴”和“雨”的概念。有时候模型可能会根据数据分布挖掘出你没想到的状态比如“阴天”或者“特殊天气事件”这就是数据驱动的魅力。3. 进阶挑战用GMMHMM分析复杂股价模式天气的例子相对简单一个状态对应一个高斯分布。但金融市场要复杂得多。股价的涨跌背后可能由多种“市场情绪”或“资金模式”混合驱动单一的高斯分布很难刻画。比如“上涨”这个状态里可能还细分为“温和上涨”和“暴力拉升”。这时GMMHMM就派上用场了。3.1 理解GMMHMM为什么需要混合模型你可以把GMMHMM理解为GaussianHMM的“威力加强版”。在GaussianHMM里每个隐藏状态比如“牛市”只用一个“钟形曲线”高斯分布来描述所有可能的股价收益率。但在GMMHMM里每个状态可以用多个“钟形曲线”加权组合来描述。这就好比说“牛市”这个状态实际上可能是由“慢牛”一个高斯分布和“快牛”另一个高斯分布以一定比例混合而成的。模型参数里会多一个n_mix用来指定每个状态下高斯分量的个数。3.2 实战股价分析数据准备与特征工程我们以分析股票收盘价为例。直接使用原始价格序列效果往往不好因为价格非平稳。更常见的做法是使用收益率序列今日收盘价/昨日收盘价 - 1或者对数收益率作为观测值。这里我们使用对数收益率因为它具有更好的统计性质。同时我们加入成交量作为第二个观测特征因为价量关系是技术分析的核心。import pandas as pd import yfinance as yf # 一个好用的金融数据获取库 # 1. 获取数据这里以苹果股票AAPL为例你需要确保能访问相关数据源 # 注意在无法获取实时数据的环境请准备本地CSV文件用pd.read_csv读取 # 这里假设我们有一个本地文件 AAPL_historical.csv try: # 尝试从网络获取 ticker AAPL stock_data yf.download(ticker, start2020-01-01, end2023-12-31) prices stock_data[Close] volumes stock_data[Volume] except: # 如果网络不行使用本地模拟数据或文件 print(网络数据获取失败使用模拟数据演示。) np.random.seed(2024) n_days 1000 # 模拟一个带有趋势和波动的价格序列 base_price 100 returns np.random.normal(0.0005, 0.02, n_days) # 日均收益率0.05%波动2% prices base_price * np.exp(np.cumsum(returns)) # 模拟成交量通常与价格波动正相关 volumes np.random.lognormal(mean15, sigma0.5, sizen_days) * (1 np.abs(returns)*10) # 2. 计算特征对数收益率和标准化后的成交量 df pd.DataFrame({Close: prices, Volume: volumes}) df[Log_Return] np.log(df[Close] / df[Close].shift(1)) df[Volume_Change] df[Volume].pct_change() # 成交量变化率 # 3. 处理缺失值第一行会有NaN df df.dropna() # 4. 准备观测序列X两列特征 [对数收益率 成交量变化率] # 通常需要对特征进行标准化使模型训练更稳定 from sklearn.preprocessing import StandardScaler scaler StandardScaler() X scaler.fit_transform(df[[Log_Return, Volume_Change]].values) print(f观测数据形状{X.shape}) # 应该是 (n_samples, 2) print(f数据预览前5行:\n, df[[Close, Log_Return, Volume_Change]].head())现在我们的观测数据X是一个两列的数组每一行代表一天第一列是标准化后的对数收益率第二列是标准化后的成交量变化率。我们假设股价背后有3种隐藏的市场状态例如震荡、温和上涨、温和下跌并且每种状态下的收益率-成交量关系可能由2个高斯分量混合而成。3.3 构建与训练GMMHMM模型# 创建GMMHMM模型 # n_components3: 我们假设有3个隐藏市场状态 # n_mix2: 每个状态由2个高斯分布混合而成 # covariance_typefull: 每个高斯分量有自己的完整协方差矩阵能捕捉特征间的相关性如价量关系 model_gmm hmm.GMMHMM(n_components3, n_mix2, covariance_typefull, n_iter200, random_state42) # 训练模型 model_gmm.fit(X) print(GMMHMM模型训练完成) print(f迭代次数{model_gmm.monitor_.iter}) # 解码得到历史数据中每一天最可能的市场状态 hidden_states_gmm model_gmm.predict(X) df[Predicted_State_GMM] hidden_states_gmm # 查看每个状态的一些统计信息 print(\n各隐藏状态下收益率的统计描述) for i in range(3): state_data df[df[Predicted_State_GMM] i][Log_Return] print(f状态 {i}: 样本数{len(state_data):4d}, 平均收益率{state_data.mean():.6f}, 收益率标准差{state_data.std():.6f})运行后模型会输出每个状态对应的平均收益率和波动率。你可能会发现状态0的平均收益率接近0且波动小震荡市状态1的平均收益率为正上涨市状态2的平均收益率为负下跌市。这就是模型从杂乱无章的价格和成交量序列中自动识别出的“市场模式”。3.4 可视化分析与策略启发我们可以把模型识别出的状态叠加到股价走势图上看看模型划分的“市场阶段”是否合理。# 可视化股价与隐藏状态 fig, axes plt.subplots(3, 1, figsize(16, 12), sharexTrue) # 子图1股价走势与隐藏状态 ax1 axes[0] ax1.plot(df.index, df[Close], colorblack, linewidth1, label收盘价) # 用背景色标注不同的预测状态 for state in range(3): idx df[df[Predicted_State_GMM] state].index ax1.fill_between(idx, df[Close].min(), df[Close].max(), alpha0.3, labelf状态 {state}) ax1.set_ylabel(收盘价 (USD), fontsize12) ax1.set_title(股票收盘价与GMMHMM预测的市场状态, fontsize14) ax1.legend(locupper left) ax1.grid(True, linestyle--, alpha0.5) # 子图2对数收益率与隐藏状态 ax2 axes[1] colors [red, green, blue] for state in range(3): state_idx df[df[Predicted_State_GMM] state].index ax2.scatter(state_idx, df.loc[state_idx, Log_Return], ccolors[state], s10, alpha0.7, labelf状态 {state}) ax2.axhline(y0, colork, linestyle-, linewidth0.5) ax2.set_ylabel(对数收益率, fontsize12) ax2.legend() ax2.grid(True, linestyle--, alpha0.5) # 子图3状态序列本身 ax3 axes[2] ax3.step(df.index, df[Predicted_State_GMM], wheremid, linewidth1) ax3.set_ylabel(预测状态, fontsize12) ax3.set_xlabel(日期, fontsize12) ax3.set_yticks([0, 1, 2]) ax3.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.show()这张图能给你很多启发。你会看到模型识别出的状态通常会持续一段时间状态转移矩阵对角线上概率高这符合市场的“趋势”特性。在股价快速拉升或下跌的波段状态可能会发生切换。更重要的是你可以基于这个状态序列去构建简单的量化策略原型。例如一个最朴素的想法是当模型判断市场进入“状态1”上涨市时买入或持有当进入“状态2”下跌市时卖出或空仓在“状态0”震荡市时观望。当然这只是一个极其简单的示例真实的策略需要考虑交易成本、滑点、过拟合等无数细节。但HMM为你提供了一个全新的、基于数据驱动模式识别的视角。4. 处理离散事件MultinomialHMM与中文分词前面我们处理的都是连续数值。现在来看MultinomialHMM的舞台——离散观测序列。一个经典应用就是中文分词。在分词任务中观测序列是句子中的一个个汉字而隐藏状态则是每个汉字在词语中所处的位置标签通常采用{B词首, M词中, E词尾, S单字词}这四种标签。4.1 原理简述从字到词的解码其核心思想是我们将分词问题转化为一个序列标注问题。给定一个汉字序列观测序列HMM的任务是找出最可能的标签序列隐藏状态序列。模型需要学习三个概率初始状态概率π一句话开头第一个字是B、M、E、S的概率各是多少显然M和E的概率应该极低。状态转移概率A给定当前字的标签下一个字的标签是什么的概率例如B后面很可能接M或E但几乎不可能接S或B。发射概率B给定当前标签如B观察到某个特定汉字如“中”的概率是多少这需要从大量已分词语料中统计学习。训练完成后对于一个新句子模型通过维特比算法就能找出概率最大的标签序列然后根据“BME”规则就能还原出词语的边界。4.2 代码实战简易分词器搭建由于完整的分词器需要大规模的标注语料库如人民日报语料来统计三大矩阵过程较为繁琐。这里我展示一个极度简化但完整的示例帮助你理解流程。我们假设有一个微型的训练语料和对应的状态标签。import numpy as np from hmmlearn import hmm # 1. 定义状态和观测空间 states [B, M, E, S] # 隐藏状态词首词中词尾单字 n_states len(states) # 假设我们的汉字观测空间只有6个字实际是成千上万个 observations [我, 爱, 北, 京, 天, 安] n_observations len(observations) # 2. 基于一个极小规模语料“人工”定义模型参数实际应从大数据统计 # 初始概率句子开头是B或S的可能性大 start_prob np.array([0.6, 0.0, 0.0, 0.4]) # 对应 [B, M, E, S] # 状态转移概率矩阵 A # 行当前状态 列下一状态 # 例如 trans_mat[0,1]0.3 表示当前是B下一状态是M的概率是0.3 trans_mat np.array([ [0.0, 0.5, 0.5, 0.0], # B - (M, E) [0.0, 0.4, 0.6, 0.0], # M - (M, E) [0.3, 0.0, 0.0, 0.7], # E - (B, S) 一个词结束后很可能开始新词或接单字 [0.4, 0.0, 0.0, 0.6], # S - (B, S) 单字后可能接新词或另一个单字 ]) # 发射概率矩阵 B # 行状态 列观测汉字索引对应observations列表 # 例如 emit_mat[0,2]0.2 表示在状态B下观察到汉字“北”的概率是0.2 emit_mat np.array([ [0.1, 0.1, 0.3, 0.3, 0.1, 0.1], # 状态B下各汉字的概率 [0.0, 0.0, 0.4, 0.4, 0.1, 0.1], # 状态M [0.0, 0.0, 0.1, 0.1, 0.4, 0.4], # 状态E [0.4, 0.4, 0.05, 0.05, 0.05, 0.05], # 状态S ]) # 每一行的概率和需要为1 emit_mat emit_mat / emit_mat.sum(axis1, keepdimsTrue) # 3. 创建并配置MultinomialHMM模型 model_multi hmm.MultinomialHMM(n_componentsn_states, n_iter100) model_multi.startprob_ start_prob model_multi.transmat_ trans_mat model_multi.emissionprob_ emit_mat # 4. 对新句子进行分词解码 # 假设新句子是“我爱北京天安门”我们只取前四个字“我爱北京”演示 test_sentence 我爱北京 # 将句子转换为观测索引序列 obs_seq [] for char in test_sentence: try: idx observations.index(char) obs_seq.append(idx) except ValueError: # 如果字不在我们的小字典里可以给一个默认索引或特殊处理 print(f警告字符 {char} 不在观测字典中已跳过。) continue if obs_seq: obs_seq np.array(obs_seq).reshape(-1, 1) # 转换为列向量 # 使用维特比算法解码最可能的状态序列 log_prob, state_seq model_multi.decode(obs_seq, algorithmviterbi) print(f观测句子{test_sentence}) print(f观测索引{obs_seq.flatten()}) print(f解码得到的状态序列{[states[i] for i in state_seq]}) # 5. 根据状态序列进行分词 words [] word_start 0 for i, state in enumerate(state_seq): state_label states[state] if state_label B: word_start i elif state_label E: words.append(test_sentence[word_start:i1]) elif state_label S: words.append(test_sentence[i]) # 处理特殊情况如果以B或M结尾不符合规范也作为一个词 if states[state_seq[-1]] in [B, M]: words.append(test_sentence[word_start:]) print(f分词结果{ / .join(words)}) else: print(无法处理该句子。)这个例子虽然字典小、参数是手工设的但它清晰地揭示了MultinomialHMM用于分词的核心流程观测序列汉字 - 解码 - 状态序列B/M/E/S标签 - 根据规则合并成词。在实际项目中你需要用海量分词语料如“我/B 爱/E 北/B 京/E”来通过model.fit()方法自动学习startprob_,transmat_,emissionprob_这三个矩阵而不是手动设置。hmmlearn的MultinomialHMM同样提供了fit方法来完成这个无监督学习过程。5. 避坑指南与最佳实践用了这么多年hmmlearn我踩过的坑也不少。下面这些经验希望能帮你少走弯路。1. 确定隐藏状态数n_components这是玄学也是科学。没有银弹。你可以基于业务知识比如天气预测你明确知道有“晴、雨、阴”三种那就设3。使用信息准则类似AIC赤池信息准则或BIC贝叶斯信息准则。用不同状态数训练多个模型选择AIC/BIC最小的那个。hmmlearn模型训练后可以通过model.score(X)得到对数似然用于计算AIC/BIC。交叉验证如果你的目标是最佳预测性能可以将数据分成训练集和验证集在验证集上评估不同状态数模型的表现例如预测下一个观测值的准确率。从少开始如果不确定先从较小的数如2或3开始观察模型学到的状态是否有明确的物理意义。再逐步增加直到新增的状态变得难以解释或收益很小。2. 数据预处理与标准化至关重要。连续数据对于GaussianHMM和GMMHMM强烈建议对每个特征进行标准化零均值单位方差。因为模型假设数据服从高斯分布不同特征量纲差异过大会导致模型被大数值特征主导且EM算法可能收敛困难。使用sklearn.preprocessing.StandardScaler是标准操作。缺失值与异常值HMM对异常值比较敏感。在金融数据中暴涨暴跌的“黑天鹅”事件可能会被模型误认为是一个独立的“状态”。需要根据业务判断是否进行平滑或截断处理。序列长度hmmlearn的fit和predict方法可以接受一个lengths参数它是一个列表指明如何将一长串观测数据切分成多个独立的序列。这在处理多段文本、多个用户的独立行为序列时非常有用。3. 模型初始化与收敛。init_params和params参数init_params控制训练开始时初始化哪些参数如s初始概率t转移矩阵m均值c协方差。params控制训练过程中更新哪些参数。如果你对某些参数有先验知识比如从其他数据估计的转移矩阵可以将其设置好然后在params中去掉对应的字母让训练过程固定它。收敛性检查训练后务必检查model.monitor_.converged是否为True以及model.monitor_.iter是否小于n_iter。如果没收敛考虑增加n_iter或减小tol收敛阈值。随机性EM算法对初始值敏感。可以设置random_state保证结果可复现或者多次运行取最优。4. 理解模型输出与局限性。状态标签是任意的模型输出的状态0, 1, 2...没有内在含义。你需要通过分析每个状态对应的观测数据分布如model.means_来赋予其业务意义如“上涨市”、“震荡市”。HMM是生成模型它学习的是整个序列的联合概率分布。这既是优点也是缺点。优点是理论上更完备缺点是对长期依赖关系建模能力较弱因为它遵循一阶马尔可夫假设。对于非常复杂的序列模式深度学习模型如LSTM可能更具优势。预测 vs. 滤波 vs. 平滑model.predict()给出的是在给定全部观测序列下每个时刻最可能的状态维特比路径。如果你想要的是基于到当前时刻为止的观测对当前状态的估计滤波或者基于全部观测对过去状态的重新估计平滑则需要使用model.predict_proba()等方法并理解其返回的概率含义。说到底hmmlearn是一个强大而精巧的工具它将隐马尔可夫模型的复杂数学封装成了几行简单的Python代码。无论是探索数据中隐藏的节律还是为更复杂的系统构建基础模块它都能提供独特的价值。关键在于你要清楚地知道你的数据是什么你的假设是什么然后让模型去验证或发现那些隐藏的故事。