Случайные числа
Для генерирования случайных чисел в 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.Random
— Module
Random
Поддержка генерирования случайных чисел. Предоставляет rand
, randn
, AbstractRNG
, MersenneTwister
и RandomDevice
.
Функции для генерирования случайных чисел
#
Base.rand
— Function
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
Для поддержки |
Примеры
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
Сложность |
#
Random.rand!
— Function
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
#
Random.bitrand
— Function
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
#
Base.randn
— Function
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
#
Random.randn!
— Function
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
#
Random.randexp
— Function
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
#
Random.randexp!
— Function
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
#
Random.randstring
— Function
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"
|
Подпоследовательности, перестановки и перетасовка
#
Random.randsubseq
— Function
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
#
Random.randsubseq!
— Function
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
#
Random.randperm
— Function
randperm([rng=default_rng(),] n::Integer)
Создает случайную перестановку длиной n
. В необязательном аргументе rng
указывается генератор случайных чисел (см. раздел Случайные числа). Тип элемента результата совпадает с типом n
.
Совместимость: Julia 1.1
В Julia 1.1 |
Примеры
julia> randperm(MersenneTwister(1234), 4)
4-element Vector{Int64}:
2
1
4
3
#
Random.randperm!
— Function
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
#
Random.randcycle
— Function
randcycle([rng=default_rng(),] n::Integer)
Создает случайную циклическую перестановку длиной n
. В необязательном аргументе rng
указывается генератор случайных чисел; см. раздел Случайные числа. Тип элемента результата совпадает с типом n
.
Совместимость: Julia 1.1
В Julia 1.1 |
Примеры
julia> randcycle(MersenneTwister(1234), 6)
6-element Vector{Int64}:
3
5
4
6
1
2
#
Random.randcycle!
— Function
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
#
Random.shuffle
— Function
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
#
Random.shuffle!
— Function
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
Генераторы (создание и инициализация)
#
Random.default_rng
— Function
default_rng() -> rng
Возвращает глобальный генератор случайных чисел по умолчанию.
То, какой генератор случайных чисел используется по умолчанию, зависит от реализации. В разных версиях Julia генератор случайных чисел по умолчанию может меняться и возвращать разные потоки случайных чисел для одного и того же начального значения. |
Совместимость: Julia 1.3
Эта функция появилась в версии Julia 1.3. |
#
Random.seed!
— Function
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
#
Random.AbstractRNG
— Type
AbstractRNG
Супертип для генераторов случайных чисел, таких как MersenneTwister
и RandomDevice
.
#
Random.TaskLocalRNG
— Type
TaskLocalRNG
Состояние TaskLocalRNG
является локальным для задачи, а не потока. Оно инициализируется при создании задачи на основе состояния родительской задачи. Поэтому создание задачи является событием, изменяющим состояние родительского генератора случайных чисел.
Положительной стороной является то, что TaskLocalRNG
работает достаточно быстро и обеспечивает воспроизводимые многопоточные симуляции (если только не возникнет состояние гонки) независимо от решений планировщика. При условии что количество потоков не учитывается при принятии решений во время создания задачи, результаты симуляции также не зависят от количества доступных потоков или ЦП. Случайный поток не должен зависеть от особенностей оборудования, в том числе порядка следования байтов и, возможно, размера слова.
Использование или инициализация генератора случайных чисел любой другой задачи, отличной от возвращаемой функцией current_task()
, может приводить к неопределенному результату: обычно он будет корректным, но иногда без предупреждения может происходить ошибка.
#
Random.Xoshiro
— Type
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
#
Random.MersenneTwister
— Type
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
#
Random.RandomDevice
— Type
RandomDevice()
Создает объект генератора случайных чисел (RNG) RandomDevice
. Два таких объекта всегда генерируют разные потоки случайных чисел. Энтропия берется из операционной системы.
Подключение к API Random
Существует два совершенно разных способа расширения возможностей модуля Random
:
-
генерирование случайных значений пользовательских типов;
-
создание новых генераторов.
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)
, но в большинстве ситуаций достаточно следующих предварительно определенных сэмплеров:
-
SamplerType{T}()
можно использовать для реализации сэмплеров, осуществляющих выборку из типаT
(например,rand(Int)
). Это сэмплер по умолчанию, возвращаемыйSampler
для типов. -
SamplerTrivial(self)
— простая оболочка дляself
, доступная через[]
. Этот сэмплер рекомендуется в случаях, когда предварительно вычисленная информация не требуется (например,rand(1:3)
), и возвращаетсяSampler
по умолчанию для значений. -
SamplerSimple(self, data)
также содержит дополнительное полеdata
, позволяющее хранить произвольные предварительно вычисленные значения, которые должны вычисляться в пользовательском методеSampler
.
Для каждого из сэмплеров мы приводим примеры. Здесь предполагается, что выбор алгоритма не зависит от генератора случайных чисел, поэтому в сигнатурах используется AbstractRNG
.
#
Random.Sampler
— Type
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
можно использовать для хранения предварительно вычисленных значений, не определяя дополнительные типы только лишь с этой целью.
#
Random.SamplerType
— Type
SamplerType{T}()
Сэмплер для типов, не содержащих другой информации. Вариант по умолчанию для Sampler
при вызове с типами.
#
Random.SamplerTrivial
— Type
SamplerTrivial(x)
Создает сэмплер, который просто инкапсулирует указанное значение x
. Используется по умолчанию для значений. Значение eltype
этого сэмплера равно eltype(x)
.
Рекомендуемый вариант использования — выборка из значений без предварительно вычисленных данных.
#
Random.SamplerSimple
— Type
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 пока еще не определен окончательно, но следует придерживаться следующих правил.
-
Для конкретного генератора случайных чисел должны быть определены все необходимые методы
rand
, возвращающие «базовые» типы (целочисленные типы и типы с плавающей запятойisbitstype
вBase
). -
Другие задокументированные методы
rand
, принимающиеAbstractRNG
, должны работать без дополнительной подготовки (при условии реализации методов из пункта 1), но могут и специализироваться для данного генератора случайных чисел, если есть возможность для оптимизации. -
Функция
copy
для генераторов псевдослучайных чисел должна возвращать независимую копию, которая генерирует точно ту же случайную последовательность, что и исходный генератор, при вызове аналогичным способом. Если это невозможно (как в случае с генераторами на аппаратной основе), функциюcopy
реализовывать не следует.
Что касается пункта 1, метод rand
может работать автоматически, но официально такая возможность не поддерживается, и он может перестать работать без предупреждения в одной из будущих версий.
Чтобы определить метод rand
для гипотетического генератора MyRNG
и спецификации значения s
(например, s == Int
или s == 1:10
) типа S==typeof(s)
или S==Type{s}
(если s
— это тип), следует реализовать те же два метода, что были рассмотрены ранее:
-
Sampler(::Type{MyRNG}, ::S, ::Repetition)
, возвращающий объект типа, напримерSamplerS
-
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.