Box-Muller 算法可以把独立的均匀分布随机数变换为独立的正态分布随机数
设 U1,U2∼i.i.d.U(0,1),则 −2lnU1cos(2πU2) 和 −2lnU1sin(2πU2) 独立且都服从标准正态分布 N(0,1)。
这种算法简单得令人难以置信,不过证明它的正确性其实并不困难:核心思路是对二维正态分布进行极坐标变换。只要注意到二维正态分布独特的对称性质,那么接下来的一切推导就都是普通和理所当然的。
数学证明#
二维正态分布的极坐标表示#
众所周知,一维正态分布的概率密度函数为
fX(x)=2πσ1e2σ2−(x−μ)2x∈R 令 μ=0,σ=1 就可以得到标准正态分布
fX(x)=2π1e−2x2 二维正态分布与一维的类似。设 X,Y∼i.i.d.N(0,1),则其联合概率密度函数为
fX,Y(x,y)=2π1e−2x2+y2(x,y)∈R2 对随机变量作极坐标变换
{X=RcosΘY=RsinΘ 得到
{x(r,θ)=rcosθy(r,θ)=rsinθ 其中
{r≥0θ∈[0,2π) 变换的雅可比矩阵为
J=[∂r∂x∂r∂y∂θ∂x∂θ∂y]=[cosθsinθ−rsinθrcosθ] 雅可比行列式为
∣J∣=cosθsinθ−rsinθrcosθ=rcos2θ+rsin2θ=r 得到 R,Θ 的联合概率密度
fR,Θ(r,θ)=fX,Y(x(r,θ),y(r,θ))∣J∣=2π1e−2(rcosθ)2+(rsinθ)2r=2π1re−2r2 即
fR,Θ(r,θ)=2π1re−2r2r≥0,θ∈[0,2π) 计算 Θ 的边缘密度
fΘ(θ)=∫0∞fR,Θ(r,θ)dr=∫0∞2π1re−2r2dr 不过这里并不需要真的进行积分——显然积分的结果是个和 θ 无关的常数。于是 fΘ(θ) 在取值范围内为常数。又已知 θ∈[0,2π),所以
Θ∼U(0,2π) 计算 R 的边缘密度
fR(r)=∫02πfR,Θ(r,θ)dθ=∫02π2π1re−2r2dθ=re−2r2 如果缺乏注意力的话,这里不太能够直接看出 R 的分布。但这个概率密度函数和指数分布的 fX(x)=λe−λx 有点相似,因此接下来再对 R 进行换元。
令 R=S,则 r(s)=s,从而有
fS(s)=fR(r(s))dsdr=fR(s)dsds=se−2s2s1=21e−2s 现在和指数分布的概率密度完全一样了。因此 S 服从 λ=1/2 的指数分布,即
S∼Exp(1/2) 下一步就是把均匀分布的随机变量变为指数分布的随机变量。
随机变量逆变换#
将随机变量 X 变为 Y 的通用公式为
Y=FY−1(FX(X)) 该公式分为两个部分。首先对源分布 X 进行概率积分变换 FX(X) 得到均匀分布 U
P(FX(X)≤u)=P(X≤FX−1(u))=FX(FX−1(u))=u=P(U≤u) 再对均匀分布 U 进行逆变换采样 FY−1(U) 得到目标分布 Y
P(FY−1(U)≤y)=P(U≤FY(y))=FY(y)=P(Y≤y) 由于源分布已经是均匀分布 U(0,1),因此接下来只需计算指数分布的累积分布函数 FS ,然后再求其反函数 FS−1 即可。
对于 Exp(1/2),其累积分布函数为
FS(s)=∫0sfS(s)ds=∫0s21e−2sds=[−e−2t]0s=1−e−2s 推导其反函数
e−2s−2ss=1−FS=ln(1−FS)=−2ln(1−FS) 即
FS−1(u)=−2ln(1−u)u∈[0,1) 设 U1∼U(0,1),于是有
S=FS−1(U1)=−2ln(1−U1) 由 1−U1∼U(0,1) 进一步得到
S=−2lnU1 而之前已经得到 Θ∼U(0,2π)。设 U2∼U(0,1),则
Θ=2πU2 于是
{R=S=−2lnU1Θ=2πU2 带入
{X=RcosΘY=RsinΘ 得到最终结果
XY=−2lnU1cos(2πU2)=−2lnU1sin(2πU2) 显然,若 U1 与 U2 独立,则 X 与 Y 独立。这样就成功用一对独立的均匀分布随机数生成了独立的高斯分布随机数。
代码实现#
由于我就是在 最近做的一个 Web 项目 中用到了 Box-Muller 算法(这也是写本文的原因),因此这里给出该算法的 JavaScript 实现。
其实 JavaScript 的 Math.random() 能够生成 U(0,1) 随机数,但该方法不允许设置种子,导致缺少很多乐趣,所以我这里先实现了一个基于线性同余的均匀随机数生成器。
function createUniformRandomGenerator(seed) {
let state = seed >>> 0;
return () => {
state = (1664525 * state + 1013904223) >>> 0;
return state / 0xffffffff;
};
}
function createGaussianRandomGenerator(seed) {
const getUniformRandom = createUniformRandomGenerator(seed);
let spare = null;
return () => {
if (spare !== null) {
const value = spare;
spare = null;
return value;
}
let u_1 = getUniformRandom();
while (u_1 === 0) {
u_1 = getUniformRandom();
}
const u_2 = getUniformRandom();
const r = Math.sqrt(-2 * Math.log(u_1));
const theta = 2 * Math.PI * u_2;
spare = r * Math.sin(theta);
return r * Math.cos(theta);
};
}
实现中有两个需要注意的点
- 由于不能对 0 取对数,因此当
u_1 随机到 0 时需要重试 - 由于算法一次调用生成两个随机数,但只返回一个,所以用
spare 缓存另一个,这样偶数次调用时就不会重复计算了
调用示例
SEED=114514
COUNT=5
const g = createGaussianRandomGenerator(SEED)
for (let i = 0; i < COUNT; i++) console.log(g())
输出为
0.6756921337282962
0.7153015025880592
-1.090593327207687
-0.43173865912412884
2.1300190649693405
本文选择给出 JavaScript 实现,其实还有一个好处:每个能浏览网页的设备,事实上都有 JavaScript 的运行环境。对于桌面端,只需在浏览器里按下 F12 打开 DevTools,然后找到 控制台(Console)面板,再把上面的两段代码复制进去就能运行了

移动端的话没法打开 DevTools,可以到能够在线运行 JavaScript 的网站上尝试。