Core Functionality

This article provides an overview of the core functionality in Turing.jl, which are likely to be used across a wide range of models.

Basics

Introduction

A probabilistic program is Julia code wrapped in a @model macro. It can use arbitrary Julia code, but to ensure correctness of inference it should not have external effects or modify global state. Stack-allocated variables are safe, but mutable heap-allocated objects may lead to subtle bugs when using task copying. By default Libtask deepcopies Array and Dict objects when copying task to avoid bugs with data stored in mutable structure in Turing models.

To specify distributions of random variables, Turing programs should use the ~ notation:

x ~ distr where x is a symbol and distr is a distribution. If x is undefined in the model function, inside the probabilistic program, this puts a random variable named x, distributed according to distr, in the current scope. distr can be a value of any type that implements rand(distr), which samples a value from the distribution distr. If x is defined, this is used for conditioning in a style similar to Anglican (another PPL). In this case, x is an observed value, assumed to have been drawn from the distribution distr. The likelihood is computed using logpdf(distr,y). The observe statements should be arranged so that every possible run traverses all of them in exactly the same order. This is equivalent to demanding that they are not placed inside stochastic control flow.

Available inference methods include Importance Sampling (IS), Sequential Monte Carlo (SMC), Particle Gibbs (PG), Hamiltonian Monte Carlo (HMC), Hamiltonian Monte Carlo with Dual Averaging (HMCDA) and The No-U-Turn Sampler (NUTS).

Simple Gaussian Demo

Below is a simple Gaussian demo illustrate the basic usage of Turing.jl.

# Import packages.
using Turing
using StatsPlots

# Define a simple Normal model with unknown mean and variance.
@model function gdemo(x, y)
~ InverseGamma(2, 3)
    m ~ Normal(0, sqrt(s²))
    x ~ Normal(m, sqrt(s²))
    return y ~ Normal(m, sqrt(s²))
end
gdemo (generic function with 2 methods)

Note: As a sanity check, the prior expectation of is mean(InverseGamma(2, 3)) = 3/(2 - 1) = 3 and the prior expectation of m is 0. This can be easily checked using Prior:

setprogress!(false)
p1 = sample(gdemo(missing, missing), Prior(), 100000)
Chains MCMC chain (100000×5×1 Array{Float64, 3}):

Iterations        = 1:1:100000
Number of chains  = 1
Samples per chain = 100000
Wall duration     = 1.59 seconds
Compute duration  = 1.59 seconds
parameters        = s², m, x, y
internals         = lp

Summary Statistics
  parameters      mean       std      mcse      ess_bulk     ess_tail      rha ⋯
      Symbol   Float64   Float64   Float64       Float64      Float64   Float6 ⋯

          s²    2.9919    7.4094    0.0235    98384.6455   99053.2196    1.000 ⋯
           m    0.0029    1.7321    0.0055   100087.6129   99287.1957    1.000 ⋯
           x    0.0073    2.4562    0.0078   100142.4909   99210.4786    1.000 ⋯
           y    0.0113    2.4796    0.0078   100788.3866   99373.8813    1.000 ⋯
                                                               2 columns omitted

Quantiles
  parameters      2.5%     25.0%     50.0%     75.0%     97.5%
      Symbol   Float64   Float64   Float64   Float64   Float64

          s²    0.5373    1.1149    1.7904    3.1018   12.4757
           m   -3.3867   -0.9041    0.0100    0.9115    3.3866
           x   -4.8049   -1.2816    0.0101    1.2931    4.8532
           y   -4.7914   -1.2718    0.0062    1.2922    4.8389

We can perform inference by using the sample function, the first argument of which is our probabilistic program and the second of which is a sampler. More information on each sampler is located in the API.

#  Run sampler, collect results.
c1 = sample(gdemo(1.5, 2), SMC(), 1000)
c2 = sample(gdemo(1.5, 2), PG(10), 1000)
c3 = sample(gdemo(1.5, 2), HMC(0.1, 5), 1000)
c4 = sample(gdemo(1.5, 2), Gibbs(PG(10, :m), HMC(0.1, 5, :s²)), 1000)
c5 = sample(gdemo(1.5, 2), HMCDA(0.15, 0.65), 1000)
c6 = sample(gdemo(1.5, 2), NUTS(0.65), 1000)
┌ Info: Found initial step size
└   ϵ = 1.6
┌ Info: Found initial step size
└   ϵ = 0.8
Chains MCMC chain (1000×14×1 Array{Float64, 3}):

Iterations        = 501:1:1500
Number of chains  = 1
Samples per chain = 1000
Wall duration     = 2.0 seconds
Compute duration  = 2.0 seconds
parameters        = s², m
internals         = lp, n_steps, is_accept, acceptance_rate, log_density, hamiltonian_energy, hamiltonian_energy_error, max_hamiltonian_energy_error, tree_depth, numerical_error, step_size, nom_step_size

Summary Statistics
  parameters      mean       std      mcse   ess_bulk   ess_tail      rhat   e ⋯
      Symbol   Float64   Float64   Float64    Float64    Float64   Float64     ⋯

          s²    1.9383    1.3971    0.0842   332.4439   455.1277    1.0000     ⋯
           m    1.2405    0.8169    0.0357   535.6918   458.0631    0.9991     ⋯
                                                                1 column omitted

Quantiles
  parameters      2.5%     25.0%     50.0%     75.0%     97.5%
      Symbol   Float64   Float64   Float64   Float64   Float64

          s²    0.6119    1.0666    1.5542    2.3650    5.7819
           m   -0.3825    0.7231    1.1910    1.7231    2.8908

The MCMCChains module (which is re-exported by Turing) provides plotting tools for the Chain objects returned by a sample function. See the MCMCChains repository for more information on the suite of tools available for diagnosing MCMC chains.

# Summarise results
describe(c3)

# Plot results
plot(c3)
savefig("gdemo-plot.png")

The arguments for each sampler are:

  • SMC: number of particles.
  • PG: number of particles, number of iterations.
  • HMC: leapfrog step size, leapfrog step numbers.
  • Gibbs: component sampler 1, component sampler 2, …
  • HMCDA: total leapfrog length, target accept ratio.
  • NUTS: number of adaptation steps (optional), target accept ratio.

For detailed information on the samplers, please review Turing.jl’s API documentation.

Modelling Syntax Explained

Using this syntax, a probabilistic model is defined in Turing. The model function generated by Turing can then be used to condition the model onto data. Subsequently, the sample function can be used to generate samples from the posterior distribution.

In the following example, the defined model is conditioned to the data (arg1 = 1, arg2 = 2) by passing (1, 2) to the model function.

@model function model_name(arg_1, arg_2)
    return ...
end

The conditioned model can then be passed onto the sample function to run posterior inference.

model_func = model_name(1, 2)
chn = sample(model_func, HMC(..)) # Perform inference by sampling using HMC.

The returned chain contains samples of the variables in the model.

var_1 = mean(chn[:var_1]) # Taking the mean of a variable named var_1.

The key (:var_1) can be a Symbol or a String. For example, to fetch x[1], one can use chn[Symbol("x[1]")] or chn["x[1]"]. If you want to retrieve all parameters associated with a specific symbol, you can use group. As an example, if you have the parameters "x[1]", "x[2]", and "x[3]", calling group(chn, :x) or group(chn, "x") will return a new chain with only "x[1]", "x[2]", and "x[3]".

Turing does not have a declarative form. More generally, the order in which you place the lines of a @model macro matters. For example, the following example works:

# Define a simple Normal model with unknown mean and variance.
@model function model_function(y)
    s ~ Poisson(1)
    y ~ Normal(s, 1)
    return y
end

sample(model_function(10), SMC(), 100)
Chains MCMC chain (100×3×1 Array{Float64, 3}):

Log evidence      = -27.943630147637165
Iterations        = 1:1:100
Number of chains  = 1
Samples per chain = 100
Wall duration     = 1.9 seconds
Compute duration  = 1.9 seconds
parameters        = s
internals         = lp, weight

Summary Statistics
  parameters      mean       std      mcse   ess_bulk   ess_tail      rhat   e ⋯
      Symbol   Float64   Float64   Float64    Float64    Float64   Float64     ⋯

           s    3.0000    0.0000       NaN        NaN        NaN       NaN     ⋯
                                                                1 column omitted

Quantiles
  parameters      2.5%     25.0%     50.0%     75.0%     97.5%
      Symbol   Float64   Float64   Float64   Float64   Float64

           s    3.0000    3.0000    3.0000    3.0000    3.0000

But if we switch the s ~ Poisson(1) and y ~ Normal(s, 1) lines, the model will no longer sample correctly:

# Define a simple Normal model with unknown mean and variance.
@model function model_function(y)
    y ~ Normal(s, 1)
    s ~ Poisson(1)
    return y
end

sample(model_function(10), SMC(), 100)

Sampling Multiple Chains

Turing supports distributed and threaded parallel sampling. To do so, call sample(model, sampler, parallel_type, n, n_chains), where parallel_type can be either MCMCThreads() or MCMCDistributed() for thread and parallel sampling, respectively.

Having multiple chains in the same object is valuable for evaluating convergence. Some diagnostic functions like gelmandiag require multiple chains.

If you do not want parallelism or are on an older version Julia, you can sample multiple chains with the mapreduce function:

# Replace num_chains below with however many chains you wish to sample.
chains = mapreduce(c -> sample(model_fun, sampler, 1000), chainscat, 1:num_chains)

The chains variable now contains a Chains object which can be indexed by chain. To pull out the first chain from the chains object, use chains[:,:,1]. The method is the same if you use either of the below parallel sampling methods.

Multithreaded sampling

If you wish to perform multithreaded sampling and are running Julia 1.3 or greater, you can call sample with the following signature:

using Turing

@model function gdemo(x)
~ InverseGamma(2, 3)
    m ~ Normal(0, sqrt(s²))

    for i in eachindex(x)
        x[i] ~ Normal(m, sqrt(s²))
    end
end

model = gdemo([1.5, 2.0])

# Sample four chains using multiple threads, each with 1000 samples.
sample(model, NUTS(), MCMCThreads(), 1000, 4)

Be aware that Turing cannot add threads for you – you must have started your Julia instance with multiple threads to experience any kind of parallelism. See the Julia documentation for details on how to achieve this.

Distributed sampling

To perform distributed sampling (using multiple processes), you must first import Distributed.

Process parallel sampling can be done like so:

# Load Distributed to add processes and the @everywhere macro.
using Distributed

# Load Turing.
using Turing

# Add four processes to use for sampling.
addprocs(4; exeflags="--project=$(Base.active_project())")

# Initialize everything on all the processes.
# Note: Make sure to do this after you've already loaded Turing,
#       so each process does not have to precompile.
#       Parallel sampling may fail silently if you do not do this.
@everywhere using Turing

# Define a model on all processes.
@everywhere @model function gdemo(x)
~ InverseGamma(2, 3)
    m ~ Normal(0, sqrt(s²))

    for i in eachindex(x)
        x[i] ~ Normal(m, sqrt(s²))
    end
end

# Declare the model instance everywhere.
@everywhere model = gdemo([1.5, 2.0])

# Sample four chains using multiple processes, each with 1000 samples.
sample(model, NUTS(), MCMCDistributed(), 1000, 4)

Sampling from an Unconditional Distribution (The Prior)

Turing allows you to sample from a declared model’s prior. If you wish to draw a chain from the prior to inspect your prior distributions, you can simply run

chain = sample(model, Prior(), n_samples)

You can also run your model (as if it were a function) from the prior distribution, by calling the model without specifying inputs or a sampler. In the below example, we specify a gdemo model which returns two variables, x and y. The model includes x and y as arguments, but calling the function without passing in x or y means that Turing’s compiler will assume they are missing values to draw from the relevant distribution. The return statement is necessary to retrieve the sampled x and y values. Assign the function with missing inputs to a variable, and Turing will produce a sample from the prior distribution.

@model function gdemo(x, y)
~ InverseGamma(2, 3)
    m ~ Normal(0, sqrt(s²))
    x ~ Normal(m, sqrt(s²))
    y ~ Normal(m, sqrt(s²))
    return x, y
end
gdemo (generic function with 2 methods)

Assign the function with missing inputs to a variable, and Turing will produce a sample from the prior distribution.

# Samples from p(x,y)
g_prior_sample = gdemo(missing, missing)
g_prior_sample()
(-3.657010514967226, -3.98468964634056)

Sampling from a Conditional Distribution (The Posterior)

Treating observations as random variables

Inputs to the model that have a value missing are treated as parameters, aka random variables, to be estimated/sampled. This can be useful if you want to simulate draws for that parameter, or if you are sampling from a conditional distribution. Turing supports the following syntax:

@model function gdemo(x, ::Type{T}=Float64) where {T}
    if x === missing
        # Initialize `x` if missing
        x = Vector{T}(undef, 2)
    end
~ InverseGamma(2, 3)
    m ~ Normal(0, sqrt(s²))
    for i in eachindex(x)
        x[i] ~ Normal(m, sqrt(s²))
    end
end

# Construct a model with x = missing
model = gdemo(missing)
c = sample(model, HMC(0.01, 5), 500)
Chains MCMC chain (500×14×1 Array{Float64, 3}):

Iterations        = 1:1:500
Number of chains  = 1
Samples per chain = 500
Wall duration     = 3.51 seconds
Compute duration  = 3.51 seconds
parameters        = s², m, x[1], x[2]
internals         = lp, n_steps, is_accept, acceptance_rate, log_density, hamiltonian_energy, hamiltonian_energy_error, numerical_error, step_size, nom_step_size

Summary Statistics
  parameters      mean       std      mcse   ess_bulk   ess_tail      rhat   e ⋯
      Symbol   Float64   Float64   Float64    Float64    Float64   Float64     ⋯

          s²    0.7730    0.3002    0.2384     1.5148    10.9080    1.8174     ⋯
           m    0.3313    0.1833    0.0517    13.1240    31.8787    1.1245     ⋯
        x[1]   -0.0504    0.4485    0.1939     6.5455    11.3031    1.6894     ⋯
        x[2]    0.4198    0.3864    0.3154     1.6319    21.5169    1.7044     ⋯
                                                                1 column omitted

Quantiles
  parameters      2.5%     25.0%     50.0%     75.0%     97.5%
      Symbol   Float64   Float64   Float64   Float64   Float64

          s²    0.3263    0.5520    0.7370    0.9054    1.5563
           m   -0.0152    0.2060    0.3395    0.4561    0.6956
        x[1]   -0.9285   -0.4135    0.0654    0.2613    0.7371
        x[2]   -0.1491    0.0847    0.3848    0.7682    1.1318

Note the need to initialize x when missing since we are iterating over its elements later in the model. The generated values for x can be extracted from the Chains object using c[:x].

Turing also supports mixed missing and non-missing values in x, where the missing ones will be treated as random variables to be sampled while the others get treated as observations. For example:

@model function gdemo(x)
~ InverseGamma(2, 3)
    m ~ Normal(0, sqrt(s²))
    for i in eachindex(x)
        x[i] ~ Normal(m, sqrt(s²))
    end
end

# x[1] is a parameter, but x[2] is an observation
model = gdemo([missing, 2.4])
c = sample(model, HMC(0.01, 5), 500)
Chains MCMC chain (500×13×1 Array{Float64, 3}):

Iterations        = 1:1:500
Number of chains  = 1
Samples per chain = 500
Wall duration     = 2.12 seconds
Compute duration  = 2.12 seconds
parameters        = s², m, x[1]
internals         = lp, n_steps, is_accept, acceptance_rate, log_density, hamiltonian_energy, hamiltonian_energy_error, numerical_error, step_size, nom_step_size

Summary Statistics
  parameters      mean       std      mcse   ess_bulk   ess_tail      rhat   e ⋯
      Symbol   Float64   Float64   Float64    Float64    Float64   Float64     ⋯

          s²    2.2259    0.6641    0.2610     6.4484    16.7553    1.1022     ⋯
           m    0.1795    0.2438    0.0759    10.4471    26.2981    1.0543     ⋯
        x[1]   -0.1662    0.3690    0.1421     7.4953    41.2461    1.0248     ⋯
                                                                1 column omitted

Quantiles
  parameters      2.5%     25.0%     50.0%     75.0%     97.5%
      Symbol   Float64   Float64   Float64   Float64   Float64

          s²    1.0903    1.8066    2.2125    2.5629    3.6351
           m   -0.3695    0.0600    0.2108    0.3454    0.6120
        x[1]   -0.8842   -0.4710   -0.1136    0.1575    0.3638

Default Values

Arguments to Turing models can have default values much like how default values work in normal Julia functions. For instance, the following will assign missing to x and treat it as a random variable. If the default value is not missing, x will be assigned that value and will be treated as an observation instead.

using Turing

@model function generative(x=missing, ::Type{T}=Float64) where {T<:Real}
    if x === missing
        # Initialize x when missing
        x = Vector{T}(undef, 10)
    end
~ InverseGamma(2, 3)
    m ~ Normal(0, sqrt(s²))
    for i in 1:length(x)
        x[i] ~ Normal(m, sqrt(s²))
    end
    return s², m
end

m = generative()
chain = sample(m, HMC(0.01, 5), 1000)
Chains MCMC chain (1000×22×1 Array{Float64, 3}):

Iterations        = 1:1:1000
Number of chains  = 1
Samples per chain = 1000
Wall duration     = 2.94 seconds
Compute duration  = 2.94 seconds
parameters        = s², m, x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10]
internals         = lp, n_steps, is_accept, acceptance_rate, log_density, hamiltonian_energy, hamiltonian_energy_error, numerical_error, step_size, nom_step_size

Summary Statistics
  parameters      mean       std      mcse   ess_bulk   ess_tail      rhat   e ⋯
      Symbol   Float64   Float64   Float64    Float64    Float64   Float64     ⋯

          s²    3.4818    1.1380    0.4333     7.5363    31.2133    1.2207     ⋯
           m   -2.4871    0.5343    0.1711    10.3024    12.8740    1.0047     ⋯
        x[1]   -6.7516    0.7408    0.4690     2.6395    20.4981    1.9716     ⋯
        x[2]   -4.1522    0.2829    0.1360     4.8182    34.4280    1.3620     ⋯
        x[3]   -0.4997    0.6574    0.3676     3.7249    23.5060    1.4306     ⋯
        x[4]   -0.7948    0.6227    0.3740     2.9156    20.5942    1.9006     ⋯
        x[5]   -1.1705    0.5416    0.3371     2.7845    20.6255    1.8404     ⋯
        x[6]   -1.1076    0.3721    0.1797     4.3225    26.3191    1.4044     ⋯
        x[7]   -2.4813    0.8850    0.5872     2.5392    20.7337    2.1166     ⋯
        x[8]   -3.4257    0.6130    0.3381     3.5346    15.6032    1.3788     ⋯
        x[9]   -1.4135    0.5689    0.2521     6.2088    20.3984    1.0965     ⋯
       x[10]   -3.2352    0.7397    0.4630     2.6751    13.3469    1.8548     ⋯
                                                                1 column omitted

Quantiles
  parameters      2.5%     25.0%     50.0%     75.0%     97.5%
      Symbol   Float64   Float64   Float64   Float64   Float64

          s²    1.9357    2.6449    3.2676    4.2282    6.3107
           m   -3.4230   -2.8256   -2.5240   -2.1866   -1.3496
        x[1]   -8.0919   -7.2838   -6.8218   -6.1752   -5.4486
        x[2]   -4.6989   -4.3948   -4.1116   -3.9286   -3.6730
        x[3]   -1.5836   -0.9980   -0.6686    0.1223    0.6372
        x[4]   -2.0528   -1.2240   -0.7970   -0.3023    0.2271
        x[5]   -2.2711   -1.6440   -1.0281   -0.7538   -0.3514
        x[6]   -1.6993   -1.3890   -1.1251   -0.8718   -0.3491
        x[7]   -3.8563   -3.4538   -2.2519   -1.7969   -1.1032
        x[8]   -4.6541   -3.7568   -3.5282   -3.1336   -2.1946
        x[9]   -2.7872   -1.6859   -1.3168   -1.0081   -0.6232
       x[10]   -4.2696   -3.8577   -3.3356   -2.8950   -1.6689

Access Values inside Chain

You can access the values inside a chain several ways:

  1. Turn them into a DataFrame object
  2. Use their raw AxisArray form
  3. Create a three-dimensional Array object

For example, let c be a Chain:

  1. DataFrame(c) converts c to a DataFrame,
  2. c.value retrieves the values inside c as an AxisArray, and
  3. c.value.data retrieves the values inside c as a 3D Array.

Variable Types and Type Parameters

The element type of a vector (or matrix) of random variables should match the eltype of its prior distribution, <: Integer for discrete distributions and <: AbstractFloat for continuous distributions. Moreover, if the continuous random variable is to be sampled using a Hamiltonian sampler, the vector’s element type needs to either be:

  1. Real to enable auto-differentiation through the model which uses special number types that are sub-types of Real, or

  2. Some type parameter T defined in the model header using the type parameter syntax, e.g. function gdemo(x, ::Type{T} = Float64) where {T}.

Similarly, when using a particle sampler, the Julia variable used should either be:

  1. An Array, or

  2. An instance of some type parameter T defined in the model header using the type parameter syntax, e.g. function gdemo(x, ::Type{T} = Vector{Float64}) where {T}.

Querying Probabilities from Model or Chain

Turing offers three functions: loglikelihood, logprior, and logjoint to query the log-likelihood, log-prior, and log-joint probabilities of a model, respectively.

Let’s look at a simple model called gdemo:

@model function gdemo0()
    s ~ InverseGamma(2, 3)
    m ~ Normal(0, sqrt(s))
    return x ~ Normal(m, sqrt(s))
end
gdemo0 (generic function with 2 methods)

If we observe x to be 1.0, we can condition the model on this datum using the condition syntax:

model = gdemo0() | (x=1.0,)
DynamicPPL.Model{typeof(gdemo0), (), (), (), Tuple{}, Tuple{}, DynamicPPL.ConditionContext{@NamedTuple{x::Float64}, DynamicPPL.DefaultContext}}(Main.Notebook.gdemo0, NamedTuple(), NamedTuple(), ConditionContext((x = 1.0,), DynamicPPL.DefaultContext()))

Now, let’s compute the log-likelihood of the observation given specific values of the model parameters, s and m:

loglikelihood(model, (s=1.0, m=1.0))
-0.9189385332046728

We can easily verify that value in this case:

logpdf(Normal(1.0, 1.0), 1.0)
-0.9189385332046728

We can also compute the log-prior probability of the model for the same values of s and m:

logprior(model, (s=1.0, m=1.0))
-2.221713955868453
logpdf(InverseGamma(2, 3), 1.0) + logpdf(Normal(0, sqrt(1.0)), 1.0)
-2.221713955868453

Finally, we can compute the log-joint probability of the model parameters and data:

logjoint(model, (s=1.0, m=1.0))
-3.1406524890731258
logpdf(Normal(1.0, 1.0), 1.0) +
logpdf(InverseGamma(2, 3), 1.0) +
logpdf(Normal(0, sqrt(1.0)), 1.0)
-3.1406524890731258

Querying with Chains object is easy as well:

chn = sample(model, Prior(), 10)
Chains MCMC chain (10×3×1 Array{Float64, 3}):

Iterations        = 1:1:10
Number of chains  = 1
Samples per chain = 10
Wall duration     = 0.02 seconds
Compute duration  = 0.02 seconds
parameters        = s, m
internals         = lp

Summary Statistics
  parameters      mean       std      mcse   ess_bulk   ess_tail      rhat   e ⋯
      Symbol   Float64   Float64   Float64    Float64    Float64   Float64     ⋯

           s    3.1065    2.6577    0.8404    10.0000    10.0000    0.9963     ⋯
           m    0.5369    2.2984    0.9810     5.1372    10.0000    1.4156     ⋯
                                                                1 column omitted

Quantiles
  parameters      2.5%     25.0%     50.0%     75.0%     97.5%
      Symbol   Float64   Float64   Float64   Float64   Float64

           s    1.0906    1.6586    2.1190    3.2984    8.8175
           m   -1.6813   -1.1732    0.0161    1.3439    5.0373
loglikelihood(model, chn)
10×1 Matrix{Float64}:
 -2.4784254879914696
 -1.6284759112007414
 -4.173413317315328
 -1.1739081872577541
 -1.223317164985711
 -1.746467140016372
 -2.447932879651244
 -2.401940285782973
 -2.7734953801334137
 -1.367742272168727

Maximum likelihood and maximum a posterior estimates

Turing also has functions for estimating the maximum aposteriori and maximum likelihood parameters of a model. This can be done with

mle_estimate = maximum_likelihood(model)
map_estimate = maximum_a_posteriori(model)
ModeResult with maximized lp of -2.81
[0.8125000008508182, 0.5000000002816697]

For more details see the mode estimation page.

Beyond the Basics

Compositional Sampling Using Gibbs

Turing.jl provides a Gibbs interface to combine different samplers. For example, one can combine an HMC sampler with a PG sampler to run inference for different parameters in a single model as below.

@model function simple_choice(xs)
    p ~ Beta(2, 2)
    z ~ Bernoulli(p)
    for i in 1:length(xs)
        if z == 1
            xs[i] ~ Normal(0, 1)
        else
            xs[i] ~ Normal(2, 1)
        end
    end
end

simple_choice_f = simple_choice([1.5, 2.0, 0.3])

chn = sample(simple_choice_f, Gibbs(HMC(0.2, 3, :p), PG(20, :z)), 1000)
Chains MCMC chain (1000×3×1 Array{Float64, 3}):

Iterations        = 1:1:1000
Number of chains  = 1
Samples per chain = 1000
Wall duration     = 13.4 seconds
Compute duration  = 13.4 seconds
parameters        = p, z
internals         = lp

Summary Statistics
  parameters      mean       std      mcse   ess_bulk   ess_tail      rhat   e ⋯
      Symbol   Float64   Float64   Float64    Float64    Float64   Float64     ⋯

           p    0.4314    0.2150    0.0251    69.7360    82.6593    1.0078     ⋯
           z    0.1690    0.3749    0.0167   504.1618        NaN    0.9999     ⋯
                                                                1 column omitted

Quantiles
  parameters      2.5%     25.0%     50.0%     75.0%     97.5%
      Symbol   Float64   Float64   Float64   Float64   Float64

           p    0.0527    0.2699    0.4113    0.5842    0.8655
           z    0.0000    0.0000    0.0000    0.0000    1.0000

The Gibbs sampler can be used to specify unique automatic differentiation backends for different variable spaces. Please see the Automatic Differentiation article for more.

For more details of compositional sampling in Turing.jl, please check the corresponding paper.

Working with filldist and arraydist

Turing provides filldist(dist::Distribution, n::Int) and arraydist(dists::AbstractVector{<:Distribution}) as a simplified interface to construct product distributions, e.g., to model a set of variables that share the same structure but vary by group.

Constructing product distributions with filldist

The function filldist provides a general interface to construct product distributions over distributions of the same type and parameterisation. Note that, in contrast to the product distribution interface provided by Distributions.jl (Product), filldist supports product distributions over univariate or multivariate distributions.

Example usage:

@model function demo(x, g)
    k = length(unique(g))
    a ~ filldist(Exponential(), k) # = Product(fill(Exponential(), k))
    mu = a[g]
    return x .~ Normal.(mu)
end
demo (generic function with 2 methods)

Constructing product distributions with arraydist

The function arraydist provides a general interface to construct product distributions over distributions of varying type and parameterisation. Note that in contrast to the product distribution interface provided by Distributions.jl (Product), arraydist supports product distributions over univariate or multivariate distributions.

Example usage:

@model function demo(x, g)
    k = length(unique(g))
    a ~ arraydist([Exponential(i) for i in 1:k])
    mu = a[g]
    return x .~ Normal.(mu)
end
demo (generic function with 2 methods)

Working with MCMCChains.jl

Turing.jl wraps its samples using MCMCChains.Chain so that all the functions working for MCMCChains.Chain can be re-used in Turing.jl. Two typical functions are MCMCChains.describe and MCMCChains.plot, which can be used as follows for an obtained chain chn. For more information on MCMCChains, please see the GitHub repository.

describe(chn) # Lists statistics of the samples.
plot(chn) # Plots statistics of the samples.

There are numerous functions in addition to describe and plot in the MCMCChains package, such as those used in convergence diagnostics. For more information on the package, please see the GitHub repository.

Changing Default Settings

Some of Turing.jl’s default settings can be changed for better usage.

AD Chunk Size

ForwardDiff (Turing’s default AD backend) uses forward-mode chunk-wise AD. The chunk size can be set manually by AutoForwardDiff(;chunksize=new_chunk_size).

AD Backend

Turing supports four automatic differentiation (AD) packages in the back end during sampling. The default AD backend is ForwardDiff for forward-mode AD. Three reverse-mode AD backends are also supported, namely Tracker, Zygote and ReverseDiff. Zygote and ReverseDiff are supported optionally if explicitly loaded by the user with using Zygote or using ReverseDiff next to using Turing.

For more information on Turing’s automatic differentiation backend, please see the Automatic Differentiation article.

Progress Logging

Turing.jl uses ProgressLogging.jl to log the sampling progress. Progress logging is enabled as default but might slow down inference. It can be turned on or off by setting the keyword argument progress of sample to true or false. Moreover, you can enable or disable progress logging globally by calling setprogress!(true) or setprogress!(false), respectively.

Turing uses heuristics to select an appropriate visualization backend. If you use Jupyter notebooks, the default backend is ConsoleProgressMonitor.jl. In all other cases, progress logs are displayed with TerminalLoggers.jl. Alternatively, if you provide a custom visualization backend, Turing uses it instead of the default backend.

Back to top