結論:親のseedから子の系列を作り、条件との対応を固定する
複数条件で乱数実験を行うなら、SeedSequence(seed).spawn(n)で子の系列を作り、条件ごとに生成器を割り当てる。実験全体を同じseedでやり直せる一方、各条件が同じ乱数列を使い回す失敗を避けやすい。ここではアルゴリズムも明示するためGenerator(PCG64(child))を使う。生成器をループのたびに同じseedで作り直さないことがポイントである。
乱数の再現と系列の分割は別の要件である。全条件でseedを同じ値へ戻せば再現性は得られるが、条件ごとの乱数まで同一になる。反対にseedを記録せず毎回生成器を作れば、後から同じ試行を追いにくい。親のseed、分割順、条件名、生成アルゴリズムを一組として管理すると、この二つを整理しやすくなる。
そのまま動かせる例
以下は対照・処置・検証という3条件へ子系列を割り当て、各条件で0以上1000未満の整数を5個作る。説明用の短い系列なので、大規模なモンテカルロ計算は行わない。NumPyが使える環境で例全体を実行でき、外部ファイルや通信は不要である。関数を同じseedで2回呼び、各条件の出力が一致することを確認する。
末尾のbadは、同じseedで生成器を作り直した場合の意図的な誤用例である。同一の配列になることを確認しており、独立な試行を増やす書き方として勧めているわけではない。なお、2本の短い出力が違ったことだけで統計的な独立性を証明できるわけではない。ここでの比較は、明らかな使い回しを検出するための確認である。
import numpy as np
from numpy.random import Generator, PCG64, SeedSequence
names = ["control", "treatment", "validation"]
def experiment(seed):
children = SeedSequence(seed).spawn(len(names))
result = {}
for name, child in zip(names, children):
rng = Generator(PCG64(child))
result[name] = rng.integers(0, 1000, size=5)
return result, [c.spawn_key for c in children]
first, keys = experiment(20261002)
second, _ = experiment(20261002)
for name in names:
np.testing.assert_array_equal(first[name], second[name])
print(name, first[name].tolist())
assert not np.array_equal(first[names[0]], first[names[1]])
print("spawn keys:", keys)
print("repeated run matches: True")
# Expected misuse: resetting the same seed repeats the same stream.
bad = [Generator(PCG64(123)).integers(0, 1000, size=5) for _ in range(2)]
assert np.array_equal(bad[0], bad[1])
print("same seed reset repeats data:", bool(np.array_equal(bad[0], bad[1])))
実行結果
control [921, 105, 968, 635, 419]
treatment [936, 897, 203, 792, 342]
validation [821, 363, 239, 246, 799]
spawn keys: [(0,), (1,), (2,)]
repeated run matches: True
same seed reset repeats data: True
spawn_keyと順序が意味するもの
出力のspawn keyは親から見た子の位置を示す。最初の子をcontrol、次をtreatmentへ割り当てたので、再実行でも同じ対応を使う。名前の一覧を並べ替えると、名前と系列の対応は変わる。処理が終了した順番で生成器を配るより、実行前に条件IDと系列を対応付けておく方が、並行処理へ変更した際にも追いやすい。
同じSeedSequenceオブジェクトにspawnを繰り返すと、新しい子が追加される。最初と同じ子を作り直したい場合は、元のseedと同じ分割手順から再構成する。実験途中のオブジェクトを適当に使い回すと「同じseedなのに違う」という混乱につながる。生成器が今どこまで乱数を消費したかも状態の一部なので、関数へ明示的に渡して扱うとよい。
再現できる条件を狭く正確に記録する
同じseedだけで、任意のNumPy版・環境・呼び出し方法にわたる全結果の一致を保証できるわけではない。アルゴリズム、NumPyの版、使った分布、引数、乱数を消費する順序などをそろえる必要がある。途中でデバッグ用に乱数を一つ引いたり、1回の生成サイズを変えたりすると、その後の系列がずれることもある。結果の再現条件を「seed固定」だけで済ませないようにする。
条件ごとの乱数消費量が違っても、別の子生成器なら他条件の消費位置へ直接影響しにくい。長い実験では、試料IDや反復IDなど安定した単位へ割り当てる設計が便利である。ただし条件IDの作り方や分割の階層も保存しよう。Pythonの組み込みhashの値など、実行ごとに変わり得る識別値を無検討にseedへ使うことは避けたい。
独立な試行と共通乱数を区別する
比較実験では、意図的に共通の乱数を使って条件間の差を評価する設計もある。その場合は同じ系列を使うこと自体が誤りとは限らない。大切なのは、独立な反復を増やしたつもりなのに同一の試行を繰り返していないかである。実験計画に沿って、共有する乱数と分ける乱数を先に決め、図や表にもその前提を記す。
SeedSequenceの分割は独立性を高い確率で得るための仕組みであり、暗号用途の乱数生成を目的とするものではない。また、並行処理にしただけで統計精度や再現性が自動的に向上するわけでもない。各仕事の結果を元の条件へ戻して集計し、失敗や再試行の際にどの系列を使ったかまで残すと、後から再解析しやすくなる。
確認環境と参考資料
例はLinux・CPython 3.12.14・NumPy 2.3.5で実行した。掲載した出力はこの環境での結果である。公式資料のstable版や最新版は更新されるため、手元のバージョンと対応する仕様も確認してほしい。
- https://numpy.org/doc/stable/reference/random/parallel.html(2026年10月2日参照)
- https://numpy.org/doc/stable/reference/random/compatibility.html(2026年10月2日参照)
関連項目:ProcessPoolExecutorが動かないとき:mainガード・pickle・起動方式 / 解析をやり直せる実行記録を残す:条件・入力ハッシュ・環境情報
