Генерация случайных чисел

в 15:01, , рубрики: arduino, ruvds_статьи, время отклика, генерация случайных чисел, микроконтроллеры, тепловой шум, фотонный шум

Генерация случайных чисел - 1

Картинка Benzoix, Freepik

Зачем вообще нужны случайные числа? Дело в том, что случайные числа не представляют собой оторванную от жизни абстракцию, а широко применяются во множестве областей, начиная от научных исследований и заканчивая технологиями, окружающими нас.

Рассмотрим наиболее известные применения случайных чисел и способы их получения как программным способом, так и комбинированным (программно-физическим).

Когда говорят о случайных числах, можно назвать следующие сферы, где они нашли своё применение:

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

Компьютерные игры и симуляция: в компьютерных играх есть множество применений для этих чисел, так как требуется обеспечить соответствующую непредсказуемость, например, выпадение лута, поведение персонажей, анимация спецэффектов; кроме этого, они применяются также и в научных исследованиях, например, для моделирования потоков жидкостей, где может происходить расчёт траектории движения даже отдельных молекул жидкости.

Лотереи и азартные игры: здесь случайные числа служат для цели обеспечения честности игры, гарантируя каждому участнику игры равные шансы на победу.

Статистика и машинное обучение: для осуществления выборок (в случае статистики), в то время как в области машинного обучения они используются для инициализации параметров моделей с целью увеличить производительность и предотвратить переобучение.

Тестирование программного обеспечения: здесь они могут использоваться для проверки работоспособности программ в нестандартных ситуациях или в условиях, которые сложно воспроизвести вручную.

Научные исследования: здесь они полезны для моделирования сложных процессов, например, таких как движение частиц, мутации и т. д.

Искусство и музыка: для генерации уникальных произведений.

Квантовые вычисления: для выполнения квантовых алгоритмов, например, алгоритма Шора, для факторизации больших чисел.

Блокчейн и смарт-контракты: для генерации уникальных идентификаторов, выбора валидаторов или выполнения смарт-контрактов.

Военные технологии: шифрование систем управления и передачи данных.

Существует множество и других сфер, где случайные числа могут найти своё применение, и полное перечисление их может занять существенное время.

Однако из этого краткого обзора выше нам должно быть понятно, что областей применения случайных чисел очень много, поэтому знать, как они получаются, весьма невредно.

Рассмотрим физические способы получения случайных чисел.

Генерация на базе физических эффектов

▍ Квантовый генератор случайных чисел на базе фотонного шума

Этот метод базируется на квантовой неопределённости, и для него достаточно только светодиода и фотодетектора для регистрации случайных фотонов.

Благодаря своей природе этот способ является одним из самых надёжных вариантов получения истинно случайных чисел, так как в отличие от псевдослучайных чисел, которые генерируются алгоритмом и могут быть предсказаны в случае известности начальных условий, подобный метод позволяет генерировать числа, которые невозможно предсказать, так как они проистекают из квантовых случайных процессов.

Так как этот способ достаточно простой и привлекательный, рассмотрим его подробнее.

Как было уже выше сказано, подобный квантовый генератор эксплуатирует фотонный шум, который возникает из-за случайного характера испускания и поглощения фотонов.

Таким образом, если использовать светодиод и фотоприёмник, то можно регистрировать случайные колебания интенсивности освещения, которые и будут источником случайных чисел.

Чтобы реализовать эту схему, достаточно взять любой светодиод, запитать его и направить на фотодетектор, подключённый, например, к аналоговому пину микроконтроллера (arduino, esp32).

Встроенный в микроконтроллер АЦП будет считывать этот сигнал и преобразовывать в случайные числа.

При этом для увеличения элемента случайности можно брать младшие биты этого сигнала, так как они более случайны, а для обработки сигнала можно использовать вот такую простую программу:

void setup() {
  Serial.begin(9600);
}

void loop() {
  int sensorValue = analogRead(A0); // Чтение сигнала с фотодетектора
  int randomBit = sensorValue & 1;  // Извлечение младшего бита
  Serial.println(randomBit);        // Вывод случайного бита
  delay(100);                       // Задержка для стабилизации
}

Для увеличения качества можно использовать алгоритмы постобработки, например, сжатие фон Неймана, что поможет устранить возможные смещения в данных.

Кстати говоря, мы выше упомянули, что младшие биты более случайны, но интересно, почему так получается?

Дело в том, что когда мы считываем аналоговый сигнал, то его значение колеблется из-за шума и это колебание как раз и отражается в большей степени в младших битах, где старшие биты определяют общий уровень сигнала и достаточно стабильны, в то время как младшие биты регистрируют даже мелкие изменения.

Например, если мы рассмотрим аналоговое значение 543, которое в двоичном представлении выглядит как 1000011111, то в нём наиболее подвержены колебаниям будут последние 4 бита: 1111.

▍ Считывание высокоимпедансного значения с пина микроконтроллера

Этот метод использует такое состояние пинов микроконтроллера, которое называется высокоимпедансным, то есть с высоким сопротивлением, и на таком пине возникают наводки от радиоволн, вызывая случайные колебания электрических шумов.

Для использования этого метода достаточно всего лишь аналоговый пин оставить не подключённым ни к чему или даже подключить специально к куску провода, который будет выступать в роли антенны (ещё более усиливая эффект).

Точно так же, как и в случае выше, стоит использовать более случайные младшие биты (см, тот же самый код выше).

▍ Генерация случайных чисел на основе шума АЦП

АЦП микроконтроллеров также подвержен внутренним шумам, и это можно использовать для генерации случайных чисел.

Для этого достаточно подключить аналоговый пин к заземлению через резистор (например 10 кОм) и считывать значения с него:

void setup() {
  Serial.begin(115200);
  analogReadResolution(12); // Установка разрешения АЦП (12 бит для ESP32)
}

void loop() {
  int randomValue = analogRead(A0); // Чтение шума АЦП
  int randomBit = randomValue & 1;  // Извлечение младшего бита
  Serial.println(randomBit);        // Вывод случайного бита
  delay(100);                       // Задержка для стабилизации
}

▍ Генерация случайных чисел на основе теплового шума

Тепловой шум электронных компонентов также может быть использован для дела генерации случайных чисел, только для этого потребуется собрать дополнительную схему.

Делается это так: например, резистор в 1 МОм подключается к операционному усилителю, в то время как выход операционного усилителя подключается к аналоговому пину микроконтроллера, после чего точно так же происходит считывание данных, где, как и в вариантах выше, желательно использовать младшие биты.

▍ Генерация случайных чисел на основе времени отклика

Для этого метода используется человеческий фактор, где время нажатия на кнопку будет всегда случайным.

То есть мы подключаем кнопку к цифровому пину микроконтроллера и измеряем время между нажатиями кнопки:

void setup() {
  Serial.begin(9600);
  pinMode(2, INPUT_PULLUP); // Кнопка подключена к пину 2
}

void loop() {
  if (digitalRead(2) == LOW) { // Если кнопка нажата
    unsigned long time = millis(); // Запись текущего времени
    int randomBit = time & 1;     // Извлечение младшего бита
    Serial.println(randomBit);    // Вывод случайного бита
    delay(200);                   // Задержка для предотвращения дребезга
  }
}

▍ Генерация случайных чисел на базе шума микрофона

Обычный шум микрофона также может служить источником случайных чисел, если подключить его к микроконтроллеру, также к аналоговому пину (см. первый код выше).

▍ Генерация случайных чисел за счёт шума в оперативной памяти

У некоторых микроконтроллеров, например у esp32, есть такая интересная функция, которая позволяет генерировать случайные числа за счёт шума в оперативной памяти, для чего служит встроенная функция esp_random():

void setup() {
  Serial.begin(115200);
}

void loop() {
  uint32_t randomValue = esp_random(); // Генерация случайного числа
  int randomBit = randomValue & 1;     // Извлечение младшего бита
  Serial.println(randomBit);           // Вывод случайного бита
  delay(100);                          // Задержка для стабилизации
}

Шум оперативной памяти возникает в виде случайных колебаний данных, причиной чего являются физические процессы, такие как тепловые эффекты (нагрев элементов и, соответственно, изменение заряда ячеек памяти), электромагнитные помехи (от окружающих компонентов), квантовые явления (туннелирование электронов), колебания питания.

В любом случае, эти случайные числа являются истинно случайными, так как базируются на физических случайных процессах.

Последствия этих шумовых явлений можно отловить и использовать в качестве средства генерации случайных чисел, так как они не только влияют на заряд ячеек, но и несколько меняют время доступа к ним, что тоже может быть полезным образом использовано.

Подытоживая этот раздел, можно заметить, что многие физические явления могут быть использованы для генерации истинно случайных чисел, где среди них попадаются даже забавные варианты: например, в офисе американского сервиса Cloudflare установили стену из лава-ламп, на которую смотрит камера и делает фотографии. Любые изменения в форме и размерах пузырей в лампах влияют на то, что каждый раз сохраняется новая случайная последовательность бинарных чисел, что потом используется компанией для генерации ключей шифрования в их системах безопасности.

Говоря об использовании физических эффектов в профессиональных системах, можно назвать и ещё один способ, который иногда используется: квантовое туннелирование, то есть преодоление частицей энергетического барьера, который она обычно не может преодолеть — и этот процесс является случайным.

Как это делается: туннельный диод подключается к аналоговому пину микроконтроллера и считываются случайные колебания тока.

Программные методы генерации случайных чисел

Помимо физических методов, есть и программные методы, рассмотрим некоторые из них.

Начнём с известной платформы Arduino, где есть функция random(), задачей которой является генерация псевдослучайных чисел с помощью детерминированного алгоритма.

На платформе Arduino используется конгруэнтный генератор (Linear Congruential Generator) для генерации псевдослучайных чисел, представляющий собой один из самых простых и распространённых алгоритмов генерации.

Вычисление каждого нового числа с помощью этого алгоритма производится по формуле:

Xₙ₊₁ = (a * Xₙ + c) % m

Где:
Xₙ — текущее число (или начальное значение, если это первый шаг);
Xₙ₊₁ — следующее число в последовательности;
a, c, m — константы, которые определяют качество генератора.

Алгоритм начинает работу с начального значения X₀, которое задаётся с помощью функции randomseed(seed).

Функция генерирует длинную последовательность чисел, которая всегда одна и та же, однако точка, с которой начинается генерация, зависит от передаваемого параметра seed.

Если же seed не задан, то будет генерироваться всегда одна и та же последовательность чисел при каждом запуске.

Чтобы последовательность получилась более случайной, в официальной документации приводят пример со считыванием значения с аналогового неподключённого пина.

Как можно видеть по формуле выше, на каждом новом шаге алгоритм берёт текущее число, умножает его на константу а, после чего прибавляет константу с, а затем берёт остаток от деления на m. Результатом является новое число Xₙ₊₁.

Далее процесс повторяется.

Как мы выше уже выяснили, в esp32 для генерации случайных чисел используется физический шум, поэтому мы её здесь не рассматриваем.

Если же говорить о генерации случайных чисел в C++ в целом, то там для этого служит библиотека random, которая предоставляет достаточно гибкие инструменты для работы со случайными числами, где задействуется так называемый алгоритм Mersenne Twister, который и обеспечивает высококачественные псевдослучайные числа.

Алгоритм был разработан в 1997 году японскими учёными Макото Мацутомо и Такудзи Нисимурой и получил своё название из-за использования простых чисел Мерсенна, под которыми понимаются числа вида $2^{n}-1$.

Алгоритм отличается быстрой работой, имеет огромный период ($2^{199937}-1$), — то есть это число с более чем 6000 цифр, а результаты его работы практически неотличимы от истинно случайных, благодаря чему он проходит множество статистических тестов на случайность.

Работа алгоритма заключается в том, что он заполняет массив из 624 чисел, которые могут быть заданы вручную или сгенерированы на основе начального значения.

На каждом своём шаге работы алгоритм берёт этот заполненный массив, применяет к нему математические функции (сдвиги, XOR и т.д.) и выдаёт новое случайное число.

После генерации очередного числа состояние массива обновляется, чтобы подготовиться к следующему шагу.

Результатом работы алгоритма являются высококачественные случайные числа, что делает его подходящим для научных расчётов и моделирования.

Кроме того, алгоритм позволяет генерировать числа в разных диапазонах и с разной точностью.

Благодаря своим свойствам, этот алгоритм реализован во многих языках программирования, в том числе C++, Python, Java.

Пример использования его в языке C++ приведён ниже:

#include <iostream>
#include <random> // Библиотека для работы со случайными числами

int main() {
    std::random_device rd;  // Источник энтропии для инициализации
    std::mt19937 gen(rd()); // Генератор Mersenne Twister
    std::uniform_int_distribution<> distrib(0, 99); // Распределение от 0 до 99

    for (int i = 0; i < 10; ++i) {
        int randomValue = distrib(gen); // Генерация случайного числа
        std::cout << randomValue << std::endl; // Вывод числа
    }

    return 0;
}

Если же говорить о Java, как ещё одном С-подобном языке, то там для генерации случайных чисел используется два класса: java.util.Random и java.security.SecureRandom, где класс Random использует примерно такой же генератор случайных чисел, как и в Arduino, однако он не годится для целей криптографии или научных расчётов.

Почему «примерно» — так как несмотря на похожесть, есть и различия:

  • Arduino использует 32-битное состояние* (примерно 2 миллиарда чисел), тогда как Java использует 48-битное состояние (примерно 281 триллион чисел), что увеличивает период последовательности;
  • У Arduino начальное значение (seed) можно задать с помощью randomSeed(). Если seed не задан, используется фиксированное значение. У Java начальное значение задаётся при создании объекта Random. Если seed не задан, используется текущее время в наносекундах.

*Несмотря на то, что классическая Arduino представляет собой 8-битный микроконтроллер, функция random() в Arduino реализована на уровне программного обеспечения и использует 32-битные вычисления. Это позволяет ей генерировать числа с периодом $2^{31}$, что значительно больше, чем можно было бы ожидать от 8-битного микроконтроллера.

В результате Java имеет гораздо больший период и лучшее качество случайности.

Пример кода с его использованием выглядит так:

import java.util.Random;

public class RandomExample {
    public static void main(String[] args) {
        Random rand = new Random(); // Создание объекта Random
        for (int i = 0; i < 10; i++) {
            int randomValue = rand.nextInt(100); // Генерация числа от 0 до 99
            System.out.println(randomValue);     // Вывод числа
        }
    }
}

В то время как второй класс (SecureRandom) использует более сложные алгоритмы (SHA1PRNG или DRBG) и может быть применён для целей криптографии.

В качестве источника случайности этот класс использует энтропию операционной системы — шум RAM, движение мыши, нажатие клавиш.

Пример его использования выглядит так:

import java.security.SecureRandom;

public class SecureRandomExample {
    public static void main(String[] args) {
        SecureRandom secureRand = new SecureRandom(); // Создание объекта SecureRandom
        for (int i = 0; i < 10; i++) {
            int randomValue = secureRand.nextInt(100); // Генерация числа от 0 до 99
            System.out.println(randomValue);           // Вывод числа
        }
    }
}

Однако выше мы упоминали алгоритм Mersenne Twister, а в примерах для java его нет, так где же он?

Дело в том, что реализация этого алгоритма не встроена в стандартную библиотеку Java из-за своих лицензионных ограничений, однако он реализован в сторонних библиотеках, например, таких как Apache Commons Math.

Если для удобства попытаться обобщить всё вышесказанное и представить в виде таблицы среди некоторых других алгоритмов генерации, то получится примерно следующая картина (наверху таблицы — самые качественные алгоритмы, внизу — самые некачественные; жёлтым цветом выделены рассмотренные):

Генерация случайных чисел - 5

Итак, подытоживая весь рассказ выше, можно сказать, что существует достаточно большое количество способов генерации случайных чисел, однако истинно случайными числами всё-таки являются основанные на физических процессах, а программные, в зависимости от продвинутости алгоритма, могут только в некоторой степени приблизиться к этому показателю у первых.

А если кто-то захочет ещё больше погрузиться в тему случайных чисел, то для этого есть хороший ресурс Random.org.

© 2025 ООО «МТ ФИНАНС»

Автор: DAN_SEA

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js