十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手写MCMC采样器:从原理到实战的贝叶斯推断指南

手写MCMC采样器:从原理到实战的贝叶斯推断指南 简介贝叶斯推断作为现代统计与机器学习中处理不确定性问题的核心框架其关键挑战在于高维积分求解归一化常数往往无法解析完成。马尔可夫链蒙特卡洛MCMC方法通过构造平稳分布等于目标后验的随机游走以采样替代积分让统计推断得以绕开这一障碍。Metropolis-Hastings算法是MCMC家族中最基础的成员它借助提议分布与接受准则在参数空间中高效探索即使面对多峰分布和复杂层次模型也能有效逼近后验特征。在实际工程应用中从参数估计到不确定性量化从变点检测到预测分布生成MCMC都发挥着不可替代的作用。本文从零实现了一个纯Python的Metropolis-Hastings采样器结合双峰混合高斯和线性回归后验采样案例讲解了细致平衡原理、收敛诊断、有效样本量以及调参与数值稳定性等实战技巧帮助读者真正理解贝叶斯采样的底层机制。 干数据分析这行迟早会撞上一堵墙贝叶斯模型建好了先验也选了看起来一切设计都很完美然后你发现后验分布的分母根本算不出来。不是能力问题是那个归一化常数涉及的高维积分在绝大多数真实模型里压根没有解析解。马尔可夫链蒙特卡洛MCMC算法就是专门来解决这个问题的一套采样框架它让你不必求出那个积分也能从后验分布中抽出样本做统计推断。我第一次接触MCMC是在做一个带先验约束的参数估计任务当时用了不少现成库但它内部到底怎么在高维空间里探索的我一直是半懂不懂。直到我决定不依赖PyMC、STAN这些框架纯手写一遍Metropolis-Hastings采样器之后才真正理解了贝叶斯推断和采样之间的关系。这篇文章把我从零实现MCMC的完整过程、原理拆解、收敛判断和实战踩坑都放出来适合那些用了贝叶斯库但想搞懂底层机制的人也适合需要在Python里自定义采样的研究工程师和量化建模岗。1. 为什么贝叶斯推断离了MCMC就跑不动贝叶斯推断的核心公式其实非常简洁后验分布等于似然乘以先验再除以边缘似然。用数学语言写出来就是 p(θ∣X)p(X∣θ)p(θ)/p(X)。这里分母 p(X)∫p(X∣θ)p(θ)dθ 看起来只是积个分但真正算起来你会发现参数空间维度稍微高一点这个积分就变成天文数字级别的计算量。1.1 后验分布的分母是那只房间里的大象举个具体例子假设你有一个只有五个参数的模型每个参数空间只离散化为100个网格点那么总的状态数就是100 的5次方也就是10的10次方。这还只是5个参数如果你做的是某种分层贝叶斯模型参数动不动二三十个网格法直接失去意义。更重要的是即使你把网格画得再密遇到参数空间比较高的维度网格法需要的样本数量是指数增长的这个就是所谓的“维度灾难”。数值积分方法在低维时还能凑合比如高斯求积在二维三维问题里表现得很好。但到了高维空间要么需要极其精巧的采样点设计要么等待时间长得不现实。这就是MCMC进入视野的原因它不直接计算高维积分而是构造一个转移规则让马尔可夫链在状态空间中游走使得链的平稳分布恰好等于我们的目标后验分布。一旦链收敛我们就把游走过程中记录的样本当作后验的近似抽样结果然后在这些样本上计算均值、分位数、可信区间替代了那个棘手的积分。1.2 MAP只能给一个点给不了分布很多人刚开始做贝叶斯时有个误区既然分母不好算那我直接用优化算法找使后验最大的参数不就行了吗这个做法在工程上确实常用叫做最大后验估计MAP很多工业级模型里面也用它来做近似推断。但MAP有一个天然缺陷它只返回一个点估计。它无法告诉你“参数大概落在哪个区间里”也无法告诉你“参数与参数之间的联合分布长什么样”。我在实际工作中吃过这个亏。之前在做时序数据的变点检测时模型的似然函数比较复杂直接优化MAP跑得很快出来的参数看起来也很合理。但当我需要评估“变点位置的置信程度有多高”时MAP就完全帮不上忙了。变点位置是一个离散点MAP只能告诉你“最可能的点是第47个位置”但无法告诉你“第40到第55个位置的可信度分别是多少”。只有对后验分布进行完整的采样才能回答这种问题。所以当你的分析目标不仅仅是“拿到一个预测值”而是“量化不确定性”MCMC就是绕不开的一个工具。1.3 MCMC的中心思想随机游走骗过积分MCMC有两个组成部分马尔可夫链和蒙特卡洛方法。马尔可夫链赋予我们一个带记忆的随机游走规则每一步的状态只依赖当前状态不依赖更早的历史。蒙特卡洛则告诉我们当我们有一堆服从某个分布的样本时可以用样本均值来估计期望。结合起来MCMC就是设计一个马尔可夫链让它在状态空间里逛来逛去但逛得非常有偏向性——在目标分布概率密度高的地方多停留密度低的地方少停留。链条最终达到平稳后它访问每个区域的比例就近似等于该区域的概率质量。这种思想非常反直觉的地方在于我们不需要知道目标密度具体等于多少只需要知道“任意两个状态之间的相对概率”就够了。因为链的转移规则看的是比值而不是绝对值那个难以计算的归一化常数在比值中被约掉了。这一点是整个MCMC家族的基石后面实现的时候我们会反复用到。2. MCMC的内核马尔可夫链的平稳分布与细致平衡要真正理解Metropolis-Hastings算法M-H算法的工作原理需要回到马尔可夫链的平稳分布理论。我刚开始学的时候直接看算法伪代码感觉就是在“瞎猜、随机接受、继续猜”直到自己推导了一遍细致平衡条件才明白这背后有一套非常漂亮的数学支撑。2.1 细致平衡条件为什么是MCMC的基石一个定义在连续状态空间上的马尔可夫链由一个转移核 T(θ|θ) 描述它表示当前状态为 θ 时下一步跳到 θ 的概率密度。如果存在一个分布 π(θ)使得对于任意两个状态 θ 和 θ下面的等式成立π(θ) T(θ|θ) π(θ) T(θ|θ)那么这个 π 就是这个马尔可夫链的平稳分布这就是细致平衡条件。直观理解就是在平稳状态下从 θ 流到 θ 的概率流量恰好等于从 θ 流回 θ 的概率流量。两边抵消了所以整个分布的形状保持稳定。问题来了我们手里只有目标分布 π(θ)但 π(θ) 本身不会告诉我们怎么构造转移核。M-H算法的巧妙之处在于它先随便设一个简单的提议分布 q(θ|θ)比如高斯分布然后再引入一个接受概率 A(θ|θ)使得新的有效转移核 T q × A 满足细致平衡条件。这样一来我们不需要精心设计复杂的转移规则只需在“随意提随机游走”的基础上加上一个修正项就能保证链最终收敛到目标分布。2.2 从细致平衡反推接受率假设我们已经有提议分布 q(θ|θ)并定义接受概率 A(θ|θ)。我们希望π(θ) q(θ|θ) A(θ|θ) π(θ) q(θ|θ) A(θ|θ)如果右边的接受率较低我们可以定义 A(θ|θ) min(1, [π(θ)q(θ|θ)] / [π(θ)q(θ|θ)])。代入上式会发现无论从哪个方向看接受率都会被 min 函数中的“1”截断细致平衡条件自然成立。这里最关键的观察是公式里 π(θ) 和 π(θ) 是以比值形式出现的所以目标分布的归一化常数在计算中被约掉了。这也是为什么我们在实际实现中只需要写出未归一化的目标密度函数。当提议分布是对称的也就是 q(θ|θ)q(θ|θ)比如高斯分布 N(θ, σ²)那么接受率进一步退化为A min(1, π(θ) / π(θ))这看起来太简单了但实践中的确是这样工作的从当前位置出发在附近随机走一步得到候选位置比较候选位置的密度和当前位置的密度。如果候选位置密度更高就接受它如果候选位置密度更低就以一个概率接受它这个概率等于新旧密度的比值。这样链不会死在高密度区域不出来但也偶尔允许“往山下走”从而探索整个空间。2.3 蒙特卡洛部分采样完怎么用采样的目标不是得到一串数字而是用这串数字做统计推断。蒙特卡洛的原理是如果样本 θ1, θ2, ..., θN 是从目标分布 π 中独立抽取的那么 E[f(θ)] 可以用 (1/N)Σf(θi) 来近似。但MCMC给出的样本并不独立相邻样本之间存在相关性。这并不致命只要链遍历了目标分布样本均值依然以概率1收敛到真实的期望值只是收敛速度会比独立样本慢。我们后面讲有效样本量就是在量化这种“不独立性”带来的信息损失。3. 手写一个Metropolis-Hastings采样器理论说够了现在进入实际环节。我建议所有人都裸写一遍M-H算法不用任何贝叶斯专用库只靠NumPy和SciPy。这不难核心循环只有十几行但写完你对采样的理解会完全不同。3.1 目标分布选择双峰混合高斯为了展示MCMC处理非标准分布的能力我选择用双峰混合高斯作为采样目标。两个峰中心分别在 -4 和 4标准差为 1权重各 0.5。这个分布本身密度函数很容易写出来但它有一个明显的多峰特性非常适合用来观察MCMC是否卡在某个局部峰值。import numpy as np from scipy import stats import matplotlib.pyplot as plt def log_target_mixture(x): # 未归一化的对数目标密度常数项直接省略 return np.log( 0.5 * stats.norm.pdf(x, loc-4.0, scale1.0) 0.5 * stats.norm.pdf(x, loc4.0, scale1.0) ) def metropolis_hastings(log_target, theta0, n_samples, proposal_sd): samples np.zeros(n_samples) current theta0 n_accepted 0 for i in range(n_samples): # 从高斯提议分布提出候选状态 candidate np.random.normal(loccurrent, scaleproposal_sd) # 计算对数接受概率 log_alpha log_target(candidate) - log_target(current) # 注意这里用对数形式的均匀随机数 if np.log(np.random.uniform()) log_alpha: current candidate n_accepted 1 samples[i] current return samples, n_accepted / n_samples np.random.seed(42) samples, acc_rate metropolis_hastings( log_targetlog_target_mixture, theta00.0, n_samples10000, proposal_sd2.0 ) print(f接受率: {acc_rate:.3f})代码里有两个容易出错的细节。第一我全程使用对数密度而非原始密度。原因很现实当目标分布维度较高或后验比较极端时密度值可能小到浮点数下溢直接相除得到 0/0 导致崩溃。而对数形式把乘法变成了加法稳定性好得多。第二比较的时候用的是 np.log(np.random.uniform()) log_alpha这等价于常见的 uniform(0,1) alpha 的写法但更安全因为当 alpha 很小的时候取对数也不会发生数值溢出。3.2 采样结果可视化跑完上面的代码可以做三件事绘制采样轨迹trace plot、画样本直方图、和真实目标密度曲线对比。fig, axes plt.subplots(1, 3, figsize(15, 4)) # 轨迹图 axes[0].plot(samples[:1000]) axes[0].set_title(Trace Plot (前1000个样本)) axes[0].set_xlabel(迭代次数) axes[0].set_ylabel(θ) # 直方图对比理论密度 x_grid np.linspace(-10, 10, 500) true_density 0.5 * stats.norm.pdf(x_grid, -4, 1) 0.5 * stats.norm.pdf(x_grid, 4, 1) axes[1].hist(samples, bins60, densityTrue, alpha0.6, labelMCMC样本) axes[1].plot(x_grid, true_density, r-, lw2, label真实密度) axes[1].legend() axes[1].set_title(样本分布 vs 真实分布) # 样本自相关 def autocorr(x): n len(x) x_centered x - x.mean() c np.correlate(x_centered, x_centered, modefull)[n-1:] c c / c[0] return c ac autocorr(samples) axes[2].plot(ac[:200]) axes[2].set_title(自相关函数) axes[2].set_xlabel(滞后步数) axes[2].set_ylabel(相关系数) plt.tight_layout() plt.show()双峰分布的直方图会非常直观地显示出MCMC的游走行为如果链成功穿越了两个谷底你会看到左右两个峰都有样本覆盖如果链卡住了你会看到样本只集中在其中一个峰附近。提案分布的标准差直接影响链能否在峰之间流动这个我们马上就会讲到。3.3 提议分布的标准差为什么决定成败很多人第一次跑M-H算法都会把注意力放在目标分布的代码上但我测试下来提议分布的标准差才是真正的性能瓶颈。下面这张表我用同一个双峰分布跑了三组实验提议标准差接受率链的行为有效样本量估算0.194%几乎每次都接受但每一步只挪动很小距离链需要极长的时间才能穿越两个峰很低2.042%既有足够大的步长探索空间又有合理接受率高20.02%经常提出远离当前高概率区的候选被大量拒绝链长时间原地不动极低接受率不是越高越好这是新手最大的认知误区。“高接受率”意味着提议分布太窄你永远在局部小范围打转虽然每一步都在“前进”但走了一万步也没走出出发点的停车场。低接受率意味着你太激进经常提出被否决的离谱位置链在原地罚站。经验上对于单维目标分布接受率在 40%-70% 比较健康高维问题接受率会降一些20%-30% 也可以接受。这个表格是我在自己电脑上跑了多次实验总结出来的实际应用时你也可以用一段“预跑”来快速试探一个合适的 scale先跑500步看接受率高了就调大 scale低了就调小。4. 用MCMC做真正的贝叶斯推断线性回归后验采样实战前面的混合高斯例子只验证了算法本身。现在把MCMC接到一个完整贝叶斯推断任务里用线性回归来展示整个链路建模、设先验、算对数后验、采样、后验区间估计。4.1 模型和先验怎么设假设我们有观测数据 (x₁,y₁), ..., (x_n,y_n)用直线关系描述yᵢ w₀ w₁·xᵢ εᵢ其中 εᵢ 独立同分布于 N(0, σ²)贝叶斯推断中我们需要为参数 w₀、w₁ 和 σ 设定先验。我知道有些人会纠结先验选什么其实先验是模型假设的一部分不存在“绝对正确”的先验。这里为了演示方便我采用业界常用的弱信息先验w₀ ~ N(0, 1)w₁ ~ N(0, 1)σ ~ HalfCauchy(1)等价于对 log σ 用某种平滑先验完整的数据生成和采样代码如下。注意我把 σ 参数化成了 log_σ理由很简单σ 必须为正数直接采样很容易把候选值采到负数域而被拒绝的效率较低。log 变换把 σ 的支撑集从 (0, ∞) 映射到 (-∞, ∞)这样 MCMC 就可以在无约束空间里自由游走。import numpy as np from scipy import stats # 生成模拟数据 np.random.seed(123) n 50 x np.linspace(-2, 2, n) w0_true, w1_true, sigma_true 1.5, 2.8, 0.8 y w0_true w1_true * x np.random.normal(0, sigma_true, sizen) def log_prior(theta): w0, w1, log_sigma theta sigma np.exp(log_sigma) # 各参数独立先验的对数密度之和 lp stats.norm.logpdf(w0, loc0, scale1.0) lp stats.norm.logpdf(w1, loc0, scale1.0) # half-cauchy 分布sigma 提取出概率密度 lp stats.cauchy.logpdf(sigma, loc0, scale1.0) log_sigma return lp def log_likelihood(theta, x, y): w0, w1, log_sigma theta sigma np.exp(log_sigma) # 残差平方和等价于正态似然的对数 residual y - (w0 w1 * x) return -0.5 * np.sum(residual**2 / sigma**2) - n * np.log(sigma) def log_posterior(theta, x, y): return log_prior(theta) log_likelihood(theta, x, y)这里的先验为什么要写 log_sigma 的雅可比项因为 θ 包含的是 log_σ我们需要目标分布是关于 θ (含 log_σ) 的密度。如果你直接把 HalfCauchy(σ) 的对数密度加上去忽略了 log_σ 和 σ 之间的变量替换因子得到的后验就会有偏差。这个细节是我在调试时发现后验结果往错误方向偏移之后才反应过来的属于那种不看仔细就永远找不出来的玄学错误。4.2 多变量M-H采样器维度上升之后提议分布从一维高斯变成多维高斯。一个简单的做法是使用各分量独立的高斯提议每个分量有自己独立的步长。也可以用完整的多维高斯协方差矩阵用历史样本来估计这实际上就是自适应MCMC的思路。新手阶段我建议先用手工调参的独立高斯提议因为更容易理解和控制。def metropolis_hastings_multi(log_target, theta0, n_samples, proposal_scales): dim len(theta0) samples np.zeros((n_samples, dim)) current np.array(theta0, dtypefloat) n_accepted 0 for i in range(n_samples): candidate current np.random.normal(0, proposal_scales, sizedim) log_alpha log_target(candidate) - log_target(current) if np.log(np.random.uniform()) log_alpha: current candidate n_accepted 1 samples[i] current return samples, n_accepted / n_samples theta0 np.array([0.0, 0.0, 0.0]) scales np.array([0.3, 0.3, 0.2]) samples, acc metropolis_hastings_multi( lambda th: log_posterior(th, x, y), theta0, n_samples20000, proposal_scalesscales ) print(f平均接受率: {acc:.3f})这里要注意一个细节多元提议中每个维度步长的比例应该与后验在对应方向上的尺度匹配。如果 w₀ 的后验标准偏差是 0.2而 σ 的后验标准偏差是 0.1那么你把它们的步长都设成 0.5会导致某些方向移动太激进某些方向移动太保守。一个笨但有效的办法是先跑一小段计算每个维度的样本标准偏差然后用这个标准偏差的 2.4/√d 倍作为下一步的建议步长。这个 2.4/√d 来源自 Gelman 等人在高斯目标分布下推导的最优尺度公式虽然不是万能公式但作为初始设置非常实用。4.3 后验参数的可信区间与预测分布采样完成后我们可以非常自然地获得一整套贝叶斯推断结果。直接把样本的后 10000 个丢弃掉用前 10000 个作为 burn-in剩下的样本做统计。burn_in 10000 final_samples samples[burn_in:] # 参数后验均值 w0_mean, w1_mean, log_sigma_mean final_samples.mean(axis0) print(fw0 后验均值: {w0_mean:.3f} (真实值: {w0_true})) print(fw1 后验均值: {w1_mean:.3f} (真实值: {w1_true})) # 参数可信区间 w0_ci np.percentile(final_samples[:, 0], [2.5, 97.5]) w1_ci np.percentile(final_samples[:, 1], [2.5, 97.5]) print(fw0 95%可信区间: {w0_ci}) print(fw1 95%可信区间: {w1_ci})输出结果应该和真实值很接近w₀ 和 w₁ 的真实值都会被包含在 95% 可信区间内。这正是贝叶斯推断最强大的地方你不仅拿到了系数的期望还拿到了系数的完整后验分布可以回答“参数有多大的概率落在某个区间”这种频率学派回答不了的问题。预测分布也是一样对最终样本中的每一组参数 (w₀, w₁, σ)我们都用它在给定 x 处生成一个预测值最后把所有预测值并在一起形成后验预测分布。这个分布天然的包含了参数不确定性和数据噪声的不确定性比单纯的均值预测要诚实得多。5. 采样质量的三道体检收敛诊断、自相关与有效样本量MCMC算法会给你一串样本但这并不意味着这串样本一定收敛到了目标分布。很多人喜欢直接拿结果去画图、做推断结果得到了一个偏差很大的后验估计。所以采样完成后的第一件事不是解读结果而是做质量体检。5.1 迹图第一眼印象最基础的诊断方式是画迹图也就是把 θ 的采样值对迭代次数画一条线。一个健康的迹图应该看起来像“杂草堆”——在某个中心区域密集而均匀地震荡没有明显的漂移或长周期波动。我见过太多人犯这样的错误看到一个漂亮的迹图就认为链已经收敛。其实迹图只能排除“明显还没收敛”的情况但不能证明收敛。比如两条链可能分别卡在两个不同的峰上它们各自的迹图看起来都很平稳但实际上都没探索完整个分布。这就是为什么业界强烈建议同时跑多条链而不是只跑一条。5.2 自相关与有效样本量ESSMCMC样本不是独立样本相邻样本之间通常存在正相关。高自相关会造成一个严重的问题样本里包含的有效信息比表面上的样本量要少得多。为了量化这个损失统计学家提出了有效样本量Effective Sample Size, ESS的概念。自相关系数 ρ_k 定义的是相隔 k 步的两个样本之间的相关性。有效样本量的近似计算公式是ESS N / (1 2 * Σ_{k1}^{∞} ρ_k)直观上讲如果样本之间完全独立ρ_k0 且 ESSN所有样本都提供了完整的信息如果相邻样本强相关ρ_1 接近于 1那么 ESS 会远远小于 N意味着你需要采集多好几倍的样本才能达到和独立采样同样的统计精度。采样后的诊断代码大概长这样def effective_sample_size(x): n len(x) x_centered x - x.mean() ac np.correlate(x_centered, x_centered, modefull)[n-1:] ac ac / ac[0] # 找到第一个负自相关或变号的滞后位置避免累加噪声 lag_limit 0 for k in range(1, len(ac)): if ac[k] 0.05: lag_limit k break ess n / (1 2 * np.sum(ac[1:lag_limit])) return ess for dim_idx, name in enumerate([w0, w1, log_sigma]): ess effective_sample_size(final_samples[:, dim_idx]) print(f{name}: ESS ≈ {ess:.0f} / {final_samples.shape[0]} 个样本)提高有效样本量的方法有几条。第一增大提议步长减少自相关但要警惕接受率过低。第二对样本做稀疏化处理每隔 k 步取一个样本这能显著降低自相关但要注意稀疏化不会创造新的信息量只是减少了存储和后续计算的负担。第三从算法层面使用更高效的采样器比如 Hamiltonian Monte Carlo 或者 NUTS它们利用目标函数的梯度信息来大幅减少随机游走行为。5.3 多链诊断Gelman-Rubin R-hat如果你手里有多条从不同初始值出发的链可以用 Gelman-Rubin 统计量来评估收敛。这个统计量的核心思路是同时比较“链间方差”和“链内方差”。如果链之间的差异远大于链内差异说明链们还在不同的区域各自探索没有收敛到一起。实践中我常用 R-hat 判断标准小于 1.1 基本可以认为收敛稳妥一点我会要求小于 1.05。如果你用的现成库比如 PyMC它会自动输出这个指标。手写的话实现也不复杂关键是每一条链都要从不同初始值出发不能所有链路都从同一个起点开始。6. 实战中的坑与调参经验最后这部分是我跑了大量MCMC实验后总结的一些零碎但特别实用的经验。每一条都是我真实踩过的坑写出来帮你避开。6.1 接受率不是越高越好但也不是越低越好我在前文给过一个经验区间单维问题 40%-70%多维问题 20%-30%。但很多人忽略的一点是这个区间是一个“最终平衡值”并不是说初始阶段的接受率也应该如此。在 burn-in 阶段链往往还在向高概率区域迁移接受率可能偏低进入平稳后才会稳定下来。所以如果你一开始看到接受率比较低不要急着调参先多跑几步看看总体趋势。如果你用的提议分布是高斯的步长和接受率之间有一个近似关系步长与接受率的 -1/6 次方成正比。这意味着想从接受率 20% 调到 40%大约需要把步长乘以 (0.4/0.2)^(1/6)≈1.12也就是小幅调大即可。这个关系在高斯目标下非常准确可以作为快速调参的起点。6.2 burn-in到底该丢弃多少burn-in 的长度取决于初始位置离高概率区域有多远以及链的混合速度。一个常见做法是丢弃前 10%-50% 的样本。但如果你用的是自适应MCMC或者链的初始位置是随机抽取的建议把 burn-in 比例留大一些。经验法则是先画出累积均值图也就是从第 N 个样本开始计算后续样本的均值看看什么时候累积均值稳定下来。那个稳定点之前的样本就是 burn-in。这个方法比凭感觉决定丢弃数量可靠得多。6.3 多峰分布一个让人头疼但实际上很常见的问题混合高斯例子已经展示了如果两个峰相距很远M-H算法很容易只探索到一个峰然后原地踏步。这是因为从峰 A 跨到峰 B 需要穿越一片概率密度极低的区域M-H 的“爬山”特性导致它在低密度区提出的候选几乎都会被拒绝。解决这个问题有几种思路。第一跑多条链让不同初始值的链分别落在不同峰上最后把所有链的样本合并起来。第二使用温度调度或模拟退火的变体在早期允许链“高温”探索后期慢慢降温回真实后验。第三在 M-H 的基础上发展出更高级的算法比如平行回火parallel tempering或全局优化结合局部采样。如果你只是想快速检查是否存在多个峰可以先跑一个高噪声的提议分布看链是否能穿越峰间谷底。6.4 数值稳定性的细节比你想象的更重要采样代码里最容易被忽略的就是数值稳定性。我见到过不少人直接用 scipy.stats.norm.pdf 去算目标密度然后相乘结果在高维空间里密度值掉到 1e-300 以下直接变成 0。0 除以 0 在 Python 里是个 nan然后整个采样器就陷入了 nan 的泥潭你检查代码逻辑半天也不知道哪里错了。我的习惯是只要是密度函数就用 logpdf只要是乘积就改成求和只要是除法就改成减法。代价是要多打几行代码但换来的稳定性非常值得。另一个细节是不要在采样循环内部做重量级计算。比如每次迭代都重新算一次 scipy.stats 的包装对象或者重新计算无关的常量都会白白拖慢速度。应该把常数项提到循环外面循环内只保留和当前参数相关的计算。我在实现的过程中把线性回归的对数似然函数里的残差平方和写法优化了一版速度提升非常明显。6.5 参数变换使用无约束参数化前文在贝叶斯线性回归里我用了 log_σ 而不是直接用 σ这已经是参数变换的典型例子。凡是有边界约束的参数比如概率必须落在 0 到 1 区间、方差异必须为正都应该考虑变换到无约束空间。这个思路在处理 Dirichlet 分布参数、协方差矩阵参数时尤其重要。变换的时候一定要记得加上雅可比修正项否则后验分布会发生系统性的偏移。我最初在参数变换这里栽了跟头后来学乖了每次变换前都用数值方式验证一下比如拿一个小样本手动计算变换前后密度面积不一致就说明雅可比项漏了。MCMC算法的实现过程其实就是不断在“探索”和“利用”之间找平衡的过程。提议分布太窄你只在小范围内精耕细作提议分布太宽你频繁被拒绝举步维艰。贝叶斯推断中的所有不确定性最终都靠这些样本转化为可读的区间、概率和分布。起初我总觉得这套机制过于随机不够优雅直到在几个复杂模型上真正跑通之后才理解那种在高维空间里不断尝试、偶尔被拒绝、但长期来看总能找到正确区域的特性正是它能够绕过高维积分难题的关键所在。后面如果再遇到那些难缠的非共轭后验或者层次模型我第一时间想起的还是这个几十行代码就能跑起来的M-H采样器。本文还有配套的精品资源点击获取
返回列表