多くのプログラムでは、現実世界の出来事をシミュレートするために、(見かけ上の)ランダムな値が必要になります。
よくある身近な例を挙げましょう。
('H', 'T')からランダムに選んだ値。コンピューターで真にランダムな値を生成するのは驚くほど難しい技術的課題なので、こうした結果は「疑似ランダム」と呼ばれることがあります。
重要:この概念では、暗号学的に安全な乱数を_扱いません_。それははるかに難しい課題です。
とはいえ、Juliaの標準ライブラリにあるRandomモジュールのような、よく設計されたライブラリは高速で柔軟であり、モデリングやシミュレーション、ゲームのほとんどの用途に十分なほど良い結果を返してくれます。
Juliaでは、ランダム関連の機能がいくつかの場所に分かれています。
Baseにある、基本的でありながら非常に汎用性の高いわずかな関数。これらはいつでも利用できます。Randomモジュールにある、より幅広い選択肢。Randomは標準ライブラリの一部で、おそらくすでにインストールされていますが、その内容を名前空間に読み込むには、プログラムの先頭にusing Randomを追加する必要があります。
rand()関数この関数が何をするかは、渡す引数によって決まります。 選択肢は_たくさん_あります。
引数を渡さない場合、0(0を含む)から1の間の小数を生成します。
これはuniform分布で、すべての値が等しく起こり得ます。詳しくは、後述の「確率分布を扱う」セクションで説明します。
整数を1つだけ引数に渡すと、その長さのベクトルを生成します。
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つ目のボールを取り出す方法には、2つの可能性があります。
シナリオ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]分布と呼ばれます。
「正規」分布や「釣り鐘型」の曲線とも呼ばれ、測定値のばらつきを表す非常によく使われる方法です。
たとえば、勤め先の工場が、同じはずのボルトを1万本購入したとします。
これらのボルトを扱えるように工場のロボットを設定したいので、100本をサンプルとして計量したところ、平均(mean)の重さが4.731gであることがわかりました。
これは、すべてのボルトがちょうど4.731gであることを意味する可能性は極めて低いです。
おそらく、値は4.627gから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
出力を見ただけでは、生の出力が一様分布よりもゼロの近くに集まっていることを見分けるのは難しいです。 疑わしい場合は、1000個以上生成してプロットすると、よりはっきりわかります。
Randomモジュールこのモジュールには、次の段階の機能が含まれています。これらは、Juliaのデフォルト構成のサイズを最小限に抑えるためにBaseから省かれています。
Randomは、Baseにあるrandとrandnを、破壊的なバージョンであるrand!とrandn!で補います。
便利な追加機能としてrandstringがあり、指定した長さの文字列を生成します。
デフォルトでは、大文字と小文字の英字に加えて0から9の数字を使いますが、他の選択肢も指定できます。
julia> using Random
julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"
さらに、指定した長さのランダムなBitArrayを生成するbitrand関数もあります。
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)
場合によっては、シャッフルされたインデックスのほうが役に立ちます。
その場合は、nをシーケンスの長さとしてrandperm(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パッケージがあります。