Псевдослучайные числа и зачем нужен 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с настоящей энтропией.