Tracking Extra Quantities

Often, there are quantities in models that we might be interested in viewing the values of, but which are not random variables in the model that are explicitly drawn from a distribution.

As a motivating example, the most natural parameterisation for a model might not be the most computationally feasible. Consider the following (efficiently reparametrized) implementation of Neal’s funnel (Neal, 2003):

using Turing
setprogress!(false)

@model function Neal()
    # Raw draws
    y_raw ~ Normal(0, 1)
    x_raw ~ arraydist([Normal(0, 1) for i in 1:9])

    # Transform:
    y = 3 * y_raw
    x = exp.(y ./ 2) .* x_raw
    return nothing
end
[ Info: [Turing]: progress logging is disabled globally
Neal (generic function with 2 methods)

In this case, the random variables exposed in the chain (x_raw, y_raw) are not in a helpful form — what we’re after are the deterministically transformed variables x and y.

There are two ways to track these extra quantities in Turing.jl.

Using := (during inference)

The first way is to use the := operator, which behaves exactly like = except that the values of the variables on its left-hand side are automatically added to the chain returned by the sampler. For example:

@model function Neal_coloneq()
    # Raw draws
    y_raw ~ Normal(0, 1)
    x_raw ~ arraydist([Normal(0, 1) for i in 1:9])

    # Transform:
    y := 3 * y_raw
    x := exp.(y ./ 2) .* x_raw
end

sample(Neal_coloneq(), NUTS(), 1000)
Info: Found initial step size
  ϵ = 1.6
╭─FlexiChain (1000 iterations, 1 chain) ───────────────────────────────────────
 ↓ iter  = 501:1500                                                           
 → chain = 1:1                                                                
                                                                              
 Parameters (4) ── AbstractPPL.VarName                                        
  Float64          y_raw, y                                                   
  Vector{Float64}  x_raw, x                                                   
                                                                              
 Extras (14)                                                                  
  Int64    n_steps, tree_depth                                                
  Bool     is_accept, numerical_error                                         
  Float64  acceptance_rate, log_density, hamiltonian_energy,                  
           hamiltonian_energy_error, max_hamiltonian_energy_error, step_size, 
           nom_step_size, logprior, loglikelihood, logjoint                   
╰──────────────────────────────────────────────────────────────────────────────╯

Using returned (post-inference)

Alternatively, one can specify the extra quantities as part of the model function’s return statement:

@model function Neal_return()
    # Raw draws
    y_raw ~ Normal(0, 1)
    x_raw ~ arraydist([Normal(0, 1) for i in 1:9])

    # Transform and return as a NamedTuple
    y = 3 * y_raw
    x = exp.(y ./ 2) .* x_raw
    return (x=x, y=y)
end

chain = sample(Neal_return(), NUTS(), 1000)
Info: Found initial step size
  ϵ = 1.6
╭─FlexiChain (1000 iterations, 1 chain) ───────────────────────────────────────
 ↓ iter  = 501:1500                                                           
 → chain = 1:1                                                                
                                                                              
 Parameters (2) ── AbstractPPL.VarName                                        
  Float64          y_raw                                                      
  Vector{Float64}  x_raw                                                      
                                                                              
 Extras (14)                                                                  
  Int64    n_steps, tree_depth                                                
  Bool     is_accept, numerical_error                                         
  Float64  acceptance_rate, log_density, hamiltonian_energy,                  
           hamiltonian_energy_error, max_hamiltonian_energy_error, step_size, 
           nom_step_size, logprior, loglikelihood, logjoint                   
╰──────────────────────────────────────────────────────────────────────────────╯

The sampled chain does not contain x and y, but we can extract the values using the returned function. Calling this function outputs an array:

nts = returned(Neal_return(), chain)
1000×1 DimArray{@NamedTuple{x::Vector{Float64}, y::Float64}, 2}
├─────────────────────────────────────────────────────────── dims ┤
  iter Sampled{Int64} 501:1500 ForwardOrdered Regular Points,
  chain Sampled{Int64} 1:1 ForwardOrdered Regular Points
└─────────────────────────────────────────────────────────────────┘
     1
  501        (x = [0.282922, -0.169133, 0.110429, 0.0715578, 0.376746, 0.267518, 0.150827, -0.364486, 0.523348], y = -2.44537)
  502        (x = [-0.908427, 1.81899, 0.0996961, -0.1388, -2.32171, -3.11879, 0.191075, 3.08142, -2.79929], y = 2.22404)
  503        (x = [0.548741, 1.20134, 0.174024, -2.47339, -0.54717, -0.619945, 0.322272, 1.92493, -0.523358], y = 1.171)
  504        (x = [0.213834, 0.459941, 0.274258, -0.103542, 0.26272, 0.017928, -0.342788, -0.139792, -0.0908345], y = -2.82301)
    ⋮    ⋱  
 1497        (x = [1.91886, -0.179963, 0.384338, 0.229444, -0.853197, 0.0849165, 0.909597, 0.299276, -2.22477], y = 0.205081)
 1498        (x = [-1.53253, -0.0256252, -0.578854, 0.601605, -0.00861642, -0.15997, -0.46773, -0.114916, 1.13116], y = -0.769315)
 1499        (x = [-0.970829, 3.16634, -8.12862, -7.6297, -2.57471, -0.783679, -8.50783, 0.561513, 5.00781], y = 3.34255)
 1500    …   (x = [-0.109686, -0.0523531, 0.135572, 0.160781, 0.0787242, 0.00686893, 0.155659, 0.0440652, -0.113417], y = -4.95074)

where each element of which is a NamedTuple, as specified in the return statement of the model.

nts[1]
(x = [0.2829217345898299, -0.1691327809537191, 0.11042863863554053, 0.0715578021244054, 0.3767460846355524, 0.2675179583187902, 0.15082667370006864, -0.36448570790114876, 0.5233482953622169], y = -2.445373731237608)

Which to use?

There are some pros and cons of using returned, as opposed to :=.

One drawback is that naively using returned can lead to unnecessary computation during inference. This is because during the sampling process, the return values are also calculated (since they are part of the model function), but then thrown away. So, if the extra quantities are expensive to compute, this can be a problem.

To avoid this, you will essentially have to create two different models, one for inference and one for post-inference. The simplest way of doing this is to add a parameter to the model argument:

@model function Neal_coloneq_optional(track::Bool)
    # Raw draws
    y_raw ~ Normal(0, 1)
    x_raw ~ arraydist([Normal(0, 1) for i in 1:9])

    if track
        y = 3 * y_raw
        x = exp.(y ./ 2) .* x_raw
        return (x=x, y=y)
    else
        return nothing
    end
end

chain = sample(Neal_coloneq_optional(false), NUTS(), 1000)
Info: Found initial step size
  ϵ = 1.6
╭─FlexiChain (1000 iterations, 1 chain) ───────────────────────────────────────
 ↓ iter  = 501:1500                                                           
 → chain = 1:1                                                                
                                                                              
 Parameters (2) ── AbstractPPL.VarName                                        
  Float64          y_raw                                                      
  Vector{Float64}  x_raw                                                      
                                                                              
 Extras (14)                                                                  
  Int64    n_steps, tree_depth                                                
  Bool     is_accept, numerical_error                                         
  Float64  acceptance_rate, log_density, hamiltonian_energy,                  
           hamiltonian_energy_error, max_hamiltonian_energy_error, step_size, 
           nom_step_size, logprior, loglikelihood, logjoint                   
╰──────────────────────────────────────────────────────────────────────────────╯

The above ensures that x and y are not calculated during inference, but allows us to still use returned to extract them:

returned(Neal_coloneq_optional(true), chain)
1000×1 DimArray{@NamedTuple{x::Vector{Float64}, y::Float64}, 2}
├─────────────────────────────────────────────────────────── dims ┤
  iter Sampled{Int64} 501:1500 ForwardOrdered Regular Points,
  chain Sampled{Int64} 1:1 ForwardOrdered Regular Points
└─────────────────────────────────────────────────────────────────┘
     1
  501        (x = [0.710389, -0.512088, 1.45332, -0.64808, -0.816193, -1.17001, -1.58935, 0.929246, 0.981064], y = -0.418394)
  502        (x = [0.557345, -0.730549, 0.954088, 0.364098, -0.841986, 0.240159, -0.949528, -0.811757, -0.929336], y = -1.13924)
  503        (x = [0.160563, -0.322039, -0.428131, -0.517257, -0.170639, 0.0515686, -0.271811, 0.51752, 0.138443], y = -2.27448)
  504        (x = [-0.989609, 0.973965, 4.33124, -1.6859, 3.61758, 1.16776, 0.628372, 1.41894, -0.202279], y = 1.27133)
    ⋮    ⋱  
 1497        (x = [-0.0218123, 0.0119705, 0.0218064, 0.00251816, -0.0137657, 0.0463079, -0.0442626, -0.000729207, -0.0213441], y = -6.50138)
 1498        (x = [-0.224404, -1.60874, -3.81985, 0.312724, 2.6594, -6.5141, 7.08536, -1.64689, 2.8846], y = 3.49631)
 1499        (x = [-0.586045, -0.554166, -0.0580872, 1.56588, 0.780932, -0.35012, -0.192225, -1.16379, -0.685818], y = 0.847691)
 1500    …   (x = [0.426403, -0.942829, 2.32446, 0.0731771, 1.98799, -0.80886, -0.254072, -0.0190821, 0.711714], y = -0.0899002)

Another equivalent option is to use a submodel:

@model function Neal()
    y_raw ~ Normal(0, 1)
    x_raw ~ arraydist([Normal(0, 1) for i in 1:9])
    return (x_raw=x_raw, y_raw=y_raw)
end

chain = sample(Neal(), NUTS(), 1000)

@model function Neal_with_extras()
    neal ~ to_submodel(Neal(), false)
    y = 3 * neal.y_raw
    x = exp.(y ./ 2) .* neal.x_raw
    return (x=x, y=y)
end

returned(Neal_with_extras(), chain)
Info: Found initial step size
  ϵ = 1.6
1000×1 DimArray{@NamedTuple{x::Vector{Float64}, y::Float64}, 2}
├─────────────────────────────────────────────────────────── dims ┤
  iter Sampled{Int64} 501:1500 ForwardOrdered Regular Points,
  chain Sampled{Int64} 1:1 ForwardOrdered Regular Points
└─────────────────────────────────────────────────────────────────┘
     1
  501        (x = [0.0304588, 0.0207058, -0.0183389, -0.0169392, 0.00498436, 0.0601237, 0.0314205, 0.0211649, -0.0397902], y = -6.80558)
  502        (x = [0.00369129, 0.0312568, 0.0296336, -0.00113074, 0.0265627, -0.0553351, -0.0464952, 0.00861146, -0.0548527], y = -6.55879)
  503        (x = [-0.681852, -0.252764, 0.346588, -0.792776, -0.806107, -0.145162, -0.0625323, 0.0866044, 0.22716], y = -0.972907)
  504        (x = [0.186241, 0.461821, 0.624047, 1.2832, -0.241253, 0.325531, -1.07056, 0.623223, 0.499316], y = -0.102119)
    ⋮    ⋱  
 1497        (x = [1.60438, -0.773408, -1.13686, -0.290958, 0.426417, 0.437782, 1.27541, 0.0956741, 0.309047], y = -0.657576)
 1498        (x = [-1.11782, 0.942392, 1.18547, 0.222219, -0.0959954, -0.601022, -1.33806, 0.91711, 0.258092], y = -0.49824)
 1499        (x = [0.686515, -1.3127, -0.998687, -0.224235, -0.189202, 0.518733, 0.955397, 0.732856, 0.940813], y = -0.659215)
 1500    …   (x = [0.753733, -8.06581, 0.954684, 1.79632, -0.228575, 1.73187, 0.467518, 0.418771, -1.88002], y = 2.39006)

Note that for the returned call to work, the Neal_with_extras() model must have the same variable names as stored in chain. This means the submodel Neal() must not be prefixed, i.e. to_submodel() must be passed a second parameter false.

Back to top