Abstract
RNG (Random Number Generator? Royal Never Give-up!) 或者说(伪)随机数生成器,在统计计算中很常见。我们经常在 $\mathcal U(0,1)$ 均匀分布或者 $\mathcal N(0,1)$ 正态分布中进行采样。然而,我们不满足于当一个调包侠,所以自然会好奇:这些分布是怎么生成的呢?(怎么确保生成的随机数服从我们想要的分布?)所以,本文将从零开始(不使用 Python 的任何包含随机数的库,例如 random
, numpy
)写一个随机数生成器,实现常见的概率分布。
下面的内容 follow 周在莹老师的 lecture slides. 下文的随机数指的都是伪 (pseudo) 随机数。
对一个随机数生成器,我们要求它具有 __init__()
和 next()
方法,后者需要返回下一个服从分布的随机数。先定义一个基类:
class RandomNumberGenerator:
"""
Basic class for random number generator.
"""
def __init__(self):
raise NotImplementedError("__init__() function is not implemented yet")
def next(self):
"""
Get the next random number from the generator
"""
raise NotImplementedError("next() function is not implemented yet")
Uniform Distribution
最常见的 RNG 是线性同余生成器 (Linear Congruential): 为了生成 $0,1,\cdots,m-1$ 这些整数,可以考虑 $$ X_{i+1}=aX_i+c \pmod m $$ 这样生成的 ${X_i}$ 序列在 $a,c,m,X_0$ 处于特定条件下时,可以取遍 ${0,1,\cdots,m-1}$. 详见 Hull and Dobell, 1962. 一个常见的取法是:$a=16807,c=0,m=2^{31}-1$. (Lewis, Goodman and Miller, 1969)
于是,当 $m$ 足够大时,${X_i/m}$ 可以视为 $[0,1)$ 上的均匀分布。
class LinearCongruentialGenerator(RandomNumberGenerator):
"""
Generate a random integer in [0..m-1] using the linear congruential generator.
"""
def __init__(self, seed, a, c, m):
self.seed = seed
self.a = a
self.c = c
self.m = m
def next(self):
self.seed = (self.a * self.seed + self.c) % self.m
return self.seed
class UniformGenerator(RandomNumberGenerator):
"""
Uniform [0, 1) random number generator using linear congruential generator.
"""
def __init__(self, seed=1, a=16807, c=0, mod=2147483647):
self.mod = mod
self.rng = LinearCongruentialGenerator(seed, a, c, self.mod)
def next(self):
return self.rng.next() / self.mod
当然,生成均匀分布的方法有很多,这不是本文的重点。
重点是,假设我们已经有了一个 $\mathcal U(0,1)$ 的随机数生成器,如何生成其它分布呢?
Discrete Distributions
对于离散分布,有通用的生成方法。下面给出两个引理:
Lemma 1: Let $U\sim \mathcal U[0,1]$ and $n\in\mathbb N$. Define a random variable $X=\lfloor nU\rfloor$, then $X\sim U{0,1,\cdots,n-1}$.
Proof. $\mathbb P(X=k)=\mathbb P(\lfloor nU\rfloor=k)=\mathbb P(nU\in [k,k+1))=\mathbb P(U\in [k/n,(k+1)/n))=1/n$.
Lemma 2: Assume $A={a_i|i\in I}$ where either $I={1,2,\cdots,n}$ for some $n\in\mathbb N$ or $I=\mathbb N$, and where $a_i\neq a_j$ for $i\neq j$. Let $p_i\geq 0$ with $\sum_I p_i=1$. Finally let $U\sim\mathcal U[0,1]$ and define $K=\min{k\in I|\sum_{i=1}^k p_i\geq U }$. Then $X=a_K\in A$ satisfies $\mathbb P(X=a_K)=p_k$ for all $k\in I$.
Proof. $\mathbb P(X=a_K)=\mathbb P(\sum_{i=1}^{k-1}p_i<U,\sum_{i=1}^k p_i\geq U)=p_k$.
根据以上引理,假设我们已经有 $U\sim \mathcal U[0,1]$ 了,那么只需要构造特定的 ${a_i}$ 和 ${p_i}$ 即可得到想要的离散分布。
例如对于几何分布而言,目标是 $$ \mathbb P(X=i)=(1-p)^{i-1}p=p_i,i\in\mathbb N^+ $$ 对照 Lemma 2 可知,应当令 $a_i=i$, $p_i=(1-p)^{i-1}p$. 则 $$ \sum_{i=1}^k p_i=\sum_{i=1}^k (1-p)^{i-1}p=1-(1-p)^k $$
$$ \sum_{i=1}^k p_i\geq U\Longleftrightarrow 1-(1-p)^k\geq U\Longleftrightarrow k\geq\frac{\log(1-U)}{\log (1-p)} $$
所以, $$ \left\lceil\frac{\log(1-U)}{\log(1-p)}\right\rceil\sim G(p) $$
class GeometricGenerator(RandomNumberGenerator):
"""
Generate a random number from a geometric distribution with parameter p.
"""
def __init__(self, uniform_generator: RandomNumberGenerator, p):
self.rng = uniform_generator
self.p = p
if not isinstance(p, float):
raise ValueError("p must be a float")
if p <= 0 or p >= 1:
raise ValueError("p must be in range (0, 1)")
def next(self):
U = self.rng.next()
return math.ceil(math.log(U) / math.log(1 - self.p))
Inverse Transform Method
对于连续分布,有类似的处理方法。假设 $X$ 是连续型随机变量,它的 cumulative distribution 是 $F_X(x)$. 那么 Probability Integral Transformation 定理告诉我们,$Y=F_X(X)\sim \mathcal U(0,1)$.
Proof. $$ F_Y(y)=\mathbb P(Y\leq y)=\mathbb P(F_X(X)\leq y)=\mathbb P(X\leq F_X^{-1}(y))=F_X(F_X^{-1}(y))=y $$ 例如,对于 Rayleigh distribution: $$ f(x;\sigma^2)=\frac{x}{\sigma^2}\exp(-x^2/2\sigma^2),x\geq 0 $$ 我们试图通过均匀分布得到该分布: $$ F(x)=\int_0^x f(t)\mathrm dt=1-\exp(-x^2/2\sigma^2)=y $$ 则 $$ x=\sqrt{-2\sigma^2\log(1-y)} $$ 于是, $$ \sqrt{-2\sigma^2\log (1-U)}\sim f $$ 当然,这个方法仅适用于 cumulative distribution 容易计算的分布。对于 normal distribution 这样没有累积分布的解析表达式的分布,需要考虑其它方法。