生成任意分布的随机数


生成任意分布的随机数

一般的编程语言或库都有生成均匀分布的(伪)随机数的函数。但有时我们会需要生成一个具有指定分布函数的随机数。这就需要我们对均匀分布的随机数进行变换。

比如我们可能会经常遇到的,生成一个均匀分布于二维平面单位圆内的随机点。通常做法可以是生成 ,然后判断是否在单位圆内,如果不在则重新取。

圆内均匀分布的简单实现

import random
from typing import Tuple, List
from matplotlib import pyplot as plt

Point = Tuple[float, float]

def random_point_in_unit_circle_trivial() -> Point:
    while True:
        x = 2.0 * random.random() - 1.0
        y = 2.0 * random.random() - 1.0
        if x ** 2 + y ** 2 < 1:
            return x, y

# https://stackoverflow.com/questions/9081553/python-scatter-plot-size-and-style-of-the-marker/24567352#24567352
# https://stackoverflow.com/a/24568380/4635234
def draw_circle(center: Point, radius: float, **kwargs):
    import matplotlib.patches
    import matplotlib.collections
    patches = [matplotlib.patches.Circle(center, radius)]
    collection = matplotlib.collections.PatchCollection(patches, **kwargs)

    ax = plt.gca()
    ax.add_collection(collection)
    ax.autoscale_view()
    ax.axis('equal')

def draw_points(points: List[Point]):
    x, y = zip(*points)
    plt.scatter(x, y)

points = [random_point_in_unit_circle_trivial() for _ in range(500)]
draw_points(points)
draw_circle((0, 0), 1, alpha=0.2, edgecolor='b')
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
31
32
33

png

这当然是一个方法,但它局限于均匀分布。而且重复随机采样,虽然平均复杂度仍为 ,但是似乎不太优雅。有没有一种更通用的方法呢?

基于分布函数的方法

还是同样的问题。我们可以看出,在单位圆内均匀分布的点,用笛卡尔坐标表示为 ,满足如下分布函数:

这看起来并不是一个非常简单的分布函数。我们不妨用极坐标表示为 。(其中 为随机变量, 是 的大写。)这样有

(以下将两个分布函数简写为 、。)

这个分布函数是如何得到的呢?这就是多元微积分的知识了。实际上我们可以先验证一下这个分布函数是否正确,至少它应满足积分为。

确实没错。

教材上会告诉我们,要对分布函数进行变换,需要计算 Jacobi 矩阵的行列式,然后可能会给出一堆证明,令人头秃。实际上,我们可以通过极坐标和笛卡尔坐标的微分转换关系,用一个比较形象的方法得到上述分布函数。

在单位圆内取一个微小的区域 。无论用何种坐标系,点落在这个微小区域内的概率应该是一样的,即

用分布函数的积分值表示上述点落在这个区域内的概率,即

而根据微分的转换关系,有

这可以由 Jacobi 矩阵的行列式得到,也可以用上图来看出。 是用直角坐标系计算 的面积,而 是用极坐标系计算 的面积,其中 是沿极坐标径向的长度, 是垂直于径向的弧长。注意这只是形象的解释,具体计算原则还是应该按照教科书上的推导。

所以有

那么可以看出 和 的分布函数相互独立,即

我们只需要生成一个 均匀分布的 ,和一个在 上、分布函数为 的 即可。

接下来问题就是如何生成分布函数为 的随机变量了。

生成服从某分布函数的随机变量

考虑服从以下分布函数的随机变量 :

我们在程序中可以直接随机生成的随机数 一般是服从 的。即分布函数为

现在我们希望建立 、 之间的函数关系,这样我们就可以通过一个变换,从随机生成的 得到 。这个函数关系我们还不知道。那么我们仍然可以用两个分布函数分别表示随机数落在某一微小区间 内的概率:

用分布函数的微分表示:

那么

神奇了,我们得到了 关于 的导数。那么对其积分,可得

根据 和 的定义域,我们可以约定 时 ,那么 ,于是

即

也就是说,我们生成一个 ,然后求根号,就得到了服从分布函数 的 了。

我们试试用这个方法生成单位圆内均匀分布的点。

import math

def random_point_in_unit_circle() -> Point:
    while True:
        r = math.sqrt(random.random())
        theta = 2 * math.pi * random.random()
        return r * math.cos(theta), r * math.sin(theta)

points = [random_point_in_unit_circle() for _ in range(500)]
draw_points(points)
draw_circle((0, 0), 1, alpha=0.2, edgecolor='b')
1
2
3
4
5
6
7
8
9
10
11

png

也可以验证一下 的分布是不是服从上述函数。

def draw_density(x, nbins=20):
    import matplotlib.cm
    cmap = matplotlib.cm.get_cmap()
    plt.hist(x, nbins, density=True, facecolor='w', edgecolor=cmap(0.25))

r = [math.sqrt(random.random()) for _ in range(2000)]
draw_density(r)
# draw f(r) = 2r
_ = plt.plot([0, 1], [0, 2])
1
2
3
4
5
6
7
8
9

png

方法总结

为了生成服从某分布函数 的随机变量 ,我们需要首先利用已有函数生成服从 均匀分布的随机变量 ,然后根据

推导出

对其积分得到

然后根据 的取值范围确定不定积分需要的常数 ,得到 关于 的函数

最后求其反函数得

对随机数 进行该变换后即可得到服从 的 。

再实践一次

利用上述方法,我们尝试一下生成服从标准正态分布 的随机变量 。可知

其不定积分

其中 是误差函数open in new window。若我们希望取随机数 时对应 ,那么由于

所以 。因此我们有 和 之间的变换关系:

取其反函数

让我们用代码验证一下。

import scipy.special
import numpy as np

def random_normal() -> float:
    z = random.random()
    x = math.sqrt(2) * scipy.special.erfinv(2 * z - 1)
    return x

x = [random_normal() for _ in range(2000)]
draw_density(x)
# draw normal distribution function
t = np.linspace(-4.0, 4.0, 100)
y = np.exp(-0.5 * t ** 2) / math.sqrt(2 * math.pi)
_ = plt.plot(t, y)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

png

讨论

为什么我们的方法中,要先求

对其积分得到 关于 的函数,然后还要去求反函数。为什么我们不能反过来,直接求

关于 积分,这样不是可以直接得到 关于 的表达式,而不用求反函数了吗?

这样做当然可以。但是需要观察到,由于我们的 是均匀分布,其概率密度函数的表达式不含 。而我们想要生成的一般的 ,其概率密度函数 通常是含 的。我们用

得到了一个右侧只含 的表达式,对 积分相对容易。但如果用

左侧是 关于 的导数,右侧是含 的表达式,那么这是一个关于函数 的微分方程。如果是比较简洁的形式,如第一个例子中

那么也许还能求解。但如果是像第二个例子中的复杂形式,大概就没法求解了吧。所以说,我们的方法利用了均匀分布的概率密度函数表达式为常数这一特殊性,简化了需要解决的问题。