简介:本资源为哈尔滨工业大学2023年秋季《计算建模》课程配套实验包,面向计算机、数学、自动化等相关专业本科生及建模初学者,聚焦数学建模→算法设计→编程实现的全链路实践训练。压缩包共17个文件(1.3MB),含10个Python源码文件(覆盖Viterbi解码、HMM建模、EM算法、FFT/DCT图像处理、中值滤波去噪等典型实验)、2张PNG/TIFF格式示例图像、1份Excel实验数据、1个CSV结果文件、1份Markdown说明书(README.md)及1个TIFF原始图像,类型分布体现“理论-代码-数据-结果-文档”闭环结构。已有237人学习下载,资源开放可修改,提供完整实验任务框架、可运行代码与清晰说明,便于读者理解隐马尔可夫模型、随机模拟、参数拟合、图像频域处理等核心知识点,并通过动手调试深化对算法原理与工程实现间关系的认知。
1. 这不是一份“交完就扔”的课程实验包,而是哈工大计算建模课里真正能跑通HMM参数学习与序列解码的实操入口
如果你在搜索“哈工大2023秋计算建模实验”时点开这个压缩包,却发现解压后只有几个.py文件和一份PDF说明书——别急着关掉。它不是模板作业,而是一套完整可复现的隐马尔可夫模型(HMM)教学实现:从观测序列生成、初始参数设定,到用EM算法迭代估计转移/发射概率,再用Viterbi算法回溯最优隐状态路径。整个流程不依赖任何黑盒库(如hmmlearn),所有矩阵运算、对数似然计算、前向-后向递推都手写Python实现。适合两类人:一是刚学完《数值分析》想验证HMM中矩阵迭代收敛性的哈工大学生;二是需要快速搭建可调试HMM基线、理解EM收敛行为或Viterbi剪枝逻辑的算法工程师。它不追求工程封装,但每行代码都对应教材公式——比如EM_algorithm.py里gamma和xi的更新逻辑,直接映射到《统计学习方法》第10章的推导步骤。
2. 为什么用纯NumPy重写HMM核心算法?从哈工大课程设计目标看选型逻辑
哈工大计算建模课程强调“可解释性优先于封装性”,这决定了本实验不采用scikit-learn或PyTorch等高层框架。当学生需要调试EM算法中E步的后向概率溢出、M步的归一化失效,或Viterbi路径回溯时索引越界,黑盒API只会返回ValueError,而手写实现能让你在print()里看到每一帧的alpha值、每轮的log_likelihood变化。这种设计直指HMM教学中的三个关键断点:
- 数值稳定性陷阱:原始概率连乘易下溢,必须转为log-space运算;
- 边界条件混淆:Viterbi初始化时
delta[0]应取log(π_i) + log(b_i(o_0)),而非直接乘; - EM收敛判据误设:用参数差值而非对数似然增量判断收敛,会导致过早终止。
本实验包通过HMM.py定义基础类、Viterbi.py专注解码、EM_algorithm.py分离E/M步,形成清晰职责边界。所有函数签名强制接收log_space=True参数,默认启用对数运算——这是哈工大数值分析课反复强调的“避免浮点灾难”实践。
2.1 HMM类的结构设计:为什么forward_log比forward更适合作为教学基线
HMM.py中forward_log函数是整个流程的数值锚点。它不返回原始α_t(i),而是返回log_alpha[t][i],其递推式为:
log_alpha[t][i] = logsumexp( [log_alpha[t-1][j] + np.log(A[j][i]) for j in range(N)] ) + np.log(B[i][O[t]])提示:
logsumexp是关键——它先减去最大值再指数求和再加回,避免exp(-1000)导致的0.0。哈工大数值分析课中“防止下溢的补偿技巧”在此直接落地。
对比传统forward实现(需处理0.0除零),forward_log天然规避了三类错误:
- 初始时刻
t=0时log(0)报错(因π_i或B_i(o_0)为0); t>0时sum(α_{t-1}·A)结果为0导致后续log(0);- 多次迭代后α值趋近机器精度下限(~1e-308)引发NaN传播。
实验说明书中明确要求:所有概率运算必须经过np.log和logsumexp封装。这不是代码洁癖,而是哈工大保研面试中常被追问的“如何保证HMM训练数值鲁棒性”的标准答案。
2.2 Viterbi算法的路径回溯陷阱:为什么psi数组必须用整数索引而非状态名
Viterbi.py中viterbi_decode函数的psi数组定义为psi[t][i] = argmax_j (delta[t-1][j] + log(A[j][i])),其数据类型为int而非str。这个细节常被忽略,却直接影响哈工大实验验收——当隐状态集为['Sunny', 'Rainy']时,若psi存字符串,回溯时无法用psi[t][i]作为delta[t-1]的索引(因delta是数值数组)。正确做法是:
- 将状态映射为
{0:'Sunny', 1:'Rainy'}; psi[t][i]存储整数j(即上一时刻最优前驱状态编号);- 回溯时用
path[t] = psi[t+1][path[t+1]],最后用映射表转换为可读名。
# 正确回溯逻辑(摘自Viterbi.py) path = [0] * T path[-1] = np.argmax(delta[-1]) for t in range(T-2, -1, -1): path[t] = psi[t+1][path[t+1]] # psi[t+1]是t+1时刻记录的t时刻最优前驱 return [state_names[i] for i in path]注意:
psi维度为(T, N),但psi[0]无意义(首时刻无前驱),实际只用psi[1:T]。实验说明书中要求打印psi中间值验证,正是为排查此索引偏移错误。
3. 用真实观测序列跑通EM-Viterbi全流程:从数据生成到参数收敛验证
本实验包自带generate_data.py脚本,可按指定π、A、B生成带标签的观测序列。但真正体现哈工大计算建模深度的是参数学习闭环验证:用生成数据训练模型,再对比学习出的A/B与真值的Frobenius范数误差。以下是在本地复现的最小命令链:
3.1 生成带标签的训练数据(含隐状态真值)
python generate_data.py \ --n_states 3 \ --n_obs 4 \ --seq_len 100 \ --n_samples 50 \ --output_dir ./data/该命令生成50条长度为100的观测序列(数字0~3),同时保存对应隐状态序列(0~2)到./data/true_states.npy。关键参数说明:
--n_states:隐状态数,影响HMM类初始化时的N;--n_obs:观测符号数,决定发射矩阵B的列数;--seq_len:单条序列长度,过短会导致EM收敛震荡(哈工大实验要求≥50);--n_samples:训练样本数,少于20时EM易陷入局部最优。
生成的数据结构为numpy.ndarray,形状(50, 100),直接喂给EM_algorithm.py的train函数。
3.2 执行EM算法并监控收敛过程
from EM_algorithm import EMTrainer from HMM import HMM # 加载数据 X = np.load('./data/observed_sequences.npy') # shape: (50, 100) true_A = np.load('./data/true_transition.npy') # 真值,用于对比 # 初始化HMM(随机π,A,B) hmm = HMM(n_states=3, n_obs=4, log_space=True) trainer = EMTrainer(hmm=hmm, max_iter=100, tol=1e-4) # 训练并获取每轮log_likelihood log_likelihoods, A_history, B_history = trainer.train(X) # 验证收敛:检查最后10轮log_likelihood波动是否<tol converged = np.std(log_likelihoods[-10:]) < 1e-5 print(f"EM converged: {converged}, final LL: {log_likelihoods[-1]:.4f}")提示:
tol=1e-4是哈工大实验默认阈值,但若log_likelihoods出现平台期后突然下降,说明E步中xi计算有误(常见于未用logsumexp处理分母)。
3.3 用Viterbi解码并评估状态预测准确率
训练完成后,对同一批数据做解码:
# 加载真值隐状态 true_states = np.load('./data/true_states.npy') # shape: (50, 100) # 解码预测 pred_states = [] for seq in X: pred = hmm.viterbi_decode(seq) # 返回list[int] pred_states.append(pred) pred_states = np.array(pred_states) # (50, 100) # 计算整体准确率(哈工大验收硬指标) accuracy = np.mean(pred_states == true_states) print(f"Viterbi accuracy: {accuracy:.4f}")若准确率低于0.7,需检查:
Viterbi.py中delta[0]是否用了log(π_i) + log(B[i][o_0]);psi数组是否在t=0时未赋值(应跳过);logsumexp是否对delta[t-1] + log(A[:,i])整体操作(而非逐元素)。
4. 哈工大保研面试高频考点:EM算法中E步的xi矩阵为何必须用log-space重写
在EM_algorithm.py的e_step函数中,xi[t][i][j]表示在t时刻处于状态i、t+1时刻转移到状态j的概率。其原始公式为:
xi[t][i][j] = alpha[t][i] * A[i][j] * B[j][o_{t+1}] * beta[t+1][j] / P(O|λ)但直接实现会因alpha/beta下溢导致xi全零。哈工大计算建模课要求将其转为log-space:
log_xi = ( log_alpha[t][i] + np.log(A[i][j]) + np.log(B[j][O[t+1]]) + log_beta[t+1][j] - log_likelihood ) xi[t][i][j] = np.exp(log_xi) # 最终才转回概率这里log_likelihood是forward_log返回的总对数似然。面试官常追问:“如果log_xi小于-700,np.exp(log_xi)会是0.0,此时xi矩阵稀疏,M步更新会失效——如何避免?”
答案是:在M步中不直接用xi,而用其log-sum形式更新A:
# M步更新转移矩阵A[i][j] log_numerator = logsumexp([ log_xi[t][i][j] for t in range(T-1) ]) log_denominator = logsumexp([ log_gamma[t][i] for t in range(T-1) ]) A[i][j] = np.exp(log_numerator - log_denominator)其中log_gamma[t][i] = logsumexp([log_xi[t][i][j] for j in range(N)])。这种写法将数值问题完全隔离在log-space内,直到最后一步才指数化——这正是哈工大数值分析课强调的“延迟指数化”原则。
4.1 三个必调参数:max_iter、tol、log_space的协同影响
| 参数 | 典型值 | 调整逻辑 | 哈工大实验约束 |
|---|---|---|---|
max_iter | 50~100 | 过小导致未收敛;过大增加耗时但不提升精度 | ≥80(说明书明确要求) |
tol | 1e-4~1e-6 | 过大易早停;过小在噪声数据中引发震荡 | 1e-4(保研面试常考此值合理性) |
log_space | True | 关闭则必然失败(测试集已预设下溢场景) | 必须为True,否则forward_log报错 |
当tol=1e-6且max_iter=200时,若log_likelihoods在第150轮后波动<1e-7,说明模型已稳定;但若第180轮突降,大概率是logsumexp实现有缺陷(如未减去最大值)。
5. 修改源码的实操技巧:如何安全添加高斯发射概率以适配连续观测
原实验包仅支持离散观测(B[i][k]为状态i发射符号k的概率)。若需处理哈工大数值分析课中的温度序列(连续值),需将HMM.py中的发射矩阵B替换为高斯分布参数:每个状态i对应均值mu[i]和方差sigma2[i]。修改要点如下:
5.1 在HMM类中扩展_gaussian_emission_log方法
def _gaussian_emission_log(self, state_i, obs_val): """计算状态i发射连续观测obs_val的log概率""" return ( -0.5 * np.log(2 * np.pi * self.sigma2[state_i]) - 0.5 * ((obs_val - self.mu[state_i]) ** 2) / self.sigma2[state_i] )注意:self.mu和self.sigma2需在__init__中初始化为np.random.randn(n_states)和np.random.rand(n_states)。
5.2 改写forward_log中的观测概率项
原离散版:
log_alpha[t][i] += np.log(self.B[i][O[t]])改为连续版:
log_alpha[t][i] += self._gaussian_emission_log(i, O[t])提示:
O[t]此时为float而非int,需确保输入数据为np.float64。哈工大实验说明书中要求“连续观测需先标准化”,即O = (O - O.mean()) / O.std(),否则mu/sigma2更新会发散。
5.3 EM算法中M步的高斯参数更新公式
在EM_algorithm.py的m_step中,新增:
# 更新高斯均值mu[i] numerator = sum( gamma[t][i] * O[t] for t in range(T) ) denominator = sum(gamma[t][i] for t in range(T)) self.hmm.mu[i] = numerator / denominator # 更新方差sigma2[i] numerator = sum( gamma[t][i] * (O[t] - self.hmm.mu[i]) ** 2 for t in range(T) ) self.hmm.sigma2[i] = numerator / denominator此处gamma[t][i]仍来自log-space的log_gamma,但sum()操作在概率空间进行(需先np.exp(log_gamma[t][i]))。哈工大保研面试曾以此题考察“如何在log-space中安全执行加权平均”。
本文还有配套的精品资源,点击获取