ARTICLE DETAIL

资讯详情

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

MCMC作为数据生成器:Matlab实现Metropolis-Hastings与Gibbs采样

MCMC作为数据生成器:Matlab实现Metropolis-Hastings与Gibbs采样 很多人第一次接触MCMC是在贝叶斯统计的参数估计里。但我在实际项目里更愿意把它当成一台“数据生成器”只要你给我一个能写出函数表达式的目标分布MCMC就能吐出一批从这个分布里来的、可独立使用的样本数据。用Matlab做这件事代码量不大但真正跑通、真正让链快速收敛、真正让生成的数据质量可验证中间有很多文档里不会写的东西。这篇文章就围绕“用MCMC生成数据”这个目标把Metropolis-Hastings和Gibbs两种常见采样器的原理、完整Matlab代码、调参经验、质量验证方法全部梳理一遍。适合两类人一类是刚接触MCMC想搞懂它到底怎么把“指定分布”变成“真实样本”另一类是已经在写采样器但遇到链不收敛、自相关严重、生成数据明显不像目标分布等问题的实践者。1. 把MCMC当作“数据生成器”而不是“参数估计器”1.1 我们平时生成随机数据的三种老办法卡在哪里做数据仿真时最自然的三招是均匀分布采样、逆变换采样和拒绝采样。均匀分布采样只能处理均匀分布本身适用范围太窄。逆变换采样要求目标分布的CDF有解析的反函数比如指数分布、Weibull分布这种老实巴交的分布可以但一旦换成混合分布、截断分布、重尾分布或者高维联合分布反函数往往根本写不出来。拒绝采样稍微灵活一点思路是找一个容易采样的建议分布用接受/拒绝把样本修成目标分布可是在高维空间里建议分布和真实分布的差异会被维度指数放大接受率低到一场模拟跑完几乎采不到几个有效样本。这三招本质上都是“独立采样”每个样本靠一次独立随机过程生成和之前的样本没关系。问题是很多我们真正关心的分布连密度函数都只能算到“未归一化”的程度更别说轻松抽样。这时候就需要MCMC登场。打个比方逆变换采样像查表发牌CDF是一张带刻度的表一个均匀随机数就对应一个样本位置拒绝采样像考官反复出题抽到不合适的就重来题越难重来次数越多MCMC则像游客在一个陌生城市里逛先随便站在一个位置然后不断根据“周围哪里更值得去”做决定逛得足够久之后你出现在每个街区的时间比例就会自动趋近这个城市真正的“热度分布”。1.2 MCMC的产物直接就是样本合成数据、后验预测、缺失填补MCMC的输出是一串自相关的序列也就是一条马尔可夫链。这串序列经过一定处理之后可以直接当成从目标分布近似采样的数据集合。这个视角在几个场景里特别值钱。合成数据是最直接的用途。比如团队要测试一个聚类算法对双峰数据的敏感性但手头真实数据只有一个单峰样本这时候就可以写一个双峰高斯混合密度函数用MCMC生成一批指定形状的合成数据去验证算法。又比如做隐私保护场景下的数据替代先从真实数据拟合出一个密度模型再从密度模型里MCMC采样生成一份“长相类似但不含原始隐私”的模拟数据。贝叶斯后验预测也很典型。模型参数的后验分布很难直接采样但MCMC可以从后验里采出大量参数再用每个参数仿真一组新数据这组数据的分布就叫后验预测分布专门用来检验模型对未知新样本的预测能力。缺失数据填补则是把MCMC当成条件生成器在给定观测数据的前提下从隐变量或缺失值条件分布中采样每采一次就是一次合理的补充。其实很多论文里的多重插补、数据增强底层都是这么干的。1.3 需要先弄懂的术语表MCMC相关的术语有点多我们先在同一个语境里对齐后面讲到代码时就不再单独解释。目标分布π(x)你想生成数据来源的那个分布通常只知道它的密度函数有时候连常数都不知道只知道正比于某个函数。建议分布q(x*|x_t)从当前状态x_t出发提议下一个候选x*的随机规则。最常见的是一维/多维高斯随机游走。接受概率α决定是否把候选x*当作下一个状态。马尔可夫性下一个状态只取决于当前状态和更早的历史无关。burn-in预热期从初始点出发到链进入平稳状态之前的那一段样本通常要丢弃。thinning间隔抽样每隔若干步保留一个样本用于减弱自相关。平稳分布马尔可夫链跑得足够久之后状态的边际分布会收敛到某个固定分布。MCMC的全部目的就是设计转移规则让这个平稳分布恰好等于你的目标分布π(x)。2. 两种常用采样机制的Matlab实现Metropolis-Hastings与Gibbs2.1 Metropolis-Hastings核心公式与最小可运行代码Metropolis-HastingsM-H是MCMC家族里最通用的一款。它不要求你知道目标分布的归一化常数只需要能计算任意点x上的密度值或对数密度值。基本流程只有四步从当前状态x_t出发按建议分布q(x*|x_t)抽一个候选样本x*计算接受概率α min(1, [π(x*) q(x_t|x*)] / [π(x_t) q(x*|x_t)])以概率α接受x*否则原地保留x_t重复。当建议分布是对称的时候典型的就是高斯随机游走q(x*|x_t) q(x_t|x*)接受概率可以简化为α min(1, π(x*)/π(x_t))。很多教程用的就是这个简化版因为写起来最简单。我给出一个最小可复现的Matlab代码骨架% 目标分布标准高斯我们知道它可以直接用randn但这里只为了验证MCMC逻辑 logp (x) -0.5 * x.^2; N 5000; % 采样数 x 0; % 初始状态 samples zeros(N, 1); for t 1:N x_star x 1.5 * randn(); % 高斯随机游走建议 log_alpha logp(x_star) - logp(x); if log(rand()) log_alpha x x_star; end samples(t) x; end这段代码可以做任何一元分布的M-H采样只要把logp换成你自己的对数密度即可。注意我用的是log(rand()) log_alpha而不是rand() exp(log_alpha)这是为了数值稳定性下面专门说。2.2 为什么一律写在log域里很多目标分布的密度是很多项相乘比如高斯混合里是多项指数相加。直接算π(x)的值在x离中心区域很远的时候密度可能小到1e-300直接乘除会下溢成0进而导致接受概率算出0/0或0/1这种错误结果。把密度全部写成对数形式接受概率的分子分母就变成了差值数值范围一下从1e-300级别变成几十到几百的普通级别完全在双精度浮点的舒适区里。这是MCMC代码里最容易踩的一个坑。我见过很多初学写法是alpha (target(x_star) / target(x)); if rand() alpha一旦目标分布稍复杂或者维度升高这个式子很快就崩。请一律写成log_alpha log_target(x_star) - log_target(x); if log(rand()) log_alpha2.3 Gibbs采样当条件分布好采样的时候就直接采Gibbs采样是另一种很常用的MCMC机制和M-H的通用性不太一样。Gibbs的思路是多维联合分布难采样但每个维度在“给定其他维度条件下”的条件分布往往很好采样那就一个维度一个维度地来。以二维高斯为例。假设目标分布是零均值的二元正态相关系数ρ0.8。这个联合分布在Matlab里直接用mvnrnd就能采但作为教学例子它可以完美展示Gibbs的机制给定y时x的条件分布是均值为ρy、方差为1-ρ²的正态分布给定x时y的条件分布同理。rho 0.8; N 3000; X zeros(N, 1); Y zeros(N, 1); x 0; y 0; for t 1:N x rho * y sqrt(1 - rho^2) * randn(); y rho * x sqrt(1 - rho^2) * randn(); X(t) x; Y(t) y; endGibbs的好处是不需要手动调建议分布尺度每一步都在朝着高概率区域移动几乎没有“拒绝”的概念收敛速度通常比随机游走M-H快。但它有个前提条件你必须能算出每个维度的条件分布并从中采样。很多模型的条件分布不是标准形式这时候要么退回到M-H要么用“M-H within Gibbs”——有些维度用Gibbs不好采的维度用M-H。实际选型上我的经验是条件分布能写出来的优先Gibbs只能写出联合密度算不出条件密度的用M-H维度很高或者目标分布很扭曲的时候两类都要靠边站得上HMC第5节会讲。3. 一个完整的数据生成项目混合高斯合成数据集3.1 场景定义与目标分布理论说了一堆还是用完整例子把整个流程走一遍。场景是这样一个需求我要给一个二分类聚类算法做压力测试需要生成一个双峰形状的合成数据集。数据本身的形状要求是左边峰值在-2附近、比较集中右边峰值在3附近、稍微分散两个峰权重比大概是3比2。写成目标分布就是混合高斯p(x) 0.6 · N(x; -2, 0.8²) 0.4 · N(x; 3, 1.5²)这个分布直接用逆变换采样很麻烦因为混合分布的CDF反函数没有解析形式用拒绝采样也能做但双峰之间那个低谷区域会浪费大量样本。MCMC则轻松得多我们只需要写对数密度然后跑一个M-H采样器。3.2 采样器主函数与完整Matlab代码%% MCMC生成混合高斯合成数据 % 目标分布0.6*N(-2,0.8^2) 0.4*N(3,1.5^2) rng(42, twister); % 固定随机种子保证可复现 theta [0.6, 0.4]; mu [-2, 3]; sigma [0.8, 1.5]; logp (x) log( theta(1) * normpdf(x, mu(1), sigma(1)) ... theta(2) * normpdf(x, mu(2), sigma(2)) ); N 4000; % 最终保留的样本数 burnin 500; % 预热期 thin 4; % 间隔抽样步数 proposal_sigma 1.8; x 0; samples zeros(N, 1); acc 0; idx 1; for t 1 : (burnin N * thin) % 随机游走建议 x_star x proposal_sigma * randn(); % 对数接受概率 log_alpha logp(x_star) - logp(x); if log(rand()) log_alpha x x_star; acc acc 1; end % 过预热期后按间隔保留样本 if t burnin mod(t - burnin, thin) 0 samples(idx) x; idx idx 1; end end accept_rate acc / (burnin N * thin); fprintf(接受率 %.2f%%\n, accept_rate * 100);这个代码里有几个设计是刻意的解释一下。随机游走的建议步长proposal_sigma 1.8是我预先估的。目标分布整体标准差大约2.7我取了它0.6倍左右的经验值期望的接受率在30%-60%之间。如果你的目标分布尺度完全不同一定要按这个比例重新估计步长。预热期burnin和间隔抽样thin是两个需要解释的概念。预热期丢弃的是链在平稳之前的“漂移过程”样本间隔抽样是为了降低相邻样本的相关性让最终的数据集更接近“独立样本”。这里的取值是工程惯例burnin取几百到几千thin取2到10之间往往已经足够。3.3 输出与基本的收敛性检查跑完代码第一件事不是看直方图而是看trace plot也就是把整个链按顺序画出来。如果链画出来要花很久才从一个区域跳到另一个区域说明自相关很强如果链在某个区间上下波动得很均匀没有长时间躺平在一个值附近说明收敛状态基本健康。figure; subplot(2,1,1); plot(samples, LineWidth, 0.3); title(Trace Plot); xlabel(迭代次数); ylabel(样本值); % 对比直方图与理论密度 subplot(2,1,2); histogram(samples, 60, Normalization, pdf); hold on; xgrid linspace(-7, 9, 500); plot(xgrid, exp(logp(xgrid)), LineWidth, 2); legend(MCMC生成样本, 理论密度);实际跑下来会发现MCMC样本直方图和理论密度曲线吻合得很好。如果发现直方图在某个峰的权重明显偏大另一个峰的比例不够大概率是链还没有足够长或者卡在一个峰里没出来。这个现象我放到第4节专门讲。顺便说一个细节直方图的Normalization, pdf参数会被不理解的人误当成“生成概率”其实它只是让直方图的面积归一化为1这样才方便和理论密度曲线画在同一张图上。3.4 “工程化”细节随机种子、预分配、保存结果写MCMC代码工程上的稳定性很容易被忽略。我踩过几个坑提前列出来。随机种子必须固定。rng(42, twister)这行看着不起眼但如果没有它同样的代码每次跑出来的样本都不同你无法判断一次修改到底改善了收敛还是纯粹是随机波动。实际项目中我建议用两个种子一个固定的开发用种子一个可变的生产用种子。数组预分配很重要。上面代码里我用了zeros(N, 1)预分配内存而不是在循环里不断[samples, new]拼接。N到几万时两者性能差异还不明显但N上到百万级动态拼接会让循环慢几十倍。保存中间结果。生成一批百万级样本往往要跑几分钟如果直接关掉Matlab窗口什么都没了。跑完收敛检查之后建议立刻把样本存成.mat文件后面所有质量分析都从文件读取重跑成本为零。save(mcmc_synthetic_data.mat, samples, mu, sigma, theta);4. 真正跑起来之后才会遇到的坑调参与收敛诊断4.1 接受率过高/过低先调整建议分布尺度M-H采样器最常见的症状是“跑是能跑但结果怎么调都别扭”。这时候先看接受率也就是代码里fprintf打出来的那个百分比。接受率高于70%说明建议步长太小候选样本几乎都在当前点附近打转链移动得很慢生成的数据自相关极高等效样本量很低。接受率低于15%说明建议步长太大候选点经常跳到密度极低的区域被拒绝链长时间原地踏步白烧CPU。理论上随机游走M-H一维目标分布的较优接受率在40%-70%高维目标则通常在20%-30%。调法很简单接受率偏高就把proposal_sigma调大偏低就调小。经验公式是每次用0.5到2倍的倍率去调整不要一上来就大幅改动。跑前几百步先看接受率再决定正式链的长度这是标准做法。还有一个容易被忽略的点接受率本身会随着目标分布的局部形状变化。同一个建议步长在陡峭峰上接受率低在平坦尾部接受率高。所以不要只跑几百步就下结论至少观察几千步再结合trace plot判断。4.2 链卡在某个峰多峰分布与局部陷阱混合高斯是最典型的多峰例子也是MCMC最容易翻车的地方。两个峰之间的密度很低的时候建议步长又不够大链会在一个峰附近逛很久几乎不可能翻越低谷到另一个峰去。具体表现是直方图只有一个峰或者两个峰比例严重失调比如理论权重是0.6对0.4采样出来却是0.95对0.05。这其实不是代码写错了而是链还没能在两个峰之间“串门”。处理办法有几种。最简单的是调大建议步长让链有能力跨越低谷但这会降低接受率需要权衡。比较稳的方法是跑多条链每条链从不同的初始点出发比如一条从-2起步一条从3起步最后把所有链的样本混合在一起统计上这叫“多链合并”虽然相邻样本相关但混合后通常能覆盖多个峰。更彻底的方法是用模拟退火式MCMC先构造一个“烧得足够热”的分布把密度函数取个小于1的指数次幂让峰之间的低谷变浅让链能自由穿越再逐步“降温”回到原分布。实现起来比基本M-H复杂一截但遇到高维强多峰问题几乎必用。实际项目里如果数据只用于测试和仿真多链合并是最性价比高的方案。% 多链并行示意代码 n_chains 4; chain_starts [-5, -1, 2, 5]; all_samples zeros(N, n_chains); for c 1:n_chains x chain_starts(c); s zeros(N, 1); % 这里填同样的M-H主循环 % ... all_samples(:, c) s; end combined all_samples(:); % 合并所有链的样本注意这里有个细节如果多链之间的初始点分布跨度太大合并样本的额定分布确实接近目标分布但每条链自身还没有充分进入平稳状态时合并结果会引入初始点偏差。稳妥做法是每条链各自先跑burn-in再合并。4.3 自相关与有效样本量为什么抽样一万不如有效样本三百MCMC生成的数据不是你想象中的“独立同分布”样本。因为每个点都从上一个点变过来相邻样本天然相关。相关性越强数据里的“新鲜信息”越少。要量化这个问题就看自相关函数ACF。Matlab里如果装了Econometrics Toolbox可以直接用autocorr没装的话自己用几十行代码也能算ml 50; xw samples - mean(samples); r zeros(ml 1, 1); for k 0:ml r(k 1) xw(1:end-k) * xw(k1:end) / (length(xw) - k); end r r / r(1);画出来之后你会看到自相关从1开始随lag下降。一个很实用的判断标准是如果lag50时自相关还没降到0.1以下说明链的记忆太长了需要增加thinning间隔或者回头调建议步长。有效样本量ESS是比单纯样本数N更诚实的指标。近似计算公式是ESS N / (1 2 * sum(ACF的前若干项))用大白话说就是如果相邻样本相关性很强等效的“独立样本数”会远小于N。我见过一个真实案例一个高维模型采了一万样本算下来ESS只有三百也就是说这“一万”样本的有效信息量只相当于三百个独立样本很多分析结论其实是在靠运气支撑。实际工程里我不建议盲目加大thinning来追求“低自相关”因为thinning会浪费大量计算样本。更推荐的做法是保留全链样本用于最终数据生成但做统计推断时用ESS指导有效样本量评估。数据生成任务对“不完全独立”其实没那么敏感——后面直方图拟合质量能通过检验自相关影响可以忽略。4.4 多链并行与Gelman-Rubin诊断前面提到多链合并这里补一个正式的多链收敛诊断Gelman-Rubin的R-hat值。思路是跑至少两条链比较链内方差和链间方差如果链间均值差异明显大于链内波动说明各条链还没有收敛到同一个分布R-hat远大于1。经验阈值是R-hat小于1.1越接近1越好。Matlab里实现这个诊断只需要简单统计量不需要特殊工具箱。我自己的习惯是新写一个MCMC代码先跑4条链各2000步算一次R-hat再决定正式采样长度。这样能避免把大量时间浪费在一条根本不收敛的链上。这个诊断方法对第3节提到的混合高斯多峰场景特别有用如果两条链从不同峰出发始终没有交换位置R-hat必然超标直接暴露问题。文档上经常写MCMC是“理论上收敛”的算法但工程上必须靠诊断保证“实际上收敛”。5. 生成质量验证、实际用法与更好工具的进阶方向5.1 判断样本像不像真分布直方图、QQ图和统计检验数据生成之后最重要的问题是生成的数据真的和目标分布一致吗我的标准三件套是直方图叠加理论密度、QQ图、矩统计对比。直方图叠加密度曲线最直观肉眼就能发现双峰比例失调。QQ图更适合看尾部把生成样本的分位数和目标分布的理论分位数点对点画出来如果点基本落在一条直线上说明分布整体拟合不错尾部上翘或下弯则说明重尾区域有偏差。矩统计对比可以量化评估。计算生成数据的均值、方差、偏度、峰度和目标分布的理论值对比。混合高斯分布的矩可以手算也可以用样本去逼近。误差在1%-3%以内通常是可以接受的。gen_mean mean(samples); gen_var var(samples); gen_skew skewness(samples); gen_kurt kurtosis(samples); fprintf(均值%.3f (理论%.3f)\n, gen_mean, 0.6*(-2) 0.4*3);再专业一点可以跑Kolmogorov-Smirnov检验但要注意一个坑KS检验的p值在样本量较大时会变得过于敏感即使分布几乎完全一致也会给出显著差异的结论。所以KS检验更适合当作“拒绝假设”的线索不适合当作“生成成功”的证据。数据生成质量把关最终还是要靠分布形态本身。5.2 这些生成数据能做什么三类高频场景我在文章开头提到的三类场景这里展开成具体落地方案。第一类是合成数据测试。给聚类、分类、异常检测算法做压力测试时需要“指定形状”的数据集。用MCMC的好处是你对数据真实分布有完全的控制权——你写出来的logp就是数据生成真相不像真实数据那样混杂各种未知因素。这样当算法表现不好时你可以确定是算法问题而非数据问题。第二类是贝叶斯后验预测。先对模型参数做MCMC采样得到一组后验参数样本再对每个参数样本仿真一次新观测得到的观测集就是后验预测分布。在风险评估、可靠性分析里这组数据可以直接用来计算极端事件的概率比单一参数取值做预测要稳得多。第三类是缺失数据填补。假设一个时间序列缺失了若干段你可以基于已有数据的模型用MCMC从缺失段的条件分布中反复采样每次采样得到一份完整的填补数据集。多份数据集之间的差异就反映了填补的不确定性这是普通插值方法给不了的。5.3 向上进阶切片采样、HMC和NUTS以及工具箱给你省掉的代码如果你不打算手写主循环Matlab的Statistics and Machine Learning Toolbox自带mhsample和slicesample。前者封装了标准的M-H逻辑支持自定义建议分布后者实现切片采样slice sampling它自动适应目标分布尺度省去了调建议步长这一步。做大作业或快速实验时可以直接用但当你要批量生成百万级数据或者需要精细控制链行为时手写循环反而更灵活、更好调试。如果你的数据维度到了几十维以上随机游走M-H的效率会断崖式下降因为高维空间里方向性很强的随机游走几乎都是在乱撞。这时候应该考虑哈密尔顿蒙特卡洛HMC。Hamming、NUTS这些进阶算法在Python生态比较流行Matlab这边也有第三方实现但集成度和文档丰富程度稍弱一些。HMC的核心思路是引入动量变量让采样过程模拟物理动力学沿梯度方向大步前进大幅降低相邻样本的相关性。代价是必须能计算目标分布对数密度的梯度。如果你的项目里有混合模型、层次贝叶斯这类高维问题强烈建议把HMC作为下一步学习重点。MCMC这块的知识是越用越深的但底层的Metropolis-Hastings和Gibbs永远是地基把地基打牢后面上什么高级算法都顺畅。我个人最后想分享的一个小习惯是每次写MCMC代码前先写一行目标分布的精确形态定义并打印出来。我吃过太多亏——花了几个小时调参最后发现密度函数写错了一个符号导致链一直收敛不到预期分布。把“要生成什么分布”和“实际生成了什么分布”这两件事用可视化和统计检验钉死MCMC数据生成就成功了一大半。
返回列表