蒙特卡洛估计n维球体积:Matlab实现、接受-拒绝采样与维度诅咒
简介基于蒙特卡洛方法计算n维球体积的Matlab仿真代码包面向本科、硕士阶段数值计算与统计模拟方向的教学研究也适合正在学习随机采样或高维几何计算的开发者参考。代码支持Matlab 2014/2019a/2021a等常用版本并附有运行结果便于快速验证算法效果代码结构简洁变量与注释完整方便后续调整参数。压缩包共4个文件大小约136KB包括一个m源码文件、一份txt说明文档和两张结果png截图m文件实现完整投点估计流程txt说明阐述原理与参数设置png图片展示不同维度下的体积估算曲线方便逐项对照。目前已有116人浏览/学习尤其适合课程设计、毕业设计或自主研习。使用者可在此代码框架上调整维度或采样数观察估算误差与随机性的关系从而深入理解高维空间体积计算的数值方法。1. 从单位立方体到n维球蒙特卡洛体积估计的直觉你手上只有一组均匀随机数怎么算一个任意维度的球体积最简单的答案是蒙特卡洛算法把球装进一个边长为2的立方体随机撒N个点数一数落在球内多少个用占比乘立方体体积就得到球体积。这个直觉并不昂贵真正反直觉的是维度升高之后的景象10维单位球的体积只占包围立方体的0.25%20维时比例降到千万分之一。也就是说你觉得“在立方体里随便撒点总有一半能中”实际上一万次都未必中一次。这也是高维贝叶斯采样、路径积分和机器学习里一旦维度变大原始蒙特卡洛立刻失灵的根本原因。这篇笔记围绕一个很小的Matlab实现把接受-拒绝采样、参数选择、误差分析、低差异序列加速完整过一遍适合准备自己写采样器的人做对照。2. 接受-拒绝采样n维球体积的数学依据与误差来源2.1 n维球体积解析式一个可对照的标尺在写蒙特卡洛估计之前先放上精确解否则你不知道估计值到底准不准。半径为r的n维球体积公式为V_n π^(n/2) · r^n / Γ(n/2 1)当r1时分母是Γ(n/21)。这个式子把n1、2、3分别对应区间长度2、圆面积π和球体积4π/3。用Matlab算解析值非常短n 3; V_exact pi^(n/2) / gamma(n/2 1);gamma函数在这里扩展了阶乘的定义。n为偶数时Γ(n/21)(n/2)!n为奇数时要用Γ函数的值。比较估计值总是用这个作为基准。蒙特卡洛估计的思路来自接受-拒绝采样单位球被超立方体 C [-1,1]^n 包住立方体体积是V_cube2^n。在C上均匀采样设M为落入球内的点数则M/N是球体积占立方体体积的比例于是V_hat (M / N) · 2^n这是一个无偏估计量多次重复取平均会收敛到真值但单次估计有方差。理解这个式子是后面调参数的前提。2.2 随机种子、采样次数与复现性Matlab的rng(42)把全局随机数生成器固定到指定状态。固定种子不是为了让结果骗人好看而是为了调试时同一个bug能被稳定复现。日常开发我会用rng(default)提交实验时改成固定种子。注意rng的编号不重要只要两次调用的种子一致生成的随机数序列就一致。采样次数N直接决定方差。比例估计的方差是p(1-p)/N其中p是真实体积占比。标准误差随√N衰减也就是说要让误差缩小10倍采样次数需要增长100倍。这是蒙特卡洛收敛慢的根源。N的选择没有银弹工程上通常先跑小N看方差再按误差公式反推。提示提交实验代码时把rng(42)放在参数区最前面。否则每次运行的随机序列不同排错时很难判断是代码变化还是随机波动导致的。这里必须提醒rand和randn不是一个东西。rand(m,n)生成[0,1)均匀分布适合撒在立方体里randn生成标准正态分布中心密集、尾部稀疏用它采样立方体点会让中心附近的点偏多接受率虚高估计结果系统性偏大。很多初学者把两个混用出来的体积经常比真值大一截。2.3 一段最小的Matlab实现骨架function V mc_sphere_volume(n, N) % n: 维度 % N: 采样点数 rng(42); % 固定随机种子 X rand(N, n) * 2 - 1; % 将[0,1)映射到[-1,1) r2 sum(X.^2, 2); % 计算平方距离 inside r2 1; % 球内判定 M sum(inside); % 球内点数 V (M / N) * (2^n); % 体积估计 end这段代码里有三个细节值得说明。第一rand(N,n)*2-1把均匀分布搬到了[-1,1]这一步漏了立方体体积就会被当成1结果会差2^n倍。第二用sum(X.^2,2)算平方距离而不是先sqrt可以省掉N次开方。第三r2 1使用的是浮点比较rand生成的值严格小于1采样点落在球壳的概率为零所以边界用还是没有影响。调用它时直接传维度n和采样数N即可。比如mc_sphere_volume(3, 1e6)会得到接近4.19的值。如果要封装成test.m那样带输出信息的脚本可以补充fprintf但核心逻辑就是上面这6行。3. test.m逐段拆解随机数生成、判定条件与体积公式这一章专门把test.m拆开讲。实际下载包里test.m的运行流程并不神秘设置参数、生成随机点、判定计数、乘立方体体积、输出结果。逐段看下来陷阱反而比算法多。朴素版本不依赖任何额外工具箱Matlab 2014a之后都能直接跑这也是这个例子适合入门的原因。3.1 参数区维度n、采样点数N和随机种子test.m通常从参数块开始%% 参数设置 n 3; % 球所在维度 N 1e6; % 采样点数 rng(42); % 固定随机种子复现实验结果n3是入门选择能与解析值4.1888直接对照。N的选择要同时考虑方差和内存在三维下N1e6时X矩阵是1e6×3 double约24MB没什么压力但把n改成100时同样N需要800MB就可能撑爆旧机器。建议固定种子后再跑否则每次运行结果都有肉眼可见的波动初学者容易误以为是bug。3.2 采样区一次性采样与分批采样一次性写法X rand(N, n) * 2 - 1; r2 sum(X.^2, 2); inside r2 1; M sum(inside); V M / N * 2^n;对于大N、高n我一般改成批次累加batchSize 100000; totalInside 0; for startIdx 1:batchSize:N nb min(batchSize, N - startIdx 1); Xb rand(nb, n) * 2 - 1; totalInside totalInside sum(sum(Xb.^2, 2) 1); end V totalInside / N * 2^n;nb min(batchSize, N - startIdx 1)保证了最后一批不会越界。这种写法只维持一个nb×n的临时矩阵内存占用被限制在batchSize×n以内同时还能在循环里打印进度。注意每一次rand(nb,n)都从同一个随机流中取数连续批次之间依然满足均匀采样不会出现分段相关。3.3 判定条件为什么用平方距离而不是距离很多人会把判定写成sqrt(sum(X.^2,2)) 1这在n较小时没有太大区别但在N很大时多出一次开方开销。因为单位球表面测度为零r2 1与r1在概率意义上完全等价所以应优先使用平方距离。更进一步sum(X.^2,2)里的X.^2是对整个矩阵逐元素平方Matlab会隐式分配临时矩阵如果n和N都很大也可以考虑X .* X两者效果一样但X .* X少一次函数调用开销。当然通常不构成瓶颈。3.4 解析解对照与结果展示test.m里加上解析解可以在同一个脚本里评估误差V_exact pi^(n/2) / gamma(n/2 1); fprintf(MC %.6f, exact %.6f, relative error %.2e\n, ... V, V_exact, abs(V - V_exact) / V_exact);gamma(n/2 1)在n为奇数时返回的是非整数阶乘值这是Matlab能直接计算的原因。fprintf的格式串里%.2e用于显示相对误差的科学计数法方便看量级。下表是一次典型运行的示意值固定rng(42)N1e5实际每次运行的数字会不同但走势一致nMC估计体积解析体积相对误差12.00002.00000%23.14003.14160.05%34.19004.18880.03%55.27365.26380.19%102.58002.55021.17%200.00000.0258100%n20的那一行看起来像错误其实是N1e5时落入20维球内的期望点数只有0.00246个所以几乎一定为0估计出0是正常的。要得到稳定结果N至少到10^11量级这不是个人电脑短时间能完成的。看到0不要慌先检查p和N的乘积。3.5 运行结果图怎么看接受率打开运行结果图时通常能看到二维投影下的采样点分布红色表示球内、蓝色表示球外。红点占比就是接受率p。若红点全部集中在中心区域可能是坐标映射漏了*2-1若红蓝点整个混成均匀一片说明投影角度选得不好看不出球体边界。收敛曲线则应该围绕解析值上下波动波动幅度随√N收窄。如果曲线一直单调爬升或下降先怀疑固定种子的位置再检查判定条件。4. 维度诅咒当n10时蒙特卡洛为什么开始失灵4.1 体积比的指数下降用精确公式算体积占比pfor n [1,2,3,5,10,20] V_exact pi^(n/2) / gamma(n/2 1); p V_exact / 2^n; fprintf(n%d, p%.3e\n, n, p); end输出大致是np11.000e027.854e-135.236e-151.645e-1102.490e-3202.460e-8到了20维随机撒点命中球内的概率已经低于亿分之一。这个现象在高维几何里叫体积比例坍缩因为超立方体的顶点数量指数增长相当一部分体积被推到角落。直观上n维球的体积集中在半径接近1的球壳但立方体从中心到角落距离为√n球远远塞不满立方体。4.2 方差公式N的底线体积估计V_hat的标准差为std(V_hat) 2^n · sqrt(p(1-p)/N)相对误差为rel_err sqrt((1-p)/(pN))p很小时1-p接近1相对误差近似为1/sqrt(pN)。如果要求相对误差不超过1%即√(pN)≥100所以N≥10000/p。按这个算n10需要N≈4×10^6n20需要N≈4×10^11。所以当n≥20原始蒙特卡洛在桌面计算环境里基本不可行。这不是算法bug而是方差随维度爆炸的固有代价。在论文和工程中常见的误用是看到n10时误差还行就推到n50最后被误差表直接打脸。4.3 用Matlab观察误差改善的速度反复跑同一个N记录方差n 10; N 1e6; rng(42); Vruns zeros(1, 20); for i 1:20 X rand(N, n) * 2 - 1; Vruns(i) sum(sum(X.^2, 2) 1) / N * 2^n; end fprintf(mean%.4f, std%.4f\n, mean(Vruns), std(Vruns));输出中std就是单次估计的波动范围。如果解析值是2.5502而std达到0.16说明单次估计落在2.39~2.71范围内的很常见。把N提高到4e6std会减半但还是不够精细。这就是收敛速度只有1/√N的直接感受。4.4 常见坑内存、randn混用、坐标变换第一个坑是内存。N1e7、n50时rand(N,n)需要4GB内存而且X.^2又会生成一个同样大小的临时矩阵峰值接近8GB。解决办法是分批采样像上一章那样。第二个坑是randn。有人觉得正态分布也覆盖空间想当然用来生成点但正态分布中心密度高落在球内比例会比p大很多得到的体积虚高。第三个坑是坐标变换。如果忘记了*2-1生成的是[0,1]^n球的中心在(0.5,0.5,...,0.5)半径1不能包含边界判定逻辑会被破坏。更隐蔽的是有人用X*2-1但之后判定条件写成X.^21没有求和导致错误。检查这类问题的最好办法是先跑n1看输出是否接近2再跑n2看是否接近π。注意当n20且N1e5时输出为0并不意味着程序有bug先检查p*N是否远小于1。5. 用拟随机数给蒙特卡洛提速Sobol序列与分层采样5.1 低差异序列的接入方式前面说的是朴素蒙特卡洛收敛速度只有1/√N。如果想在同样N下把精度提高一两个数量级通常会用低差异序列也叫拟随机数。如果你的Matlab装有统计和机器学习工具箱sobolset可以直接生成Sobol序列它不像rand那样完全独立而是让点尽量均匀地铺满超立方体。低差异序列的经验收敛速度接近O(1/N)对高维积分非常有用。替换代码很直接n 10; N 1e6; seq sobolset(n, Skip, 1e3, Leap, 1e4); seq scramble(seq, MatousekAffineOwen); X net(seq, N); X X * 2 - 1; r2 sum(X.^2, 2); V sum(r2 1) / N * 2^n;sobolset(n)生成n维的Sobol点集Skip和Leap用来跳过初始的部分点因为前几百个低维点投影通常不均匀。scramble做随机化处理让序列在保持低差异的同时具备随机性方便做误差统计。net(seq,N)取前N个点。可以看到后面判定和体积公式完全没变只是随机源变了。5.2 验证方法不要只看一次结果应重复多次比较波动。朴素随机和Sobol各跑10次打印标准差。Sobol序列估计值会更稳定波动往往小一个数量级。另一种验证是检查接受率p和理论p是否一致尤其在n10时p约0.0025如果估计出的p超过0.003多半是随机数类型或坐标映射出了问题。分层采样中的拉丁超立方体也是同一思路lhsdesign(N,n)能保证每个维度边缘均匀覆盖适合N比较小的情况但投影到高维后效果不如Sobol序列稳定。遇到n大于20的积分我会先检查能否降维再用Sobol重新跑一次如果体积估计值随N还在漂移说明N还是不够或者问题本身不适合做蒙特卡洛积分。本文还有配套的精品资源点击获取