この記事では、Python の標準ライブラリと NumPy で使用できる、一様乱数(どの値も等しい確率で得られる乱数)を生成する関数について解説します。
疑似乱数生成器
Python 標準ライブラリの random モジュールには様々な種類の 疑似乱数 を生成する関数が用意されています。疑似乱数は一定の周期をもちますが、この周期が長いほど高品質の疑似乱数であることを意味します。Pythonではメルセンヌツイスタ (Mersenne twister) とよばれる周期 2**19937-1 の生成器(ジェネレータ)を使用して疑似乱数を作りだしていきます。
たとえば、整数の乱数が必要な時は randrange() または randint() を使います。randrange() の引数の指定の仕方は Python エンジニアにはお馴染みの range() 関数とよく似ています。range(10) が 0 ~ 9 の整数を格納したシーケンスを返すように、randrange(10) は 0 ~ 9 の整数の中から無作為に一つの数を選んで返します。
# PYTHON_RANDOM # In[1] # randomモジュールをインポート import random # 乱数シードを固定 random.seed(0) # 0~9の乱数を生成 val = random.randrange(10) print(val) # 6
記事の後半で詳しく解説しますが、上のコードでは random.seed() で乱数の種を固定して、皆さんが上のコードをコピペして実行したときに同じ結果になるようにしています。この一文を削除すると、コードを実行するたびに異なる数値が返ります。
複数の乱数をリストに格納したいときは、空のリストを用意して、append() メソッドで乱数を順次追加します。
# In[2] random.seed(0) rand_list = [] # 0~9の乱数リストを生成 for i in range(10): val = random.randrange(10) rand_list.append(val) print(rand_list) # [6, 6, 0, 4, 8, 7, 6, 4, 7, 5]
実は randrange() に引き数を一つだけ渡した時は第1引数を省略して第2引数で上限値のみ指定していることになります。このあたりも range() と同じですね。第1引数を明示すれば下限値を設定することもできます。たとえば、サイコロのように 1 ~ 6 の乱数を得たいときは randrange(1, 7) と書きます。
# In[3] random.seed(3) dice_list = [] # サイコロを10回振った時の目を記録する for i in range(10): val = random.randrange(1, 7) dice_list.append(val) print(dice_list) # [2, 5, 5, 2, 3, 5, 4, 6, 5, 1]
randrange() の第3引数は step です。たとえば、randrange(1, 100, 2) は 1 から 2 step 間隔で並んだ整数リスト [1, 3, 5, 7, 9, … 97, 99] から無作為に一つの要素を抜き出します(つまり奇数の乱数です)。
# In[4] random.seed(0) odd_rand_list = [] # 1~99のランダムな奇数のリストを生成 for i in range(10): val = random.randrange(1, 100, 2) odd_rand_list.append(val) print(odd_rand_list) # [49, 97, 53, 5, 33, 65, 63, 51, 39, 61]
0.0 ~ 1.0 の浮動小数点数型乱数が欲しい場合は、random() 関数を用います。
# In[5] random.seed(0) # float型の乱数を生成 val = random.random() print(val) # 0.8444218515250481
乱数の上限値と下限値を指定したい場合は、random.uniform() を使います。
# In[6] random.seed(0) rand_list = [] # 100~200のfloat型乱数リスト for i in range(5): val = random.uniform(100, 200) rand_list.append(round(val, 2)) print(rand_list) # [184.44, 175.8, 142.06, 125.89, 151.13]
メモリを節約したいならば、ジェネレータ式で乱数ジェネレータを作成しておくのも一つのテクニックです。
# In[7] random.seed(0) # 乱数ジェネレータを生成 rand_gen = (random.randrange(10) for x in range(5))
取り出された乱数と、乱数の平方数を出力してみましょう。
# In[8] for x in rand_gen: val = (x, x**2) print(val) # (6, 36) # (6, 36) # (0, 0) # (4, 16) # (8, 64)
外部ライブラリの NumPy をインポートすれば、乱数配列を効率的に作成できます。NumPy の乱数生成関数は numpy.random モジュールにまとめられています。関数名や使い方は標準ライブラリの random モジュールとほとんど同じですが、整数型乱数を生成する numpy.random.randint() の引数の指定方法 random.randint() ではなく、random.randrange() と同じです。numpy.random.randint(a, b) の戻り値の上限値は a – 1 であることに注意してください。たとえば、numpy.random.randint(1, 10, 15) は下限値 1, 上限値 9 の乱数が 15 個格納された配列を返します。
# In[9] import numpy as np # seedを固定 np.random.seed(1) # 整数乱数の配列を生成 # 乱数の下限値は1,上限値9,配列の要素数は15 rand_list = np.random.randint(1, 10, 15) print(rand_list) # [6 9 6 1 1 2 8 7 3 5 6 3 5 3 5]
random.random()
random.random() は 0.0 以上、1 未満の 浮動小数点数型の一様乱数を生成します。
# PYTHON_RANDOM_RANDOM # In[1] import random random.seed(0) # 0以上1未満のfloat型乱数を生成 val = random.random() print(val) # 0.8444218515250481
random.uniform(a, b)
random.uniform(a, b) は a ≦ b のときは、a ≦ N ≦ b の範囲で浮動小数点数型の一様乱数 N を返します。
# PYTHON_RANDOM_UNIFORM # In[1] import random random.seed(0) rand_list = [] # 15以上30未満のfloat型乱数リスト for i in range(5): val = random.uniform(15, 30) rand_list.append(round(val, 3)) print(rand_list) # [27.666, 26.369, 21.309, 18.884, 22.669]
a > b のときは、b ≦ N ≦ a の範囲で浮動小数点数型の乱数 N を返します。
# In[2] import random random.seed(0) rand_list = [] # 100以上50未満のfloat型乱数リスト for i in range(5): val = random.uniform(100, 50) rand_list.append(round(val, 3)) print(rand_list) # [57.779, 62.102, 78.971, 87.054, 74.436]
random.randrange(start, stop [, step])
random.randrange(start, stop [, step]) は start から stop-1 まで step 刻みで並んだ数字の中から1つをランダムに選んで返します。たとえば
x = random.randrange(1, 16, 2)
と記述した場合、1 から 15 まで 2 刻みで並んだ数字
1, 3, 5, 7, 9, 11, 13, 15
の中から1つを無作為に選んで x に格納します。実際には、この関数は range(start, stop, step) で生成された数字のシーケンスから1つの要素を抜き出すという処理を行なっています。
# PYTHON_RANDOM_RANDRANGE # In[1] import random random.seed(5) rand_list = [] # [1,3,5,7,9,11,13,15]から無作為に5個選ぶ for i in range(5): val = random.randrange(1, 16, 2) rand_list.append(val) print(rand_list) # [9, 11, 1, 15, 7]
キーワード引数 step が省略されると、step=1 が設定されたことになり、連番から無作為に数値が選ばれます。
# In[2] import random random.seed(0) rand_list = [] # [1,2,3,4,5]から無作為に5個選ぶ for i in range(5): val = random.randrange(1, 6) rand_list.append(val) print(rand_list) # [4, 4, 1, 3, 5]
random.randrange() に一つだけ引数を渡して実行する場合、下限値ではなく上限値が指定していることに注意してください。このとき、第1引数の下限値は 0、キーワード引数 step は step=1 が指定されたことになります。
# In[3] import random random.seed(0) rand_list = [] # [0,1,2,3,4]から無作為に5個選ぶ for i in range(5): val = random.randrange(6) rand_list.append(val) print(rand_list) # [3, 3, 0, 2, 4]
random.randint(a, b)
random.randint(a, b) は、a ≦ N ≦ b の範囲で無作為に選ばれた整数 N を返します。この関数は randrange(a, b+1) のエイリアスです。すなわち、実際にはこの関数の内部で randrange() に引数 a と b+1 を渡して呼び出すという処理を行なっています。引数の指定の仕方が randrange() とは異なっていることに注意してください。
# PYTHON_RANDOM_RANDINT # In[1] import random # 空白のリストを用意 my_list = [] # 1~100の乱数を5個生成してリストの要素に追加 for k in range(5): x = random.randint(1, 100) my_list.append(x) print(my_list) # [38, 44, 98, 83, 34]
random.seed()
randomモジュールのジェネレータ(メルセンヌツイスタ)は seed とよばれる初期値をもとに疑似乱数列を生成しています。seed は「種」という意味の英語です(数字を次々と生み出すのでこのようによばれています)。seed 値が与えられると疑似乱数列は一意に定まります。random.seed(a) を使うと、seed 値を固定してジェネレータを初期化することができます。具体例を下のサンプルコードで説明します。
# PYTHON_RANDOM_SEED # In[1] import random # 疑似乱数ジェネレータを初期化して乱数シードを固定 random.seed(1) # 1~10の乱数を3個生成 for k in range(3): x = random.random() print(x) # 1行開ける print("") # 疑似乱数ジェネレータを初期化して乱数シードを固定 random.seed(1) # 1~10の乱数を3個生成 for k in range(3): x = random.random() print(x) # 0.13436424411240122 # 0.8474337369372327 # 0.763774618976614 # 0.13436424411240122 # 0.8474337369372327 # 0.763774618976614
最初の random.seed(1) という記述によって、for ループで seed を 1 として乱数を順に生成(ジェネレート)しています。そのあとに、もう一度 random.seed(1) と記述することによって、ジェネレータが初期化され、次の for ループで再び seed = 1 の乱数列の 1 番目から同じ数字が並びます。random.seed(a) の引数 a が省略された場合には、seed として現在のシステム時刻が指定されます。
いくつかの乱数をまとめて作りたい場合、先ほどの randint() のサンプルコードのように for 文を使っても可能ですが、リスト内包表記 (comprehention) を使うと、たった1行で乱数リストをつくることができます。
# PYTHON_RANDOM_COMPREHENTION # In[1] import random # 0から9までの整数乱数のリストを作成 num_list = [random.randrange(10) for i in range(10)] print(num_list) # [5, 8, 5, 0, 3, 9, 8, 8, 2, 9]
numpy.random.rand()
numpy.random.rand() は 0 以上 1 未満の一様乱数を要素にもつ配列を返します。
rand(d0, d1, ..., dn)
引数には配列の形を指定します。たとえば、2, 3 を渡すと、2行3列の乱数配列を返します。
# NUMPY_RANDOM_RAND # In[1] import numpy as np # seedを設定 np.random.seed(10) # 2行3列の乱数配列 x = np.random.rand(2, 3) print(x) # [[0.77132064 0.02075195 0.63364823] # [0.74880388 0.49850701 0.22479665]]
numpy.random.uniform()
numpy.random.uniform() は指定した範囲内の一様乱数を要素にもつ配列を返します。
numpy.random.uniform(low=0.0, high=1.0, size=None)
引数の low は乱数の下限値、high は乱数の上限値です。
size で戻り値の配列の形状を指定することもできます。
# NUMPY_RANDOM_UNIFORM # In[1] import numpy as np # seedを設定 np.random.seed(0) # 1~5のfloat型乱数を2×2サイズの配列に格納 x = np.random.uniform(1, 6, (2, 2)) print(x) # [[3.74406752 4.57594683] # [4.01381688 3.72441591]]
numpy.random.randint()
numpy.random.randint() は指定範囲内で整数型の一様乱数配列を返します。
numpy.random.randint(low, high=None, size=None, dtype='l')
引数の low は乱数の下限値、high は乱数の上限値です。
size で戻り値の配列の形状を指定することもできます。
# PYTHON_RANDOM_RANDINT # In[1] import numpy as np # seedを設定 np.random.seed(1) # 1~9の一様整数乱数を要素にもつ3×3配列を生成 x = np.random.randint(1, 10, (3, 3)) print(x) # [[6 9 6] # [1 1 2] # [8 7 3]]
random_integers は非推奨です
Jupyter Notebook でコードを書いてnumpy モジュールの random_integers という関数を呼び出そうと思ったら、
DeprecationWarning: This function is deprecated. Please call randint(1, 100 + 1) instead.
という警告メッセージが表示されました。日本語に訳しておきます:
この関数は廃止されます(非推奨です)。
代わりに randint(1, 100 + 1) を呼び出してください。
警告文が表示されるだけで、まだ使えることは使えるので、Python 本体 (現在の最新版は 3.6.5) から消えたわけではないようです。しかし、こんなに嫌われるには何か理由があるのだろうと思案してみると…確かに、この関数が非推奨になるのも当然かもしれないと思えてきました。
random_integers はランダムな整数を呼びだす関数です。
たとえば “random_integers(100)” と記述すると、1 から 100 までの乱数を生成します。
しかし、上の忠告にもある通り、randint() 関数があるので、わざわざこんな関数を作る必要もないはずです。「似たような機能は極力重複しないようにする」という Python の哲学にも明らかに反しています。
そもそも、random_integers() は引数の指定の仕方が不自然です。Range() 関数は Range(100) と書けば 0 から 99 の数字を返し、リストやタプルなどのイテラブルオブジェクトの要素のインデックスのつけ方も同様です。だから他の人が random_integers() を使ったコードを読むと、いらぬ誤解を与えてしまう可能性もあるわけです。う~む。これはいけませんね(← 使おうとしたくせに)。結論としては非推奨な関数は使わないほうが良いということです(← そんなことは誰でもわかる)。
それにしても最近のエディタは親切ですね。
「これを使ってはいけないよ」
と警告するだけでなく、
「代わりにこの関数を使おうね」
とアドバイスまでくれるのですから。私がプログラミングを学び始めた頃 (つまり大昔) は、コンピュータというものは、こちらが間違えると無愛想なエラーを返すだけなので、必死にどこが間違っているかを目で追っていました。本当に良い時代になったものです。
コメント
【Pythonの乱数生成法についての熱い議論】
執筆:ChatGPT
登場人物:
ジョン – 経験豊富なITエンジニア
メアリー – データサイエンティスト
デイビッド – ソフトウェア開発者
場面: IT会社のランチルームでの昼休み
(ジョン、メアリー、デイビッドがランチルームのテーブルに座っている)
ジョン:ねえ、最近Pythonの乱数生成法について調べていたんだけど、みんなはどの方法を使っているの?
メアリー:それは興味深い話題ね。私は主にNumpyのrandomモジュールを使っているわ。Mersenne Twisterアルゴリズムを採用していて、高品質な乱数を生成できるんだ。
デイビッド:そうか、私も同じくNumpyを使っているけど、なぜMersenne Twisterを選ぶのか教えてほしいな。他にも乱数生成法はいくつかあるよね?
ジョン:そうだね、確かに他にもいくつかの方法があるけど、Mersenne Twisterは周期が非常に長いことが特徴だよ。2の19937乗の周期なんてすごいでしょ?
メアリー:それに加えて、Mersenne Twisterは均等な分布特性と大きな内部状態空間を持っているから、様々な応用に適しているのよ。ただし、セキュリティ上の用途には向いていないという点も注意が必要ね。
デイビッド:なるほど、それは興味深い情報だ。他にも有名な乱数生成法としては、リニアコングルーバ法やウェルチ・タウチャー法などがあるけど、どうなのかな?
ジョン:それらのアルゴリズムも確かに使われているけど、周期の短さや分布特性の偏りが問題になることがあるんだ。特にシミュレーションや暗号など、高品質な乱数が求められる場合には慎重に選ぶ必要があるよ。
メアリー:その通り。乱数生成法の選択は、応用の目的や性能要件によって異なるわけだから、注意が必要よね。
デイビッド:確かにそうだね。乱数生成法の選択は重要な要素だし、目的に合わせて慎重に選ぶべきだよね。ありがとう、みんな。また他のアルゴリズムについても調べてみようと思うよ。
(3人は議論を続けながらランチを楽しむ)
【AI連載小説】科学とコードの交差点(18)「一様乱数を生成するベストプラクティス」
Pythonサークルのメンバーたちは、一様乱数を生成するベストプラクティスについて熱心に議論していた。六郷開誠と刑部明信もその中にいて、新しい知識を得ることに興奮していた。サークルのメンバーが話し合っていると、一人が質問を投げかけた。「一様乱数を生成する方法って、いくつかあるけど、どれがベストなんだろう?」
別のメンバーが応えた。「確かに、randomモジュールのrandom()関数を使うこともできますが、大量の乱数を生成する場合や、特定の分布に従った乱数が必要な場合には、numpyやrandomモジュールのuniform()関数を使うこともあります」
開誠が続けて質問した。「他にも、numpyの方が高速で効率的なんですか?」
メンバーが答えた。「そうですね。numpyは高度な数値計算用のライブラリで、C言語で実装されているため処理が速いです。また、numpy.randomモジュールにはさまざまな分布に基づく乱数生成関数が用意されています」
「それに、numpyを使うと配列を扱いやすく、ベクトル化された演算が可能ですよね」
「そうです。特に大規模なデータセットで一様乱数を生成する場合は、numpyの利用がおすすめです。」
サークルの中で実際にコードを交換しながら、開誠と明信は一様乱数の生成におけるベストプラクティスを学び、新たな知識を吸収していった。しかし、しばらく経つと、Pythonサークルのメンバーたちは、一様乱数生成についてのベストプラクティスについて激しい言い合いになってしまった。雰囲気が熱くなり、開誠と明信もその中に巻き込まれていった。一人のメンバーが言い出した。「やっぱりrandomモジュールのrandom()関数がシンプルで良いんだよ」
別のメンバーが反論した。「それは確かだけど、大量の乱数を生成する場合には、numpyの方が速いし効率的だろう」
「でも、シンプルな用途であればrandomモジュールでも事足りるし、追加のライブラリを導入する手間も省けるね」
言い合いが激しさを増す中、別のメンバーが「それならば、どっちも使えばいいじゃないか」と提案した。すると、もう一人のメンバーが冷静に言った。「まずは目的によって選ぶのが良いんじゃないか。シンプルな使い方であればrandomモジュールでも十分かもしれないし、大規模で高速な処理が必要ならnumpyの方が向いている」
開誠と明信も冷静になり、意見が割れる中で各自が主張するポイントに理解を示し合っていった。サークルの雰囲気は激しい言い争いから、建設的な議論へと変わっていった。