もし直接numpy.random配下のメソッドを呼んだり、初期値を固定するためにRandomStateによって状態を記録する方法をとっているならnumpy.random.Generatorを使ったもっと良い方法がある。どう使うのか、なぜこちらの方が良いと思ったのかについて説明する。

Generatorの基本的な使い方

まずはどう使うのか。

from numpy import random
rng = random.default_rng() # or random.default_rng(0)

以上がnumpy.random.Generatorを使うにあたって必要な基本的なコードとなる。初期値を固定して決定的なシーケンスを得たい場合にはdefault_rng(0)のように適当な整数を与えればよい。整数以外も渡すことができるが知らなくてよいだろう。

全ての乱数生成はnumpy.random.Generatorのデフォルトのインスタンスであるところのrngによってアクセスすることが出来る。メルセンヌ・ツイスタを使いたいなどがあればデフォルト以外の方法でGeneratorのインスタンスを作ることもできるがこちらも知らなくてよいだろう。以前におけるRandomStateと同じだが後で述べるようにこちらの方が機能面で上回っている。

具体的に乱数を生成する方法についていくつかの例をあげておく。

#区間[0,1)の一様分布
x = rng.random() 

#区間[a, b)の一様分布
x = rng.uniform(-1, 1)

#標準正規分布(=rng.normal(0, 1))
x = rng.standard_normal()

#平均1,分散2^2の正規分布
x = rng.normal(1, 2) 

#0以上100未満の整数(等確率)
x = rng.integers(0, 100) 

他にchoiceshufflepermutationなどの前からあるものは使える。完全なリストについてはhttps://numpy.org/doc/stable/reference/random/generator.htmlにある。

なぜGeneratorによる疑似乱数生成がよいのか

次になぜ良いのか。

  • PCG64というビットジェネレータ
  • 多くの分布に対応するメソッドを持っている
  • メソッド名やキーワード引数の名前が以前よりも直観的

アルゴリズム

v1.19現在においてGeneratorは以前のバージョンにおけるRandomStateを置き換えるものと位置づけられており、メソッドなどの見た目以外に変わった点として、疑似乱数生成のアルゴリズム(リファレンスではビットジェネレータと言われている)として以前のバージョンではデフォルトだったMT19937(メルセンヌ・ツイスタ)がPCG64に置き換えらたことがある。

疑似乱数生成のアルゴリズムの良しあしは知る所ではないが、より新しいGeneratorがデフォルトでPCG64を採用しているのだからPCG64の方が性能は良いと推測するのが自然だろう。GeneratorからMT19937を使う手段も残されているため性能は同じかもしくは良いくらいなのかもしれない。乱数の周期以外にも計算量の側面もあるので一概にどちらとは言えないのかもしれない。

なお、default_rng()が用いるビットジェネレータは今後のバージョンで変更が成させる可能性があるとリファレンスは言っている。従って初期値を指定して乱数を固定するという操作はバージョンアップによっては働かなくなるため注意が必要だ。一方でただ乱数を生成したいという目的であるならdefault_rng()を使用している限りその時で最も良いとされる疑似乱数生成アルゴリズム、ビットジェネレータが用いられるためその点は便利とも言える。

扱える乱数の分布

これについてもhttps://numpy.org/doc/stable/reference/random/generator.htmlを見るのが早いだろう。標準的なものからマニアックなものまでおよそその確率分布のパラメタを指定するだけで乱数値を生成することが出来る。

実際、リファレンスでも"a much larger number of probability distribution"と言っていて従来のnp.randomにあるメソッドやRandomStateからそれらにアクセスするよりも扱える分布が非常に多い。これは明らかな利点だろう。

直観的なメソッド名とキーワード

まず問いたいのはrandom.randn(100,10)が何をしているか直観的にわかる人がどれくらいいるのかということだ。答えは「標準正規分布に従う乱数を要素に持つサイズ(100,10)のndarrayを返せ」だ。わかるかこんなもん。しかもnumpy.ones((100,10))という表現とも一貫性が無く非常にわかりにくい。こちらはサイズをタプルで与えるためにカッコが余分に必要だが意味するところは同じ。

一方でGeneratorによって同じリクエストを書いた場合はこうだ。

x = rng.standard_normal(size=(100,10))

これは見るだけで全員わかるだろう。以下も同じ結果を与える。

x = rng.normal(0, 1, size=(100,10))

standard_の文字数が多いせいでこちらの方が短く書けてしまうのは単なる事故のようなものだ。Generatorに実装されたメソッドには一貫してsizeキーワードが用意されていて整数でもタプルでもとにかくこれにndarrayのshapeを指定すれば所望する分布の乱数を要素にもつndarrayが返される。キーワードがshapeではなくsizeな点はやや微妙かもしれないが可読性は前より格段にマシだろう?

まとめ

いますぐnumpy.random.default_rng()からnumpy.random.Generatorを使え。