Псевдослучайные числа и зачем нужен seed

Компьютер — машина строго детерминированная: одни и те же входные данные всегда дают один и тот же результат. Откуда же тогда берутся «случайные» числа для броска кубика в игре или для моделирования очереди в банке? Ответ парадоксален: их вычисляют по жёсткой формуле. Такая случайность называется псевдослучайной, и именно она — рабочая лошадка любого имитационного моделирования.

Генератор псевдослучайных чисел (ГПСЧ) — детерминированный алгоритм, который по начальному значению (seed) порождает последовательность чисел, статистически неотличимую от случайной, хотя на самом деле полностью предсказуемую.

Приставка «псевдо» здесь — не недостаток, а честное предупреждение. Настоящая случайность приходит из физического мира: радиоактивный распад, тепловой шум, дрожание электронов. Обычная программа такого источника не имеет, поэтому она имитирует случайность арифметикой. Удивительно, но для подавляющего большинства задач — игр, симуляций, статистических экспериментов — этой имитации более чем достаточно.

Зачем вообще управлять случайностью

Может показаться: если нам нужна случайность, то чем меньше мы её контролируем, тем лучше. На практике всё наоборот. Способность повторить ровно ту же «случайную» последовательность — одна из самых ценных возможностей в моделировании. Представьте, что вы запустили симуляцию работы склада, и на 9473-м шаге программа упала с ошибкой. Если случайность была настоящей и неповторимой, воспроизвести баг невозможно — он растворился. Если же мы зафиксировали seed, то запускаем прогон заново и получаем точно тот же сценарий вплоть до последнего числа.

Управляемая случайность нужна как минимум для трёх вещей:

  • Воспроизводимость. Коллега, запустивший ваш код с тем же seed, увидит ровно те же результаты. Наука и инженерия держатся на повторяемости экспериментов.
  • Отладка. Ошибку, проявляющуюся «иногда», можно поймать и изучать снова и снова, пока seed зафиксирован.
  • Честное сравнение сценариев. Хотите узнать, какая из двух стратегий обслуживания очереди лучше? Прогоните обе на одной и той же последовательности случайных событий — тогда разница в результатах вызвана стратегией, а не разной удачей.

Линейный конгруэнтный генератор: случайность из одной строки

Чтобы понять, как из формулы рождается похожее на хаос поведение, разберём простейший ГПСЧ — линейный конгруэнтный генератор (LCG). Вся его суть в одной рекуррентной формуле: x[n+1] = (a * x[n] + c) mod m. Здесь x — текущее состояние генератора, а a, c, m — фиксированные константы (множитель, приращение и модуль). Каждый новый член получается из предыдущего: умножаем на a, прибавляем c и берём остаток от деления на m. Операция «mod m» удерживает число в диапазоне от 0 до m−1 и заодно создаёт ту самую кажущуюся непредсказуемость: старшие биты постоянно «перемешиваются» и отбрасываются.

def lcg(seed, n):
    a, c, m = 1664525, 1013904223, 2**32
    x = seed
    out = []
    for _ in range(n):
        x = (a * x + c) % m
        out.append(x)
    return out

xs = lcg(42, 5)
print("Целые значения:", xs)
print("Нормированные [0,1):", [round(x / 2**32, 6) for x in xs])

Вывод:

Целые значения: [1083814273, 378494188, 2479403867, 955863294, 1613448261]
Нормированные [0,1): [0.252345, 0.088125, 0.577281, 0.222554, 0.37566]

Обратите внимание на две вещи. Во-первых, числа выглядят беспорядочно — закономерность в этом ряду на глаз не разглядеть, хотя она строго есть. Во-вторых, мы получили сырые целые в диапазоне до 2**32, а затем поделили их на m, чтобы перейти к привычным дробям из полуинтервала [0, 1). Этот приём нормировки — стандартный мост от «больших целых внутри генератора» к «числам, с которыми удобно работать».

Период: почему последовательность рано или поздно зациклится

Раз состояние x всегда лежит в диапазоне от 0 до m−1, различных состояний всего m штук. Как только генератор повторно попадёт в уже встречавшееся состояние, вся последующая последовательность начнёт в точности повторяться — ведь следующий шаг зависит только от текущего значения.

Период — длина последовательности, после которой ГПСЧ начинает повторять сам себя. Для LCG период не превышает m; при удачном выборе констант он равен m.

В нашем примере m = 2**32 ≈ 4 миллиарда, и при таких константах период как раз максимален. Для учебных задач этого хватает, но для серьёзной статистики LCG слаб: его точки, если разложить их в многомерном пространстве, ложатся на параллельные плоскости, выдавая скрытую структуру.

Seed на практике: один и тот же ключ — один и тот же поток

Встроенный в Python генератор куда совершеннее LCG (под капотом — алгоритм Mersenne Twister с гигантским периодом), но принцип ровно тот же: вся последовательность однозначно определяется начальным seed. Зафиксировав seed, мы получаем абсолютно повторяемый поток «случайных» чисел.

import random
random.seed(123)
a = [random.random() for _ in range(3)]
random.seed(123)
b = [random.random() for _ in range(3)]
print("Два прогона с одним seed дают одно и то же:", a == b)

Вывод:

Два прогона с одним seed дают одно и то же: True

Мы дважды «перемотали» генератор на одну и ту же точку отсчёта вызовом random.seed(123) — и оба раза получили идентичные тройки чисел. Если убрать второй seed, поток продолжится с того места, где остановился, и списки разойдутся. Именно так устроена воспроизводимость в реальных симуляциях: вызвали seed в начале — получили детерминированный эксперимент.

Как работает под капотом

Любой ГПСЧ — это конечный автомат с внутренним состоянием. У LCG состояние — единственное целое число x. На каждом шаге автомат применяет к состоянию детерминированную функцию перехода (для LCG это (a*x + c) mod m) и выдаёт наружу очередное число, попутно обновляя состояние. Никакой «настоящей» случайности внутри нет — есть только арифметика над текущим состоянием.

Команда random.seed(s) делает ровно одно: устанавливает внутреннее состояние генератора в значение, однозначно вычисляемое из s. Поскольку дальше всё детерминировано, одинаковый seed гарантирует одинаковую последовательность. У Mersenne Twister состояние — это большой массив чисел, и переходы между ними устроены хитрее, чем у LCG, но логика «состояние → функция перехода → выход → новое состояние» абсолютно та же. Когда seed не задают явно, Python берёт его из системного источника энтропии (например, из текущего времени и шума ОС) — поэтому без фиксации seed каждый запуск выглядит «по-новому случайным».

Качество генератора целиком определяется выбором функции перехода. Для LCG неудачные a, c, m дают короткий период и заметные закономерности; знаменитый «плохой» генератор RANDU из 1960-х породил целое поколение испорченных научных расчётов именно из-за этого. Хорошие константы (как в нашем примере, взятые из классического справочника Numerical Recipes) дают максимальный период и приличное распределение для учебных целей.

Частые ошибки

  • Считать seed «уровнем случайности». Seed — не настройка «насколько всё случайно», а просто стартовая точка. seed(0) ничуть не «менее случаен», чем seed(999); меняется лишь конкретная последовательность.
  • Вызывать random.seed() внутри цикла перед каждым числом. Тогда генератор каждый раз стартует с одного места и выдаёт одно и то же значение — случайности не остаётся вовсе. Seed ставят один раз в начале.
  • Ожидать настоящей случайности от ГПСЧ для криптографии. Предсказуемость, полезная в моделировании, фатальна для безопасности: зная несколько выходов LCG, можно восстановить состояние и предсказать всё. Для ключей и паролей нужен secrets, а не random.
  • Путать «выглядит случайно» и «статистически качественно». На глаз почти любой ряд кажется хаотичным. Реальное качество (равномерность, отсутствие корреляций, длина периода) проверяют статистическими тестами, а не визуально.
  • Брать слишком короткий период. Если симуляции нужно больше чисел, чем длина периода, последовательность начнёт повторяться, и результаты исказятся скрытой периодичностью.

Итоги

  • ГПСЧ — детерминированный алгоритм: по seed он выдаёт фиксированную, заранее предопределённую последовательность, лишь имитирующую случайность.
  • Seed нужен для воспроизводимости, отладки и честного сравнения сценариев на одинаковой случайности — это управляемость, а не «уровень хаоса».
  • LCG (x = (a*x + c) mod m) — простейший ГПСЧ; его период не превышает m и сильно зависит от выбора констант.
  • Встроенный random использует Mersenne Twister — он мощнее LCG, но работает по тому же принципу «состояние → функция перехода → выход».
  • Для криптографии псевдослучайность не годится — там нужен secrets с настоящей энтропией.
Проверьте себя
1. Почему случайность, которую выдаёт обычный ГПСЧ, называют «псевдослучайной»?
AПотому что числа на самом деле вычисляются детерминированным алгоритмом и полностью предсказуемы при известном seed
BПотому что генератор иногда ошибается и выдаёт неслучайные числа
CПотому что эти числа нельзя нормировать в диапазон [0, 1)
DПотому что они берутся из физического шума и потому ненадёжны
2. Что произойдёт, если вызвать random.seed(123) дважды подряд — перед первым и перед вторым набором вызовов random.random()?
AВторой набор чисел будет ещё «случайнее» первого
BОба набора чисел окажутся в точности одинаковыми
CПрограмма завершится с ошибкой повторной инициализации
DЧисла второго набора станут больше по величине
3. Чему равен максимально возможный период линейного конгруэнтного генератора x = (a*x + c) mod m?
AБесконечности — LCG никогда не повторяется
BЗначению a * c
CМодулю m, поскольку всего различных состояний ровно m
DУдвоенному значению seed
4. Почему генератор random из стандартной библиотеки нельзя использовать для генерации криптографических ключей?
AУ него слишком короткий период для длинных ключей
BОн выдаёт только числа из диапазона [0, 1) и не умеет давать целые
CЕго выход предсказуем: зная несколько значений, можно восстановить состояние и вычислить остальные
DОн работает слишком медленно для криптографии