别再死记硬背公式了!用Python+NumPy手把手模拟MCMC采样(附完整代码)
用Python+NumPy实战MCMC采样:从零构建马尔科夫链蒙特卡洛模拟器
当概率分布的维度超过三维时,传统的采样方法就会像在迷宫中盲目行走——你永远不知道下一个拐角会遇到什么。这正是马尔科夫链蒙特卡洛(MCMC)方法大显身手的时刻。本文将带你用Python和NumPy从零搭建一个MCMC采样器,通过动态可视化观察马尔科夫链如何像智慧探险者般逐步探索并最终锁定目标分布的核心区域。
1. 环境配置与基础概念
在开始编写代码前,我们需要确保环境准备就绪。推荐使用Anaconda创建专属的Python 3.8+环境:
conda create -n mcmc_demo python=3.8 numpy matplotlib pandas conda activate mcmc_demoMCMC的核心在于两个关键组件:
- 马尔科夫链:下一个状态仅取决于当前状态的记忆特性
- 蒙特卡洛:通过随机采样逼近目标分布的统计方法
二者的结合创造了一种智能随机游走策略,能够高效探索复杂概率空间。下面是我们将要实现的关键参数对照表:
| 参数名称 | 数学符号 | 作用描述 | 典型取值 |
|---|---|---|---|
| 燃烧期 | burn-in | 丢弃的初始采样数 | 500-2000 |
| 采样间隔 | lag | 减少自相关的采样间隔 | 5-20 |
| 提议分布标准差 | σ | 控制新样本的探索范围 | 0.1-2.0 |
| 目标分布 | π(x) | 需要采样的未知分布 | 自定义函数 |
| 当前状态 | xₜ | 马尔科夫链的当前位置 | 随机初始化 |
2. Metropolis-Hastings算法实现
作为最经典的MCMC算法,Metropolis-Hastings只需要目标分布的点值计算能力。让我们分解其实现步骤:
import numpy as np import matplotlib.pyplot as plt def target_dist(x): """混合高斯分布示例""" return 0.3*np.exp(-0.2*x**2) + 0.7*np.exp(-0.2*(x-4)**2) def metropolis_hastings(n_samples, sigma=1.0): samples = [] x = np.random.randn() # 初始状态 for _ in range(n_samples): # 从提议分布生成候选样本 x_candidate = x + sigma * np.random.randn() # 计算接受概率 acceptance_ratio = target_dist(x_candidate) / target_dist(x) alpha = min(1, acceptance_ratio) # 决定是否接受新样本 if np.random.rand() < alpha: x = x_candidate samples.append(x) return np.array(samples)注意:提议分布的标准差σ需要仔细调整——太大导致拒绝率高,太小则探索效率低下。建议通过实验找到20-40%接受率的σ值。
算法运行时,我们可以观察到三个典型阶段:
- 探索期:链在状态空间快速移动寻找高概率区域
- 过渡期:开始在高概率区域附近振荡
- 稳定期:在目标分布的主要模式周围平稳采样
3. 采样过程可视化与分析
动态展示采样过程能直观理解MCMC的工作原理。我们使用Matplotlib创建动画:
from matplotlib.animation import FuncAnimation def animate_chain(samples): fig, ax = plt.subplots(figsize=(10,6)) x_range = np.linspace(-3, 7, 200) ax.plot(x_range, target_dist(x_range), 'r-', lw=2, label='目标分布') line, = ax.plot([], [], 'b-', alpha=0.3) point, = ax.plot([], [], 'bo', alpha=0.5) ax.legend() def update(frame): line.set_data(samples[:frame+1], np.zeros(frame+1)) point.set_data(samples[frame], 0) return line, point ani = FuncAnimation(fig, update, frames=len(samples), interval=50) plt.close() return ani运行采样并保存动画:
samples = metropolis_hastings(500) ani = animate_chain(samples) ani.save('mcmc_walk.gif', writer='pillow', fps=20)分析采样质量时,需要关注以下诊断指标:
- 自相关图:检查样本间的相关性衰减速度
- 轨迹图:观察链是否充分探索状态空间
- Gelman-Rubin统计量:多链运行时的收敛诊断
4. 高级技巧与性能优化
基础实现后,我们可以通过以下策略提升采样效率:
4.1 自适应提议分布
动态调整提议分布的参数以提高接受率:
def adaptive_mh(n_samples, initial_sigma=1.0, target_rate=0.3): sigma = initial_sigma samples = [] x = np.random.randn() accept_count = 0 for i in range(n_samples): x_candidate = x + sigma * np.random.randn() alpha = min(1, target_dist(x_candidate)/target_dist(x)) if np.random.rand() < alpha: x = x_candidate accept_count += 1 samples.append(x) # 每100次迭代调整sigma if i > 0 and i % 100 == 0: current_rate = accept_count / 100 sigma *= 1 + 0.5*(current_rate - target_rate) accept_count = 0 return np.array(samples)4.2 并行链与收敛诊断
同时运行多条链并计算R-hat统计量:
def run_multiple_chains(n_chains, n_samples): chains = [] for _ in range(n_chains): chain = metropolis_hastings(n_samples) chains.append(chain) # 计算R-hat chain_means = [np.mean(c) for c in chains] chain_vars = [np.var(c, ddof=1) for c in chains] W = np.mean(chain_vars) B = n_samples * np.var(chain_means, ddof=1) var_plus = (n_samples-1)/n_samples * W + B/n_samples rhat = np.sqrt(var_plus / W) return chains, rhat4.3 处理多峰分布
对于具有多个模态的复杂分布,需要特殊策略:
- 温度调节:使用回火技术增强探索能力
- 模式跳跃:专门设计在模式间跳转的提议分布
- 并行回火:同时运行不同温度的多个链并交换状态
def tempered_transition(x, sigma, temp): # 高温状态更容易跨越能垒 x_candidate = x + sigma * np.random.randn() log_alpha = (1/temp)*np.log(target_dist(x_candidate)) - (1/temp)*np.log(target_dist(x)) return x_candidate if np.log(np.random.rand()) < log_alpha else x5. 实际应用案例:贝叶斯逻辑回归
将我们的MCMC采样器应用于真实统计模型:
from scipy.special import expit def logistic_likelihood(beta, X, y): p = expit(X @ beta) return np.prod(p**y * (1-p)**(1-y)) def log_prior(beta): return -0.5 * np.sum(beta**2) # 高斯先验 def log_posterior(beta, X, y): return np.log(logistic_likelihood(beta, X, y)) + log_prior(beta) def bayes_logistic_mcmc(X, y, n_samples): p = X.shape[1] beta = np.zeros(p) samples = [] for _ in range(n_samples): beta_candidate = beta + 0.1 * np.random.randn(p) log_alpha = log_posterior(beta_candidate, X, y) - log_posterior(beta, X, y) if np.log(np.random.rand()) < log_alpha: beta = beta_candidate samples.append(beta.copy()) return np.array(samples)在这个实现中,我们观察到:
- 后验分布的维度等于特征数量
- 需要针对不同尺度的参数调整提议分布
- 可以引入变量变换改善采样效率
运行示例:
# 生成模拟数据 np.random.seed(42) X = np.random.randn(100, 3) true_beta = np.array([1.5, -2.0, 0.5]) y = (np.random.rand(100) < expit(X @ true_beta)).astype(int) # 运行MCMC samples = bayes_logistic_mcmc(X, y, 5000) print("后验均值:", np.mean(samples[1000:], axis=0))MCMC采样在贝叶斯分析中展现出独特优势——不需要复杂的解析推导,只需指定模型和先验就能获得完整的后验分布信息。当我在实际项目中处理带有层次结构的复杂模型时,这种"计算代替推导"的方法大大缩短了开发周期。
