トラック
/
Julia
Julia
/
演習
/
船長の日誌
船長の日誌

船長の日誌

学習演習

はじめに

多くのプログラムでは、現実世界の出来事をシミュレートするために(見かけ上の)ランダムな値が必要になります。

よく知られた身近な例には、次のようなものがあります。

  • コイン投げ:('H', 'T')からランダムに取り出した値。
  • サイコロの目:1から6までのランダムな整数。
  • カードのシャッフル:カードのリストをランダムに並べ替えたもの。

コンピューターで真にランダムな値を生成するのは、驚くほど難しい技術的な課題です。そのため、こうした結果は「疑似乱数」と呼ばれることがあります。

重要:このコンセプトでは、暗号学的に安全な乱数を扱い_ません_。これははるかに難しい課題です。

しかし、Juliaの標準ライブラリにあるRandomモジュールのように、よく設計されたライブラリは高速で柔軟であり、モデリング、シミュレーション、ゲームといったほとんどの用途にとって十分に優れた結果をもたらします。

Juliaでは、乱数の機能が複数の場所に分かれています。

  • 常に利用できるBaseにある、少数の基本的でありながら非常に汎用性の高い関数。
  • Randomモジュールにある、より幅広い選択肢。
  • 使用前にインストールが必要なパッケージにある、より専門的な機能(Exercismでは利用できません)。

Randomは標準ライブラリの一部であり、あらかじめインストールされている可能性が高いですが、その内容を名前空間に持ち込むには、プログラムの先頭にusing Randomを追加する必要があります。

rand()関数

この関数が何をするかは、与える引数によって変わります。 選択肢は_たくさん_あります。

引数を指定しない場合、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はランダムに1つの要素を返します。

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. 1つ目の玉を袋に戻し、よく振ってからもう1つ取り出す。 玉の数は前と同じ(7つ)で、赤と緑の比率も同じです。
  2. 1つ目の玉をテーブルの上に置いてから、2つ目を取り出す。 袋の中の玉は6つだけになり、赤と緑の比率は1つ目の玉の色によって変わります。

シナリオ1は復元抽出、シナリオ2は非復元抽出で、得られる結果は異なります。

Juliaで非復元抽出をシミュレートする方法は、いくつかあります。

最も簡単な方法(Exercismでは唯一の方法)は、Random.shuffle()で要素をランダムな順序に並べ替え、先頭からn個の要素を使うことです。 これは小さな問題には十分ですが、大きなコレクションにはうまく対応できないかもしれません。shuffleは、そのごく一部しか必要としない場合でも、配列全体を生成する必要があるからです。

復元抽出を「きちんと」行うには、StatsBase.jlパッケージをインストールします。 これにより、あらゆるオプションを備えたsample()関数が提供されます。

同様の機能が将来のリリースでRandomに追加され、標準ライブラリの一部になることを期待してよいでしょう(このドキュメントのコード例はJulia 1.11でテストしました)。

分布を扱う

ここまでは、すべての結果が等しい確率で起こる場合に注目してきました。 たとえば、rand(1:100)は1から100までのどの整数も同じ確率で返します。

現実世界の多くの状況は、これよりずっと複雑です。 そのため、統計学者たちは「現実世界」の結果を数学的に記述するために、多種多様なdistributionsを生み出してきました。

一様分布

上で説明したrand()関数は、すべての確率が等しいときに使います。 これはuniform分布と呼ばれます。

ガウス分布

「正規」分布や「ベル型」の曲線とも呼ばれ、測定値の不正確さを表すごく一般的な方法です。

たとえば、勤め先の工場が、本来は同一であるはずのボルトを1万本購入したとします。 そのボルトを扱えるように工場のロボットを設定したいので、100本をサンプルとして重さを量ったところ、平均(mean)の重さが4.731gでした。 だからといって、すべてがちょうど4.731gである可能性は極めて低いです。 値は4.627gから4.794gの範囲にありながら、4.731gの周辺に集まっているかもしれません。

これがGaussian distributionで、確率は平均で最も高くなり、両側に対称的に裾を引きます(だから「ベル型」なのです)。 これをソフトウェアでシミュレートするには、曲線の幅を指定する何らかの方法が必要です(一般に、高価なボルトは安価なボルトよりも平均の周りに密集します!)。

慣例として、これはstandard deviationで行います。値が小さいほど鋭く狭い曲線に、大きいほど低く広い曲線になります。 数学者はギリシャ文字が大好きなので、平均を表すのにμ(「ミュー」)、標準偏差を表すのにσ(「シグマ」)を使います。 したがって、「値の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

出力を見ただけでは、生の出力が一様分布よりも0の近くに集まっているとは分かりにくいです。 疑わしいなら、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)と同じ結果になります。

説明

Maryはテレビドラマ『Star Trek: The Next Generation』の大ファンです。彼女はよく、ペンと紙を使うロールプレイングゲームで遊んでいて、そこでは友人たちと一緒に Starship Enterprise のクルーになりきります。MaryのキャラクターはCaptain 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. ランダムな宇宙船の登録番号を生成する

エンタープライズ号(登録番号NCC-1701)は、宇宙を飛び回っている唯一の宇宙船ではありません! 別の宇宙船とすれ違うとき、Maryはその宇宙船の登録番号をログに記録する必要があります。

登録番号はNCC-という接頭辞で始まり、そのあとに1000から9999までの数値(両端を含む)が続きます。

random_ship_registry_number関数を実装しましょう。ランダムな宇宙船の登録番号を返します。

julia> random_ship_registry_number()
"NCC-1947"

3. ランダムな宇宙暦を生成する

日付が含まれていないログに、何の意味があるのでしょうか?

宇宙暦は浮動小数点数です。The Next Generation の第1シーズンにおける Starship Enterprise の冒険は、宇宙暦41000.0から42000.0の間で繰り広げられます。「4」は24世紀を、「1」は第1シーズンを表します。

random_stardate関数を実装しましょう。41000.0(両端を含む)から42000.0までの浮動小数点数を返します。

julia> random_stardate()
41458.15721310934

4. 丸めた宇宙暦を生成する

宇宙暦の小数点の桁が多すぎるという苦情が寄せられたため、Maryはログを小数点以下1桁に丸めることにしました。

random_stardate()の値を丸めても実現できますが、もっとずっと簡単な方法があることに気づくはずです。

random_stardate_v2関数を実装しましょう。41000.0から42000.0まで(両端を含む)の範囲で、小数点以下が1桁の浮動小数点数を返します。

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を学んでマスターできます。すべて無料です。