随机

随机性 属于 Julia

2 个练习

关于 随机性

许多程序都需要(看起来)随机的值来模拟现实世界中的事件。

常见而熟悉的例子包括:

  • 抛硬币:从('H', 'T')中随机取一个值。
  • 掷骰子:1 到 6 之间的随机整数。
  • 洗一副牌:把牌列表随机排成一个顺序。

用计算机生成真正随机的值是一个出奇困难的技术挑战,所以你可能会看到这类结果被称为“伪随机”。

重要:本概念_不_涉及密码学安全的随机数,那要困难得多。

不过,像 Julia 标准库中的Random模块这样设计良好的库,既快又灵活,其结果对建模、仿真和游戏中的大多数应用来说都足够好了。

Julia 把随机功能分散在好几个地方:

  • Base里只有几个基础但非常通用的函数,它们始终可用。
  • Random模块里则有更丰富的选项。
  • 更专门的功能则在需要先安装才能使用的包里(这些包在 Exercism 上不可用)。

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. 把第一个球放回袋中,摇匀后再摸一个。此时球的总数和之前一样(7 个),红球与绿球的比例也一样。
  2. 把第一个球放在桌上,再摸第二个。此时袋中只剩 6 个球,红绿比例取决于第一个球的颜色。

情形 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包,适合具备相应统计学背景的人。

通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 随机性

练习已锁定

再解锁 1 个练习即可练习 随机性