學習軌道
/
Julia
Julia
/
練習
/
艦長日誌
艦長日誌

艦長日誌

學習練習

簡介

許多程式需要(表面上)隨機的值來模擬真實世界的事件。

常見且熟悉的例子包括:

  • 擲硬幣:從('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分佈。

高斯分佈

也稱為「常態」分佈或「鐘形」曲線,這是一種非常常見的方式,用來描述測量值的不精確性。

例如,假設你工作的工廠剛買了 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 建立隨機產生器,用來產生艦長日誌中常見的資料。

1. 產生隨機行星

Starship Enterprise 在航行途中會遇到許多行星。Star Trek 宇宙中的行星會依其特性分成不同類別。例如,地球是 M 級行星。所有可能的行星等級為:D、H、J、K、L、M、N、R、T 和 Y。

請實作random_planet函式。它應該隨機回傳其中一種行星等級。

julia> random_planet_class()
'K'

2. 產生隨機星艦註冊編號

Enterprise(註冊編號 NCC-1701)並不是唯一在太空中飛行的星艦!當它與另一艘星艦會合時,Mary 需要記錄那艘星艦的註冊編號。

註冊編號以「NCC-」開頭,接著是 1000 到 9999 之間(含)的數字。

請實作random_ship_registry_number函式,它會回傳隨機的星艦註冊編號。

julia> random_ship_registry_number()
"NCC-1947"

3. 產生隨機星曆

如果日誌不包含日期,那還有什麼用呢?

星曆是浮點數。Starship Enterprise 在 The Next Generation 第一季中的冒險,發生在星曆 41000.0 到 42000.0 之間。「4」代表 24 世紀,「1」代表第一季。

請實作random_stardate函式,回傳 41000.0(含)到 42000.0 之間的浮點數。

julia> random_stardate()
41458.15721310934

4. 產生四捨五入的星曆

有人抱怨星曆的小數點太多了,所以 Mary 決定把日誌四捨五入到小數第一位。

雖然可以透過四捨五入random_stardate()的值來做到,但你會發現有更簡單的做法。

請實作random_stardate_v2函式,回傳 41000.0 到 42000.0(含)之間、小數點後只有一位的浮點數。

julia> random_stardate_v2()
41732.6

5. 從陣列中隨機挑選幾艘星艦。

遊戲的某個版本會和其他星艦互動,而 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"

5a. 可選:想想其他引數型別。

我們把starships引數限制成向量。你會如何實作pick_starships(),讓它也能處理其他可迭代物件,例如元組或集合?

這個部分沒有測試。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Julia Exercism

準備好開始 艦長日誌 了嗎?

註冊 Exercism,透過 35 個概念128 個練習 和真人引導來學習並精通 Julia,全部免費。