别再死记硬背了!用Python代码和可视化动画,5分钟搞懂MCMC采样到底在干什么
用Python动画拆解MCMC:从随机游走到双峰分布采样
在机器学习和统计建模中,我们经常遇到需要从复杂概率分布中采样的场景。传统方法面对非标准分布时往往束手无策,而马尔可夫链蒙特卡洛(MCMC)方法则提供了优雅的解决方案。本文将通过Python代码和动态可视化,带你直观理解MCMC的核心思想——如何让随机游走收敛到目标分布。
1. 蒙特卡洛方法基础
蒙特卡洛方法本质是通过随机采样来近似计算数值结果。假设我们需要计算函数f(x)在区间[a,b]上的积分:
import numpy as np def monte_carlo_integrate(f, a, b, n_samples=10000): samples = np.random.uniform(a, b, n_samples) return (b - a) * np.mean(f(samples))这种方法在均匀分布时效果不错,但当目标分布不均匀时,简单的均匀采样效率低下。我们需要重要性采样技术——在概率密度高的区域采集更多样本:
def importance_sampling(f, p, q, n_samples=10000): # p: 目标分布,q: 建议分布 samples = q.rvs(n_samples) weights = p(samples) / q.pdf(samples) return np.mean(f(samples) * weights) / np.mean(weights)接受-拒绝采样是另一种思路,它通过一个包络函数来生成样本:
def accept_reject(p, M, q, n_samples=10000): samples = [] while len(samples) < n_samples: x = q.rvs() u = np.random.uniform(0, M*q.pdf(x)) if u <= p(x): samples.append(x) return np.array(samples)这些传统方法虽然有效,但在高维空间或复杂分布下效率急剧下降。下表对比了不同采样方法的适用场景:
| 方法 | 优点 | 缺点 | 适用维度 |
|---|---|---|---|
| 均匀采样 | 实现简单 | 效率低 | 低维 |
| 重要性采样 | 方差减小 | 依赖建议分布 | 中低维 |
| 接受-拒绝 | 精确采样 | 接受率低 | 低维 |
| MCMC | 高维有效 | 需要预热 | 所有维度 |
2. 马尔可夫链的魔法
马尔可夫链的核心性质是无记忆性——下一状态只依赖当前状态。这种特性使得它能够逐渐"忘记"初始分布,收敛到平稳分布。让我们用股市状态转移的例子来说明:
transition_matrix = np.array([ [0.9, 0.075, 0.025], # 牛市→牛市/熊市/横盘 [0.15, 0.8, 0.05], # 熊市→... [0.25, 0.25, 0.5] # 横盘→... ]) def simulate_markov_chain(transition, initial, n_steps=100): states = [initial] for _ in range(n_steps): states.append(np.random.choice( len(transition), p=transition[states[-1]] )) return states通过动画演示,我们可以观察到无论从牛市、熊市还是横盘开始,经过足够多的状态转移后,系统都会收敛到相同的稳态分布。这正是MCMC采样的理论基础——构造一个马尔可夫链,使其平稳分布就是我们的目标分布。
3. Metropolis-Hastings算法实战
M-H算法是MCMC的典型实现,它通过建议分布和接受概率的巧妙设计,确保马尔可夫链收敛到目标分布。让我们用Python实现一个完整的M-H采样器:
def metropolis_hastings(p, q, q_sample, n_samples=10000, burn_in=1000): samples = np.zeros(n_samples + burn_in) current = q_sample() # 初始状态 for i in range(len(samples)): # 从建议分布生成候选样本 candidate = q_sample(current) # 计算接受概率 acceptance = min(1, p(candidate)*q(current, candidate)/(p(current)*q(candidate, current))) # 决定是否接受 if np.random.rand() < acceptance: current = candidate samples[i] = current return samples[burn_in:] # 去除预热期样本为了直观展示采样过程,我们创建一个双峰分布作为目标分布:
def bimodal_distribution(x): return 0.3*np.exp(-0.2*(x-2)**2) + 0.7*np.exp(-0.2*(x+2)**2) # 建议分布:以当前点为中心的正态分布 def proposal_distribution(x, sigma=1): return np.random.normal(x, sigma) # 生成采样动画 def animate_mh_sampling(p, n_frames=200): fig, ax = plt.subplots() x = np.linspace(-6, 6, 1000) ax.plot(x, p(x), 'r-', lw=2, label='Target Distribution') current = np.random.randn() samples = [] line, = ax.plot([], [], 'b-', alpha=0.5) scat = ax.scatter([], [], c='g', s=50) def update(frame): nonlocal current candidate = proposal_distribution(current) acceptance = min(1, p(candidate)/p(current)) if np.random.rand() < acceptance: current = candidate samples.append(current) scat.set_offsets([[current, 0]]) line.set_data(samples, 0.1*np.ones_like(samples)) return line, scat anim = FuncAnimation(fig, update, frames=n_frames, blit=True) plt.close() return anim通过动画可以清晰看到,采样点最初随机游走,但逐渐在概率密度高的区域聚集,最终完美拟合目标分布的形状。
4. 收敛诊断与调优
MCMC采样需要特别注意收敛诊断。常用的方法包括:
- 轨迹图:观察采样序列是否达到稳定波动
- 自相关图:检查样本之间的相关性衰减速度
- Gelman-Rubin诊断:多链运行比较方差
def plot_convergence(samples): fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) # 轨迹图 ax1.plot(samples, alpha=0.5) ax1.set_title('Trace Plot') # 自相关图 from statsmodels.graphics.tsaplots import plot_acf plot_acf(samples, lags=50, ax=ax2) ax2.set_title('Autocorrelation') plt.tight_layout()实际应用中,我们还需要调整建议分布的宽度。太窄会导致探索不足,太宽则接受率下降。经验上,接受率在20-50%之间通常效果较好:
def optimize_proposal(p, initial_sigma=1, target_acceptance=0.3, n_trials=1000): sigma = initial_sigma for _ in range(10): # 调整轮次 samples, acceptances = [], [] current = np.random.randn() for _ in range(n_trials): candidate = np.random.normal(current, sigma) alpha = min(1, p(candidate)/p(current)) accept = np.random.rand() < alpha acceptances.append(accept) current = candidate if accept else current samples.append(current) acceptance_rate = np.mean(acceptances) sigma *= 1 + 0.5*(acceptance_rate - target_acceptance) # 动态调整 return sigma, samples5. 多维扩展与Gibbs采样
当面对高维分布时,M-H算法需要调整。Gibbs采样是一种特殊情况的M-H算法,它通过轮流更新每个维度来简化采样过程:
def gibbs_sampling(conditional_dists, n_samples=10000, burn_in=1000): dim = len(conditional_dists) samples = np.zeros((n_samples + burn_in, dim)) current = np.random.randn(dim) # 初始状态 for i in range(len(samples)): for j in range(dim): # 从条件分布采样 current[j] = conditional_dists[j](current, i) samples[i] = current return samples[burn_in:]例如,对于二维正态分布,条件分布仍然是正态分布,可以高效采样:
def bivariate_normal_conditional(x, index): # 假设相关系数rho=0.5 if index == 0: # x1|x2 return np.random.normal(0.5*x[1], np.sqrt(0.75)) else: # x2|x1 return np.random.normal(0.5*x[0], np.sqrt(0.75))Gibbs采样特��适合各维度之间有较强相关性的情况,因为它能利用条件独立性质大幅提高效率。
6. 实际应用技巧
在真实项目中应用MCMC时,有几个实用建议:
- 预热期处理:通常丢弃前10-20%的样本作为预热
- 稀疏采样:每隔k个样本保留一个,减少自相关
- 多链运行:从不同初始点启动多个链检查收敛
- 参数化技巧:对受限空间使用logit变换等技巧
def run_mcmc_in_practice(p, n_chains=4, n_samples=10000): chains = [] for _ in range(n_chains): chain = metropolis_hastings( p, q=lambda x,y: np.exp(-0.5*(y-x)**2), # 正态建议 q_sample=lambda x=None: np.random.randn() if x is None else np.random.normal(x), n_samples=n_samples, burn_in=n_samples//5 ) chains.append(chain[::5]) # 稀疏采样 return np.concatenate(chains)可视化多链运行结果可以帮助我们确认收敛:
def plot_multiple_chains(chains): plt.figure(figsize=(10, 6)) for i, chain in enumerate(chains): plt.plot(chain, alpha=0.5, label=f'Chain {i+1}') plt.title('Multiple Chain Trace Plot') plt.legend()7. 进阶主题与性能优化
对于更复杂的场景,我们可以考虑以下优化策略:
- 自适应MCMC:在预热期动态调整建议分布参数
- 哈密尔顿蒙特卡洛(HMC):利用梯度信息提高效率
- 并行化:对独立成分使用并行采样
HMC的实现需要更多数学工具,但可以显著提升在高维空间的探索效率:
def hamiltonian_monte_carlo(p, grad_p, step_size, n_steps, n_samples=10000): # p: 目标分布,grad_p: 其梯度 samples = [] current_q = np.random.randn() for _ in range(n_samples): q = current_q p = np.random.randn() # 辅助动量变量 current_p = p # 蛙跳积分 p -= step_size * grad_p(q) / 2 for _ in range(n_steps): q += step_size * p p -= step_size * grad_p(q) p -= step_size * grad_p(q) / 2 # 接受/拒绝 current_U = -np.log(p(current_q)) current_K = 0.5 * current_p**2 proposed_U = -np.log(p(q)) proposed_K = 0.5 * p**2 if np.random.rand() < np.exp(current_U - proposed_U + current_K - proposed_K): current_q = q samples.append(current_q) return np.array(samples)在实际项目中,推荐使用成熟库如PyMC3或Stan,它们实现了这些高级算法并提供了友好的API:
import pymc3 as pm with pm.Model(): # 定义模型 theta = pm.Normal('theta', mu=0, sigma=1) obs = pm.Normal('obs', mu=theta, sigma=1, observed=data) # 运行采样 trace = pm.sample(1000, tune=1000, cores=4) # 可视化结果 pm.plot_trace(trace)通过本文的代码示例和动画演示,相信你已经对MCMC如何通过随机游走探索复杂分布有了直观理解。记住,MCMC的核心价值在于它能够处理传统方法难以应对的高维、非标准分布问题,这使它成为现代统计建模和机器学习中不可或缺的工具。
