许多程序都需要(看起来)随机的值来模拟现实世界中的事件。
常见而熟悉的例子包括:
('H', 'T')中随机取一个值。用计算机生成真正随机的值是一个出奇困难的技术挑战,所以你可能会看到这类结果被称为“伪随机”。
重要:本概念_不_涉及密码学安全的随机数,那要困难得多。
不过,像 Julia 标准库中的Random模块这样设计良好的库,既快又灵活,其结果对建模、仿真和游戏中的大多数应用来说都足够好了。
Julia 把随机功能分散在好几个地方:
Base里只有几个基础但非常通用的函数,它们始终可用。Random模块里则有更丰富的选项。Random属于标准库,很可能已经预装好了,但你需要在自己程序的顶部加上using Random,才能把它的内容引入命名空间。
rand()函数这个函数做什么取决于你传给它的实参。选项有很多。
不带实参时,它会生成一个 0(含)到 1 之间的浮点数。这是一个uniform分布,所有取值出现的可能性相同,下文“处理分布”一节会进一步讨论。
传入一个整数实参时,会生成一个该长度的向量。
julia> rand()
0.10261774967264703
julia> rand(5)
5-element Vector{Float64}:
0.24134501977563894
0.5664193284851202
0.9804412082089355
0.6229551330613335
0.47589221741904664
想要别的范围,只要对结果做相应的平移和缩放即可。
下面的例子用广播来完成减法,向量运算概念里讲过。.-就是把这段运算应用到向量的每个元素上。
# numbers between -1.0 and +1.0
julia> (rand(5) .- 0.5) * 2
5-element Vector{Float64}:
-0.5303906759076336
0.9635682226775855
-0.048823697086981754
0.465842804648374
0.9880834344780736
如果唯一的实参是一个类型,rand会用typemin和typemax作为边界。这多半不是你想要的!
要生成随机整数,可以传入一个范围,另外还可以选择性地指定生成多少个值。
julia> rand(Int64)
-9159538335234594326 # not very useful
julia> rand(1:10, 5)
5-element Vector{Int64}:
1
1
1
4
7
在上面的rand(1:10, 5)例子里,可以看到有(碰巧)重复的值,因为每次抽取都是独立的。这就是“有放回抽样”,下面会详细讨论。
要在某个范围内生成浮点数,通常需要指定步长。否则步长会默认为 1.0,这基本没什么用。
julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
3.19
2.53
3.14
3.13
另一种做法是传入数组或元组,rand会从里面随机返回一个元素:
julia> rand([4, 9, 16, 25])
16
# coin flip
julia> rand(['H', 'T'])
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
# mixed types in tuple
julia> rand( (1, 3.2, "name"), 2 )
2-element Vector{Any}:
1
"name"
想象一个袋子里有 3 个红球和 4 个绿球,我们随机从袋中摸出一个球。要再摸第二个球,有两种做法:
情形 1 是有放回,情形 2 是无放回,两者的结果不同。
在 Julia 中模拟无放回抽样,有几种做法。
最简单的一种(在 Exercism 上也是唯一可行的),是用Random.shuffle()把各个元素随机排序,然后取前n个元素。对小规模问题这没什么问题,但对大型集合可能就不太合适:即使你只想要其中一小部分,shuffle也需要生成整个数组。
要想“正规地”做有放回抽样,可以安装StatsBase.jl包。它提供的sample()函数有完整的选项。
我们有理由期待类似的功能会在将来的版本中被加入Random,从而成为标准库的一部分(本文档中的代码示例用 Julia 1.11 测试过)。
到目前为止,我们关注的都是各种结果出现可能性相同的情况。例如,rand(1:100)取 1 到 100 之间任何整数的可能性都一样。
现实世界中的许多情况远比这复杂。因此,统计学家设计了各种各样的distributions,用来从数学上描述“现实世界”的结果。
前面介绍的rand()函数用于所有概率都相等的情况。这称为[uniform][uniform-distribution]分布。
它也叫“正态”分布或“钟形”曲线,是描述测量值不精确性的一种非常常见的方式。
例如,假设你所在的工厂刚买了 10,000 个本应完全相同的螺栓。你想设置工厂机器人来处理它们,于是称量了 100 个作为样本,发现它们的平均(或称mean)重量是 4.731g。这绝不意味着它们的重量都恰好是 4.731g。也许你会发现数值在 4.627 到 4.794g 之间,但大多集中在 4.731g 附近。
这就是Gaussian distribution,它的概率在均值处最高,并向两侧对称地递减(因此叫“钟形”)。要在软件中模拟它,我们需要某种方式来表示曲线的宽度(通常,昂贵的螺栓会比便宜的螺栓更紧密地集中在均值附近!)。
按照惯例,这用standard deviation来表示:数值小则曲线尖锐而窄,数值大则曲线低平而宽。数学家喜欢希腊字母,所以我们用μ('mu')表示均值,用σ('sigma')表示标准差。因此,如果你读到“95% 的值都在 μ 的 2σ 范围内”或“希格斯玻色子以 5 sigma 的置信度被探测到”,这类说法都与标准差有关。
关于这一点,Statistics概念里还有更多内容。
randn()函数它是“随机正态”的缩写,与rand()的浮点数版本类似,只是它的取值服从均值为 0、标准差为 1 的高斯分布。
同样,你可能想把randn的原始输出按标准差缩放,再按均值平移。下面的例子把它转换成均值 30、标准差 5。
julia> raw = randn(5)
5-element Vector{Float64}:
3.0762588867281475
1.5101100620253902
-0.5914858221637778
0.684175554069735
-0.8416433926114673
julia> raw * 5 .+ 30
5-element Vector{Float64}:
45.38129443364074
37.55055031012695
27.04257088918111
33.420877770348675
25.791783036942665
光看输出很难看出,原始输出比均匀分布更集中在零附近。如果你有疑问,就生成 1000 个或更多,并画图,这样会明显得多。
Random模块这个模块包含下一层级的功能,它们没有放进Base,是为了尽量减小 Julia 默认配置的体积。
Random为Base中的rand和randn补充了会原地修改的版本:rand!和randn!。
一个很实用的补充是randstring,它能生成指定长度的字符串。默认情况下,它使用大小写字母加数字 0 到 9,但也可以指定别的字符。
julia> using Random
julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"
此外还有一个bitrand函数,用来生成指定长度的随机BitArray。
julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
1
1
0
0
1
要随机打乱Vector中的各个元素,可以用shuffle;还有shuffle!,它会原地修改输入的向量。
julia> v = ['A', '1', '2', 'J', 'Q', 'K'];
julia> shuffle(v)
6-element Vector{Char}:
'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
'1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
'2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
# shuffles are random:
julia> shuffle(v)
6-element Vector{Char}:
'2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
'1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
有时得到打乱后的下标反而更有用。这时就用randperm(n),其中 n 是序列的长度。
julia> randperm(6)
6-element Vector{Int64}:
6
2
4
1
3
5
实际上,上面的例子和shuffle(1:6)的结果相同。
相关函数还有randsubseq,用于按固定概率取出元素,以及randcycle,用于循环排列。这些函数需要一些专门知识,如果你感兴趣,可以查阅文档。
Random内置了好几种随机数生成器(RNG)算法,任何具备相应数学能力的人都可以再添加更多。这些内容远远超出本文档的范围!
更常见的操作随机数生成器的原因,是指定一个seed,这样“随机”输出的序列就能在每次运行时重现。这种可重现性不适合用在生产代码里,但对测试和调试很有帮助。
在 Exercism 之外,有许多可安装的包涉及随机性、概率和统计。想了解更多,可以看Statistics概念。
StatsBase.jl包StatsBase的大部分函数都相当专门,与本文档无关。
例外是StatsBase.sample,它完整实现了有放回和无放回抽样(见上文较早的一节)。另外还有用于加权(非均匀)抽样的函数。
Distributions.jl包uniform和normal(或高斯)分布前面已经介绍过。
Random模块还包含randexp,用于从指数分布中抽样,它与(非常常见的)泊松分布有关。
想要更多选择,还有Distributions.jl包,适合具备相应统计学背景的人。