Regression

In this tutorial, we will illustrate how to use the AdaptEllipticalSliceSampler.jl package, to perform Bayesian computation using the adaptive generalized elliptical slice sampler[1] in regression settings. We consider four settings in this tutorial: (1) Bayesian linear regression, (2) Bayesian generalized linear regression (3) high-dimensional linear sparse regression[2] [3], and (4) generalized ReLU regression.

Download this tutorial as a Jupyter notebook

Bayesian Linear Regression

In this section, we consider using AGESS in the context of standard Bayesian linear regression. Letting $\mathbf{Y} \in \mathbb{R}^N$ be the response variable and $\mathbf{X} \in \mathbb{R}^{N \times D}$ be the set of covariates, we can specify our model as follows:

\[Y_i \sim \mathcal{N}(\mathbf{x}_i' \boldsymbol{\beta}, \sigma^2),\]

\[\boldsymbol{\beta} \sim \mathcal{N}(\mathbf{0}, \mathbf{I}),\]

\[\sigma^2 \sim \text{Inv-Gamma}(1,1).\]

Let's start by first generating data from our model.

import Random
import LogExpFunctions
using AdaptEllipticalSliceSampler
using Distributions
using Plots
using LinearAlgebra
using Turing, MCMCChains, StatsPlots

Random.seed!(123)

function generate_data(N::T, D::T) where {T<:Integer}
    β = randn(D) * (2 * log(D))^(1.0 / 4)
    x = randn(N, D)
    y = zeros(Float64, N)
    for i in 1:N
        y[i] = randn() * 0.5 + dot(x[i,:], β)
    end

    return β, x, y
end

# Generate data with 1000 observations and 10 covariates
D = 10
β, X, y = generate_data(1000, D)
([1.1840703709557707, -1.6437370448488724, -1.6181942515709322, -0.6108573527050607, 0.4212909718278306, 0.3366640790635556, -0.6178538133826769, -1.9858204398026311, -1.5941439357192193, 1.0309437423699197], [1.171685221802266 -1.177965631527714 … 0.029681358521803834 1.230714630625736; -0.30729971233631087 -0.5501257332792802 … 0.9497156839835412 -0.7604970817954496; … ; 0.9685819653962602 -2.142791626046223 … -0.7609766068757015 0.8187625833900045; 2.1012750843330763 0.08399913120329579 … -0.12142493814515284 -1.8514310605297297], [4.2509485214568254, 3.702067516660768, -3.653119910060239, -0.28567345139499334, -2.210986356964136, -0.8846381981280158, 5.936056534923045, 3.724379258679478, -1.4427757741863716, 2.033800483057324  …  5.339047977921442, 4.924010342402756, 1.1051788074805757, -7.155263299094693, 2.1151175613179456, 1.4371440995778353, 1.4916887518714412, -1.7523530126411566, 5.845578252046485, 2.7464899232331477])

We can see that we generated the data from our model under $\sigma = 0.5$ ($N = 1000$, $D = 10$). As of version 0.2.0 of AdaptEllipticalSliceSampler.jl, the package is now integrated into the AbstractMCMC.jl framework, allowing us to utilize the Turing ecosystem. Thus, we have two options: (1) we can write a function that calculates the log posterior density or (2) we can specify a Turing.jl model and directly use that to conduct Bayesian computation. While one can easily write a function for evaluating the log_posterior density (while making sure to transform $\sigma^2$ into an unconstrained space), let's look at how to use AGESS using a Turing.jl model. Luckily, Turing.jl (and the AdaptEllipticalSliceSampler.jl package) will automatically transform constrained parameters into an unconstrained space for sampling, then transform it back to the original space before returning the chain back to the user. Thus, in cases where we can use Turing.jl, we can not worry about remembering Jacobians!

@model function linear_regression(X::AbstractMatrix{Y}, y::AbstractVector{Y}) where {Y<:AbstractFloat}
    N, D = size(X)
    # Make sure dimensions conform
    @assert length(y) == N

    # Start with priors
    β ~ MvNormal(zeros(D), I)
    σ² ~ InverseGamma(1.0, 1.0)

    # Specify Likelihood
    y ~ MvNormal(X * β,  σ² * I)

end;

model = linear_regression(X, y)
DynamicPPL.Model{typeof(Main.linear_regression), (:X, :y), (), (), Tuple{Matrix{Float64}, Vector{Float64}}, Tuple{}, DynamicPPL.DefaultContext, false}(Main.linear_regression, (X = [1.171685221802266 -1.177965631527714 … 0.029681358521803834 1.230714630625736; -0.30729971233631087 -0.5501257332792802 … 0.9497156839835412 -0.7604970817954496; … ; 0.9685819653962602 -2.142791626046223 … -0.7609766068757015 0.8187625833900045; 2.1012750843330763 0.08399913120329579 … -0.12142493814515284 -1.8514310605297297], y = [4.2509485214568254, 3.702067516660768, -3.653119910060239, -0.28567345139499334, -2.210986356964136, -0.8846381981280158, 5.936056534923045, 3.724379258679478, -1.4427757741863716, 2.033800483057324  …  5.339047977921442, 4.924010342402756, 1.1051788074805757, -7.155263299094693, 2.1151175613179456, 1.4371440995778353, 1.4916887518714412, -1.7523530126411566, 5.845578252046485, 2.7464899232331477]), NamedTuple(), DynamicPPL.DefaultContext())

Now that we have constructed the model, we can use AGESS to conduct Bayesian computation by simply calling two functions: AGESSSampler() and sample().

n_MCMC = 10_000
sampler = AGESSSampler(model, n_MCMC)
results = sample(model, sampler, n_MCMC)
Chains MCMC chain (10000×12×1 Array{Float64, 3}):

Iterations        = 1:1:10000
Number of chains  = 1
Samples per chain = 10000
Wall duration     = 1.49 seconds
Compute duration  = 1.49 seconds
parameters        = β[1], β[2], β[3], β[4], β[5], β[6], β[7], β[8], β[9], β[10], σ²
internals         = lp

Use `describe(chains)` for summary statistics and quantiles.

We will start by discarding the initial part of the chain due to burn-in and will assess the results using describe().

# Discard first 2500 iterations due to burn-in
results = results[2501:end,:,:]
describe(results)
Chains MCMC chain (7500×12×1 Array{Float64, 3}):

Iterations        = 2501:1:10000
Number of chains  = 1
Samples per chain = 7500
Wall duration     = 1.49 seconds
Compute duration  = 1.49 seconds
parameters        = β[1], β[2], β[3], β[4], β[5], β[6], β[7], β[8], β[9], β[10], σ²
internals         = lp

Summary Statistics

  parameters      mean       std      mcse    ess_bulk    ess_tail      rhat   ⋯
      Symbol   Float64   Float64   Float64     Float64     Float64   Float64   ⋯

        β[1]    1.1570    0.0163    0.0002   5014.8909   3138.4541    1.0005   ⋯
        β[2]   -1.6451    0.0162    0.0002   4895.1858   2854.1499    1.0006   ⋯
        β[3]   -1.6209    0.0162    0.0002   5432.4816   3154.7118    1.0004   ⋯
        β[4]   -0.5915    0.0154    0.0002   4884.7873   3107.5498    0.9999   ⋯
        β[5]    0.4329    0.0159    0.0002   4970.9530   3097.4966    0.9999   ⋯
        β[6]    0.3331    0.0155    0.0002   5273.8825   3225.9524    1.0001   ⋯
        β[7]   -0.5978    0.0156    0.0002   5218.5250   2950.9723    1.0043   ⋯
        β[8]   -1.9606    0.0162    0.0002   5046.8622   2717.6087    1.0005   ⋯
        β[9]   -1.5953    0.0162    0.0002   5237.6577   3366.3075    1.0005   ⋯
       β[10]    1.0340    0.0167    0.0002   5293.3710   3440.4684    1.0003   ⋯
          σ²    0.2576    0.0116    0.0002   4656.1634   3101.5824    1.0002   ⋯

                                                                1 column omitted

Quantiles

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

        β[1]    1.1246    1.1461    1.1572    1.1681    1.1885
        β[2]   -1.6768   -1.6562   -1.6453   -1.6342   -1.6135
        β[3]   -1.6528   -1.6318   -1.6210   -1.6100   -1.5890
        β[4]   -0.6214   -0.6018   -0.5920   -0.5813   -0.5605
        β[5]    0.4016    0.4224    0.4330    0.4437    0.4645
        β[6]    0.3026    0.3229    0.3330    0.3437    0.3631
        β[7]   -0.6281   -0.6082   -0.5977   -0.5876   -0.5666
        β[8]   -1.9929   -1.9719   -1.9605   -1.9494   -1.9289
        β[9]   -1.6272   -1.6062   -1.5954   -1.5841   -1.5638
       β[10]    1.0011    1.0227    1.0341    1.0451    1.0672
          σ²    0.2359    0.2496    0.2572    0.2652    0.2812

Notice how the parameter names that we specified also follow in the subsequent evaluations of the chain–-this holds for calling plot(results), ess(results), autocorplot(results), etc. instead of using the default plotting function, let's visualize only the positive coefficients along with the true value (due to the dimension).

# Plot trace plot
plot(results[:, findall(β .> 0),:])
# Plot true values, one per coefficient's own subplot
true_vals = β[findall(β .> 0)]
hline!(reshape(true_vals, 1, :), subplot = reshape(1:2:2*length(true_vals), 1, :),
       color = :red, linestyle = :dash)

Similarly, we can view the trace plot of $\sigma^2$.

# Plot trace plot, don't forget to transform the transformed variables back
plot(results[:σ²], label = false)
# Plot true value
hline!([0.25], color = :red, label = false)

Lastly, we can plot the log posterior density at every iteration of the Markov chain to potentially detect convergence issues.

plot(results[:lp], legend = false)

High-Dimensional Sparse Linear Regression

In many modern applications, the number of covariates may be larger than the sample size ($N < D$), often requiring regularization in order to achieve good predictive properties. Here we consider the sparse regression setting, where sparsity is induced via continuous shrinkage through global-local shrinkage priors. Here, we consider the horseshoe prior[2] [3]. Specifically, we consider the following model:

\[Y_i \sim \mathcal{N}(\mathbf{x}_i'\boldsymbol{\beta}, \sigma^2)\]

\[\beta_j \sim \mathcal{N}(0, \sigma^2\tau^2\lambda_j^2) \;\;\;\;\; p(\sigma^2) \propto \frac{1}{\sigma^2},\]

\[\tau \sim C^+(0,1) \;\;\;\;\; \lambda_j \sim C^+(0,1),\]

for $i = 1, \dots, N$ and $j = 1, \dots, D$, where $C^+$ denotes the half-Cauchy distribution. We will first start by generating data, where the design matrix has correlated covariates. We will consider the case where we have 25 observations and 25 covariates. Here we will generate a dataset where the probability of $\beta_j = 0$ is $0.9$ ($1 \le j \le 25$).

function gen_data_AR1(N::T, P::T; sparsity::Y = 0.8, ρ::Y = 0.2,
                      σ_sq::Y = 1.0) where {Y<:AbstractFloat, T<:Integer}
    Σ = ones(P, P)
    for i in 1:P
      for j in 1:P
        Σ[i,j] = ρ^(abs(i - j))
      end
    end
    Σ[diagind(Σ)] .= 1
    X = zeros(N, P)
    X .= rand(MultivariateNormal(zeros(P), Σ), N)'
    β = zeros(P)
    for i in 1:P
        if rand(Bernoulli(1 - sparsity)) == 1
          β[i] = (rand() * 3 + 1) * (-1)^i
        end
    end
    if sum(β) == 0
      β[1] = (rand() * 3 + 1) * (-1)^1
    end

    Y_obs = rand(MultivariateNormal(X * β, σ_sq * diagm(ones(N))))

    return X, Y_obs, β
end

Random.seed!(123)

N = 50
D = 25

X, y, β = gen_data_AR1(N, D, ρ = 0.7, sparsity = 0.9, σ_sq = 1.0)
([0.8082879284649668 -0.2355185009078168 … 2.1952434858659013 1.4455194595375302; 0.4694770042412599 -0.7617789966031957 … -0.8139537433814701 0.32963692676718603; … ; 0.8997726927030626 -0.13588404285163935 … -1.0202408659575293 -0.7975464687328946; -0.13619889472272229 -0.48904002253757134 … -1.0667748660476324 -0.24415843476965626], [-7.087267835848337, 5.205687034665962, -2.9744791427205506, 2.721384400176828, -9.042270062149427, -5.781077205943315, 5.9846966163908295, 3.54759428164667, 6.227456840612067, 2.4912291860230695  …  -11.072039215600881, 4.196282397376866, 3.2177888069892715, 3.9834053074485922, -9.839587460552332, -2.686279615079939, 0.2743214758522353, 0.6333020085530112, 9.189151700095069, 7.141090576424618], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.7980509144925407  …  2.9162735141224414, 0.0, 0.0, 0.0, 0.0, -1.176280495800773, 0.0, 0.0, 0.0, 0.0])

We visualize the correlation structure of the design matrix.

heatmap(cor(X))

We can also view the values of $\boldsymbol{\beta}$.

scatter(β,legend = false)

Specification of the Model

While we could directly specify a function evaluating the log posterior density (remembering to log-transform $\tau$ and $\lambda$), we will instead simply specify the model using the Turing.jl framework.

@model function HS_regression(X::AbstractMatrix{Y}, y::AbstractVector{Y}) where {Y<:AbstractFloat}
    N,D = size(X)
    @assert length(y) == N

    # Define Half-Cauchy distribution
    half_cauchy = truncated(Cauchy(0, 1); lower=0)

    # Priors
    τ ~ half_cauchy                 # Global Shrinkage param
    λ ~ filldist(half_cauchy, D)    # Local Shrinkage param
    log_σ² ~ Flat()                 # Flat prior to create Jeffrey's prior
    σ² = exp(log_σ²)
    β ~ MvNormal(zeros(D), σ² .* Diagonal((λ .* τ).^2))

    # Likelihood
    y ~ MvNormal(X * β, σ² * I)
end

model = HS_regression(X, y)
DynamicPPL.Model{typeof(Main.HS_regression), (:X, :y), (), (), Tuple{Matrix{Float64}, Vector{Float64}}, Tuple{}, DynamicPPL.DefaultContext, false}(Main.HS_regression, (X = [0.8082879284649668 -0.2355185009078168 … 2.1952434858659013 1.4455194595375302; 0.4694770042412599 -0.7617789966031957 … -0.8139537433814701 0.32963692676718603; … ; 0.8997726927030626 -0.13588404285163935 … -1.0202408659575293 -0.7975464687328946; -0.13619889472272229 -0.48904002253757134 … -1.0667748660476324 -0.24415843476965626], y = [-7.087267835848337, 5.205687034665962, -2.9744791427205506, 2.721384400176828, -9.042270062149427, -5.781077205943315, 5.9846966163908295, 3.54759428164667, 6.227456840612067, 2.4912291860230695  …  -11.072039215600881, 4.196282397376866, 3.2177888069892715, 3.9834053074485922, -9.839587460552332, -2.686279615079939, 0.2743214758522353, 0.6333020085530112, 9.189151700095069, 7.141090576424618]), NamedTuple(), DynamicPPL.DefaultContext())

Running AGESS

Similarly to before, once we have constructed the model, we can sample by simply calling two functions.

# Specify the number of MCMC iterations
n_MCMC = 100_000

# Run AGESS
sampler = AGESSSampler(model, n_MCMC)
results = sample(model, sampler, n_MCMC)
Chains MCMC chain (100000×53×1 Array{Float64, 3}):

Iterations        = 1:1:100000
Number of chains  = 1
Samples per chain = 100000
Wall duration     = 7.3 seconds
Compute duration  = 7.3 seconds
parameters        = τ, λ[1], λ[2], λ[3], λ[4], λ[5], λ[6], λ[7], λ[8], λ[9], λ[10], λ[11], λ[12], λ[13], λ[14], λ[15], λ[16], λ[17], λ[18], λ[19], λ[20], λ[21], λ[22], λ[23], λ[24], λ[25], log_σ², β[1], β[2], β[3], β[4], β[5], β[6], β[7], β[8], β[9], β[10], β[11], β[12], β[13], β[14], β[15], β[16], β[17], β[18], β[19], β[20], β[21], β[22], β[23], β[24], β[25]
internals         = lp

Use `describe(chains)` for summary statistics and quantiles.

After running AGESS, we can view the trace plots of the non-zero coefficients.

# Discard initial 25% of chain due to burn-in
results = results[25_001:end,:,:]

# Get β parameters
β_samps = get(results, :β)
p = plot()
for i in findall(β .!= 0)
    p = plot!(β_samps.β[i][1:10:end], legend = false)
end

p
hline!(β[findall(β .!= 0)], line = :dash, color =:black)

We can also view the trace plots of the coefficients that are equal to zero.

p = plot()
for i in findall(β .== 0)
    p = plot!(β_samps.β[i][1:10:end], legend = false)
end

p
hline!([0.0], line = :dash, color =:black)

Next we will view the trace plot of $\sigma^2$.

plot(exp.(results[:log_σ²][1:10:end]), legend = false)
hline!([1], line = :dash, color =:black)

Lastly, we can plot the log pdf of the posterior at every iteration of the Markov chain to potentially detect convergence issues.

plot(results[:lp], legend = false)

Generalized ReLU Regression

As discussed in the main manuscript[1], AGESS can handle non-differentiable target functions. In this section of the tutorial, we consider a generalized regression setting where the posterior distribution is not differentiable everywhere. Therefore, alternative samplers such as HMC[4] are not suitable for these types of target distributions. Let $\mathbf{Y} \in \mathbb{R}^N$ be the response variable and $\mathbf{X} \in \mathbb{R}^{N \times D}$ be the covariates of interest. Consider the following model, inspired by density discontinuity modeling[5]:

\[Y_i \sim Bernoulli(\Phi(\mu_i)) \;\;\;\;\;\Phi(z) = \frac{e^z}{1 + e^z} \;\;\;\;\; \mu_i = \max(0, \mathbf{x}_i'\boldsymbol{\beta}),\]

for $i = 1, \dots, N$, where $\boldsymbol{\beta} \sim \mathcal{N}_{D}(\mathbf{0}, \mathbf{I})$. We will first start by specifying a function to generate data from our model. We will consider the simple case of when $D = 2$, allowing for easy visualization of the target distribution.

function generate_data(N::T, P::T, ν::Y = 6.0) where {Y<:AbstractFloat, T<:Integer}
    β = randn(P) * (2 * log(P))^(1.0 / 4)
    β .*= sqrt(rand(Gamma(ν/2, 2/ ν)))
    x::Matrix{typeof(ν)} = randn(N, P) .+ randn() * 0.5
    μ = zeros(N)
    y::Vector{typeof(N)} = zeros(Int64, N)

    for i in 1:N
        μ[i] = max(0, dot(x[i,:], β))
        y[i] = rand(Binomial(1, LogExpFunctions.logistic(μ[i])), 1)[1]
    end


    return β, x, μ, y
end

Random.seed!(123)

D = 2
N = 1000
β, x, μ, y = generate_data(N, D)
([-0.3611372440159916, -0.8183513546797343], [0.25957106302208094 -0.9556035799933374; 0.20180178045009275 -0.4422196885740821; … ; 0.9358808315839681 -0.06709585879776; -0.42646210250593763 0.010337450394495056], [0.6882787058982559, 0.28901294238138364, 0.2554305477921972, 0.9893159580995416, 0.0, 1.19171914741668, 0.0, 1.2432768875517346, 0.0, 0.27356355703349333  …  0.32521446983604785, 0.0, 0.0, 0.0, 0.4929140191342167, 0.9870706892189209, 0.0, 1.409099581139658, 0.0, 0.14555168184199005], [1, 1, 1, 1, 1, 1, 1, 1, 1, 0  …  1, 1, 0, 1, 0, 1, 0, 1, 1, 0])

Specification of the Log Posterior Density

Now that we have generated the data, we can construct a function which evaluates the posterior log pdf. As in the previous sections, it is important that we specify a function that is efficiently implemented. We will first start by constructing a function that evaluates the log likelihood, and then a function that evaluates the log posterior density.

function log_likelihood(β::AbstractVector{Y}, x::Matrix{Y},
                        y::Vector{T}) where {Y <:AbstractFloat, T<:Integer}
    log_lik::Float64 = 0.0
    z::Float64 = 0.0
    for i in eachindex(y)
        @views z = dot(x[i,:], β)
        if z < 0.0
            z = 0.0
        end
        log_lik -= log1p(exp(-(sign(y[i] - 0.5) * z)))
    end

    return log_lik
end

function log_posterior_ReLU(β::AbstractVector{Y}, x::Matrix{Y},
                            y::Vector{T}) where {Y <:AbstractFloat, T<:Integer}
    log_lik::Float64 = log_likelihood(β, x, y) - 0.5 * dot(β, β)
    return log_lik
end
log_posterior_ReLU (generic function with 1 method)

Running AGESS

Now that we have specified a function to efficiently evaluate the log posterior density, we can use the AGESS function to generate samples from the posterior distribution.

# Specify the dimension of the target distribution
P = D
# Specify the number of MCMC iterations
n_MCMC = 10_000

# Let's specify the param_names this time
param_names = [string("β_",i) for i in 1:D]
# Run AGESS
results = AGESS(β -> log_posterior_ReLU(β, x, y), n_MCMC, P; param_names = param_names)
Chains MCMC chain (10000×3×1 Array{Float64, 3}):

Iterations        = 1:1:10000
Number of chains  = 1
Samples per chain = 10000
Wall duration     = 1.28 seconds
Compute duration  = 1.28 seconds
parameters        = β_1, β_2
internals         = lp

Use `describe(chains)` for summary statistics and quantiles.

Discard the initial 2500 samples to burnin

results = results[2501:end,:,:]
describe(results)
Chains MCMC chain (7500×3×1 Array{Float64, 3}):

Iterations        = 2501:1:10000
Number of chains  = 1
Samples per chain = 7500
Wall duration     = 1.28 seconds
Compute duration  = 1.28 seconds
parameters        = β_1, β_2
internals         = lp

Summary Statistics

  parameters      mean       std      mcse    ess_bulk    ess_tail      rhat   ⋯
      Symbol   Float64   Float64   Float64     Float64     Float64   Float64   ⋯

         β_1   -0.3665    0.0946    0.0012   6078.8333   4057.4212    1.0001   ⋯
         β_2   -0.8262    0.1063    0.0013   6274.1341   4111.5716    0.9999   ⋯

                                                                1 column omitted

Quantiles

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

         β_1   -0.5564   -0.4274   -0.3656   -0.3052   -0.1828
         β_2   -1.0420   -0.8947   -0.8252   -0.7533   -0.6242

Using the output of AGESS, we can visualize the samples from the target distribution.

scatter(results[:β_1], results[:β_2], alpha = 0.1,
        legend = false)
scatter!([β[1]], [β[2]], color = "red")

Additionally, we can plot the log pdf of the posterior at every iteration of the Markov chain to potentially detect convergence issues.

plot(results[:lp], legend = false)

Conclusion

Adaptive generalized elliptical slice sampling[1] can be utilized in many generalized regression settings, including target distributions that are non-differentiable. While limited by computational resources in this tutorial, AGESS can be used to sample from relatively high-dimensional target distributions. Comparisons between AGESS and alternative samplers can be found in the main manuscript[1].

# Free the sampled chains before the next tutorial's page is built.
results = β_samps = nothing
GC.gc()

This page was generated using Literate.jl.

  • 1N. Marco and S. T. Tokdar. Adaptive generalized elliptical slice sampling. arXiv preprint arXiv:2605.21659, 2026.
  • 2C. M. Carvalho, N. G. Polson, and J. G. Scott. Handling sparsity via the horseshoe. In Artificial intelligence and statistics, pages 73–80. PMLR, 2009.
  • 3C. M. Carvalho, N. G. Polson, and J. G. Scott. The horseshoe estimator for sparse signals. Biometrika, pages 465–480, 2010.
  • 4M. Betancourt. A conceptual introduction to hamiltonian monte carlo. arXiv preprint arXiv:1701.02434, 2017.
  • 5S. T. Tokdar, R. Sen, H. Zheng, and S. Zhang. Density discontinuity regression. arXiv preprint arXiv:2507.05581, 2025.