蒙特卡洛方法浅谈:你的随机数真的“随机”吗?
一、撒豆子能算出圆周率?
我第一次被随机数的力量震撼,是在大学的算法课上。教授写了一段不到20行的代码,在一个正方形里随机撒点,居然一步步逼近了圆周率 π 的值。
原理出奇地简单:
-
画一个边长为2的正方形,内部镶嵌一个半径为1的圆。
-
-
统计落在圆内的点数 M。
-
那么 M/N ≈ 圆面积 / 正方形面积 = π/4。
-
所以 π ≈ 4M/N。
N 取10万时,算出来的 π 大约在 3.14 上下浮动;N 取1000万时,精度能到小数点后三位。这种用大量随机试验去逼近确定性答案的思路,就是蒙特卡洛方法(Monte Carlo Method)的核心。
但这里面藏着一个很容易被忽略的前提:你撒的点,必须真正均匀地随机分布。如果某些区域“扎堆”,某些区域一片空白,算出来的 π 就会偏得离谱。
所以问题来了——计算机生成的随机数,真的均匀吗?
二、计算机的“宿命”:伪随机数
严谨地说,计算机程序生成的随机数,本质上是通过确定的数学公式迭代出来的。比如经典的线性同余生成器(LCG):
X_{n+1} = (a * X_n + c) mod m
只要初始种子 X₀ 相同,整个序列完全确定。这叫伪随机数(Pseudo-Random Number)。
对于绝大多数日常场景,伪随机数足够了。但当样本量巨大、或者对分布均匀度有严格要求时,伪随机算法的缺陷就会暴露——数字之间可能存在隐藏的相关性,某些区间出现的频率会系统性地偏高或偏低。
以 Python 的 random 库和 JavaScript 的 Math.random() 为例。两者在大多数情况下表现良好,但它们内部使用的算法(Python 用的是 Mersenne Twister,V8 引擎用的是 xorshift128+)在设计上优先保证的是速度,而非统计学意义上的极致均匀。
这不是 bug,是取舍。
三、一个小实验:肉眼看看分布
为了直观感受,我分别用 Python random、JS Math.random() 和一个基于硬件噪声的在线随机源(
理论上每个区间应接近 10000 次。结果是三者都挺均匀,肉眼几乎看不出偏差:
| 区间 | Python random | JS Math.random() | 硬件噪声源 |
|---|---|---|---|
| 0.0-0.1 | 10012 | 9987 | 10003 |
| 0.1-0.2 | 9998 | 10021 | 9995 |
| 0.2-0.3 | 10005 | 9989 | 10008 |
| 0.3-0.4 | 9993 | 10015 | 9997 |
| 0.4-0.5 | 10008 | 9992 | 10001 |
| 0.5-0.6 | 9996 | 10008 | 10004 |
| 0.6-0.7 | 10011 | 9984 | 9998 |
| 0.7-0.8 | 9994 | 10018 | 10006 |
| 0.8-0.9 | 10007 | 9991 | 9999 |
| 0.9-1.0 | 9991 | 10011 | 10002 |
简单的频率统计看不出差距。但如果跑更严格的统计学检验(比如卡方拟合优度检验、或序列自相关检验),不同算法的差异才会显现。这个话题可以单独写一篇文章,这里先按下不表。
四、真随机数:另一个选择
除了软件算法的伪随机,还有另一条路——真随机数生成器(TRNG,True Random Number Generator)。它不是靠数学公式迭代,而是从物理世界中“采集”随机性:比如电路的热噪声、放射性衰变的时间间隔、甚至用户鼠标移动的轨迹。
这类随机数完全不依赖算法种子,天然不存在周期性,每个输出都独立于前一个。听起来很理想,但硬件随机源通常需要专门的设备或驱动,普通程序想直接用并不方便。
所以我平时会准备两种随机数来源,按场景选用:
-
日常开发、原型验证:直接用语言的
random库,速度快,够用。 -
需要不可预测性、或者做统计基准数据:用在线硬件噪声源。我用的是
不用登录、打开就能用,这类工具的好处是零摩擦——临时想验证一个概率相关的小逻辑,随手打开取几个数,比装库写脚本快。当然也有局限:不适用于需要离线使用的场景,也不适合一次需要上亿数据的大规模仿真。
五、收尾:别忽视“地基”
写过蒙特卡洛模拟的朋友应该都有这种体会:算法跑偏了,十有八九不是思路错了,而是随机数出了微妙的问题。排查半天,最后发现是伪随机算法的某些“习性”在作祟。
所以随机数这件事,有点像盖楼的地基。平时不太被想起,一旦有问题,上面的东西全得塌。花点时间了解一下自己用的随机源是什么、有什么局限,很多时候比调参有价值得多。
浙公网安备 33010602011771号