Random Numbers

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

Генераторы псевдослучайных чисел (PRNG), экспортируемые пакетом Random, это:

  • TaskLocalRNG: токен, который представляет использование текущего активного потока, локального для задачи, детерминированно инициализированного от родительской задачи или с помощью RandomDevice (с системной случайностью) при запуске программы
  • Xoshiro: генерирует высококачественный поток случайных чисел с небольшим вектором состояния и высокой производительностью, используя алгоритм Xoshiro256++
  • RandomDevice: для предоставленной ОС энтропии. Это может быть использовано для криптографически безопасных случайных чисел (CS(P)RNG).
  • MersenneTwister: альтернативный высококачественный ПСГ, который был по умолчанию в старых версиях Julia и также довольно быстр, но требует гораздо больше места для хранения вектор состояния и генерации случайной последовательности.

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

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

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

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

Warning

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

Random numbers module

Random generation functions

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

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

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

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

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

  • типом из списка ниже, соответствующим указанному набору значений

    • конкретные целочисленные типы выбираются из typemin(S):typemax(S) (за исключением BigInt, который не поддерживается)
    • конкретные типы с плавающей запятой выбираются из [0, 1)
    • конкретные комплексные типы Complex{T}, если T является типом, который можно выбирать, берут их действительные и мнимые компоненты независимо из набора значений, соответствующих T, но не поддерживаются, если T не является выборочным.
    • все <:AbstractChar типы выбираются из набора допустимых скалярных значений Unicode
    • пользовательский тип и набор значений; для получения рекомендаций по реализации смотрите Hooking into the Random API
    • кортеж типа известного размера, где каждый параметр S сам по себе является выборочным типом; возвращает значение типа S. Обратите внимание, что типы кортежей, такие как Tuple{Vararg{T}} (неизвестный размер) и Tuple{1:2} (параметризованный значением) не поддерживаются
    • тип Pair, например, Pair{X, Y}, такой что rand определен для X и Y, в этом случае производятся случайные пары.

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

Смотрите также randn для нормально распределенных чисел, и rand! и randn! для эквивалентов на месте.

Julia 1.1

Поддержка S в качестве кортежа требует как минимум Julia 1.1.

Julia 1.11

Поддержка S в качестве типа Tuple требует как минимум Julia 1.11.

Примеры

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

julia> using Random

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

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
Note

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

source
Random.rand!Function
rand!([rng=default_rng()], A, [S=eltype(A)])

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

Примеры

julia> rand!(Xoshiro(123), zeros(5))
5-element Vector{Float64}:
 0.521213795535383
 0.5868067574533484
 0.8908786980927811
 0.19090669902576285
 0.5256623915420473
source
Random.bitrandFunction
bitrand([rng=default_rng()], [dims...])

Сгенерировать BitArray случайных булевых значений.

Примеры

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

Сгенерировать нормально распределенное случайное число типа T со средним 0 и стандартным отклонением 1. Учитывая необязательный аргумент dims, сгенерировать массив размером dims таких чисел. Стандартная библиотека Julia поддерживает randn для любого типа с плавающей запятой, который реализует rand, например, для типов Base Float16, Float32, Float64 (по умолчанию) и BigFloat, а также для их Complex аналогов.

(Когда T является комплексным, значения берутся из кругово-симметричного комплексного нормального распределения с дисперсией 1, соответствующего действительным и мнимым частям, имеющим независимое нормальное распределение со средним ноль и дисперсией 1/2).

См. также randn! для выполнения операций на месте.

Примеры

Генерация одного случайного числа (с типом по умолчанию Float64):

julia> randn()
-0.942481877315864

Генерация матрицы нормальных случайных чисел (с типом по умолчанию Float64):

julia> randn(2,3)
2×3 Matrix{Float64}:
  1.18786   -0.678616   1.49463
 -0.342792  -0.134299  -1.45005

Настройка генератора случайных чисел rng с пользовательским начальным значением (для воспроизводимых чисел) и его использование для генерации случайного числа Float32 или матрицы случайных чисел ComplexF32:

julia> using Random

julia> rng = Xoshiro(123);

julia> randn(rng, Float32)
-0.6457307f0

julia> randn(rng, ComplexF32, (2, 3))
2×3 Matrix{ComplexF32}:
  -1.03467-1.14806im  0.693657+0.056538im   0.291442+0.419454im
 -0.153912+0.34807im    1.0954-0.948661im  -0.543347-0.0538589im
source
Random.randn!Function
randn!([rng=default_rng()], A::AbstractArray) -> A

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

Примеры

julia> randn!(Xoshiro(123), zeros(5))
5-element Vector{Float64}:
 -0.6457306721039767
 -1.4632513788889214
 -1.6236037455860806
 -0.21766510678354617
  0.4922456865251828
source
Random.randexpFunction
randexp([rng=default_rng()], [T=Float64], [dims...])

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

Примеры

julia> rng = Xoshiro(123);

julia> randexp(rng, Float32)
1.1757717f0

julia> randexp(rng, 3, 3)
3×3 Matrix{Float64}:
 1.37766  0.456653  0.236418
 3.40007  0.229917  0.0684921
 0.48096  0.577481  0.71835
source
Random.randexp!Function
randexp!([rng=default_rng()], A::AbstractArray) -> A

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

Примеры

julia> randexp!(Xoshiro(123), zeros(5))
5-element Vector{Float64}:
 1.1757716836348473
 1.758884569451514
 1.0083623637301151
 0.3510644315565272
 0.6348266443720407
source
Random.randstringFunction
randstring([rng=default_rng()], [chars], [len=8])

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

Примеры

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

julia> randstring(Xoshiro(3), 'a':'z', 6)
"iyzcsm"

julia> randstring("ACGT")
"TGCTCCTC"

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

source

Subsequences, permutations and shuffling

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

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

Примеры

julia> randsubseq(Xoshiro(123), 1:8, 0.3)
2-element Vector{Int64}:
 4
 7
source
Random.randsubseq!Function
randsubseq!([rng=default_rng(),] S, A, p)

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

Примеры

julia> S = Int64[];

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

julia> S
2-element Vector{Int64}:
 4
 7
source
Random.randpermFunction
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(Xoshiro(123), 4)
4-element Vector{Int64}:
 1
 4
 2
 3
source
Random.randperm!Function
randperm!([rng=default_rng(),] A::Array{<:Integer})

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

Примеры

julia> randperm!(Xoshiro(123), Vector{Int}(undef, 4))
4-element Vector{Int64}:
 1
 4
 2
 3
source
Random.randcycleFunction
randcycle([rng=default_rng(),] n::Integer)

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

Здесь "циклическая перестановка" означает, что все элементы находятся в одном цикле. Если n > 0, существует $(n-1)!$ возможных циклических перестановок, которые выбираются равномерно. Если n == 0, randcycle возвращает пустой вектор.

randcycle! является вариантом этой функции, работающим на месте.

Julia 1.1

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

Примеры

julia> randcycle(Xoshiro(123), 6)
6-element Vector{Int64}:
 5
 4
 2
 6
 3
 1
source
Random.randcycle!Function
randcycle!([rng=default_rng(),] A::Array{<:Integer})

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

Здесь "циклическая перестановка" означает, что все элементы находятся в одном цикле. Если A не пустой (n > 0), существует $(n-1)!$ возможных циклических перестановок, которые выбираются равномерно. Если A пустой, randcycle! оставляет его без изменений.

randcycle является вариантом этой функции, который выделяет новый вектор.

Примеры

julia> randcycle!(Xoshiro(123), Vector{Int}(undef, 6))
6-element Vector{Int64}:
 5
 4
 2
 6
 3
 1
source
Random.shuffleFunction
shuffle([rng=default_rng(),] v::AbstractArray)

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

Примеры

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

Версия в месте выполнения shuffle: случайным образом перемешивает v на месте, при необходимости предоставляя генератор случайных чисел rng.

Примеры

julia> shuffle!(Xoshiro(123), Vector(1:10))
10-element Vector{Int64}:
  5
  4
  2
  3
  6
 10
  8
  1
  9
  7
source

Generators (creation and seeding)

Random.default_rngFunction
Random.default_rng() -> rng

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

Когда модуль Random загружается, RNG по умолчанию случайно инициализируется, через Random.seed!(): это означает, что каждый раз, когда начинается новая сессия julia, первый вызов rand() дает другой результат, если сначала не был вызван seed!(seed).

Он безопасен для потоков: разные потоки могут безопасно вызывать функции, связанные с rand, на default_rng() одновременно, например, rand(default_rng()).

Note

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

Julia 1.3

Эта функция была введена в Julia 1.3.

source
Random.seed!Function
seed!([rng=default_rng()], seed) -> rng
seed!([rng=default_rng()]) -> rng

Переинициализируйте генератор случайных чисел: rng будет выдавать воспроизводимую последовательность чисел, если и только если задан seed. Некоторые генераторы случайных чисел не принимают семя, такие как RandomDevice. После вызова seed!, rng эквивалентен вновь созданному объекту, инициализированному с тем же семенем. Типы принимаемых семян зависят от типа 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
source
Random.TaskLocalRNGType
TaskLocalRNG

TaskLocalRNG имеет состояние, которое локально для своей задачи, а не для своего потока. Он инициализируется при создании задачи, на основе состояния родительской задачи, но без продвижения состояния RNG родителя.

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

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

При инициализации TaskLocalRNG() с помощью seed! переданное значение семени, если таковое имеется, может быть любым целым числом.

Julia 1.11

Инициализация TaskLocalRNG() с отрицательным целым числом требует как минимум Julia 1.11.

Julia 1.10

Создание задачи больше не продвигает состояние RNG родительской задачи, начиная с Julia 1.10.

source
Random.XoshiroType
Xoshiro(seed::Union{Integer, AbstractString})
Xoshiro()

Xoshiro256++ — это быстрый генератор псевдослучайных чисел, описанный Дэвидом Блэкманом и Себастьяно Виньей в статье "Scrambled Linear Pseudorandom Number Generators", ACM Trans. Math. Softw., 2021. Ссылка на реализацию доступна по адресу https://prng.di.unimi.it

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

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

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

Julia 1.11

Передача отрицательного целого семени требует как минимум Julia 1.11.

Примеры

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
source
Random.MersenneTwisterType
MersenneTwister(seed)
MersenneTwister()

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

Julia 1.11

Передача отрицательного целого числа в качестве семени требует как минимум Julia 1.11.

Примеры

julia> rng = MersenneTwister(123);

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

julia> x2 = rand(MersenneTwister(123), 2)
2-element Vector{Float64}:
 0.37453777969575874
 0.8735343642013971

julia> x1 == x2
true
source
Random.RandomDeviceType
RandomDevice()

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

source

Hooking into the Random API

Существует два в основном ортогональных способа расширить функциональность Random:

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

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

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

Generating random values of custom types

Генерация случайных значений для некоторых распределений может включать различные компромиссы. Предварительно вычисленные значения, такие как alias table для дискретных распределений, или “squeezing” functions для унивариантных распределений, могут значительно ускорить выборку. Сколько информации следует предварительно вычислить, может зависеть от количества значений, которые мы планируем извлечь из распределения. Кроме того, некоторые генераторы случайных чисел могут иметь определенные свойства, которые различные алгоритмы могут захотеть использовать.

Модуль 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 в наших сигнатурах.

Random.SamplerType
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 можно использовать для хранения предварительно вычисленных значений без определения дополнительных типов только для этой цели.

source
Random.SamplerTypeType
SamplerType{T}()

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

source
Random.SamplerTrivialType
SamplerTrivial(x)

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

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

source
Random.SamplerSimpleType
SamplerSimple(x, data)

Создайте выборщик, который оборачивает данное значение x и data. eltype этого выборщика равен eltype(x).

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

source

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

rng = Xoshiro()
sp = Random.Sampler(rng, 1:20) # or Random.Sampler(Xoshiro, 1:20)
for x in X
    n = rand(rng, sp) # similar to n = rand(rng, 1:20)
    # use n
end

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

Generating values from a type

Учитывая тип 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 # number of sides
end

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

# output

Скалярные и массивные методы для Die теперь работают как ожидалось:

julia> rand(Die)
Die(5)

julia> rand(Xoshiro(0), Die)
Die(10)

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)

A simple sampler without pre-computed data

Здесь мы определяем выборщик для коллекции. Если не требуется предварительно вычисленные данные, его можно реализовать с помощью выборщика 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.

Generating values for an AbstractFloat type

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

An optimized sampler with pre-computed data

Рассмотрим дискретное распределение, где числа 1:n выбираются с заданными вероятностями, которые в сумме равны единице. Когда необходимо получить много значений из этого распределения, самым быстрым методом является использование alias table. Мы не предоставляем алгоритм для построения такой таблицы здесь, но предположим, что он доступен в 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(distribution, make_alias_table(distribution.probabilities))
end

должен быть определен для возврата выборщика с предвычисленными данными, затем

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

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

Custom sampler types

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

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

import Random: Sampler, rand

struct SamplerDie <: Sampler{Int} # generates values of type Int
    die::Die
    sp::Sampler{Int} # this is an abstract type, so this could be improved
end

Sampler(RNG::Type{<:AbstractRNG}, die::Die, r::Random.Repetition) =
    SamplerDie(die, Sampler(RNG, 1:die.nsides, r))
# the `r` parameter will be explained later on

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 и может быть изменен в любое время без надлежащих устареваний.

Using distinct algorithms for scalar or array generation

В некоторых случаях, хочет ли кто-то сгенерировать лишь несколько значений или большое количество значений, это повлияет на выбор алгоритма. Это обрабатывается третьим параметром конструктора 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}}.

Creating new generators

API еще не четко определен, но в качестве правила:

  1. любой метод rand, производящий "базовые" типы (isbitstype целые и плавающие типы в Base), должен быть определен для этого конкретного RNG, если они необходимы;
  2. другие задокументированные методы rand, принимающие AbstractRNG, должны работать из коробки, (при условии, что методы из 1), на которые полагаются, реализованы), но, конечно, могут быть специализированы для этого RNG, если есть возможность для оптимизации;
  3. copy для псевдослучайных генераторов чисел (pseudo-RNGs) должен возвращать независимую копию, которая генерирует ту же самую случайную последовательность, что и оригинал, начиная с момента вызова тем же образом. Когда это невозможно (например, для аппаратных генераторов случайных чисел), 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) на практике (если кто-то хочет специализировать генерацию для этого конкретного типа RNG), но соответствующий тип SamplerS считается внутренней деталью и может быть изменен без предупреждения.

Specializing array generation

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

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

Reproducibility

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

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

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

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