您的位置:首页 > 其它

如何产生正态分布的随机数?

2016-03-30 13:36 405 查看


如何产生正态分布的随机数?

添加评论

分享

按投票排序按时间排序


28 个回答

86赞同
反对,不会显示你的姓名



Milo
Yip,游戏程序员、《游戏引擎架构》译者

Porphyah熊墩子就是xob任路遥 等人赞同

我为了这个问题做了个开源项目 miloyip/normaldist-benchmark
· GitHub。

这个项目比较不同正态随机数生成算法的性能,它测试的函数原型是:

void normaldistf(float* data, size_t n);
void normaldist(double* data, size_t n);


它测试了以下实现的性能,并验证其正确性(mean, SD, Skewness, Kurtosis)。

boxmuller: Box-Muller
Transform。
cpp11normal: C++11 的 std::normal_distribution。(在
clang/LLVM 中的实现是 Box-Muller transform。)
clt:利用中心极限定理,把m个均匀分布随机数相加。

inverse: 利用正态分布 CDF 的反函数做 Inverse
transform sampling。

marsagliapolar: Marsaglia
polar method。
ziggurat: Ziggurat
algorithm,n=128。
null: 作为比较基准,生成均匀分布的随机数。

一些算法包含 SSE2 和 AVX 指令集的版本。

注意 clt 算法的 abs(kurtosis) >= 0.01,未能通过正确性验证。其他除了 null 外,都通过正确性验证。

先看看 float 版本的结果(iMac Corei5-3330S@2.70GHz clang 6.1 64-bit):



http://rawgit.com/miloyip/normaldist-benchmark/master/result/Corei5-3330S@2.70GHz_mac64_clang6.1.html

可能很多人以为 Zigguart 算法是最快的,但查表操作、不定数量的分支都不利于 SIMD 并行化。而 Marsaglia polar method 也要做 rejection sampling,所以也会造成分支。Marsaglia polar method 原意是为了改善 Box-Muller 的性能的,但在此测试中反而较慢。Inverse 虽然简单,但该反函数也需要分支处理中央和尾的部分,形成分支。

在加入 SSE2 和 AVX 加速后(其 PRNG 为简单 SIMD 并行的 LCG),Box-Muller 有很明显优势,SSE2 已是 ziggurat 的 1.79x,AVX 是 2.99x。AVX 实现中使用了SSE2/SSE4.1的指令分拆来做一些操作,要在 AVX2 中才能完全实现 8-way 计算。

相对于维基上 Box-Muller 的例子代码:

double generateGaussianNoise(double mu, double sigma)
{
const double epsilon = std::numeric_limits<double>::min();
const double two_pi = 2.0*3.14159265358979323846;

static double z0, z1;
static bool generate;
generate = !generate;

if (!generate)
return z1 * sigma + mu;

double u1, u2;
do
{
u1 = rand() * (1.0 / RAND_MAX);
u2 = rand() * (1.0 / RAND_MAX);
}
while ( u1 <= epsilon );

z0 = sqrt(-2.0 * log(u1)) * cos(two_pi * u2);
z1 = sqrt(-2.0 * log(u1)) * sin(two_pi * u2);
return z0 * sigma + mu;
}


我实现的Box-Muller 做了一些优化:

template <class T>
void boxmuller(T* data, size_t count) {
assert(count % 2 == 0);
static const T twopi = T(2.0 * 3.14159265358979323846);

LCG<T> r;
for (size_t i = 0; i < count; i += 2) {
T u1 = 1.0f - r(); // [0, 1) -> (0, 1]
T u2 = r();
T radius = std::sqrt(-2 * std::log(u1));
T theta = twopi * u2;
data[i    ] = radius * std::cos(theta);
data[i + 1] = radius * std::sin(theta);
}
}


直接写入两个结果(这个与 API 设计相关)
把 [0, 1) 的随机数转换成 (0, 1],避免了 log(0) 的出现,不需要做 rejection。
在 SSE2/AVX 版本中,利用 sincos() 函数同时计算 sin 和 cos。

如果目标平台有 SIMD 指令集,建议使用 Box-Muller,否则可考虑 Zigguart。

如果有其他改进或算法,欢迎 pull request。

编辑于 2015-07-09 8
条评论 感谢

分享

收藏没有帮助举报作者保留权利

收起

17赞同
反对,不会显示你的姓名



何史提,从一到无穷大

小小、知乎用户、知乎用户 等人赞同

谢邀。

先假设



用Box-Muller方法,随机抽出两个从

均匀分布的数字



。然后









都是正态分布的。

证明可用极坐标,请参考教科书中的Box-Muller方法。

另外使用反函数,先随机抽出一个从

均匀分布的数字

,然后





是正态分布的。

Python实现:

import numpy as np
from scipy.special import erfinv

def boxmullersampling(mu=0, sigma=1, size=1):
u = np.random.uniform(size=size)
v = np.random.uniform(size=size)
z = np.sqrt(-2*np.log(u))*np.cos(2*np.pi*v)
return mu+z*sigma

def inverfsampling(mu=0, sigma=1, size=1):
z = np.sqrt(2)*erfinv(2*np.random.uniform(size=size)-1)
return mu+z*sigma


发布于 2015-05-05 3
条评论 感谢

分享

收藏没有帮助举报作者保留权利

118赞同
反对,不会显示你的姓名



孙晓博,重度网瘾需电击,晚期懒癌无药医

知乎用户、Porphyah、Xenophen
Li 等人赞同

最简单的:rejection sampling,思路很简单,也很容易实现,但效率较差
较复杂的:inverse CDF,直接利用累积分布函数(CDF)的反函数生成随机数,但计算中牵扯到比较复杂的误差函数erf(非初等函数)
更好的:Box-Muller算法,在很长时间内都是生成正态分布随机数的"标准"算法。Box-Muller算法的特点是效率高,并且计算过程比较简单(只用到了初等函数)。参见:Box-Muller
transform

目前最好的(相较于其它实用算法):ziggurat算法,效率很高,很多现代的编程语言都使用了这一算法。ziggurat并不是人名,其含义是“金字形神塔”,不是埃及那个金字塔,而是古代苏美尔人建造的类金字塔结构的神坛:神坛由多层平台构成,每层平台都呈矩形、卵形或正方形,且自下而上面积逐渐减小。ziggurat算法实际上是一种改进的、包含查表操作的rejection
sampling。参见:Ziggurat algorithm

--------------------------------------------------------------------------------------------------------------------------------------

楼上知友 @何史提 的答案中已经涵盖了inverse CDFBox-Muller算法,这里只简要介绍一下ziggurat算法

先考虑最简单的rejection sampling,其方法如下图所示,首先生成在蓝色矩形框中均匀分布的随机点

,然后舍弃(即拒绝,reject)所有未落在正态分布钟形曲线以下的点,剩余点的横坐标

就构成一个服从相应正态分布的随机数序列。rejection
sampling的效率之所以低下,就是由于很多生成的随机点因为落在钟形曲线以上而被白白舍弃。



如果有一种方法能够直接产生在钟形曲线下方面积内均匀分布的随机点,那么就不存在舍弃随机点的情况,也就能够避免上面所说的“浪费”问题,这正是ziggurat算法的基本思路。具体来说,ziggurat算法的内容如下:

% 以下内容均遵从MATLAB语言的规定,数组及向量的下标索引从1开始编号

正态分布的钟形曲线左右对称,这里只取右半边考虑,如下图所示。首先将钟形曲线以下的面积等分

份,包括

个水平矩形和最下方一直延伸至无限大的尾巴,即

个矩形及最下方尾巴的面积都相等
。这些层叠的矩形看起来像古代苏美尔人建造的金字形神塔,这就是ziggurat这个名称的由来。图中取

,实践中

可能达到

以上。设各水平矩形右边界的横坐标(即各圆圈标记的横坐标)为

,其中

。当

确定后,

也就确定了,可以通过数值方法求解超越方程得到

的值并制表记录。这里称长度方向上与上一层矩形重叠的部分为当前矩形的核心区,下图中各核心区的右边界就是虚线边,显见最上面的矩形没有核心区。


图片出自Numerical
Computing with MATLAB一书的第九章Random Numbers。顺便提一下,此书作者就是MATLAB的创始者、MathWorks公司的主席和首席数学家Cleve
B. Moler。

定义

,即相邻两层矩形水平方向长度的比值,规定

。显然,依照所要求的参数(

)生成正态分布时,所有的



都只需要在初始化时计算一次并制表记录即可,之后不需要再重复计算,除非所要求生成的正态分布的参数(

)发生改变。

初始化结束后就可以开始生成正态分布随机数了:首先生成一个

区间内均匀分布的随机整数

和一个

区间内均匀分布的随机浮点数

。然后检查

是否成立,若成立则

就在第

段核心区内,显然也就在钟形曲线下方,故可将

作为正态分布的一个采样点返回。相应的伪代码为

j = randint(1, n);  % [1,n]区间内均匀分布的随机整数
u = 2 * rand() - 1;  % (-1,1)区间内均匀分布的随机浮点数
if abs(u) < sigma[j]
return u * z[j];  % u * z[j]在第j段核心区内
end


由此可见,使用ziggurat算法时只需要生成一个随机整数和一个随机浮点数,做一次条件判断和一次乘法运算,外加两次查表即可得到一个服从正态分布的随机数,整个过程中没有开根号、求对数、算三角函数等复杂运算。

在上面伪代码中,那个if条件判断在绝大多数情况下都为true,剩下为false的就是被reject的情况,具体又包含三种可能:(1).

,最上面的矩形没有核心区;(2).



恰好落在核心区之外(即右侧包含曲线的小矩形中);(3).



落在底部核心区之外的尾巴中。在这三种情况下就需要依据Box-Muller算法利用已经生成的均匀分布随机数进行额外运算以生成正态分布随机数。容易看出,

越大,出现这三种需要额外运算的情况的可能性就越低。Numerical
Computing with MATLAB一书中给出的数据是当

时,需要额外运算的可能性就小于3%,所以这部分额外运算对ziggurat算法的总体效率影响很小。

ziggurat算法适用于连续生成大量服从同一正态分布的随机数的情形,在这种情况下ziggurat算法的效率远高于Box-Muller算法。反之,如果只是需要少量正态分布随机数,那么考虑到计算



的开销,ziggurat算法的总体表现未必优于Box-Muller算法。

ziggurat算法的基本思想最早由美国数学家兼计算机科学家George Marsaglia于20世纪60年代提出,在之后的岁月中该算法不断得到改进和发展,目前常用的版本大多源自于George Marsaglia和香港数学家曾衛寰(Wai Wan Tsang)在2000年发表的论文The
Ziggurat Method for Generating Random Variables。

最后需要说明两点:

实际使用的ziggurat算法会比上面介绍的版本更加复杂。如果要在实际项目(特别是与加密有关的项目)中使用正态分布随机数,强烈建议直接使用标准库或是经广泛检验的第三方库所提供的函数,而不是照着上面的介绍自己随便写一个凑合着用。
ziggurat算法实际上并不局限于生成正态分布随机数,只要一个连续分布的概率密度函数(PDF)图象是一条下降的曲线或是像正态分布这样的钟形曲线,理论上都可以使用ziggurat算法生成。

编辑于 2015-07-04 10
条评论 感谢

分享

收藏没有帮助举报作者保留权利

2赞同
反对,不会显示你的姓名


知乎用户,摸鱼失败
身败名裂

落菌、知乎用户 赞同

如何生成随机数(上) « Free Mind

发布于 2015-07-02 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

3赞同
反对,不会显示你的姓名



慧航,公众号:ecopaper,Keep
calm and DO your…

Light
Spell、毕成、知乎用户 赞同

刚好我的专栏里面有一个现成的code:Julia call C - 码农经济学 - 知乎专栏

编辑于 2015-05-11 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

1赞同
反对,不会显示你的姓名



九铭书记,知道一点微末的数学知识,一直想还原有趣…

李修 赞同

最简单粗暴的方法:下载r,写一行r代码:y<--dnorm(x,0,1)。这是标准正态分布。把0和1可以替换成想要的mean和standard deviation

发布于 2015-07-04 2
条评论 感谢

分享

收藏没有帮助举报作者保留权利

1赞同
反对,不会显示你的姓名

匿名用户

知乎用户 赞同

在获得均匀分布随机数的基础上, 进行 Box-Muller 变换, 一次计算可获得两个正态分布的随机数.

Box-Muller transform: https://en.wikipedia.org/wiki/Box-Muller_transform

下面是 C# 代码的实现:

/// <summary>
/// 产生正态分布的随机数
/// </summary>
/// <param name="averageValue"> 正态分布的平均值, 即 N(μ, σ^2) 中的 μ </param>
/// <param name="standardDeviation"> 正态分布的标准差, 即 N(μ, σ^2) 中的 σ </param>
/// <returns> 返回正态分布的随机数. 理论值域是 μ±∞; 落在 μ±σ, μ±2σ, μ±3σ 的概率依次为 68.26%, 95.44%, 99.74% </returns>
public float Normal(float averageValue, float standardDeviation)
{
return averageValue + standardDeviation * (float)
(
Math.Sqrt(-2.0 * Math.Log(1.0 - NextDouble()))
* Math.Sin((Math.PI + Math.PI) * NextDouble())
);
}


其中 NextDouble 是产生一个 [0, 1) 区间的均匀分布随机数. 这个方法丢弃了另一个结果, 并且一次调用导致随机数种子变化两次.

发布于 2015-07-05 1
条评论 感谢

分享

收藏没有帮助举报作者保留权利

4赞同
反对,不会显示你的姓名



Xi
Yang,生物、计算机、数学、图像、音频全栈二把刀

ketsu
Lih、吴志伟小张闲 等人赞同

如果你的要求不高,比如只是应用在游戏里,那么可以简单地累加若干个均匀分布的随机数。实际上,累加三个随机数就可以形成看起来很平滑的分布。我从《游戏编程精粹7》学到的这个技巧。

发布于 2015-07-03 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

0赞同
反对,不会显示你的姓名



JX
Consp,朱门酒肉臭,路有克苏鲁

我记得统计之都有篇文章讲过这个,一个是Box-Muller直接算,一个是 CDF 反演,一个我忘了,最后一个我比较喜欢,是 rejection sample

链接在此

Editor: 漫谈正态分布的生成

编辑于 2015-07-02 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

0赞同
反对,不会显示你的姓名

匿名用户

python

random.gauss(aver,sigma)

编辑于 2015-05-08 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

0赞同
反对,不会显示你的姓名



we
we,做个好人/为人民服务

可以normrnd,

发布于 2015-07-03 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

1赞同
反对,不会显示你的姓名



Lightbro,math/econ在读,mfe/msf申请准备中

知乎用户 赞同

看了前几名的答案, 能不能考虑一下ln运算开根号运算的速度。。。

其实我自己一直用一个很tricky的方法,中心极限定理可以得到n个0-1均匀分布变量之和服从N(n,sqrt(n)),取n等于15已经基本上服从正太了,而且速度快

发布于 2015-07-04 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

0赞同
反对,不会显示你的姓名



我是一捆葱,程序员。

C++11标准库

发布于 2015-07-09 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

0赞同
反对,不会显示你的姓名


知乎用户,生活在理想中的傻瓜

如果使用C++11,这是很简单的

请参考http://stackoverflow.com/questions/7114043/random-number-generation-in-c11-how-to-generate-how-do-they-work

编辑于 2015-07-04 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

0赞同
反对,不会显示你的姓名



林彼方,作死小能手

取两个(或多个)随机数的平均值即可产生非常近似于正态分布的结果。龙与地下城就广泛使用了这一方法。(就是那个2d6)

发布于 2015-07-06 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

0赞同
反对,不会显示你的姓名


知乎用户,嗷呜~

用均匀分布的随机数生成器多随几个,取平均数就行

发布于 2015-07-09 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

10赞同
反对,不会显示你的姓名



GeeK,CAE
&& Python

xiaoneigoldman清洁工 等人赞同

MATLAB大法好

R=normrnd(MU,SIGMA,m,n): 生成m×n的服从正态分布的随机数矩阵。

其中:MU为返回均值,SIGMA为标准差

如果要画直方图的话可以用bar函数。

发布于 2015-07-06 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

0赞同
反对,不会显示你的姓名



游逸明,一只忧郁的耗子

randn( )

发布于 2015-07-10 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

0赞同
反对,不会显示你的姓名

匿名用户

《概率论与数理统计》一书中似乎给了一种办法

发布于 2015-07-10 添加评论 感谢

分享

收藏没有帮助举报作者保留权利

0赞同
反对,不会显示你的姓名

匿名用户

SAS中使用 RANNOR(seed )或者NORMAL(seed)

发布于 2015-07-10 添加评论 感谢

分享

收藏没有帮助举报作者保留权利
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: