できない.dev

乱数のシードを固定して同じ結果を再現するには

乱数のシードを固定して、実行のたびに同じ数列を得る書き方を各言語で示す。
同じシード 42 でも言語ごとに数列が違う点や、シードを渡せない乱数の落とし穴もまとめる。

公開:

選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。

Python

import random
 
random.seed(42)
first = [random.randint(1, 100) for _ in range(5)]
 
random.seed(42)
second = [random.randint(1, 100) for _ in range(5)]
 
print(first)          # [82, 15, 4, 95, 36]
print(first == second)  # True
 
# 専用の乱数生成器にすると、他のコードの random 呼び出しに影響されない
rng = random.Random(42)
print([rng.randint(1, 100) for _ in range(5)])  # [82, 15, 4, 95, 36]

乱数を作る前に random.seed(42) を呼ぶと、そのあとの数列が毎回同じになる。
グローバルな状態に頼ると他の処理の乱数呼び出しに影響されるので、再現したい処理には random.Random(42) で専用の生成器を作って使う。

JavaScript

// Math.random() にはシードを渡せない。シード付きの生成器を自分で用意する(mulberry32)
function mulberry32(seed) {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
 
const randint = (rand, min, max) => min + Math.floor(rand() * (max - min + 1));
 
const rand1 = mulberry32(42);
const first = Array.from({ length: 5 }, () => randint(rand1, 1, 100));
 
const rand2 = mulberry32(42);
const second = Array.from({ length: 5 }, () => randint(rand2, 1, 100));
 
console.log(first); // [ 61, 45, 86, 67, 18 ]
console.log(JSON.stringify(first) === JSON.stringify(second)); // true

Math.random() にはシードを渡す手段がないので、シード付きの生成器を自分で用意する。
ここでは数行で書ける mulberry32 を使い、0 以上 1 未満の値を返す関数として Math.random() の代わりに差し込む。
暗号用途には使えない。

TypeScript

type Rand = () => number;
 
// Math.random() にはシードを渡せない。シード付きの生成器を自分で用意する(mulberry32)
function mulberry32(seed: number): Rand {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
 
const randint = (rand: Rand, min: number, max: number): number =>
  min + Math.floor(rand() * (max - min + 1));
 
const rand1 = mulberry32(42);
const first: number[] = Array.from({ length: 5 }, () => randint(rand1, 1, 100));
 
const rand2 = mulberry32(42);
const second: number[] = Array.from({ length: 5 }, () => randint(rand2, 1, 100));
 
console.log(first); // [ 61, 45, 86, 67, 18 ]
console.log(first.join() === second.join()); // true

生成器を () => number という型にしておくと、Math.random もシード付きの生成器も同じ引数で受け取れる。
本番では Math.random を、テストでは mulberry32(42) を渡す使い分けができる。

Go

package main
 
import (
	"fmt"
	"math/rand/v2"
)
 
func draw(seed uint64) []int {
	rng := rand.New(rand.NewPCG(seed, 0))
	out := make([]int, 5)
	for i := range out {
		out[i] = rng.IntN(100) + 1 // 1〜100
	}
	return out
}
 
func main() {
	first := draw(42)
	second := draw(42)
	fmt.Println(first)                                   // [86 97 14 9 19]
	fmt.Println(fmt.Sprint(first) == fmt.Sprint(second)) // true
}

math/rand/v2 では rand.New(rand.NewPCG(seed1, seed2)) でシード付きの生成器を作る。
トップレベルの rand.IntN などはシードを指定できないため、再現したい処理では生成器を自分で持って関数に渡す。

Rust

// Cargo.toml: rand = "0.10"
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
 
fn draw(seed: u64) -> Vec<u32> {
    let mut rng = StdRng::seed_from_u64(seed);
    (0..5).map(|_| rng.random_range(1..=100)).collect()
}
 
fn main() {
    let first = draw(42);
    let second = draw(42);
    println!("{:?}", first); // [14, 53, 25, 55, 87]
    println!("{}", first == second); // true
}

標準ライブラリには乱数がないので rand クレートを使う。
StdRng::seed_from_u64 でシードから生成器を作り、random_range で範囲を指定して値を取り出す。

できないときRust「failed to resolve: use of undeclared crate or module」の原因と直し方

Java

import java.util.Arrays;
import java.util.Random;
import java.util.stream.IntStream;
 
public class Main {
    static int[] draw(long seed) {
        Random rng = new Random(seed);
        return IntStream.range(0, 5).map(i -> rng.nextInt(1, 101)).toArray(); // 1〜100
    }
 
    public static void main(String[] args) {
        int[] first = draw(42);
        int[] second = draw(42);
        System.out.println(Arrays.toString(first)); // [31, 64, 49, 85, 71]
        System.out.println(Arrays.equals(first, second)); // true
    }
}

new Random(seed) で同じシードから同じ数列が得られる。
nextInt(1, 101) のように下限と上限を渡せるのは Java 17 以降で、上限は含まれない。

C#

using System;
using System.Linq;
 
static int[] Draw(int seed)
{
    var rng = new Random(seed);
    return Enumerable.Range(0, 5).Select(_ => rng.Next(1, 101)).ToArray(); // 1〜100
}
 
var first = Draw(42);
var second = Draw(42);
Console.WriteLine($"[{string.Join(", ", first)}]"); // [67, 15, 13, 53, 17]
Console.WriteLine(first.SequenceEqual(second)); // True

new Random(seed) のようにシードを渡して生成器を作る。
Next(1, 101) の上限 101 は含まれないので、1〜100 の値になる。

同じシード 42 でも言語が違えば数列は違う

各言語で、シード 42 から 1〜100 の整数を 5 個取り出して比べた。

  • Python は 82, 15, 4, 95, 36 だった。
  • JavaScript と TypeScript(mulberry32)は 61, 45, 86, 67, 18 だった。
  • Go(PCG)は 86, 97, 14, 9, 19 だった。
  • Rust(StdRng、rand 0.10.3)は 14, 53, 25, 55, 87 だった。
  • Java は 31, 64, 49, 85, 71 だった。
  • C# は 67, 15, 13, 53, 17 だった。

再現できるのは同じ言語・同じ実装の中だけである。
Python で作ったテストデータを別の言語の生成器で再現しようとしても、数列は一致しない。

シードを渡せない乱数がある

手近な乱数関数がシードに対応しているとは限らない。

  • JavaScript の Math.random() にはシードを渡す手段がない。自作するか、ライブラリのシード付き生成器を使う。
  • Go の math/rand/v2 には rand.Seed がなく、rand.Seed(42) と書くと undefined: rand.Seed でコンパイルエラーになる(Go 1.27.3 で確認)。rand.New で生成器を作る。
  • Java の ThreadLocalRandom.current().setSeed(42) は UnsupportedOperationException を投げる(Java 21 で確認)。new Random(42) を使う。
  • C# の Random.Shared はシードを受け付けない型(実行時の型名は ThreadSafeRandom)だった。new Random(42) を使う。

速くて手軽な乱数ほどシード固定に向かない作りになっている。
再現が要る処理では、最初から生成器を引数で受け取る形にしておくとよい。

Python のグローバルな乱数は余分な呼び出しで数列がずれる

random.seed(42) のあとに別の場所で random.random() を 1 回呼ぶと、続く randint の結果が変わった。

  • 余分な呼び出しなしでは 82, 15, 4, 95, 36 になる。
  • 1 回呼んだあとは 4, 95, 36, 32, 29 になり、元の数列の 3 番目から始まっていた。

random モジュールの生成器は 1 つを全体で共有している。
ライブラリや別のスレッドが乱数を使うだけで、テストの再現性が崩れる。
再現したい処理には random.Random(42) で専用の生成器を作る。

再現できる範囲と使ってはいけない場面

シードを固定しても、同じ数列が得られる範囲には限りがある。

  • 生成アルゴリズムをドキュメントで保証していない型は、ライブラリの版が上がると数列が変わりうる。Rust の StdRng や .NET の Random がその例である。版をまたいで固定したいときは、アルゴリズムを名指しできる生成器(Rust なら rand_chacha の ChaCha8Rng など)を使う。
  • 固定したシードの乱数は予測できる。パスワード、トークン、抽選の本番には使わず、OS の乱数を使う関数を選ぶ。

用途はテストの再現、シミュレーションの比較、機械学習の実験の条件をそろえることなどに限るのがよい。