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 proposals #39

Open
wants to merge 34 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e9fd602
adaptive proposal
Nov 22, 2020
5f0ddfa
Adaptive univariate proposals tests
Nov 22, 2020
f42b784
swp files from vim...
Nov 22, 2020
a188780
test ESS
Nov 22, 2020
d8989aa
export AdaptiveProposal
Nov 22, 2020
565f12a
Update src/adaptive.jl
arzwa Nov 22, 2020
cc16195
Update src/adaptive.jl
arzwa Nov 22, 2020
802ec67
Update src/adaptive.jl
arzwa Nov 22, 2020
c7623c4
Update src/adaptive.jl
arzwa Nov 22, 2020
a33937e
Update src/adaptive.jl
arzwa Nov 22, 2020
4007bd0
Update src/adaptive.jl
arzwa Nov 22, 2020
71e010b
Update src/adaptive.jl
arzwa Nov 22, 2020
2d59ede
Update src/adaptive.jl
arzwa Nov 22, 2020
279aea7
Update src/adaptive.jl
arzwa Nov 22, 2020
93f17c5
Update src/adaptive.jl
arzwa Nov 23, 2020
16715e1
Update src/adaptive.jl
arzwa Nov 23, 2020
046c21b
Update src/adaptive.jl
arzwa Nov 23, 2020
b91fcc0
test update
Nov 23, 2020
387eff4
Merge branch 'adaptive' of https://github.com/arzwa/AdvancedMH.jl int…
Nov 23, 2020
8fb1048
Adaptor docstring
Nov 23, 2020
4071675
Update test/runtests.jl
arzwa Nov 23, 2020
a63262d
updated tests
Nov 23, 2020
afd3ed1
updated tests
Nov 23, 2020
fe8562c
Merge branch 'adaptive' of https://github.com/arzwa/AdvancedMH.jl int…
Nov 23, 2020
f66f647
adaptive MvNormal
Dec 10, 2020
2988ff2
merge with master
Dec 11, 2020
e5ad041
AdaptiveMvNormal with tests
Dec 11, 2020
7aa8631
Update src/adaptivemvnormal.jl
arzwa Dec 11, 2020
4999349
Update src/adaptivemvnormal.jl
arzwa Dec 11, 2020
d4b3f6b
Update src/adaptivemvnormal.jl
arzwa Dec 11, 2020
cb52c7f
Update test/Project.toml
arzwa Dec 11, 2020
5a2a175
Update src/mh-core.jl
arzwa Dec 11, 2020
b2be967
Update src/adaptivemvnormal.jl
arzwa Dec 11, 2020
518aab1
removed test Project.toml
Dec 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/AdvancedMH.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Random

# Exports
export MetropolisHastings, DensityModel, RWMH, StaticMH, StaticProposal,
RandomWalkProposal, Ensemble, StretchProposal
RandomWalkProposal, Ensemble, StretchProposal, AdaptiveProposal

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

end # module AdvancedMH
107 changes: 107 additions & 0 deletions src/adaptive.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
Adaptor(; tune=25, target=0.44, bound=10., δmax=0.2)

A helper struct for univariate adaptive proposal kernels. This tracks the
number of accepted proposals and the total number of attempted proposals. The
proposal kernel is tuned every `tune` proposals, such that the scale (log(σ) in
the case of a Normal kernel, log(b) for a Uniform kernel) of the proposal is
increased (decreased) by `δ(n) = min(δmax, 1/√n)` at tuning step `n` if the
estimated acceptance probability is higher (lower) than `target`. The target
acceptance probability defaults to 0.44 which is supposedly optimal for 1D
proposals. To ensure ergodicity, the scale of the proposal has to be bounded
(by `bound`), although this is often not required in practice.
"""
mutable struct Adaptor
accepted::Int
total::Int
tune::Int # tuning interval
target::Float64 # target acceptance rate
bound::Float64 # bound on logσ of Gaussian kernel
δmax::Float64 # maximum adaptation step
end

function Adaptor(; tune=25, target=0.44, bound=10., δmax=0.2)
return Adaptor(0, 0, tune, target, bound, δmax)
end

"""
AdaptiveProposal{P}

An adaptive Metropolis-Hastings proposal. In order for this to work, the
proposal kernel should implement the `adapted(proposal, δ)` method, where `δ`
is the increment/decrement applied to the scale of the proposal distribution
during adaptation (e.g. for a Normal distribution the scale is `log(σ)`, so
that after adaptation the proposal is `Normal(0, exp(log(σ) + δ))`).

# Example
```julia
julia> p = AdaptiveProposal(Uniform(-0.2, 0.2));

julia> rand(p)
0.07975590594518434
```

# References

Roberts, Gareth O., and Jeffrey S. Rosenthal. "Examples of adaptive MCMC."
Journal of Computational and Graphical Statistics 18.2 (2009): 349-367.
"""
mutable struct AdaptiveProposal{P} <: Proposal{P}
proposal::P
adaptor::Adaptor
end

function AdaptiveProposal(p; kwargs...)
return AdaptiveProposal(p, Adaptor(; kwargs...))
end

accepted!(p::AdaptiveProposal) = p.adaptor.accepted += 1
accepted!(p::Vector{<:AdaptiveProposal}) = map(accepted!, p)
accepted!(p::NamedTuple{names}) where names = map(x->accepted!(getfield(p, x)), names)

# this is defined because the first draw has no transition yet (I think)
function propose(rng::Random.AbstractRNG, p::AdaptiveProposal, m::DensityModel)
return rand(rng, p.proposal)
end

# the actual proposal happens here
function propose(
rng::Random.AbstractRNG,
proposal::AdaptiveProposal{<:Union{Distribution,Proposal}},
model::DensityModel,
t
)
consider_adaptation!(proposal)
return t + rand(rng, proposal.proposal)
end

function q(proposal::AdaptiveProposal, t, t_cond)
return logpdf(proposal, t - t_cond)
end

Comment on lines +81 to +84
Copy link
Member

Choose a reason for hiding this comment

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

Is this actually needed? It seems like the default for Proposal should cover this.

Copy link
Author

Choose a reason for hiding this comment

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

Currently there seems to be no default q for Proposal. However it's also unnecessary because these adaptive proposals are always symmetric.

function consider_adaptation!(p)
(p.adaptor.total % p.adaptor.tune == 0) && adapt!(p)
p.adaptor.total += 1
end

function adapt!(p::AdaptiveProposal)
a = p.adaptor
a.total == 0 && return
δ = min(a.δmax, sqrt(a.tune / a.total)) # diminishing adaptation
α = a.accepted / a.tune # acceptance ratio
p_ = adapted(p.proposal, α > a.target ? δ : -δ, a.bound)
a.accepted = 0
p.proposal = p_
end

function adapted(d::Normal, δ, bound=Inf)
_lσ = log(d.σ) + δ
lσ = sign(_lσ) * min(bound, abs(_lσ))
return Normal(d.μ, exp(lσ))
end

function adapted(d::Uniform, δ, bound=Inf)
lσ = log(d.b) + δ
σ = exp(sign(lσ) * min(bound, abs(lσ)))
return Uniform(-σ, σ)
end
4 changes: 4 additions & 0 deletions src/mh-core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,12 @@ function AbstractMCMC.step(

# Decide whether to return the previous params or the new one.
if -Random.randexp(rng) < logα
accepted!(spl.proposal)
return params, params
else
return params_prev, params_prev
end
end

function accepted!(p::P) where P<:Proposal end

arzwa marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion src/proposal.jl
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,4 @@ function q(
t_cond
)
return q(proposal(t_cond), t, t_cond)
end
end
28 changes: 27 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ using Test
Random.seed!(1234)

# Generate a set of data from the posterior we want to estimate.
data = rand(Normal(0, 1), 300)
data = rand(Normal(0., 1), 300)
arzwa marked this conversation as resolved.
Show resolved Hide resolved

# Define the components of a basic model.
insupport(θ) = θ[2] >= 0
Expand Down Expand Up @@ -52,6 +52,32 @@ using Test
@test mean(chain2.μ) ≈ 0.0 atol=0.1
@test mean(chain2.σ) ≈ 1.0 atol=0.1
end

@testset "Adaptive random walk" begin
# Set up our sampler with initial parameters.
spl1 = MetropolisHastings([AdaptiveProposal(Normal(0,.4)), AdaptiveProposal(Normal(0,1.2))])
spl2 = MetropolisHastings((μ=AdaptiveProposal(Normal(0,1.4)), σ=AdaptiveProposal(Normal(0,0.2))))

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

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

@testset "Compare adaptive to simple random walk" begin
data = rand(Normal(2., 1.), 500)
Copy link
Member

Choose a reason for hiding this comment

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

@arzwa This might be the problem - you redefine data and hence I assume you implicitly change the density model which is defined as a closure over data.

You could just rename the variable here but actually I think the better approach might be to "fix" the data in the model to avoid any such surprises in the future.

I guess this can be achieved by defining

density = let data = data
  θ -> insupport(θ) ? sum(logpdf.(dist(θ), data)) : -Inf
end

Copy link
Member

Choose a reason for hiding this comment

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

But I haven't tested it, so make sure it actually fixes the problem 😄

Copy link
Author

Choose a reason for hiding this comment

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

Yes, I just saw this too, thanks. I'll check and push an updated test suite. (Actually, we could just as well test against the same data defined above in the test suite, but I find testing against a mean different from 0 a bit more reassuring since the sampler actually has to 'move' to somewhere from where it starts).

m1 = DensityModel(x -> loglikelihood(Normal(x,1), data))
p1 = RandomWalkProposal(Normal())
p2 = AdaptiveProposal(Normal())
c1 = sample(m1, MetropolisHastings(p1), 10000; chain_type=Chains)
c2 = sample(m1, MetropolisHastings(p2), 10000; chain_type=Chains)
@test ess(c2).nt.ess > ess(c1).nt.ess
end

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