许多程序需要用(看似)随机的值来模拟现实世界的事件。
常见且熟悉的例子包括:
('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分布。
也叫“正态”分布或“钟形”曲线,这是描述测量值不精确性的一种非常常见的方式。
例如,假设你工作的工厂刚买了 10,000 个本应完全相同的螺栓。
你想设置工厂机器人来处理它们,于是称量了 100 个作为样本,发现它们的平均(或mean)重量是 4.731g。
这极不可能意味着它们每个都恰好重 4.731g。
也许你发现值分布在 4.627 到 4.794g 之间,但聚集在 4.731g 附近。
这就是Gaussian distribution,其概率在平均值处达到峰值,并向两侧对称地逐渐减小(因此是“钟形”)。
要在软件中模拟它,我们需要某种方式来指定曲线的宽度(通常来说,昂贵的螺栓会比便宜的螺栓更紧密地聚集在平均值附近!)。
按照惯例,这用standard deviation来表示:值小则曲线又尖又窄,值大则曲线又低又宽。
数学家喜欢希腊字母,所以我们用μ('mu')表示平均值,用σ('sigma')表示标准差。
因此,如果你读到“95% 的值都在 μ 的 2σ 范围内”或“希格斯玻色子以 5 西格玛的置信度被探测到”,这类说法都和标准差有关。
关于这一点,Statistics概念里会有更多介绍。
randn()函数它是“random normal”的缩写,和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
光看输出,很难看出原始输出比均匀分布更聚集在 0 附近。 如果你不信,就生成 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)一样。
Mary 是电视剧 Star Trek: The Next Generation 的忠实粉丝。她经常玩纸上角色扮演游戏,在游戏里,她和朋友们假扮 Starship Enterprise 的船员。Mary 扮演的角色是 Picard 舰长,所以她得负责记录舰长日志。她很喜欢游戏里需要发挥创意的部分,但不喜欢临场编造随机数据。
帮 Mary 写一些随机生成器,用来生成舰长日志中常见的数据吧。
Starship Enterprise 在航行途中会遇到许多行星。星际迷航宇宙中的行星按照各自的特性被划分成不同类别。比如,地球是一颗 M 级行星。所有可能的行星级别有:D、H、J、K、L、M、N、R、T 和 Y。
实现 random_planet 函数。它应该随机返回其中一种行星级别。
julia> random_planet_class()
'K'
Enterprise(注册号 NCC-1701)可不是唯一在太空中飞行的星舰!当它与其他星舰会合时,Mary 需要记录下那艘星舰的注册号。
注册号以“NCC-”为前缀,后面是 1000 到 9999 之间的数字(含两端)。
实现 random_ship_registry_number 函数,返回一个随机的星舰注册号。
julia> random_ship_registry_number()
"NCC-1947"
日志要是不包含日期,那还有什么用?
星历是一个浮点数。Starship Enterprise 在 The Next Generation 第一季中的冒险,发生在星历 41000.0 到 42000.0 之间。其中的“4”代表 24 世纪,“1”代表第一季。
实现 random_stardate 函数,返回一个介于 41000.0(含)和 42000.0 之间的浮点数。
julia> random_stardate()
41458.15721310934
有人抱怨星历的小数点太多了,于是 Mary 决定把日志里的星历四舍五入到一位小数。
虽然可以给 random_stardate() 返回的值做四舍五入,但你发现还有一种简单得多的做法。
实现 random_stardate_v2 函数,返回一个介于 41000.0 和 42000.0 之间(含两端)、保留一位小数的浮点数。
julia> random_stardate_v2()
41732.6
游戏有一个版本需要与其他星舰互动,Mary 得挑选几艘出来。
这里有一份所有可能星舰的完整列表,但每局游戏只需要其中几艘,而且应该随机挑选。
实现 pick_starships(starships, number_needed) 函数,它接收一个由不重复的星舰注册号组成的向量,并返回一个长度为 number_needed 的向量。结果中的值应当取自 starships,且不能重复。
julia> pick_starships(["NCC-5011", "NCC-1228", "NCC-7039", "NCC-3978", "NCC-1476"], 2)
2-element Vector{String}:
"NCC-1228"
"NCC-5011"
我们把 starships 实参限制成了向量。如果想让 pick_starships() 也能处理其他可迭代对象,比如元组或集合,你会怎么实现?
这部分没有测试。