許多程式需要(表面上)隨機的值,來模擬真實世界的事件。
常見又熟悉的例子包括:
('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()函式它是「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)。
相關的函式還有randsubseq,用來以固定機率抽出項目,以及randcycle,用來產生循環排列。這些都需要一些專門知識,如果你有興趣,請查閱文件。
Random內建了好幾種亂數產生器(RNG)演算法,任何具備適當數學能力的人都能再自行加入更多。這些都遠超出本文件的範圍了!
更常見會去碰 RNG 的原因,是指定seed,這樣就能讓「隨機」輸出的序列在每次執行之間可重現。
這種可重現性不適合用在正式產品程式碼裡,但有助於測試與除錯。
在 Exercism 之外,有許多可安裝的套件跟隨機、機率與統計有關。想多了解一些,請參閱Statistics概念。
StatsBase.jl套件StatsBase的大多數函式都相當專門,跟本文件沒什麼關係。
例外是StatsBase.sample,它完整實作了取後放回與取後不放回抽樣(見前面〈取後放回與取後不放回〉一節)。另外也有加權(非均勻)抽樣的函式。
Distributions.jl套件uniform與normal(或高斯)分布前面已經介紹過了。
Random模組裡還有randexp,可以從指數分布取樣,這跟(非常常見的)卜瓦松分布有關。
想要多得多的選項,還有Distributions.jl套件,適合具備適當統計背景的人。