# Guide

## 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. To help avoid those we provide a Turing-safe datatype `TArray`

that can be used to create mutable arrays in Turing programs.

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)
s² ~ InverseGamma(2, 3)
m ~ Normal(0, sqrt(s²))
x ~ Normal(m, sqrt(s²))
y ~ Normal(m, sqrt(s²))
end
```

Note: As a sanity check, the expectation of `s`

is 49/24 (2.04166666...) and the expectation of `m`

is 7/6 (1.16666666...).

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)
```

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 date (arg*1 = 1, arg*2 = 2) by passing (1, 2) to the model function.

```
@model function model_name(arg_1, arg_2)
...
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)
```

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)
s² ~ 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)
# 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)
s² ~ 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.

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

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()
```

Output:

```
(0.685690547873451, -1.1972706455914328)
```

### 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
s² ~ 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)
```

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)
s² ~ 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)
```

#### 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
s² ~ 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)
```

#### Access Values inside Chain

You can access the values inside a chain several ways:

- Turn them into a
`DataFrame`

object - Use their raw
`AxisArray`

form - Create a three-dimensional
`Array`

object

For example, let `c`

be a `Chain`

:

`DataFrame(c)`

converts`c`

to a`DataFrame`

,`c.value`

retrieves the values inside`c`

as an`AxisArray`

, and`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 the 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. `gdemo(x, ::Type{T} = Float64) where {T} = begin`

. Similarly, when using a particle sampler, the Julia variable used should either be: 1. A `TArray`

, or 2. An instance of some type parameter `T`

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

.

### Querying Probabilities from Model or Chain

Consider the following `gdemo`

model:

```
@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
```

The following are examples of valid queries of the `Turing`

model or chain:

`prob"x = 1.0, y = 1.0 | model = gdemo, s = 1.0, m = 1.0"`

calculates the likelihood of`x = 1`

and`y = 1`

given`s = 1`

and`m = 1`

.`prob"s = 1.0, m = 1.0 | model = gdemo, x = nothing, y = nothing"`

calculates the joint probability of`s = 1`

and`m = 1`

ignoring`x`

and`y`

.`x`

and`y`

are ignored so they can be optionally dropped from the RHS of`|`

, but it is recommended to define them.`prob"s = 1.0, m = 1.0, x = 1.0 | model = gdemo, y = nothing"`

calculates the joint probability of`s = 1`

,`m = 1`

and`x = 1`

ignoring`y`

.`prob"s = 1.0, m = 1.0, x = 1.0, y = 1.0 | model = gdemo"`

calculates the joint probability of all the variables.- After the MCMC sampling, given a
`chain`

,`prob"x = 1.0, y = 1.0 | chain = chain, model = gdemo"`

calculates the element-wise likelihood of`x = 1.0`

and`y = 1.0`

for each sample in`chain`

. - If
`save_state=true`

was used during sampling (i.e.,`sample(model, sampler, N; save_state=true)`

), you can simply do`prob"x = 1.0, y = 1.0 | chain = chain"`

.

In all the above cases, `logprob`

can be used instead of `prob`

to calculate the log probabilities instead.

### Maximum likelihood and maximum a posterior estimates

Turing provides support for two mode estimation techniques, maximum likelihood estimation (MLE) and maximum a posterior (MAP) estimation. Optimization is performed by the Optim.jl package. Mode estimation is currently a optional tool, and will not be available to you unless you have manually installed Optim and loaded the package with a `using`

statement. To install Optim, run `import Pkg; Pkg.add("Optim")`

.

Mode estimation only works when all model parameters are continuous – discrete parameters cannot be estimated with MLE/MAP as of yet.

To understand how mode estimation works, let us first load Turing and Optim to enable mode estimation, and then declare a model:

```
# Note that loading Optim explicitly is required for mode estimation to function,
# as Turing does not load the opimization suite unless Optim is loaded as well.
using Turing
using Optim
@model function gdemo(x)
s² ~ InverseGamma(2, 3)
m ~ Normal(0, sqrt(s²))
for i in eachindex(x)
x[i] ~ Normal(m, sqrt(s²))
end
end
```

Once the model is defined, we can construct a model instance as we normally would:

```
# Create some data to pass to the model.
data = [1.5, 2.0]
# Instantiate the gdemo model with our data.
model = gdemo(data)
```

Mode estimation is typically quick and easy at this point. Turing extends the function `Optim.optimize`

and accepts the structs `MLE()`

or `MAP()`

, which inform Turing whether to provide an MLE or MAP estimate, respectively. By default, the LBFGS optimizer is used, though this can be changed. Basic usage is:

```
# Generate a MLE estimate.
mle_estimate = optimize(model, MLE())
# Generate a MAP estimate.
map_estimate = optimize(model, MAP())
```

If you wish to change to a different optimizer, such as `NelderMead`

, simply place your optimizer in the third argument slot:

```
# Use NelderMead
mle_estimate = optimize(model, MLE(), NelderMead())
# Use SimulatedAnnealing
mle_estimate = optimize(model, MLE(), SimulatedAnnealing())
# Use ParticleSwarm
mle_estimate = optimize(model, MLE(), ParticleSwarm())
# Use Newton
mle_estimate = optimize(model, MLE(), Newton())
# Use AcceleratedGradientDescent
mle_estimate = optimize(model, MLE(), AcceleratedGradientDescent())
```

Some methods may have trouble calculating the mode because not enough iterations were allowed, or the target function moved upwards between function calls. Turing will warn you if Optim fails to converge by running `Optim.converge`

. A typical solution to this might be to add more iterations, or allow the optimizer to increase between function iterations:

```
# Increase the iterations and allow function eval to increase between calls.
mle_estimate = optimize(model, MLE(), Newton(), Optim.Options(iterations=10_000, allow_f_increases=true))
```

More options for Optim are available here.

#### Analyzing your mode estimate

Turing extends several methods from `StatsBase`

that can be used to analyze your mode estimation results. Methods implemented include `vcov`

, `informationmatrix`

, `coeftable`

, `params`

, and `coef`

, among others.

For example, let's examine our ML estimate from above using `coeftable`

:

```
# Import StatsBase to use it's statistical methods.
using StatsBase
# Print out the coefficient table.
coeftable(mle_estimate)
```

```
─────────────────────────────
estimate stderror tstat
─────────────────────────────
s 0.0625 0.0625 1.0
m 1.75 0.176777 9.8995
─────────────────────────────
```

Standard errors are calculated from the Fisher information matrix (inverse Hessian of the log likelihood or log joint). t-statistics will be familiar to frequentist statisticians. Warning – standard errors calculated in this way may not always be appropriate for MAP estimates, so please be cautious in interpreting them.

#### Sampling with the MAP/MLE as initial states

You can begin sampling your chain from an MLE/MAP estimate by extracting the vector of parameter values and providing it to the `sample`

function with the keyword `init_theta`

. For example, here is how to sample from the full posterior using the MAP estimate as the starting point:

```
# Generate an MAP estimate.
map_estimate = optimize(model, MAP())
# Sample with the MAP estimate as the starting point.
chain = sample(model, NUTS(), 1_000, init_theta = map_estimate.values.array)
```

## 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)
```

The `Gibbs`

sampler can be used to specify unique automatic differentation 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 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.

### Working with Libtask.jl

The Libtask.jl library provides write-on-copy data structures that are safe for use in Turing's particle-based samplers. One data structure in particular is often required for use – the `TArray`

. The following sampler types require the use of a `TArray`

to store distributions:

`IPMCMC`

`IS`

`PG`

`PMMH`

`SMC`

If you do not use a `TArray`

to store arrays of distributions when using a particle-based sampler, you may experience errors.

Here is an example of how the `TArray`

(using a `TArray`

constructor function called `tzeros`

) can be applied in this way:

```
# Turing model definition.
@model function BayesHmm(y)
# Declare a TArray with a length of N.
s = tzeros(Int, N)
m = Vector{Real}(undef, K)
T = Vector{Vector{Real}}(undef, K)
for i = 1:K
T[i] ~ Dirichlet(ones(K)/K)
m[i] ~ Normal(i, 0.01)
end
# Draw from a distribution for each element in s.
s[1] ~ Categorical(K)
for i = 2:N
s[i] ~ Categorical(vec(T[s[i-1]]))
y[i] ~ Normal(m[s[i]], 0.1)
end
return (s, m)
end;
```

### 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 manually set by `setchunksize(new_chunk_size)`

; alternatively, use an auto-tuning helper function `auto_tune_chunk_size!(mf::Function, rep_num=10)`

, which will profile various chunk sizes. Here `mf`

is the model function, e.g. `gdemo(1.5, 2)`

, and `rep_num`

is the number of repetitions during profiling.

#### AD Backend

Turing supports four packages of automatic differentiation (AD) 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 progress of sampling. 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`

, respectively. 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 Juno, the progress is displayed with a progress bar in the Atom window. For 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.