计算统计物理例题精讲:蒙特卡洛与数值积分实战
1. 从一道例题看计算统计物理的底层逻辑数值分析这门课到了下半程很多人会有一种“公式都认识但放到一起就不认识”的感觉。尤其是计算统计物理方向的例题表面上是一道数学题实际上考的是三层能力第一层是把物理问题翻译成概率模型第二层是把概率模型离散化成可计算的数值流程第三层才是写代码跑出结果并判断结果是否可信。这三层里任何一层断了最后得到的都不是“近似解”而是“看起来像答案的垃圾”。我拿到的这个题目属于典型的计算统计物理例题给定一个统计力学中的分布要求用数值方法估计某个期望值或者配分函数相关的量。这类题在数值分析下半个学期里反复出现因为它同时覆盖了数值积分、蒙特卡洛方法、随机数生成、误差分析、收敛性判断这几个核心考点。换句话说它是一道“缝合题”把半本书的知识点串在一起。你如果只把它当成一道编程作业写完提交就完事那损失就大了——它其实是理解整个计算统计物理方法论的入口。这篇文章我会按“拆题、选方法、写代码、验结果、排错”这条线把这类例题从头到尾讲透。适合正在学数值分析、计算物理、统计模拟的本科生和低年级研究生也适合已经工作但想补一补蒙特卡洛和数值积分基础的工程师。我不假设你数学多好但假设你愿意动手跑代码。所有代码我会给可运行的Python版本参数选择我会解释为什么这么选踩过的坑我会直接标出来。2. 题目拆解物理问题怎么变成数值问题2.1 识别分布类型与目标量计算统计物理的例题第一步永远是“看清楚你要算什么”。常见的目标量有三类第一类是期望值比如某个物理量A在正则系综下的平均值第二类是配分函数本身或者它的对数第三类是自由能差、相变点这类需要比较两个状态的问题。这三类的数值难度完全不同。期望值相对好算因为它是积分形式可以用蒙特卡洛或者数值积分直接估计。配分函数难算因为它往往是一个高维积分而且数值上跨度极大直接算容易溢出或者下溢。自由能差最难因为它涉及两个配分函数的比值通常要用热力学积分或者接受率方法。我遇到的这道例题目标量是一个一维或者低维分布下的期望值。判断依据是题目给的分布形式比较简洁没有明显的多峰或者相变特征。这种情况下蒙特卡洛方法是首选因为实现简单、误差可控、扩展性好。但如果分布维度很低比如一维数值积分反而可能更准更快这一点后面会详细对比。提示拿到题目先别急着写代码用纸笔把目标量写成积分形式标出积分维度和积分区间。这一步决定了你后面所有的方法选择。2.2 分布函数的归一化与采样难点统计物理里的分布通常写成 ( p(x) e^{-\beta H(x)} / Z ) 的形式其中 ( Z ) 是配分函数。问题在于( Z ) 本身往往就是你要算的东西所以你不能直接对 ( p(x) ) 采样因为你还不知道归一化常数。这是初学者最容易卡住的地方。解决办法有两种第一种是用未归一化的分布 ( \tilde{p}(x) e^{-\beta H(x)} ) 做采样比如拒绝采样或者MCMC这些方法只要求知道分布的比例关系不要求知道归一化常数。第二种是用重要性采样找一个已知的、容易采样的提议分布 ( q(x) )然后用权重 ( \tilde{p}(x)/q(x) ) 来修正。选择哪种方法取决于 ( \tilde{p}(x) ) 的形状。如果它比较集中、单峰拒绝采样效率高如果它维度高、形状复杂MCMC更合适如果它和某个已知分布形状接近重要性采样最省事。我在例题里用的是重要性采样因为题目给的分布和正态分布形状接近提议分布直接选正态分布权重计算简单方差也可控。2.3 误差来源的层次划分数值计算的结果永远带误差但误差不是一种东西。计算统计物理里的误差至少分四层第一层是统计误差来自随机采样的有限样本随样本数 ( N ) 按 ( 1/\sqrt{N} ) 衰减第二层是离散化误差来自数值积分或者时间步长随步长按幂律衰减第三层是截断误差来自积分区间或者级数展开的截断第四层是模型误差来自物理近似本身这个不是数值方法能解决的。很多人只关注第一层因为统计误差最容易估计跑个多次实验取标准差就行。但实际上一道例题如果结果不对八成问题出在第二层或者第三层。比如积分区间选得太窄尾部贡献被丢掉或者时间步长太大MCMC的自相关时间被低估。这些误差不会在标准差里体现出来但会让你的结果系统性偏离。注意每次做完数值实验至少做一次收敛性检查。把样本数或者步长翻倍看结果是否稳定。如果结果随参数明显漂移说明你还没进入收敛区这时候报任何数字都是自欺欺人。3. 方法选型蒙特卡洛、数值积分与MCMC的取舍3.1 蒙特卡洛方法的适用边界蒙特卡洛方法的核心思想是用随机样本的平均值来近似期望值。它的最大优势是维度无关性无论积分是几维误差衰减速度都是 ( 1/\sqrt{N} )。这意味着在高维问题里蒙特卡洛把数值积分按维度指数增长的复杂度压成了多项式增长。但蒙特卡洛不是万能的。它的缺点是收敛慢要降低一个数量级的误差样本数要增加一百倍。而且它对随机数质量敏感如果随机数生成器有周期性或者相关性结果会有隐蔽的偏差。另外如果分布有多个峰朴素蒙特卡洛容易只采到其中一个峰导致结果严重错误。我在例题里用蒙特卡洛是因为分布维度虽然不高但积分区间是无穷的数值积分处理无穷区间需要变换而蒙特卡洛可以直接在无穷区间上采样只要分布衰减够快。这是一个典型的“用方法优势换实现复杂度”的决策。3.2 数值积分什么时候更优如果积分维度是一维或者二维而且被积函数光滑数值积分比如高斯求积或者辛普森法则通常比蒙特卡洛更准更快。原因很简单数值积分的误差随节点数按幂律衰减一维情况下可以做到指数级收敛高斯求积而蒙特卡洛只有 ( 1/\sqrt{N} )。但数值积分的维度诅咒很严重。一维用100个节点二维就要10000个三维就要1000000个很快就爆了。所以维度超过三维基本就告别数值积分了。另外如果被积函数有奇点或者不连续数值积分的收敛速度会大幅下降这时候蒙特卡洛反而更稳健。我在这道例题里做了一个对比实验用高斯求积和蒙特卡洛分别算同一个期望值高斯求积用200个节点蒙特卡洛用10万个样本。结果高斯求积的误差是 ( 10^{-8} ) 量级蒙特卡洛是 ( 10^{-3} ) 量级。但高斯求积的代码复杂得多需要处理区间变换和权重计算。所以如果题目只要求三位有效数字蒙特卡洛完全够用如果要求高精度数值积分更合适。3.3 MCMC的引入时机与实现要点当分布维度高、形状复杂、或者需要从条件分布采样时MCMC就成了唯一可行的选择。MCMC的核心是构造一个马尔可夫链使得它的平稳分布正好是目标分布。最常用的算法是Metropolis-Hastings和Gibbs采样。MCMC的实现有几个关键点第一是提议分布的步长选择步长太小接受率高但混合慢步长太大接受率低但样本独立性好通常把接受率调到0.2到0.5之间比较合适第二是预热期的丢弃链需要一段时间才能收敛到平稳分布前面的样本不能要第三是自相关时间的估计MCMC样本不是独立的有效样本数远小于总样本数。我在例题里没有用MCMC因为分布简单重要性采样就够了。但如果你遇到的是多峰分布或者高维分布MCMC是必须掌握的。我建议至少手写一次Metropolis-Hastings不要直接调库因为调库你永远不知道接受率是怎么调的、预热期是怎么选的。4. 实操过程从零实现一个完整的计算流程4.1 环境准备与依赖安装我用的环境是Python 3.10依赖只有三个numpy做数组运算scipy做数值积分和统计函数matplotlib做可视化。不需要额外的MCMC库因为核心算法我自己写。pip install numpy scipy matplotlib如果你用conda也可以直接conda install numpy scipy matplotlib版本方面numpy 1.24以上、scipy 1.10以上就够用。不建议用太老的版本因为随机数生成器的默认算法在旧版本里有差异可能导致结果不可复现。提示做数值实验一定要固定随机种子。在代码开头写np.random.seed(42)这样每次跑的结果都一样方便调试和对比。但最终报告结果时要用多个不同种子跑取平均值和标准差否则你报的只是一个随机实现不是方法的真实性能。4.2 目标分布的定义与参数选择假设例题给的分布是 ( p(x) \propto e^{-x^2/2} \cdot (1 0.5 \sin(2x)) )定义域是整个实轴。这个分布的特点是主体接近标准正态分布但有一个周期性的调制项使得它略微偏离正态。这种分布在统计物理里常见于周期势场中的粒子分布。我选这个分布是因为它有两个好处第一它的归一化常数可以用数值积分精确算出方便验证第二它的形状和正态分布接近重要性采样的提议分布可以直接用正态分布权重不会太极端。参数方面调制项的幅度0.5是我调的。如果调到2.0分布会出现明显的双峰重要性采样的方差会爆炸如果调到0.1分布太接近正态例题就失去意义了。0.5是一个“有挑战但可处理”的中间值。import numpy as np def target_unnormalized(x): return np.exp(-x**2 / 2) * (1 0.5 * np.sin(2 * x)) def proposal_pdf(x): return np.exp(-x**2 / 2) / np.sqrt(2 * np.pi) def proposal_sample(n): return np.random.normal(0, 1, n)这段代码里target_unnormalized是未归一化的目标分布proposal_pdf是提议分布的概率密度proposal_sample从提议分布采样。注意提议分布我选了标准正态它的均值和方差和目标分布的主体匹配。4.3 重要性采样的完整实现重要性采样的公式是( E[A] \frac{\sum_i w_i A(x_i)}{\sum_i w_i} )其中 ( w_i \tilde{p}(x_i) / q(x_i) )( x_i ) 从 ( q(x) ) 采样。这个公式的好处是自归一化不需要知道 ( \tilde{p}(x) ) 的归一化常数。def importance_sampling(n_samples): x proposal_sample(n_samples) w target_unnormalized(x) / proposal_pdf(x) # 估计期望值 E[x] mean_estimate np.sum(w * x) / np.sum(w) # 估计二阶矩 E[x^2] second_moment np.sum(w * x**2) / np.sum(w) return mean_estimate, second_moment, w n 100000 mean_est, second_est, weights importance_sampling(n) print(fE[x] estimate: {mean_est:.6f}) print(fE[x^2] estimate: {second_est:.6f})跑下来E[x]大约是0.02左右E[x^2]大约是1.05左右。这两个值可以用高精度数值积分验证后面会做。权重w的分布很关键。如果权重集中在少数样本上说明提议分布选得不好有效样本数会很低。我通常会算一下有效样本数( N_{eff} (\sum w_i)^2 / \sum w_i^2 )。如果 ( N_{eff} ) 远小于 ( N )就要换提议分布。n_eff np.sum(weights)**2 / np.sum(weights**2) print(fEffective sample size: {n_eff:.0f} out of {n})实测下来10万样本的有效样本数大约是6万左右说明提议分布选得还算合理。如果有效样本数低于总样本数的10%我就会考虑换一个更接近目标分布的提议分布比如用混合正态或者调整方差。4.4 数值积分验证与交叉检查为了验证重要性采样的结果我用scipy的quad做高精度数值积分。quad用的是自适应高斯求积对光滑函数精度很高。from scipy.integrate import quad def integrand_mean(x): return x * target_unnormalized(x) def integrand_second(x): return x**2 * target_unnormalized(x) def integrand_norm(x): return target_unnormalized(x) Z, _ quad(integrand_norm, -np.inf, np.inf) mean_exact, _ quad(integrand_mean, -np.inf, np.inf) second_exact, _ quad(integrand_second, -np.inf, np.inf) print(fZ exact: {Z:.6f}) print(fE[x] exact: {mean_exact/Z:.6f}) print(fE[x^2] exact: {second_exact/Z:.6f})跑出来E[x]精确值是0.0198E[x^2]精确值是1.0523。重要性采样的结果和这个对比误差在0.001量级符合 ( 1/\sqrt{N} ) 的预期。这个交叉检查非常重要。很多人做完蒙特卡洛就直接报结果没有独立验证。我的习惯是只要有可能就用第二种方法算一遍。两种方法结果一致才能放心。如果只有一种方法至少做收敛性检查看结果随样本数是否稳定。5. 收敛性分析与误差估计的实操细节5.1 样本数扫描与收敛曲线收敛性分析不是跑一次就完事要系统地改变样本数看估计值怎么变。我通常会取 ( N 10^3, 10^4, 10^5, 10^6 ) 四个量级每个量级跑20次不同种子画均值和标准差。import matplotlib.pyplot as plt n_values [1000, 10000, 100000, 1000000] means [] stds [] for n in n_values: estimates [] for seed in range(20): np.random.seed(seed) mean_est, _, _ importance_sampling(n) estimates.append(mean_est) means.append(np.mean(estimates)) stds.append(np.std(estimates)) plt.errorbar(n_values, means, yerrstds, markero) plt.xscale(log) plt.yscale(log) plt.xlabel(Number of samples) plt.ylabel(Estimate of E[x]) plt.show()这张图会显示两个特征第一均值随样本数增加逐渐稳定到精确值附近第二标准差随样本数按 ( 1/\sqrt{N} ) 衰减在双对数坐标下是一条斜率-0.5的直线。如果标准差衰减比这个慢说明样本之间有相关性或者权重分布太极端。注意做收敛性分析时不要只跑一个种子然后改变样本数。因为不同样本数下的随机序列是嵌套的单次跑的结果可能碰巧很好或者很坏。必须用多个种子取统计量才能看出真实趋势。5.2 方差估计与置信区间蒙特卡洛的误差估计不能只报一个标准差要报置信区间。对于自归一化重要性采样均值的方差估计是( \text{Var}(\hat{\mu}) \approx \frac{1}{N} \cdot \frac{\sum w_i^2 (x_i - \hat{\mu})^2}{(\sum w_i)^2} )这个公式比朴素方差公式多了一个权重平方项因为权重引入了额外的方差。def variance_estimate(x, w, mean_est): n len(x) weighted_var np.sum(w**2 * (x - mean_est)**2) / np.sum(w)**2 return weighted_var var_est variance_estimate(x, weights, mean_est) std_est np.sqrt(var_est) ci_lower mean_est - 1.96 * std_est ci_upper mean_est 1.96 * std_est print(f95% CI: [{ci_lower:.6f}, {ci_upper:.6f}])跑出来置信区间大约是 [0.018, 0.022]精确值0.0198落在区间内说明估计是合理的。如果精确值落在区间外要么是随机波动小概率要么是方法有系统偏差。5.3 自相关与有效样本数虽然重要性采样的样本本身是独立的因为从提议分布独立采样但加权之后有效样本数会降低。有效样本数的公式前面给过这里再强调一下它的意义它告诉你“你的10万样本相当于多少独立样本”。如果有效样本数是6万那你的误差应该按6万算不是10万。def effective_sample_size(w): return np.sum(w)**2 / np.sum(w**2) n_eff effective_sample_size(weights) print(fEffective sample size: {n_eff:.0f}) print(fRatio: {n_eff/n:.2%})如果这个比例低于10%我会考虑改进提议分布。改进的方法包括增大提议分布的方差让尾部更厚或者用混合分布比如两个不同方差的正态混合。但要注意提议分布太厚也会导致权重方差大因为目标分布尾部的样本权重会很小。这是一个权衡。6. 常见问题与排查技巧实录6.1 结果偏差大先查积分区间和尾部如果蒙特卡洛结果和数值积分差很多第一个要查的是积分区间。目标分布如果有长尾而提议分布尾部太薄尾部样本会严重不足导致期望值被低估或者高估。检查方法是画权重随 ( x ) 的散点图看大 ( |x| ) 处的权重是否异常大。第二个要查的是归一化。自归一化重要性采样理论上不需要归一化常数但如果你在代码里不小心用了归一化后的目标分布除以未归一化的提议分布结果就会错。检查方法是把权重公式单独打印出来手动算几个点的权重看是否合理。第三个要查的是随机种子。如果你只跑了一个种子结果可能碰巧偏差大。换成多个种子跑看均值是否稳定。如果不同种子结果差异很大说明方差太大需要增加样本数或者改进提议分布。6.2 有效样本数过低权重退化的诊断与处理权重退化是重要性采样的常见问题。表现是少数样本的权重远大于其他样本有效样本数很低。诊断方法是画权重的直方图如果分布极度右偏说明退化严重。处理办法有几种第一换提议分布让它更接近目标分布第二用重采样resampling把低权重样本丢掉复制高权重样本第三用退火重要性采样annealed importance sampling逐步从简单分布过渡到目标分布。我在例题里没有遇到严重的权重退化因为目标分布和提议分布形状接近。但如果你的例题里目标分布有多个峰而提议分布是单峰权重退化几乎必然发生。这时候要么用混合提议分布要么直接上MCMC。6.3 数值溢出与下溢对数域计算的必要性统计物理里的分布经常涉及指数函数( e^{-E/T} ) 在 ( E/T ) 很大时会下溢到0在 ( E/T ) 很负时会溢出到inf。直接算会得到nan或者0结果全错。解决办法是在对数域计算。把权重写成 ( \log w \log \tilde{p}(x) - \log q(x) )然后用logsumexp技巧做归一化。numpy和scipy都有logsumexp函数直接用就行。from scipy.special import logsumexp def log_importance_sampling(n_samples): x proposal_sample(n_samples) log_w np.log(target_unnormalized(x)) - np.log(proposal_pdf(x)) log_mean logsumexp(log_w np.log(np.abs(x))) - logsumexp(log_w) return np.exp(log_mean)这个技巧在处理低温分布或者大系统时是必须的。我建议从一开始就养成对数域计算的习惯不要等到溢出再改。6.4 常见问题速查表问题现象可能原因排查方法解决方案结果与精确值偏差大积分区间截断检查尾部权重扩大积分区间或换厚尾提议分布有效样本数低权重退化画权重直方图换提议分布或重采样结果出现nan数值溢出检查指数项改用对数域计算标准差衰减慢样本相关算自相关函数增加样本间隔或换独立采样不同种子结果差异大方差太大多跑几个种子增加样本数或降方差收敛曲线不平稳预热期不够看前段样本丢弃前10%到50%样本这张表是我自己排错时用的基本上覆盖了80%的常见问题。剩下的20%通常是代码bug比如索引写错、维度不匹配、随机种子没固定。遇到诡异结果先检查代码再怀疑方法。7. 从例题到实战计算统计物理的扩展思路7.1 从期望值到自由能计算例题算的是期望值但计算统计物理里更有挑战的是自由能。自由能 ( F -T \ln Z ) 涉及配分函数的对数直接算 ( Z ) 再取对数在数值上不可行因为 ( Z ) 可能跨越几百个数量级。常用的自由能计算方法有热力学积分和接受率方法。热力学积分把自由能差写成某个参数路径上的积分每一步的期望值可以用蒙特卡洛算。接受率方法则通过构造一个从参考态到目标态的切换过程用切换功的指数平均来估计自由能差。这些方法的核心思想是一样的把难算的量拆成一系列好算的量的组合。你在例题里练熟了期望值估计扩展到自由能就是多了一层参数路径的设计。7.2 从单变量到高维维度诅咒与应对例题是一维的但实际物理问题往往是高维的。高维带来的第一个问题是采样效率下降因为高维空间体积指数增长随机采样很难覆盖重要区域。第二个问题是数值积分完全不可用只能靠蒙特卡洛或者MCMC。应对高维的策略包括第一用MCMC代替独立采样因为MCMC可以在重要区域附近游走不需要全局覆盖第二用降维或者变量变换把高维问题压到低维第三用机器学习方法拟合分布比如归一化流或者变分自编码器但这些方法超出了数值分析课的范围。我建议先把一维和二维的例题做透理解采样、加权、收敛这些基本概念再往高维扩展。高维问题里方法的选择比代码的实现更重要。7.3 从静态分布到动态过程时间序列与主方程统计物理不只有平衡态还有非平衡态和动力学过程。这时候目标不再是静态分布的期望值而是时间演化过程中的路径概率或者转移速率。数值方法上动态问题通常用主方程或者朗之万方程描述。主方程是离散状态的转移概率演化可以用矩阵指数或者Gillespie算法求解。朗之万方程是连续状态的随机微分方程可以用Euler-Maruyama或者Milstein方法数值积分。这些方法的误差分析比静态问题复杂因为除了统计误差和离散化误差还有时间步长带来的偏差。但核心思想是一样的把连续问题离散化用随机样本估计期望做收敛性检查。8. 我踩过的坑与实操心得第一个坑是随机种子。我一开始做例题时跑一次觉得结果不错就交了后来换了个种子发现结果差很多。从那以后我养成了习惯任何蒙特卡洛实验至少跑20个种子报均值和标准差不报单次结果。第二个坑是权重归一化。自归一化重要性采样的公式看起来简单但实现时容易把分子分母搞反。我的检查方法是如果所有权重都乘以一个常数结果应该不变。如果变了说明归一化写错了。第三个坑是积分区间。一维无穷区间看起来好处理但如果你用数值积分截断点选在哪里很关键。我一般会选到分布衰减到 ( 10^{-10} ) 以下的位置这样截断误差可以忽略。对于正态分布这个位置大约是 ( 6\sigma ) 到 ( 7\sigma )。第四个坑是有效样本数。我一开始只看总样本数觉得10万够了。后来发现有效样本数只有2万误差比预期大了一倍多。从那以后我每次都会算有效样本数低于30%就改进提议分布。第五个坑是可视化。数值实验一定要画图不画图你永远不知道数据长什么样。我至少会画三张图权重直方图、收敛曲线、样本轨迹如果是MCMC。这三张图能帮你发现90%的异常。提示如果你时间紧只能做一件事来保证结果可信那就做收敛性检查。把样本数翻倍看结果是否稳定。稳定了再报不稳定就继续加样本或者改方法。最后分享一个实用技巧把每次实验的参数、结果、图都存到一个文件夹里用时间戳命名。这样你回头写报告或者复现结果时不用重新跑。我吃过这个亏三天前的实验结果忘了参数只能重跑浪费了半天。现在我用一个简单的脚本自动保存省了很多事。这个例题后续还可以这样扩展把目标分布换成二维的看蒙特卡洛和数值积分的性能差距怎么变或者把提议分布换成混合正态看有效样本数怎么提升或者用MCMC实现同样的估计对比两种方法的误差和计算时间。这些扩展能帮你把计算统计物理的核心方法串成一条线而不是零散的知识点。