计算机程序的运行是确定性的,即每一步都有一个明确的描述,如何在确定性下生成随机的内容?这似乎是个自相矛盾的问题。

事实上,这个怀疑是对的,在没有外部不确定性的输入下,计算机程序永远以确定性的方式运行。为让计算机程序稳定地生成随机内容,需要稳定的不确定性的输入。而这样做的成本非常高。于是,为何不抛开这个直观思路?让计算机生成一些看起来随机的确定性序列。易知,伪随机数有一个非常好的优点,可重复。比如我们再训练神经网络时,要初始化大量参数,伪随机数则可以让我们获得同样的初始化参数,从而避免对此训练。

采样

采样,可以看做是从给定分布中采集样本,也可以看做是从给定数据集,通过某种策略,采集构造新的数据集的方法。本文讲述从均匀分布采样、从指数分布采样、从正态分布、Laplace分布采样等常见分布采样方法。

首先我们谈谈分布变换,即有已知分布的样本集,通过一定的方法使其变换为符合其他分布的样本集。比如,我现在有一个均匀分布生成器,获得一系列样本$x_1, x_2, \dots, x_n \sim U[0, 1]$,那么通过$g(x)$​​,它如何变换为其他的分布呢?比如变换为正态分布,

这就是分布变换的问题。一般来说,简单的分布容易采样,在获得简单分布的样本后,通过分布变换获得复杂分布的样本。但是对于一般的分布函数,采样方法都不是普遍适用的,因此需要具体分布具体讨论,为每种分布设计最高效的采样方法。

逆变换采样

累积分布函数(CDF)是个单调函数,那么累积分布函数的反函数为,

如果$u \sim U[0, 1]$,那么$F^{-1}(u)$服从累积分布$F(x)$。

证明,

这里有一个细节需要说明,由于$u$采样自均匀分布,因此容易知道$P(u \le y) = y$。这一点是以上推导的关键细节。对于很多分布来说,逆函数$F^{-1}(u)$并不容易计算,因此很多情况下无法直接使用逆变换采样。

注意到,

也就是说,如果获得累积分布为$F(x)$的随机变量$t$,那么$F(t)$为均匀分布。例如$x_i \sim N(0, 1)$,那么标准正态分布的累积分布函数(CDF)$\Phi(x_i)$服从均匀分布。不过$\Phi(x)$无法直接计算,可以使用其近似形式,

于是,对于$x \sim N(0, 1)$代入上式即可获得$U[0, 1]$样本。

正态分布

正态分布的概率密度函数,

均值和方差分别为$\mu$和$\sigma^{2}$。

取 $z=\frac{x-\mu}{\sigma}$​ 化成标准正态分布,

如果从标准正态分布采样$u_i$,那么$\mu + u_i \times \sigma$就是均值和方差分别为$\mu$和$\sigma^{2}$的随机变量。

逆变换采样法

计算累计分布函数,

对于一般正态分布也容易得到

然后你会发现其逆$F^{-1}(x)$​​没那么容易求。scipy这个科学计算库给我们提供了一种实现。Python实践如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt

# 正态分布采样的逆变换法,函数为scipy.stats.norm.pdf
# 原理为分段近似

def gen_normal_curve(rs, u, s):
x = np.linspace(np.min(rs), np.max(rs), len(rs))
rs = 1 / (np.sqrt(2*np.pi)*s) * np.exp(-(x-u)**2/(2*s**2))
return x, rs

x = stats.norm.ppf(np.random.uniform(size=10000))
print(stats.normaltest(x))
plt.hist(x, bins=100, density=True)
mu = 0
sigma = 1
x, c = gen_normal_curve(x, u=mu, s=sigma)
plt.plot(x, c, color="red", label="$N({},{}^2)$".format(mu, sigma))
plt.legend(loc="upper left")
plt.show()

可视化如下,

可以看到采样样本(蓝色部分)和标准正态分布拟合得非常好。

另外一种思路是寻找一个可逆的函数逼近误差函数。注意到$\operatorname{erf}(x)$可以用可逆的函数光滑逼近,

求逆,解得$x$​为,

可视化这种采样效果,

可以看到,还是有较大差异。

此外$\operatorname{erf}(x)$还有一个不错的光滑逼近,

这个出发点,然后求$\tanh$的逆,然后解一元三次方程,获得逼近函数的逆,作为$\operatorname{erf}(x)$的逆的近似。

正态分布的CDF无法直接求解反变换函数,但是可以通过一定的技巧绕过这个问题。例如使用 ziggurat 有效地求累积分布函数的逆函数。

Box-Muller 方法

既然$\operatorname{erf}(x)$​和$\Phi(x)$​的逆变换难求,那么我们对其升维后再试试使用逆变换的思路。这是机器学习的思路:低维无法解决问题,升维来解决

这里的具体思路是$\Phi(x)$难求逆函数,但是二维$\Phi(x)\Phi(y)$容易求逆函数,获得逆函数后通过逆变换采样得到正态分布的采样公式,这种方法称为Box-Muller方法。

二维的标准正态分布为,

其累积分布函数通过极坐标变换容易求解。这里另,

于是,我们解决$r$与$\cos(\theta),\sin(\theta)$的采样问题即可解决正态分布采样问题。

考虑如下积分计算,

该积分值为1,整体上可以看做是某个随机变量的概率密度函数。根据这个结果,我们使用逆变换思路。对于随机变量$R$​,根据以上结果有,

对于指数分布有,

于是,这里把 $r^{2}$ 看作一个整体,$P(r)$ 实质是参数$a = \frac{1}{2}$的指数分布,于是逆变换采样思路有,

也就有$r$的采样方法,

考虑到$\theta$​的取值范围为$2\pi u_2$​,于是根据$x = r\cos(\theta), y = r \sin(\theta)$​,有正态分布采样方法,

生成正态分布任取其中一条公式即可。Box-Muller 方法需要进行三角运算,计算量较大。通过一定的技巧可以把其去掉三角运算。

以上技巧如何逆着来,即可通过正态分布生成器获得均匀分布生成器,$n_1,n_2$独立采样自标准正态分布,那么

服从均匀分布$U[0, 1]$。

修正的 Box-Muller 方法

假设$x \sim U[-1, 1], y \sim U[-1, 1]$,即在矩形上采样$(x, y)$,对于满足$r = x^{2} + y^{2} \le 1$的样本留下,否则丢弃,这样获得的样本$r$满足均匀分布,因为这相当于等可能地落到圆内。这个方法也称为Marsaglia polar method。

于是,$\frac{x}{\sqrt{r}}$可以作为$\cos 2 \pi u_{2}$的替代;$\frac{y}{\sqrt{r}}$可以作为$\sin 2 \pi u_{2}$​的替代。

因此有,

修正的 Box-Muller 方法有一个好处是避免进行三角运算,提升计算效率。

中心极限定理思路

中心极限定理表明多个独立统计量(可以具有不同分布)的平均值满足正态分布。以均匀分布为例,这里均匀分布$x_{i} \sim U[0, 1]$​,易知均值为$\frac{1}{2}$​,方差为$\frac{1}{12}$​,从中采样一系列样本$x_1, \dots, x_n$,有

也就有,

以上是对于采样分布均为均匀分布的特殊情况。中心极限定理表明多个具有不同分布独立统计量也满足条件,这里就不进行数学推导了,Python实践编程验证一下。

一下的实现从正态分布、泊松分布、拉普拉斯分布、指数分布等中采样样本,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats

poisson = lambda: np.random.poisson(lam=100)
uniform = lambda: np.random.uniform()
normal = lambda: np.random.normal()
laplace = lambda: np.random.laplace()
gumbel = lambda: np.random.gumbel()
exponential = lambda: np.random.exponential()

probs = [poisson, uniform, normal, laplace, gumbel, exponential]

def gen_random(n=100, p=None):
nums = []
for _ in range(n):
f = np.random.choice(probs, p=p)
nums.append(f())
return np.mean(nums)

size = 10000
nums = [gen_random(100) for _ in range(size)]
plt.hist(nums, bins=100, density=True)

mu = np.mean(nums)
sigma = np.std(nums)
x = np.linspace(mu - 4*sigma, mu + 4*sigma, size)
y = stats.norm.pdf(x, mu, sigma)
plt.plot(x, y, 'r')
plt.show()

可视化如下,

可以看到采样样本(蓝色部分)和正态分布(红色曲线)拟合得较好。

这种方法不建议工程环境使用:

  • 效率低
  • 这个思路更多是理论意义,实际计算效率不高
  • 正态检验pvalue非常小,不显著

实现

实现更新到:sampling-from-distribution,可能会根据需要持续更新~

总结

本文介绍了一些正态分布采样方法,具体如下:

  • Box-Muller
  • 修正的Box-Muller
  • 接受-拒绝采样
  • 累积分布函数的逆或近似逆

此外,还有accept-reject抽样,可以用于复杂形式的概率密度函数的抽样。

参考

[1] https://en.wikipedia.org/wiki/Lehmer_random_number_generator

[2] https://en.wikipedia.org/wiki/Inverse_transform_sampling

[3] https://en.wikipedia.org/wiki/Diehard_tests

转载请包括本文地址:https://allenwind.github.io/blog/10395
更多文章请参考:https://allenwind.github.io/blog/archives/