ML Foundations
What a NumPy random seed does and does not reproduce
A seed restarts a NumPy generator, but does not freeze its state or promise identical streams across NumPy versions.
Evidence and testing
Review record: Tensor Almanac automated evidence gate (no independent human review). Example versions, recorded outputs and limitations appear below.
A seed restarts a fresh generator
`numpy.random.default_rng(seed)` creates a `Generator` from the supplied seed. The NumPy guide demonstrates that a fresh generator initialized with the same seed reproduces the same example array after an interpreter restart. This is useful when you need to replay a small sampling step while debugging. [1]
Use a dedicated generator at the point where randomness enters the code, then pass that object to the functions that need it. The practical benefit is visible ownership: a function using its own generator is easier to reproduce than one that silently draws from shared global random state. The ownership recommendation is an engineering inference from `default_rng` constructing a generator rather than managing a global instance. [1]
References: [1]
The generator's state advances as you draw
A seed initializes the generator; it does not reset the stream before every call. After one draw, the generator is in a later state. Two runs that start from the same seed can therefore diverge if one path consumes an extra value, changes draw order, or requests samples differently. NumPy documents `Generator` as a stateful object backed by a `BitGenerator`; the divergence example below demonstrates the practical consequence for a fixed sequence of integer draws. [1]
When a debug replay stops matching, compare the first call site that consumes random values, not only the seed literal. Logging the NumPy version, generator type, seed, and a concise record of sampling call order makes a mismatch easier to investigate. This logging advice is an engineering practice inferred from the documented stateful generator and version policy; it does not make a run fully reproducible by itself. [1, 2]
Executed with Python and NumPy in the available code environment; captured output and environment are recorded in the private research evidence.
import numpy as np
seed = 20261022
first = np.random.default_rng(seed)
restarted = np.random.default_rng(seed)
partly_consumed = np.random.default_rng(seed)
baseline = first.integers(0, 100, size=5)
repeated = restarted.integers(0, 100, size=5)
discarded = partly_consumed.integers(0, 100)
after_extra_draw = partly_consumed.integers(0, 100, size=5)
print(f"numpy version: {np.version.version}")
print("bit generator: PCG64 (NumPy default)")
print(f"seed: {seed}")
print(f"fresh generators match: {np.array_equal(baseline, repeated)}")
print(f"first sequence: {baseline.tolist()}")
print(f"extra value consumed: {discarded}")
print(f"sequence after extra draw: {after_extra_draw.tolist()}")
Recorded output
numpy version: 2.3.5 bit generator: PCG64 (NumPy default) seed: 20261022 fresh generators match: True first sequence: [13, 14, 42, 0, 69] extra value consumed: 13 sequence after extra draw: [14, 42, 0, 69, 16]
References: [1]
A seed is not a cross-version stream contract
NumPy explicitly says `Generator` does not provide a version compatibility guarantee and that its bit stream may change as algorithms evolve. Saving only an integer seed is therefore insufficient if a future environment must recreate the exact same generated values. [1]
For a replayable experiment, preserve the seed and the software environment that produced the run, then verify the relevant sample or downstream result after upgrades. If exact historical samples matter more than regenerating them, retaining the sampled data is a stronger record than assuming a seed will recreate it forever. These are operational recommendations inferred from NumPy's compatibility statement, not guarantees that version pinning alone controls every source of nondeterminism in a complete ML pipeline. [1, 2]