Bayesian Neural Network
Consider a Bayesian feedforward neural network[1] with $L$ hidden layers. Letting $x_i$ be the input vector and $y_i$ be the observed output, we will assume the following model:
\[y_i \mid x_i, \theta \sim \mathcal{N}(f(x_i; \theta), 1 / \beta),\]
where
\[f(x;\theta) = W^{L+1}h\left(W^{L}h\left(W^{L-1} \dots h(W^{1}x + b^{1})\dots \right) + b^{L} \right) + b^{L+1}.\]
Here, $h$ is a non-linear activation function applied element-wise (taken to be $\tanh$ in this tutorial), $W^{l}$ are the weights associated with the $l^{th}$ layer, and $b^{l}$ are the biases associated with the $l^{th}$ layer. Let the widths of each layer be $\{n_0, n_1, \dots, n_L, n_{L+1}\}$ where $n_0$ and $n_{L+1}$ correspond to the input dimension and output dimension, respectively. Following Neal, we will use the following prior specification
\[w^{1}_{jk} \sim \mathcal{N}(0, 1 / \alpha_1), \qquad w^{l}_{ji} \sim \mathcal{N}(0, 1/(\alpha_l \times n_{l-1}))\quad (l \ge 2), \qquad b \sim \mathcal{N}(0, \sigma^2_b).\]
We will assume that $\sigma^2_b = 1$ is fixed, and put hyperpriors on $\alpha_l$ and $\beta$ such that $\alpha_l \sim \text{Gamma}(a,b)$ and $\beta \sim \text{Gamma}(a,b)$ $(l = 1, \dots, L +1)$. To use AGESS[2], we need only specify the log target density.
Specifying the log posterior density
We start by defining a struct which contains all of the information of the network architecture. In addition, we will specify a function which returns the total number of parameters in our model (n_params) and a function which evaluates $f(x; \theta)$ (forward!). Using these functions, we will build a function that evaluates the transformed log posterior density; taking in a flattened vector of parameters (along with other variables used for intermediate calculations to save on memory allocation) and returning the transformed log posterior density. Here, the scale parameters are transformed to the log-scale so all variables are unbounded.
using LinearAlgebra
using Distributions
using Random
using SpecialFunctions: loggamma
struct BNNArchitecture
sizes::Vector{Int} # [n_0, n_1, ..., n_L, n_out]
end
function n_params(arch::BNNArchitecture)
params = 0
## Iterate over layers
for l in 1:(length(arch.sizes) - 1)
## Number of weights
params += arch.sizes[l + 1] * arch.sizes[l]
## Number of biases
params += arch.sizes[l + 1]
end
params += (length(arch.sizes) - 1) # precision parameters
params += 1 # precision in likelihood β
return params
end
# Forward pass: tanh hidden layer
function forward!(y::AbstractMatrix{Y}, W::AbstractVector{<:AbstractMatrix{Y}},
b::AbstractVector{<:AbstractVector{Y}}, X::AbstractMatrix{Y},
H_ph::AbstractVector{<:AbstractMatrix{Y}}) where {Y<:AbstractFloat}
# X is n_in x n_samples
H_ph[1] .= X
for i in 1:(length(W) -1)
H_ph[i+1] .= tanh.(W[i] * H_ph[i] .+ b[i])
end
y .= W[end] * H_ph[end] .+ b[end] # n_out x n_samples
end
# ---- Hyperpriors on precisions: Gamma(shape, rate) on α1, α2, β ----
function log_gamma_hyperprior(log_α::Y, α::Y, shape::Y, rate::Y) where {Y<:AbstractFloat}
lp = shape * log(rate) - loggamma(shape) +
(shape - 1) * log_α - rate * α + log_α # Jacobian of α = exp(log_α)
return lp
end
# ----------------------------------------------------------------------
# Calculation of the log-posterior; taking a flattened vector of parameters.
# ----------------------------------------------------------------------
function transformed_log_posterior(θ::AbstractVector{Y}, X::AbstractMatrix{Y}, y::AbstractMatrix{Y},
W_ph::AbstractVector{<:AbstractMatrix{Y}},
b_ph::AbstractVector{<:AbstractVector{Y}}, log_α::AbstractVector{Y},
H_ph::AbstractVector{<:AbstractMatrix{Y}}, Y_ph::AbstractMatrix{Y},
resid::AbstractMatrix{Y}, arch::BNNArchitecture, gamma_shape::Y,
gamma_rate::Y, σ2b::Y) where {Y<:AbstractFloat}
# ----------------------------------------------------------------------
# Unflatten
# ----------------------------------------------------------------------
## index
idx = 1
for l in 1:(length(arch.sizes)-1)
## get weights
nW = arch.sizes[l+1] * arch.sizes[l]
@views W_ph[l] .= reshape(θ[idx:idx+nW-1], arch.sizes[l+1], arch.sizes[l])
idx += nW
## get biases
@views b_ph[l] .= θ[idx:idx+arch.sizes[l+1]-1]
idx += arch.sizes[l+1]
end
## get precision for hyperpriors
for l in 1:(length(arch.sizes)-1)
log_α[l] = θ[idx]
idx += 1
end
## get precision for likelihood
log_β = θ[idx]
idx += 1
# ----------------------------------------------------------------------
# Calculate log posterior density
# ----------------------------------------------------------------------
forward!(Y_ph, W_ph, b_ph, X, H_ph)
resid .= y .- Y_ph
lpdf = 0.5 * (length(resid) * log_β - exp(log_β) * sum(abs2, resid))
for l in 1:(length(arch.sizes)-1)
## prior on weights
nW = arch.sizes[l+1] * arch.sizes[l]
if l == 1
lpdf += 0.5 * (nW * log_α[l] - exp(log_α[l]) * sum(abs2, W_ph[l]))
else
## scale variance by the number of nodes in previous layer
lpdf += 0.5 * (nW * (log_α[l] + log(arch.sizes[l])) - exp(log_α[l]) * arch.sizes[l] * sum(abs2, W_ph[l]))
end
## prior on biases
lpdf += 0.5 * (-sum(abs2, b_ph[l]) / σ2b)
# Prior on precision parameters
lpdf += log_gamma_hyperprior(log_α[l], exp(log_α[l]), gamma_shape, gamma_rate)
end
# Prior on precision parameter
lpdf += log_gamma_hyperprior(log_β, exp(log_β), gamma_shape, gamma_rate)
return lpdf
endtransformed_log_posterior (generic function with 1 method)Case Study: Yacht Hydrodynamics
We will explore the utility of Bayesian neural networks by considering the Yacht Hydrodynamics dataset. The dataset contains 308 observations with 6 predictor variables and 1 response variable. We will standardize the dataset, and randomly split the dataset into a training set and a test set, so that we can evaluate the performance of the neural network.
using DelimitedFiles
using StatsBase
using AdaptEllipticalSliceSampler
using Plots
Random.seed!(123)
# ----------------------------------------------------------------------
# Read data
# ----------------------------------------------------------------------
data = readdlm(download("http://archive.ics.uci.edu/ml/machine-learning-databases/00243/yacht_hydrodynamics.data"))
X = Matrix(data[:, 1:6]') # 6 x 308
y = reshape(data[:, 7], 1, :) # 1 x 308
y_mean = mean(y); y_std = std(y)
y = (y .- y_mean) ./ y_std
## Standardize dataset
X = zscore(X, 2)
## Split into training and testing
perm = randperm(size(X, 2))
n_test = 50
test_idx = perm[1:n_test]
train_idx = perm[n_test+1:end]
X_train = X[:, train_idx]
y_train = y[:, train_idx]
X_test = X[:, test_idx]
y_test = y[:, test_idx]
n_obs = size(X_train, 2)
## Specify architecture of neural network
# ----------------------------------------------------------------------
## Input = 6
## Hidden_1 = 8
## Hidden_2 = 10
## Output = 1
# ----------------------------------------------------------------------
arch = BNNArchitecture([6, 8, 10, 1])
P = n_params(arch)
## Specify intermediate values used in likelihood calculation
b_ph = Vector{Vector{Float64}}(undef, length(arch.sizes) - 1)
H_ph = Vector{Matrix{Float64}}(undef, length(arch.sizes) - 1)
W_ph = Vector{Matrix{Float64}}(undef, length(arch.sizes) - 1)
log_α = zeros(length(arch.sizes) - 1)
for l in 1:(length(arch.sizes) - 1)
b_ph[l] = zeros(arch.sizes[l+1])
H_ph[l] = zeros(arch.sizes[l], n_obs)
W_ph[l] = zeros(arch.sizes[l+1], arch.sizes[l])
end
Y_ph = similar(y_train)
resid = similar(y_train)
## Hyperpriors
gamma_shape = 2.0
gamma_rate = 2.0
println("--- Number of Parameters = ", P, " ---- \n")--- Number of Parameters = 161 ----Now that we have all of the necessary function and variables specified, we can use the AGESS function to conduct MCMC.
# ----------------------------------------------------------------------
# Run AGESS
# ----------------------------------------------------------------------
n_MCMC = 20000 # Should run for longer in realistic situations
results = AGESS(θ -> transformed_log_posterior(θ, X_train, y_train, W_ph, b_ph, log_α,
H_ph, Y_ph, resid, arch,
gamma_shape, gamma_rate, 1.0),
n_MCMC, P)AdaptEllipticalSliceSampler.MCMC_output([0.0 0.0 … 0.0 0.0; 1.5804526217142627 -0.7262663174413617 … -0.02200962764123645 0.008492884222287805; … ; 0.272664748134917 -0.27060862408639896 … -1.0227451296029688 3.952437841821936; 0.28199864292902876 -0.25721900014863863 … -1.0446996400530133 3.9506786317992946], [-34.237330141050904, -3.0415534885191473, 43.30579452135466, 60.13544111583218, 62.595782795609296, 72.48531752345095, 102.76934963437253, 123.38408011533677, 128.4730910320038, 138.1901501451082 … 461.87649031676926, 464.21614084901813, 465.34087549987106, 462.9132845593915, 465.3000496409691, 470.5674604986001, 471.19960096156353, 470.9444413740633, 470.0684173968511, 470.71790985938753], AGESS_MCMC_params(Main.var"#2#3"(), [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 … 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0 0.0 … 0.0 0.0; 0.0 1.0 … 0.0 0.0; … ; 0.0 0.0 … 1.0 0.0; 0.0 0.0 … 0.0 1.0], true, 6.0, 0.5, 0.05, 0.5, 0.05, 20000, 161), [0.14901780150821778 0.010479874725695408 … -0.040918870485106146 0.002984025037189022; 0.010479874725695408 0.04918076467114283 … 0.001801474287636948 -4.284800444518104e-5; … ; -0.040918870485106146 0.001801474287636948 … 0.1411666449703852 -0.0010605312396601114; 0.002984025037189022 -4.284800444518102e-5 … -0.001060531239660111 0.007328683026695501], [0.12764015177787275, -0.22536522387844235, 0.012672945250164873, -0.008956763004399664, -0.008409512554628453, -0.09570815127806685, -0.0011472333030095624, -0.10761227851836616, -0.000560005964620597, 0.33388019357883736 … -0.3377764785016477, 0.2061033769365951, -1.4986492807078609, 0.3255364854434929, -0.6305645286767302, 0.5935281044756927, 1.6013173734057362, 0.7414451572110532, -1.2815316206941951, 3.9661503804577385])By plotting the log target density, we can see that our Markov chain appears to converge relatively fast. However, we will conservatively discard the first 50\% of the chain and use the remaining 50\% for inference.
plot(results.l_pdf[25:end], label = "log target density")
vline!([10000], color =:red, label = "burnin")Posterior Predictive Distributions
One of the main benefits of using a Bayesian neural network over a traditional neural network is that we obtain a full posterior distribution (instead of just maximizing the weights as is done in traditional neural networks), which allows us to utilized the posterior predictive distribution to evaluate the uncertainty around a new data point. Let's first take a look at the posterior predictive mean. While harder to visualize when there is more than one covariate, we will look at the posterior predictive mean as a function of the Froude number (one of the predictors), while holding all other variables at their mean value.
# Function for generating posterior predictive draws
function posterior_predictive_draws(post_samples::AbstractMatrix{Y}, X_grid::AbstractMatrix{Y},
W_ph::AbstractVector{<:AbstractMatrix{Y}},
b_ph::AbstractVector{<:AbstractVector{Y}}, log_α::AbstractVector{Y},
H_ph::AbstractVector{<:AbstractMatrix{Y}}, Y_ph::AbstractMatrix{Y},
arch::BNNArchitecture) where {Y<: AbstractFloat}
n_s = size(post_samples,1)
n_g = size(X_grid, 2)
draws = zeros(n_s, n_g) # noisy draws
f_means = zeros(n_s, n_g) # noiseless function draws
for s in 1:n_s
# ----------------------------------------------------------------------
# Unflatten
# ----------------------------------------------------------------------
## index
idx = 1
for l in 1:(length(arch.sizes)-1)
## get weights
nW = arch.sizes[l+1] * arch.sizes[l]
@views W_ph[l] .= reshape(post_samples[s, idx:idx+nW-1], arch.sizes[l+1], arch.sizes[l])
idx += nW
## get biases
@views b_ph[l] .= post_samples[s, idx:idx+arch.sizes[l+1]-1]
idx += arch.sizes[l+1]
end
## get precision for hyperpriors
for l in 1:(length(arch.sizes)-1)
log_α[l] = post_samples[s, idx]
idx += 1
end
## get precision for likelihood
log_β = post_samples[s, idx]
idx += 1
# ----------------------------------------------------------------------
# Run forward Model
# ----------------------------------------------------------------------
forward!(Y_ph, W_ph, b_ph, X_grid, H_ph)
@views f_means[s, :] .= vec(Y_ph)
σ = sqrt(1 / exp(log_β))
# Add error term
@views draws[s, :] .= f_means[s, :] .+ σ .* randn(n_g)
end
return draws, f_means
end
froude_idx = 6
# Build a grid over Froude number for visualization, holding other
# features fixed at their mean
froude_grid_std = collect(range(minimum(X[froude_idx, :]),
maximum(X[froude_idx, :]), length=100))
n_grid = length(froude_grid_std)
X_grid = zeros(6, n_grid)
@views X_grid[froude_idx, :] .= froude_grid_std
### rest H and Y_ph based on number in grid
H_ph1 = Vector{Matrix{Float64}}(undef, length(arch.sizes) - 1)
for l in 1:(length(arch.sizes) - 1)
H_ph1[l] = zeros(arch.sizes[l], n_grid)
end
Y_ph1 = zeros(1, n_grid)
# Use function to get posterior draws
@views draws, f_means = posterior_predictive_draws(results.samps[10000:end,:], X_grid, W_ph,
b_ph, log_α, H_ph1, Y_ph1, arch)
pred_mean = vec(mean(f_means, dims=1)) # E[f(x)] over posterior
pred_lower = [quantile(f_means[:, j], 0.025) for j in 1:n_grid]
pred_upper = [quantile(f_means[:, j], 0.975) for j in 1:n_grid]
predictive_lower = [quantile(draws[:, j], 0.025) for j in 1:n_grid] # includes noise
predictive_upper = [quantile(draws[:, j], 0.975) for j in 1:n_grid]
p1 = plot(froude_grid_std, predictive_upper, fillrange=predictive_lower,
fillalpha=0.15, linealpha=0, color=:steelblue,
label="95% predictive interval (incl. noise)")
plot!(p1, froude_grid_std, pred_upper, fillrange=pred_lower,
fillalpha=0.55, linealpha=0, color=:steelblue,
label="95% credible interval (f only)")
plot!(p1, froude_grid_std, pred_mean, color=:navy, linewidth=2,
label="Posterior predictive mean")
scatter!(p1, X_train[froude_idx, :], y_train[1, :],
color=:black, markersize=4, label="Training data")
scatter!(p1, X_test[froude_idx, :], y_test[1, :],
color=:red, markersize=5, markershape=:diamond, label="Held-out test data")
xlabel!(p1, "Froude number")
ylabel!(p1, "Residuary resistance")
title!(p1, "Posterior predictive: BNN fit to Yacht data")By looking at this plot, we can see that there is very little variability in the residual resistance when the Froude number is small; however, the variability increases as the Froude number increases. Thus, the predictive intervals look too wide when the Froude number is small, and too narrow when the Froude number is big. However, it is important to recall that this is a view of the conditional posterior predictive distribution (conditional on all other covariates at their mean value). Thus, it is possible that perhaps the other variables can explain the increased variability observed at high Froude numbers. Therefore, lets look at the posterior predictive coverage on the test dataset to get a better sense of what is happening.
function predictive_interval_at(post_samples::AbstractMatrix{Y}, x_i::AbstractVector{Y},
W_ph::AbstractVector{<:AbstractMatrix{Y}},
b_ph::AbstractVector{<:AbstractVector{Y}},
log_α::AbstractVector{Y}, H_ph_i::AbstractVector{<:AbstractMatrix{Y}},
Y_ph::AbstractMatrix{Y},
arch::BNNArchitecture;
q::Vector{Float64} = [0.025, 0.975],
n_draw::Int = 2000) where {Y<:AbstractFloat}
n_s = size(post_samples, 1)
draws_i = Vector{Float64}(undef, n_draw)
x_mat = reshape(x_i, :, 1) # reshape to n_in x 1 matrix for forward!
for k in 1:n_draw
s = rand(1:n_s)
# ----------------------------------------------------------------------
# Unflatten
# ----------------------------------------------------------------------
## index
idx = 1
for l in 1:(length(arch.sizes)-1)
## get weights
nW = arch.sizes[l+1] * arch.sizes[l]
@views W_ph[l] .= reshape(post_samples[s, idx:idx+nW-1], arch.sizes[l+1], arch.sizes[l])
idx += nW
## get biases
@views b_ph[l] .= post_samples[s, idx:idx+arch.sizes[l+1]-1]
idx += arch.sizes[l+1]
end
## get precision for hyperpriors
for l in 1:(length(arch.sizes)-1)
log_α[l] = post_samples[s, idx]
idx += 1
end
## get precision for likelihood
log_β = post_samples[s, idx]
idx += 1
forward!(Y_ph, W_ph, b_ph, x_mat, H_ph_i)
σ_i = sqrt(1 / exp(log_β))
draws_i[k] = Y_ph[1] + σ_i * randn()
end
return quantile(draws_i, q[1]), quantile(draws_i, q[2])
end
### rest H and Y_ph based on number in grid
H_ph_i = Vector{Matrix{Float64}}(undef, length(arch.sizes) - 1)
for l in 1:(length(arch.sizes) - 1)
H_ph_i[l] = zeros(arch.sizes[l], 1)
end
Y_ph_i = zeros(1, 1)
lo_vec = zeros(n_test)
hi_vec = zeros(n_test)
covered = falses(n_test)
for i in 1:n_test
lo, hi = predictive_interval_at(results.samps[10000:end, :], X_test[:, i], W_ph,
b_ph, log_α, H_ph_i, Y_ph_i, arch)
lo_vec[i] = lo
hi_vec[i] = hi
covered[i] = (lo <= y_test[1, i] <= hi)
end
println("Empirical coverage (target 0.95): ", mean(covered))
# Sort by Froude number for a readable x-axis
order = sortperm(X_test[froude_idx, :])
p_coverage = plot(legend=:topleft, xlabel="Froude number",
ylabel="Residuary resistance (standardized)",
title="Posterior predictive coverage on held-out data")
# Error bars: green if covered, red if missed
for (rank, i) in enumerate(order)
color = covered[i] ? :green : :red
plot!(p_coverage, [X_test[froude_idx, i], X_test[froude_idx, i]],
[lo_vec[i], hi_vec[i]], color=color, linewidth=2, label=false, alpha=0.7)
end
scatter!(p_coverage, X_test[froude_idx, :], y_test[1, :],
group=ifelse.(covered, "Covered", "Missed"),
color=ifelse.(covered, :green, :red),
markersize=4, markerstrokewidth=0.5)
#display(p_coverage)
nominal_levels = 0.5:0.05:0.99
empirical_coverage = zeros(length(nominal_levels))
for (li, level) in enumerate(nominal_levels)
alpha = 1 - level
q_lo, q_hi = alpha/2, 1 - alpha/2
covered_at_level = falses(n_test)
for i in 1:n_test
lo, hi = predictive_interval_at(results.samps[10000:end, :], X_test[:, i], W_ph,
b_ph, log_α, H_ph_i, Y_ph_i, arch;
q=[q_lo, q_hi])
covered_at_level[i] = (lo <= y_test[1, i] <= hi)
end
empirical_coverage[li] = mean(covered_at_level)
println("Nominal $(round(level,digits=2)) -> empirical $(round(empirical_coverage[li],digits=3))")
end
p_calib = plot(nominal_levels, nominal_levels, linestyle=:dash, color=:black,
label="Perfect calibration", xlabel="Nominal coverage",
ylabel="Empirical coverage", title="Calibration curve",
legend=:topleft, aspect_ratio=:equal, xlims=(0.45,1.0), ylims=(0.45,1.0))
plot!(p_calib, nominal_levels, empirical_coverage, marker=:circle, color=:navy,
label="BNN predictive intervals", linewidth=2)# ---- Combined figure -------------------------------------------------
p_combined = plot(p_coverage, p_calib, layout=(1,2), size=(1100,450),
left_margin=5Plots.mm, bottom_margin=5Plots.mm)
display(p_combined)From the graphs above, we can see that we are overly conservative in our coverage, especially when looking at smaller credible intervals (i.e., 50 - 70% credible intervals). Recall that our model assumes a homoskedastic error term ($1/\beta$). Lets first check to see if a homoskedastic error term $\beta$ is appropriate in this setting by plotting the residual values against the Froude number.
H_ph_test = Vector{Matrix{Float64}}(undef, length(arch.sizes) - 1)
for l in 1:(length(arch.sizes) - 1)
H_ph_test[l] = zeros(arch.sizes[l], n_test)
end
Y_ph_test = zeros(1, n_test)
@views draws_test, f_means_test = posterior_predictive_draws(
results.samps[10000:end, :], X_test, W_ph, b_ph, log_α, H_ph_test, Y_ph_test, arch)
# draws_test is n_post_samples x n_test; compute per-point predictive
# mean/sd from the full (noise-included) posterior predictive draws
pred_mean_test = vec(mean(draws_test, dims=1))
pred_sd_test = vec(std(draws_test, dims=1))
z_scores = (vec(y_test) .- pred_mean_test) ./ pred_sd_test
# ---- Plot: z-scores vs Froude number ----------
order = sortperm(X_test[froude_idx, :])
p_z = scatter(X_test[froude_idx, :], z_scores, color=:navy, markersize=4,
label="Test points", xlabel="Froude number",
ylabel="Standardized residual (z)",
title="Calibration check: z-scores vs. Froude number",
legend=:topleft)
hline!(p_z, [0], color=:black, linestyle=:dash, label=false)
hline!(p_z, [-2, 2], color=:red, linestyle=:dot, label="±2 SD")By plotting the standardized residuals against the Froude number, we can see that the assumption of a homoskedastic error may not be appropriate in this setting. Specifically, we can see that high Froude numbers lead to larger residual values. One possible solution is to allow for a more complex neural network (i.e., increasing the depth and the width of the neural network). Perhaps a more complex function could explain more of the heterogeneity observed, leading to residuals that are homoskedastic. However, increasing the depth and width did not help with this problem. Instead, let's consider a Bayesian neural network with heterogeneous error terms.
Heteroskedastic Bayesian Neural Network
Consider the following heteroskedastic Bayesian neural network, which uses two different neural networks to capture the mean function and variance function:
\[y_i \mid x_i, \theta \sim \mathcal{N}(f(x_i; \theta), \exp(g(x_i; \theta))^{-1}),\]
where
\[f(x;\theta) = W_f^{L+1}h\left(W^{L_f}h\left(W_f^{L_f-1} \dots h(W^{1}x + b_f^{1})\dots \right) + b_f^{L_f} \right) + b_f^{L+1},\]
and
\[g(x;\theta) = W_g^{L+1}h\left(W^{L_g}h\left(W_g^{L_g-1} \dots h(W_g^{1}x + b_g^{1})\dots \right) + b_g^{L_g} \right) + b_g^{L+1}.\]
Notice that each neural network is assumed to have its own network architecture. We will use a similar prior structure as before:
\[(w^{1}_f)_{jk} \sim \mathcal{N}(0, 1 / \alpha_1), \qquad (w^{l}_f)_{ji} \sim \mathcal{N}(0, 1/ (\alpha_l \times n_{l-1}))\quad (l \ge 2), $$ $$(w^{1}_g)_{jk} \sim \mathcal{N}(0, 1 / \alpha_1), \qquad (w^{l}_g)_{ji} \sim \mathcal{N}(0, 1/ (\alpha_l \times n_{l-1}))\quad (l \ge 2),\]
\[b \sim \mathcal{N}(0, \sigma^2_b);\]
As before, we will use relatively uninformative priors on $\alpha_l$ (i.e., $\alpha_l \sim \text{Gamma}(a,b)$). Let's start by constructing the necessary functions to evaluate the likelihood and calculate the total number of parameters.
### New struct allowing for heterogenous model
### Can also handle homogeneous model
struct HBNNArchitecture
sizes_mean::Vector{Int} # [n_0, n_1, ..., n_L, n_out]
sizes_var::Vector{Int} # [n_0, n_1, ..., n_L, n_out]
heterogeneous::Bool # true or false
end
### overload function for heterogeneous model
function n_params(arch::HBNNArchitecture)
params = 0
## Iterate over layers for mean NN
for l in 1:(length(arch.sizes_mean) - 1)
## Number of weights
params += arch.sizes_mean[l + 1] * arch.sizes_mean[l]
## Number of biases
params += arch.sizes_mean[l + 1]
end
## precision parameters for mean
params += length(arch.sizes_mean) - 1
if arch.heterogeneous == true
## Iterate over layers for var NN
for l in 1:(length(arch.sizes_var) - 1)
## Number of weights
params += arch.sizes_var[l + 1] * arch.sizes_var[l]
## Number of biases
params += arch.sizes_var[l + 1]
end
## precision parameters for var
params += length(arch.sizes_var) - 1
else
params += 1
end
return params
end
# ----------------------------------------------------------------------
# Calculation of the log-posterior of heterogeneous model
# ----------------------------------------------------------------------
function transformed_log_posterior(θ::AbstractVector{Y}, X::AbstractMatrix{Y}, y::AbstractMatrix{Y},
W_f::AbstractVector{<:AbstractMatrix{Y}}, W_g::AbstractVector{<:AbstractMatrix{Y}},
b_f::AbstractVector{<:AbstractVector{Y}}, b_g::AbstractVector{<:AbstractVector{Y}},
log_α_f::AbstractVector{Y}, log_α_g::AbstractVector{Y},
H_f::AbstractVector{<:AbstractMatrix{Y}}, H_g::AbstractVector{<:AbstractMatrix{Y}},
f::AbstractMatrix{Y}, g::AbstractMatrix{Y}, resid::AbstractMatrix{Y},
arch::HBNNArchitecture, gamma_shape::Y, gamma_rate::Y,
gamma_shape_g::Y, gamma_rate_g::Y, σ2b::Y) where {Y<:AbstractFloat}
# ----------------------------------------------------------------------
# Unflatten
# ----------------------------------------------------------------------
## index
idx = 1
log_β = 0.0
for l in 1:(length(arch.sizes_mean)-1)
## get weights
nW = arch.sizes_mean[l+1] * arch.sizes_mean[l]
@views W_f[l] .= reshape(θ[idx:idx+nW-1], arch.sizes_mean[l+1], arch.sizes_mean[l])
idx += nW
## get biases
@views b_f[l] .= θ[idx:idx+arch.sizes_mean[l+1]-1]
idx += arch.sizes_mean[l+1]
end
## get precision for hyperpriors
for l in 1:(length(arch.sizes_mean)-1)
log_α_f[l] = θ[idx]
idx += 1
end
if arch.heterogeneous == true
for l in 1:(length(arch.sizes_var)-1)
## get weights
nW = arch.sizes_var[l+1] * arch.sizes_var[l]
@views W_g[l] .= reshape(θ[idx:idx+nW-1], arch.sizes_var[l+1], arch.sizes_var[l])
idx += nW
## get biases
@views b_g[l] .= θ[idx:idx+arch.sizes_var[l+1]-1]
idx += arch.sizes_var[l+1]
end
## get precision for hyperpriors
for l in 1:(length(arch.sizes_var)-1)
log_α_g[l] = θ[idx]
idx += 1
end
else
## get precision for likelihood in homogeneous model
log_β = θ[idx]
idx += 1
end
# ----------------------------------------------------------------------
# Calculate log posterior density
# ----------------------------------------------------------------------
forward!(f, W_f, b_f, X, H_f)
if arch.heterogeneous == true
forward!(g, W_g, b_g, X, H_g)
end
resid .= y .- f
lpdf = 0.0
if arch.heterogeneous == true
for i in eachindex(resid)
lpdf += 0.5 * (g[i] - exp(g[i]) * resid[i]^2)
end
else
lpdf = 0.5 * (length(resid) * log_β - exp(log_β) * sum(abs2, resid))
end
for l in 1:(length(arch.sizes_mean)-1)
## prior on weights
nW = arch.sizes_mean[l+1] * arch.sizes_mean[l]
if l == 1
lpdf += 0.5 * (nW * log_α_f[l] - exp(log_α_f[l]) * sum(abs2, W_f[l]))
else
## scale variance by the number of nodes in previous layer
lpdf += 0.5 * (nW * (log_α_f[l] + log(arch.sizes_mean[l])) - (exp(log_α_f[l]) *
arch.sizes_mean[l] * sum(abs2, W_f[l])))
end
## prior on biases
lpdf += 0.5 * (-sum(abs2, b_f[l]) / σ2b)
# Prior on precision parameters
lpdf += log_gamma_hyperprior(log_α_f[l], exp(log_α_f[l]), gamma_shape, gamma_rate)
end
if arch.heterogeneous == true
for l in 1:(length(arch.sizes_var)-1)
## prior on weights
nW = arch.sizes_var[l+1] * arch.sizes_var[l]
if l == 1
lpdf += 0.5 * (nW * log_α_g[l] - exp(log_α_g[l]) * sum(abs2, W_g[l]))
else
## scale variance by the number of nodes in previous layer
lpdf += 0.5 * (nW * (log_α_g[l] + log(arch.sizes_var[l])) - (exp(log_α_g[l]) *
arch.sizes_var[l] * sum(abs2, W_g[l])))
end
## prior on biases
lpdf += 0.5 * (-sum(abs2, b_g[l]) / σ2b)
# Prior on precision parameters
lpdf += log_gamma_hyperprior(log_α_g[l], exp(log_α_g[l]), gamma_shape_g, gamma_rate_g)
end
else
# Prior on precision parameter for homogeneous model
lpdf += log_gamma_hyperprior(log_β, exp(log_β), gamma_shape, gamma_rate)
end
return lpdf
endtransformed_log_posterior (generic function with 2 methods)Run AGESS
Now that we have constructed the necessary functions, lets consider a heteroskedastic neural network with the same architecture for the mean function as before ($n_0 = 6, n_1 = 8, n_2 = 10, n_{\text{out}} = 1$) and a relatively shallow and narrow neural network to capture the heterogeneity of the error terms ($n_0 = 6, n_1 = 6, n_{\text{out}} = 1$). Since the variance is a second-order statistic, and thus requires more data in order to learn, it is reasonable to restrict the class of functions to less complex functions by having a more shallow and narrow neural network compared to the mean function.
arch = HBNNArchitecture([6, 8, 10, 1], [6, 6, 1], true)
P = n_params(arch)
## Specify intermediate values used in likelihood calculation
b_f = Vector{Vector{Float64}}(undef, length(arch.sizes_mean) - 1)
H_f = Vector{Matrix{Float64}}(undef, length(arch.sizes_mean) - 1)
W_f = Vector{Matrix{Float64}}(undef, length(arch.sizes_mean) - 1)
log_α_f = zeros(length(arch.sizes_mean) - 1)
for l in 1:(length(arch.sizes_mean) - 1)
b_f[l] = zeros(arch.sizes_mean[l+1])
H_f[l] = zeros(arch.sizes_mean[l], n_obs)
W_f[l] = zeros(arch.sizes_mean[l+1], arch.sizes_mean[l])
end
b_g = Vector{Vector{Float64}}(undef, length(arch.sizes_var) - 1)
H_g = Vector{Matrix{Float64}}(undef, length(arch.sizes_var) - 1)
W_g = Vector{Matrix{Float64}}(undef, length(arch.sizes_var) - 1)
log_α_g = zeros(length(arch.sizes_var) - 1)
for l in 1:(length(arch.sizes_var) - 1)
b_g[l] = zeros(arch.sizes_var[l+1])
H_g[l] = zeros(arch.sizes_var[l], n_obs)
W_g[l] = zeros(arch.sizes_var[l+1], arch.sizes_var[l])
end
f = similar(y_train)
g = similar(y_train)
resid = similar(y_train)
## Hyperpriors
gamma_shape = 2.0
gamma_rate = 2.0
gamma_shape_g = 2.0
gamma_rate_g = 2.0
println("--- Number of Parameters = ", P, " ---- \n")--- Number of Parameters = 211 ----While convergence was relatively fast in the homoskedastic model, this is not the case in the heteroskedastic model. Since we have the same network structure for the mean, we can use a warm-start of the mean function, to help the markov chain converge faster (however, convergence in general is much slower as the posterior geometry seems to be much more challenging).
# ----------------------------------------------------------------------
# Run AGESS
# ----------------------------------------------------------------------
n_MCMC = 500000 # Should run for longer in realistic scenarios
### use warm start using the homogeneous model for W_f and b_f
θ_init = zeros(P)
index_f = let idx = 0
for l in 1:(length(arch.sizes_mean)-1)
idx += arch.sizes_mean[l+1] * arch.sizes_mean[l]
idx += arch.sizes_mean[l+1]
end
idx
end
### set to posterior median of homogeneous model
@views θ_init[1:index_f] .= vec(median(results.samps[10000:end,1:index_f], dims = 1))
## run AGESS
results2 = AGESS(θ -> transformed_log_posterior(θ, X_train, y_train, W_f, W_g, b_f, b_g,
log_α_f, log_α_g, H_f, H_g, f, g, resid, arch, gamma_shape,
gamma_rate, gamma_shape_g, gamma_rate_g, 1.0),
n_MCMC, P, init_x = θ_init)AdaptEllipticalSliceSampler.MCMC_output([0.07219391970016333 -0.2210082789451856 … 0.0 0.0; 0.3075637210859426 0.5311124998016215 … 1.5184479519753058 -0.1512971603712065; … ; 0.4925115976615051 0.5428233013875429 … 0.6554746834959146 -3.9747846344827527; 0.49248391567059563 0.5433723571209523 … 0.6492271168582329 -3.9783001186522675], [51.418616524997866, 158.30167611243124, 439.9939454583772, 543.0587419654787, 698.0761030676572, 779.3382749564364, 824.920518659978, 841.7893299699325, 877.788646616471, 887.9946154959697 … 1168.6856145177192, 1168.6436008988687, 1169.0727500842243, 1168.9183891902708, 1169.209693536485, 1167.9928699314055, 1167.7627010357185, 1168.0112542878896, 1167.9125089086276, 1168.0088567885145], AGESS_MCMC_params(Main.var"#18#19"(), [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 … 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0 0.0 … 0.0 0.0; 0.0 1.0 … 0.0 0.0; … ; 0.0 0.0 … 1.0 0.0; 0.0 0.0 … 0.0 1.0], true, 6.0, 0.5, 0.05, 0.5, 0.05, 500000, 211), [0.002504462226526211 0.00028104195594959807 … -0.0003437722726884833 -0.00015424555062984148; 0.00028104195594959807 0.001035393803335016 … 0.001144517939164393 0.0003253205735962878; … ; -0.0003437722726884833 0.001144517939164393 … 0.15665064857541064 0.00789337222236818; -0.00015424555062984148 0.0003253205735962878 … 0.00789337222236818 0.2518656335784249], [0.5121212229371686, 0.562667684999612, -0.8398806770494502, -0.10010006653019868, -0.1777744012228244, 0.1679100809821109, -0.07463753767132192, 0.5811763392885063, -0.7352073122394033, -0.2871128858343224 … -0.08274873508314647, -0.08698563485868643, -11.83267587599752, 0.07524139418180484, -0.21549729917179997, -0.30266753925565154, 0.12402442230741384, 1.07886464509754, 0.9216926300559447, -4.5424073935085465])As we can see, the Markov chain took significantly longer to converge under this more flexible model.
burnin = floor(Int64, results2.params.burnin * results2.params.n_MCMC)
index = collect(25:10:n_MCMC)
plot(index, results2.l_pdf[index], label = "log target density")
vline!([burnin], color =:red, label = "burnin")Posterior Predictive Distributions
Lets compare the posterior predictive distributions under the heteroskedastic model to the homoskedastic model.
# Function for generating posterior predictive draws
function posterior_predictive_draws(post_samples::AbstractMatrix{Y}, X_grid::AbstractMatrix{Y},
W_f::AbstractVector{<:AbstractMatrix{Y}}, W_g::AbstractVector{<:AbstractMatrix{Y}},
b_f::AbstractVector{<:AbstractVector{Y}}, b_g::AbstractVector{<:AbstractVector{Y}},
log_α_f::AbstractVector{Y}, log_α_g::AbstractVector{Y},
H_f::AbstractVector{<:AbstractMatrix{Y}},
H_g::AbstractVector{<:AbstractMatrix{Y}}, f::AbstractMatrix{Y},
g::AbstractMatrix{Y}, arch::HBNNArchitecture) where {Y<: AbstractFloat}
n_s = size(post_samples,1)
n_g = size(X_grid, 2)
draws = zeros(n_s, n_g) # noisy draws
f_means = zeros(n_s, n_g) # noiseless function draws
σ = zeros(n_g)
log_β = 0.0
for s in 1:n_s
# ----------------------------------------------------------------------
# Unflatten
# ----------------------------------------------------------------------
## index
idx = 1
for l in 1:(length(arch.sizes_mean)-1)
## get weights
nW = arch.sizes_mean[l+1] * arch.sizes_mean[l]
@views W_f[l] .= reshape(post_samples[s,idx:idx+nW-1], arch.sizes_mean[l+1], arch.sizes_mean[l])
idx += nW
## get biases
@views b_f[l] .= post_samples[s,idx:idx+arch.sizes_mean[l+1]-1]
idx += arch.sizes_mean[l+1]
end
## get precision for hyperpriors
for l in 1:(length(arch.sizes_mean)-1)
log_α_f[l] = post_samples[s,idx]
idx += 1
end
if arch.heterogeneous == true
for l in 1:(length(arch.sizes_var)-1)
## get weights
nW = arch.sizes_var[l+1] * arch.sizes_var[l]
@views W_g[l] .= reshape(post_samples[s,idx:idx+nW-1], arch.sizes_var[l+1], arch.sizes_var[l])
idx += nW
## get biases
@views b_g[l] .= post_samples[s,idx:idx+arch.sizes_var[l+1]-1]
idx += arch.sizes_var[l+1]
end
## get precision for hyperpriors
for l in 1:(length(arch.sizes_var)-1)
log_α_g[l] = post_samples[s,idx]
idx += 1
end
else
## get precision for likelihood in homogeneous model
log_β = post_samples[s,idx]
idx += 1
end
# ----------------------------------------------------------------------
# Run forward Model
# ----------------------------------------------------------------------
forward!(f, W_f, b_f, X_grid, H_f)
if arch.heterogeneous == true
forward!(g, W_g, b_g, X_grid, H_g)
end
@views f_means[s, :] .= vec(f)
if arch.heterogeneous == true
σ .= vec(sqrt.(1 ./ exp.(g)))
else
σ .= sqrt(1 / exp(log_β))
end
# Add error term
@views draws[s, :] .= f_means[s, :] .+ σ .* randn(n_g)
end
return draws, f_means
end
froude_idx = 6
# Build a grid over Froude number for visualization, holding other
# features fixed at their mean
froude_grid_std = collect(range(minimum(X[froude_idx, :]),
maximum(X[froude_idx, :]), length=100))
n_grid = length(froude_grid_std)
X_grid = zeros(6, n_grid)
@views X_grid[froude_idx, :] .= froude_grid_std
### reset H, f, and g based on number in grid
H_f1 = Vector{Matrix{Float64}}(undef, length(arch.sizes_mean) - 1)
for l in 1:(length(arch.sizes_mean) - 1)
H_f1[l] = zeros(arch.sizes_mean[l], n_grid)
end
H_g1 = Vector{Matrix{Float64}}(undef, length(arch.sizes_var) - 1)
for l in 1:(length(arch.sizes_var) - 1)
H_g1[l] = zeros(arch.sizes_var[l], n_grid)
end
f1 = zeros(1, n_grid)
g1 = zeros(1, n_grid)
# Use function to get posterior draws
@views draws, f_means = posterior_predictive_draws(results2.samps[burnin:end,:], X_grid, W_f,
W_g, b_f, b_g, log_α_f, log_α_g, H_f1, H_g1,
f1, g1, arch)
pred_mean = vec(mean(f_means, dims=1)) # E[f(x)] over posterior
pred_lower = [quantile(f_means[:, j], 0.025) for j in 1:n_grid]
pred_upper = [quantile(f_means[:, j], 0.975) for j in 1:n_grid]
predictive_lower = [quantile(draws[:, j], 0.025) for j in 1:n_grid] # includes noise
predictive_upper = [quantile(draws[:, j], 0.975) for j in 1:n_grid]
p1 = plot(froude_grid_std, predictive_upper, fillrange=predictive_lower,
fillalpha=0.15, linealpha=0, color=:steelblue,
label="95% predictive interval (incl. noise)")
plot!(p1, froude_grid_std, pred_upper, fillrange=pred_lower,
fillalpha=0.55, linealpha=0, color=:steelblue,
label="95% credible interval (f only)")
plot!(p1, froude_grid_std, pred_mean, color=:navy, linewidth=2,
label="Posterior predictive mean")
scatter!(p1, X_train[froude_idx, :], y_train[1, :],
color=:black, markersize=4, label="Training data")
scatter!(p1, X_test[froude_idx, :], y_test[1, :],
color=:red, markersize=5, markershape=:diamond, label="Held-out test data")
xlabel!(p1, "Froude number")
ylabel!(p1, "Residuary resistance")
title!(p1, "Posterior predictive: BNN fit to Yacht Hydrodynamics data")Compared to before, we can see that predictive intervals are much narrower when the Froude number is small, and grows with Froude number. However, we can see that even when the Froude number is larger ($ > 1.5$), the predictive interval is narrower compared to the homoskedastic model. Again, one has to remember that this is a view of the conditional predictive distribution, and that all predictor values can influence the width of the predictive intervals. To get a better sense of the calibration, lets again look at the coverage under the test set.
function predictive_interval_at(post_samples::AbstractMatrix{Y}, x_i::AbstractVector{Y},
W_f::AbstractVector{<:AbstractMatrix{Y}}, W_g::AbstractVector{<:AbstractMatrix{Y}},
b_f::AbstractVector{<:AbstractVector{Y}}, b_g::AbstractVector{<:AbstractVector{Y}},
log_α_f::AbstractVector{Y}, log_α_g::AbstractVector{Y},
H_f::AbstractVector{<:AbstractMatrix{Y}},
H_g::AbstractVector{<:AbstractMatrix{Y}},
f_i::AbstractMatrix{Y}, g_i::AbstractMatrix{Y},
arch::HBNNArchitecture;
q::Vector{Float64} = [0.025, 0.975],
n_draw::Int = 10000) where {Y<:AbstractFloat}
n_s = size(post_samples, 1)
draws_i = Vector{Float64}(undef, n_draw)
x_mat = reshape(x_i, :, 1) # reshape to n_in x 1 matrix for forward!
σ_i = 0.0
for k in 1:n_draw
s = rand(1:n_s)
# ----------------------------------------------------------------------
# Unflatten
# ----------------------------------------------------------------------
## index
idx = 1
for l in 1:(length(arch.sizes_mean)-1)
## get weights
nW = arch.sizes_mean[l+1] * arch.sizes_mean[l]
@views W_f[l] .= reshape(post_samples[s,idx:idx+nW-1], arch.sizes_mean[l+1], arch.sizes_mean[l])
idx += nW
## get biases
@views b_f[l] .= post_samples[s,idx:idx+arch.sizes_mean[l+1]-1]
idx += arch.sizes_mean[l+1]
end
## get precision for hyperpriors
for l in 1:(length(arch.sizes_mean)-1)
log_α_f[l] = post_samples[s,idx]
idx += 1
end
if arch.heterogeneous == true
for l in 1:(length(arch.sizes_var)-1)
## get weights
nW = arch.sizes_var[l+1] * arch.sizes_var[l]
@views W_g[l] .= reshape(post_samples[s,idx:idx+nW-1], arch.sizes_var[l+1], arch.sizes_var[l])
idx += nW
## get biases
@views b_g[l] .= post_samples[s,idx:idx+arch.sizes_var[l+1]-1]
idx += arch.sizes_var[l+1]
end
## get precision for hyperpriors
for l in 1:(length(arch.sizes_var)-1)
log_α_g[l] = post_samples[s,idx]
idx += 1
end
else
## get precision for likelihood in homogeneous model
log_β = post_samples[s,idx]
idx += 1
end
# ----------------------------------------------------------------------
# Run forward Model
# ----------------------------------------------------------------------
forward!(f_i, W_f, b_f, x_mat, H_f)
if arch.heterogeneous == true
forward!(g_i, W_g, b_g, x_mat, H_g)
end
if arch.heterogeneous == true
σ_i = sqrt(1 / exp(g_i[1,1]))
else
σ_i = sqrt(1 / exp(log_β))
end
draws_i[k] = f_i[1] + σ_i * randn()
end
return quantile(draws_i, q[1]), quantile(draws_i, q[2])
end
### rest H and Y_ph based on number in grid
H_f_i = Vector{Matrix{Float64}}(undef, length(arch.sizes_mean) - 1)
for l in 1:(length(arch.sizes_mean) - 1)
H_f_i[l] = zeros(arch.sizes_mean[l], 1)
end
H_g_i = Vector{Matrix{Float64}}(undef, length(arch.sizes_var) - 1)
for l in 1:(length(arch.sizes_var) - 1)
H_g_i[l] = zeros(arch.sizes_var[l], 1)
end
Y_ph_i = zeros(1, 1)
lo_vec = zeros(n_test)
hi_vec = zeros(n_test)
covered = falses(n_test)
for i in 1:n_test
lo, hi = predictive_interval_at(results2.samps[burnin:end, :], X_test[:, i], W_f, W_g,
b_f, b_g, log_α_f, log_α_g, H_f_i, H_g_i, f1, g1, arch)
lo_vec[i] = lo
hi_vec[i] = hi
covered[i] = (lo <= y_test[1, i] <= hi)
end
println("Empirical coverage (target 0.95): ", mean(covered))
# Sort by Froude number for a readable x-axis
order = sortperm(X_test[froude_idx, :])
p_coverage = plot(legend=:topleft, xlabel="Froude number",
ylabel="Residuary resistance (standardized)",
title="Posterior predictive coverage on held-out data")
# Error bars: green if covered, red if missed
for (rank, i) in enumerate(order)
color = covered[i] ? :green : :red
plot!(p_coverage, [X_test[froude_idx, i], X_test[froude_idx, i]],
[lo_vec[i], hi_vec[i]], color=color, linewidth=2, label=false, alpha=0.7)
end
scatter!(p_coverage, X_test[froude_idx, :], y_test[1, :],
group=ifelse.(covered, "Covered", "Missed"),
color=ifelse.(covered, :green, :red),
markersize=4, markerstrokewidth=0.5)
#display(p_coverage)
nominal_levels = 0.5:0.05:0.99
empirical_coverage = zeros(length(nominal_levels))
for (li, level) in enumerate(nominal_levels)
alpha = 1 - level
q_lo, q_hi = alpha/2, 1 - alpha/2
covered_at_level = falses(n_test)
for i in 1:n_test
lo, hi = predictive_interval_at(results2.samps[burnin:end, :], X_test[:, i], W_f, W_g,
b_f, b_g, log_α_f, log_α_g, H_f_i, H_g_i, f1, g1, arch;
q=[q_lo, q_hi])
covered_at_level[i] = (lo <= y_test[1, i] <= hi)
end
empirical_coverage[li] = mean(covered_at_level)
println("Nominal $(round(level,digits=2)) -> empirical $(round(empirical_coverage[li],digits=3))")
end
p_calib = plot(nominal_levels, nominal_levels, linestyle=:dash, color=:black,
label="Perfect calibration", xlabel="Nominal coverage",
ylabel="Empirical coverage", title="Calibration curve",
legend=:topleft, aspect_ratio=:equal, xlims=(0.45,1.0), ylims=(0.45,1.0))
plot!(p_calib, nominal_levels, empirical_coverage, marker=:circle, color=:navy,
label="BNN predictive intervals", linewidth=2)# ---- Combined figure -------------------------------------------------
p_combined = plot(p_coverage, p_calib, layout=(1,2), size=(1100,450),
left_margin=5Plots.mm, bottom_margin=5Plots.mm)
display(p_combined)We can see that the predictive distributions are better calibrated under the heteroskedastic model. While there is a bit of undercoverage, one has to remember that this is coverage on a test dataset, and some undercoverage is perhaps expected.
Summary
Here we explored the capability of AGESS to fit Bayesian neural networks on a real dataset. We must emphasize this is not meant to be a tutorial on Bayesian neural networks (there are likely much better sources available), but is instead meant to demonstrate the performance of AGESS. Indeed, we illustrated efficient sampling performance when targeting the homoskedastic model, with relatively fast convergence of the Markov chain. However, the introduction of a heteroskedastic error term, led to significantly longer burn-in requirements, and a relatively slow-mixing Markov chain. It appears that the additional flexibility of the heteroskedastic model leads to complex posterior geometry, such as funnel or multimodal posteriors, which in general are challenging for many sampling algorithms. Indeed, funnels often lead to divergent transitions when using gradient-based methods (the common type of MCMC algorithms used in these settings). While additional studies are needed to compare the advantages and disadvantages of AGESS to gradient-based methods, this tutorial provides initial evidence that AGESS can be utilized in these settings.