Skip to content

Commit 2028fb2

Browse files
authored
Merge pull request #3 from devmotion/abstractmcmc
Switch to AbstractMCMC interface
2 parents a80fbc9 + 043564f commit 2028fb2

12 files changed

+332
-210
lines changed

Project.toml

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
name = "EllipticalSliceSampling"
22
uuid = "cad2338a-1db2-11e9-3401-43bc07c9ede2"
3-
authors = ["David Widmann <dev+github@devmotion.de>"]
4-
version = "0.1.0"
3+
authors = ["David Widmann <dev+git@devmotion.de>"]
4+
version = "0.2.0"
55

66
[deps]
7+
AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001"
78
ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"
89
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
9-
Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a"
10-
ProgressLogging = "33c8b6b6-d38a-422a-b730-caa89a2f386c"
1110
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
11+
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1212

1313
[compat]
14+
AbstractMCMC = "0.5"
1415
ArrayInterface = "2"
15-
Distributions = "0.21.8"
16-
Parameters = "0.12"
17-
ProgressLogging = "0.1"
16+
Distributions = "0.22"
1817
julia = "1"
1918

2019
[extras]

README.md

+12-6
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,22 @@ priors with non-zero means and handle the change of variables internally.
2424

2525
Probably most users would like to use the exported function
2626
```julia
27-
ESS_mcmc([rng::AbstracRNG, ]prior, loglikelihood, N::Int[; burnin::Int = 0])
27+
ESS_mcmc([rng, ]prior, loglikelihood, N[; kwargs...])
2828
```
2929
which returns a vector of `N` samples for approximating the posterior of
3030
a model with a Gaussian prior that allows sampling from the `prior` and
31-
evaluation of the log likelihood `loglikelihood`. The burn-in phase with
32-
`burnin` samples is discarded.
31+
evaluation of the log likelihood `loglikelihood`.
3332

3433
If you want to have more control about the sampling procedure (e.g., if you
3534
only want to save a subset of samples or want to use another stopping
3635
criterion), the function
3736
```julia
38-
ESS_mcmc_sampler([rng::AbstractRNG, ]prior, loglikelihood)
37+
AbstractMCMC.steps!(
38+
[rng,]
39+
EllipticalSliceSampling.Model(prior, loglikelihood),
40+
EllipticalSliceSampling.EllipticalSliceSampler();
41+
kwargs...
42+
)
3943
```
4044
gives you access to an iterator from which you can generate an unlimited
4145
number of samples.
@@ -56,9 +60,11 @@ use a custom distribution type `GaussianPrior`, the following methods should be
5660
implemented:
5761
```julia
5862
# state that the distribution is actually Gaussian
59-
EllipticalSliceSampling.isnormal(::Type{<:GaussianPrior}) = true
63+
EllipticalSliceSampling.isgaussian(::Type{<:GaussianPrior}) = true
6064

6165
# define the mean of the distribution
66+
# alternatively implement `proposal(prior, ...)` and
67+
# `proposal!(out, prior, ...)` (only if the samples are mutable)
6268
Statistics.mean(dist::GaussianPrior) = ...
6369

6470
# define how to sample from the distribution
@@ -87,7 +93,7 @@ be useful.
8793
### Progress monitor
8894

8995
If you use a package such as [Juno](https://junolab.org/) or
90-
[ConsoleProgressMonitor.jl](https://github.com/tkf/ConsoleProgressMonitor.jl) that supports
96+
[TerminalLoggers.jl](https://github.com/c42f/TerminalLoggers.jl) that supports
9197
progress logs created by the
9298
[ProgressLogging.jl](https://github.com/JunoLab/ProgressLogging.jl) API, then you can
9399
monitor the progress of the sampling algorithm.

src/EllipticalSliceSampling.jl

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
module EllipticalSliceSampling
22

3-
using ArrayInterface
4-
using Distributions
5-
using Parameters
6-
using ProgressLogging
3+
import AbstractMCMC
4+
import ArrayInterface
5+
import Distributions
76

8-
using Random
7+
import Random
8+
import Statistics
99

10-
export ESS_mcmc, ESS_mcmc_sampler
10+
export ESS_mcmc
1111

12-
include("utils.jl")
13-
include("types.jl")
14-
include("iterator.jl")
12+
include("abstractmcmc.jl")
13+
include("model.jl")
14+
include("distributions.jl")
1515
include("interface.jl")
1616

1717
end # module

src/abstractmcmc.jl

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# elliptical slice sampler
2+
struct EllipticalSliceSampler <: AbstractMCMC.AbstractSampler end
3+
4+
# state of the elliptical slice sampler
5+
struct EllipticalSliceSamplerState{S,L}
6+
"Sample of the elliptical slice sampler."
7+
sample::S
8+
"Log-likelihood of the sample."
9+
loglikelihood::L
10+
end
11+
12+
# first step of the elliptical slice sampler
13+
function AbstractMCMC.step!(
14+
rng::Random.AbstractRNG,
15+
model::AbstractMCMC.AbstractModel,
16+
::EllipticalSliceSampler,
17+
N::Integer,
18+
::Nothing;
19+
kwargs...
20+
)
21+
# initial sample from the Gaussian prior
22+
f = initial_sample(rng, model)
23+
24+
# compute log-likelihood of the initial sample
25+
loglikelihood = Distributions.loglikelihood(model, f)
26+
27+
return EllipticalSliceSamplerState(f, loglikelihood)
28+
end
29+
30+
# subsequent steps of the elliptical slice sampler
31+
function AbstractMCMC.step!(
32+
rng::Random.AbstractRNG,
33+
model::AbstractMCMC.AbstractModel,
34+
::EllipticalSliceSampler,
35+
N::Integer,
36+
state::EllipticalSliceSamplerState;
37+
kwargs...
38+
)
39+
# sample from Gaussian prior
40+
ν = sample_prior(rng, model)
41+
42+
# sample log-likelihood threshold
43+
loglikelihood = state.loglikelihood
44+
threshold = loglikelihood - Random.randexp(rng)
45+
46+
# sample initial angle
47+
θ = 2 * π * rand(rng)
48+
θmin = θ - 2 * π
49+
θmax = θ
50+
51+
# compute the proposal
52+
f = state.sample
53+
fnext = proposal(model, f, ν, θ)
54+
55+
# compute the log-likelihood of the proposal
56+
loglikelihood = Distributions.loglikelihood(model, fnext)
57+
58+
# stop if the log-likelihood threshold is reached
59+
while loglikelihood < threshold
60+
# shrink the bracket
61+
if θ < zero(θ)
62+
θmin = θ
63+
else
64+
θmax = θ
65+
end
66+
67+
# sample angle
68+
θ = θmin + rand(rng) * (θmax - θmin)
69+
70+
# recompute the proposal
71+
if ArrayInterface.ismutable(fnext)
72+
proposal!(fnext, model, f, ν, θ)
73+
else
74+
fnext = proposal(model, f, ν, θ)
75+
end
76+
77+
# compute the log-likelihood of the proposal
78+
loglikelihood = Distributions.loglikelihood(model, fnext)
79+
end
80+
81+
return EllipticalSliceSamplerState(fnext, loglikelihood)
82+
end
83+
84+
# only save the samples by default
85+
function AbstractMCMC.transitions_init(
86+
state::EllipticalSliceSamplerState,
87+
model::AbstractMCMC.AbstractModel,
88+
::EllipticalSliceSampler,
89+
N::Integer;
90+
kwargs...
91+
)
92+
return Vector{typeof(state.sample)}(undef, N)
93+
end
94+
95+
function AbstractMCMC.transitions_save!(
96+
samples::AbstractVector{S},
97+
iteration::Integer,
98+
state::EllipticalSliceSamplerState{S},
99+
model::AbstractMCMC.AbstractModel,
100+
::EllipticalSliceSampler,
101+
N::Integer;
102+
kwargs...
103+
) where S
104+
samples[iteration] = state.sample
105+
return
106+
end

src/distributions.jl

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# define the element type of the samples
2+
randtype(::Type{D}) where {D<:Distributions.MultivariateDistribution} = Vector{eltype(D)}
3+
randtype(::Type{D}) where {D<:Distributions.MatrixDistribution} = Matrix{eltype(D)}
4+
function randtype(
5+
::Type{D}
6+
) where {D<:Distributions.Sampleable{Distributions.Multivariate}}
7+
return Vector{eltype(D)}
8+
end
9+
function randtype(
10+
::Type{D}
11+
) where {D<:Distributions.Sampleable{Distributions.Matrixvariate}}
12+
return Matrix{eltype(D)}
13+
end
14+
15+
# define trait for Gaussian distributions
16+
isgaussian(::Type{<:Distributions.Normal}) = true
17+
isgaussian(::Type{<:Distributions.NormalCanon}) = true
18+
isgaussian(::Type{<:Distributions.AbstractMvNormal}) = true
19+
20+
# compute the proposal of the next sample
21+
function proposal(prior::Distributions.Normal, f::Real, ν::Real, θ)
22+
sinθ, cosθ = sincos(θ)
23+
μ = prior.μ
24+
if iszero(μ)
25+
return cosθ * f + sinθ * ν
26+
else
27+
a = 1 - (sinθ + cosθ)
28+
return cosθ * f + sinθ * ν + a * μ
29+
end
30+
end
31+
32+
function proposal(
33+
prior::Distributions.MvNormal,
34+
f::AbstractVector{<:Real},
35+
ν::AbstractVector{<:Real},
36+
θ
37+
)
38+
sinθ, cosθ = sincos(θ)
39+
a = 1 - (sinθ + cosθ)
40+
return @. cosθ * f + sinθ * ν + a * prior.μ
41+
end
42+
43+
function proposal!(
44+
out::AbstractVector{<:Real},
45+
prior::Distributions.MvNormal,
46+
f::AbstractVector{<:Real},
47+
ν::AbstractVector{<:Real},
48+
θ
49+
)
50+
sinθ, cosθ = sincos(θ)
51+
a = 1 - (sinθ + cosθ)
52+
@. out = cosθ * f + sinθ * ν + a * prior.μ
53+
return out
54+
end

src/interface.jl

+67-28
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,72 @@
1-
# perform elliptical slice sampling for a fixed number of iterations
2-
ESS_mcmc(prior, loglikelihood, N::Int; kwargs...) =
3-
ESS_mcmc(Random.GLOBAL_RNG, prior, loglikelihood, N; kwargs...)
1+
# public interface
42

5-
function ESS_mcmc(rng::AbstractRNG, prior, loglikelihood, N::Int; burnin::Int = 0)
6-
# define the internal model
3+
"""
4+
ESS_mcmc([rng, ]prior, loglikelihood, N; kwargs...)
5+
6+
Create a Markov chain of `N` samples for a model with given `prior` and `loglikelihood`
7+
functions using the elliptical slice sampling algorithm.
8+
"""
9+
function ESS_mcmc(
10+
rng::Random.AbstractRNG,
11+
prior,
12+
loglikelihood,
13+
N::Integer;
14+
kwargs...
15+
)
716
model = Model(prior, loglikelihood)
17+
return AbstractMCMC.sample(rng, model, EllipticalSliceSampler(), N; kwargs...)
18+
end
19+
20+
function ESS_mcmc(prior, loglikelihood, N::Integer; kwargs...)
21+
return ESS_mcmc(Random.GLOBAL_RNG, prior, loglikelihood, N; kwargs...)
22+
end
23+
24+
# private interface
25+
26+
"""
27+
initial_sample(rng, model)
28+
29+
Return the initial sample for the `model` using the random number generator `rng`.
830
9-
# create the sampler
10-
sampler = EllipticalSliceSampler(rng, model)
11-
12-
# create MCMC chain
13-
chain = Vector{eltype(sampler)}(undef, N)
14-
niters = N + burnin
15-
@withprogress name = "Performing elliptical slice sampling" begin
16-
# discard burnin phase
17-
for (i, _) in zip(1:burnin, sampler)
18-
@logprogress i / niters
19-
end
20-
21-
for (i, f) in zip(1:N, sampler)
22-
@inbounds chain[i] = f
23-
@logprogress (i + burnin) / niters
24-
end
25-
end
26-
27-
chain
31+
By default, sample from the prior by calling [`sample_prior(rng, model)`](@ref).
32+
"""
33+
function initial_sample(rng::Random.AbstractRNG, model::AbstractMCMC.AbstractModel)
34+
return sample_prior(rng, model)
2835
end
2936

30-
# create an elliptical slice sampler
31-
ESS_mcmc_sampler(prior, loglikelihood) = ESS_mcmc_sampler(Random.GLOBAL_RNG, prior, loglikelihood)
32-
ESS_mcmc_sampler(rng::AbstractRNG, prior, loglikelihood) =
33-
EllipticalSliceSampler(rng, Model(prior, loglikelihood))
37+
"""
38+
sample_prior(rng, model)
39+
40+
Sample from the prior of the `model` using the random number generator `rng`.
41+
"""
42+
function sample_prior(::Random.AbstractRNG, ::AbstractMCMC.AbstractModel) end
43+
44+
"""
45+
proposal(model, f, ν, θ)
46+
47+
Compute the proposal for the next sample in the elliptical slice sampling algorithm for the
48+
`model` from the previous sample `f`, the sample `ν` from the Gaussian prior, and the angle
49+
`θ`.
50+
51+
Mathematically, the proposal can be computed as
52+
```math
53+
\\cos θ f + ν \\sin θ ν + μ (1 - \\sin θ + \\cos θ),
54+
```
55+
where ``μ`` is the mean of the Gaussian prior.
56+
"""
57+
function proposal(model::AbstractMCMC.AbstractModel, f, ν, θ) end
58+
59+
"""
60+
proposal!(out, model, f, ν, θ)
61+
62+
Compute the proposal for the next sample in the elliptical slice sampling algorithm for the
63+
`model` from the previous sample `f`, the sample `ν` from the Gaussian prior, and the angle
64+
`θ`, and save it to `out`.
65+
66+
Mathematically, the proposal can be computed as
67+
```math
68+
\\cos θ f + ν \\sin θ ν + μ (1 - \\sin θ + \\cos θ),
69+
```
70+
where ``μ`` is the mean of the Gaussian prior.
71+
"""
72+
function proposal!(out, model::AbstractMCMC.AbstractModel, f, ν, θ) end

0 commit comments

Comments
 (0)