Документация Engee

Случайные числа

Для генерирования случайных чисел в Julia по умолчанию используется алгоритм Xoshiro256++ с состоянием на уровне задачи (Task). Другие типы генераторов случайных чисел можно получить путем наследования от типа AbstractRNG, после чего их можно использовать для получения нескольких потоков случайных чисел.

Из пакета Random экспортируются следующие генераторы псевдослучайных чисел:

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

  • Xoshiro: генерирует высококачественный поток случайных чисел с небольшим вектором состояний; высокая производительность достигается за счет использования алгоритма Xoshiro256++.

  • RandomDevice: для энтропии, обеспечиваемой ОС. Может применяться для получения криптографически надежных случайных чисел (CS(P)RNG).

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

Большинство функций, связанных с генерированием случайных чисел, принимают в качестве первого аргумента необязательный объект AbstractRNG. Кроме того, некоторые из них принимают спецификации измерений dims... (которые также могут предоставляться в виде кортежа) для генерирования массивов случайных значений. В многопоточной программе для обеспечения потокобезопасности, как правило, следует использовать отдельные генераторы случайных чисел для разных потоков или задач. Однако генератор случайных чисел по умолчанию является потокобезопасным начиная с версии Julia 1.3 (до версии 1.6 использовался генератор случайных чисел на уровне потока, а далее — на уровне задачи).

Предоставляемые генераторы случайных чисел могут генерировать равномерные случайные числа следующих типов: Float16, Float32, Float64, BigFloat, Bool, Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, BigInt (или комплексные числа этих типов). Случайные числа с плавающей запятой генерируются равномерно в полуинтервале . Так как BigInt представляет неограниченные целые числа, необходимо указать интервал (например, rand(big.(1:6))).

Кроме того, для некоторых типов AbstractFloat и Complex реализованы нормальное и экспоненциальное распределения; подробные сведения см. в описании функций randn и randexp.

Для генерирования случайных чисел из других распределений можно воспользоваться пакетом Distributions.jl.

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

Модуль для генерирования случайных чисел

Random

Поддержка генерирования случайных чисел. Предоставляет rand, randn, AbstractRNG, MersenneTwister и RandomDevice.

Функции для генерирования случайных чисел

rand([rng=default_rng()], [S], [dims...])

Выбирает случайный элемент или массив случайных элементов из множества значений, переданных в аргументе S; S может быть:

  • индексируемой коллекцией (например, 1:9 или ('x', "y", :z));

  • объектом AbstractDict или AbstractSet;

  • строкой (представляемой как коллекция символов);

  • типом; в этом случае множество значений эквивалентно typemin(S):typemax(S) для целых чисел (неприменимо к BigInt), для чисел с плавающей запятой и для комплексных чисел с плавающей запятой.

По умолчанию S — это тип Float64. Если помимо необязательного аргумента rng передается только один аргумент, представляющий собой кортеж (Tuple), он интерпретируется как коллекция значений (S), а не как dims.

Совместимость: Julia 1.1

Для поддержки S в качестве кортежа требуется версия не ниже Julia 1.1.

Примеры

julia> rand(Int, 2)
2-element Array{Int64,1}:
 1339893410598768192
 1575814717733606317

julia> using Random

julia> rand(MersenneTwister(0), Dict(1=>2, 3=>4))
1=>2

julia> rand((2, 3))
3

julia> rand(Float64, (2, 3))
2×3 Array{Float64,2}:
 0.999717  0.0143835  0.540787
 0.696556  0.783855   0.938235

Сложность rand(rng, s::Union{AbstractDict,AbstractSet}) является линейной относительно длины s, если не доступен оптимизированный метод с постоянной сложностью, как в случае с Dict, Set и плотными BitSet. Если вызовов достаточно много, используйте вместо этого rand(rng, collect(s)) либо rand(rng, Dict(s)), либо rand(rng, Set(s)) в зависимости от ситуации.

rand!([rng=default_rng()], A, [S=eltype(A)])

Заполняет массив A случайными значениями. Если указан аргумент S (S может быть типом или коллекцией; подробные сведения см. в описании функции rand), значения выбираются случайным образом из S. Это равноценно copyto!(A, rand(rng, S, size(A))), но новый массив не создается.

Примеры

julia> rng = MersenneTwister(1234);

julia> rand!(rng, zeros(5))
5-element Vector{Float64}:
 0.5908446386657102
 0.7667970365022592
 0.5662374165061859
 0.4600853424625171
 0.7940257103317943
bitrand([rng=default_rng()], [dims...])

Создает BitArray случайных логических значений.

Примеры

julia> rng = MersenneTwister(1234);

julia> bitrand(rng, 10)
10-element BitVector:
 0
 0
 0
 0
 1
 0
 0
 0
 1
 1
randn([rng=default_rng()], [T=Float64], [dims...])

Создает нормально распределенное случайное число типа T со средним значением 0 и среднеквадратичным отклонением 1. Также может использоваться для создания массива нормально распределенных случайных чисел. В модуле Base в настоящее время имеется реализация для типов Float16, Float32 и Float64 (по умолчанию), а также для их комплексных (Complex) аналогов. Если в аргументе типа указан комплексный тип, значения соответствуют кругообразно симметричному комплексному нормальному распределению с дисперсией 1 (что соответствует независимому нормальному распределению вещественной и мнимой частей со средним значением, равным нулю, и дисперсией 1/2).

Примеры

julia> using Random

julia> rng = MersenneTwister(1234);

julia> randn(rng, ComplexF64)
0.6133070881429037 - 0.6376291670853887im

julia> randn(rng, ComplexF32, (2, 3))
2×3 Matrix{ComplexF32}:
 -0.349649-0.638457im  0.376756-0.192146im  -0.396334-0.0136413im
  0.611224+1.56403im   0.355204-0.365563im  0.0905552+1.31012im
randn!([rng=default_rng()], A::AbstractArray) -> A

Заполняет массив A нормально распределенными случайными числами (со средним значением 0 и среднеквадратичным отклонением 1). См. также описание функции rand.

Примеры

julia> rng = MersenneTwister(1234);

julia> randn!(rng, zeros(5))
5-element Vector{Float64}:
  0.8673472019512456
 -0.9017438158568171
 -0.4944787535042339
 -0.9029142938652416
  0.8644013132535154
randexp([rng=default_rng()], [T=Float64], [dims...])

Создает случайное число типа T согласно экспоненциальному распределению с масштабом 1. Также может использоваться для создания массива таких случайных чисел. В модуле Base в настоящее время имеется реализация для типов Float16, Float32 и Float64 (по умолчанию).

Примеры

julia> rng = MersenneTwister(1234);

julia> randexp(rng, Float32)
2.4835055f0

julia> randexp(rng, 3, 3)
3×3 Matrix{Float64}:
 1.5167    1.30652   0.344435
 0.604436  2.78029   0.418516
 0.695867  0.693292  0.643644
randexp!([rng=default_rng()], A::AbstractArray) -> A

Заполняет массив A случайными значениями согласно экспоненциальному распределению (с масштабом 1).

Примеры

julia> rng = MersenneTwister(1234);

julia> randexp!(rng, zeros(5))
5-element Vector{Float64}:
 2.4835053723904896
 1.516703605376473
 0.6044364871025417
 0.6958665886385867
 1.3065196315496677
randstring([rng=default_rng()], [chars], [len=8])

Создает случайную строку длиной len, состоящую из символов из chars (по умолчанию это множество строчных и прописных букв, а также цифр 0—​9). В необязательном аргументе rng указывается генератор случайных чисел; см. раздел Случайные числа.

Примеры

julia> Random.seed!(3); randstring()
"Lxz5hUwn"

julia> randstring(MersenneTwister(3), 'a':'z', 6)
"ocucay"

julia> randstring("ACGT")
"TGCTCCTC"

chars может быть любой коллекцией символов типа Char или UInt8 (более эффективный вариант), из которого функция rand может случайным образом выбирать символы.

Подпоследовательности, перестановки и перетасовка

randsubseq([rng=default_rng(),] A, p) -> Vector

Возвращает вектор, представляющий собой случайную подпоследовательность из массива A, в которую каждый элемент A включается (в соответствующем порядке) с независимой вероятностью p. (Сложность является линейной относительно p*length(A), поэтому данная функция эффективна, даже если p имеет небольшое значение, а A — большой размер.) Формально этот процесс называется выбором по схеме Бернулли из A.

Примеры

julia> rng = MersenneTwister(1234);

julia> randsubseq(rng, 1:8, 0.3)
2-element Vector{Int64}:
 7
 8
randsubseq!([rng=default_rng(),] S, A, p)

Действует аналогично randsubseq, но результаты сохраняются в векторе S (размер которого изменяется по мере необходимости).

Примеры

julia> rng = MersenneTwister(1234);

julia> S = Int64[];

julia> randsubseq!(rng, S, 1:8, 0.3)
2-element Vector{Int64}:
 7
 8

julia> S
2-element Vector{Int64}:
 7
 8
randperm([rng=default_rng(),] n::Integer)

Создает случайную перестановку длиной n. В необязательном аргументе rng указывается генератор случайных чисел (см. раздел Случайные числа). Тип элемента результата совпадает с типом n.

Для случайной перестановки произвольного вектора используйте функцию shuffle или shuffle!.

Совместимость: Julia 1.1

В Julia 1.1 randperm возвращает вектор v с eltype(v) == typeof(n), а в Julia 1.0 — с eltype(v) == Int.

Примеры

julia> randperm(MersenneTwister(1234), 4)
4-element Vector{Int64}:
 2
 1
 4
 3
randperm!([rng=default_rng(),] A::Array{<:Integer})

Создает в A случайную перестановку длиной length(A). В необязательном аргументе rng указывается генератор случайных чисел (см. раздел Случайные числа). Для случайной перестановки произвольного вектора используйте функцию shuffle или shuffle!.

Примеры

julia> randperm!(MersenneTwister(1234), Vector{Int}(undef, 4))
4-element Vector{Int64}:
 2
 1
 4
 3
randcycle([rng=default_rng(),] n::Integer)

Создает случайную циклическую перестановку длиной n. В необязательном аргументе rng указывается генератор случайных чисел; см. раздел Случайные числа. Тип элемента результата совпадает с типом n.

Совместимость: Julia 1.1

В Julia 1.1 randcycle возвращает вектор v с eltype(v) == typeof(n), а в Julia 1.0 — с eltype(v) == Int.

Примеры

julia> randcycle(MersenneTwister(1234), 6)
6-element Vector{Int64}:
 3
 5
 4
 6
 1
 2
randcycle!([rng=default_rng(),] A::Array{<:Integer})

Создает в A случайную циклическую перестановку длиной length(A). В необязательном аргументе rng указывается генератор случайных чисел; см. раздел Случайные числа.

Примеры

julia> randcycle!(MersenneTwister(1234), Vector{Int}(undef, 6))
6-element Vector{Int64}:
 3
 5
 4
 6
 1
 2
shuffle([rng=default_rng(),] v::AbstractArray)

Возвращает случайно перестановленную копию v. В необязательном аргументе rng указывается генератор случайных чисел (см. раздел Случайные числа). Для перестановки v на месте используйте функцию shuffle!. Для получения случайно перестановленных индексов используйте функцию randperm.

Примеры

julia> rng = MersenneTwister(1234);

julia> shuffle(rng, Vector(1:10))
10-element Vector{Int64}:
  6
  1
 10
  2
  3
  9
  5
  7
  4
  8
shuffle!([rng=default_rng(),] v::AbstractArray)

Версия shuffle, выполняющая случайную перестановку v на месте; в необязательном аргументе rng можно указать генератор случайных чисел.

Примеры

julia> rng = MersenneTwister(1234);

julia> shuffle!(rng, Vector(1:16))
16-element Vector{Int64}:
  2
 15
  5
 14
  1
  9
 10
  6
 11
  3
 16
  7
  4
 12
  8
 13

Генераторы (создание и инициализация)

default_rng() -> rng

Возвращает глобальный генератор случайных чисел по умолчанию.

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

Совместимость: Julia 1.3

Эта функция появилась в версии Julia 1.3.

seed!([rng=default_rng()], seed) -> rng
seed!([rng=default_rng()]) -> rng

Повторно инициализирует генератор случайных чисел: rng возвращает воспроизводимую последовательность чисел только в том случае, если указано значение seed. Некоторые генераторы случайных чисел, например RandomDevice, не принимают начальное значение. После вызова seed! rng эквивалентно новому объекту, инициализированному с тем же начальным значением.

Если аргумент rng не указан, по умолчанию инициализируется состояние общего генератора, локального для задачи.

Примеры

julia> Random.seed!(1234);

julia> x1 = rand(2)
2-element Vector{Float64}:
 0.32597672886359486
 0.5490511363155669

julia> Random.seed!(1234);

julia> x2 = rand(2)
2-element Vector{Float64}:
 0.32597672886359486
 0.5490511363155669

julia> x1 == x2
true

julia> rng = Xoshiro(1234); rand(rng, 2) == x1
true

julia> Xoshiro(1) == Random.seed!(rng, 1)
true

julia> rand(Random.seed!(rng), Bool) # невозможно воспроизвести
true

julia> rand(Random.seed!(rng), Bool) # также невозможно воспроизвести
false

julia> rand(Xoshiro(), Bool) # также невозможно воспроизвести
true
AbstractRNG

Супертип для генераторов случайных чисел, таких как MersenneTwister и RandomDevice.

TaskLocalRNG

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

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

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

Xoshiro(seed)
Xoshiro()

Xoshiro256++ — это быстрый генератор псевдослучайных чисел, описанный Дэвидом Блэкманом и Себастьяно Винья в публикации «Скремблированные линейные генераторы псевдослучайных чисел», ACM Trans. Math. Softw., 2021. Эталонная реализация доступна на сайте http://prng.di.unimi.it.

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

Реализация Xoshiro в Julia имеет режим пакетного генерирования: новые виртуальные генераторы псевдослучайных чисел инициализируются из родительского генератора, а для генерирования значений в параллельном режиме используется SIMD (то есть пакетный поток состоит из нескольких чередующихся экземпляров xoshiro). После выполнения пакетного запроса виртуальные генераторы псевдослучайных чисел удаляются (и не должны занимать место в куче).

Примеры

julia> using Random

julia> rng = Xoshiro(1234);

julia> x1 = rand(rng, 2)
2-element Vector{Float64}:
 0.32597672886359486
 0.5490511363155669

julia> rng = Xoshiro(1234);

julia> x2 = rand(rng, 2)
2-element Vector{Float64}:
 0.32597672886359486
 0.5490511363155669

julia> x1 == x2
true
MersenneTwister(seed)
MersenneTwister()

Создает объект генератора случайных чисел (RNG) MersenneTwister. У разных объектов RNG могут быть собственные начальные значения, что может быть полезно для генерирования разных потоков случайных чисел. Значением seed может быть неотрицательное целое число или вектор целых чисел UInt32. Если начальное значение не указано, оно создается случайным образом (с использованием энтропии системы). С помощью функции seed! можно повторно инициализировать уже существующий объект MersenneTwister.

Примеры

julia> rng = MersenneTwister(1234);

julia> x1 = rand(rng, 2)
2-element Vector{Float64}:
 0.5908446386657102
 0.7667970365022592

julia> rng = MersenneTwister(1234);

julia> x2 = rand(rng, 2)
2-element Vector{Float64}:
 0.5908446386657102
 0.7667970365022592

julia> x1 == x2
true
RandomDevice()

Создает объект генератора случайных чисел (RNG) RandomDevice. Два таких объекта всегда генерируют разные потоки случайных чисел. Энтропия берется из операционной системы.

Подключение к API Random

Существует два совершенно разных способа расширения возможностей модуля Random:

  1. генерирование случайных значений пользовательских типов;

  2. создание новых генераторов.

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

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

Генерирование случайных значений пользовательских типов

Генерирование случайных чисел для некоторых распределений может требовать определенного компромисса. Предварительно вычисленные значения, такие как таблица псевдонимов для дискретных распределений или функции «уплотнения» для одномерных распределений, могут существенно ускорить выборку. Объем предварительно вычисляемой информации может зависеть от количества значений, которое планируется выбрать из сборки. Кроме того, у некоторых генераторов случайных чисел могут быть определенные свойства, которые могут быть полезны для различных алгоритмов.

В модуле Random определен настраиваемый фреймворк для получения случайных значений, который может решить эти проблемы. При каждом вызове rand создается сэмплер, который можно настроить с учетом описанных выше компромиссов путем добавления методов в объект Sampler, который, в свою очередь, может диспетчеризоваться по генератору случайных чисел, объекту, описывающему распределение, и предположительному числу повторов. В настоящее время во втором случае используется Val{1} (для единичной выборки) и Val{Inf} (для произвольного числа), причем для обоих вариантов псевдонимом служит Random.Repetition.

Объект, возвращаемый Sampler, используется для генерирования случайных чисел. При реализации интерфейса генерирования случайных чисел для значения X, из которого может осуществляться выборка, необходимо определить метод

rand(rng, sampler)

для определенного объекта sampler, возвращаемого Sampler(rng, X, repetition).

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

  1. SamplerType{T}() можно использовать для реализации сэмплеров, осуществляющих выборку из типа T (например, rand(Int)). Это сэмплер по умолчанию, возвращаемый Sampler для типов.

  2. SamplerTrivial(self) — простая оболочка для self, доступная через []. Этот сэмплер рекомендуется в случаях, когда предварительно вычисленная информация не требуется (например, rand(1:3)), и возвращается Sampler по умолчанию для значений.

  3. SamplerSimple(self, data) также содержит дополнительное поле data, позволяющее хранить произвольные предварительно вычисленные значения, которые должны вычисляться в пользовательском методе Sampler.

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

Sampler(rng, x, repetition = Val(Inf))

Возвращает объект сэмплера, с помощью которого можно генерировать случайные значения из rng для x.

Если sp = Sampler(rng, x, repetition), для выборки случайных значений будет применяться функция rand(rng, sp), которая должна быть определена соответствующим образом.

repetition может быть Val(1) или Val(Inf). Исходя из этого аргумента принимается решение об объеме предварительно вычисленных данных, если они требуются.

Random.SamplerType и Random.SamplerTrivial — это сэмплеры по умолчанию для типов и значений соответственно. Random.SamplerSimple можно использовать для хранения предварительно вычисленных значений, не определяя дополнительные типы только лишь с этой целью.

SamplerType{T}()

Сэмплер для типов, не содержащих другой информации. Вариант по умолчанию для Sampler при вызове с типами.

SamplerTrivial(x)

Создает сэмплер, который просто инкапсулирует указанное значение x. Используется по умолчанию для значений. Значение eltype этого сэмплера равно eltype(x).

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

SamplerSimple(x, data)

Создает сэмплер, который инкапсулирует указанное значение x и data. Значение eltype этого сэмплера равно eltype(x).

Рекомендуемый вариант использования — выборка из значений с предварительно вычисленными данными.

Возможность отвязать предварительное вычисление от собственно генерирования значений является частью API и также доступна пользователю. Для примера предположим, что функция rand(rng, 1:20) должна вызываться повторно в цикле. В этом случае реализовать данную возможность можно так:

rng = MersenneTwister()
sp = Random.Sampler(rng, 1:20) # или Random.Sampler(MersenneTwister, 1:20)
for x in X
    n = rand(rng, sp) # что аналогично n = rand(rng, 1:20)
    # используем n
end

Данный механизм также применяется в стандартной библиотеке, например в реализации по умолчанию для создания случайных массивов (как в rand(1:20, 10)).

Генерирование значений на основе типа

Для данного типа T в настоящее время предполагается, что если определен метод rand(T), будет создан объект типа T. SamplerType — это сэмплер по умолчанию для типов. Чтобы реализовать генерирование случайных значений типа T, следует определить метод rand(rng::AbstractRNG, ::Random.SamplerType{T}), который должен возвращать значения, которые ожидаются от rand(rng, T).

Возьмем следующий пример: мы реализуем тип Die (игральная кость) с переменным количеством сторон n, пронумерованных от 1 до n. Мы хотим, чтобы метод rand(Die) создавал объект Die со случайным количеством сторон (до 20, но не менее 4):

struct Die
    nsides::Int # количество сторон
end

Random.rand(rng::AbstractRNG, ::Random.SamplerType{Die}) = Die(rand(rng, 4:20))

# output

Методы для операций со скалярными значениями и массивами теперь работают правильно для Die:

julia> rand(Die)
Die(5)

julia> rand(MersenneTwister(0), Die)
Die(11)

julia> rand(Die, 3)
3-element Vector{Die}:
 Die(9)
 Die(15)
 Die(14)

julia> a = Vector{Die}(undef, 3); rand!(a)
3-element Vector{Die}:
 Die(19)
 Die(7)
 Die(17)

Простой сэмплер без предварительно вычисленных данных

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

Чтобы реализовать генерирование случайных значений из объектов типа S, следует определить следующий метод: rand(rng::AbstractRNG, sp::Random.SamplerTrivial{S}). Здесь sp просто инкапсулирует объект типа S, к которому можно обращаться через sp[]. Продолжая пример с Die, теперь мы хотим определить метод rand(d::Die) для получения значения Int, соответствующего одной из сторон d:

julia> Random.rand(rng::AbstractRNG, d::Random.SamplerTrivial{Die}) = rand(rng, 1:d[].nsides);

julia> rand(Die(4))
1

julia> rand(Die(4), 3)
3-element Vector{Any}:
 2
 3
 3

Для данного типа коллекции S в настоящее время предполагается, что если определен метод rand(::S), будет создан объект типа eltype(S). В последнем примере получается Vector{Any}, так как eltype(Die) == Any. Для решения этой проблемы можно определить Base.eltype(::Type{Die}) = Int.

Генерирование значений для типа AbstractFloat

Типы AbstractFloat представляют собой особый случай, потому что по умолчанию случайные значения создаются не во всей области типа, а в полуинтервале [0,1). Для T <: AbstractFloat следует реализовать следующий метод: Random.rand(::AbstractRNG, ::Random.SamplerTrivial{Random.CloseOpen01{T}})

Оптимизированный сэмплер с предварительно вычисленными данными

Рассмотрим дискретное распределение, из которого числа 1:n выбираются с определенными вероятностями, в сумме равными единице. Если из этого распределения требуется много значений, самым быстрым методом будет использование таблицы псевдонимов. Здесь мы не приводим алгоритм построения такой таблицы, а просто предполагаем, что он реализован в make_alias_table(probabilities), а для выборки случайного числа можно использовать метод draw_number(rng, alias_table).

Допустим, что распределение описывается следующим образом:

struct DiscreteDistribution{V <: AbstractVector}
    probabilities::V
end

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

Random.eltype(::Type{<:DiscreteDistribution}) = Int

function Random.Sampler(::Type{<:AbstractRNG}, distribution::DiscreteDistribution, ::Repetition)
    SamplerSimple(disribution, make_alias_table(distribution.probabilities))
end

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

function rand(rng::AbstractRNG, sp::SamplerSimple{<:DiscreteDistribution})
    draw_number(rng, sp.data)
end

будет использоваться для выборки значений.

Пользовательские типы сэмплеров

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

Вернемся к примеру с Die: rand(::Die) генерирует случайные значения из диапазона, что дает возможность для оптимизации. Назовем наш пользовательский сэмплер SamplerDie.

import Random: Sampler, rand

struct SamplerDie <: Sampler{Int} # генерирует значения типа Int
    die::Die
    sp::Sampler{Int} # это абстрактный тип, поэтому можно сделать лучше
end

Sampler(RNG::Type{<:AbstractRNG}, die::Die, r::Random.Repetition) =
    SamplerDie(die, Sampler(RNG, 1:die.nsides, r))
# параметр `r` будет рассмотрен далее

rand(rng::AbstractRNG, sp::SamplerDie) = rand(rng, sp.sp)

Теперь можно получить сэмплер с помощью sp = Sampler(rng, die) и использовать sp вместо die в любом вызове rand с rng. В простейшем примере выше die не требуется хранить в SamplerDie, но на практике такое часто бывает необходимо.

Такой шаблон встречается настолько часто, что на этот случай предлагается вспомогательный тип, который использовался выше, а именно Random.SamplerSimple. Благодаря ему можно не определять SamplerDie: развязку можно было бы реализовать так:

Sampler(RNG::Type{<:AbstractRNG}, die::Die, r::Random.Repetition) =
    SamplerSimple(die, Sampler(RNG, 1:die.nsides, r))

rand(rng::AbstractRNG, sp::SamplerSimple{Die}) = rand(rng, sp.data)

Здесь sp.data ссылается на второй параметр в вызове конструктора SamplerSimple (в данном случае Sampler(rng, 1:die.nsides, r)), а объект Die доступен через sp[].

Как и SamplerDie, любой пользовательский сэмплер должен быть подтипом Sampler{T}, где T — это тип генерируемых значений. Обратите внимание, что SamplerSimple(x, data) isa Sampler{eltype(x)}. Это накладывает ограничение на возможные типы первого аргумента SamplerSimple (SamplerSimple рекомендуется использовать как в примере с Die, где x просто передается при определении метода Sampler). Аналогичным образом, SamplerTrivial(x) isa Sampler{eltype(x)}.

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

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

В некоторых случаях на выбор алгоритма влияет то, нужно ли сгенерировать лишь несколько значений или большое их число. За это отвечает третий параметр конструктора Sampler. Предположим, что мы определили два вспомогательных типа для Die, например SamplerDie1 для генерирования лишь нескольких случайных значений и SamplerDieMany для множества значений. Эти типы можно использовать следующим образом:

Sampler(RNG::Type{<:AbstractRNG}, die::Die, ::Val{1}) = SamplerDie1(...)
Sampler(RNG::Type{<:AbstractRNG}, die::Die, ::Val{Inf}) = SamplerDieMany(...)

Безусловно, для этих типов также должна быть определена функция rand (то есть rand(::AbstractRNG, ::SamplerDie1) и rand(::AbstractRNG, ::SamplerDieMany)). Обратите внимание, что, как и обычно, если пользовательские типы не требуются, можно использовать SamplerTrivial и SamplerSimple.

Примечание. Sampler(rng, x) — это просто краткая форма записи для вызова Sampler(rng, x, Val(Inf)), а Random.Repetition — псевдоним для Union{Val{1}, Val{Inf}}.

Создание новых генераторов

API пока еще не определен окончательно, но следует придерживаться следующих правил.

  1. Для конкретного генератора случайных чисел должны быть определены все необходимые методы rand, возвращающие «базовые» типы (целочисленные типы и типы с плавающей запятой isbitstype в Base).

  2. Другие задокументированные методы rand, принимающие AbstractRNG, должны работать без дополнительной подготовки (при условии реализации методов из пункта 1), но могут и специализироваться для данного генератора случайных чисел, если есть возможность для оптимизации.

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

Что касается пункта 1, метод rand может работать автоматически, но официально такая возможность не поддерживается, и он может перестать работать без предупреждения в одной из будущих версий.

Чтобы определить метод rand для гипотетического генератора MyRNG и спецификации значения s (например, s == Int или s == 1:10) типа S==typeof(s) или S==Type{s} (если s — это тип), следует реализовать те же два метода, что были рассмотрены ранее:

  1. Sampler(::Type{MyRNG}, ::S, ::Repetition), возвращающий объект типа, например SamplerS

  2. rand(rng::MyRNG, sp::SamplerS)

Метод Sampler(rng::AbstractRNG, ::S, ::Repetition) может уже быть определен в модуле Random. В этом случае на практике можно пропустить шаг 1 (если нужно специализировать поведение для данного типа генератора случайных чисел), но соответствующий тип SamplerS считается внутренним компонентом и может быть изменен без предупреждения.

Специализация генерирования массивов

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

Чтобы реализовать такую специализацию для MyRNG и спецификации s, благодаря которой создаются элементы типа S, можно определить следующий метод: rand!(rng::MyRNG, a::AbstractArray{S}, ::SamplerS), где SamplerS — это тип сэмплера, возвращаемого Sampler(MyRNG, s, Val(Inf)). Вместо AbstractArray эту функциональную возможность можно также реализовать только для подтипа, например Array{S}. Неизменяющий метод rand для массива будет автоматически вызывать эту специализацию.

Воспроизводимость

Инициализируя параметр RNG с определенным начальным значением, можно воспроизводить одну и ту же последовательность псевдослучайных чисел при повторном выполнении программы. Однако в новом вспомогательном выпуске Julia (например, при переходе с версии 1.3 на 1.4) последовательность псевдослучайных чисел, генерируемых на основе определенного начального значения, может измениться, в частности при использовании MersenneTwister. (Даже если последовательность, создаваемая низкоуровневой функцией, такой как rand, не изменяется, результат высокоуровневой функции, такой как randsubseq, может измениться из-за обновлений алгоритма.) Поэтому чтобы гарантировать неизменность потоков псевдослучайных чисел, приходится отказываться от многих улучшений алгоритмов.

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

Для программных тестов, использующих определенные «случайные» данные, также необходимо либо сохранять данные, либо включать их в код теста, либо применять сторонние пакеты, такие как StableRNGs.jl. С другой стороны, в тестах, которые должны успешно выполняться для большинства случайных данных (как в случае с тестированием A \ (A*x) ≈ x для случайной матрицы A = randn(n,n)), можно использовать генератор случайных чисел с фиксированным начальным значением. Многократное выполнение такого теста позволит убедиться в том, что в случае с крайне маловероятными данными (например, очень плохо обусловленной матрицей) сбой не происходит.

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