許多程式需要(表面上)隨機的值來模擬真實世界的事件。
常見且熟悉的例子包括:
('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-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)相同的結果。
Mary 是電視影集 Star Trek: The Next Generation 的超級粉絲。她常玩桌上角色扮演遊戲,在遊戲中她和朋友們假裝自己是 Starship Enterprise 的船員。Mary 的角色是 Picard 艦長,這表示她得負責記錄艦長日誌。她喜歡遊戲裡充滿創意的部分,但不喜歡臨時產生隨機資料。
請幫 Mary 建立隨機產生器,用來產生艦長日誌中常見的資料。
Starship Enterprise 在航行途中會遇到許多行星。Star Trek 宇宙中的行星會依其特性分成不同類別。例如,地球是 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(),讓它也能處理其他可迭代物件,例如元組或集合?
這個部分沒有測試。