
做仿真、做数据建模的朋友估计都遇到过这种尴尬手里的目标分布根本不是教科书上那种标准分布Matlab自带的rand、randn和那些内置随机数生成器全都派不上用场。逆变换采样要先求出CDF再求反函数很多复杂分布根本没有解析解拒绝采样在高维空间里接受率低到让人崩溃。我最早被这个问题卡住是在一个贝叶斯模型里后验密度只有一个未归一化的表达式需要生成一批服从该后验的参数样本绕了一大圈才发现真正靠谱的思路是马尔科夫链蒙特卡洛Markov Chain Monte CarloMCMC。这篇文章把完整的实现过程整理出来——从原理、Matlab代码、收敛诊断到一个贝叶斯线性回归案例最后还会聊几个调试时容易踩的坑。想用Matlab做数据生成、参数抽样或者刚开始接触MCMC的读者可以直接照着跑。1. 为什么复杂分布的数据生成绕不开MCMC1.1 逆变换采样和拒绝采样的天花板先说说为什么常规方法搞不定。**逆变换采样Inverse Transform Sampling**的思路是先产生均匀分布随机数u再通过目标分布CDF的反函数F^(-1)(u)得到样本。这个方法在理论上很漂亮但前提是能写出F^(-1)的解析表达式。正态分布、指数分布、均匀分布这些经典族没问题但一旦遇到混合分布、截断分布、或者只有密度函数没有CDF的分布这条路基本走不通。**拒绝采样Rejection Sampling**看似通用找一个容易采样的提议分布q(x)再按某个标准接受或拒绝候选点。问题在于接受概率高度依赖提议分布和真实分布的重合程度。目标分布是多峰的时候提议分布很难同时覆盖所有峰维度稍微高一点接受率随维度指数下降这就是所谓的“维数灾难”。我试过用拒绝采样从一个五维后验里采样跑了半天样本量还是不够用。1.2 MCMC的思路转变放弃独立性换取可行性MCMC用了一个完全不同的策略放弃“每次采样互相独立”这个执念构造一条马尔科夫链让链在状态空间里随着迭代次数不断游走最终这条链的平稳分布恰好是目标分布。链上的样本虽然彼此相关但只要链收敛了把足够多次迭代留下的状态收集起来这些状态的统计特征就逼近目标分布的特征。用一个生活化的类比想统计一座城市居民的收入分布最直接的思路是随机抽取大量独立个体但这要求手里有一份完整的抽样框拒绝采样相当于反复抛硬币决定要不要记录某个人遇到分布复杂时代价极高。MCMC的做法更像是“随机游走式入户调查”从某个地方出发每次按规则随机移动到下一个受访者只要移动规则设计得合理你在城市里待足够久记录下来的收入分布就自然趋向真实的收入分布。这座城市就是状态空间移动规则就是转移核记录的数据就是我们要生成的样本。什么时候应该认真考虑MCMC我一般按这三条判断目标分布只有函数形式、没有解析CDF或CDF反函数分布形态复杂多峰、有界、截断、高维、高度偏斜需要从贝叶斯后验中抽样而后验通常只已知到未归一化的密度。命中任意一条MCMC就是值得优先尝试的方案。2. MCMC的核心机制从马尔科夫链到Metropolis-Hastings接受规则2.1 马尔科夫链与转移核马尔科夫链的核心假设是下一时刻的状态只依赖于当前状态与更早的历史无关。用数学语言说转移核P(x→x)描述了从状态x出发转移到x的概率密度。这个“只记上一脚”的特性和醉汉随机游走的逻辑很像——醉汉下一步往哪走只取决于他当前站在哪里不需要回忆之前走过的路线。2.2 细致平衡让链稳定下来的关键条件只是构造一条链还不够必须保证链的长期行为稳定在目标分布π上。这时候就用到细致平衡条件Detailed Balance如果转移核满足π(x) P(x→x) π(x) P(x→x)那么π就是这条链的平稳分布也就是说链跑足够久之后状态x出现的频率会收敛到π(x)。这个等式表达的意思很直白从x流向x的概率质量恰好等于从x流向x的概率质量两边达成动态平衡。MCMC的设计目标由此变成——构造一个满足细致平衡条件的转移核。2.3 Metropolis-Hastings接受规则如何构造这样的转移核Metropolis-Hastings简称MH算法给出了一个精巧的答案。从当前状态x出发从提议分布q(x|x)中抽一个候选值x然后计算接受概率α(x→x) min(1, [π(x) q(x|x)] / [π(x) q(x|x)])以概率α接受x否则停在原地x。这里最关键的一点是接受概率只依赖目标分布的比值π(x)/π(x)分母里的归一化常数会相互约掉。所以即使目标分布只有一个未归一化的密度函数也就是只知道π(x) c·f(x)中的f(x)而不知道常数c照样可以精确采样。这在贝叶斯推断里是决定性的优势——后验分布几乎总是只知道分子不知道分母。2.4 为什么链能收敛到目标分布专门用随机游走提议对称分布做一个简化版本这时q(x|x) q(x|x)接受概率变成min(1, π(x)/π(x))。直观理解很自然如果候选点比当前点更“可能”目标密度更高几乎总是接受如果候选点密度更低则按比例概率接受。这个机制保证了链在目标分布密度高的区域停留更久、访问频率更高同时在低密度区域也能偶尔跨越避免被困在单一模态里。长期统计下来访问频率就匹配目标密度这就是“链的平稳分布等于目标分布”背后的直观原因。3. Matlab实现一个通用MH采样器与三种分布实测3.1 通用采样器代码对数域计算与循环写法理论讲再多不如直接看代码。先写一个最通用的一维随机游走MH采样器建议直接用对数密度作为输入好处是避免概率乘积下溢也让边界处理-Inf变得自然。function [samples, acc_rate] mh_sampler(log_target, x0, n_samples, sigma, burnin) % 随机游走Metropolis-Hastings采样器一维 % log_target : 目标分布的对数密度函数句柄(x) log(pi(x)) % x0 : 起始点尽量选在目标分布主体内部 % n_samples : 需要保留的样本数量 % sigma : 高斯提议分布的标准差控制步长 % burnin : 预热期迭代次数提前丢弃的样本 if nargin 5 burnin round(n_samples * 0.2); end total n_samples burnin; samples zeros(n_samples, 1); x x0; acc 0; for i 1:total x_star x sigma * randn(); % 随机游走提议 log_alpha log_target(x_star) - log_target(x); % 对称提议下的对数接受概率 if log(rand()) log_alpha x x_star; if i burnin acc acc 1; end end if i burnin samples(i - burnin) x; end end acc_rate acc / n_samples; end这里有两个工程上的细节。第一为什么用log(rand())而不是直接rand()比较α因为候选点密度很低时π(x)/π(x)可能小到超出双精度浮点数的表示范围取对数之后数值稳定性好得多。第二MCMC本身是顺序采样没法像一般统计计算那样整体向量化循环不可避免但建议预先分配samples数组而不是动态增长Matlab跑起来会快不少。3.2 自检采样正态分布验证代码正确性写完之后先别急着用用一个已知解析分布的案例验证代码。下面用MCMC从N(2, 1.5²)采样和normpdf的真值曲线对比mu 2; sig 1.5; log_target (x) -0.5 * ((x - mu) ./ sig).^2 - log(sig) - 0.5 * log(2 * pi); rng(2024); [samples, acc_rate] mh_sampler(log_target, 0, 20000, 1.2, 2000); fprintf(接受率: %.2f\n, acc_rate); figure; histogram(samples, 100, Normalization, pdf); hold on; xs linspace(-5, 9, 300); plot(xs, normpdf(xs, mu, sig), r-, LineWidth, 2); xlabel(x); ylabel(概率密度); legend({MCMC 样本, 真实正态密度}, Location, northwest);我实测跑下来的接受率大约0.68直方图和真实密度曲线几乎完全重合。这一步确认了采样器本身没有逻辑错误可以用于更复杂的分布。3.3 多峰分布混合高斯的采样真正体现MCMC价值的是多峰分布。比如生成一个双峰混合高斯0.3·N(-2, 0.8²) 0.7·N(3, 1.2²)的样本这种分布的CDF和反函数没有简洁解析式用逆变换方法非常痛苦log_target (x) log(0.3 * normpdf(x, -2, 0.8) 0.7 * normpdf(x, 3, 1.2)); [samples, acc_rate] mh_sampler(log_target, 0, 30000, 1.5, 3000); fprintf(接受率: %.2f\n, acc_rate); figure; histogram(samples, 120, Normalization, pdf); hold on; xs linspace(-6, 8, 400); true_density 0.3 * normpdf(xs, -2, 0.8) 0.7 * normpdf(xs, 3, 1.2); plot(xs, true_density, r-, LineWidth, 2); xlabel(x); ylabel(概率密度);关键是步长σ1.5设置得比较大让链有机会在两个峰之间来回跨越。如果把σ改成0.1链就会在左边那个峰附近打转右边峰几乎访问不到生成的样本就不能代表真实混合分布——这是调试MCMC时一个非常典型的现象后面专门讲。3.4 有界支撑Beta分布与边界处理如果目标分布的支撑集不是整个实数轴比如Beta(2,5)定义在(0,1)上那么提议分布产生的候选点可能越界。此时必须让对数密度在越界时返回-Inf接受概率自动变成0候选点自然被拒绝。我建议写成独立子函数不要用匿名函数硬凑function lp log_beta_pdf(x, a, b) % 对数Beta密度支撑集(0,1) if x 0 x 1 lp (a - 1) * log(x) (b - 1) * log(1 - x) - betaln(a, b); else lp -Inf; end end调用方式a 2; b 5; log_target (x) log_beta_pdf(x, a, b); [samples, acc_rate] mh_sampler(log_target, 0.5, 20000, 0.15, 2000); fprintf(接受率: %.2f\n, acc_rate); figure; histogram(samples, 80, Normalization, pdf); hold on; xs linspace(0.001, 0.999, 200); plot(xs, betapdf(xs, a, b), r-, LineWidth, 2); xlabel(x); ylabel(概率密度);这里步长σ0.15是刻意选的Beta(2,5)的均值在0.29附近标准差大约0.16步长和分布尺度匹配接受率才能维持合理水平。4. 采样后别急着画直方图收敛诊断与样本质量控制4.1 Trace Plot先看链有没有动起来拿到样本后的第一件事不是histogram而是画轨迹图Trace Plot。只看前1000个迭代的样本轨迹基本就能判断链是否正常混合figure; subplot(2, 1, 1); plot(samples(1:1000)); xlabel(迭代序号); ylabel(x); title(Trace Plot前1000个样本);正常的轨迹应该在目标分布的几个区域之间来回穿梭看起来像一片均匀的“毛虫”。如果轨迹长时间停留在一个值附近或者出现明显的上升/下降趋势说明链还没进入平稳状态或者混合能力太差。我个人的习惯是任何一次MCMC跑完先花十秒钟看轨迹图再决定要不要接受这批样本。4.2 Burn-in预热期的时间怎么定链从初始点出发需要一段“热身”时间才能进入目标分布的主体区域这段被丢弃的迭代就是burn-in预热期。上面代码里我习惯取总迭代次数的20%作为burn-in但这只是默认值。更合理的做法是看轨迹图如果在某一次迭代之后链彻底离开了初始点附近并且之后的行为稳定那么从这个点开始之后的样本都可以保留。理论保证只针对平稳状态之后的样本burn-in之前的数据一律不要用于统计推断。4.3 自相关与抽稀什么时候需要ThinningMCMC样本天生自相关相邻样本的信息重叠程度很高。如果画自相关图会看到延迟k增大时自相关系数缓慢衰减。抽稀thinning就是每隔几步保留一个样本用来降低相关性。Matlab里没有专用工具箱时可以手动计算自相关subplot(2, 1, 2); L 30; s_centered samples - mean(samples); v var(samples); acf zeros(L1, 1); acf(1) 1; for k 1:L acf(k1) mean(s_centered(1:end-k) .* s_centered(k1:end)) / v; end stem(0:L, acf, MarkerSize, 4); xlabel(延迟 k); ylabel(自相关);如果自相关衰减得非常慢比如延迟20时还在0.5以上说明链的混合很差可以考虑每10步或每20步抽一个样本。不过我的经验是抽稀治标不治本真正的问题是步长或参数化方式不合理调步长比强行thinning更划算。4.4 有效样本量与多链对比只看自相关还不够更定量的指标是有效样本量Effective Sample SizeESS。MCMC提供的n个样本因为相关性的存在信息量只相当于若干个独立样本。一个工程上可用的近似计算L 30; s_centered samples - mean(samples); v var(samples); rho zeros(L, 1); for k 1:L rho(k) mean(s_centered(1:end-k) .* s_centered(k1:end)) / v; end sum_rho 0; for k 1:L if rho(k) 0.05 sum_rho sum_rho rho(k); else break; end end ESS length(samples) / (1 2 * sum_rho); fprintf(有效样本量 ESS %.0f链长 %d\n, ESS, length(samples));ESS和链长的差距越大说明样本越“不值钱”。另一个实用习惯是并行跑4条独立链从不同初始点出发如果几条链得到的后验均值、分位数都差不多说明收敛基本可靠。如果某条链明显偏离大概率是初始点选在了奇怪的位置或者目标分布存在多个局部模态。5. 完整案例用MCMC生成贝叶斯后验预测数据5.1 问题设定参数后验没有解析形式形式化的展示告一段落来一个能直接迁移到实际工作的案例。假设有一组观测数据(x_i, y_i)满足线性关系y a·x b ε其中ε ~ N(0, σ²)σ已知为1。现在需要估计斜率a和截距b的联合后验分布并基于后验生成新的y值。这个场景在工程预测、仿真实验里非常常见模型参数本身不确定我们想生成的不是“某条最优拟合线”的预测而是包含参数不确定性在内的完整预测分布。先验取a ~ N(0, 5²)b ~ N(0, 5²)。似然函数是高斯形式所以对数后验可以写成rng(42); x_obs linspace(0, 10, 25); a_true 2; b_true 1; sigma 1; y_obs a_true * x_obs b_true sigma * randn(size(x_obs)); log_prior (a, b) -0.5 * (a / 5)^2 - 0.5 * (b / 5)^2; log_lik (a, b) -sum((y_obs - a * x_obs - b).^2) / (2 * sigma^2); log_post (a, b) log_prior(a, b) log_lik(a, b);5.2 二维MH实现与接受率观察二维情形需要把提议分布换成二元高斯协方差矩阵控制联合步长。注意这里提议分布是多元的接受概率仍然是对称提议下的密度比和前面推导完全一致。Sigma_prop 0.15 * eye(2); n_samples 30000; burnin 5000; post_samples zeros(n_samples, 2); theta [a_true; b_true]; % 用真实值附近做初值链起步更稳 acc 0; for i 1:(n_samples burnin) theta_star theta mvnrnd([0; 0], Sigma_prop); if log(rand()) log_post(theta_star(1), theta_star(2)) - log_post(theta(1), theta(2)) theta theta_star; if i burnin acc acc 1; end end if i burnin post_samples(i - burnin, :) theta; end end acc_rate acc / n_samples; fprintf(接受率: %.2f\n, acc_rate);这一步跑出来接受率大约0.4左右采样效率不错。把后验样本画成散点图可以看到a和b之间存在明显的负相关——这是线性回归里很自然的现象斜率估计高了截距估计往往就要低一点才能拟合同样的数据。5.3 从后验样本生成预测数据现在进入标题里“数据生成”的核心环节。要生成新输入点x_new对应的y值不能只用一组“最优参数”去计算因为参数本身是不确定的。正确做法是从后验样本中随机抽取若干组(a, b)对每一组参数再叠加观测噪声ε这样得到的就是完整后验预测分布。x_pred linspace(0, 10, 50); n_pred 5000; idx randi(n_samples, n_pred, 1); y_pred_all zeros(n_pred, length(x_pred)); for j 1:n_pred a_j post_samples(idx(j), 1); b_j post_samples(idx(j), 2); y_pred_all(j, :) a_j * x_pred b_j sigma * randn(size(x_pred)); end y_mean mean(y_pred_all); y_lo quantile(y_pred_all, 0.05); y_hi quantile(y_pred_all, 0.95); figure; plot(x_obs, y_obs, ko, MarkerFaceColor, k); hold on; plot(x_pred, a_true * x_pred b_true, k--, LineWidth, 1.5); plot(x_pred, y_mean, b-, LineWidth, 2); fill([x_pred; flipud(x_pred)], [y_lo; flipud(y_hi)], ... [0.8 0.8 1], EdgeColor, none, FaceAlpha, 0.4); xlabel(x); ylabel(y); legend({观测值, 真实直线, 后验预测均值, 90%预测区间}, Location, northwest);这里的y_pred_all矩阵就是“用MCMC生成的数据”每一行代表一条可能的未来观测序列包含了参数不确定性后验样本的波动和观测噪声σ·randn两层随机性。90%预测区间比单纯回归标准误更直观地表现了不确定性范围而且没有任何解析近似全部来自实际样本。如果之后还需要生成更多数据比如给某个仿真系统喂输入直接用这个矩阵继续抽样即可。6. 调试MCMC的实用经验和几个值得注意的坑6.1 步长选不好链就废了步长σ是随机游走Metropolis里最敏感的参数。根据我反复调试的经验可以用一张表概括不同步长下的表现步长类型接受率链的行为实际后果过小高于0.6每次只挪一小步链混合慢自相关极强ESS很低适中0.2~0.4偶尔接受远距离跳跃探索和接受平衡推荐过大低于0.1候选点经常落在密度极低区域链长时间原地踏步等于没跑一个不能机械照搬的经验是先按目标分布标准差的数量级设步长跑几百步看轨迹图然后根据接受率调整。如果分布是多峰的步长至少要能和峰间距相比拟否则链很难跨峰。我之前在混合高斯调试时把σ从0.5调到1.5之后右峰才真正被访问到。6.2 边界、支撑集与NaN陷阱有界分布的边界处理必须用-Inf而不是NaN。很多新手在写Beta分布对数密度时会直接写log(x)x一旦越界得到NaNNaN在比较运算中会静默传播——log(rand()) NaN永远是false链会无限拒绝所有状态看起来像“卡住”了但其实是在无效计算里打转。正确做法是子函数里用if判断显式返回-Inf。同理截断分布、带有约束的参数空间比如方差必须为正都建议用这个模式。6.3 初始点的重要性与多维性能问题初始点最好选在目标分布主体内部比如后验的众数附近或先验均值附近。从密度极低的区域出发链需要很长的burn-in才能爬回主体区域过程中还可能被数值问题困扰。如果初始点落点的对数密度是-Inf整个链的接受概率都会变成NaN表现为“一切都被拒绝”这时候最先检查的就是初始点。多维情形的坑更多。随机游走MH在高维下收敛速度明显下降因为提议分布同时移动所有维度每个方向都要兼顾相当于在维度空间里“乱枪打鸟”。两个常用替代方案一是逐分量更新每步只对其中一个维度做MH提议相当于把多维问题拆成一串一维问题二是在线性回归这类参数后验近似高斯的问题上先粗略估计后验协方差再把它缩放后当成提议协方差使用。真正的高维问题几十上百维则建议考虑HMC或NUTS这些更高级的采样器但那已经超出了本文随机游走MH的讨论范围。6.4 Matlab运行效率的几条小建议MCMC顺序采样的本质决定了它没法大规模向量化但还是有些提速技巧。预先分配数组是第一位的不要在循环内动态增长数组对数密度函数里尽量用向量化计算批量处理观测数据比如贝叶斯线性回归里一次算完整条似然如果确实需要跑多条独立链做收敛诊断Matlab的parfor可以并行执行但要注意每个worker的随机数流独立否则并行和串行没区别。实测下来几万次迭代配合简单似然函数Matlab跑起来毫无压力真正的瓶颈只在高维或超大数据集的似然计算上。我个人做MCMC项目最后总结出的一条体会是MCMC不是万能钥匙能用randn、rand或者内置随机数生成器解决的分布优先用简单方法。MCMC的价值在于把“理论上可以采样、实现起来困难重重”的分布变成真正可用的样本代价是要花心思调步长、判断收敛、处理边界。调试时多花一分钟看轨迹图往往比盲目多跑十万次迭代更有效。如果哪天你被某个奇怪的密度函数卡住我还有个建议先画一下目标分布的函数图像看看密度主体落在哪、有几个峰、支撑集在哪再决定初始点和步长——这一步几乎没人写进文档里但真的能省下大量试错时间。