0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NumPyの乱数生成について

0
Last updated at Posted at 2026-05-10

はじめに

NumPy の乱数生成について、理解があいまいなまま使用していたせいでハマったため、ここでちゃんとまとめて供養しておきます。

  • Version
Python: 3.10.17
NumPy: 2.2.6

Generator による「内部状態」管理

同じ seed を default_rng()(コンストラクタ) に渡して Generator を作成した場合、以下の rng1 と rng2 は別々の Generator オブジェクトですが、同じ内部状態 (state) を持ちます。

from numpy.random import default_rng
seed = 0
rng1 = default_rng(seed)
rng2 = default_rng(seed)

ここでは、この内部状態について詳しく見ていきます。
まず、rng1 と rng2 が持つ共通の内部状態を例えば、

state = 1

とします。(極端に単純化しています。実際ははるかに大きな状態空間と複雑な変換を使います。)
イメージとしては、「次の乱数を作るための現在状態」を表しています。
この内部状態を使って rng.random() メソッドが [0, 1) の乱数を生成するまでの過程を見ていきます。
説明用として、以下のような単純な疑似乱数生成器を考えます。

state = (5 * state + 1) % 10     # mod(10) で 0~9 の範囲に収める
output = state / 10              # [0, 1) にスケーリング

このとき、1回目の更新は、

state = (5 * 1 + 1) % 10
      = 6

となり、state の値が 1 ⇒ 6 に更新されます。
rng.random() メソッドは半開区間 [0, 1) の値を返したいので、この整数 6 を 10 で割った 0.6 を返す、という感じです。
まとめると、

state = (5 * state + 1) % 10
output = state / 10

となります。

さて、次にもう一度 rng.random() を呼び出す場合を考えます。
このとき、内部状態 state は呼び出しごとに初期化されているのではなく、先ほど更新した state = 6 から始まることになります。つまり、

state = (5 * 6 + 1) % 10
      = 1

となり、 state の値は 6 ⇒ 1 に更新されます。
そして、先ほどと同様に 1 を 10 で割った値 0.1 を返します。

以上をまとめると、

シード seed = 0 ⇒ 初期状態 state = 1

1回目 random()
    state: 1 ⇒ 6 (決まったアルゴリズムで次の整数・ビット列をつくる + 内部状態を更新する)
    output: 6 / 10 = 0.6 ([0, 1) の小数にスケーリング)

2回目 random()
    state: 6 ⇒ 1
    output: 1 / 10 = 0.1

というように、シードによって一意に決まった Generator の内部状態 state は、Generator のメソッドを呼び出すたびに更新され、その状態から次の出力値が計算されます。

つまり、rng.random() は本当にランダムに小数を 「選んでいる」 のではなく、決まったアルゴリズムに従って 「計算」 しているだけなのです。なので、同じ seed から始めれば同じ内部状態になり、同じ内部状態から同じ計算をすれば、当然同じ値になる、というわけです。

また、rng.choice(10, 2) も同様に、seed によって内部状態が定まった後、決められたアルゴリズムに従って内部状態の更新と計算が行われ、最終的に2つの整数を返す、という仕組みです。
つまり、Generator が持つ内部状態から、疑似乱数アルゴリズムに従って乱数をつくり、それを各メソッドのアウトプットに応じた形へ変換している ということです。

random()
  内部状態 ⇒ 小数

choice()
  内部状態 ⇒ 整数選択

注意
厳密には、Generator 自体は分布メソッド(「どんな確率分布に従う乱数が欲しいか」を指定して乱数を返すメソッド)を提供し、Generator が内部に保持している BitGenerator (PCG64) が状態管理と乱数ビットの生成を担当します。

BitGenerator
    ↓
ランダムなビット列・整数列を作る
    ↓
Generator
    ↓
そのビット列を、ほしい確率分布の乱数に変換する

本記事では説明を簡単にするため、以降も「Generator が内部状態を持つ」と表現します。

同じ seed でも、呼び出しの回数や順序によって結果は変わる

重要な注意点として、「たとえ同じ seed でも、メソッド呼び出しのたびに内部状態は更新されるので、呼び出しの回数や順序によって乱数生成結果が異なる」 という点があります。

  • 呼び出しの回数が違う場合
seed = 0
rng1 = default_rng(seed)
rng2 = default_rng(seed)
rng1.random() # rng1だけを1回余分に呼び出す
print(rng1.random())
print(rng2.random())

# 0.2697867137638703
# 0.6369616873214543
  • 呼び出しの順序が違う場合
seed = 0
rng = default_rng(seed)
print(rng.random())
print(rng.choice(10, 2))

# 0.6369616873214543
# [5 2]
seed = 0
rng = default_rng(seed)
print(rng.choice(10, 2))
print(rng.random())

# [8 6]
# 0.2697867137638703

そのため、複数の実験条件を比較する場合には、1つの Generator (rng) を使いまわすのではなく、「実験単位」や「用途単位」で Generator を分けて管理すると、実行順序などに依存しない再現性のある比較がしやすくなります。

SeedSequence を使った実験管理

numpyの公式ドキュメント では、複数の実験間で再現性のある疑似乱数を生成するために SeedSequence を使った運用が紹介されています。SeedSequence を使うことで、例えば「実験IDに対応する値を親 seed として扱い、そこから用途別の子 Generator を作る」といった実験管理が可能になります。

SeedSequence は Generator を作るための「seed の設計図」のようなものであり、default_rng() に渡して初めて Generator となります。
SeedSequence に対して entropy と呼ばれる seed のようなものを与えることでインスタンス化します。

例えば、ある実験を seed (= 実験ID) を変えて繰り返し行う場合は以下のようになります。

from numpy.random import SeedSequence, default_rng
for seed in range(1000):
  ss = SeedSequence(seed)
  prob1_ss, prob2_ss, prob3_ss = ss.spawn(3)
  prob1_rng = default_rng(prob1_ss)
  prob2_rng = default_rng(prob2_ss)
  prob3_rng = default_rng(prob3_ss)

ss.spawn(3) によって、親の SeedSequence から 子の SeedSequence を3つ作成しています。子の SeedSequence は、同じ親 seed から生まれていますが、それぞれ用途別の Generator を作るために使うことができます。

最初の seed = 0 では、以下のイメージのようになり、

seed = 0
   ↓
SeedSequence(seed = 0)
   ↓ spawn(3) (目的別に seed 設計図を作る)
   |
   ├── prob1_ss  → prob1_rng(seed 設計図を default_rng() に渡して Generator をつくる)
   ├── prob2_ss  → prob2_rng
   └── prob3_ss  → prob3_rng

次の seed = 1 では、seed = 0 の時とは別の Generator セットが作られます。

seed = 1
   ↓
SeedSequence(seed = 1)
   ↓ spawn(3)
   |
   ├── prob1_ss  → prob1_rng(seed = 0 の時の prob1_rng とは異なる)
   ├── prob2_ss  → prob2_rng
   └── prob3_ss  → prob3_rng

このとき、3つの Generator は同じ親 seed 由来ですが、それぞれ異なる内部状態を持ちます。つまり、 実験管理用の seed を固定しつつ、内部状態の独立性が高い Generator を用途別に複数作ることができる、ということです。

なお、利便性を考慮し、直接 SeedSequence を使わずに 親 Generator から直接 spawn() することも可能です。

parant_rng = default_rng(seed)
prob1_rng, prob2_rng, prob3_rng = parent_rng.spawn(3)

今回は以上になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?