本文重点:
要理解伪随机数生成算法需要先了解几个重要概念
“种子”决定了你会得到什么样的随机数序列,同时也决定了内部状态的初始值。对于给定的种子,你总会得到相同的随机数序列;而另一方面,几乎每一个互不相同的种子都会生成一个不同的随机数序列。
内部状态”由伪随机数生成算法用于生成随机数和下一个内部状态的多个变量构成。只要你知道了内部状态以及伪随机数生成器或是密码学安全伪随机数生成器的算法类型,你就可以预测下一个随机数了。
每个随机数序列的长度就是“周期”。一旦超出周期范围,伪随机数生成器生成的值就会开始重复。正是因为伪随机数生成器生成的值按照固定的间隔或者说周期重复,我们才说伪随机数生成器是周期函数。
通常希望得到的随机数服从均匀分布,伪随机数生成器一般也确实可以产生(0,1)区间内具有等可能性的随机数。生成大量(0, 1)区间内的随机数的结果,如下图所示,服从均匀分布。
随机数在(0,1)范围内等可能地分布,这种现象被称作“均匀分布”或“均匀随机数”。这种分布使得在特定区间内取到任意值的可能性都相等。
我们可以将得到的随机数缩放到任何目标区间,缩放方法和归一化的方法有点类似。
rand(high,low)=rand()*(high-low)+low
有一些编程语言会生成正态分布随机数的接口。
出现概率最大的随机数集中在0附近,其中并没有明确的上限和下限。每个整数都表示一个标准差,随着标准差在正负两个方向上逐渐增大,这些随机数被取到的概率急剧下降,在区间(-4,4)之外几乎就取不到什么数了。正态分布随机数最常见的应用场景是通过增加一个微小的随机偏置来改变某个数字。
Uniform:Random.NextDouble
Normal:NA
Uniform:rand
Normal:NA
Uniform:Random.NextDouble
Normal:Random.nextGaussian
Uniform:random.random
Normal:random.randn
Uniform:runif
Normal:rnorm
这也是一种随机数方案,只不过这种技术与赌场中实际的轮盘略有相似之处而已。通常当你要在3个及以上类别中做选择时,就需要用到这一技术。
假设你要创造一个在栅格中随机游走的机器人,并且这个机器人只具备下列3种行为能力:
if 0<x<0.8 then move forward //低于0.8的情况占80%
if 0.8<x<0.9 then move left //0.8到0.9的情况占10%
if 0.9<x<1.0 then move right //0.9到1.0的情况占10%
性能是选择伪随机数生成算法的另一个重要的考量因素。
线性同余生成法是历史最悠久同时也最为常用的伪随机数生成算法,并且还是C/C++、Java和C#等语言内置的伪随机数生成算法。
线性同余生成法不适用于对随机性有较高要求的应用场景;此外由于算法的序列相关性,线性同余生成法通常对蒙特卡洛方法也无甚大用(序列相关又称“自相关”,指的是变量随时间序列的变化对自身也会产生影响)。这就意味着线性同余生成法得到的随机数品质并不太好,也就不适用于密码学场景。
算法的实现要用到一个被限制在一个特定的周期内的线性函数
m = 2e31
a = 1103515245
c = 12345
进位乘数伪随机数生成算法是由George Marsaglia创造的,其目的是生成周期很长的随机整数序列,其周期之长,最低约为260,最高可达22000000。该算法使用一个从2到数千内随机选取的数值作为初始种子,算法的主要优点在于它调用的是简单的计算机整数运算,可以以极快的速度产生随机数序列。
必须要定义一个变量r,用以描述进位乘数法中的“延迟”概念,并且必须提供r个值作为种子。
a为乘法因子,b为模数,此外还有一个表示进位的变量c。
进位计算方式如下:
变量n表示你当前所计算的数在序列中的序号,必须保证n不小于r,其原因在于n之前的x值会作为种子值,而我们共有r个x的初值作为种子。这个式子是需要向下取整。
进位乘数生成法所得结果的周期远长于线性同余生成法,并且其实现的执行速度还通常都特别快,相对于线性同余生成法来说更具竞争力。
梅森旋转算法[插图]是由Makoto Matsumoto和Takuji Nishimura于1997年开发的伪随机数生成算法,可以以极快的速度生成高质量的伪随机整数。该算法的设计初衷是要解决已有算法的各种缺陷。
梅森旋转算法是一种常用的伪随机数生成算法,也是Ruby、Python和R语言的内置伪随机数生成算法,可以生成适用于蒙特卡洛方法的高质量随机数。虽说梅森旋转算法并非密码学意义上的安全随机数发生器,但其在运行速度和随机性上的优势依然使它在人工智能领域大受欢迎。
“梅森旋转算法”这个名字来自于“该算法的周期总是一个梅森素数”这么一个事实。
所谓“素数”,指的是只能被1和其本身整除的数,比如5就是一个素数。而梅森素数则是满足 M n = 2 n − 1 M_n=2^n-1 Mn=2n−1的素数
比如 2 5 = 31 , 32 − 1 = 31 2^5=31 , 32-1=31 25=31,32−1=31 31是一个素数并且满足上式,所以31是一个梅森素数。
可以把均匀分布随机数转换为正态分布随机数的算法。因为并非所有编程语言都支持生成正态分布随机数。
设 X 1 , X 2 X_{1},X_{2} X1,X2 是在[0,1)上遵从均匀分布的随机数(生成[0,1)上遵从均匀分布的随机数通常使用的是梅森旋转算法),令
Y 1 = − 2 l n X 1 c o s ( 2 π X 2 ) Y 2 = − 2 l n X 2 c o s ( 2 π X 1 ) Y_{1}=\sqrt{-2lnX_{1}}cos(2\pi X_{2})\, \, \, \, Y_{2}=\sqrt{-2lnX_{2}}cos(2\pi X_{1}) Y1=−2lnX1cos(2πX2)Y2=−2lnX2cos(2πX1)
则 ( Y 1 , Y 2 ) T (Y_{1},Y_{2})^{T} (Y1,Y2)T 必然遵守二元正态分布,即利用两个独立的遵从均匀分布的随机数得到两个独立的正态分布的随机数
#include
#include
#include
using namespace std;
const double pi = 3.1415926897932384;
int main() {
ios::sync_with_stdio(false);
double x1, x2,y1,y2;
int seed;//手动输入种子
cout << "输入种子(任给整数)" << endl;
cin >> seed;
srand(seed);
x1 = rand()% RAND_MAX /(double) RAND_MAX;
x2= rand() % RAND_MAX / (double)RAND_MAX;
cout << "产生的均匀分布的随机数种子为" << endl;
cout << x1 <<" "<< x2 << endl;
y1 = sqrt(-2 * log(x1))*cos(2 * pi*x2);
y2 = sqrt(-2 * log(x2))*sin(2 * pi*x1);
cout << "输出的正态分布的随机数为" << endl;
cout << y1 << " " << y2 << endl;
return 0;
}
由于很多情况下计算实际值会耗费大量时间,因此蒙特卡洛方法的思想是通过随机采样来对实际值进行估算,并且可以得到良好的估算结果。
蒙特卡洛方法的一个简单示例是用蒙特卡洛方法估算PI值。图4-4是一个内切于正方形的圆。
我们在正方形和圆形中随机放置一些点,利用圆内点比上正方形内全部点的比例就可以计算出PI值。正方形的面积等于长和宽的乘积,由于正方形长宽相等,实际上正方形的面积就是“宽乘宽”,或者说是“边长的平方”。圆形的面积公式为“PI乘以半径的平方”,而该圆直径又等于正方形边长。
tries = 0
success = 0
for i in from 0 to 1000{
//随机取点
x = rand()
y = rand()
tries = tries +1
if(x*x+y*y<=1){
success++
}
}
pi = 4*success / tries
由此可以看出,随机的点越多,PI的估计值就越精确。