Advanced Usage
How to Define a Customized Distribution
Turing.jl supports the use of distributions from the Distributions.jl package. By extension it also supports the use of customized distributions, by defining them as subtypes of Distribution
type of the Distributions.jl package, as well as corresponding functions.
Below shows a workflow of how to define a customized distribution, using a flat prior as a simple example.
1. Define the Distribution Type
First, define a type of the distribution, as a subtype of a corresponding distribution type in the Distributions.jl package.
struct Flat <: ContinuousUnivariateDistribution
…
end
2. Implement Sampling and Evaluation of the log-pdf
Second, define rand()
and logpdf()
, which will be used to run the model.
Distributions.rand(d::Flat) = rand()
Distributions.logpdf{T<:Real}(d::Flat, x::T) = zero(x)
3. Define Helper Functions
In most cases, it may be required to define helper functions, such as the minimum
, maximum
, rand
, and logpdf
functions, among others.
3.1 Domain Transformation
Some helper functions are necessary for domain transformation. For univariate distributions, the necessary ones to implement are minimum()
and maximum()
.
Distributions.minimum(d::Flat) = -Inf
Distributions.maximum(d::Flat) = +Inf
Functions for domain transformation which may be required by multivariate or matrix-variate distributions are size(d)
, link(d, x)
and invlink(d, x)
. Please see Turing’s transform.jl
for examples.
3.2 Vectorization Support
The vectorization syntax follows rv ~ [distribution]
, which requires rand()
and logpdf()
to be called on multiple data points at once. An appropriate implementation for Flat
are shown below.
Distributions.rand(d::Flat, n::Int) = Vector([rand() for _ = 1:n])
Distributions.logpdf{T<:Real}(d::Flat, x::Vector{T}) = zero(x)
Model Internals
The @model
macro accepts a function definition and generates a Turing.Model
struct for use by the sampler. Models can be constructed by hand without the use of a macro. Taking the gdemo
model as an example, the two code sections below (macro and macro-free) are equivalent.
using Turing
@model gdemo(x) = begin
# Set priors.
s ~ InverseGamma(2,3)
m ~ Normal(0,sqrt(s))
# Observe each value of x.
[x ~ Normal(m, sqrt(s))]
end
sample(gdemo([1.5, 2.0]), HMC(1000, 0.1, 5))
using Turing
# Initialize a NamedTuple containing our data variables.
data = (x = [1.5, 2.0],)
# Create the model function.
mf(vi, sampler, model) = begin
# Set the accumulated logp to zero.
vi.logp = 0
# If x is provided, use the provided values.
# Otherwise, treat x as an empty vector with
# two entries.
if isdefined(model.data, :x)
x = model.data.x
else # x is a parameter
x = model.defaults.x
end
# Assume s has an InverseGamma distribution.
s, lp = Turing.assume(sampler,
InverseGamma(2, 3),
Turing.VarName(vi, [:c_s, :s], ""), vi)
# Add the lp to the accumulated logp.
vi.logp += lp
# Assume m has a Normal distribution.
m, lp = Turing.assume(sampler,
Normal(0,sqrt(s)),
Turing.VarName(vi, [:c_m, :m], ""), vi)
# Add the lp to the accumulated logp.
vi.logp += lp
# Observe each value of x[i], according to a
# Normal distribution.
for i = 1:length(x)
vi.logp += Turing.observe(sampler,
Normal(m, sqrt(s)),
x[i], vi)
end
end
# Define the default value for x when missing
defaults = (x = Vector{Real}(undef, 2),)
# Instantiate a Model object.
model = Turing.Model{Tuple{:s, :m}, Tuple{:x}}(mf, data, defaults)
# Sample the model.
chain = sample(model, HMC(1000, 0.1, 5))
Note that the Turing.Model{Tuple{:s, :m}, Tuple{:x, :y}}
accepts two parameter tuples. The first set, Tuple{:s, :m}
, represents parameter variables that will be generated by the model, while the second (Tuple{:x}
) contains the variables to be observed.
Task Copying
Turing copies Julia tasks to deliver efficient inference algorithms, but it also provides alternative slower implementation as a fallback. Task copying is enabled by default. Task copying requires we use the CTask
facility which is provided by Libtask to create tasks.
Maximum a Posteriori Estimation
Turing does not currently have built-in methods for calculating the maximum a posteriori (MAP) for a model. This is a goal for Turing’s implementation (see this issue), but for the moment, we present here a method for estimating the MAP using Optim.jl.
using Turing
# Define the simple gdemo model.
@model gdemo(x, y) = begin
s ~ InverseGamma(2,3)
m ~ Normal(0,sqrt(s))
x ~ Normal(m, sqrt(s))
y ~ Normal(m, sqrt(s))
return s, m
end
# Define our data points.
x = 1.5
y = 2.0
# Set up the model call, sample from the prior.
model = gdemo(x, y)
vi = Turing.VarInfo()
model(vi, Turing.SampleFromPrior())
# Define a function to optimize.
function nlogp(sm)
vi.vals .= sm
model(vi, Turing.SampleFromPrior())
-vi.logp
end
# Import Optim.jl.
using Optim
# Create a starting point, call the optimizer.
sm_0 = Float64.(vi.vals)
lb = [0.0, -Inf]
ub = [Inf, Inf]
result = optimize(nlogp, lb, ub, sm_0, Fminbox())