Реализация AbstractMCMC в Turing
Введение
Рассмотрим блок кода Turing:
@model function gdemo(x, y)
s² ~ InverseGamma(2, 3)
m ~ Normal(0, sqrt(s²))
x ~ Normal(m, sqrt(s²))
y ~ Normal(m, sqrt(s²))
end
mod = gdemo(1.5, 2)
alg = IS()
n_samples = 1000
chn = sample(mod, alg, n_samples)
Функция sample
является частью интерфейса AbstractMCMC. Как объясняется в руководстве по интерфейсу, создание метода выборки, который может быть использован sample
, заключается в перегрузке структур и функций в AbstractMCMC
. В руководстве по интерфейсу также приведен отдельный пример реализации [AdvancedMH.jl
].
Методы выборки Turing (большинство из которых приведены здесь) также реализуют AbstractMCMC
. Turing определяет особую архитектуру для реализаций AbstractMCMC
, которая позволяет работать с моделями, определенными макросом @model
, и использует DynamicPPL в качестве бэкенда. Цель этой статьи — описать эту архитектуру и рассказать о том, как реализовать собственный метод выборки в Turing, используя выборку по значимости в качестве примера. Мы не будем вдаваться во все детали: например, здесь не рассматриваются селекторы или параллелизм.
Сначала мы узнаем, как работает выборка по значимости в абстрактном виде. Рассмотрим модель, определенную в первом блоке кода. Математически это можно записать так.
Латентные переменные — это и , наблюдаемые переменные — это и . Модель совместного распределения разлагается на априорное распределение и правдоподобие . Поскольку и наблюдаются, цель состоит в том, чтобы вывести апостериорное распределение .
Выборка по значимости выполняет независимые выборки из априорного распределения. Она также выводит ненормализованные веса,
так что эмпирическое распределение
является хорошей аппроксимацией для апостериорного.
1. Определение сэмплера (Sampler
)
Вспомните последнюю строку приведенного выше блока кода:
chn = sample(mod, alg, n_samples)
Здесь sample
принимает в качестве аргументов модель mod
, алгоритм alg
и количество выборок n_samples
и возвращает экземпляр chn
Chains
, который может быть проанализирован с помощью функций в MCMCChains
.
Модели
Чтобы определить модель, следует объявить совместное распределение по переменным в макросе @model
и указать, какие переменные являются наблюдаемыми, а какие должны быть выведены, а также значения наблюдаемых переменных. Таким образом, при реализации выборки по значимости
mod = gdemo(1.5, 2)
создает экземпляр mod
структуры Model
, который соответствует наблюдениям значения 1.5
для x
и значения 2
для y
.
Алгоритмы
Алгоритм — это просто метод выборки: в Turing он является подтипом абстрактного типа InferenceAlgorithm
. Для определения алгоритма может потребоваться указать несколько параметров высокого уровня. Например, «гамильтониан Монте-Карло» может быть слишком расплывчатым, но «гамильтониан Монте-Карло с 10 шагами с перешагиванием для каждого предложения и размером шага 0,01» — это алгоритм. «Метрополис-Гастингс» может быть слишком расплывчатым, но «Метрополис-Гастингс с распределением предложений p
» — это алгоритм. Таким образом
stepsize = 0.01
L = 10
alg = HMC(stepsize, L)
определяет алгоритм гамильтониана Монте-Карло, экземпляр HMC
, который является подтипом InferenceAlgorithm
.
В случае выборки по значимости нет необходимости указывать дополнительные параметры:
alg = IS()
определяет алгоритм выборки по значимости, экземпляр IS
, который является подтипом InferenceAlgorithm
.
При создании собственного метода выборки Turing вы должны построить подтип InferenceAlgorithm
, соответствующий вашему методу.
Сэмплеры
Сэмплеры — это не то же самое, что и алгоритмы. Алгоритм — это универсальный метод выборки, а сэмплер представляет собой объект, хранящий информацию о том, как алгоритм и модель взаимодействуют во время выборки, и изменяемый по мере выполнения выборки. Структура Sampler
определяется в DynamicPPL.
Turing реализует AbstractSampler
из AbstractMCMC
с помощью структуры Sampler
, определенной в DynamicPPL
. Наиболее важными атрибутами экземпляра spl
сэмплера (Sampler
) являются следующие.
-
spl.alg
: используемый метод выборки, экземпляр подтипаInferenceAlgorithm
; -
spl.state
: информация о процессе выборки (см. ниже).
Когда вы вызываете sample(mod, alg, n_samples)
, Turing сначала использует model
и alg
для создания экземпляра spl
сэмплера (Sampler
), а затем вызывает собственную функцию AbstractMCMC
sample(mod, spl, n_samples)
.
При определении собственного метода выборки Turing вы должны построить следующие компоненты:
-
конструктор сэмплера, который использует модель, и алгоритм для инициализации экземпляра
Sampler
. Для выборки по значимости:
function Sampler(alg::IS, model::Model, s::Selector)
info = Dict{Symbol, Any}()
state = ISState(model)
return Sampler(alg, info, s, state)
end
-
структура состояния, реализующая
AbstractSamplerState
, соответствующее вашему методу (см. следующий абзац).
Состояния
Поле vi
содержит всю важную информацию о выборке: прежде всего, значения всех образцов, а также распределения, из которых они выбраны, имена параметров модели и другие метаданные. Как мы увидим ниже, многие важные шаги во время выборки соответствуют запросам или обновлениям spl.state.vi
.
По умолчанию вы можете использовать SamplerState
, конкретный тип, определенный в inference/Inference.jl
, который расширяет AbstractSamplerState
и не имеет полей, кроме vi
:
mutable struct SamplerState{VIType<:VarInfo} <: AbstractSamplerState
vi :: VIType
end
При выполнении выборки по значимости важны не только значения образцов, но и их веса. Ниже мы увидим, что вес каждого образца также прибавляется к spl.state.vi
. Более того, среднее значение
весов образцов является особенно важной величиной:
-
она используется для нормализации эмпирической аппроксимации апостериорного распределения
-
ее логарифм — это оценка выборки по значимости доказательства логарифма .
Чтобы избежать необходимости повторных вычислений, is.jl
определяет специфичный для выборки по значимости конкретный тип ISState
для состояний сэмплера с дополнительным полем final_logevidence
, содержащим
mutable struct ISState{V<:VarInfo, F<:AbstractFloat} <: AbstractSamplerState
vi :: V
final_logevidence :: F
end
# дополнительный конструктор
ISState(model::Model) = ISState(VarInfo(model), 0.0)
На следующей схеме приводится обобщенный вид представленной выше иерархии.
2. Перегрузка функций, используемых внутри mcmcsample
Многое здесь зависит от метода. Однако в Turing также есть функции, которые облегчают реализацию этих функций.
Переходы
AbstractMCMC
хранит информацию, соответствующую каждому отдельному образцу, в объектах, называемых переходами (transition
), но не указывает, какой может быть структура этих объектов. Вы можете принять решение о реализации типа MyTransition
для переходов, соответствующий специфике ваших методов. Однако существует множество ситуаций, в которых единственная информация, необходимая для каждого образца, — это:
-
его значение:
-
логарифм совместной вероятности наблюдаемых данных и данной выборки:
lp
Inference.jl
определяет структуру Transition
, которая соответствует этой ситуации по умолчанию.
struct Transition{T, F<:AbstractFloat}
θ :: T
lp :: F
end
Принцип работы sample
Примерная схема, в которой не учитываются такие вещи, как параллелизм, выглядит следующим образом:
sample
вызывает функцию mcmcsample
, которая вызывает
-
sample_init!
для настройки всего необходимого -
step!
многократно, чтобы создать несколько новых переходов -
sample_end!
для выполнения операций после получения всех образцов -
bundle_samples
для преобразования вектора переходов в более подходящий тип, напримерChain
.
Конечно, вы можете реализовать все эти функции, но AbstractMCMC
и Turing также предоставляют реализации по умолчанию для простых случаев. Например, выборка по значимости использует стандартные реализации sample_init!
и bundle_samples
, поэтому вы не увидите код для них внутри is.jl
.
3. Перегрузка assume
и observe
Упомянутые выше функции, такие как sample_init!
, step!
и т. д., конечно же, должны использовать информацию о модели для создания образцов. В частности, этим функциям могут понадобиться образцы из распределений, определенных в модели, или оценка плотности этих распределений при некоторых значениях соответствующих параметров или наблюдений.
В качестве примера первого варианта рассмотрим выборку по значимости, как это определено в is.jl
. Эта реализация выборки по значимости использует априорное распределение модели в качестве распределения предложения, и поэтому для нее требуются образцы из априорного распределения модели. Другой пример — приближенное байесовское вычисление, для которого требуется несколько образцов из априорного распределения и распределения вероятности модели, чтобы сгенерировать один образец.
Примером последнего является алгоритм Метрополиса-Гастингса. На каждом шаге выборки из целевого апостериорного распределения
для вычисления коэффициента принятия необходимо оценить совместную плотность модели
с помощью образца из предложения и наблюдаемых данных .
В связи с этим возникает вопрос: как эти функции могут получить доступ к информации о модели во время выборки? Напомним, что модель хранится как экземпляр m
модели (Model
). Одним из атрибутов m
является функция оценки модели m.f
, которая строится путем компиляции макроса @model
. При выполнении f
по порядку запускаются операторы тильды модели, и на каждом шаге информация о модели добавляется в сэмплер (экземпляр Sampler
, хранящий информацию о текущем процессе выборки) (дополнительные сведения о компиляции макроса @model
см. здесь). Функции DynamicPPL assume
и observe
определяют, какую информацию добавлять в сэмплер для каждого оператора тильды.
Рассмотрим экземпляр m
модели (Model
) и сэмплер spl
со связанными VarInfo
vi = spl.state.vi
. В какой-то момент в процессе выборки функция AbstractMCMC, например step!
, вызывает функцию m(vi, ...)
, которая вызывает функцию оценки модели m.f(vi, ...)
.
-
Для каждого оператора тильды в макросе
@model
функцияm.f(vi, ...)
возвращает информацию, связанную с моделью (образцы, значение плотности модели и т. д.), и добавляет ее вvi
. Как это происходит?-
Вспомним, что код для
m.f(vi, ...)
автоматически генерируется при компиляции макроса@model
. -
Для каждого оператора тильды в объявлении
@model
этот код содержит вызовassume(vi, ...)
, если переменная в LHS тильды является выводимым параметром модели, иobserve(vi, ...)
, если переменная в LHS тильды является наблюдением. -
В файле, соответствующем вашему методу выборки (т. е. в
Turing.jl/src/inference/<your_method>.jl
), вы перегрузилиassume
иobserve
, чтобы они могли изменитьvi
для включения необходимых информации и образцов. -
Как минимум,
assume
иobserve
возвращают логарифмическую плотностьlp
образца или наблюдения. Затем функция оценки модели немедленно вызывает функциюacclogp!!(vi, lp)
, которая добавляетlp
к значению логарифмической совместной плотности, хранящемуся вvi
.
-
Вот как выглядит assume
для выборки по значимости:
function DynamicPPL.assume(rng, spl::Sampler{<:IS}, dist::Distribution, vn::VarName, vi)
r = rand(rng, dist)
push!(vi, vn, r, dist, spl)
return r, 0
end
Сначала функция генерирует образец r
из распределения dist
(правая часть оператора тильды). Затем она добавляет r
в vi
и возвращает r
и 0.
Функция observe
еще проще:
function DynamicPPL.observe(spl::Sampler{<:IS}, dist::Distribution, value, vi)
return logpdf(dist, value)
end
Она просто возвращает плотность (в дискретном случае — вероятность) наблюдаемого значения при распределении dist
.
4. Сводка: пошаговое выполнение алгоритма выборки по значимости
Мы сосредоточимся на функциях AbstractMCMC, которые переопределяются в is.jl
и выполняются внутри mcmcsample
: step!
, которая вызывается n_samples
раз, и sample_end!
, которая выполняется один раз после этих n_samples
итераций.
-
Во время -й итерации
step!
выполняет три операции:-
empty!!(spl.state.vi)
: удаляет информацию о предыдущем образце изVarInfo
сэмплера -
model(rng, spl.state.vi, spl)
: вызывает функцию оценки модели-
вызовы
assume
добавляют образцы из априорных распределений и вspl.state.vi
-
после вызовов
assume
илиobserve
следует строкаacclogp!!(vi, lp)
, гдеlp
— это выводassume
иobserve
-
для
lp
задается 0 послеassume
, а также значение плотности в наблюдении послеobserve
-
когда все операторы тильды выполнены,
spl.state.vi.logp[]
— это суммаlp
, т. е правдоподобие наблюдений с учетом образцов латентных переменных и .
-
-
return Transition(spl)
: строит переход из сэмплера и возвращает этот переход-
поле
vi
перехода — это простоspl.state.vi
-
поле
lp
содержит правдоподобиеspl.state.vi.logp[]
.
-
-
-
Когда итерации
n_samples
завершены,sample_end!
заполняет полеfinal_logevidence
вspl.state
-
функция просто берет логарифм среднего значения весов образцов, используя логарифмические веса для численной устойчивости.
-