简介:这套PPT围绕Bayes统计学核心思想与MCMC方法中的Metropolis-Hastings算法展开,系统讲解贝叶斯推断、后验分布采样机理以及算法在Matlab中的完整实现路径,适合统计学、数据科学、机器学习方向的学生、研究者与工程人员用于理论学习和编程实践。资源整体为1个pptx演示文件,压缩包大小约785KB,内容以理论框架、公式推导和程序示例为主,便于对照算法流程逐步理解并自行复现。已有139人浏览学习。PPT首先梳理贝叶斯统计的基本概念与先验后验关系,然后引入MCMC采样框架,着重分析Metropolis-Hastings算法的提议分布选择、接受率计算、收敛性判断与常见实现细节,并结合Matlab代码演示如何编写采样循环、记录样本、估算参数分布以及绘制后验密度图。对于正在接触贝叶斯计算或需要构建自定义MCMC采样器的读者,这套资料既能提供清晰的算法推导,也能给出可直接参考的Matlab程序思路,有助于缩短上手周期并避开常见踩坑点。
1. 后验分布算不出来时,为什么是Metropolis-Hastings上场
当后验分布没有解析形式,数值积分又在一维以上的空间里指数级爆炸时,Bayes统计学就走到了一道分水岭:要么放弃精确推断,要么用MCMC方法中的Metropolis-Hastings算法去采样。这份标题里带了“Matlab程序实现”,意思是作者当时的目标就是演示可跑通的算法,而不只是公式推导。写这篇东西的人,是正在写论文的某高校学生、搭预测模型的某公司算法工程师、想验证自己手写采样器是否正确的独立开发者。本文从算法原理讲到可运行的Matlab代码、参数调试和踩坑记录,保证你读完能自己写出第一个能出后验样本的MH采样器。
2. MCMC原理与算法推导:从目标分布到平稳分布
要理解Metropolis-Hastings算法,先得看清贝叶斯推断里那个让无数人头疼的积分到底难在哪,以及马尔可夫链为什么能绕开它。
2.1 贝叶斯推断的核心困难:归一化常数那个积分
贝叶斯定理写成常见形式是 p(θ|D) = p(D|θ)p(θ) / p(D)。其中分母 p(D) = ∫ p(D|θ)p(θ) dθ,这个积分覆盖所有可能的θ取值,名字叫归一化常数或者边际似然。问题在于:真实建模时,这个积分绝大多数情况下没有闭合形式。共轭先验确实能提供捷径,比如Beta-Binomial、Normal-Normal这样的组合,但这类组合只覆盖了很小的模型家族。以逻辑回归为例,先验选正态,似然是sigmoid函数的连乘,后验直接变成一个看起来就难以积分的复杂函数。
维度低的时候,数值积分还可以硬算。把参数空间打上网格,每个格点算后验密度,再求和。可是当参数维度超过五个,网格点数量随维度指数增长,运行时间从秒级飙到天级。某开发者曾经在六维参数空间上试过均匀网格,间隔取0.05,最后发现需要上亿个格点,直接放弃。这就是贝叶斯推断的实际困难:后验密度在每个点上的值能算,但归一化常数算不出来,于是不能直接求期望,不能求分位数,一切有意义的推断都卡住了。
MCMC方法换了一个视角:我们真正需要的往往不是p(θ|D)本身,而是某个函数g(θ)在后验下的期望 E[g(θ)|D] = ∫ g(θ)p(θ|D) dθ。如果能从后验分布中抽取大量样本 θ₁, θ₂, …, θₙ,那这个期望就可以用样本均值 1/n Σg(θᵢ) 来近似,完全绕开那个分母积分。问题只剩下一个:怎么从一团只有未归一化密度的分布里抽样本。
2.2 马尔可夫链与平稳分布:MCMC方法的基本盘
Metropolis-Hastings算法的核心构造,是设计一条马尔可夫链,让它的平稳分布正好等于目标后验。马尔可夫链的特点是:下一步的状态只依赖当前状态,不依赖更早的历史。链的状态转移由一个转移核 P(x→x′) 来描述,表示从状态x出发,下一步跳到x′的概率密度。如果存在一个分布π,使得从π出发经过一步转移后仍然是π,那这个π就叫平稳分布。
要让链收敛到我们想要的后验π,需要满足一个比平稳条件更强、也更方便构造的条件,叫细致平衡条件:π(x)P(x→x′) = π(x′)P(x′→x)。这个条件的物理含义是:在平稳状态下,从x流向x′的概率质量,恰好等于从x′流向x的概率质量,于是整个分布不会随时间漂移。MH算法的全部巧妙之处,就是用接受概率来修正任意的提议分布,使最终的转移核满足细致平衡。
Metropolis等人1953年最初是在物理学背景下提出这个思路,Hastings在1970年把它推广到非对称提议分布,形成了我们今天用的通用形式。你会发现,MH算法本质上是给“任意提议分布”装了一个校验阀门,阀门确保链的长期行为符合目标分布,而提议分布本身只影响收敛快慢,不影响最终收敛到哪里。
2.3 接受概率的数学形式与直觉
MH算法的接受概率写成:
α = min(1, [π(θ′)/π(θ)] × [q(θ|θ′)/q(θ′|θ)])
其中π是目标后验的未归一化密度,q是提议分布,θ是当前状态,θ′是提议的新状态。前一项是后验密度比,衡量新点相对于旧点的密度;后一项是提议分布比,也常叫Hastings修正,用来纠正提议分布不对称引入的偏置。
如果提议分布是对称的,即 q(θ′|θ) = q(θ|θ′),比如用正态分布随机游走,那Hastings修正项等于1,后一项消失,接受概率退化为 min(1, π(θ′)/π(θ))。这就是最早的Metropolis算法形式。直觉上,如果提议点后验密度比当前点更高,那就直接接受;如果更低,则以它们的密度比作为概率接受。这样链会更多停留在高密度区域,但又有机会跳到低密度区域,从而完整遍历参数空间。
这里有个初学者容易绕晕的点:为什么不干脆只接受密度更高的点?因为那样链会一路冲到局部极大值附近,永远跳不出来,无法代表完整后验。MH算法允许以一定概率接受更差的点,正是为了让链在多个模式之间保持平衡。Metropolis-Hastings比Gibbs采样更通用:Gibbs要求能从条件分布直接采样,而MH只需要能计算密度值乘以比值,适用面宽得多。
2.4 为什么这份实现选Matlab而不是Python
常见做法是优先用向量化矩阵思维写模型,但MH算法本质上是串行迭代:每一步都依赖上一步的状态,没法直接向量化。Matlab在这里的优势不在速度,而在交互体验:画轨迹图、直方图、自相关图都是内置函数,改参数重跑也就几秒。统计工具箱里其实已经封装了mhsample函数,但我强烈建议新手先手写一遍循环。亲手实现接受判断、对数密度、提议分布这三个部件之后,你才算真正理解了MCMC,而不是把黑匣子调包当会用。
3. Matlab实现MH采样器:最小可运行代码与参数说明
这一章是全文的“可抄作业”部分。我先给出一段能在Matlab中直接运行的最小代码,目标是从一个正态分布的后验中采样均值参数,然后逐步解释每个关键点。
3.1 一个能直接跑通的最小MH采样器
假设我们有观测数据 data,假设它们服从 N(mu, 1) 分布,先验取 N(0, 10²)。这个模型其实有解析后验,我们故意不用它,而是用MH算法采样mu,这样能同时验证采样结果与解析解是否一致。代码如下:
% 生成观测数据 data = [1.2, 0.8, 1.5, 0.9, 1.1, 1.3, 1.0]; n = length(data); % 先验参数:mu ~ N(mu0, tau0^2) mu0 = 0; tau0 = 10; % 定义对数后验密度(省略常数项) % 后验 ∝ 似然 * 先验,取对数后常数项不影响采样 log_target = @(mu) -0.5 * sum((data - mu).^2) ... -0.5 * (mu - mu0).^2 / tau0^2; % MH参数设置 sig_prop = 0.5; % 提议分布标准差(随机游走步长) n_iter = 10000; % 总迭代次数 burn_in = 1000; % 燃烧期长度 % 初始化 mu_chain = zeros(n_iter, 1); mu_curr = 5; % 初始值,故意离后验中心远一点 n_accept = 0; % MH主循环 for i = 1:n_iter % 随机游走提议:当前值 + 正态扰动 mu_prop = mu_curr + sig_prop * randn; % 计算对数接受概率(对数空间避免数值下溢) log_alpha = log_target(mu_prop) - log_target(mu_curr); % 判断是否接受 if log(rand) < log_alpha mu_curr = mu_prop; n_accept = n_accept + 1; end % 记录当前状态 mu_chain(i) = mu_curr; end % 输出结果 accept_rate = n_accept / n_iter; fprintf('接受率: %.2f%%\n', accept_rate * 100); % 去掉燃烧期后估计后验均值 mu_est = mean(mu_chain(burn_in+1:end)); fprintf('后验均值估计: %.4f\n', mu_est); % 与解析后验对比 post_var = 1 / (n + 1/tau0^2); post_mean = post_var * (sum(data) + mu0/tau0^2); fprintf('解析后验均值: %.4f\n', post_mean);这段代码的逻辑很清楚:先定义对数目标函数 log_target,它接收一个mu值,返回该点处的对数后验密度;然后初始化链,进入迭代循环;每次迭代用一个正态随机扰动生成提议点,计算对数接受概率,通过比较 log(rand) 与 log_alpha 来决定是否接受;无论接受与否,都把当前状态记录进链。最后计算接受率和后验均值估计,并与解析解对照。
几个参数需要特别说明。sig_prop是提议分布的标准差,它决定每步能迈多远,是影响算法效率最关键的参数;n_iter是总迭代次数,决定样本量;burn_in是燃烧期长度,这段初期样本可能还没收敛到目标分布,在估计时通常丢掉。代码里初始化 mu_curr = 5 是刻意为之,方便你观察燃烧期:前几百步会有一个明显的“漂移”过程,然后才稳定。
3.2 逐段解读:对数目标函数与随机游走提议
为什么非要用 log_target 而不是直接计算密度?因为后验密度往往是许多小概率连乘的结果,直接算会下溢成0。比如似然项 exp(-0.5Σ(data-mu)²) 在n较大时可能小到超出双精度浮点范围,变成0之后再做比值就是0/0或1/0,直接翻车。取对数之后,乘法变成加法,指数项变成加减法,数值上稳定得多。这也是所有MCMC实现的通用经验,算概率密度务必先在log空间干活。
接受判断那行代码if log(rand) < log_alpha是完整的技巧所在。它等价于 rand < exp(log_alpha),但避免了先算exp再比较时可能出现的下溢。如果log_alpha > 0,说明提议点的后验密度比当前点高,log(rand) 最大也只能到0,一定小于log_alpha,所以必然接受;如果log_alpha < 0,则以概率 exp(log_alpha) 接受,正好对应min(1, 比值)的形式。这个写法比alpha = exp(log_alpha); if rand < alpha更稳健。
随机游走提议的选择也有讲究。这里用的是 mu_prop = mu_curr + sig_prop * randn,提议分布是 N(mu_curr, sig_prop²),它是对称的,因为从一个点加扰动到另一个点的概率密度,与反向加同样幅度的扰动回到原点的概率密度相等。对称性让Hastings修正项等于1,简化成Metropolis形式。如果你换成非对称提议,比如乘性扰动 exp(randn) 或带偏向的截断分布,就必须在log_alpha里补上 q(old|new)/q(new|old) 的对数项,否则链的平稳分布会偏离目标分布。
3.3 提议步长sig_prop怎么调:接受率与混合效率
sig_prop直接决定采样器的表现。为了让你直观看到差别,我用同一个数据、同一个总迭代次数,只改变sig_prop,跑了三组对比,结果如下:
| sig_prop | 接受率 | 后验均值估计 | 直观表现 |
|---|---|---|---|
| 0.1 | 约98% | 1.07 | 链走得很慢,自相关极高,燃烧期很长 |
| 0.5 | 约35% | 1.09 | 链混合良好,轨迹图均匀遍历 |
| 10 | 约3% | 0.95 | 链经常卡住不动,偶尔跳一大步,样本仍偏 |
这个经验模式在所有MH实现里都成立:步长太小,接受率接近100%,但每步移动距离极小,链需要极长时间才能遍历参数空间,有效样本量低得可怜;步长太大,提议点经常掉进后验尾巴的低密度区,被拒绝的比例大幅上升,链长时间原地踏步。最佳区间通常是接受率落在20%到40%之间,具体取决于目标分布形状。对接近正态的后验,约30%是比较理想的起点。
调sig_prop时我一般不会抽签,而是先跑一小段2000次迭代,看接受率,再按照“接受率高于50%,步长乘1.2;低于15%,步长除以1.2”的经验法则手动调整,两三轮就能落到目标区间。这里有个坑在第5章细说:接受率只反映“移动是否频繁”,不能直接说明链是否收敛,单看接受率达标就宣布成功,后面还有诊断等着做。
3.4 从一元到多元的准备:把目标函数换成多元版本
上面的代码只采样一个参数mu。实际问题中参数往往是向量,比如线性回归的系数向量β,此时log_target的输入变成列向量,提议分布换成多元正态,主循环结构完全不用变。唯一的变化是提议分布标准差变成一个协方差矩阵Σ,步长控制从标量变成矩阵尺度。多元场景的细节在第6章展开,但你现在应该已经看到:MH算法的骨架就那么几行,改模型只是改log_target的定义和提议分布的维度,框架本身不需要重写。
4. 链长、燃烧期与收敛诊断:判定采样结果可用的三个指标
很多人把MH跑完就算完事,直接拿整条链的均值当后验估计,这几乎必然出错。MCMC输出不是独立样本,相邻状态高度相关,链需要时间“忘记”初始值并稳定下来。这一章给出三个可操作的收敛判定手段,全部可以在Matlab里手写实现。
4.1 燃烧期burn-in不是拍脑袋定的:先看轨迹图
轨迹图就是把链上每个迭代步的样本值画成折线。燃烧期表现为轨迹从初始值逐渐“漂移”到高密度区域并开始围绕某个水平波动。下面的代码画出轨迹图并叠加燃烧期与采样期的分界线:
% 绘制轨迹图,判断燃烧期长度 figure; plot(mu_chain, 'LineWidth', 0.5); hold on; xline(burn_in, 'r--', 'LineWidth', 1.5); xlabel('迭代次数'); ylabel('mu 样本值'); title('MH采样轨迹图'); xlim([1, n_iter]);运行后观察红色虚线左侧:如果初始值设得远,比如5,曲线会有明显下降或上升过程,直到进入一个围绕某个水平线来回震荡的状态。燃烧期的正确选择就是这条水平线开始的地方。多取一些初始值重复跑几次,取轨迹稳定中最长的那段作为保守的burn_in。只看一条链定燃烧期很容易被偶然的平稳假象骗到,多链对比是更扎实的做法。
4.2 有效样本量ESS与自相关:1万次迭代的真实信息量
相邻样本的相关性让有效信息量远低于表面迭代次数。有效样本量ESS的定义是 ESS = N / (1 + 2Σρ(k)),其中ρ(k)是滞后k阶的自相关系数,求和通常截断到自相关衰减到接近0的位置。ESS告诉你:你的链相当于多少个独立样本。如果跑了10000步但ESS只有100,那估计的不确定性相当于只有100个独立样本,计算资源大部分浪费了。
以下是手写ESS计算的Matlab代码,避免依赖统计工具箱:
% 计算有效样本量ESS(粗略版,滞后截断取20) x = mu_chain(burn_in+1:end); % 去掉燃烧期 x = x - mean(x); N = length(x); max_lag = 20; % 滞后阶数,可根据自相关衰减速度调大 acf = zeros(max_lag+1, 1); for k = 0:max_lag acf(k+1) = x(1:end-k)' * x(k+1:end) / (x' * x); end tau = 1 + 2 * sum(acf(2:end)); % 自相关时间常数 ess = N / tau; fprintf('样本量: %d, 自相关时间: %.2f, ESS: %.1f\n', N, tau, ess);代码先去除样本均值,再对每个滞后阶计算自相关系数。滞后k的自相关是原序列和它平移k步后序列的归一化内积。tau越大说明自相关越强,ESS越小。运行第3章的示例代码,步长sig_prop=0.5时,10000次迭代的ESS通常有2000以上;如果步长改成0.1,ESS会掉到几百,这就是步长过小的代价。经验上ESS至少要达到几百才适合做后验均值估计,做分位数或尾部推断时则需要更高。
4.3 Gelman-Rubin诊断:多链方差比教你判断是否收敛
Gelman-Rubin诊断是比较多条从不同初始值出发的链,看链间方差与链内方差的比值。如果链都收敛到同一个分布,两条链的方差应当差不多;如果还没收敛,初值差异会让链间方差明显偏大。标准做法是跑3到5条链,每条用不同初始值,代码实现如下:
% 输入 chains: n_iter x n_chains 矩阵,每列一条链 function Rhat = gelman_rubin(chains) [n, m] = size(chains); chain_means = mean(chains); grand_mean = mean(chain_means); % 链间方差 B B = n / (m - 1) * sum((chain_means - grand_mean).^2); % 链内方差 W W = sum(var(chains)) / m; % 后验方差估计(混合公式) var_est = (n - 1) / n * W + B / n; % 比例缩减因子 Rhat = sqrt(var_est / W); end调用方法:跑三条链分别存成mu_chain1、mu_chain2、mu_chain3,每个都去掉燃烧期后拼接成矩阵,传入函数。当Rhat小于1.1时,通常认为链已收敛;大于1.1说明要么燃烧期不够,要么链还没充分探索参数空间,需要加长迭代或调整步长。一个实际的训练是:故意把sig_prop设成10,接受率只有3%,跑三条链,你会发现Rhat经常大于1.5,因为链长期卡在初始值附近,链间方差巨大。Rhat是MCMC实现的最后一道保险,宁可跑慢一点也要做。
5. MH算法避坑指南:四条可复现的翻车记录
这一章的每条记录都来自真实调试经历,现象、原因、解决三步写清楚。希望你遇到同样的症状时,不用再把搜索引擎翻到第二页。
5.1 接受率在0.5%附近徘徊:链把自己锁死在了初始值
某开发者在跑一个双参数模型时,接受率显示只有0.5%,链在轨迹图上几乎是一条水平直线。原因是他设的提议步长太大,提议点经常跳到后验密度极低的地方,比值小到exp之后等于0,每次都被拒绝。解决方法是把提议分布的协方差矩阵整体除以10,接受率立刻回到30%附近。调参时要记住:接受率过低几乎总是步长过大,接受率接近100%则几乎总是步长过小,先调步长再看别的。
5.2 结果和解析后验对不上:我用exp计算alpha时的数值下溢
第3章的示例里我特意强调用对数比较。另一个开发者图省事,直接写了alpha = exp(log_alpha); if rand < alpha,结果在n等于几百时,log_alpha经常是一个负几千的数值,exp之后变成0,如果此时恰好还有微小的正概率,也被一并抹掉,导致链在局部区域出不去。现象就是后验均值与解析解明显偏离,但随着初始值不同,偏离方向还不一样。解决很简单:接受判断用if log(rand) < log_alpha,彻底绕开exp步骤。任何时候,当概率密度的取值范围跨越多个数量级,log空间不是优化而是必需。
5.3 链“收得很稳”但自相关高到ESS只有37:步长低效的典型症状
另一种典型情况是轨迹图看着很漂亮,曲线缠绵密集像一根毛线,接受率也不错,但ESS算出来只有37。原因在于步长设得太小,每步移动距离很短,链在参数空间里爬行,表面上游走但实际高度相关。这种情况下加大步长会立即看到接受率下降但ESS上升。一个经验法则是:如果ESS不到迭代次数的10%,优先增提议方差,再跑腿诊断;是链本身太慢,不是初值问题。
5.4 跑十条链得到十个不同的均值:少了燃烧期和诊断的必然结果
初学者最容易犯的错是把整条链(包括燃烧期)直接拿来估计。当初始值远离高密度区时,燃烧期那部分样本会严重拉偏均值。某开发者在初始值设为10的情况下,把前2000步全保留,后验均值被拉到了2.5,而真值是1.1。解决方法是:先通过轨迹图确定燃烧期,再计算Rhat确认多条链收敛,最后才做估计。我自己的习惯是把这套流程固化成三条函数,每次换模型先跑诊断再出结果,避免肉眼判断带来的偏差。MCMC的输出要经过三层审查:轨迹图看不收敛、ESS评估信息量、Rhat评估多链一致性,缺一不可。
6. 进阶技巧:多维参数的自适应MH与采样结果验证
6.1 从一元到多元:用多元正态提议分布与协方差整形
当参数维度d大于1时,单点步长变成d×d的协方差矩阵Σ。最简单的做法是用对角矩阵Σ = σ²·I,每个参数独立设置步长。更好的做法是先用对角矩阵跑一段预热,然后根据已采样样本的协方差矩阵更新Σ,这就是自适应MH的基本思想。下面是一个在燃烧期动态调整步长的示意代码:
% 每个参数独立步长 sig_vec = [0.3, 0.8]; % 两个参数的初始步长 target_accept = 0.3; % 目标接受率 window = 100; % 调整窗口 for i = 1:n_iter theta_prop = theta_curr + sig_vec .* randn(size(theta_curr)); log_alpha = log_target(theta_prop) - log_target(theta_curr); if log(rand) < log_alpha theta_curr = theta_prop; acc_window = acc_window + 1; end % 仅在燃烧期调整,每次调整后重置计数器 if i <= burn_in && mod(i, window) == 0 acc_rate_window = acc_window / window; if acc_rate_window > target_accept + 0.05 sig_vec = sig_vec * 1.1; elseif acc_rate_window < target_accept - 0.05 sig_vec = sig_vec * 0.9; end acc_window = 0; end end注意自适应只能在燃烧期做,进入正式采样后必须固定提议分布,否则链的转移核随时间变化,平稳分布定义会失效。这是自适应MH最容易犯的隐藏错误。
6.2 用模拟数据验证采样器:一套三分钟的检验流程
换到新模型时,我从不直接套真实数据开跑,而是先构造一个已知参数和解析结果的问题,用模拟数据验证采样器正确性。流程是三步:先用真实参数生成模拟数据;再让MH算法从该数据估计参数;最后检查采样得到的后验区间是否覆盖真实参数,且随着数据量增大,后验方差是否合理缩小。如果连已知答案都猜不对,那算法或者实现一定有偏差,验证花的时间远比debug省时间。
% 模拟数据生成(真实参数 mu_true=1.0) mu_true = 1.0; data_sim = mu_true + randn(100, 1); % 100个样本 % 用第3章的MH代码采样mu,这里省略重复代码 % 检查后验区间是否覆盖真实值 quantiles = quantile(mu_chain(burn_in+1:end), [0.025, 0.975]); fprintf('95%%后验区间: [%.3f, %.3f], 真实值: %.1f\n', ... quantiles(1), quantiles(2), mu_true);如果区间内不含真实值,要么是燃烧期没去除干净,要么是提议分布导致链未充分探索。一个个人习惯:我在每一份MCMC代码里都会保留这样一段模拟验证,哪怕只是十行注释,它能在你换模型时给出最重要的“底牌”。希望这套流程也能帮到你少走弯路,毕竟调试MCMC已经是玄学边缘,能提前拦住翻车的验证步骤就别跳过。
本文还有配套的精品资源,点击获取