Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adaptive Metropolis algorithm #57

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ version = "0.6.0"
[deps]
AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
PDMats = "90014a1f-27ba-587c-ab20-58faa44d9150"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a [compat] entry for PDMats?

Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"

Expand Down
7 changes: 6 additions & 1 deletion src/AdvancedMH.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module AdvancedMH
using AbstractMCMC
using Distributions
using Requires
using LinearAlgebra
using PDMats

import Random

Expand All @@ -19,7 +21,9 @@ export
SymmetricRandomWalkProposal,
Ensemble,
StretchProposal,
MALA
MALA,
AMProposal,
AMSampler
kaandocal marked this conversation as resolved.
Show resolved Hide resolved

# Reexports
export sample, MCMCThreads, MCMCDistributed
Expand Down Expand Up @@ -120,5 +124,6 @@ end
include("proposal.jl")
include("mh-core.jl")
include("emcee.jl")
include("adaptivemetropolis.jl")

end # module AdvancedMH
56 changes: 56 additions & 0 deletions src/adaptivemetropolis.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Simple Adaptive Metropolis Proposal
# The proposal at each step is equal to a scalar multiple
# of the empirical posterior covariance plus a fixed, small covariance
# matrix epsilon which is also used for initial exploration.
#
# Reference:
# H. Haario, E. Saksman, and J. Tamminen, "An adaptive Metropolis algorithm",
# Bernoulli 7(2): 223-242 (2001)
mutable struct AMProposal{FT <: Real, CT <: AbstractMatrix{FT},
MNT <: AbstractMvNormal} <: Proposal{MNT}
epsilon::CT
scalefactor::FT
proposal::MNT
μ::Vector{FT}
M::Matrix{FT}
δ::Vector{FT}
N::Int
end

function AMProposal(epsilon::AbstractMatrix{FT},
scalefactor=FT(2.38^2 / size(epsilon, 1))) where {FT <: Real}
kaandocal marked this conversation as resolved.
Show resolved Hide resolved
sym = PDMat(Symmetric(epsilon))
proposal = MvNormal(zeros(size(sym, 1)), sym)
AMProposal(sym, scalefactor, proposal, zeros(size(sym,1)), zeros(size(sym)...),
zeros(size(sym,1)), 0)
end

logratio_proposal_density(::AMProposal, params_prev, params) = 0

# When the proposal is initialised the empirical posterior covariance is zero
function trackstep!(proposal::AMProposal, params)
proposal.μ .= params
proposal.M .= 0
proposal.proposal = MvNormal(zeros(size(proposal.μ)), proposal.epsilon)
proposal.N = 1
end

# Recompute the empirical posterior covariance matrix
function trackstep!(proposal::AMProposal, params, ::Union{Val{true}, Val{false}})
proposal.N += 1
proposal.δ .= params .- proposal.μ
proposal.μ .+= proposal.δ ./ proposal.N

mul!(proposal.M, params .- proposal.μ, proposal.δ', 1.0, 1.0)
kaandocal marked this conversation as resolved.
Show resolved Hide resolved

prop_cov = proposal.M .* proposal.scalefactor ./ proposal.N .+ proposal.epsilon
proposal.proposal = MvNormal(mean(proposal.proposal), Symmetric(prop_cov))
end

function propose(rng::Random.AbstractRNG, p::AMProposal, ::DensityModel)
return rand(rng, p.proposal)
end

function propose(rng::Random.AbstractRNG, p::AMProposal, ::DensityModel, t)
return t + rand(rng, p.proposal)
end
19 changes: 19 additions & 0 deletions src/mh-core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,22 @@ end
transition(sampler, model, params) = transition(model, params)
transition(model, params) = Transition(model, params)

# Called to update proposal when the first sample is drawn
trackstep!(proposal::Proposal, params) = nothing
trackstep!(proposal::AbstractArray, params) = foreach(trackstep!, proposal, params)
trackstep!(proposal::NamedTuple, params) = foreach(trackstep!, proposal, params)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameters in the NamedTuple params are not necessarily in the same order as the proposal.


# Called to update proposal when a new step is performed
# The last argument determines if the step is an acceptance step
trackstep!(proposal::Proposal, params,
::Union{Val{true}, Val{false}}) = nothing

trackstep!(proposal::AbstractArray, params, accept::Union{Val{true},Val{false}}) =
foreach((prop, par) -> trackstep!(prop, par, accept), proposal, params)

trackstep!(proposal::NamedTuple, params, accept::Union{Val{true},Val{false}}) =
foreach((prop, par) -> trackstep!(prop, par, accept), proposal, params)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove the type annotations on accept? They are not needed.

Suggested change
trackstep!(proposal::Proposal, params,
::Union{Val{true}, Val{false}}) = nothing
trackstep!(proposal::AbstractArray, params, accept::Union{Val{true},Val{false}}) =
foreach((prop, par) -> trackstep!(prop, par, accept), proposal, params)
trackstep!(proposal::NamedTuple, params, accept::Union{Val{true},Val{false}}) =
foreach((prop, par) -> trackstep!(prop, par, accept), proposal, params)
trackstep!(proposal::Proposal, params, accept) = nothing
function trackstep!(proposal::AbstractArray, params, accept)
return foreach((prop, par) -> trackstep!(prop, par, accept), proposal, params)
end
function trackstep!(proposal::NamedTuple, params, accept)
return foreach((prop, par) -> trackstep!(prop, par, accept), proposal, params)
end

Additionally, again the NamedTuple implementation is not completely correct.


# Define the first sampling step.
# Return a 2-tuple consisting of the initial sample and the initial state.
# In this case they are identical.
Expand All @@ -157,6 +173,7 @@ function AbstractMCMC.step(
transition = AdvancedMH.transition(spl, model, init_params)
end

trackstep!(spl.proposal, transition.params)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The step implementations are written for generic AbstractTransitions and MHSampler, so neither spl.proposal nor transition.params might exist.

return transition, transition
end

Expand All @@ -180,8 +197,10 @@ function AbstractMCMC.step(

# Decide whether to return the previous params or the new one.
if -Random.randexp(rng) < logα
trackstep!(spl.proposal, params.params, Val(true))
return params, params
else
trackstep!(spl.proposal, params_prev.params, Val(false))
return params_prev, params_prev
end
end
Expand Down
30 changes: 28 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ using AdvancedMH
using Distributions
using StructArrays
using MCMCChains
using LinearAlgebra

using Random
using Test
Expand Down Expand Up @@ -57,6 +58,32 @@ include("util.jl")
@test mean(chain2.σ) ≈ 1.0 atol=0.1
end

@testset "AdaptiveMH" begin
# Set up our sampler with initial parameters.
doubledensity(θ::Vector{Float64}) = density(θ[1:2]) + density(θ[3:4])
doubledensity(θ::Vector{Vector{Float64}}) = density(θ[1]) + density(θ[2])
doublemodel = DensityModel(doubledensity)

spl1 = MetropolisHastings(AMProposal(diagm(0 => [0.01, 0.01, 0.01, 0.01])))
spl2 = MetropolisHastings([AMProposal(diagm(0 => [0.01, 0.01])),
AMProposal(diagm(0 => [0.01, 0.01]))])

# Sample from the posterior.
chain1 = sample(doublemodel, spl1, 100000; chain_type=StructArray,
param_names=["μ1", "σ1", "μ2", "σ2"])
chain2 = sample(doublemodel, spl2, 100000; chain_type=StructArray,
param_names=["p1", "p2"])

# chn_mean ≈ dist_mean atol=atol_v
@test mean(chain1.μ1) ≈ 0.0 atol=0.1
@test mean(chain1.σ1) ≈ 1.0 atol=0.1
@test mean(chain1.μ2) ≈ 0.0 atol=0.1
@test mean(chain1.σ2) ≈ 1.0 atol=0.1

@test mean(chain2.p1) ≈ [ 0.0 1.0 ]' atol=0.1
@test mean(chain2.p2) ≈ [ 0.0 1.0 ]' atol=0.1
end

@testset "parallel sampling" begin
spl1 = StaticMH([Normal(0,1), Normal(0, 1)])

Expand Down Expand Up @@ -108,7 +135,7 @@ include("util.jl")
@test keys(c3[1]) == (:a, :b, :lp)
@test keys(c4[1]) == (:param_1, :lp)
end

kaandocal marked this conversation as resolved.
Show resolved Hide resolved
@testset "Initial parameters" begin
# Set up our sampler with initial parameters.
spl1 = StaticMH([Normal(0,1), Normal(0, 1)])
Expand Down Expand Up @@ -207,5 +234,4 @@ include("util.jl")
end

@testset "EMCEE" begin include("emcee.jl") end

end