From faa6139e70a01712c5dc10a035b9733a44ce48b0 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Sun, 24 Nov 2019 13:38:23 +0000 Subject: [PATCH 01/25] concept - not running yet This is the concept for the package. The package does not run yet! --- Tests/TestTuring.jl | 109 ++++ src/AdvancedPS.jl | 49 ++ src/ConditionalParticleFilter.jl | 1020 ++++++++++++++++++++++++++++++ src/resample.jl | 56 ++ src/resample_functions.jl | 288 +++++++++ src/samplers.jl | 27 + src/taskinfo.jl | 10 + src/tasks.jl | 264 ++++++++ 8 files changed, 1823 insertions(+) create mode 100644 Tests/TestTuring.jl create mode 100644 src/AdvancedPS.jl create mode 100644 src/ConditionalParticleFilter.jl create mode 100644 src/resample.jl create mode 100644 src/resample_functions.jl create mode 100644 src/samplers.jl create mode 100644 src/taskinfo.jl create mode 100644 src/tasks.jl diff --git a/Tests/TestTuring.jl b/Tests/TestTuring.jl new file mode 100644 index 00000000..35ec810f --- /dev/null +++ b/Tests/TestTuring.jl @@ -0,0 +1,109 @@ +using("Turing.jl") +using LinearAlgebra +using CUDAdrv +using CuArrays +# We use a very easy markovian model to test... +@model gdemo(y) = begin + # Set priors. + s ~ InverseGamma(2, 3) + m ~ Normal(0,5) + x = Vector{Real}(undef, 10) + z = Vector{Real}(undef, 10) + x[1] ~ Normal(0,1) + for j = 2:10 + x[j] ~ Normal(m+x[j-1], sqrt(s)) + x[j] ~ Normal(x[j-1], sqrt(s)) + z[j] = x[j]^2 + y[j-1] ~ Normal(x[j],0.1) + end + return z +end + +y = [ 1 2 3 4 5 6 7 8 9 10] +c = sample(gdemo(y), Gibbs(HMC(0.2,10,:m,:s),MH(:x)), 100) +sample(gdemo(y), SMC(:x,:y), 100) + + +hi = (4,"a") + +arr = Vector{Symbol}(undef,1) +typeof((Array{Symbol,1},"a")) +isempty(arr) + +Float64[] + + + + +v = rand(Normal()) +pdf = logpdf(Normal(),v) + + +vectorize(Normal(),v) + +dist =MvNormal([ 2. 1 1; 1 2 1; 1 1 2]) + +dist2 = Normal(2,3) + +std(dist2) + +mean(dist2) + + + +s = [] +for i = 1:10 + push!(s,i) +end + +function pr(x) + println(x) +end + + +M = rand(InverseWishart(100,Matrix{Float64}(I, 100, 100))) + + + + + + +@model gdemo() = begin + # Set priors. + pr("start") + s ~ InverseGamma(2, 3) + pr("in the first block") + cholesky(M) + m ~ Normal(0,5) + pr("hi n the block") + x = Normal(0, 10) + pr("end") + return x +end + +sample(gdemo(), Gibbs(HMC(0.2,1,:m,:s),MH(:x)), 2) + + + +push!(LOAD_PATH, string(pwd(),"/Turing/src/")) +push!(LOAD_PATH, string(pwd(),"/AdvancedPS/src/")) +using Pkg +include(string(pwd(),"/Turing/src/Turing.jl")) +include(string(pwd(),"/AdvancedPS/src/AdvancedPS.jl")) + +using AdvancedPS +using Turing + +@model mld = begin + x ~Normal() +end + + +F = (copy = copy, pr = println) +F[:pr](4) +typeof(F) <: NamedTuple{} + +NamedTuple{}() + +names(F) +@assert (:copy,:pr) in keys(F) diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl new file mode 100644 index 00000000..db1d9bc7 --- /dev/null +++ b/src/AdvancedPS.jl @@ -0,0 +1,49 @@ +### Attention this is a development package! It wount run. + + +module AdvancedPS + + + + const DEBUG = Bool(parse(Int, get(ENV, "DEBUG_APS", "0"))) + + + + + + import StatsBase: sample + + + abstract type AbstractTaskInfo end + + include("resample.jl") + + export resample! + + + include("tasks.jl") + + export ParticleContainer, Trace + + + include("taskinfo.jl") + + export AbstractTaskInfo, TaskInfo + + include("samplers.jl") + + export sampleSMC!, samplePG! + + + include("resample_functions.jl") + + export resample, + randcat, + resample_multinomial, + resample_residual, + resample_stratified, + resample_systematic + + + +end # module diff --git a/src/ConditionalParticleFilter.jl b/src/ConditionalParticleFilter.jl new file mode 100644 index 00000000..9893d224 --- /dev/null +++ b/src/ConditionalParticleFilter.jl @@ -0,0 +1,1020 @@ +using Turing +using Turing. + + + +### + +### General SMC sampler with proposal distributions + +### Conditional Particle Filter (Andireu 2010 https://www.stats.ox.ac.uk/~doucet/andrieu_doucet_holenstein_PMCMC.pdf) + +### CPF-Ancestor Sampling (Lindsten 2014 http://jmlr.org/papers/volume15/lindsten14a/lindsten14a.pdf) + +### + + + + +####################### + +# Particle Transition # + +####################### + + + + +""" + + ParticleTransition{T, F<:AbstractFloat} <: AbstractTransition + + + +Fields: + +- `θ`: The parameters for any given sample. + +- `lp`: The log pdf for the sample's parameters. + +- `le`: The log evidence retrieved from the particle. + +- `weight`: The weight of the particle the sample was retrieved from. + +""" + + + + +#### + +#### Generic Sequential Monte Carlo sampler. + +#### + + + +""" + + GSMC() + + + +General Proposal Sequential Monte Carlo sampler. + +This is an extension to the SMC sampler form the Turing repository. + +It allows to set the proposal distributions. + + + +Note that this method is particle-based, and arrays of variables + +must be stored in a [`TArray`](@ref) object. + + + +Fields: + + +- `resampler`: A function used to sample particles from the particle container. + + Defaults to `resample_systematic`. + +- `resampler_threshold`: The threshold at which resampling terminates -- defaults to 0.5. If + + the `ess` <= `resampler_threshold` * `n_particles`, the resampling step is completed. + + + + Usage: + + + +```julia + +GSMC() + + +# Only specify for z a proposal distributon independent +# from the other parameters + +GSMC(:x,:y,(:z , () -> f())) + + +# Only specify for z a proposal distributon +# which is dependent on x and y + +GSMC(:x,:y,(:z,[:x,:y], (args) -> f(args))) + +``` + +""" + + + + +struct GSMC{space, RT<:AbstractFloat} <: ParticleInference + + # Any contains a proposal distribution and a list of additional input variables + + proposals :: Dict{Symbol, Any} # Proposals for paramters + + resampler :: Function + + resampler_threshold :: RT + +end + + + +alg_str(spl::Sampler{GSMC}) = "GSMC" + + +## Inspired from MH algorithm + +function GSMC( + resampler::Function, + + resampler_threshold::RT, + + space::Tuple + +) where {RT<:AbstractFloat} + + new_space = () + + proposals = Dict{Symbol,Any}() + + + + # parse random variables with their hypothetical proposal + + for element in space + + if isa(element, Symbol) + + new_space = (new_space..., element) + + else + + @assert isa(element[1], Symbol) "[GSMC] ($element[1]) should be a Symbol. For proposal, use the syntax MH(N, (:m, () -> Normal(0, 0.1)))" + @assert length(element) == 2 || length(element) == 3 "[GSMC] ($element[1]) has wrong shape. Use syntax (:m, () -> Normal(0,0.1)) or (:z,[:x,:y], (args) -> f(args))" + new_space = (new_space..., element[1]) + if length(element)== 2 + proposals[element[1]] = (Vector{Symbol}(undef,0), element[2]) #No input arguments + elseif length(element) == 3 + @assert isa(element[2],Vector{Symbol}) "[GSMC] For length three elements, ($element[2]) should be a Vecotr{Symbol}" + proposal[element[1]] = (element[2], element[3]) + end + end + + end + + return GSMC{new_space, RT}(proposals ,resampler, resampler_threshold) + +end + +GSMC() = GSMC(resample_systematic, 0.5, ()) + +GSMC(::Tuple{}) = GSMC() + +function SMC(space::Symbol...) + + GSMC(resample_systematic, 0.5, space) + +end + + +## We can use the SMCState from the Turing repo + +# mutable struct SMCState{V<:VarInfo, F<:AbstractFloat} <: AbstractSamplerState +# +# vi :: V +# +# # The logevidence after aggregating all samples together. +# +# average_logevidence :: F +# +# particles :: ParticleContainer +# +# end +# +# +# +# function SMCState( +# +# model::M, +# +# ) where { +# +# M<:Model +# +# } +# +# vi = VarInfo(model) +# +# particles = ParticleContainer{Trace}(model) +# +# +# +# return SMCState(vi, 0.0, particles) +# +# end + +## We leave this function unchanged + +function Sampler(alg::T, model::Model, s::Selector) where T<:GSMC + + dict = Dict{Symbol, Any}() + + state = SMCState(model) + + return Sampler(alg, dict, s, state) + +end + +## We leave this function unchanged + +function sample_init!( + + ::AbstractRNG, + + model::Turing.Model, + + spl::Sampler{<:SMC}, + + N::Integer; + + kwargs... + +) + + # Set the parameters to a starting value. + + initialize_parameters!(spl; kwargs...) + + + + # Update the particle container now that the sampler type + + # is defined. + + spl.state.particles = ParticleContainer{Trace{typeof(spl), + + typeof(spl.state.vi), typeof(model)}}(model) + + + + spl.state.vi.num_produce = 0; # Reset num_produce before new sweep\. + + set_retained_vns_del_by_spl!(spl.state.vi, spl) + + resetlogp!(spl.state.vi) + + + + push!(spl.state.particles, N, spl, empty!(spl.state.vi)) + + + + while consume(spl.state.particles) != Val{:done} + + ess = effectiveSampleSize(spl.state.particles) + + if ess <= spl.alg.resampler_threshold * length(spl.state.particles) + + resample!(spl.state.particles, spl.alg.resampler) + + end + + end + +end + +## We leave this function unchanged + +function step!( + + ::AbstractRNG, + + model::Turing.Model, + + spl::Sampler{<:SMC}, + + ::Integer; + + iteration=-1, + + kwargs... + +) + + # Check that we received a real iteration number. + + @assert iteration >= 1 "step! needs to be called with an 'iteration' keyword argument." + + + + ## Grab the weights. + + Ws = weights(spl.state.particles) + + + + # update the master vi. + + particle = spl.state.particles.vals[iteration] + + params = tonamedtuple(particle.vi) + + lp = getlogp(particle.vi) + + + + return ParticleTransition(params, lp, spl.state.particles.logE, Ws[iteration]) + +end + + + +#### + +#### Particle Gibbs sampler. + +#### + + + +""" + + CPF(n_particles::Int,space) + + + +Conditional Particle Filter sampler. + + + +Note that this method is particle-based, and arrays of variables + +must be stored in a [`TArray`](@ref) object. + + + +Usage: + + + +```julia + +CPF(100) + +``` + +""" + +struct CPF{space} <: ParticleInference + + + proposals :: Dict{Symbol,Any}() # Proposal distributions + + n_particles :: Int # number of particles used + + resampler :: Function # function to resample + +end + +function CPF(n_particles::Int, resampler::Function, space::Tuple) + + new_space = () + + proposals = Dict{Symbol,Any}() + + + + # parse random variables with their hypothetical proposal + + for element in space + + if isa(element, Symbol) + + new_space = (new_space..., element) + + else + + @assert isa(element[1], Symbol) "[GSMC] ($element[1]) should be a Symbol. For proposal, use the syntax MH(N, (:m, () -> Normal(0, 0.1)))" + @assert length(element) == 2 || length(element) == 3 "[GSMC] ($element[1]) has wrong shape. Use syntax (:m, () -> Normal(0,0.1)) or (:z,[:x,:y], (args) -> f(args))" + new_space = (new_space..., element[1]) + if length(element)== 2 + proposals[element[1]] = (Vector{Symbol}(undef,0), element[2]) #No input arguments + elseif length(element) == 3 + @assert isa(element[2],Vector{Symbol}) "[GSMC] For length three elements, ($element[2]) should be a Vecotr{Symbol}" + proposal[element[1]] = (element[2], element[3]) + end + end + + end + + return GSMC{new_space, RT}(proposals ,n_particles,resampler) +end + +CPF(n1::Int, ::Tuple{}) = CPF(n1) + +function CPF(n1::Int, space::Symbol...) + + CPF(n1, resample_systematic, space) + +end + + + +alg_str(spl::Sampler{CPF}) = "CPF" + +## We resue this... +# +# mutable struct PGState{V<:VarInfo, F<:AbstractFloat} <: AbstractSamplerState +# +# vi :: V +# +# # The logevidence after aggregating all samples together. +# +# average_logevidence :: F +# +# end +# +# +# +# function PGState(model::M) where {M<:Model} +# +# vi = VarInfo(model) +# +# return PGState(vi, 0.0) +# +# end + + + + + + +""" + + Sampler(alg::PG, model::Model, s::Selector) + + + +Return a `Sampler` object for the PG algorithm. + +""" + +function Sampler(alg::T, model::Model, s::Selector) where T<:CPF + + info = Dict{Symbol, Any}() + + state = PGState(model) + + return Sampler(alg, info, s, state) + +end + + + +function step!( + + ::AbstractRNG, + + model::Turing.Model, + + spl::Sampler{<:PG}, + + ::Integer; + + kwargs... + +) + + particles = ParticleContainer{Trace{typeof(spl), typeof(spl.state.vi), typeof(model)}}(model) + + + + spl.state.vi.num_produce = 0; # Reset num_produce before new sweep. + + ref_particle = isempty(spl.state.vi) ? + + nothing : + + forkr(Trace(model, spl, spl.state.vi)) + + + + set_retained_vns_del_by_spl!(spl.state.vi, spl) + + resetlogp!(spl.state.vi) + + + + if ref_particle === nothing + + push!(particles, spl.alg.n_particles, spl, spl.state.vi) + + else + + push!(particles, spl.alg.n_particles-1, spl, spl.state.vi) + + push!(particles, ref_particle) + + end + + + + while consume(particles) != Val{:done} + + resample!(particles, spl.alg.resampler, ref_particle) + + end + + + + ## pick a particle to be retained. + + Ws = weights(particles) + + indx = randcat(Ws) + + + + # Extract the VarInfo from the retained particle. + + params = tonamedtuple(spl.state.vi) + + spl.state.vi = particles[indx].vi + + lp = getlogp(spl.state.vi) + + + + # update the master vi. + + return ParticleTransition(params, lp, particles.logE, 1.0) + +end + + + +function sample_end!( + + ::AbstractRNG, + + ::Model, + + spl::Sampler{<:ParticleInference}, + + N::Integer, + + ts::Vector{ParticleTransition}; + + kwargs... + +) + + # Set the default for resuming the sampler. + + resume_from = get(kwargs, :resume_from, nothing) + + + + # Exponentiate the average log evidence. + + # loge = exp(mean([t.le for t in ts])) + + loge = mean(t.le for t in ts) + + + + # If we already had a chain, grab the logevidence. + + if resume_from !== nothing # concat samples + + @assert resume_from isa Chains "resume_from needs to be a Chains object." + + # pushfirst!(samples, resume_from.info[:samples]...) + + pre_loge = resume_from.logevidence + + # Calculate new log-evidence + + pre_n = length(resume_from) + + loge = (pre_loge * pre_n + loge * N) / (pre_n + N) + + end + + + + # Store the logevidence. + + spl.state.average_logevidence = loge + +end + +## We need to sample form the proposal instead of the transition + +function assume( spl::Sampler{T}, + + dist::Distribution, + + vn::VarName, + + _::VarInfo + + ) where T<:Union{CPF,GSMC} + + + ## The part concerning the tasks is left unchanged ! + + vi = current_trace().vi + + if isempty(getspace(spl.alg)) || vn.sym in getspace(spl.alg) + + if ~haskey(vi, vn) + + ## Here, our changings start... + ## Attention, we do not check for the support of the proposal distribution!! + ## We implicitely assume so far that the support is not violated!! + ## We changed r = rand(dist) with the following code in order to include proposal distributions. + + if vn.sym in keys(spl.alg.proposals) # Custom proposal for this parameter + + + tuple = spl.alg.proposals[vn.sym]() + + #First extract the argument variables + extracted_symbosl = tuple[1] + if isempty(extracted_symbosl) + proposal = tuple[2]() + else + + args = [] + for sym in extracted_symbosl + if isempty(getspace(spl.alg)) || sym in getspace(spl.alg) + error("[CPF] Symbol ($sym) is not defined yet. The arguments for the propsal must occur before!") + end + push!(args,vi[sym]) + end + proposal = tuble[2](args) + end + r = rand(proposal) + ## In this case, we need to set the logp because we do not sample directly from the prior! + acclogp!(vi, logpdf(dist, r) -logpdf(proposal, r)) + + end + else # Prior as proposal + + r = rand(dist) + + spl.state.prior_prob += logpdf(dist, r) # accumulate prior for PMMH + + end + + + push!(vi, vn, r, dist, spl) + + elseif is_flagged(vi, vn, "del") + + + + unset_flag!(vi, vn, "del") + + + ## Here, our changings start... + ## Attention, we do not check for the support of the proposal distribution!! + ## We implicitely assume so far that the support is not! violated + + if vn.sym in keys(spl.alg.proposals) # Custom proposal for this parameter + + + tuple = spl.alg.proposals[vn.sym]() + + #First extract the argument variables + extracted_symbosl = tuple[1] + if isempty(extracted_symbosl) + proposal = tuple[2]() + else + + args = [] + for sym in extracted_symbosl + if isempty(getspace(spl.alg)) || sym in getspace(spl.alg) + error("[CPF] Symbol ($sym) is not defined yet. The arguments for the propsal must occur before!") + end + push!(args,vi[sym]) + end + proposal = tuble[2](args) + end + r = rand(proposal) + + ## In this case, we need to set the logp because we do not sample directly from the prior! + acclogp!(vi, logpdf(dist, r) -logpdf(proposal, r)) + + end + else # Prior as proposal + + r = rand(dist) + + spl.state.prior_prob += logpdf(dist, r) # accumulate prior for PMMH + + end + + vi[vn] = vectorize(dist, r) + + setgid!(vi, spl.selector, vn) + + setorder!(vi, vn, vi.num_produce) + + else + + updategid!(vi, vn, spl) + + r = vi[vn] + + end + + else # vn belongs to other sampler <=> conditionning on vn + + if haskey(vi, vn) + + r = vi[vn] + + else # What happens here? I assume that vn does not belong to any sampler - how is this possible + # Should the sanity check not prevent his + r = rand(dist) + + push!(vi, vn, r, dist, Selector(:invalid)) + + end + + acclogp!(vi, logpdf_with_trans(dist, r, istrans(vi, vn))) + + end + + return r, zero(Real) + +end + +## Only need to change A<:Union{CPF,GSMC} + +function assume( spl::Sampler{A}, + + dists::Vector{D}, + + vn::VarName, + + var::Any, + + vi::VarInfo + + ) where {A<:Union{PG,SMC},D<:Distribution} + + error("[Turing] CPF and GSMC doesn't support vectorizing assume statement") + +end + +## Only need to change A<:Union{CPF,GSMC} + + +function observe(spl::Sampler{T}, dist::Distribution, value, vi) where T<:Union{CPF,GSMC} + + produce(logpdf(dist, value)) + + return zero(Real) + +end + + +## Only need to change A<:Union{CPF,GSMC} + +function observe( spl::Sampler{A}, + + ds::Vector{D}, + + value::Any, + + vi::VarInfo + + ) where {A<:Union{CPF,GSMC},D<:Distribution} + + error("[Turing] CPF and GSMC doesn't support vectorizing observe statement") + +end + + + +for alg in (:GSMC,:CPF) + + @eval getspace(::$alg{space}) where {space} = space + + @eval getspace(::Type{<:$alg{space}}) where {space} = space + +########################################################################################### +## We do not change these - therefore, we use the code directly from the turing repo +# +# +# #### +# +# #### Resampling schemes for particle filters +# +# #### +# +# +# +# # Some references +# +# # - http://arxiv.org/pdf/1301.4019.pdf +# +# # - http://people.isy.liu.se/rt/schon/Publications/HolSG2006.pdf +# +# # Code adapted from: http://uk.mathworks.com/matlabcentral/fileexchange/24968-resampling-methods-for-particle-filtering +# +# +# +# # Default resampling scheme +# +# function resample(w::AbstractVector{<:Real}, num_particles::Integer=length(w)) +# +# return resample_systematic(w, num_particles) +# +# end +# +# +# +# # More stable, faster version of rand(Categorical) +# +# function randcat(p::AbstractVector{T}) where T<:Real +# +# r, s = rand(T), 1 +# +# for j in eachindex(p) +# +# r -= p[j] +# +# if r <= zero(T) +# +# s = j +# +# break +# +# end +# +# end +# +# return s +# +# end +# +# +# +# function resample_multinomial(w::AbstractVector{<:Real}, num_particles::Integer) +# +# return rand(Distributions.sampler(Categorical(w)), num_particles) +# +# end +# +# +# +# function resample_residual(w::AbstractVector{<:Real}, num_particles::Integer) +# +# +# +# M = length(w) +# +# +# +# # "Repetition counts" (plus the random part, later on): +# +# Ns = floor.(length(w) .* w) +# +# +# +# # The "remainder" or "residual" count: +# +# R = Int(sum(Ns)) +# +# +# +# # The number of particles which will be drawn stocastically: +# +# M_rdn = num_particles - R +# +# +# +# # The modified weights: +# +# Ws = (M .* w - floor.(M .* w)) / M_rdn +# +# +# +# # Draw the deterministic part: +# +# indx1, i = Array{Int}(undef, R), 1 +# +# for j in 1:M +# +# for k in 1:Ns[j] +# +# indx1[i] = j +# +# i += 1 +# +# end +# +# end +# +# +# +# # And now draw the stocastic (Multinomial) part: +# +# return append!(indx1, rand(Distributions.sampler(Categorical(w)), M_rdn)) +# +# end +# +# +# +# function resample_stratified(w::AbstractVector{<:Real}, num_particles::Integer) +# +# +# +# Q, N = cumsum(w), num_particles +# +# +# +# T = Array{Float64}(undef, N + 1) +# +# for i=1:N, +# +# T[i] = rand() / N + (i - 1) / N +# +# end +# +# T[N+1] = 1 +# +# +# +# indx, i, j = Array{Int}(undef, N), 1, 1 +# +# while i <= N +# +# if T[i] < Q[j] +# +# indx[i] = j +# +# i += 1 +# +# else +# +# j += 1 +# +# end +# +# end +# +# return indx +# +# end +# +# +# +# function resample_systematic(w::AbstractVector{<:Real}, num_particles::Integer) +# +# +# +# Q, N = cumsum(w), num_particles +# +# +# +# T = collect(range(0, stop = maximum(Q)-1/N, length = N)) .+ rand()/N +# +# push!(T, 1) +# +# +# +# indx, i, j = Array{Int}(undef, N), 1, 1 +# +# while i <= N +# +# if T[i] < Q[j] +# +# indx[i] = j +# +# i += 1 +# +# else +# +# j += 1 +# +# end +# +# end +# +# return indx +# +# end diff --git a/src/resample.jl b/src/resample.jl new file mode 100644 index 00000000..2e2094ec --- /dev/null +++ b/src/resample.jl @@ -0,0 +1,56 @@ + +function resample!( + pc :: ParticleContainer, + indx :: Vecotr{Int64} = resample_systematic, + ref :: Union{Particle, Nothing} = nothing + ancestor_idx :: int = -1 + new_ancestor_traj :: Union{Particle,Nothing} = nothing +) + + + # count number of children for each particle + num_children = zeros(Int, n) + @inbounds for i in indx + num_children[i] += 1 + end + + # fork particles + particles = collect(pc) + children = similar(particles) + j = 0 + @inbounds for i in 1:n + ni = num_children[i] + + if ni > 0 + # fork first child + pi = particles[i] + isref = pi === ref + p = isref ? fork(pi, pc.mainpulators["copy"], isref, pc.mainpulators["set_retained_vns_del_by_spl!"]) : pi + children[j += 1] = p + + # fork additional children + for _ in 2:ni + children[j += 1] = fork(p, pc.mainpulators["copy"], isref, pc.mainpulators["set_retained_vns_del_by_spl!"]) + end + end + end + + if ref !== nothing + # Insert the retained particle. This is based on the replaying trick for efficiency + # reasons. If we implement PG using task copying, we need to store Nx * T particles! + # This is a rather effcient way of how to solve the ancestor problem. + if ancestor_idx == n || ancestor_idx == -1 + @inbounds children[n] = ref + else + @assert isa(Particle,new_ancestor_traj) "[AdvancedPS] ($new_ancestor_traj) must be of type particle" + @inbounds children[n] = new_ancestor_traj + else + + end + + # replace particles and log weights in the container with new particles and weights + pc.vals = children + pc.logWs = zeros(n) + + pc +end diff --git a/src/resample_functions.jl b/src/resample_functions.jl new file mode 100644 index 00000000..ece99815 --- /dev/null +++ b/src/resample_functions.jl @@ -0,0 +1,288 @@ +### + +### Resampler Functions + +### + + +# Default resampling scheme + +function resample(w::AbstractVector{<:Real}, num_particles::Integer=length(w)) + + return resample_systematic(w, num_particles) + +end + + + +# More stable, faster version of rand(Categorical) + +function randcat(p::AbstractVector{T}) where T<:Real + + r, s = rand(T), 1 + + for j in eachindex(p) + + r -= p[j] + + if r <= zero(T) + + s = j + + break + + end + + end + + return s + +end + + + +function resample_multinomial(w::AbstractVector{<:Real}, num_particles::Integer) + + return rand(Distributions.sampler(Categorical(w)), num_particles) + +end + + + +function resample_residual(w::AbstractVector{<:Real}, num_particles::Integer) + + + + M = length(w) + + + + # "Repetition counts" (plus the random part, later on): + + Ns = floor.(length(w) .* w) + + + + # The "remainder" or "residual" count: + + R = Int(sum(Ns)) + + + + # The number of particles which will be drawn stocastically: + + M_rdn = num_particles - R + + + + # The modified weights: + + Ws = (M .* w - floor.(M .* w)) / M_rdn + + + + # Draw the deterministic part: + + indx1, i = Array{Int}(undef, R), 1 + + for j in 1:M + + for k in 1:Ns[j] + + indx1[i] = j + + i += 1 + + end + + end + + + + # And now draw the stocastic (Multinomial) part: + + return append!(indx1, rand(Distributions.sampler(Categorical(w)), M_rdn)) + +end + + + +""" + + resample_stratified(weights, n) + + + +Return a vector of `n` samples `x₁`, ..., `xₙ` from the numbers 1, ..., `length(weights)`, + +generated by stratified resampling. + + + +In stratified resampling `n` ordered random numbers `u₁`, ..., `uₙ` are generated, where + +``uₖ \\sim U[(k - 1) / n, k / n)``. Based on these numbers the samples `x₁`, ..., `xₙ` + +are selected according to the multinomial distribution defined by the normalized `weights`, + +i.e., `xᵢ = j` if and only if + +``uᵢ \\in [\\sum_{s=1}^{j-1} weights_{s}, \\sum_{s=1}^{j} weights_{s})``. + +""" + +function resample_stratified(weights::AbstractVector{<:Real}, n::Integer) + + # check input + + m = length(weights) + + m > 0 || error("weight vector is empty") + + + + # pre-calculations + + @inbounds v = n * weights[1] + + + + # generate all samples + + samples = Array{Int}(undef, n) + + sample = 1 + + @inbounds for i in 1:n + + # sample next `u` (scaled by `n`) + + u = oftype(v, i - 1 + rand()) + + + + # as long as we have not found the next sample + + while v < u + + # increase and check the sample + + sample += 1 + + sample > m && + + error("sample could not be selected (are the weights normalized?)") + + + + # update the cumulative sum of weights (scaled by `n`) + + v += n * weights[sample] + + end + + + + # save the next sample + + samples[i] = sample + + end + + + + return samples + +end + + + +""" + + resample_systematic(weights, n) + + + +Return a vector of `n` samples `x₁`, ..., `xₙ` from the numbers 1, ..., `length(weights)`, + +generated by systematic resampling. + + + +In systematic resampling a random number ``u \\sim U[0, 1)`` is used to generate `n` ordered + +numbers `u₁`, ..., `uₙ` where ``uₖ = (u + k − 1) / n``. Based on these numbers the samples + +`x₁`, ..., `xₙ` are selected according to the multinomial distribution defined by the + +normalized `weights`, i.e., `xᵢ = j` if and only if + +``uᵢ \\in [\\sum_{s=1}^{j-1} weights_{s}, \\sum_{s=1}^{j} weights_{s})``. + +""" + +function resample_systematic(weights::AbstractVector{<:Real}, n::Integer) + + # check input + + m = length(weights) + + m > 0 || error("weight vector is empty") + + + + # pre-calculations + + @inbounds v = n * weights[1] + + u = oftype(v, rand()) + + + + # find all samples + + samples = Array{Int}(undef, n) + + sample = 1 + + @inbounds for i in 1:n + + # as long as we have not found the next sample + + while v < u + + # increase and check the sample + + sample += 1 + + sample > m && + + error("sample could not be selected (are the weights normalized?)") + + + + # update the cumulative sum of weights (scaled by `n`) + + v += n * weights[sample] + + end + + + + # save the next sample + + samples[i] = sample + + + + # update `u` + + u += one(u) + + end + + + + return samples + +end diff --git a/src/samplers.jl b/src/samplers.jl new file mode 100644 index 00000000..a5efc29b --- /dev/null +++ b/src/samplers.jl @@ -0,0 +1,27 @@ + + +function sampleSMC!(pc::ParticleContainer,resampler_threshold::AbstractFloat) + while consume(pc) != Val{:done} + ess = effectiveSampleSize(pc) + if ess <= spl.alg.resampler_threshold * length(pc) + # compute weights + Ws = weights(pc) + # check that weights are not NaN + @assert !any(isnan, Ws) + # sample ancestor indices + n = length(pc) + nresamples = n + indx = randcat(Ws, nresamples) + resample!(particles, indx) + end + end +end + + +function samplePG!(pc::ParticleContainer,ref_particle::Union{Particle,},resampler::Function ) + + while consume(pc) != Val{:done} + resample!(pc, resampler, ref_particle) + end + +end diff --git a/src/taskinfo.jl b/src/taskinfo.jl new file mode 100644 index 00000000..9a2a4095 --- /dev/null +++ b/src/taskinfo.jl @@ -0,0 +1,10 @@ + + +# The idea behind the TaskInfo struct is that it +# allows to propagate information trough the computation. +# This is important because we do not want to + +struct TaskInfo <:AbstractTaskInfo + getidcs::Function # Function to get gicds from sampler + logpseq::Float64 +end diff --git a/src/tasks.jl b/src/tasks.jl new file mode 100644 index 00000000..9f94b97c --- /dev/null +++ b/src/tasks.jl @@ -0,0 +1,264 @@ + + +using Turing.Core.RandomVariables +using Turing + + + +# Idea: We decouple tasks form the model by only allowing to pass a function. +# This way, we do no longer need to have the model and the sampler saved in the trace struct +# However, we still need to insantiate the VarInfo + +# TaskInfo stores additional informatino about the task +mutable struct Trace{Tvi, Tinfo <:AbstractTaskInfo} + vi::Tvi # Unfortunatley, we can not set force this to be a subtype of VarInfo... + task::Task + taksinfo::Tinfo + function Trace{AbstractTaskInfo}(vi, taskinfo<:AbstractTaskInfo, task::Task) + return new{typeof(vi),typeof(taskinfo)}(vi, task, taskinfo) + end +end + +function Base.copy(trace::Trace, copy_vi::Function) + try + vi = copy_vi(trace.vi) + catch e + error("[AdvancedPS] The copy function given by the manipulators must only accept vi as argument") + end + taskinfo = copy(trace.taskinfo) + res = Trace{AbstractTaskInfo}(vi, copy(trace.task), taskinfo) + return res +end + +# The procedure passes a function which is specified by the model. + +function Trace(f::Function, vi<:RandomVariables.AbstractVarInfo, taskinfo<:AbstractTaskInfo, copy_vi::Function) where {T <: AbstractSampler} + task = CTask( () -> begin res=f(); produce(Val{:done}); res; end ) + + res = Trace{AbstractTaskInfo}(copy_vi(vi), task, copy(taskinfo)); + # CTask(()->f()); + if res.task.storage === nothing + res.task.storage = IdDict() + end + res.task.storage[:turing_trace] = res # create a backward reference in task_local_storage + return res +end + +## We need to design the task in the Turing wrapper. +function Trace(task::Task, vi<:RandomVariables.AbstractVarInfo, taskinfo<:AbstractTaskInfo, copy_vi::Function) + try + new_vi = copy_vi(vi) + catch e + error("[AdvancedPS] The copy function given by the manipulators must only accept vi as argument") + end + res = Trace{AbstractTaskInfo}(new_vi, task, copy(taskinfo)); + # CTask(()->f()); + res.task = task + if res.task.storage === nothing + res.task.storage = IdDict() + end + res.task.storage[:turing_trace] = res # create a backward reference in task_local_storage + return res +end + + + +# step to the next observe statement, return log likelihood +Libtask.consume(t::Trace) = (t.vi.num_produce += 1; consume(t.task)) + +# Task copying version of fork for Trace. +function fork(trace :: Trace, copy_vi::Function, is_ref :: Bool = false, set_retained_vns_del_by_spl!::Function = nothing) + newtrace = copy(trace) + if is_ref + @assert set_retained_vns_del_by_spl! != nothing "[AdvancedPF] set_retained_vns_del_by_spl! is not set." + try + set_retained_vns_del_by_spl!(newtrace.vi, taskinfo.idcs) + catch e + error("[AdvancedPS] set_retained_vns_del_by_spl! must have the form set_retained_vns_del_by_spl!(newtrace.vi, taskinfo.idcs)") + end + newtrace.task.storage[:turing_trace] = newtrace + return newtrace +end + +# PG requires keeping all randomness for the reference particle +# Create new task and copy randomness +function forkr(trace :: Trace, copy_vi::Function) + try + new_vi = copy_vi(trace.vi) + catch e + error("[AdvancedPS] The copy function given by the manipulators must only accept vi as argument") + end + newtrace = Trace(trace.task.code,new_vi ,copy(trace.taskinfo)) + newtrace.vi.num_produce = 0 + return newtrace +end + +current_trace() = current_task().storage[:turing_trace] + +const Particle = Trace + +""" +Data structure for particle filters +- effectiveSampleSize(pc :: ParticleContainer) +- normalise!(pc::ParticleContainer) +- consume(pc::ParticleContainer): return incremental likelihood +""" +mutable struct ParticleContainer{T<:Particle, F} + vals::Vector{T} + # A named tuple with functions to manipulate the particles vi. + manipulators::NamedTuple{} + # logarithmic weights (Trace) or incremental log-likelihoods (ParticleContainer) + logWs::Vector{Float64} + #This corresponds to log p(x_{0:t}), therefore the log likelihood of the transitions up to this point. + #Especially important for ancestor sampling. + logPseq::Vector{Float64} + # log model evidence + logE::Float64 + # helpful for rejuvenation steps, e.g. in SMC2 + n_consume::Int +end + +## Empty initilaizaiton +ParticleContainer{T}() where T = ParticleContainer{T}(NamedTuple{}(),0) +ParticleContainer{T}(manipulators::NamedTuple{}) where T = ParticleContainer{T}(manipulators,0) + + +#Some initilaizaiton... +function ParticleContainer{T}(manipulators::NamedTuple{}, n::Int) where {T} + if !(:copy in keys(pc.manipulators)) manipulators = merge(manipulators, (copy = deepcopy,) end + if !(:set_retained_vns_del_by_spl! in keys(pc.manipulators)) manipulators = merge(manipulators, (set_retained_vns_del_by_spl! = (args) -> (),)) end + ParticleContainer( Vector{T}(undef, n), manipulators, Float64[],Float64[], 0.0, 0) + +end + + +Base.collect(pc :: ParticleContainer) = pc.vals # prev: Dict, now: Array +Base.length(pc :: ParticleContainer) = length(pc.vals) +Base.similar(pc :: ParticleContainer{T}) where T = ParticleContainer{T}(0) +# pc[i] returns the i'th particle +Base.getindex(pc :: ParticleContainer, i :: Real) = pc.vals[i] + + +# registers a new x-particle in the container +function Base.push!(pc::ParticleContainer, p::Particle) + push!(pc.vals, p) + push!(pc.logWs, 0.0) + push!(pc.logPseq,0.0) + pc +end + +function Base.push!(pc::ParticleContainer, n::Int, varInfo::VarInfo, tasks::Task, taskInfo<:AbstractTaskInfo) + # compute total number of particles number of particles + n0 = length(pc) + ntotal = n0 + n + + # add additional particles and weights + vals = pc.vals + logWs = pc.logWs + model = pc.model + resize!(vals, ntotal) + resize!(logWs, ntotal) + resize!(logPseq, ntotal) + + @inbounds for i in (n0 + 1):ntotal + vals[i] = Trace(varInfo, tasks, taskInfo) + logWs[i] = 0.0 + logPseq[i] = 0.0 + end + + pc +end + +# clears the container but keep params, logweight etc. +function Base.empty!(pc::ParticleContainer) + pc.vals = eltype(pc.vals)[] + pc.logWs = Float64[] + pc.logPseq = Float64[] + pc +end + +# clones a theta-particle +function Base.copy(pc::ParticleContainer) + # fork particles + vals = eltype(pc.vals)[fork(p, pc.mainpulators["copy"]) for p in pc.vals] + + # copy weights + logWs = copy(pc.logWs) + logPseq = copy(pc.logPseq) + + ParticleContainer(pc.model, vals, logWs, logPseq, pc.logE, pc.n_consume) +end + +# run particle filter for one step, return incremental likelihood +function Libtask.consume(pc :: ParticleContainer) + # normalisation factor: 1/N + z1 = logZ(pc) + n = length(pc) + + particles = collect(pc) + num_done = 0 + for i=1:n + p = particles[i] + score = Libtask.consume(p) + + if in(:logpseq,fieldnames(p.taskinfo)) && isa(p.taskinfo.logpseq,Float64) + set_logpseg(pc,i,p.taskinfo.logpseq) + end + + if score isa Real + score += p.taskinfo.logp + + ## Equivalent to reset logp + p.taskinfo.logp = 0 + + increase_logweight(pc, i, Float64(score)) + elseif score == Val{:done} + num_done += 1 + else + error("[consume]: error in running particle filter.") + end + end + + if num_done == n + res = Val{:done} + elseif num_done != 0 + error("[consume]: mis-aligned execution traces, num_particles= $(n), num_done=$(num_done).") + else + # update incremental likelihoods + z2 = logZ(pc) + res = increase_logevidence(pc, z2 - z1) + pc.n_consume += 1 + # res = increase_loglikelihood(pc, z2 - z1) + end + + res +end + +# compute the normalized weights +weights(pc::ParticleContainer) = softmax(pc.logWs) + +# compute the log-likelihood estimate, ignoring constant term ``- \log num_particles`` +logZ(pc::ParticleContainer) = logsumexp(pc.logWs) + +# compute the effective sample size ``1 / ∑ wᵢ²``, where ``wᵢ```are the normalized weights +function effectiveSampleSize(pc :: ParticleContainer) + Ws = weights(pc) + return inv(sum(abs2, Ws)) +end + +increase_logweight(pc :: ParticleContainer, t :: Int, logw :: Float64) = + (pc.logWs[t] += logw) + + +set_logpseg(pc :: ParticleContainer, t :: Int, logp :: Float64) = + (pc.logPseq[t] = logp) + +increase_logevidence(pc :: ParticleContainer, logw :: Float64) = + (pc.logE += logw) + + + ### + + ### Resample Steps + + ### From 0eebbe1d4192a1d875e3eb78013a7dfa26f9876a Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Sun, 24 Nov 2019 20:51:08 +0000 Subject: [PATCH 02/25] develop -- First runnable version A short demo is added to demonstarte.jl. It shows how this package could be used outside of Turing! --- demonstarte.jl | 54 ++ src/AdvancedPS.jl | 31 +- src/ConditionalParticleFilter.jl | 1020 ------------------------------ src/Interface.jl | 46 ++ src/resample.jl | 23 +- src/samplers.jl | 27 +- src/taskinfo.jl | 16 +- src/tasks.jl | 104 ++- 8 files changed, 213 insertions(+), 1108 deletions(-) create mode 100644 demonstarte.jl delete mode 100644 src/ConditionalParticleFilter.jl create mode 100644 src/Interface.jl diff --git a/demonstarte.jl b/demonstarte.jl new file mode 100644 index 00000000..fa188a45 --- /dev/null +++ b/demonstarte.jl @@ -0,0 +1,54 @@ +## It is not yet a package... +using AdvancedPS +using Libtask +const APS = AdvancedPS +using Distributions + +n = 20 + + + +## Our states +vi = Container(zeros(n),0) +## Our observations +y = Vector{Float64}(undef,n) +for i = 1:n-1 + y[i] = -i +end +## The particle container +particles = APS.ParticleContainer{typeof(vi),APS.PGTaskInfo }() + +# Define a short model. +# The syntax is rather simple. Observations need to be reported with report_observation. +# Transitions must be reported using report_transition. +# The trace contains the variables which we want to infer using particle gibbs. +# Thats all! +function task_f() + var = init() + + set_x(var,1,rand(Normal())) # We sample + logpseq += logpdf(Normal(),get_x(var,1)) + + for i = 2:n + ## Sampling + set_x(var,i, rand(Normal(get_x(var,i-1)-1,0.8))) # We sample from proposal + logγ = logpdf(Normal(get_x(var,i-1)-1,0.8)),get_x(var,i)) #γ(x_t|x_t-1) + logp = logpdf(Normal(),get_x(var,i)) # p(x_t|x_t-1) + report_transition!(var,logp,logγ) + + #Proposal and Resampling + logpy = logpdf(Normal(get_x(var,i),0.4),y[i-1]) + report_observation!(var,logpy) + end +end + + +# + + +m = 10 +task = create_task(task_f) +APS.extend!(particles, 10, vi, task, PGTaskInfo(0.0,0.0)) + +## Do one SMC step. +APS.sampleSMC!(particles,0.0) diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index db1d9bc7..5366fcb6 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -6,29 +6,35 @@ module AdvancedPS const DEBUG = Bool(parse(Int, get(ENV, "DEBUG_APS", "0"))) + using Libtask + using StatsFuns: logsumexp, softmax! + import Base.copy + abstract type AbstractTaskInfo end + abstract type AbstractParticleContainer end + abstract type AbstractTrace end + include("tasks.jl") - - import StatsBase: sample + export ParticleContainer, + Trace, + weights, + logZ, + current_trace, + extend! - abstract type AbstractTaskInfo end - include("resample.jl") export resample! - include("tasks.jl") - - export ParticleContainer, Trace include("taskinfo.jl") - export AbstractTaskInfo, TaskInfo + export PGTaskInfo include("samplers.jl") @@ -44,6 +50,15 @@ module AdvancedPS resample_stratified, resample_systematic + include("interface.jl") + export report_transition!, + report_observation!, + init, + Container, + set_x, + get_x, + copyC, + create_task end # module diff --git a/src/ConditionalParticleFilter.jl b/src/ConditionalParticleFilter.jl deleted file mode 100644 index 9893d224..00000000 --- a/src/ConditionalParticleFilter.jl +++ /dev/null @@ -1,1020 +0,0 @@ -using Turing -using Turing. - - - -### - -### General SMC sampler with proposal distributions - -### Conditional Particle Filter (Andireu 2010 https://www.stats.ox.ac.uk/~doucet/andrieu_doucet_holenstein_PMCMC.pdf) - -### CPF-Ancestor Sampling (Lindsten 2014 http://jmlr.org/papers/volume15/lindsten14a/lindsten14a.pdf) - -### - - - - -####################### - -# Particle Transition # - -####################### - - - - -""" - - ParticleTransition{T, F<:AbstractFloat} <: AbstractTransition - - - -Fields: - -- `θ`: The parameters for any given sample. - -- `lp`: The log pdf for the sample's parameters. - -- `le`: The log evidence retrieved from the particle. - -- `weight`: The weight of the particle the sample was retrieved from. - -""" - - - - -#### - -#### Generic Sequential Monte Carlo sampler. - -#### - - - -""" - - GSMC() - - - -General Proposal Sequential Monte Carlo sampler. - -This is an extension to the SMC sampler form the Turing repository. - -It allows to set the proposal distributions. - - - -Note that this method is particle-based, and arrays of variables - -must be stored in a [`TArray`](@ref) object. - - - -Fields: - - -- `resampler`: A function used to sample particles from the particle container. - - Defaults to `resample_systematic`. - -- `resampler_threshold`: The threshold at which resampling terminates -- defaults to 0.5. If - - the `ess` <= `resampler_threshold` * `n_particles`, the resampling step is completed. - - - - Usage: - - - -```julia - -GSMC() - - -# Only specify for z a proposal distributon independent -# from the other parameters - -GSMC(:x,:y,(:z , () -> f())) - - -# Only specify for z a proposal distributon -# which is dependent on x and y - -GSMC(:x,:y,(:z,[:x,:y], (args) -> f(args))) - -``` - -""" - - - - -struct GSMC{space, RT<:AbstractFloat} <: ParticleInference - - # Any contains a proposal distribution and a list of additional input variables - - proposals :: Dict{Symbol, Any} # Proposals for paramters - - resampler :: Function - - resampler_threshold :: RT - -end - - - -alg_str(spl::Sampler{GSMC}) = "GSMC" - - -## Inspired from MH algorithm - -function GSMC( - resampler::Function, - - resampler_threshold::RT, - - space::Tuple - -) where {RT<:AbstractFloat} - - new_space = () - - proposals = Dict{Symbol,Any}() - - - - # parse random variables with their hypothetical proposal - - for element in space - - if isa(element, Symbol) - - new_space = (new_space..., element) - - else - - @assert isa(element[1], Symbol) "[GSMC] ($element[1]) should be a Symbol. For proposal, use the syntax MH(N, (:m, () -> Normal(0, 0.1)))" - @assert length(element) == 2 || length(element) == 3 "[GSMC] ($element[1]) has wrong shape. Use syntax (:m, () -> Normal(0,0.1)) or (:z,[:x,:y], (args) -> f(args))" - new_space = (new_space..., element[1]) - if length(element)== 2 - proposals[element[1]] = (Vector{Symbol}(undef,0), element[2]) #No input arguments - elseif length(element) == 3 - @assert isa(element[2],Vector{Symbol}) "[GSMC] For length three elements, ($element[2]) should be a Vecotr{Symbol}" - proposal[element[1]] = (element[2], element[3]) - end - end - - end - - return GSMC{new_space, RT}(proposals ,resampler, resampler_threshold) - -end - -GSMC() = GSMC(resample_systematic, 0.5, ()) - -GSMC(::Tuple{}) = GSMC() - -function SMC(space::Symbol...) - - GSMC(resample_systematic, 0.5, space) - -end - - -## We can use the SMCState from the Turing repo - -# mutable struct SMCState{V<:VarInfo, F<:AbstractFloat} <: AbstractSamplerState -# -# vi :: V -# -# # The logevidence after aggregating all samples together. -# -# average_logevidence :: F -# -# particles :: ParticleContainer -# -# end -# -# -# -# function SMCState( -# -# model::M, -# -# ) where { -# -# M<:Model -# -# } -# -# vi = VarInfo(model) -# -# particles = ParticleContainer{Trace}(model) -# -# -# -# return SMCState(vi, 0.0, particles) -# -# end - -## We leave this function unchanged - -function Sampler(alg::T, model::Model, s::Selector) where T<:GSMC - - dict = Dict{Symbol, Any}() - - state = SMCState(model) - - return Sampler(alg, dict, s, state) - -end - -## We leave this function unchanged - -function sample_init!( - - ::AbstractRNG, - - model::Turing.Model, - - spl::Sampler{<:SMC}, - - N::Integer; - - kwargs... - -) - - # Set the parameters to a starting value. - - initialize_parameters!(spl; kwargs...) - - - - # Update the particle container now that the sampler type - - # is defined. - - spl.state.particles = ParticleContainer{Trace{typeof(spl), - - typeof(spl.state.vi), typeof(model)}}(model) - - - - spl.state.vi.num_produce = 0; # Reset num_produce before new sweep\. - - set_retained_vns_del_by_spl!(spl.state.vi, spl) - - resetlogp!(spl.state.vi) - - - - push!(spl.state.particles, N, spl, empty!(spl.state.vi)) - - - - while consume(spl.state.particles) != Val{:done} - - ess = effectiveSampleSize(spl.state.particles) - - if ess <= spl.alg.resampler_threshold * length(spl.state.particles) - - resample!(spl.state.particles, spl.alg.resampler) - - end - - end - -end - -## We leave this function unchanged - -function step!( - - ::AbstractRNG, - - model::Turing.Model, - - spl::Sampler{<:SMC}, - - ::Integer; - - iteration=-1, - - kwargs... - -) - - # Check that we received a real iteration number. - - @assert iteration >= 1 "step! needs to be called with an 'iteration' keyword argument." - - - - ## Grab the weights. - - Ws = weights(spl.state.particles) - - - - # update the master vi. - - particle = spl.state.particles.vals[iteration] - - params = tonamedtuple(particle.vi) - - lp = getlogp(particle.vi) - - - - return ParticleTransition(params, lp, spl.state.particles.logE, Ws[iteration]) - -end - - - -#### - -#### Particle Gibbs sampler. - -#### - - - -""" - - CPF(n_particles::Int,space) - - - -Conditional Particle Filter sampler. - - - -Note that this method is particle-based, and arrays of variables - -must be stored in a [`TArray`](@ref) object. - - - -Usage: - - - -```julia - -CPF(100) - -``` - -""" - -struct CPF{space} <: ParticleInference - - - proposals :: Dict{Symbol,Any}() # Proposal distributions - - n_particles :: Int # number of particles used - - resampler :: Function # function to resample - -end - -function CPF(n_particles::Int, resampler::Function, space::Tuple) - - new_space = () - - proposals = Dict{Symbol,Any}() - - - - # parse random variables with their hypothetical proposal - - for element in space - - if isa(element, Symbol) - - new_space = (new_space..., element) - - else - - @assert isa(element[1], Symbol) "[GSMC] ($element[1]) should be a Symbol. For proposal, use the syntax MH(N, (:m, () -> Normal(0, 0.1)))" - @assert length(element) == 2 || length(element) == 3 "[GSMC] ($element[1]) has wrong shape. Use syntax (:m, () -> Normal(0,0.1)) or (:z,[:x,:y], (args) -> f(args))" - new_space = (new_space..., element[1]) - if length(element)== 2 - proposals[element[1]] = (Vector{Symbol}(undef,0), element[2]) #No input arguments - elseif length(element) == 3 - @assert isa(element[2],Vector{Symbol}) "[GSMC] For length three elements, ($element[2]) should be a Vecotr{Symbol}" - proposal[element[1]] = (element[2], element[3]) - end - end - - end - - return GSMC{new_space, RT}(proposals ,n_particles,resampler) -end - -CPF(n1::Int, ::Tuple{}) = CPF(n1) - -function CPF(n1::Int, space::Symbol...) - - CPF(n1, resample_systematic, space) - -end - - - -alg_str(spl::Sampler{CPF}) = "CPF" - -## We resue this... -# -# mutable struct PGState{V<:VarInfo, F<:AbstractFloat} <: AbstractSamplerState -# -# vi :: V -# -# # The logevidence after aggregating all samples together. -# -# average_logevidence :: F -# -# end -# -# -# -# function PGState(model::M) where {M<:Model} -# -# vi = VarInfo(model) -# -# return PGState(vi, 0.0) -# -# end - - - - - - -""" - - Sampler(alg::PG, model::Model, s::Selector) - - - -Return a `Sampler` object for the PG algorithm. - -""" - -function Sampler(alg::T, model::Model, s::Selector) where T<:CPF - - info = Dict{Symbol, Any}() - - state = PGState(model) - - return Sampler(alg, info, s, state) - -end - - - -function step!( - - ::AbstractRNG, - - model::Turing.Model, - - spl::Sampler{<:PG}, - - ::Integer; - - kwargs... - -) - - particles = ParticleContainer{Trace{typeof(spl), typeof(spl.state.vi), typeof(model)}}(model) - - - - spl.state.vi.num_produce = 0; # Reset num_produce before new sweep. - - ref_particle = isempty(spl.state.vi) ? - - nothing : - - forkr(Trace(model, spl, spl.state.vi)) - - - - set_retained_vns_del_by_spl!(spl.state.vi, spl) - - resetlogp!(spl.state.vi) - - - - if ref_particle === nothing - - push!(particles, spl.alg.n_particles, spl, spl.state.vi) - - else - - push!(particles, spl.alg.n_particles-1, spl, spl.state.vi) - - push!(particles, ref_particle) - - end - - - - while consume(particles) != Val{:done} - - resample!(particles, spl.alg.resampler, ref_particle) - - end - - - - ## pick a particle to be retained. - - Ws = weights(particles) - - indx = randcat(Ws) - - - - # Extract the VarInfo from the retained particle. - - params = tonamedtuple(spl.state.vi) - - spl.state.vi = particles[indx].vi - - lp = getlogp(spl.state.vi) - - - - # update the master vi. - - return ParticleTransition(params, lp, particles.logE, 1.0) - -end - - - -function sample_end!( - - ::AbstractRNG, - - ::Model, - - spl::Sampler{<:ParticleInference}, - - N::Integer, - - ts::Vector{ParticleTransition}; - - kwargs... - -) - - # Set the default for resuming the sampler. - - resume_from = get(kwargs, :resume_from, nothing) - - - - # Exponentiate the average log evidence. - - # loge = exp(mean([t.le for t in ts])) - - loge = mean(t.le for t in ts) - - - - # If we already had a chain, grab the logevidence. - - if resume_from !== nothing # concat samples - - @assert resume_from isa Chains "resume_from needs to be a Chains object." - - # pushfirst!(samples, resume_from.info[:samples]...) - - pre_loge = resume_from.logevidence - - # Calculate new log-evidence - - pre_n = length(resume_from) - - loge = (pre_loge * pre_n + loge * N) / (pre_n + N) - - end - - - - # Store the logevidence. - - spl.state.average_logevidence = loge - -end - -## We need to sample form the proposal instead of the transition - -function assume( spl::Sampler{T}, - - dist::Distribution, - - vn::VarName, - - _::VarInfo - - ) where T<:Union{CPF,GSMC} - - - ## The part concerning the tasks is left unchanged ! - - vi = current_trace().vi - - if isempty(getspace(spl.alg)) || vn.sym in getspace(spl.alg) - - if ~haskey(vi, vn) - - ## Here, our changings start... - ## Attention, we do not check for the support of the proposal distribution!! - ## We implicitely assume so far that the support is not violated!! - ## We changed r = rand(dist) with the following code in order to include proposal distributions. - - if vn.sym in keys(spl.alg.proposals) # Custom proposal for this parameter - - - tuple = spl.alg.proposals[vn.sym]() - - #First extract the argument variables - extracted_symbosl = tuple[1] - if isempty(extracted_symbosl) - proposal = tuple[2]() - else - - args = [] - for sym in extracted_symbosl - if isempty(getspace(spl.alg)) || sym in getspace(spl.alg) - error("[CPF] Symbol ($sym) is not defined yet. The arguments for the propsal must occur before!") - end - push!(args,vi[sym]) - end - proposal = tuble[2](args) - end - r = rand(proposal) - ## In this case, we need to set the logp because we do not sample directly from the prior! - acclogp!(vi, logpdf(dist, r) -logpdf(proposal, r)) - - end - else # Prior as proposal - - r = rand(dist) - - spl.state.prior_prob += logpdf(dist, r) # accumulate prior for PMMH - - end - - - push!(vi, vn, r, dist, spl) - - elseif is_flagged(vi, vn, "del") - - - - unset_flag!(vi, vn, "del") - - - ## Here, our changings start... - ## Attention, we do not check for the support of the proposal distribution!! - ## We implicitely assume so far that the support is not! violated - - if vn.sym in keys(spl.alg.proposals) # Custom proposal for this parameter - - - tuple = spl.alg.proposals[vn.sym]() - - #First extract the argument variables - extracted_symbosl = tuple[1] - if isempty(extracted_symbosl) - proposal = tuple[2]() - else - - args = [] - for sym in extracted_symbosl - if isempty(getspace(spl.alg)) || sym in getspace(spl.alg) - error("[CPF] Symbol ($sym) is not defined yet. The arguments for the propsal must occur before!") - end - push!(args,vi[sym]) - end - proposal = tuble[2](args) - end - r = rand(proposal) - - ## In this case, we need to set the logp because we do not sample directly from the prior! - acclogp!(vi, logpdf(dist, r) -logpdf(proposal, r)) - - end - else # Prior as proposal - - r = rand(dist) - - spl.state.prior_prob += logpdf(dist, r) # accumulate prior for PMMH - - end - - vi[vn] = vectorize(dist, r) - - setgid!(vi, spl.selector, vn) - - setorder!(vi, vn, vi.num_produce) - - else - - updategid!(vi, vn, spl) - - r = vi[vn] - - end - - else # vn belongs to other sampler <=> conditionning on vn - - if haskey(vi, vn) - - r = vi[vn] - - else # What happens here? I assume that vn does not belong to any sampler - how is this possible - # Should the sanity check not prevent his - r = rand(dist) - - push!(vi, vn, r, dist, Selector(:invalid)) - - end - - acclogp!(vi, logpdf_with_trans(dist, r, istrans(vi, vn))) - - end - - return r, zero(Real) - -end - -## Only need to change A<:Union{CPF,GSMC} - -function assume( spl::Sampler{A}, - - dists::Vector{D}, - - vn::VarName, - - var::Any, - - vi::VarInfo - - ) where {A<:Union{PG,SMC},D<:Distribution} - - error("[Turing] CPF and GSMC doesn't support vectorizing assume statement") - -end - -## Only need to change A<:Union{CPF,GSMC} - - -function observe(spl::Sampler{T}, dist::Distribution, value, vi) where T<:Union{CPF,GSMC} - - produce(logpdf(dist, value)) - - return zero(Real) - -end - - -## Only need to change A<:Union{CPF,GSMC} - -function observe( spl::Sampler{A}, - - ds::Vector{D}, - - value::Any, - - vi::VarInfo - - ) where {A<:Union{CPF,GSMC},D<:Distribution} - - error("[Turing] CPF and GSMC doesn't support vectorizing observe statement") - -end - - - -for alg in (:GSMC,:CPF) - - @eval getspace(::$alg{space}) where {space} = space - - @eval getspace(::Type{<:$alg{space}}) where {space} = space - -########################################################################################### -## We do not change these - therefore, we use the code directly from the turing repo -# -# -# #### -# -# #### Resampling schemes for particle filters -# -# #### -# -# -# -# # Some references -# -# # - http://arxiv.org/pdf/1301.4019.pdf -# -# # - http://people.isy.liu.se/rt/schon/Publications/HolSG2006.pdf -# -# # Code adapted from: http://uk.mathworks.com/matlabcentral/fileexchange/24968-resampling-methods-for-particle-filtering -# -# -# -# # Default resampling scheme -# -# function resample(w::AbstractVector{<:Real}, num_particles::Integer=length(w)) -# -# return resample_systematic(w, num_particles) -# -# end -# -# -# -# # More stable, faster version of rand(Categorical) -# -# function randcat(p::AbstractVector{T}) where T<:Real -# -# r, s = rand(T), 1 -# -# for j in eachindex(p) -# -# r -= p[j] -# -# if r <= zero(T) -# -# s = j -# -# break -# -# end -# -# end -# -# return s -# -# end -# -# -# -# function resample_multinomial(w::AbstractVector{<:Real}, num_particles::Integer) -# -# return rand(Distributions.sampler(Categorical(w)), num_particles) -# -# end -# -# -# -# function resample_residual(w::AbstractVector{<:Real}, num_particles::Integer) -# -# -# -# M = length(w) -# -# -# -# # "Repetition counts" (plus the random part, later on): -# -# Ns = floor.(length(w) .* w) -# -# -# -# # The "remainder" or "residual" count: -# -# R = Int(sum(Ns)) -# -# -# -# # The number of particles which will be drawn stocastically: -# -# M_rdn = num_particles - R -# -# -# -# # The modified weights: -# -# Ws = (M .* w - floor.(M .* w)) / M_rdn -# -# -# -# # Draw the deterministic part: -# -# indx1, i = Array{Int}(undef, R), 1 -# -# for j in 1:M -# -# for k in 1:Ns[j] -# -# indx1[i] = j -# -# i += 1 -# -# end -# -# end -# -# -# -# # And now draw the stocastic (Multinomial) part: -# -# return append!(indx1, rand(Distributions.sampler(Categorical(w)), M_rdn)) -# -# end -# -# -# -# function resample_stratified(w::AbstractVector{<:Real}, num_particles::Integer) -# -# -# -# Q, N = cumsum(w), num_particles -# -# -# -# T = Array{Float64}(undef, N + 1) -# -# for i=1:N, -# -# T[i] = rand() / N + (i - 1) / N -# -# end -# -# T[N+1] = 1 -# -# -# -# indx, i, j = Array{Int}(undef, N), 1, 1 -# -# while i <= N -# -# if T[i] < Q[j] -# -# indx[i] = j -# -# i += 1 -# -# else -# -# j += 1 -# -# end -# -# end -# -# return indx -# -# end -# -# -# -# function resample_systematic(w::AbstractVector{<:Real}, num_particles::Integer) -# -# -# -# Q, N = cumsum(w), num_particles -# -# -# -# T = collect(range(0, stop = maximum(Q)-1/N, length = N)) .+ rand()/N -# -# push!(T, 1) -# -# -# -# indx, i, j = Array{Int}(undef, N), 1, 1 -# -# while i <= N -# -# if T[i] < Q[j] -# -# indx[i] = j -# -# i += 1 -# -# else -# -# j += 1 -# -# end -# -# end -# -# return indx -# -# end diff --git a/src/Interface.jl b/src/Interface.jl new file mode 100644 index 00000000..4f466af2 --- /dev/null +++ b/src/Interface.jl @@ -0,0 +1,46 @@ + + +## Some function which make the model easier to define. + + +# A very light weight container for a state space model +# The state space is one dimensional! +# Note that this is only for a very simple demonstration. +mutable struct Container + x::Vector{Float64} + num_produce::Float64 +end + +# This is important for initalizaiton +function init() + return current_trace() +end + +# Th +function report_observation!(trace, logp::Float64) + trace.taskinfo.logp += logp + produce(logp) + trace = current_trace() +end + +# logγ corresponds to the proposal distributoin we are sampling from. +function report_transition!(trace,logp::Float64,logγ::Float64) + trace.taskinfo.logp += logp - logγ + trace.taskinfo.logpseq += logp +end + +function get_x(trace,indx) + return @inbounds trace.vi.x[indx] +end +function set_x(trace,indx,val) + return @inbounds trace.vi.x[indx] = val +end + + +function copyC(vi::Container) + Container(deepcopy(vi.x),vi.num_produce) +end + +function create_task(f::Function) + return CTask(() -> begin new_vi=f(); produce(Val{:done}); new_vi; end ) +end diff --git a/src/resample.jl b/src/resample.jl index 2e2094ec..fc3a3500 100644 --- a/src/resample.jl +++ b/src/resample.jl @@ -1,13 +1,12 @@ function resample!( pc :: ParticleContainer, - indx :: Vecotr{Int64} = resample_systematic, - ref :: Union{Particle, Nothing} = nothing - ancestor_idx :: int = -1 - new_ancestor_traj :: Union{Particle,Nothing} = nothing + indx :: Vector{Int64}, + ref :: Union{Particle, Nothing} = nothing, + new_ref:: Union{Particle, Nothing} = nothing ) - + n = length(pc.vals) # count number of children for each particle num_children = zeros(Int, n) @inbounds for i in indx @@ -36,16 +35,24 @@ function resample!( end if ref !== nothing + @inbounds ancestor_idx = indx[n] # Insert the retained particle. This is based on the replaying trick for efficiency # reasons. If we implement PG using task copying, we need to store Nx * T particles! # This is a rather effcient way of how to solve the ancestor problem. - if ancestor_idx == n || ancestor_idx == -1 + if ancestor_idx == n @inbounds children[n] = ref + elseif new_ref !== nothing + @inbounds children[n] = new_ref else @assert isa(Particle,new_ancestor_traj) "[AdvancedPS] ($new_ancestor_traj) must be of type particle" + try + @inbounds chosen_traj = fork(particle[ancestor_idx], pc.mainpulators["copy"]) + new_ancestor_traj = pc.manipulators["merge_traj"](chosen_traj,ref) + catch e + error("[Advanced PS] Ancestor sampling went wrong...") + end @inbounds children[n] = new_ancestor_traj - else - + end end # replace particles and log weights in the container with new particles and weights diff --git a/src/samplers.jl b/src/samplers.jl index a5efc29b..f07cfd19 100644 --- a/src/samplers.jl +++ b/src/samplers.jl @@ -3,7 +3,7 @@ function sampleSMC!(pc::ParticleContainer,resampler_threshold::AbstractFloat) while consume(pc) != Val{:done} ess = effectiveSampleSize(pc) - if ess <= spl.alg.resampler_threshold * length(pc) + if ess <= resampler_threshold * length(pc) # compute weights Ws = weights(pc) # check that weights are not NaN @@ -17,11 +17,28 @@ function sampleSMC!(pc::ParticleContainer,resampler_threshold::AbstractFloat) end end +function samplePG!(pc::ParticleContainer,resampler::Function,ref_particle::Union{Particle,}=nothing,resampler_threshold =1) -function samplePG!(pc::ParticleContainer,ref_particle::Union{Particle,},resampler::Function ) - - while consume(pc) != Val{:done} - resample!(pc, resampler, ref_particle) + if ref_particle === nothing + #Do normal SMC + #By defualt, resampler_threshold is set to one, so that we always resample + sampleSMC!(pc,resampler_threshold) + else + while consume(pc) != Val{:done} + # compute weights + Ws = weights(pc) + # check that weights are not NaN + @assert !any(isnan, Ws) + # sample ancestor indices + n = length(pc) + # Ancestor trajectory is not sampled + nresamples = n-1 + indx = randcat(Ws, nresamples) + # We add ancestor trajectory to the path. + # For ancestor sampling, we would change n at this point. + push!(indx,n) + resample!(particles, indx,ref= ref_particle) + end end end diff --git a/src/taskinfo.jl b/src/taskinfo.jl index 9a2a4095..5ca6a89e 100644 --- a/src/taskinfo.jl +++ b/src/taskinfo.jl @@ -4,7 +4,19 @@ # allows to propagate information trough the computation. # This is important because we do not want to -struct TaskInfo <:AbstractTaskInfo - getidcs::Function # Function to get gicds from sampler +mutable struct PGTaskInfo <: AbstractTaskInfo + # This corresponds to p(y_t | x_t)*p(x_t|x_t-1)/ γ(x_t|x_t-1,y_t), + # where γ is the porposal. + # We need this variable to compute the weights! + logp::Float64 + + # This corresponds to p(x_t|x_t-1)*p(x_t-1|x_t-2)*... *p(x_0) + # or |x_{0:t-1} for non markovian models, we need this to compute + # the ancestor weights. + logpseq::Float64 end + +function copy(info::PGTaskInfo) + PGTaskInfo(info.logp, info.logpseq) +end diff --git a/src/tasks.jl b/src/tasks.jl index 9f94b97c..3c332e93 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -1,41 +1,25 @@ - - -using Turing.Core.RandomVariables -using Turing - - - # Idea: We decouple tasks form the model by only allowing to pass a function. # This way, we do no longer need to have the model and the sampler saved in the trace struct # However, we still need to insantiate the VarInfo # TaskInfo stores additional informatino about the task -mutable struct Trace{Tvi, Tinfo <:AbstractTaskInfo} +mutable struct Trace{Tvi, TInfo} <: AbstractTrace where {Tvi, TInfo <: AbstractTaskInfo} vi::Tvi # Unfortunatley, we can not set force this to be a subtype of VarInfo... task::Task - taksinfo::Tinfo - function Trace{AbstractTaskInfo}(vi, taskinfo<:AbstractTaskInfo, task::Task) - return new{typeof(vi),typeof(taskinfo)}(vi, task, taskinfo) - end + taskinfo::TInfo end function Base.copy(trace::Trace, copy_vi::Function) - try - vi = copy_vi(trace.vi) - catch e - error("[AdvancedPS] The copy function given by the manipulators must only accept vi as argument") - end - taskinfo = copy(trace.taskinfo) - res = Trace{AbstractTaskInfo}(vi, copy(trace.task), taskinfo) + res = Trace{typeof(trace.vi),typeof(trace.taskinfo)}(copy_vi(trace.vi), copy(trace.task), copy(trace.taskinfo)) return res end # The procedure passes a function which is specified by the model. -function Trace(f::Function, vi<:RandomVariables.AbstractVarInfo, taskinfo<:AbstractTaskInfo, copy_vi::Function) where {T <: AbstractSampler} +function Trace(f::Function, vi, taskinfo, copy_vi::Function) task = CTask( () -> begin res=f(); produce(Val{:done}); res; end ) - res = Trace{AbstractTaskInfo}(copy_vi(vi), task, copy(taskinfo)); + res = Trace{typeof(vi),typeof(task)}(copy_vi(vi), task, copy(taskinfo)); # CTask(()->f()); if res.task.storage === nothing res.task.storage = IdDict() @@ -45,15 +29,11 @@ function Trace(f::Function, vi<:RandomVariables.AbstractVarInfo, taskinfo<:Abstr end ## We need to design the task in the Turing wrapper. -function Trace(task::Task, vi<:RandomVariables.AbstractVarInfo, taskinfo<:AbstractTaskInfo, copy_vi::Function) - try - new_vi = copy_vi(vi) - catch e - error("[AdvancedPS] The copy function given by the manipulators must only accept vi as argument") - end - res = Trace{AbstractTaskInfo}(new_vi, task, copy(taskinfo)); +function Trace( vi, task::Task, taskinfo, copy_vi::Function) + + + res = Trace{typeof(vi),typeof(taskinfo)}(copy_vi(vi), Libtask.copy(task), copy(taskinfo)); # CTask(()->f()); - res.task = task if res.task.storage === nothing res.task.storage = IdDict() end @@ -67,15 +47,12 @@ end Libtask.consume(t::Trace) = (t.vi.num_produce += 1; consume(t.task)) # Task copying version of fork for Trace. -function fork(trace :: Trace, copy_vi::Function, is_ref :: Bool = false, set_retained_vns_del_by_spl!::Function = nothing) +function fork(trace :: Trace, copy_vi::Function, is_ref :: Bool = false, set_retained_vns_del_by_spl!::Union{Function,Nothing} = nothing) newtrace = copy(trace) if is_ref @assert set_retained_vns_del_by_spl! != nothing "[AdvancedPF] set_retained_vns_del_by_spl! is not set." - try - set_retained_vns_del_by_spl!(newtrace.vi, taskinfo.idcs) - catch e - error("[AdvancedPS] set_retained_vns_del_by_spl! must have the form set_retained_vns_del_by_spl!(newtrace.vi, taskinfo.idcs)") - end + set_retained_vns_del_by_spl!(newtrace.vi) + end newtrace.task.storage[:turing_trace] = newtrace return newtrace end @@ -83,12 +60,8 @@ end # PG requires keeping all randomness for the reference particle # Create new task and copy randomness function forkr(trace :: Trace, copy_vi::Function) - try - new_vi = copy_vi(trace.vi) - catch e - error("[AdvancedPS] The copy function given by the manipulators must only accept vi as argument") - end - newtrace = Trace(trace.task.code,new_vi ,copy(trace.taskinfo)) + + newtrace = Trace(trace.vi,trace.task ,trace.taskinfo,copy_vi) newtrace.vi.num_produce = 0 return newtrace end @@ -103,31 +76,31 @@ Data structure for particle filters - normalise!(pc::ParticleContainer) - consume(pc::ParticleContainer): return incremental likelihood """ -mutable struct ParticleContainer{T<:Particle, F} - vals::Vector{T} +mutable struct ParticleContainer{Tvi,TInfo} <: AbstractParticleContainer where {Tvi, TInfo <: AbstractTaskInfo} + vals::Vector{Trace{Tvi,TInfo}} # A named tuple with functions to manipulate the particles vi. - manipulators::NamedTuple{} + manipulators::Dict{String,Function} # logarithmic weights (Trace) or incremental log-likelihoods (ParticleContainer) logWs::Vector{Float64} #This corresponds to log p(x_{0:t}), therefore the log likelihood of the transitions up to this point. #Especially important for ancestor sampling. - logPseq::Vector{Float64} + logpseq::Vector{Float64} # log model evidence logE::Float64 # helpful for rejuvenation steps, e.g. in SMC2 n_consume::Int -end -## Empty initilaizaiton -ParticleContainer{T}() where T = ParticleContainer{T}(NamedTuple{}(),0) -ParticleContainer{T}(manipulators::NamedTuple{}) where T = ParticleContainer{T}(manipulators,0) +end +## Empty initilaizaiton +ParticleContainer{Tvi,TInfo}() where {Tvi, TInfo <: AbstractTaskInfo} = ParticleContainer{Tvi,TInfo}(Dict{String,Function}(),0) +ParticleContainer{Tvi,TInfo}(manipulators::Dict{String,Function}) where {Tvi, TInfo <: AbstractTaskInfo} = ParticleContainer{Tvi,TInfo}(manipulators,0) #Some initilaizaiton... -function ParticleContainer{T}(manipulators::NamedTuple{}, n::Int) where {T} - if !(:copy in keys(pc.manipulators)) manipulators = merge(manipulators, (copy = deepcopy,) end - if !(:set_retained_vns_del_by_spl! in keys(pc.manipulators)) manipulators = merge(manipulators, (set_retained_vns_del_by_spl! = (args) -> (),)) end - ParticleContainer( Vector{T}(undef, n), manipulators, Float64[],Float64[], 0.0, 0) +function ParticleContainer{Tvi,TInfo}(manipulators::Dict{String,Function}, n::Int) where {Tvi, TInfo <: AbstractTaskInfo} + if !("copy" in keys(manipulators)) manipulators["copy"] = deepcopy end + if !("set_retained_vns_del_by_spl!" in keys(manipulators)) manipulators["set_retained_vns_del_by_spl!"] = (x) -> () end + ParticleContainer{Tvi,TInfo}( Vector{Trace{Tvi,TInfo}}(undef, n), manipulators, Float64[],Float64[], 0.0, 0) end @@ -143,11 +116,11 @@ Base.getindex(pc :: ParticleContainer, i :: Real) = pc.vals[i] function Base.push!(pc::ParticleContainer, p::Particle) push!(pc.vals, p) push!(pc.logWs, 0.0) - push!(pc.logPseq,0.0) + push!(pc.logpseq,0.0) pc end -function Base.push!(pc::ParticleContainer, n::Int, varInfo::VarInfo, tasks::Task, taskInfo<:AbstractTaskInfo) +function extend!(pc::ParticleContainer, n::Int, varInfo, tasks::Task, taskInfo::T) where T <: AbstractTaskInfo # compute total number of particles number of particles n0 = length(pc) ntotal = n0 + n @@ -155,15 +128,15 @@ function Base.push!(pc::ParticleContainer, n::Int, varInfo::VarInfo, tasks::Task # add additional particles and weights vals = pc.vals logWs = pc.logWs - model = pc.model + logpseq = pc.logpseq resize!(vals, ntotal) resize!(logWs, ntotal) - resize!(logPseq, ntotal) + resize!(logpseq, ntotal) @inbounds for i in (n0 + 1):ntotal - vals[i] = Trace(varInfo, tasks, taskInfo) + vals[i] = Trace(varInfo,tasks, taskInfo, pc.manipulators["copy"]) logWs[i] = 0.0 - logPseq[i] = 0.0 + logpseq[i] = 0.0 end pc @@ -173,7 +146,7 @@ end function Base.empty!(pc::ParticleContainer) pc.vals = eltype(pc.vals)[] pc.logWs = Float64[] - pc.logPseq = Float64[] + pc.logpseq = Float64[] pc end @@ -184,9 +157,9 @@ function Base.copy(pc::ParticleContainer) # copy weights logWs = copy(pc.logWs) - logPseq = copy(pc.logPseq) + logpseq = copy(pc.logpseq) - ParticleContainer(pc.model, vals, logWs, logPseq, pc.logE, pc.n_consume) + ParticleContainer( vals, pc.manipulators, logWs, logpseq, pc.logE, pc.n_consume) end # run particle filter for one step, return incremental likelihood @@ -201,7 +174,7 @@ function Libtask.consume(pc :: ParticleContainer) p = particles[i] score = Libtask.consume(p) - if in(:logpseq,fieldnames(p.taskinfo)) && isa(p.taskinfo.logpseq,Float64) + if in(:logpseq,fieldnames(typeof(p.taskinfo))) && isa(p.taskinfo.logpseq,Float64) set_logpseg(pc,i,p.taskinfo.logpseq) end @@ -234,8 +207,9 @@ function Libtask.consume(pc :: ParticleContainer) res end + # compute the normalized weights -weights(pc::ParticleContainer) = softmax(pc.logWs) +weights(pc::ParticleContainer) = softmax!(copy(pc.logWs)) # compute the log-likelihood estimate, ignoring constant term ``- \log num_particles`` logZ(pc::ParticleContainer) = logsumexp(pc.logWs) @@ -251,7 +225,7 @@ increase_logweight(pc :: ParticleContainer, t :: Int, logw :: Float64) = set_logpseg(pc :: ParticleContainer, t :: Int, logp :: Float64) = - (pc.logPseq[t] = logp) + (pc.logpseq[t] = logp) increase_logevidence(pc :: ParticleContainer, logw :: Float64) = (pc.logE += logw) From 723284cd8a7bf933157694103a785555d553ca60 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Sun, 24 Nov 2019 21:05:58 +0000 Subject: [PATCH 03/25] only typos some typose fixed --- Tests/TestTuring.jl | 109 -------------------------------------------- demonstarte.jl | 13 ++---- src/AdvancedPS.jl | 2 +- src/Interface.jl | 6 +-- 4 files changed, 9 insertions(+), 121 deletions(-) delete mode 100644 Tests/TestTuring.jl diff --git a/Tests/TestTuring.jl b/Tests/TestTuring.jl deleted file mode 100644 index 35ec810f..00000000 --- a/Tests/TestTuring.jl +++ /dev/null @@ -1,109 +0,0 @@ -using("Turing.jl") -using LinearAlgebra -using CUDAdrv -using CuArrays -# We use a very easy markovian model to test... -@model gdemo(y) = begin - # Set priors. - s ~ InverseGamma(2, 3) - m ~ Normal(0,5) - x = Vector{Real}(undef, 10) - z = Vector{Real}(undef, 10) - x[1] ~ Normal(0,1) - for j = 2:10 - x[j] ~ Normal(m+x[j-1], sqrt(s)) - x[j] ~ Normal(x[j-1], sqrt(s)) - z[j] = x[j]^2 - y[j-1] ~ Normal(x[j],0.1) - end - return z -end - -y = [ 1 2 3 4 5 6 7 8 9 10] -c = sample(gdemo(y), Gibbs(HMC(0.2,10,:m,:s),MH(:x)), 100) -sample(gdemo(y), SMC(:x,:y), 100) - - -hi = (4,"a") - -arr = Vector{Symbol}(undef,1) -typeof((Array{Symbol,1},"a")) -isempty(arr) - -Float64[] - - - - -v = rand(Normal()) -pdf = logpdf(Normal(),v) - - -vectorize(Normal(),v) - -dist =MvNormal([ 2. 1 1; 1 2 1; 1 1 2]) - -dist2 = Normal(2,3) - -std(dist2) - -mean(dist2) - - - -s = [] -for i = 1:10 - push!(s,i) -end - -function pr(x) - println(x) -end - - -M = rand(InverseWishart(100,Matrix{Float64}(I, 100, 100))) - - - - - - -@model gdemo() = begin - # Set priors. - pr("start") - s ~ InverseGamma(2, 3) - pr("in the first block") - cholesky(M) - m ~ Normal(0,5) - pr("hi n the block") - x = Normal(0, 10) - pr("end") - return x -end - -sample(gdemo(), Gibbs(HMC(0.2,1,:m,:s),MH(:x)), 2) - - - -push!(LOAD_PATH, string(pwd(),"/Turing/src/")) -push!(LOAD_PATH, string(pwd(),"/AdvancedPS/src/")) -using Pkg -include(string(pwd(),"/Turing/src/Turing.jl")) -include(string(pwd(),"/AdvancedPS/src/AdvancedPS.jl")) - -using AdvancedPS -using Turing - -@model mld = begin - x ~Normal() -end - - -F = (copy = copy, pr = println) -F[:pr](4) -typeof(F) <: NamedTuple{} - -NamedTuple{}() - -names(F) -@assert (:copy,:pr) in keys(F) diff --git a/demonstarte.jl b/demonstarte.jl index fa188a45..8357d077 100644 --- a/demonstarte.jl +++ b/demonstarte.jl @@ -27,28 +27,25 @@ function task_f() var = init() set_x(var,1,rand(Normal())) # We sample - logpseq += logpdf(Normal(),get_x(var,1)) for i = 2:n - ## Sampling + # Sampling set_x(var,i, rand(Normal(get_x(var,i-1)-1,0.8))) # We sample from proposal - logγ = logpdf(Normal(get_x(var,i-1)-1,0.8)),get_x(var,i)) #γ(x_t|x_t-1) + logγ = logpdf(Normal(get_x(var,i-1)-1,0.8),get_x(var,i)) #γ(x_t|x_t-1) logp = logpdf(Normal(),get_x(var,i)) # p(x_t|x_t-1) report_transition!(var,logp,logγ) #Proposal and Resampling logpy = logpdf(Normal(get_x(var,i),0.4),y[i-1]) - report_observation!(var,logpy) + var = report_observation!(var,logpy) + end end -# - - m = 10 task = create_task(task_f) -APS.extend!(particles, 10, vi, task, PGTaskInfo(0.0,0.0)) +APS.extend!(particles, 10, vi, task, PGTaskInfo(0.0,0.0)) ## Do one SMC step. APS.sampleSMC!(particles,0.0) diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index 5366fcb6..4b425113 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -50,7 +50,7 @@ module AdvancedPS resample_stratified, resample_systematic - include("interface.jl") + include("Interface.jl") export report_transition!, report_observation!, diff --git a/src/Interface.jl b/src/Interface.jl index 4f466af2..8ad525a6 100644 --- a/src/Interface.jl +++ b/src/Interface.jl @@ -17,14 +17,14 @@ function init() end # Th -function report_observation!(trace, logp::Float64) +function report_observation(trace, logp::Float64) trace.taskinfo.logp += logp produce(logp) - trace = current_trace() + current_trace() end # logγ corresponds to the proposal distributoin we are sampling from. -function report_transition!(trace,logp::Float64,logγ::Float64) +function report_transition(trace,logp::Float64,logγ::Float64) trace.taskinfo.logp += logp - logγ trace.taskinfo.logpseq += logp end From 4f69f37fec4627db87c23506388260488c5abafe Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Mon, 25 Nov 2019 13:51:39 +0000 Subject: [PATCH 04/25] Compatible with Turing Some corrections, now it rusn with Turing. Check the ParticleGibbsExtension branch! --- demonstarte.jl | 4 +++- src/AdvancedPS.jl | 3 ++- src/resample.jl | 6 +++--- src/samplers.jl | 14 ++++++++------ src/tasks.jl | 4 ++-- test_with_turing.jl | 15 +++++++++++++++ 6 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 test_with_turing.jl diff --git a/demonstarte.jl b/demonstarte.jl index 8357d077..ec0a2422 100644 --- a/demonstarte.jl +++ b/demonstarte.jl @@ -48,4 +48,6 @@ task = create_task(task_f) APS.extend!(particles, 10, vi, task, PGTaskInfo(0.0,0.0)) ## Do one SMC step. -APS.sampleSMC!(particles,0.0) +APS.sampleSMC!(particles) + +particles[1] diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index 4b425113..fc45f4bc 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -22,7 +22,8 @@ module AdvancedPS weights, logZ, current_trace, - extend! + extend!, + empty! include("resample.jl") diff --git a/src/resample.jl b/src/resample.jl index fc3a3500..288f9f0d 100644 --- a/src/resample.jl +++ b/src/resample.jl @@ -24,12 +24,12 @@ function resample!( # fork first child pi = particles[i] isref = pi === ref - p = isref ? fork(pi, pc.mainpulators["copy"], isref, pc.mainpulators["set_retained_vns_del_by_spl!"]) : pi + p = isref ? fork(pi, pc.manipulators["copy"], isref, pc.manipulators["set_retained_vns_del_by_spl!"]) : pi children[j += 1] = p # fork additional children for _ in 2:ni - children[j += 1] = fork(p, pc.mainpulators["copy"], isref, pc.mainpulators["set_retained_vns_del_by_spl!"]) + children[j += 1] = fork(p, pc.manipulators["copy"], isref, pc.manipulators["set_retained_vns_del_by_spl!"]) end end end @@ -46,7 +46,7 @@ function resample!( else @assert isa(Particle,new_ancestor_traj) "[AdvancedPS] ($new_ancestor_traj) must be of type particle" try - @inbounds chosen_traj = fork(particle[ancestor_idx], pc.mainpulators["copy"]) + @inbounds chosen_traj = fork(particle[ancestor_idx], pc.manipulators["copy"]) new_ancestor_traj = pc.manipulators["merge_traj"](chosen_traj,ref) catch e error("[Advanced PS] Ancestor sampling went wrong...") diff --git a/src/samplers.jl b/src/samplers.jl index f07cfd19..e406cfb9 100644 --- a/src/samplers.jl +++ b/src/samplers.jl @@ -1,6 +1,8 @@ -function sampleSMC!(pc::ParticleContainer,resampler_threshold::AbstractFloat) +function sampleSMC!(pc::ParticleContainer,resampler::Function =resample_systematic ,resampler_threshold::AbstractFloat = 0.5) + println("Hmmm") + while consume(pc) != Val{:done} ess = effectiveSampleSize(pc) if ess <= resampler_threshold * length(pc) @@ -11,13 +13,13 @@ function sampleSMC!(pc::ParticleContainer,resampler_threshold::AbstractFloat) # sample ancestor indices n = length(pc) nresamples = n - indx = randcat(Ws, nresamples) - resample!(particles, indx) + indx = resampler(Ws, nresamples) + resample!(pc, indx) end end end -function samplePG!(pc::ParticleContainer,resampler::Function,ref_particle::Union{Particle,}=nothing,resampler_threshold =1) +function samplePG!(pc::ParticleContainer,resampler::Function = resample_systematic ,ref_particle::Union{Particle,Nothing}=nothing , resampler_threshold =1) if ref_particle === nothing #Do normal SMC @@ -33,11 +35,11 @@ function samplePG!(pc::ParticleContainer,resampler::Function,ref_particle::Union n = length(pc) # Ancestor trajectory is not sampled nresamples = n-1 - indx = randcat(Ws, nresamples) + indx = resampler(Ws, nresamples) # We add ancestor trajectory to the path. # For ancestor sampling, we would change n at this point. push!(indx,n) - resample!(particles, indx,ref= ref_particle) + resample!(pc, indx,ref_particle) end end diff --git a/src/tasks.jl b/src/tasks.jl index 3c332e93..c68d52bc 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -9,7 +9,7 @@ mutable struct Trace{Tvi, TInfo} <: AbstractTrace where {Tvi, TInfo <: Abstract taskinfo::TInfo end -function Base.copy(trace::Trace, copy_vi::Function) +function Base.copy(trace::Trace{Tvi,TInfo}, copy_vi::Function) where {Tvi, TInfo <: AbstractTaskInfo} res = Trace{typeof(trace.vi),typeof(trace.taskinfo)}(copy_vi(trace.vi), copy(trace.task), copy(trace.taskinfo)) return res end @@ -48,7 +48,7 @@ Libtask.consume(t::Trace) = (t.vi.num_produce += 1; consume(t.task)) # Task copying version of fork for Trace. function fork(trace :: Trace, copy_vi::Function, is_ref :: Bool = false, set_retained_vns_del_by_spl!::Union{Function,Nothing} = nothing) - newtrace = copy(trace) + newtrace = copy(trace,copy_vi) if is_ref @assert set_retained_vns_del_by_spl! != nothing "[AdvancedPF] set_retained_vns_del_by_spl! is not set." set_retained_vns_del_by_spl!(newtrace.vi) diff --git a/test_with_turing.jl b/test_with_turing.jl new file mode 100644 index 00000000..958ec898 --- /dev/null +++ b/test_with_turing.jl @@ -0,0 +1,15 @@ +include("Turing.jl/src/Turing.jl") +using Distributions + + +Turing.@model gdemo(x, y) = begin + s ~ InverseGamma(2, 3) + m ~ Normal(0, sqrt(s)) + x ~ Normal(m, sqrt(s)) + h ~ Normal(m,abs(x)) + y ~ Normal(m, sqrt(s)) + k~ Exponential(0.1) +end + +c1 = Turing.sample(gdemo(1.5, 2), Turing.PG(10), 10) +c2 = Turing.sample(gdemo(1.5, 2), Turing.SMC(), 10) From d47d664fe4264ae99ea2f17f687d847826b6a9fe Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Mon, 25 Nov 2019 19:29:25 +0000 Subject: [PATCH 05/25] Ancestor Sampling concept Ancestor Sampling concept --- demonstarte.jl | 16 +++++-- src/resample.jl | 9 ---- src/samplers.jl | 114 ++++++++++++++++++++++++++++++++++++++++++-- src/taskinfo.jl | 24 ++++++++++ src/tasks.jl | 10 ++-- test_with_turing.jl | 23 +++++++-- 6 files changed, 170 insertions(+), 26 deletions(-) diff --git a/demonstarte.jl b/demonstarte.jl index ec0a2422..bc954360 100644 --- a/demonstarte.jl +++ b/demonstarte.jl @@ -3,11 +3,13 @@ using AdvancedPS using Libtask const APS = AdvancedPS using Distributions - +using CuArrays n = 20 + + ## Our states vi = Container(zeros(n),0) ## Our observations @@ -27,16 +29,21 @@ function task_f() var = init() set_x(var,1,rand(Normal())) # We sample - + arr = cu(rand(1000,1000)) + arr2 = cu(rand(1000,1)) + arr3 = arr*arr2 for i = 2:n # Sampling set_x(var,i, rand(Normal(get_x(var,i-1)-1,0.8))) # We sample from proposal logγ = logpdf(Normal(get_x(var,i-1)-1,0.8),get_x(var,i)) #γ(x_t|x_t-1) logp = logpdf(Normal(),get_x(var,i)) # p(x_t|x_t-1) + + report_transition!(var,logp,logγ) #Proposal and Resampling logpy = logpdf(Normal(get_x(var,i),0.4),y[i-1]) + var = report_observation!(var,logpy) end @@ -46,8 +53,11 @@ end m = 10 task = create_task(task_f) +task = + APS.extend!(particles, 10, vi, task, PGTaskInfo(0.0,0.0)) ## Do one SMC step. APS.sampleSMC!(particles) -particles[1] +particles +delete!(particles.vals[1].task.storage) diff --git a/src/resample.jl b/src/resample.jl index 288f9f0d..0ebf5ab2 100644 --- a/src/resample.jl +++ b/src/resample.jl @@ -43,15 +43,6 @@ function resample!( @inbounds children[n] = ref elseif new_ref !== nothing @inbounds children[n] = new_ref - else - @assert isa(Particle,new_ancestor_traj) "[AdvancedPS] ($new_ancestor_traj) must be of type particle" - try - @inbounds chosen_traj = fork(particle[ancestor_idx], pc.manipulators["copy"]) - new_ancestor_traj = pc.manipulators["merge_traj"](chosen_traj,ref) - catch e - error("[Advanced PS] Ancestor sampling went wrong...") - end - @inbounds children[n] = new_ancestor_traj end end diff --git a/src/samplers.jl b/src/samplers.jl index e406cfb9..0f3ff319 100644 --- a/src/samplers.jl +++ b/src/samplers.jl @@ -19,12 +19,12 @@ function sampleSMC!(pc::ParticleContainer,resampler::Function =resample_systemat end end -function samplePG!(pc::ParticleContainer,resampler::Function = resample_systematic ,ref_particle::Union{Particle,Nothing}=nothing , resampler_threshold =1) +# The resampler threshold is only imprtant for the first step! +function samplePG!(pc::ParticleContainer,resampler::Function = resample_systematic ,ref_particle::Union{Particle,Nothing}=nothing , resampler_threshold =0.5) if ref_particle === nothing - #Do normal SMC - #By defualt, resampler_threshold is set to one, so that we always resample - sampleSMC!(pc,resampler_threshold) + # We do not have a reference trajectory yet, therefore, perform normal SMC! + sampleSMC!(pc,resampler,resampler_threshold) else while consume(pc) != Val{:done} # compute weights @@ -44,3 +44,109 @@ function samplePG!(pc::ParticleContainer,resampler::Function = resample_systemat end end + + +# The resampler threshold is only imprtant for the first step! +function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_systematic ,ref_particle::Union{Particle,Nothing}=nothing ,joint_logp::Union{Tuple{Vector{Symbols},Function},Nothing}=nothing, resampler_threshold =0.5) + + if ref_particle === nothing + # We do not have a reference trajectory yet, therefore, perform normal SMC! + sampleSMC!(pc,resamplerresampler_threshold) + else + # Before starting, we need to copute the ancestor weights. + # Note that there is a inconsistency with the original paper + # http://jmlr.org/papers/volume15/lindsten14a/lindsten14a.pdf + # Lindsten samples already for x1. This is not possible in + # in this situation because we do not have access to the information + # which variables belong to x1 and which to x0! + + # The procedure works as follows. The ancestor weights are only dependent on + # the states x_{0:t-1}. We make us of this by computing the ancestor indices for + # the next state. + ancestor_index = length(pc) + ancestor_particle::Union{typeof(pc[1]), Nothing} = nothing + + num_totla_consume = typemax(Int64) + first_round = true + while consume(pc) != Val{:done} + # compute weights + Ws = weights(pc) + # We need them for ancestor sampling... + logws = copy(pc.logWs) + logpseq = [for i in 1:n pc[i].taskinfo.logpseq] + # check that weights are not NaN + @assert !any(isnan, Ws) + # sample ancestor indices + n = length(pc) + # Ancestor trajectory is not sampled + nresamples = n-1 + indx = resampler(Ws, nresamples) + + # Now the ancestor sampling starts. We do not use ancestor sampling in the + # first step. This is due to the reason before. In addition, we do not need + # to compute the ancestor index for the last step, because we are always + #computing the ancestor index one step ahead. + if ancestor_particle === nothing + push!(indx,n) + resample!(pc, indx,ref_particle) + # In this case, we do have access to the joint_logp ! Therefore: + if joint_logp !== nothing + # We need to create a dictionary with symbol value paris, which we pass + # to the joint_logp function. + @assert @inbounds isa(joint_logp[1], Vector{Symbols}) "[AdvancedPS] the first argument of the joint_logp tuble must be a vector of Symbols!" + @assert @inbounds isa(joint_logp[2], Function) "[AdvancedPS] the second argument of the joint_logp tuble must be a function of the for"* + " f(num_produce, args...), which returns a value of Float64!" + + # We are executing the joint_logp function as a set of tasks. The idea behind + # this is that we might extend the task to include parallelization. + tasks = [] + for i = 1:n + args = pc.manipulators["get_AS_joint_logp_args"](joint_logp[1],pc[i],ref_particle) + task = CTask( () -> begin result=joint_logp[2](pc.n_consume,args...); produce(result); end ) + push(tasks,task) + schedule(task) + yield() + end + for (i,t) in enumerate(tasks) + logws[i] += consume(t) -logpseq[i] # The ancestor weights w_ancstor = w_i *p(x_{0:t-1},x_{t:T})/p(x_{0:t-1}) + end + + ancestor_index = randcat(softmax!(logws)) + # We are one step behind.... + selected_path = pc[ancestor_index] + new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](selected_path),ref_particle,pc.n_consume +1) + ancestor_particle = Trace{typeof(new_vi),typeof(selected_path.taskinfo)}(new_vi,copy(selected_path.task),copy(selected_path.taskinfo)) + else + if pc.n_consume <= num_totla_consume-1 #We do not need to sample the last one... + # The idea is rather simple, we extend the vs and let them run trough... + pc_ancestor = ParticleContainer{typeof(pc[1].vi),typeof(pc[1].taskinfo)}() + pc_ancestor.n_consume = pc.n_consume + for i = 1:n + new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](pc[i]),ref_particle) + new_particle = Trace{typeof(new_vi),typeof(selected_path.taskinfo)}(new_vi,copy(pc[i].task),copy(pc[i].taskinfo)) + push!(pc_ancestor,new_particle) + end + + while consume(pc_ancestor) != Val{:done} end # No resampling, we just want to get log p(x_{0:t-1},x'_{t,T}) + for i in 1:n + logws[i] += pc_ancestor[i].taskinfo.logpseq -logpseq[i] # The ancestor weights w_ancstor = w_i *p(x_{0:t-1},x_{t:T})/p(x_{0:t-1}) + end + + + ancestor_index = randcat(softmax!(logws)) + # We are one step behind.... + selected_path = pc[ancestor_index] + new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](selected_path),ref_particle,pc.n_consume +1) + ancestor_particle = Trace{typeof(new_vi),typeof(selected_path.taskinfo)}(new_vi,copy(selected_path.task),copy(selected_path.taskinfo)) + num_total_consume = pc_ancestor.n_consume + end + end + end + # Reactivate the tasks, this is only important for synchorinty. + for i =1:n + @inbounds consume(pc[i].task) + end + end + end + +end diff --git a/src/taskinfo.jl b/src/taskinfo.jl index 5ca6a89e..8ec7ade7 100644 --- a/src/taskinfo.jl +++ b/src/taskinfo.jl @@ -20,3 +20,27 @@ end function copy(info::PGTaskInfo) PGTaskInfo(info.logp, info.logpseq) end + + +mutable struct PGASTaskInfo <: AbstractTaskInfo + # This corresponds to p(y_t | x_t)*p(x_t|x_t-1)/ γ(x_t|x_t-1,y_t), + # where γ is the porposal. + # We need this variable to compute the weights! + logp::Float64 + + # This corresponds to p(x_t|x_t-1)*p(x_t-1|x_t-2)*... *p(x_0) + # or |x_{0:t-1} for non markovian models, we need this to compute + # the ancestor weights. + + logpseq::Float64 + + + hold::Bool +end +function PGASTaskInfo(logp::Float64,logpseq::Float64) + PGASTaskInfo(logp,logpseq,false) +end + +function copy(info::PGTaskInfo) + PGTaskInfo(info.logp, info.logpseq,hold) +end diff --git a/src/tasks.jl b/src/tasks.jl index c68d52bc..fb99966b 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -112,6 +112,7 @@ Base.similar(pc :: ParticleContainer{T}) where T = ParticleContainer{T}(0) Base.getindex(pc :: ParticleContainer, i :: Real) = pc.vals[i] + # registers a new x-particle in the container function Base.push!(pc::ParticleContainer, p::Particle) push!(pc.vals, p) @@ -141,7 +142,6 @@ function extend!(pc::ParticleContainer, n::Int, varInfo, tasks::Task, taskInfo:: pc end - # clears the container but keep params, logweight etc. function Base.empty!(pc::ParticleContainer) pc.vals = eltype(pc.vals)[] @@ -230,9 +230,5 @@ set_logpseg(pc :: ParticleContainer, t :: Int, logp :: Float64) = increase_logevidence(pc :: ParticleContainer, logw :: Float64) = (pc.logE += logw) - - ### - - ### Resample Steps - - ### +function set_hold(pc::ParticleContainer{Trace{T,W}}) where {T,W<:PGASTaskInfo} + for diff --git a/test_with_turing.jl b/test_with_turing.jl index 958ec898..577c9f12 100644 --- a/test_with_turing.jl +++ b/test_with_turing.jl @@ -1,15 +1,32 @@ include("Turing.jl/src/Turing.jl") using Distributions +using CuArrays +## It works even for GPU's! +cache = Dict() Turing.@model gdemo(x, y) = begin s ~ InverseGamma(2, 3) m ~ Normal(0, sqrt(s)) - x ~ Normal(m, sqrt(s)) - h ~ Normal(m,abs(x)) + if :x in spl.space + + @cond_compute cache (:x,:s,:m,:h) begin + arr = cu(rand(1000,1000)) + arr2 = cu(rand(1000,1)) + cache[:arr] = arr*arr2 + arr = cache[:arr] + + x ~ Normal(m, sqrt(s)) + h ~ Normal(m,abs(x)) + end + + + :x in spl.space ? ... : cache[:x] + y ~ Normal(m, sqrt(s)) + k~ Exponential(0.1) end -c1 = Turing.sample(gdemo(1.5, 2), Turing.PG(10), 10) +c1 = Turing.sample(gdemo(1.5, 2), Turing.PG(100), 1000) c2 = Turing.sample(gdemo(1.5, 2), Turing.SMC(), 10) From f15ae0a5af8b6163d33a1510732a98ee22445844 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Tue, 26 Nov 2019 12:50:31 +0000 Subject: [PATCH 06/25] Work on PGAS --- demonstarte.jl | 63 +++++++++++++++++++++++++++++++++++---------- src/AdvancedPS.jl | 4 +-- src/resample.jl | 2 +- src/samplers.jl | 10 +++---- src/taskinfo.jl | 4 +-- src/tasks.jl | 4 +-- test_with_turing.jl | 18 +++---------- 7 files changed, 65 insertions(+), 40 deletions(-) diff --git a/demonstarte.jl b/demonstarte.jl index bc954360..11bf468d 100644 --- a/demonstarte.jl +++ b/demonstarte.jl @@ -3,7 +3,6 @@ using AdvancedPS using Libtask const APS = AdvancedPS using Distributions -using CuArrays n = 20 @@ -17,8 +16,6 @@ y = Vector{Float64}(undef,n) for i = 1:n-1 y[i] = -i end -## The particle container -particles = APS.ParticleContainer{typeof(vi),APS.PGTaskInfo }() # Define a short model. # The syntax is rather simple. Observations need to be reported with report_observation. @@ -27,11 +24,8 @@ particles = APS.ParticleContainer{typeof(vi),APS.PGTaskInfo }() # Thats all! function task_f() var = init() - set_x(var,1,rand(Normal())) # We sample - arr = cu(rand(1000,1000)) - arr2 = cu(rand(1000,1)) - arr3 = arr*arr2 + report_transition(var,0.0,0.0) for i = 2:n # Sampling set_x(var,i, rand(Normal(get_x(var,i-1)-1,0.8))) # We sample from proposal @@ -39,25 +33,68 @@ function task_f() logp = logpdf(Normal(),get_x(var,i)) # p(x_t|x_t-1) - report_transition!(var,logp,logγ) + report_transition(var,logp,logγ) #Proposal and Resampling logpy = logpdf(Normal(get_x(var,i),0.4),y[i-1]) - var = report_observation!(var,logpy) + var = report_observation(var,logpy) end + end + +particles = APS.ParticleContainer{typeof(vi),APS.PGTaskInfo }() + + m = 10 task = create_task(task_f) -task = APS.extend!(particles, 10, vi, task, PGTaskInfo(0.0,0.0)) ## Do one SMC step. -APS.sampleSMC!(particles) +APS.samplePG!(particles) + +particles2 = APS.ParticleContainer{typeof(vi),APS.PGTaskInfo }() + + +m = 10 +task = create_task(task_f) + + +APS.extend!(particles2, 9, vi, task, PGTaskInfo(0.0,0.0,true)) +APS.extend!(particles2, 1, particles[1].vi, task, PGASTaskInfo(0.0,0.0,true)) + +## Do one SMC step. +APS.samplePGAS!(particles2,resample_systematic,particles2[1]) + + +asdf -particles -delete!(particles.vals[1].task.storage) +function task_f_prod() + var = init() + + set_x(var,1,rand(Normal())) # We sample + arr = cu(rand(1000,1000)) + arr2 = cu(rand(1000,1)) + arr3 = arr*arr2 + for i = 2:n + # Sampling + set_x(var,i, rand(Normal(get_x(var,i-1)-1,0.8))) # We sample from proposal + logγ = logpdf(Normal(get_x(var,i-1)-1,0.8),get_x(var,i)) #γ(x_t|x_t-1) + logp = logpdf(Normal(),get_x(var,i)) # p(x_t|x_t-1) + + + report_transition(var,logp,logγ) + + #Proposal and Resampling + logpy = logpdf(Normal(get_x(var,i),0.4),y[i-1]) + + var = report_observation(var,logpy) + if var.taskinfo.hold + produce(0.0) + + end +end diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index fc45f4bc..3048f0f8 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -53,8 +53,8 @@ module AdvancedPS include("Interface.jl") - export report_transition!, - report_observation!, + export report_transition, + report_observation, init, Container, set_x, diff --git a/src/resample.jl b/src/resample.jl index 0ebf5ab2..a48bea2a 100644 --- a/src/resample.jl +++ b/src/resample.jl @@ -35,7 +35,7 @@ function resample!( end if ref !== nothing - @inbounds ancestor_idx = indx[n] + ancestor_idx = indx[n] # Insert the retained particle. This is based on the replaying trick for efficiency # reasons. If we implement PG using task copying, we need to store Nx * T particles! # This is a rather effcient way of how to solve the ancestor problem. diff --git a/src/samplers.jl b/src/samplers.jl index 0f3ff319..452aaff7 100644 --- a/src/samplers.jl +++ b/src/samplers.jl @@ -47,7 +47,7 @@ end # The resampler threshold is only imprtant for the first step! -function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_systematic ,ref_particle::Union{Particle,Nothing}=nothing ,joint_logp::Union{Tuple{Vector{Symbols},Function},Nothing}=nothing, resampler_threshold =0.5) +function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_systematic ,ref_particle::Union{Particle,Nothing}=nothing ,joint_logp::Union{Tuple{Vector{Symbol},Function},Nothing}=nothing, resampler_threshold =0.5) if ref_particle === nothing # We do not have a reference trajectory yet, therefore, perform normal SMC! @@ -73,7 +73,7 @@ function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_system Ws = weights(pc) # We need them for ancestor sampling... logws = copy(pc.logWs) - logpseq = [for i in 1:n pc[i].taskinfo.logpseq] + logpseq = [pc[i].taskinfo.logpseq for i in 1:n ] # check that weights are not NaN @assert !any(isnan, Ws) # sample ancestor indices @@ -93,8 +93,8 @@ function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_system if joint_logp !== nothing # We need to create a dictionary with symbol value paris, which we pass # to the joint_logp function. - @assert @inbounds isa(joint_logp[1], Vector{Symbols}) "[AdvancedPS] the first argument of the joint_logp tuble must be a vector of Symbols!" - @assert @inbounds isa(joint_logp[2], Function) "[AdvancedPS] the second argument of the joint_logp tuble must be a function of the for"* + @assert isa(joint_logp[1], Vector{Symbols}) "[AdvancedPS] the first argument of the joint_logp tuble must be a vector of Symbols!" + @assert isa(joint_logp[2], Function) "[AdvancedPS] the second argument of the joint_logp tuble must be a function of the for"* " f(num_produce, args...), which returns a value of Float64!" # We are executing the joint_logp function as a set of tasks. The idea behind @@ -144,7 +144,7 @@ function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_system end # Reactivate the tasks, this is only important for synchorinty. for i =1:n - @inbounds consume(pc[i].task) + consume(pc[i].task) end end end diff --git a/src/taskinfo.jl b/src/taskinfo.jl index 8ec7ade7..a8d1670a 100644 --- a/src/taskinfo.jl +++ b/src/taskinfo.jl @@ -41,6 +41,6 @@ function PGASTaskInfo(logp::Float64,logpseq::Float64) PGASTaskInfo(logp,logpseq,false) end -function copy(info::PGTaskInfo) - PGTaskInfo(info.logp, info.logpseq,hold) +function copy(info::PGASTaskInfo) + PGASTaskInfo(info.logp, info.logpseq,info.hold) end diff --git a/src/tasks.jl b/src/tasks.jl index fb99966b..fa6b2afd 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -224,11 +224,9 @@ increase_logweight(pc :: ParticleContainer, t :: Int, logw :: Float64) = (pc.logWs[t] += logw) + set_logpseg(pc :: ParticleContainer, t :: Int, logp :: Float64) = (pc.logpseq[t] = logp) increase_logevidence(pc :: ParticleContainer, logw :: Float64) = (pc.logE += logw) - -function set_hold(pc::ParticleContainer{Trace{T,W}}) where {T,W<:PGASTaskInfo} - for diff --git a/test_with_turing.jl b/test_with_turing.jl index 577c9f12..8eb886ad 100644 --- a/test_with_turing.jl +++ b/test_with_turing.jl @@ -1,6 +1,5 @@ include("Turing.jl/src/Turing.jl") using Distributions -using CuArrays ## It works even for GPU's! cache = Dict() @@ -8,25 +7,16 @@ cache = Dict() Turing.@model gdemo(x, y) = begin s ~ InverseGamma(2, 3) m ~ Normal(0, sqrt(s)) - if :x in spl.space - @cond_compute cache (:x,:s,:m,:h) begin - arr = cu(rand(1000,1000)) - arr2 = cu(rand(1000,1)) - cache[:arr] = arr*arr2 - arr = cache[:arr] - x ~ Normal(m, sqrt(s)) - h ~ Normal(m,abs(x)) - end - - - :x in spl.space ? ... : cache[:x] + x ~ Normal(m, sqrt(s)) + h ~ Normal(m,abs(x)) y ~ Normal(m, sqrt(s)) k~ Exponential(0.1) end -c1 = Turing.sample(gdemo(1.5, 2), Turing.PG(100), 1000) +c1 = Turing.sample(gdemo(1.5, 2), Turing.PG(10), 10) c2 = Turing.sample(gdemo(1.5, 2), Turing.SMC(), 10) +c3 = Turing.sample(gdemo(1.5,2), Turing.PGAS(10),10) From 1c718a79ded9b47b00a920eabb9c037544653423 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Tue, 26 Nov 2019 17:19:12 +0000 Subject: [PATCH 07/25] PGAS running! --- demonstarte.jl | 14 ++++++--- demonstartePGAS.jl | 77 +++++++++++++++++++++++++++++++++++++++++++++ src/AdvancedPS.jl | 3 +- src/samplers.jl | 15 +++++---- src/tasks.jl | 3 +- test_with_turing.jl | 40 ++++++++++++++++++++--- 6 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 demonstartePGAS.jl diff --git a/demonstarte.jl b/demonstarte.jl index 11bf468d..2c78605b 100644 --- a/demonstarte.jl +++ b/demonstarte.jl @@ -64,15 +64,21 @@ m = 10 task = create_task(task_f) -APS.extend!(particles2, 9, vi, task, PGTaskInfo(0.0,0.0,true)) -APS.extend!(particles2, 1, particles[1].vi, task, PGASTaskInfo(0.0,0.0,true)) +APS.extend!(particles2, 9, vi, task, PGTaskInfo(0.0,0.0)) +APS.extend!(particles2, 1, particles[1].vi, task, PGTaskInfo(0.0,0.0)) ## Do one SMC step. -APS.samplePGAS!(particles2,resample_systematic,particles2[1]) +APS.samplePG!(particles2,resample_systematic,particles2[m]) +particles2 -asdf + + + + + +a function task_f_prod() var = init() diff --git a/demonstartePGAS.jl b/demonstartePGAS.jl new file mode 100644 index 00000000..a4d88162 --- /dev/null +++ b/demonstartePGAS.jl @@ -0,0 +1,77 @@ +## It is not yet a package... +include("AdvancedPS/src/AdvancedPS.jl") +using Libtask +APS = AdvancedPS +using Distributions +n = 20 + + + + + +## Our states +vi = APS.Container(zeros(n),0) +## Our observations +y = Vector{Float64}(undef,n) +for i = 1:n-1 + y[i] = -i +end + +# Define a short model. +# The syntax is rather simple. Observations need to be reported with report_observation. +# Transitions must be reported using report_transition. +# The trace contains the variables which we want to infer using particle gibbs. +# Thats all! +function task_f() + var = APS.init() + APS.set_x(var,1,rand(Normal())) # We sample + APS.report_transition(var,0.0,0.0) + for i = 2:n + # Sampling + APS.set_x(var,i, rand(Normal(APS.get_x(var,i-1)-1,0.8))) # We sample from proposal + logγ = logpdf(Normal(APS.get_x(var,i-1)-1,0.8),APS.get_x(var,i)) #γ(x_t|x_t-1) + logp = logpdf(Normal(),APS.get_x(var,i)) # p(x_t|x_t-1) + + + APS.report_transition(var,logp,logγ) + + #Proposal and Resampling + logpy = logpdf(Normal(APS.get_x(var,i),0.4),y[i-1]) + + var = APS.report_observation(var,logpy) + if var.taskinfo.hold + produce(0.0) + end + + end + +end + + + +particles = APS.ParticleContainer{typeof(vi),APS.PGASTaskInfo }() + + +m = 10 +task = APS.create_task(task_f) + + +APS.extend!(particles, 10, vi, task, APS.PGASTaskInfo(0.0,0.0)) +## Do one SMC step. +APS.samplePGAS!(particles) + +particles2 = APS.ParticleContainer{typeof(vi),APS.PGASTaskInfo }() + + +m = 10 +task = APS.create_task(task_f) + + +APS.extend!(particles2, 9, vi, task, APS.PGASTaskInfo(0.0,0.0,true)) +APS.extend!(particles2, 1, particles[1].vi, task, APS.PGASTaskInfo(0.0,0.0,true)) + +particles2.manipulators["merge_traj"] = (x,y,i=0) -> y # Obviously this function is wrong! +## Do one SMC step. +APS.samplePGAS!(particles2,APS.resample_systematic,particles2[m]) + +particles2 diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index 3048f0f8..000dfd4a 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -35,7 +35,8 @@ module AdvancedPS include("taskinfo.jl") - export PGTaskInfo + export PGTaskInfo, + PGASTaskInfo include("samplers.jl") diff --git a/src/samplers.jl b/src/samplers.jl index 452aaff7..c20328a1 100644 --- a/src/samplers.jl +++ b/src/samplers.jl @@ -1,7 +1,6 @@ function sampleSMC!(pc::ParticleContainer,resampler::Function =resample_systematic ,resampler_threshold::AbstractFloat = 0.5) - println("Hmmm") while consume(pc) != Val{:done} ess = effectiveSampleSize(pc) @@ -48,10 +47,10 @@ end # The resampler threshold is only imprtant for the first step! function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_systematic ,ref_particle::Union{Particle,Nothing}=nothing ,joint_logp::Union{Tuple{Vector{Symbol},Function},Nothing}=nothing, resampler_threshold =0.5) - if ref_particle === nothing # We do not have a reference trajectory yet, therefore, perform normal SMC! - sampleSMC!(pc,resamplerresampler_threshold) + sampleSMC!(pc,resampler,resampler_threshold) + else # Before starting, we need to copute the ancestor weights. # Note that there is a inconsistency with the original paper @@ -65,6 +64,7 @@ function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_system # the next state. ancestor_index = length(pc) ancestor_particle::Union{typeof(pc[1]), Nothing} = nothing + n = length(pc) num_totla_consume = typemax(Int64) first_round = true @@ -77,7 +77,6 @@ function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_system # check that weights are not NaN @assert !any(isnan, Ws) # sample ancestor indices - n = length(pc) # Ancestor trajectory is not sampled nresamples = n-1 indx = resampler(Ws, nresamples) @@ -114,7 +113,7 @@ function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_system ancestor_index = randcat(softmax!(logws)) # We are one step behind.... selected_path = pc[ancestor_index] - new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](selected_path),ref_particle,pc.n_consume +1) + new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](selected_path.vi),ref_particle.vi,pc.n_consume +1) ancestor_particle = Trace{typeof(new_vi),typeof(selected_path.taskinfo)}(new_vi,copy(selected_path.task),copy(selected_path.taskinfo)) else if pc.n_consume <= num_totla_consume-1 #We do not need to sample the last one... @@ -122,8 +121,8 @@ function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_system pc_ancestor = ParticleContainer{typeof(pc[1].vi),typeof(pc[1].taskinfo)}() pc_ancestor.n_consume = pc.n_consume for i = 1:n - new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](pc[i]),ref_particle) - new_particle = Trace{typeof(new_vi),typeof(selected_path.taskinfo)}(new_vi,copy(pc[i].task),copy(pc[i].taskinfo)) + new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](pc[i].vi),ref_particle.vi) + new_particle = Trace{typeof(new_vi),typeof(pc[i].taskinfo)}(new_vi,copy(pc[i].task),copy(pc[i].taskinfo)) push!(pc_ancestor,new_particle) end @@ -136,7 +135,7 @@ function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_system ancestor_index = randcat(softmax!(logws)) # We are one step behind.... selected_path = pc[ancestor_index] - new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](selected_path),ref_particle,pc.n_consume +1) + new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](selected_path.vi),ref_particle.vi,pc.n_consume +1) ancestor_particle = Trace{typeof(new_vi),typeof(selected_path.taskinfo)}(new_vi,copy(selected_path.task),copy(selected_path.taskinfo)) num_total_consume = pc_ancestor.n_consume end diff --git a/src/tasks.jl b/src/tasks.jl index fa6b2afd..163432cb 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -114,7 +114,7 @@ Base.getindex(pc :: ParticleContainer, i :: Real) = pc.vals[i] # registers a new x-particle in the container -function Base.push!(pc::ParticleContainer, p::Particle) +function Base.push!(pc::ParticleContainer, p::Particle{Tvi,TInfo}) where {Tvi,TInfo <: AbstractTaskInfo} push!(pc.vals, p) push!(pc.logWs, 0.0) push!(pc.logpseq,0.0) @@ -188,6 +188,7 @@ function Libtask.consume(pc :: ParticleContainer) elseif score == Val{:done} num_done += 1 else + println(score) error("[consume]: error in running particle filter.") end end diff --git a/test_with_turing.jl b/test_with_turing.jl index 8eb886ad..c271fdd5 100644 --- a/test_with_turing.jl +++ b/test_with_turing.jl @@ -1,14 +1,18 @@ include("Turing.jl/src/Turing.jl") using Distributions - +using CuArrays ## It works even for GPU's! cache = Dict() +arr = rand(10,10) +arr2 = rand(10,1) Turing.@model gdemo(x, y) = begin s ~ InverseGamma(2, 3) m ~ Normal(0, sqrt(s)) + arr3 = arr*arr2 + x ~ Normal(m, sqrt(s)) h ~ Normal(m,abs(x)) @@ -17,6 +21,34 @@ Turing.@model gdemo(x, y) = begin k~ Exponential(0.1) end -c1 = Turing.sample(gdemo(1.5, 2), Turing.PG(10), 10) -c2 = Turing.sample(gdemo(1.5, 2), Turing.SMC(), 10) -c3 = Turing.sample(gdemo(1.5,2), Turing.PGAS(10),10) +# arrcu = cu(rand(10000,10000)) +# arr2cu = cu(rand(10000,1)) +# Turing.@model gdemocu(x, y) = begin +# s ~ InverseGamma(2, 3) +# m ~ Normal(0, sqrt(s)) +# +# +# arr3cu = arrcu*arr2cu +# +# x ~ Normal(m, sqrt(s)) +# h ~ Normal(m,abs(x)) +# +# y ~ Normal(m, sqrt(s)) +# +# k~ Exponential(0.1) +# +# end + +#@elapsed arr*arr2 +#@elapsed arrcu*arr2cu +#time2 = @elapsed c1 = Turing.sample(gdemocu(1.5, 2), Turing.PG(100), 100) + +#time = @elapsed c1 = Turing.sample(gdemo(1.5, 2), Turing.PG(10), 10) + +#c2 = Turing.sample(gdemo(1.5, 2), Turing.SMC(), 10) + + + +@elapsed c1 = Turing.sample(gdemo(1.5,2), Turing.PGAS(10),100) +@elapsed c2 = Turing.sample(gdemo(1.5,2), Turing.PG(10),100) +@elapsed c3 = Turing.sample(gdemo(1.5,2), Turing.SMC(),1000) From c094338f61af2872490e79b56d874bdd93c515ea Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Wed, 27 Nov 2019 16:21:57 +0000 Subject: [PATCH 08/25] Ready for pull request --- Tests/create_data_from_old_turing_model.jl | 25 +++ demonstarte.jl => Tests/demonstarte.jl | 0 .../demonstartePGAS.jl | 2 +- Tests/test_against_previous_model.jl | 27 +++ src/samplers.jl | 210 +++++++++--------- test_with_turing.jl | 28 ++- 6 files changed, 177 insertions(+), 115 deletions(-) create mode 100644 Tests/create_data_from_old_turing_model.jl rename demonstarte.jl => Tests/demonstarte.jl (100%) rename demonstartePGAS.jl => Tests/demonstartePGAS.jl (97%) create mode 100644 Tests/test_against_previous_model.jl diff --git a/Tests/create_data_from_old_turing_model.jl b/Tests/create_data_from_old_turing_model.jl new file mode 100644 index 00000000..b4b5fcd7 --- /dev/null +++ b/Tests/create_data_from_old_turing_model.jl @@ -0,0 +1,25 @@ +# Import packages. +using Turing +using Random; Random.seed!(1) +using Distributed +using DistributionsAD +# Define a simple Normal model with unknown mean and variance. +@model gdemo(y) = begin + s ~ Exponential(0.2) + m ~ Normal(0, sqrt(s)) + x = Vector{Real}(undef,10) + x[1] ~ Normal(m,s) + y[1] ~ Normal(x[1],0.5) + for i = 2:10 + x[i] ~ Normal(x[i-1],s) + y[i] ~ Normal(x[i],0.5) + end + +end + +y = Vector{Float64}(1:10) + +chn1 = sample(gdemo(y),SMC(),1000) +write("Old_Model_SMC.jls", chn1) +chn2 = sample(gdemo(y),PG(100),100) +write("Old_Model_PG.jls",chn2) diff --git a/demonstarte.jl b/Tests/demonstarte.jl similarity index 100% rename from demonstarte.jl rename to Tests/demonstarte.jl diff --git a/demonstartePGAS.jl b/Tests/demonstartePGAS.jl similarity index 97% rename from demonstartePGAS.jl rename to Tests/demonstartePGAS.jl index a4d88162..78bcfdcf 100644 --- a/demonstartePGAS.jl +++ b/Tests/demonstartePGAS.jl @@ -1,5 +1,5 @@ ## It is not yet a package... -include("AdvancedPS/src/AdvancedPS.jl") +using AdvancedPS using Libtask APS = AdvancedPS using Distributions diff --git a/Tests/test_against_previous_model.jl b/Tests/test_against_previous_model.jl new file mode 100644 index 00000000..15189d84 --- /dev/null +++ b/Tests/test_against_previous_model.jl @@ -0,0 +1,27 @@ +# Import packages. + +using Turing +using Random; Random.seed!(1) +using Distributed +using DistributionsAD +# Define a simple Normal model with unknown mean and variance. +@Turing.model gdemo(y) = begin + s ~ Exponential(0.2) + m ~ Normal(0, sqrt(s)) + x = Vector{Real}(undef,10) + x[1] ~ Normal(m,s) + y[1] ~ Normal(x[1],0.5) + for i = 2:10 + x[i] ~ Normal(x[i-1],s) + y[i] ~ Normal(x[i],0.5) + end + +end + +y = Vector{Float64}(1:10) + +chn1 = Turing.sample(gdemo(y),Turing.SMC(),1000) +chn2 = Turing.sample(gdemo(y),Turing.PG(100),100) + +chn1_old = read("Old_Model_SMC.jls", Chains) +chn2_old = read("Old_Model_PG.jls", Chains) diff --git a/src/samplers.jl b/src/samplers.jl index c20328a1..fb3f3edc 100644 --- a/src/samplers.jl +++ b/src/samplers.jl @@ -44,108 +44,108 @@ function samplePG!(pc::ParticleContainer,resampler::Function = resample_systemat end - -# The resampler threshold is only imprtant for the first step! -function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_systematic ,ref_particle::Union{Particle,Nothing}=nothing ,joint_logp::Union{Tuple{Vector{Symbol},Function},Nothing}=nothing, resampler_threshold =0.5) - if ref_particle === nothing - # We do not have a reference trajectory yet, therefore, perform normal SMC! - sampleSMC!(pc,resampler,resampler_threshold) - - else - # Before starting, we need to copute the ancestor weights. - # Note that there is a inconsistency with the original paper - # http://jmlr.org/papers/volume15/lindsten14a/lindsten14a.pdf - # Lindsten samples already for x1. This is not possible in - # in this situation because we do not have access to the information - # which variables belong to x1 and which to x0! - - # The procedure works as follows. The ancestor weights are only dependent on - # the states x_{0:t-1}. We make us of this by computing the ancestor indices for - # the next state. - ancestor_index = length(pc) - ancestor_particle::Union{typeof(pc[1]), Nothing} = nothing - n = length(pc) - - num_totla_consume = typemax(Int64) - first_round = true - while consume(pc) != Val{:done} - # compute weights - Ws = weights(pc) - # We need them for ancestor sampling... - logws = copy(pc.logWs) - logpseq = [pc[i].taskinfo.logpseq for i in 1:n ] - # check that weights are not NaN - @assert !any(isnan, Ws) - # sample ancestor indices - # Ancestor trajectory is not sampled - nresamples = n-1 - indx = resampler(Ws, nresamples) - - # Now the ancestor sampling starts. We do not use ancestor sampling in the - # first step. This is due to the reason before. In addition, we do not need - # to compute the ancestor index for the last step, because we are always - #computing the ancestor index one step ahead. - if ancestor_particle === nothing - push!(indx,n) - resample!(pc, indx,ref_particle) - # In this case, we do have access to the joint_logp ! Therefore: - if joint_logp !== nothing - # We need to create a dictionary with symbol value paris, which we pass - # to the joint_logp function. - @assert isa(joint_logp[1], Vector{Symbols}) "[AdvancedPS] the first argument of the joint_logp tuble must be a vector of Symbols!" - @assert isa(joint_logp[2], Function) "[AdvancedPS] the second argument of the joint_logp tuble must be a function of the for"* - " f(num_produce, args...), which returns a value of Float64!" - - # We are executing the joint_logp function as a set of tasks. The idea behind - # this is that we might extend the task to include parallelization. - tasks = [] - for i = 1:n - args = pc.manipulators["get_AS_joint_logp_args"](joint_logp[1],pc[i],ref_particle) - task = CTask( () -> begin result=joint_logp[2](pc.n_consume,args...); produce(result); end ) - push(tasks,task) - schedule(task) - yield() - end - for (i,t) in enumerate(tasks) - logws[i] += consume(t) -logpseq[i] # The ancestor weights w_ancstor = w_i *p(x_{0:t-1},x_{t:T})/p(x_{0:t-1}) - end - - ancestor_index = randcat(softmax!(logws)) - # We are one step behind.... - selected_path = pc[ancestor_index] - new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](selected_path.vi),ref_particle.vi,pc.n_consume +1) - ancestor_particle = Trace{typeof(new_vi),typeof(selected_path.taskinfo)}(new_vi,copy(selected_path.task),copy(selected_path.taskinfo)) - else - if pc.n_consume <= num_totla_consume-1 #We do not need to sample the last one... - # The idea is rather simple, we extend the vs and let them run trough... - pc_ancestor = ParticleContainer{typeof(pc[1].vi),typeof(pc[1].taskinfo)}() - pc_ancestor.n_consume = pc.n_consume - for i = 1:n - new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](pc[i].vi),ref_particle.vi) - new_particle = Trace{typeof(new_vi),typeof(pc[i].taskinfo)}(new_vi,copy(pc[i].task),copy(pc[i].taskinfo)) - push!(pc_ancestor,new_particle) - end - - while consume(pc_ancestor) != Val{:done} end # No resampling, we just want to get log p(x_{0:t-1},x'_{t,T}) - for i in 1:n - logws[i] += pc_ancestor[i].taskinfo.logpseq -logpseq[i] # The ancestor weights w_ancstor = w_i *p(x_{0:t-1},x_{t:T})/p(x_{0:t-1}) - end - - - ancestor_index = randcat(softmax!(logws)) - # We are one step behind.... - selected_path = pc[ancestor_index] - new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](selected_path.vi),ref_particle.vi,pc.n_consume +1) - ancestor_particle = Trace{typeof(new_vi),typeof(selected_path.taskinfo)}(new_vi,copy(selected_path.task),copy(selected_path.taskinfo)) - num_total_consume = pc_ancestor.n_consume - end - end - end - # Reactivate the tasks, this is only important for synchorinty. - for i =1:n - consume(pc[i].task) - end - end - end - -end +# +# # The resampler threshold is only imprtant for the first step! +# function samplePGAS!(pc::ParticleContainer,resampler::Function = resample_systematic ,ref_particle::Union{Particle,Nothing}=nothing ,joint_logp::Union{Tuple{Vector{Symbol},Function},Nothing}=nothing, resampler_threshold =0.5) +# if ref_particle === nothing +# # We do not have a reference trajectory yet, therefore, perform normal SMC! +# sampleSMC!(pc,resampler,resampler_threshold) +# +# else +# # Before starting, we need to copute the ancestor weights. +# # Note that there is a inconsistency with the original paper +# # http://jmlr.org/papers/volume15/lindsten14a/lindsten14a.pdf +# # Lindsten samples already for x1. This is not possible in +# # in this situation because we do not have access to the information +# # which variables belong to x1 and which to x0! +# +# # The procedure works as follows. The ancestor weights are only dependent on +# # the states x_{0:t-1}. We make us of this by computing the ancestor indices for +# # the next state. +# ancestor_index = length(pc) +# ancestor_particle::Union{typeof(pc[1]), Nothing} = nothing +# n = length(pc) +# +# num_totla_consume = typemax(Int64) +# first_round = true +# while consume(pc) != Val{:done} +# # compute weights +# Ws = weights(pc) +# # We need them for ancestor sampling... +# logws = copy(pc.logWs) +# logpseq = [pc[i].taskinfo.logpseq for i in 1:n ] +# # check that weights are not NaN +# @assert !any(isnan, Ws) +# # sample ancestor indices +# # Ancestor trajectory is not sampled +# nresamples = n-1 +# indx = resampler(Ws, nresamples) +# +# # Now the ancestor sampling starts. We do not use ancestor sampling in the +# # first step. This is due to the reason before. In addition, we do not need +# # to compute the ancestor index for the last step, because we are always +# #computing the ancestor index one step ahead. +# if ancestor_particle === nothing +# push!(indx,n) +# resample!(pc, indx,ref_particle) +# # In this case, we do have access to the joint_logp ! Therefore: +# if joint_logp !== nothing +# # We need to create a dictionary with symbol value paris, which we pass +# # to the joint_logp function. +# @assert isa(joint_logp[1], Vector{Symbols}) "[AdvancedPS] the first argument of the joint_logp tuble must be a vector of Symbols!" +# @assert isa(joint_logp[2], Function) "[AdvancedPS] the second argument of the joint_logp tuble must be a function of the for"* +# " f(num_produce, args...), which returns a value of Float64!" +# +# # We are executing the joint_logp function as a set of tasks. The idea behind +# # this is that we might extend the task to include parallelization. +# tasks = [] +# for i = 1:n +# args = pc.manipulators["get_AS_joint_logp_args"](joint_logp[1],pc[i],ref_particle) +# task = CTask( () -> begin result=joint_logp[2](pc.n_consume,args...); produce(result); end ) +# push(tasks,task) +# schedule(task) +# yield() +# end +# for (i,t) in enumerate(tasks) +# logws[i] += consume(t) -logpseq[i] # The ancestor weights w_ancstor = w_i *p(x_{0:t-1},x_{t:T})/p(x_{0:t-1}) +# end +# +# ancestor_index = randcat(softmax!(logws)) +# # We are one step behind.... +# selected_path = pc[ancestor_index] +# new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](selected_path.vi),ref_particle.vi,pc.n_consume +1) +# ancestor_particle = Trace{typeof(new_vi),typeof(selected_path.taskinfo)}(new_vi,copy(selected_path.task),copy(selected_path.taskinfo)) +# else +# if pc.n_consume <= num_totla_consume-1 #We do not need to sample the last one... +# # The idea is rather simple, we extend the vs and let them run trough... +# pc_ancestor = ParticleContainer{typeof(pc[1].vi),typeof(pc[1].taskinfo)}() +# pc_ancestor.n_consume = pc.n_consume +# for i = 1:n +# new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](pc[i].vi),ref_particle.vi) +# new_particle = Trace{typeof(new_vi),typeof(pc[i].taskinfo)}(new_vi,copy(pc[i].task),copy(pc[i].taskinfo)) +# push!(pc_ancestor,new_particle) +# end +# +# while consume(pc_ancestor) != Val{:done} end # No resampling, we just want to get log p(x_{0:t-1},x'_{t,T}) +# for i in 1:n +# logws[i] += pc_ancestor[i].taskinfo.logpseq -logpseq[i] # The ancestor weights w_ancstor = w_i *p(x_{0:t-1},x_{t:T})/p(x_{0:t-1}) +# end +# +# +# ancestor_index = randcat(softmax!(logws)) +# # We are one step behind.... +# selected_path = pc[ancestor_index] +# new_vi = pc.manipulators["merge_traj"](pc.manipulators["copy"](selected_path.vi),ref_particle.vi,pc.n_consume +1) +# ancestor_particle = Trace{typeof(new_vi),typeof(selected_path.taskinfo)}(new_vi,copy(selected_path.task),copy(selected_path.taskinfo)) +# num_total_consume = pc_ancestor.n_consume +# end +# end +# end +# # Reactivate the tasks, this is only important for synchorinty. +# for i =1:n +# consume(pc[i].task) +# end +# end +# end +# +# end diff --git a/test_with_turing.jl b/test_with_turing.jl index c271fdd5..e5d8be57 100644 --- a/test_with_turing.jl +++ b/test_with_turing.jl @@ -1,4 +1,5 @@ -include("Turing.jl/src/Turing.jl") +# We need to have the ParticleGibbsExtension branch!!! +using Turing using Distributions using CuArrays ## It works even for GPU's! @@ -6,17 +7,20 @@ cache = Dict() arr = rand(10,10) arr2 = rand(10,1) -Turing.@model gdemo(x, y) = begin +Turing.@model gdemo(x1,x2,x3,x4) = begin s ~ InverseGamma(2, 3) m ~ Normal(0, sqrt(s)) arr3 = arr*arr2 - x ~ Normal(m, sqrt(s)) - h ~ Normal(m,abs(x)) - - y ~ Normal(m, sqrt(s)) + x1 ~ Normal(m, abs(s)) + h1 ~ Normal(m,abs(s)) + x2 ~ Normal(m, abs(h1)) + h2 ~ Normal(m,abs(h1)) + x3 ~ Normal(m, abs(h2)) + h3 ~ Normal(m,abs(h2)) + x4 ~ Normal(m, abs(h3)) k~ Exponential(0.1) end @@ -49,6 +53,12 @@ end -@elapsed c1 = Turing.sample(gdemo(1.5,2), Turing.PGAS(10),100) -@elapsed c2 = Turing.sample(gdemo(1.5,2), Turing.PG(10),100) -@elapsed c3 = Turing.sample(gdemo(1.5,2), Turing.SMC(),1000) +@elapsed c1 = Turing.sample(gdemo([1.5,2,2,2]...), Turing.PGAS(100),1000) +@elapsed c2 = Turing.sample(gdemo([1.5,2,2,2]...), Turing.PG(100),1000) +@elapsed c3 = Turing.sample(gdemo([1.5,2,2,2]...), Turing.SMC(),1000) +@elapsed c4 = Turing.sample(gdemo([1.5,2,2,2]...), Turing.MH(),100000) + +c2 +c1 +c3 +c4 From 2f13ae232ac702ca6afb5573072a4fdf8e5f6a45 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Wed, 27 Nov 2019 21:33:48 +0000 Subject: [PATCH 09/25] Update src/tasks.jl Co-Authored-By: David Widmann --- src/tasks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks.jl b/src/tasks.jl index 163432cb..e43e30a8 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -109,7 +109,7 @@ Base.collect(pc :: ParticleContainer) = pc.vals # prev: Dict, now: Array Base.length(pc :: ParticleContainer) = length(pc.vals) Base.similar(pc :: ParticleContainer{T}) where T = ParticleContainer{T}(0) # pc[i] returns the i'th particle -Base.getindex(pc :: ParticleContainer, i :: Real) = pc.vals[i] +Base.@propagate_inbounds Base.getindex(pc::ParticleContainer, i::Int) = pc.vals[i] From 9e266f0fca4120cf30df3886f736b83ba9413570 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Wed, 27 Nov 2019 21:36:39 +0000 Subject: [PATCH 10/25] Update src/tasks.jl Co-Authored-By: David Widmann --- src/tasks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks.jl b/src/tasks.jl index e43e30a8..0dea7faf 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -114,7 +114,7 @@ Base.@propagate_inbounds Base.getindex(pc::ParticleContainer, i::Int) = pc.vals[ # registers a new x-particle in the container -function Base.push!(pc::ParticleContainer, p::Particle{Tvi,TInfo}) where {Tvi,TInfo <: AbstractTaskInfo} +function Base.push!(pc::ParticleContainer, p::Particle) push!(pc.vals, p) push!(pc.logWs, 0.0) push!(pc.logpseq,0.0) From f242e32df7c2a14fd9c82c591d7e881d99ebee84 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Wed, 27 Nov 2019 21:50:07 +0000 Subject: [PATCH 11/25] Update src/tasks.jl Co-Authored-By: David Widmann --- src/tasks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks.jl b/src/tasks.jl index 0dea7faf..53700d06 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -174,7 +174,7 @@ function Libtask.consume(pc :: ParticleContainer) p = particles[i] score = Libtask.consume(p) - if in(:logpseq,fieldnames(typeof(p.taskinfo))) && isa(p.taskinfo.logpseq,Float64) + if hasproperty(typeof(p.taskinfo), logpseq) set_logpseg(pc,i,p.taskinfo.logpseq) end From 8e70b5b28fa5be0ca2f5e9d53be862e7404bf5f4 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Wed, 27 Nov 2019 21:50:39 +0000 Subject: [PATCH 12/25] Update src/tasks.jl Co-Authored-By: David Widmann --- src/tasks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks.jl b/src/tasks.jl index 53700d06..460f2f42 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -175,7 +175,7 @@ function Libtask.consume(pc :: ParticleContainer) score = Libtask.consume(p) if hasproperty(typeof(p.taskinfo), logpseq) - set_logpseg(pc,i,p.taskinfo.logpseq) + set_logpseq!(pc, i, p.taskinfo.logpseq) end if score isa Real From 24c721702a05d872d62c3e32c6a6a0444fe4978b Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Wed, 27 Nov 2019 21:53:49 +0000 Subject: [PATCH 13/25] Update src/tasks.jl Co-Authored-By: David Widmann --- src/tasks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks.jl b/src/tasks.jl index 460f2f42..cab1c1a3 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -121,7 +121,7 @@ function Base.push!(pc::ParticleContainer, p::Particle) pc end -function extend!(pc::ParticleContainer, n::Int, varInfo, tasks::Task, taskInfo::T) where T <: AbstractTaskInfo +function extend!(pc::ParticleContainer, n::Int, varInfo, tasks::Task, taskInfo::AbstractTaskInfo) # compute total number of particles number of particles n0 = length(pc) ntotal = n0 + n From feaf25383a21deebc5597399c3721a1b81b9b5e9 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Wed, 27 Nov 2019 21:54:14 +0000 Subject: [PATCH 14/25] Update src/tasks.jl Co-Authored-By: David Widmann --- src/tasks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks.jl b/src/tasks.jl index cab1c1a3..462a8dda 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -226,7 +226,7 @@ increase_logweight(pc :: ParticleContainer, t :: Int, logw :: Float64) = -set_logpseg(pc :: ParticleContainer, t :: Int, logp :: Float64) = +set_logpseq!(pc::ParticleContainer, t::Int, logp::Float64) = (pc.logpseq[t] = logp) increase_logevidence(pc :: ParticleContainer, logw :: Float64) = From f1f98652324d527c5ab149c6a9d3fbb319fd03f6 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Wed, 27 Nov 2019 21:55:31 +0000 Subject: [PATCH 15/25] Update src/tasks.jl Co-Authored-By: David Widmann --- src/tasks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks.jl b/src/tasks.jl index 462a8dda..0200f3e3 100644 --- a/src/tasks.jl +++ b/src/tasks.jl @@ -221,7 +221,7 @@ function effectiveSampleSize(pc :: ParticleContainer) return inv(sum(abs2, Ws)) end -increase_logweight(pc :: ParticleContainer, t :: Int, logw :: Float64) = +increase_logweight!(pc::ParticleContainer, t::Int, logw::Float64) = (pc.logWs[t] += logw) From a92adbbd3de3d1d9eee54cd535649623185ca7dd Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Thu, 28 Nov 2019 22:14:52 +0000 Subject: [PATCH 16/25] Some changes --- src/AdvancedPS.jl | 60 +++++----- src/Interface.jl | 46 -------- src/ParticleContainer.jl | 112 ++++++++++++++++++ src/UtilityFunctions.jl | 17 +++ src/resample.jl | 5 +- src/resample_functions.jl | 125 -------------------- src/samplers.jl | 32 ++++-- src/taskinfo.jl | 40 +++---- src/tasks.jl | 233 -------------------------------------- src/trace.jl | 70 ++++++++++++ 10 files changed, 265 insertions(+), 475 deletions(-) delete mode 100644 src/Interface.jl create mode 100644 src/ParticleContainer.jl create mode 100644 src/UtilityFunctions.jl delete mode 100644 src/tasks.jl create mode 100644 src/trace.jl diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index 000dfd4a..f21b5aaa 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -5,7 +5,6 @@ module AdvancedPS - const DEBUG = Bool(parse(Int, get(ENV, "DEBUG_APS", "0"))) using Libtask using StatsFuns: logsumexp, softmax! import Base.copy @@ -13,9 +12,22 @@ module AdvancedPS abstract type AbstractTaskInfo end abstract type AbstractParticleContainer end abstract type AbstractTrace end + abstract type AbstractPFAlgorithm end + abstract type AbstractPFUtilitFunctions end + abstract type AbstractSMCUtilitFunctions <: AbstractPFUtilitFunctions end + abstract type AbstractPGASUtilityFunctions <: AbstractSMCUtilitFunctions end + + include("UtilityFunctions") + include("ParticleContainer.jl") include("tasks.jl") + include("resample.jl") + include("taskinfo.jl") + include("samplers.jl") + include("resample_functions.jl") + include("Interface.jl") + export ParticleContainer, Trace, @@ -23,44 +35,26 @@ module AdvancedPS logZ, current_trace, extend!, - empty! - - - include("resample.jl") - - export resample! - - - - - include("taskinfo.jl") - - export PGTaskInfo, + empty!, + resample!, + PGTaskInfo, PGASTaskInfo - - include("samplers.jl") - - export sampleSMC!, samplePG! - - - include("resample_functions.jl") - - export resample, + sample!, + resample, randcat, resample_multinomial, resample_residual, resample_stratified, - resample_systematic + resample_systematic, + SMCContainer, + PGContainer, + PGASContainer, + SMCUtilityFunctions, + PGUtilityFunctions, + PGASUtilityFunctions, + SMCAlgorithm, + PGAlgorithm - include("Interface.jl") - export report_transition, - report_observation, - init, - Container, - set_x, - get_x, - copyC, - create_task end # module diff --git a/src/Interface.jl b/src/Interface.jl deleted file mode 100644 index 8ad525a6..00000000 --- a/src/Interface.jl +++ /dev/null @@ -1,46 +0,0 @@ - - -## Some function which make the model easier to define. - - -# A very light weight container for a state space model -# The state space is one dimensional! -# Note that this is only for a very simple demonstration. -mutable struct Container - x::Vector{Float64} - num_produce::Float64 -end - -# This is important for initalizaiton -function init() - return current_trace() -end - -# Th -function report_observation(trace, logp::Float64) - trace.taskinfo.logp += logp - produce(logp) - current_trace() -end - -# logγ corresponds to the proposal distributoin we are sampling from. -function report_transition(trace,logp::Float64,logγ::Float64) - trace.taskinfo.logp += logp - logγ - trace.taskinfo.logpseq += logp -end - -function get_x(trace,indx) - return @inbounds trace.vi.x[indx] -end -function set_x(trace,indx,val) - return @inbounds trace.vi.x[indx] = val -end - - -function copyC(vi::Container) - Container(deepcopy(vi.x),vi.num_produce) -end - -function create_task(f::Function) - return CTask(() -> begin new_vi=f(); produce(Val{:done}); new_vi; end ) -end diff --git a/src/ParticleContainer.jl b/src/ParticleContainer.jl new file mode 100644 index 00000000..39d2e1f9 --- /dev/null +++ b/src/ParticleContainer.jl @@ -0,0 +1,112 @@ + +""" +Data structure for particle filters +- effectiveSampleSize(pc :: ParticleContainer) +- normalise!(pc::ParticleContainer) +- consume(pc::ParticleContainer): return incremental likelihood +""" +mutable struct ParticleContainer{T} <: AbstractParticleContainer where T<:Trace + vals::Vector{T} + # A named tuple with functions to manipulate the particles vi. + logWs::Vector{Float64} + #This corresponds to log p(x_{0:t}), therefore the log likelihood of the transitions up to this point. + #Especially important for ancestor sampling. + logpseq::Vector{Float64} + # log model evidence + logE::Float64 + # helpful for rejuvenation steps, e.g. in SMC2 + n_consume::Int +end + + +Base.collect(pc :: ParticleContainer) = pc.vals # prev: Dict, now: Array +Base.length(pc :: ParticleContainer) = length(pc.vals) +# pc[i] returns the i'th particle +Base.@propagate_inbounds Base.getindex(pc::ParticleContainer, i::Int) = pc.vals[i] + + + +# registers a new x-particle in the container +function Base.push!(pc::ParticleContainer, p::Particle) + push!(pc.vals, p) + push!(pc.logWs, 0.0) + push!(pc.logpseq,0.0) + return pc +end + +# clears the container but keep params, logweight etc. +function Base.empty!(pc::ParticleContainer) + pc.vals = eltype(pc.vals)[] + pc.logWs = Float64[] + pc.logpseq = Float64[] + return pc +end + +# clones a theta-particle +function Base.copy(pc::ParticleContainer, utility_functions<:AbstractPFUtilitFunctions) + # fork particles + vals = eltype(pc.vals)[fork(p, utility_functions.copy) for p in pc.vals] + # copy weights + logWs = copy(pc.logWs) + logpseq = copy(pc.logpseq) + ParticleContainer( vals, logWs, logpseq, pc.logE, pc.n_consume) +end + +# run particle filter for one step, return incremental likelihood +function Libtask.consume(pc::ParticleContainer) + # normalisation factor: 1/N + z1 = logZ(pc) + n = length(pc) + + particles = collect(pc) + num_done = 0 + for i=1:n + p = particles[i] + score = Libtask.consume(p) + if hasproperty(typeof(p.taskinfo), logpseq) + set_logpseq!(pc, i, p.taskinfo.logpseq) + end + if score isa Real + score += p.taskinfo.logp + reset_logp!(p.taskinfo) + increase_logweight(pc, i, Float64(score)) + elseif score == Val{:done} + num_done += 1 + else + println(score) + error("[consume]: error in running particle filter.") + end + end + + if num_done == n + res = Val{:done} + elseif num_done != 0 + error("[consume]: mis-aligned execution traces, num_particles= $(n), num_done=$(num_done).") + else + # update incremental likelihoods + z2 = logZ(pc) + res = increase_logevidence(pc, z2 - z1) + pc.n_consume += 1 + # res = increase_loglikelihood(pc, z2 - z1) + end + return res +end + + +# compute the normalized weights +weights(pc::ParticleContainer) = softmax(pc.logWs) + +# compute the log-likelihood estimate, ignoring constant term ``- \log num_particles`` +logZ(pc::ParticleContainer) = logsumexp(pc.logWs) + +# compute the effective sample size ``1 / ∑ wᵢ²``, where ``wᵢ```are the normalized weights +function effectiveSampleSize(pc :: ParticleContainer) + Ws = weights(pc) + return inv(sum(abs2, Ws)) +end + +increase_logweight!(pc::ParticleContainer, t::Int, logw::Float64) = (pc.logWs[t] += logw) + +set_logpseq!(pc::ParticleContainer, t::Int, logp::Float64) = (pc.logpseq[t] = logp) + +increase_logevidence!(pc::ParticleContainer, logw::Float64) = (pc.logE += logw) diff --git a/src/UtilityFunctions.jl b/src/UtilityFunctions.jl new file mode 100644 index 00000000..9de9006c --- /dev/null +++ b/src/UtilityFunctions.jl @@ -0,0 +1,17 @@ +# Some structure +abstract type AbstractSMCUtilitFunctions <: AbstractPFUtilitFunctions end +abstract type AbstractPGASUtilityFunctions <: AbstractSMCUtilitFunctions end + +struct SMCUtilityFunctions<:AbstractSMCUtilitFunctions + copy :: Function + set_retained_vns_del_by_spl :: Function +end + +const PGUtilityFunctions = SMCUtilityFunctions + +struct PGASUtilityFunctions{Pr}<:AbstractPGASUtilityFunctions where Pr <:Union{Function,Nothing} + copy :: Function + set_retained_vns_del_by_spl :: Function + merge_traj :: Function + ancestor_proposal :: PR +end diff --git a/src/resample.jl b/src/resample.jl index a48bea2a..0b772353 100644 --- a/src/resample.jl +++ b/src/resample.jl @@ -1,6 +1,7 @@ function resample!( pc :: ParticleContainer, + utility_functions <: AbstractSMCUtilitFunctions, indx :: Vector{Int64}, ref :: Union{Particle, Nothing} = nothing, new_ref:: Union{Particle, Nothing} = nothing @@ -24,12 +25,12 @@ function resample!( # fork first child pi = particles[i] isref = pi === ref - p = isref ? fork(pi, pc.manipulators["copy"], isref, pc.manipulators["set_retained_vns_del_by_spl!"]) : pi + p = isref ? fork(pi, utility_functions.copy, isref, utility_functions.set_retained_vns_del_by_spl!) : pi children[j += 1] = p # fork additional children for _ in 2:ni - children[j += 1] = fork(p, pc.manipulators["copy"], isref, pc.manipulators["set_retained_vns_del_by_spl!"]) + children[j += 1] = fork(p, utility_functions.copy, isref, utility_functions.set_retained_vns_del_by_spl!) end end end diff --git a/src/resample_functions.jl b/src/resample_functions.jl index ece99815..06d7a6c4 100644 --- a/src/resample_functions.jl +++ b/src/resample_functions.jl @@ -8,9 +8,7 @@ # Default resampling scheme function resample(w::AbstractVector{<:Real}, num_particles::Integer=length(w)) - return resample_systematic(w, num_particles) - end @@ -18,91 +16,43 @@ end # More stable, faster version of rand(Categorical) function randcat(p::AbstractVector{T}) where T<:Real - r, s = rand(T), 1 - for j in eachindex(p) - r -= p[j] - if r <= zero(T) - s = j - break - end - end - return s - end - - function resample_multinomial(w::AbstractVector{<:Real}, num_particles::Integer) - return rand(Distributions.sampler(Categorical(w)), num_particles) - end function resample_residual(w::AbstractVector{<:Real}, num_particles::Integer) - - - M = length(w) - - - # "Repetition counts" (plus the random part, later on): - Ns = floor.(length(w) .* w) - - - # The "remainder" or "residual" count: - R = Int(sum(Ns)) - - - # The number of particles which will be drawn stocastically: - M_rdn = num_particles - R - - - # The modified weights: - Ws = (M .* w - floor.(M .* w)) / M_rdn - - - # Draw the deterministic part: - indx1, i = Array{Int}(undef, R), 1 - for j in 1:M - for k in 1:Ns[j] - indx1[i] = j - i += 1 - end - end - - - # And now draw the stocastic (Multinomial) part: - return append!(indx1, rand(Distributions.sampler(Categorical(w)), M_rdn)) - end @@ -132,67 +82,30 @@ i.e., `xᵢ = j` if and only if """ function resample_stratified(weights::AbstractVector{<:Real}, n::Integer) - # check input - m = length(weights) - m > 0 || error("weight vector is empty") - - - # pre-calculations - @inbounds v = n * weights[1] - - - # generate all samples - samples = Array{Int}(undef, n) - sample = 1 - @inbounds for i in 1:n - # sample next `u` (scaled by `n`) - u = oftype(v, i - 1 + rand()) - - - # as long as we have not found the next sample - while v < u - # increase and check the sample - sample += 1 - sample > m && - error("sample could not be selected (are the weights normalized?)") - - - # update the cumulative sum of weights (scaled by `n`) - v += n * weights[sample] - end - - - # save the next sample - samples[i] = sample - end - - - return samples - end @@ -222,67 +135,29 @@ normalized `weights`, i.e., `xᵢ = j` if and only if """ function resample_systematic(weights::AbstractVector{<:Real}, n::Integer) - # check input - m = length(weights) - m > 0 || error("weight vector is empty") - - - # pre-calculations - @inbounds v = n * weights[1] - u = oftype(v, rand()) - - - # find all samples - samples = Array{Int}(undef, n) - sample = 1 - @inbounds for i in 1:n - # as long as we have not found the next sample - while v < u - # increase and check the sample - sample += 1 - sample > m && - error("sample could not be selected (are the weights normalized?)") - - - # update the cumulative sum of weights (scaled by `n`) - v += n * weights[sample] - end - - - # save the next sample - samples[i] = sample - - - # update `u` - u += one(u) - end - - - return samples - end diff --git a/src/samplers.jl b/src/samplers.jl index fb3f3edc..11f14a91 100644 --- a/src/samplers.jl +++ b/src/samplers.jl @@ -1,10 +1,21 @@ +struct SMCAlgorithm{RT, UF<:AbstractSMCUtilitFunctions} <: AbstractPFAlgorithm where RT<:AbstractFloat + resampler :: Function + resampler_threshold :: RT + utility_functiions :: UF +end + +struct PGAlgorithm{RT, UF<:AbstractSMCUtilitFunctions} <: AbstractPFAlgorithm where RT<:AbstractFloat + resampler :: Function + resampler_threshold :: RT + utility_functiions :: UF +end -function sampleSMC!(pc::ParticleContainer,resampler::Function =resample_systematic ,resampler_threshold::AbstractFloat = 0.5) - while consume(pc) != Val{:done} +function sample!(pc::ParticleContainer, spl::SMCSampler) + while consume(pc, spl) != Val{:done} ess = effectiveSampleSize(pc) - if ess <= resampler_threshold * length(pc) + if ess <= spl.resampler_threshold * length(pc) # compute weights Ws = weights(pc) # check that weights are not NaN @@ -12,20 +23,21 @@ function sampleSMC!(pc::ParticleContainer,resampler::Function =resample_systemat # sample ancestor indices n = length(pc) nresamples = n - indx = resampler(Ws, nresamples) - resample!(pc, indx) + indx = spl.resampler(Ws, nresamples) + resample!(pc, spl.utility_functions, indx) end end end # The resampler threshold is only imprtant for the first step! -function samplePG!(pc::ParticleContainer,resampler::Function = resample_systematic ,ref_particle::Union{Particle,Nothing}=nothing , resampler_threshold =0.5) +function sample!(pc::ParticleContainer{T}, spl::PGAlgorithm, ref_traj::Union{Particle,Nothing}=nothing) - if ref_particle === nothing + if spl.ref_traj === nothing # We do not have a reference trajectory yet, therefore, perform normal SMC! - sampleSMC!(pc,resampler,resampler_threshold) + # At this point it is important that we have this hirarchical structure for the utility function struct. + sampleSMC!(pc, SMCAlgorithm(spl.resampler, spl.resampler_threshold, spl.utility_functions)) else - while consume(pc) != Val{:done} + while consume(pc, spl) != Val{:done} # compute weights Ws = weights(pc) # check that weights are not NaN @@ -38,7 +50,7 @@ function samplePG!(pc::ParticleContainer,resampler::Function = resample_systemat # We add ancestor trajectory to the path. # For ancestor sampling, we would change n at this point. push!(indx,n) - resample!(pc, indx,ref_particle) + resample!(pc, spl.utility_functions, indx, ref_traj) end end diff --git a/src/taskinfo.jl b/src/taskinfo.jl index a8d1670a..2422edff 100644 --- a/src/taskinfo.jl +++ b/src/taskinfo.jl @@ -4,43 +4,31 @@ # allows to propagate information trough the computation. # This is important because we do not want to -mutable struct PGTaskInfo <: AbstractTaskInfo - # This corresponds to p(y_t | x_t)*p(x_t|x_t-1)/ γ(x_t|x_t-1,y_t), +mutable struct PGTaskInfo{T} <: AbstractTaskInfo where {T <:AbstractFloat} + # This corresponds to p(yₜ | xₜ) p(xₜ | xₜ₋₁) / γ(xₜ | xₜ₋₁, yₜ) # where γ is the porposal. # We need this variable to compute the weights! - logp::Float64 - - # This corresponds to p(x_t|x_t-1)*p(x_t-1|x_t-2)*... *p(x_0) + logp::T + # This corresponds to p(xₜ | xₜ₋₁) p(xₜ₋₁ | xₜ₋₂) ⋯ p(x₀) # or |x_{0:t-1} for non markovian models, we need this to compute # the ancestor weights. - - logpseq::Float64 + logpseq::T end -function copy(info::PGTaskInfo) - PGTaskInfo(info.logp, info.logpseq) -end - - -mutable struct PGASTaskInfo <: AbstractTaskInfo - # This corresponds to p(y_t | x_t)*p(x_t|x_t-1)/ γ(x_t|x_t-1,y_t), - # where γ is the porposal. - # We need this variable to compute the weights! - logp::Float64 - - # This corresponds to p(x_t|x_t-1)*p(x_t-1|x_t-2)*... *p(x_0) - # or |x_{0:t-1} for non markovian models, we need this to compute - # the ancestor weights. - - logpseq::Float64 +mutable struct PGASTaskInfo{T} <: AbstractTaskInfo where {T <: AbstractFloat} + # Same as above + logp::T + logpseq::T + # This is important for ancesotr sampling to synchronize. hold::Bool end function PGASTaskInfo(logp::Float64,logpseq::Float64) PGASTaskInfo(logp,logpseq,false) end -function copy(info::PGASTaskInfo) - PGASTaskInfo(info.logp, info.logpseq,info.hold) -end +function Base.copy(info::PGTaskInfo) = (PGTaskInfo(info.logp, info.logpseq) +function Base.copy(info::PGASTaskInfo) = (PGASTaskInfo(info.logp, info.logpseq,info.hold) + +reset_logp!(ti::AbstractTaskInfo) = (ti.logp = 0.0) diff --git a/src/tasks.jl b/src/tasks.jl deleted file mode 100644 index 0200f3e3..00000000 --- a/src/tasks.jl +++ /dev/null @@ -1,233 +0,0 @@ -# Idea: We decouple tasks form the model by only allowing to pass a function. -# This way, we do no longer need to have the model and the sampler saved in the trace struct -# However, we still need to insantiate the VarInfo - -# TaskInfo stores additional informatino about the task -mutable struct Trace{Tvi, TInfo} <: AbstractTrace where {Tvi, TInfo <: AbstractTaskInfo} - vi::Tvi # Unfortunatley, we can not set force this to be a subtype of VarInfo... - task::Task - taskinfo::TInfo -end - -function Base.copy(trace::Trace{Tvi,TInfo}, copy_vi::Function) where {Tvi, TInfo <: AbstractTaskInfo} - res = Trace{typeof(trace.vi),typeof(trace.taskinfo)}(copy_vi(trace.vi), copy(trace.task), copy(trace.taskinfo)) - return res -end - -# The procedure passes a function which is specified by the model. - -function Trace(f::Function, vi, taskinfo, copy_vi::Function) - task = CTask( () -> begin res=f(); produce(Val{:done}); res; end ) - - res = Trace{typeof(vi),typeof(task)}(copy_vi(vi), task, copy(taskinfo)); - # CTask(()->f()); - if res.task.storage === nothing - res.task.storage = IdDict() - end - res.task.storage[:turing_trace] = res # create a backward reference in task_local_storage - return res -end - -## We need to design the task in the Turing wrapper. -function Trace( vi, task::Task, taskinfo, copy_vi::Function) - - - res = Trace{typeof(vi),typeof(taskinfo)}(copy_vi(vi), Libtask.copy(task), copy(taskinfo)); - # CTask(()->f()); - if res.task.storage === nothing - res.task.storage = IdDict() - end - res.task.storage[:turing_trace] = res # create a backward reference in task_local_storage - return res -end - - - -# step to the next observe statement, return log likelihood -Libtask.consume(t::Trace) = (t.vi.num_produce += 1; consume(t.task)) - -# Task copying version of fork for Trace. -function fork(trace :: Trace, copy_vi::Function, is_ref :: Bool = false, set_retained_vns_del_by_spl!::Union{Function,Nothing} = nothing) - newtrace = copy(trace,copy_vi) - if is_ref - @assert set_retained_vns_del_by_spl! != nothing "[AdvancedPF] set_retained_vns_del_by_spl! is not set." - set_retained_vns_del_by_spl!(newtrace.vi) - end - newtrace.task.storage[:turing_trace] = newtrace - return newtrace -end - -# PG requires keeping all randomness for the reference particle -# Create new task and copy randomness -function forkr(trace :: Trace, copy_vi::Function) - - newtrace = Trace(trace.vi,trace.task ,trace.taskinfo,copy_vi) - newtrace.vi.num_produce = 0 - return newtrace -end - -current_trace() = current_task().storage[:turing_trace] - -const Particle = Trace - -""" -Data structure for particle filters -- effectiveSampleSize(pc :: ParticleContainer) -- normalise!(pc::ParticleContainer) -- consume(pc::ParticleContainer): return incremental likelihood -""" -mutable struct ParticleContainer{Tvi,TInfo} <: AbstractParticleContainer where {Tvi, TInfo <: AbstractTaskInfo} - vals::Vector{Trace{Tvi,TInfo}} - # A named tuple with functions to manipulate the particles vi. - manipulators::Dict{String,Function} - # logarithmic weights (Trace) or incremental log-likelihoods (ParticleContainer) - logWs::Vector{Float64} - #This corresponds to log p(x_{0:t}), therefore the log likelihood of the transitions up to this point. - #Especially important for ancestor sampling. - logpseq::Vector{Float64} - # log model evidence - logE::Float64 - # helpful for rejuvenation steps, e.g. in SMC2 - n_consume::Int - - -end - -## Empty initilaizaiton -ParticleContainer{Tvi,TInfo}() where {Tvi, TInfo <: AbstractTaskInfo} = ParticleContainer{Tvi,TInfo}(Dict{String,Function}(),0) -ParticleContainer{Tvi,TInfo}(manipulators::Dict{String,Function}) where {Tvi, TInfo <: AbstractTaskInfo} = ParticleContainer{Tvi,TInfo}(manipulators,0) -#Some initilaizaiton... -function ParticleContainer{Tvi,TInfo}(manipulators::Dict{String,Function}, n::Int) where {Tvi, TInfo <: AbstractTaskInfo} - if !("copy" in keys(manipulators)) manipulators["copy"] = deepcopy end - if !("set_retained_vns_del_by_spl!" in keys(manipulators)) manipulators["set_retained_vns_del_by_spl!"] = (x) -> () end - ParticleContainer{Tvi,TInfo}( Vector{Trace{Tvi,TInfo}}(undef, n), manipulators, Float64[],Float64[], 0.0, 0) - -end - - -Base.collect(pc :: ParticleContainer) = pc.vals # prev: Dict, now: Array -Base.length(pc :: ParticleContainer) = length(pc.vals) -Base.similar(pc :: ParticleContainer{T}) where T = ParticleContainer{T}(0) -# pc[i] returns the i'th particle -Base.@propagate_inbounds Base.getindex(pc::ParticleContainer, i::Int) = pc.vals[i] - - - -# registers a new x-particle in the container -function Base.push!(pc::ParticleContainer, p::Particle) - push!(pc.vals, p) - push!(pc.logWs, 0.0) - push!(pc.logpseq,0.0) - pc -end - -function extend!(pc::ParticleContainer, n::Int, varInfo, tasks::Task, taskInfo::AbstractTaskInfo) - # compute total number of particles number of particles - n0 = length(pc) - ntotal = n0 + n - - # add additional particles and weights - vals = pc.vals - logWs = pc.logWs - logpseq = pc.logpseq - resize!(vals, ntotal) - resize!(logWs, ntotal) - resize!(logpseq, ntotal) - - @inbounds for i in (n0 + 1):ntotal - vals[i] = Trace(varInfo,tasks, taskInfo, pc.manipulators["copy"]) - logWs[i] = 0.0 - logpseq[i] = 0.0 - end - - pc -end -# clears the container but keep params, logweight etc. -function Base.empty!(pc::ParticleContainer) - pc.vals = eltype(pc.vals)[] - pc.logWs = Float64[] - pc.logpseq = Float64[] - pc -end - -# clones a theta-particle -function Base.copy(pc::ParticleContainer) - # fork particles - vals = eltype(pc.vals)[fork(p, pc.mainpulators["copy"]) for p in pc.vals] - - # copy weights - logWs = copy(pc.logWs) - logpseq = copy(pc.logpseq) - - ParticleContainer( vals, pc.manipulators, logWs, logpseq, pc.logE, pc.n_consume) -end - -# run particle filter for one step, return incremental likelihood -function Libtask.consume(pc :: ParticleContainer) - # normalisation factor: 1/N - z1 = logZ(pc) - n = length(pc) - - particles = collect(pc) - num_done = 0 - for i=1:n - p = particles[i] - score = Libtask.consume(p) - - if hasproperty(typeof(p.taskinfo), logpseq) - set_logpseq!(pc, i, p.taskinfo.logpseq) - end - - if score isa Real - score += p.taskinfo.logp - - ## Equivalent to reset logp - p.taskinfo.logp = 0 - - increase_logweight(pc, i, Float64(score)) - elseif score == Val{:done} - num_done += 1 - else - println(score) - error("[consume]: error in running particle filter.") - end - end - - if num_done == n - res = Val{:done} - elseif num_done != 0 - error("[consume]: mis-aligned execution traces, num_particles= $(n), num_done=$(num_done).") - else - # update incremental likelihoods - z2 = logZ(pc) - res = increase_logevidence(pc, z2 - z1) - pc.n_consume += 1 - # res = increase_loglikelihood(pc, z2 - z1) - end - - res -end - - -# compute the normalized weights -weights(pc::ParticleContainer) = softmax!(copy(pc.logWs)) - -# compute the log-likelihood estimate, ignoring constant term ``- \log num_particles`` -logZ(pc::ParticleContainer) = logsumexp(pc.logWs) - -# compute the effective sample size ``1 / ∑ wᵢ²``, where ``wᵢ```are the normalized weights -function effectiveSampleSize(pc :: ParticleContainer) - Ws = weights(pc) - return inv(sum(abs2, Ws)) -end - -increase_logweight!(pc::ParticleContainer, t::Int, logw::Float64) = - (pc.logWs[t] += logw) - - - -set_logpseq!(pc::ParticleContainer, t::Int, logp::Float64) = - (pc.logpseq[t] = logp) - -increase_logevidence(pc :: ParticleContainer, logw :: Float64) = - (pc.logE += logw) diff --git a/src/trace.jl b/src/trace.jl new file mode 100644 index 00000000..bc87bd8a --- /dev/null +++ b/src/trace.jl @@ -0,0 +1,70 @@ +# Idea: We decouple tasks form the model by only allowing to pass a function. +# This way, we do no longer need to have the model and the sampler saved in the trace struct +# However, we still need to insantiate the VarInfo + +# TaskInfo stores additional information about the task +mutable struct Trace{Tvi, TInfo} <: AbstractTrace where {Tvi, TInfo <: AbstractTaskInfo} + vi::Tvi # Unfortunatley, we can not set force this to be a subtype of VarInfo... + task::Task + taskinfo::TInfo +end + +function Base.copy(trace::Trace{Tvi,TInfo}, copy_vi::Function) where {Tvi, TInfo <: AbstractTaskInfo} + return Trace(copy_vi(trace.vi), copy(trace.task), copy(trace.taskinfo)) +end + +# The procedure passes a function which is specified by the model. + +function Trace(f::Function, vi, taskinfo, copy_vi::Function) + task = CTask( () -> begin res=f(); produce(Val{:done}); res; end ) + + res = Trace(copy_vi(vi), task, copy(taskinfo)) + # CTask(()->f()); + if res.task.storage === nothing + res.task.storage = IdDict() + end + res.task.storage[:turing_trace] = res # create a backward reference in task_local_storage + return res +end + +## We need to design the task in the Turing wrapper. +function Trace(vi, task::Task, taskinfo, copy_vi::Function) + + + res = Trace(copy_vi(vi), Libtask.copy(task), copy(taskinfo)) + # CTask(()->f()); + if res.task.storage === nothing + res.task.storage = IdDict() + end + res.task.storage[:turing_trace] = res # create a backward reference in task_local_storage + return res +end + + + +# step to the next observe statement, return log likelihood +Libtask.consume(t::Trace) = (t.vi.num_produce += 1; consume(t.task)) + +# Task copying version of fork for Trace. +function fork(trace::Trace, copy_vi::Function, is_ref::Bool = false, set_retained_vns_del_by_spl!::Union{Function,Nothing} = nothing) + newtrace = copy(trace, copy_vi) + if is_ref + @assert set_retained_vns_del_by_spl! !== nothing "[AdvancedPS] set_retained_vns_del_by_spl! is not set." + set_retained_vns_del_by_spl!(newtrace.vi) + end + newtrace.task.storage[:turing_trace] = newtrace + return newtrace +end + +# PG requires keeping all randomness for the reference particle +# Create new task and copy randomness +function forkr(trace::Trace, copy_vi::Function) + + newtrace = Trace(trace.vi,trace.task ,trace.taskinfo,copy_vi) + newtrace.vi.num_produce = 0 + return newtrace +end + +current_trace() = current_task().storage[:turing_trace] + +const Particle = Trace From f7df617f6ff6a513db70a1eae74ba8298383c50b Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Thu, 28 Nov 2019 22:16:23 +0000 Subject: [PATCH 17/25] spelling mistake --- src/AdvancedPS.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index f21b5aaa..ea778490 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -21,7 +21,7 @@ module AdvancedPS include("UtilityFunctions") include("ParticleContainer.jl") - include("tasks.jl") + include("trace.jl") include("resample.jl") include("taskinfo.jl") include("samplers.jl") From 887a5638f8a773460cdcfea10e0455ccfd60546c Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Thu, 28 Nov 2019 23:18:12 +0000 Subject: [PATCH 18/25] small mistakes small mistakes --- Project.toml | 3 + Tests/demonstarte.jl | 9 +- Tests/testInterface.jl | 177 +++++++++++++++++++++++++++++++++++++++ src/AdvancedPS.jl | 8 +- src/ParticleContainer.jl | 2 +- src/UtilityFunctions.jl | 7 +- src/resample.jl | 2 +- src/samplers.jl | 4 +- src/taskinfo.jl | 8 +- 9 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 Project.toml create mode 100644 Tests/testInterface.jl diff --git a/Project.toml b/Project.toml new file mode 100644 index 00000000..4342b747 --- /dev/null +++ b/Project.toml @@ -0,0 +1,3 @@ +name = "AdvancedPS" +authors = ["kongi "] +version = "0.1.0" diff --git a/Tests/demonstarte.jl b/Tests/demonstarte.jl index 2c78605b..de00cbf5 100644 --- a/Tests/demonstarte.jl +++ b/Tests/demonstarte.jl @@ -1,9 +1,12 @@ + ## It is not yet a package... + using AdvancedPS using Libtask const APS = AdvancedPS using Distributions n = 20 +include("testInterface.jl") @@ -53,7 +56,7 @@ m = 10 task = create_task(task_f) -APS.extend!(particles, 10, vi, task, PGTaskInfo(0.0,0.0)) +APS.push!(particles, 10, vi, task, PGTaskInfo(0.0,0.0)) ## Do one SMC step. APS.samplePG!(particles) @@ -64,8 +67,8 @@ m = 10 task = create_task(task_f) -APS.extend!(particles2, 9, vi, task, PGTaskInfo(0.0,0.0)) -APS.extend!(particles2, 1, particles[1].vi, task, PGTaskInfo(0.0,0.0)) +APS.push!(particles2, 9, vi, task, PGTaskInfo(0.0,0.0)) +APS.push!(particles2, 1, particles[1].vi, task, PGTaskInfo(0.0,0.0)) ## Do one SMC step. APS.samplePG!(particles2,resample_systematic,particles2[m]) diff --git a/Tests/testInterface.jl b/Tests/testInterface.jl new file mode 100644 index 00000000..c5dd3ae6 --- /dev/null +++ b/Tests/testInterface.jl @@ -0,0 +1,177 @@ + + + + +## Some function which make the model easier to define. + + + + + + + + + + + +# A very light weight container for a state space model + + + +# The state space is one dimensional! + + + +# Note that this is only for a very simple demonstration. + + + +mutable struct Container + + + + x::Vector{Float64} + + + + num_produce::Float64 + + + +end + + + + + + + +# This is important for initalizaiton + + + +function initialize() + + + + return current_trace() + + + +end + + + + + + + +# Th + + + +function report_observation(trace, logp::Float64) + + + + trace.taskinfo.logp += logp + + + + produce(logp) + + + + current_trace() + + + +end + + + + + + + +# logγ corresponds to the proposal distributoin we are sampling from. + + + +function report_transition(trace,logp::Float64,logγ::Float64) + + + + trace.taskinfo.logp += logp - logγ + + + + trace.taskinfo.logpseq += logp + + + +end + + + + + + + +function get_x(trace,indx) + + + + return @inbounds trace.vi.x[indx] + + + +end + + + +function set_x(trace,indx,val) + + + + return @inbounds trace.vi.x[indx] = val + + + +end + + + + + + + + + + + +function copyC(vi::Container) + + + + Container(deepcopy(vi.x),vi.num_produce) + + + +end + + + + + + + +function create_task(f::Function) + + + + return CTask(() -> begin new_vi=f(); produce(Val{:done}); new_vi; end ) + + + +end diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index ea778490..6798775a 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -19,14 +19,13 @@ module AdvancedPS abstract type AbstractPGASUtilityFunctions <: AbstractSMCUtilitFunctions end - include("UtilityFunctions") - include("ParticleContainer.jl") + include("UtilityFunctions.jl") include("trace.jl") + include("ParticleContainer.jl") include("resample.jl") include("taskinfo.jl") include("samplers.jl") include("resample_functions.jl") - include("Interface.jl") export ParticleContainer, @@ -46,9 +45,6 @@ module AdvancedPS resample_residual, resample_stratified, resample_systematic, - SMCContainer, - PGContainer, - PGASContainer, SMCUtilityFunctions, PGUtilityFunctions, PGASUtilityFunctions, diff --git a/src/ParticleContainer.jl b/src/ParticleContainer.jl index 39d2e1f9..ed2945ca 100644 --- a/src/ParticleContainer.jl +++ b/src/ParticleContainer.jl @@ -43,7 +43,7 @@ function Base.empty!(pc::ParticleContainer) end # clones a theta-particle -function Base.copy(pc::ParticleContainer, utility_functions<:AbstractPFUtilitFunctions) +function Base.copy(pc::ParticleContainer, utility_functions::AbstractPFUtilitFunctions) # fork particles vals = eltype(pc.vals)[fork(p, utility_functions.copy) for p in pc.vals] # copy weights diff --git a/src/UtilityFunctions.jl b/src/UtilityFunctions.jl index 9de9006c..d8a36533 100644 --- a/src/UtilityFunctions.jl +++ b/src/UtilityFunctions.jl @@ -1,6 +1,3 @@ -# Some structure -abstract type AbstractSMCUtilitFunctions <: AbstractPFUtilitFunctions end -abstract type AbstractPGASUtilityFunctions <: AbstractSMCUtilitFunctions end struct SMCUtilityFunctions<:AbstractSMCUtilitFunctions copy :: Function @@ -9,9 +6,9 @@ end const PGUtilityFunctions = SMCUtilityFunctions -struct PGASUtilityFunctions{Pr}<:AbstractPGASUtilityFunctions where Pr <:Union{Function,Nothing} +struct PGASUtilityFunctions{AP}<:AbstractPGASUtilityFunctions where AP<:Union{Function,Nothing} copy :: Function set_retained_vns_del_by_spl :: Function merge_traj :: Function - ancestor_proposal :: PR + ancestor_proposal :: AP end diff --git a/src/resample.jl b/src/resample.jl index 0b772353..968b4f51 100644 --- a/src/resample.jl +++ b/src/resample.jl @@ -1,7 +1,7 @@ function resample!( pc :: ParticleContainer, - utility_functions <: AbstractSMCUtilitFunctions, + utility_functions :: AbstractSMCUtilitFunctions, indx :: Vector{Int64}, ref :: Union{Particle, Nothing} = nothing, new_ref:: Union{Particle, Nothing} = nothing diff --git a/src/samplers.jl b/src/samplers.jl index 11f14a91..d0596323 100644 --- a/src/samplers.jl +++ b/src/samplers.jl @@ -12,7 +12,7 @@ struct PGAlgorithm{RT, UF<:AbstractSMCUtilitFunctions} <: AbstractPFAlgorithm wh end -function sample!(pc::ParticleContainer, spl::SMCSampler) +function sample!(pc::ParticleContainer, spl::SMCAlgorithm) while consume(pc, spl) != Val{:done} ess = effectiveSampleSize(pc) if ess <= spl.resampler_threshold * length(pc) @@ -30,7 +30,7 @@ function sample!(pc::ParticleContainer, spl::SMCSampler) end # The resampler threshold is only imprtant for the first step! -function sample!(pc::ParticleContainer{T}, spl::PGAlgorithm, ref_traj::Union{Particle,Nothing}=nothing) +function sample!(pc::ParticleContainer, spl::PGAlgorithm, ref_traj::Union{Particle,Nothing}=nothing) if spl.ref_traj === nothing # We do not have a reference trajectory yet, therefore, perform normal SMC! diff --git a/src/taskinfo.jl b/src/taskinfo.jl index 2422edff..98aa3592 100644 --- a/src/taskinfo.jl +++ b/src/taskinfo.jl @@ -28,7 +28,11 @@ function PGASTaskInfo(logp::Float64,logpseq::Float64) PGASTaskInfo(logp,logpseq,false) end -function Base.copy(info::PGTaskInfo) = (PGTaskInfo(info.logp, info.logpseq) -function Base.copy(info::PGASTaskInfo) = (PGASTaskInfo(info.logp, info.logpseq,info.hold) +function Base.copy(info::PGTaskInfo) + PGTaskInfo(info.logp, info.logpseq) +end +function Base.copy(info::PGASTaskInfo) + PGASTaskInfo(info.logp, info.logpseq,info.hold) +end reset_logp!(ti::AbstractTaskInfo) = (ti.logp = 0.0) From b6865b2971a4caf25384eb1931383a6f6a17fdae Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Thu, 28 Nov 2019 23:59:13 +0000 Subject: [PATCH 19/25] small changes --- Tests/demonstarte.jl | 14 +++++++------- src/samplers.jl | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/demonstarte.jl b/Tests/demonstarte.jl index de00cbf5..663d2355 100644 --- a/Tests/demonstarte.jl +++ b/Tests/demonstarte.jl @@ -6,7 +6,7 @@ using Libtask const APS = AdvancedPS using Distributions n = 20 -include("testInterface.jl") +include("AdvancedPS/Tests/testInterface.jl") @@ -26,7 +26,7 @@ end # The trace contains the variables which we want to infer using particle gibbs. # Thats all! function task_f() - var = init() + var = initialize() set_x(var,1,rand(Normal())) # We sample report_transition(var,0.0,0.0) for i = 2:n @@ -46,17 +46,17 @@ function task_f() end end +task = create_task(task_f) +m = 10 +particlevec = [Trace(deepcopy(vi), copy(task), PGTaskInfo(0.0,0.0)) for i =1:m] +ws = [0 for i =1:m] +particles = APS.ParticleContainer{typeof(vi),APS.PGTaskInfo }(particlevec,) -particles = APS.ParticleContainer{typeof(vi),APS.PGTaskInfo }() - - -m = 10 task = create_task(task_f) -APS.push!(particles, 10, vi, task, PGTaskInfo(0.0,0.0)) ## Do one SMC step. APS.samplePG!(particles) diff --git a/src/samplers.jl b/src/samplers.jl index d0596323..ccf7507b 100644 --- a/src/samplers.jl +++ b/src/samplers.jl @@ -13,7 +13,7 @@ end function sample!(pc::ParticleContainer, spl::SMCAlgorithm) - while consume(pc, spl) != Val{:done} + while consume(pc) != Val{:done} ess = effectiveSampleSize(pc) if ess <= spl.resampler_threshold * length(pc) # compute weights @@ -37,7 +37,7 @@ function sample!(pc::ParticleContainer, spl::PGAlgorithm, ref_traj::Union{Partic # At this point it is important that we have this hirarchical structure for the utility function struct. sampleSMC!(pc, SMCAlgorithm(spl.resampler, spl.resampler_threshold, spl.utility_functions)) else - while consume(pc, spl) != Val{:done} + while consume(pc) != Val{:done} # compute weights Ws = weights(pc) # check that weights are not NaN From ae1fd14080cf9f8032b41fcc457667215d985ec1 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Tue, 3 Dec 2019 19:28:08 +0000 Subject: [PATCH 20/25] Examples --- {Tests => Example}/demonstarte.jl | 11 +++-------- {Tests => Example}/demonstartePGAS.jl | 0 Tests/testInterface.jl => Example/test_interface.jl | 0 .../create_data_from_old_turing_model.jl | 0 .../test_against_previous_model.jl | 5 +++++ 5 files changed, 8 insertions(+), 8 deletions(-) rename {Tests => Example}/demonstarte.jl (91%) rename {Tests => Example}/demonstartePGAS.jl (100%) rename Tests/testInterface.jl => Example/test_interface.jl (100%) rename {Tests => Test_with_Turing}/create_data_from_old_turing_model.jl (100%) rename {Tests => Test_with_Turing}/test_against_previous_model.jl (82%) diff --git a/Tests/demonstarte.jl b/Example/demonstarte.jl similarity index 91% rename from Tests/demonstarte.jl rename to Example/demonstarte.jl index 663d2355..86b5b217 100644 --- a/Tests/demonstarte.jl +++ b/Example/demonstarte.jl @@ -10,8 +10,6 @@ include("AdvancedPS/Tests/testInterface.jl") - - ## Our states vi = Container(zeros(n),0) ## Our observations @@ -51,14 +49,11 @@ m = 10 particlevec = [Trace(deepcopy(vi), copy(task), PGTaskInfo(0.0,0.0)) for i =1:m] ws = [0 for i =1:m] -particles = APS.ParticleContainer{typeof(vi),APS.PGTaskInfo }(particlevec,) - - -task = create_task(task_f) - +particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),deepcopy(ws),0,0) +Algo = APS.SMCAlgorithm(APS.resample_systematic,0.0,APS.SMCUtilityFunctions(deepcopy,(x)-> x)) ## Do one SMC step. -APS.samplePG!(particles) +APS.sample!(particles,Algo) particles2 = APS.ParticleContainer{typeof(vi),APS.PGTaskInfo }() diff --git a/Tests/demonstartePGAS.jl b/Example/demonstartePGAS.jl similarity index 100% rename from Tests/demonstartePGAS.jl rename to Example/demonstartePGAS.jl diff --git a/Tests/testInterface.jl b/Example/test_interface.jl similarity index 100% rename from Tests/testInterface.jl rename to Example/test_interface.jl diff --git a/Tests/create_data_from_old_turing_model.jl b/Test_with_Turing/create_data_from_old_turing_model.jl similarity index 100% rename from Tests/create_data_from_old_turing_model.jl rename to Test_with_Turing/create_data_from_old_turing_model.jl diff --git a/Tests/test_against_previous_model.jl b/Test_with_Turing/test_against_previous_model.jl similarity index 82% rename from Tests/test_against_previous_model.jl rename to Test_with_Turing/test_against_previous_model.jl index 15189d84..0bc720a7 100644 --- a/Tests/test_against_previous_model.jl +++ b/Test_with_Turing/test_against_previous_model.jl @@ -21,7 +21,12 @@ end y = Vector{Float64}(1:10) chn1 = Turing.sample(gdemo(y),Turing.SMC(),1000) +write("tmp.jls", chn1) chn2 = Turing.sample(gdemo(y),Turing.PG(100),100) +write("tmp2.jls",chn2) chn1_old = read("Old_Model_SMC.jls", Chains) chn2_old = read("Old_Model_PG.jls", Chains) + +sum(chn1.value.data - chn1_old.value.data) +sum(chn2.value.data- chn2_old.value.data) From ee4bde193d94f89105145de1ecbd1ff66e80e0ee Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Thu, 5 Dec 2019 16:47:23 +0000 Subject: [PATCH 21/25] New Structure Core - could be used for Turing Inference - Inference outside of Turing based on AbstractMCMC --- Example/Turing_baseline.jl | 20 ++ Example/demonstarte.jl | 78 ++++---- Example/demonstrate_turing_vi.jl | 119 ++++++++++++ Example/test_interface.jl | 174 ++++-------------- Example/testfile.jl | 28 +++ Example/turing_interface.jl | 54 ++++++ Tests/tests.jl | 80 ++++++++ src/AdvancedPS.jl | 36 +++- src/Core/Algorithms/Algorithms.jl | 25 +++ .../Algorithms/Taskinfo.jl} | 17 +- src/Core/Algorithms/sample.jl | 27 +++ src/{ => Core/Container}/ParticleContainer.jl | 23 +-- src/{trace.jl => Core/Container/Trace.jl} | 4 +- src/{ => Core/Resample}/resample.jl | 0 src/{ => Core/Resample}/resample_functions.jl | 0 src/Core/Utilities/UtilityFunctions.jl | 23 +++ src/Inference/Inference.jl | 84 +++++++++ src/Inference/Model.jl | 25 +++ src/Inference/Sampler.jl | 42 +++++ src/Inference/Transitions.jl | 14 ++ src/Inference/sample_init.jl | 16 ++ src/{samplers.jl => Inference/step.jl} | 79 ++++---- src/UtilityFunctions.jl | 14 -- 23 files changed, 706 insertions(+), 276 deletions(-) create mode 100644 Example/Turing_baseline.jl create mode 100644 Example/demonstrate_turing_vi.jl create mode 100644 Example/testfile.jl create mode 100644 Example/turing_interface.jl create mode 100644 Tests/tests.jl create mode 100644 src/Core/Algorithms/Algorithms.jl rename src/{taskinfo.jl => Core/Algorithms/Taskinfo.jl} (64%) create mode 100644 src/Core/Algorithms/sample.jl rename src/{ => Core/Container}/ParticleContainer.jl (80%) rename src/{trace.jl => Core/Container/Trace.jl} (94%) rename src/{ => Core/Resample}/resample.jl (100%) rename src/{ => Core/Resample}/resample_functions.jl (100%) create mode 100644 src/Core/Utilities/UtilityFunctions.jl create mode 100644 src/Inference/Inference.jl create mode 100644 src/Inference/Model.jl create mode 100644 src/Inference/Sampler.jl create mode 100644 src/Inference/Transitions.jl create mode 100644 src/Inference/sample_init.jl rename src/{samplers.jl => Inference/step.jl} (75%) delete mode 100644 src/UtilityFunctions.jl diff --git a/Example/Turing_baseline.jl b/Example/Turing_baseline.jl new file mode 100644 index 00000000..31868bdc --- /dev/null +++ b/Example/Turing_baseline.jl @@ -0,0 +1,20 @@ +using Turing + +n = 20 + +y = Vector{Float64}(undef,n-1) +for i =1:n-1 + y[i] = sin(0.1*i) +end + +@model demo(y) = begin + x = Vector{Float64}(undef,n) + vn = @varname x[1] + x[1] ~ Normal() + for i = 2:n + x[i] ~ Normal(x[i-1],0.2) + y[i-1] ~ Normal(x[i],0.2) + end +end + +sample(demo(y),SMC(),10) diff --git a/Example/demonstarte.jl b/Example/demonstarte.jl index 86b5b217..f75f2ada 100644 --- a/Example/demonstarte.jl +++ b/Example/demonstarte.jl @@ -5,13 +5,16 @@ using AdvancedPS using Libtask const APS = AdvancedPS using Distributions -n = 20 -include("AdvancedPS/Tests/testInterface.jl") +using Plots +n = 20 +include("AdvancedPS.jl/Example/test_interface.jl") ## Our states -vi = Container(zeros(n),0) +vi = Container(zeros(n),[false for i =1:n] ,zeros(n), 0) + + ## Our observations y = Vector{Float64}(undef,n) for i = 1:n-1 @@ -24,81 +27,68 @@ end # The trace contains the variables which we want to infer using particle gibbs. # Thats all! function task_f() - var = initialize() - set_x(var,1,rand(Normal())) # We sample - report_transition(var,0.0,0.0) + + var = current_trace() + set_x!(var,1,rand(Normal())) # We sample + report_transition!(var,0.0,0.0) + for i = 2:n # Sampling - set_x(var,i, rand(Normal(get_x(var,i-1)-1,0.8))) # We sample from proposal + r = rand(Normal(get_x(var,i-1)-1,0.8)) + set_x!(var,i,r) # We sample from proposal logγ = logpdf(Normal(get_x(var,i-1)-1,0.8),get_x(var,i)) #γ(x_t|x_t-1) logp = logpdf(Normal(),get_x(var,i)) # p(x_t|x_t-1) - report_transition(var,logp,logγ) + report_transition!(var,logp,logγ) #Proposal and Resampling logpy = logpdf(Normal(get_x(var,i),0.4),y[i-1]) - var = report_observation(var,logpy) + var = report_observation!(var,logpy) end + end -task = create_task(task_f) + m = 10 +task = create_task(task_f) -particlevec = [Trace(deepcopy(vi), copy(task), PGTaskInfo(0.0,0.0)) for i =1:m] +particlevec = [Trace(vi, task, PGTaskInfo(0.0,0.0),deepcopy) for i =1:m] +particlevec ws = [0 for i =1:m] + particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),deepcopy(ws),0,0) -Algo = APS.SMCAlgorithm(APS.resample_systematic,0.0,APS.SMCUtilityFunctions(deepcopy,(x)-> x)) +Algo = APS.SMCAlgorithm(APS.resample_systematic,0.0,APS.SMCUtilityFunctions(copy_container,set_retained_vns_del_by_spl!)) ## Do one SMC step. +vi APS.sample!(particles,Algo) -particles2 = APS.ParticleContainer{typeof(vi),APS.PGTaskInfo }() - - -m = 10 -task = create_task(task_f) - - -APS.push!(particles2, 9, vi, task, PGTaskInfo(0.0,0.0)) -APS.push!(particles2, 1, particles[1].vi, task, PGTaskInfo(0.0,0.0)) -## Do one SMC step. -APS.samplePG!(particles2,resample_systematic,particles2[m]) -particles2 +particles +means = [mean([sum(particles.vals[i].vi.x[2:20]-y[1:19]) for i =1:m])] -a -function task_f_prod() - var = init() +particles - set_x(var,1,rand(Normal())) # We sample - arr = cu(rand(1000,1000)) - arr2 = cu(rand(1000,1)) - arr3 = arr*arr2 - for i = 2:n - # Sampling - set_x(var,i, rand(Normal(get_x(var,i-1)-1,0.8))) # We sample from proposal - logγ = logpdf(Normal(get_x(var,i-1)-1,0.8),get_x(var,i)) #γ(x_t|x_t-1) - logp = logpdf(Normal(),get_x(var,i)) # p(x_t|x_t-1) - report_transition(var,logp,logγ) - #Proposal and Resampling - logpy = logpdf(Normal(get_x(var,i),0.4),y[i-1]) +particlevec = [Trace(vi, task, PGTaskInfo(0.0,0.0),deepcopy) for i =1:m-1] +push!(particlevec,Trace(particles.vals[1].vi,task,PGTaskInfo(0.0,0.0),deepcopy)) +ws = [0 for i =1:m] +particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),deepcopy(ws),0,0) - var = report_observation(var,logpy) - if var.taskinfo.hold - produce(0.0) +Algo = APS.PGAlgorithm(APS.resample_systematic,0.0,APS.PGUtilityFunctions(copy_container,set_retained_vns_del_by_spl!)) +## Do one SMC step. +APS.sample!(particles,Algo) - end -end +means = [mean([sum(particles.vals[i].vi.x[2:20]-y[1:19]) for i =1:m])] diff --git a/Example/demonstrate_turing_vi.jl b/Example/demonstrate_turing_vi.jl new file mode 100644 index 00000000..f89d5c8e --- /dev/null +++ b/Example/demonstrate_turing_vi.jl @@ -0,0 +1,119 @@ + +## It is not yet a package... + +using AdvancedPS +using Libtask +const APS = AdvancedPS +using Distributions +using Plots +using Turing.Core.RandomVariables +import Turing.Core: @varname +include("AdvancedPS.jl/Example/turing_interface.jl") +import Turing: SampleFromPrior +using StatsFuns: softmax! +using AbstractMCMC +# Define a short model. +# The syntax is rather simple. Observations need to be reported with report_observation. +# Transitions must be reported using report_transition. +# The trace contains the variables which we want to infer using particle gibbs. +# Thats all! +n = 20 + +y = Vector{Float64}(undef,n-1) +for i =1:n-1 + y[i] = sin(0.1*i) +end + +function task_f() + var = initialize() + x = Vector{Float64}(undef,n) + vn = @varname x[1] + x[1] = update_var(var, vn, rand(Normal())) + report_transition!(var,0.0,0.0) + for i = 2:n + # Sampling + r = rand(Normal(x[i-1],0.2)) + vn = @varname x[i] + x[i] = update_var(var, vn, r) + logγ = logpdf(Normal(x[i-1],0.2),x[i]) #γ(x_t|x_t-1) + logp = logpdf(Normal(),x[i]) # p(x_t|x_t-1) + report_transition!(var,logp,logγ) + #Proposal and Resampling + logpy = logpdf(Normal(x[i],0.2),y[i-1]) + var = report_observation!(var,logpy) + end +end + + +task = create_task(task_f) + + + + +### This is for initilaization!! +m = 1 +vi_c = VarInfo() +task +particlevec = [APS.Trace(vi_c, task, PGTaskInfo(0.0,0.0),deepcopy) for i =1:m] +particlevec +ws = [0 for i =1:m] + +particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),zeros(m),0,0) + + +Algo = APS.SMCAlgorithm(APS.resample_systematic,1.0,APS.SMCUtilityFunctions(deepcopy,set_retained_vns_del_by_spl!)) +## Do one SMC step. + +@elapsed APS.sample!(particles,Algo) + +vi_c = empty!(VarInfo{<:NamedTuple}(particles[1].vi)) + +const vi_ct = vi_c + + +m = 200 +task = create_task(task_f) +task +particlevec = [APS.Trace(vi_c, task, PGTaskInfo(0.0,0.0),deepcopy) for i =1:m] +particlevec +ws = [0 for i =1:m] + +particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),zeros(m),0,0) + + +Algo = APS.SMCAlgorithm(APS.resample_systematic,0.5,APS.SMCUtilityFunctions(deepcopy,set_retained_vns_del_by_spl!)) +## Do one SMC step. + + +@elapsed APS.sample!(particles,Algo) + + + +particles + +#means = [mean([sum(particles.vals[i].vi.vals[2:20]-y[1:19])^2 for i =1:m])] + + +set_retained_vns_del_by_spl!(vi_c, SampleFromPrior()) +for l = 2:10 + global particles + indx = AdvancedPS.randcat(softmax!(copy(particles.logWs))) + + println(indx) + particlevec = [Trace(vi_c, task, PGTaskInfo(0.0,0.0),deepcopy) for i =1:m] + + #push!(particlevec,Trace(particles.vals[indx].vi, task, PGTaskInfo(0.0,0.0), deepcopy)) + ws = [0 for i =1:m] + particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),deepcopy(ws),0,0) + Algo = APS.PGAlgorithm(APS.resample_systematic,1.0,APS.SMCUtilityFunctions(deepcopy,set_retained_vns_del_by_spl!)) + ## Do one SMC step. + APS.sample!(particles,Algo,particles[indx]) + + push!(means, mean([sum(particles.vals[i].vi.vals[2:20]-y[1:19])^2 for i =1:m])) +end + +particles + +plot(means) + +means diff --git a/Example/test_interface.jl b/Example/test_interface.jl index c5dd3ae6..2327b3f2 100644 --- a/Example/test_interface.jl +++ b/Example/test_interface.jl @@ -1,177 +1,69 @@ - - - - ## Some function which make the model easier to define. - - - - - - - - - # A very light weight container for a state space model - - - -# The state space is one dimensional! - - - +# The state space is one dimensional # Note that this is only for a very simple demonstration. +# This is a very shallow container solely for testing puropose mutable struct Container - - - - x::Vector{Float64} - - - + x::Array{Float64,1} + marked::Vector{Bool} + produced_at::Vector{Int64} num_produce::Float64 - - - end - - - - - - -# This is important for initalizaiton - - - -function initialize() - - - - return current_trace() - - - +function set_retained_vns_del_by_spl!(container::Container) + for i in 1:length(container.marked) + if container.marked[i] + if container.produced_at[i] >= container.num_produce + container.marked[i] = false + end + end + end + container end +# This is important for initalizaiton +const initialize = current_trace - - - -# Th - - - -function report_observation(trace, logp::Float64) - - - +function report_observation!(trace, logp::Float64) trace.taskinfo.logp += logp - - - + trace.vi.num_produce += 1 produce(logp) - - - - current_trace() - - - + trace = current_trace() end - - - - - - # logγ corresponds to the proposal distributoin we are sampling from. - - - -function report_transition(trace,logp::Float64,logγ::Float64) - - - +function report_transition!(trace,logp::Float64,logγ::Float64) trace.taskinfo.logp += logp - logγ - - - trace.taskinfo.logpseq += logp - - - end - - - - - - function get_x(trace,indx) - - - - return @inbounds trace.vi.x[indx] - - - + @assert trace.vi.marked[indx] "[Interface] This should already be marked!" + return trace.vi.x[indx] end - - -function set_x(trace,indx,val) - - - - return @inbounds trace.vi.x[indx] = val - - - +# We only set it if it is not yet marked +function set_x!(trace,indx,val) + if !trace.vi.marked[indx] + trace.vi.x[indx] = val + trace.vi.marked[indx] = true + trace.vi.produced_at[indx] = trace.vi.num_produce + end + trace end - - - - - - - - - - -function copyC(vi::Container) - - - - Container(deepcopy(vi.x),vi.num_produce) - - - +# The reason for this is that we need to pass it! +function copy_container(vi::Container) + Container(deepcopy(vi.x),deepcopy(vi.marked),deepcopy(vi.produced_at),copy(vi.num_produce)) end - - - - - - function create_task(f::Function) - - - return CTask(() -> begin new_vi=f(); produce(Val{:done}); new_vi; end ) - - - end diff --git a/Example/testfile.jl b/Example/testfile.jl new file mode 100644 index 00000000..ab2b1be3 --- /dev/null +++ b/Example/testfile.jl @@ -0,0 +1,28 @@ + +v = VarInfo() + +vn = @varname hi + +push!(v,vn,0.8,Normal()) + +v[vn] = [1.2] + +v2 = VarInfo(v) + +typeof(v) <: UntypedVarInfo +typeof(v2) <:UntypedVarInfo + +v2.num_produce = 0 + +fl = Vector{Float64}(undef,1) +fl[1] = 4.0 +setindex!(v2,fl,vn) +v2 + +v2 = VarInfo{<:NamedTuple}(v) + +v2 = TypedVarInfo(v) + +v2[vn] = [3.0] + +empty!(v2) diff --git a/Example/turing_interface.jl b/Example/turing_interface.jl new file mode 100644 index 00000000..2aec914d --- /dev/null +++ b/Example/turing_interface.jl @@ -0,0 +1,54 @@ +## Some function which make the model easier to define. + + + + +# This is important for initalizaiton +const initialize = AdvancedPS.current_trace + +function report_observation!(trace, logp::Float64) + trace.taskinfo.logp += logp + produce(logp) + trace = AdvancedPS.current_trace() +end + +# logγ corresponds to the proposal distributoin we are sampling from. +function report_transition!(trace, logp::Float64, logγ::Float64) + trace.taskinfo.logp += logp - logγ + trace.taskinfo.logpseq += logp +end + + + +function in(sym::Symbol, vns::Vector{VarName})::Bool + +end + + +# We obtain the new value for our variable +# If the distribution is not specified, we simply set it to be Normal +function update_var(trace, vn, val, dist= Normal()) + + # check if the symbol is contained in the varinfo... + if haskey(trace.vi,vn) + if is_flagged(trace.vi, vn, "del") + unset_flag!(vi, vn, "del") + trace.vi[vn] = val + setgid!(vi, spl.selector, vn) + setorder!(vi, vn, vi.num_produce) + return val + else + val = trace.vi[vn] + end + else + #We do not specify the distribution... Thats why we set it to be Normal() + push!(trace.vi, vn, val, dist) + end + + return val +end + +# The reason for this is that we need to pass it! +function create_task(f::Function) + return CTask(() -> begin new_vi=f(); produce(Val{:done}); new_vi; end ) +end diff --git a/Tests/tests.jl b/Tests/tests.jl new file mode 100644 index 00000000..0fe852b7 --- /dev/null +++ b/Tests/tests.jl @@ -0,0 +1,80 @@ +using Test + + + +dir = splitdir(splitdir(pathof(Turing))[1])[1] + +include(dir*"/test/test_utils/AllUtils.jl") + + +@testset "container.jl" begin + @turing_testset "copy particle container" begin + pc = ParticleContainer(x -> x * x, Trace[]) + newpc = copy(pc) + @test newpc.logE == pc.logE + @test newpc.logWs == pc.logWs + @test newpc.n_consume == pc.n_consume + @test typeof(pc) === typeof(newpc) + end + @turing_testset "particle container" begin + n = Ref(0) + alg = PG(5) + spl = Turing.Sampler(alg, empty_model()) + dist = Normal(0, 1) + function fpc() + t = TArray(Float64, 1); + t[1] = 0; + while true + ct = current_trace() + vn = VarName(gensym(), :x, "[$n]", 1) + Turing.assume(spl, dist, vn, ct.vi); n[] += 1; + produce(0) + vn = VarName(gensym(), :x, "[$n]", 1) + Turing.assume(spl, dist, vn, ct.vi); n[] += 1; + t[1] = 1 + t[1] + end + end + model = Turing.Model{(:x,),()}(fpc, NamedTuple(), NamedTuple()) + particles = [Trace(fpc, model, spl, Turing.VarInfo()) for _ in 1:3] + pc = ParticleContainer(fpc, particles) + @test weights(pc) == [1/3, 1/3, 1/3] + @test logZ(pc) ≈ log(3) + @test pc.logE ≈ log(1) + @test consume(pc) == log(1) + resample!(pc) + @test weights(pc) == [1/3, 1/3, 1/3] + @test logZ(pc) ≈ log(3) + @test pc.logE ≈ log(1) + @test effectiveSampleSize(pc) == 3 + @test consume(pc) ≈ log(1) + resample!(pc) + @test consume(pc) ≈ log(1) + end + @turing_testset "trace" begin + n = Ref(0) + alg = PG(5) + spl = Turing.Sampler(alg, empty_model()) + dist = Normal(0, 1) + function f2() + t = Turray(Int, 1); + t[1] = 0; + while true + ct = current_trace() + vn = VarName(gensym(), :x, "[$n]", 1) + Turing.assume(spl, dist, vn, ct.vi); n[] += 1; + produce(t[1]); + vn = VarName(gensym(), :x, "[$n]", 1) + Turing.assume(spl, dist, vn, ct.vi); n[] += 1; + t[1] = 1 + t[1] + end + end + # Test task copy version of trace + model = Turing.Model{(:x,),()}(f2, NamedTuple(), NamedTuple()) + tr = Trace(f2, model, spl, Turing.VarInfo()) + consume(tr); consume(tr) + a = fork(tr); + consume(a); consume(a) + @test consume(tr) == 2 + @test consume(a) == 4 + end +end diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index 6798775a..f01394f2 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -4,9 +4,10 @@ module AdvancedPS - using Libtask using StatsFuns: logsumexp, softmax! + using AdvancedMCMC + import MCMCChains: Chains import Base.copy abstract type AbstractTaskInfo end @@ -15,18 +16,24 @@ module AdvancedPS abstract type AbstractPFAlgorithm end abstract type AbstractPFUtilitFunctions end + abstract type AbstractPFTransition <: AbstractTransition end + abstract type AbstractPFSampler <: AbstractSampler end + abstract type AbstractSMCUtilitFunctions <: AbstractPFUtilitFunctions end abstract type AbstractPGASUtilityFunctions <: AbstractSMCUtilitFunctions end + abstract type AbstractPFModel <: AbstractModel end + - include("UtilityFunctions.jl") - include("trace.jl") - include("ParticleContainer.jl") - include("resample.jl") - include("taskinfo.jl") - include("samplers.jl") - include("resample_functions.jl") + include("Core/Container/trace.jl") + include("Core/Container/ParticleContainer.jl") + include("Core/Resample/resample.jl") + include("Core/Algorithms/taskinfo.jl") + include("Core/Algorithms/sample.jl") + include("Core/Resample/resample_functions.jl") + include("Core/Algorithms/Algorithms.jl") + include("Core/Utilities/UtilityFunctions.jl") export ParticleContainer, Trace, @@ -51,6 +58,19 @@ module AdvancedPS SMCAlgorithm, PGAlgorithm + include("Inference/Model.jl") + include("Inference/sample_init.jl") + include("Inference/Sampler.jl") + inlcude("Inference/step.jl") + include("Inference/Transitions.jl") + include("Inference/Inference.jl") + export PFModel, + sample, + bundle_samples, + sample_init!, + step!, + Sampler, + PFTransition end # module diff --git a/src/Core/Algorithms/Algorithms.jl b/src/Core/Algorithms/Algorithms.jl new file mode 100644 index 00000000..ed9a9bac --- /dev/null +++ b/src/Core/Algorithms/Algorithms.jl @@ -0,0 +1,25 @@ + +struct SMCAlgorithm{RT} <: AbstractPFAlgorithm where RT<:AbstractFloat + resampler :: Function + resampler_threshold :: RT + +end + +function SMCAlgorithm() + SMCAlgorithm(resample_systematic, 0.5) +end + + +const PGAlgorithm = SMCAlgorithm + + +struct PGASAlgorithm{RT} <: AbstractPFAlgorithm + resampler :: Function + resampler_threshold :: RT + merge_traj :: Function + n :: Int64 +end + +function PGASAlgorithm(merge_traj::Function, n::Int64) + SMCAlgorithm(resample_systematic, 0.5, merge_traj, n) +end diff --git a/src/taskinfo.jl b/src/Core/Algorithms/Taskinfo.jl similarity index 64% rename from src/taskinfo.jl rename to src/Core/Algorithms/Taskinfo.jl index 98aa3592..e35c9a10 100644 --- a/src/taskinfo.jl +++ b/src/Core/Algorithms/Taskinfo.jl @@ -4,7 +4,7 @@ # allows to propagate information trough the computation. # This is important because we do not want to -mutable struct PGTaskInfo{T} <: AbstractTaskInfo where {T <:AbstractFloat} +mutable struct SMCTaskInfo{T} <: AbstractTaskInfo where {T <:AbstractFloat} # This corresponds to p(yₜ | xₜ) p(xₜ | xₜ₋₁) / γ(xₜ | xₜ₋₁, yₜ) # where γ is the porposal. # We need this variable to compute the weights! @@ -15,24 +15,29 @@ mutable struct PGTaskInfo{T} <: AbstractTaskInfo where {T <:AbstractFloat} logpseq::T end +SMCTaskInfo() = SMCTaskInfo(0.0, 0.0) +const PGTaskInfo = SMCTaskInfo -mutable struct PGASTaskInfo{T} <: AbstractTaskInfo where {T <: AbstractFloat} + + +mutable struct PGASTaskInfo{T, F} <: AbstractTaskInfo where {T <: AbstractFloat} # Same as above logp::T logpseq::T - # This is important for ancesotr sampling to synchronize. - hold::Bool + # The ancestor weight + ancestor_weight::Float64 end function PGASTaskInfo(logp::Float64,logpseq::Float64) - PGASTaskInfo(logp,logpseq,false) + PGASTaskInfo(logp, logpseq, 0.0) end function Base.copy(info::PGTaskInfo) PGTaskInfo(info.logp, info.logpseq) end function Base.copy(info::PGASTaskInfo) - PGASTaskInfo(info.logp, info.logpseq,info.hold) + PGASTaskInfo(info.logp, info.logpseq, info.ancestor_weight) end reset_logp!(ti::AbstractTaskInfo) = (ti.logp = 0.0) +set_ancestor_weight!(ti::PGASTaskInfo, w::Float64) = (ti.ancestor_weight = w) diff --git a/src/Core/Algorithms/sample.jl b/src/Core/Algorithms/sample.jl new file mode 100644 index 00000000..5779cb48 --- /dev/null +++ b/src/Core/Algorithms/sample.jl @@ -0,0 +1,27 @@ + + +## This is all we need for Turing! + +function sample!(pc::PC, alg::ALG, utility_functions::AbstractSMCUtilitFunctions, ref_traj::T) where { + PC <: ParticleContainer, + ALG <: Union{SMCAlgorithm, PGAlgoirhtm} + T <: Trace +} + n = length(pc.vals) + while consume(pc) != Val{:done} + if ref_traj !== nothing || ess <= alg.resampler_threshold * length(pc) + # compute weights + Ws = weights(pc) + # check that weights are not NaN + @assert !any(isnan, Ws) + # sample ancestor indices + # Ancestor trajectory is not sampled + nresamples = n-1 + indx = alg.resampler(Ws, nresamples) + # We add ancestor trajectory to the path. + # For ancestor sampling, we would change n at this point. + push!(indx,n) + resample!(pc, utility_functions, indx, ref_traj) + end + end +end diff --git a/src/ParticleContainer.jl b/src/Core/Container/ParticleContainer.jl similarity index 80% rename from src/ParticleContainer.jl rename to src/Core/Container/ParticleContainer.jl index ed2945ca..dcfda86e 100644 --- a/src/ParticleContainer.jl +++ b/src/Core/Container/ParticleContainer.jl @@ -9,9 +9,6 @@ mutable struct ParticleContainer{T} <: AbstractParticleContainer where T<:Trace vals::Vector{T} # A named tuple with functions to manipulate the particles vi. logWs::Vector{Float64} - #This corresponds to log p(x_{0:t}), therefore the log likelihood of the transitions up to this point. - #Especially important for ancestor sampling. - logpseq::Vector{Float64} # log model evidence logE::Float64 # helpful for rejuvenation steps, e.g. in SMC2 @@ -19,6 +16,10 @@ mutable struct ParticleContainer{T} <: AbstractParticleContainer where T<:Trace end +ParticleContainer(particles::Vector{<:Trace}) = + ParticleContainer(particles, zeros(length(particles)), 0.0, 0) + + Base.collect(pc :: ParticleContainer) = pc.vals # prev: Dict, now: Array Base.length(pc :: ParticleContainer) = length(pc.vals) # pc[i] returns the i'th particle @@ -30,7 +31,6 @@ Base.@propagate_inbounds Base.getindex(pc::ParticleContainer, i::Int) = pc.vals[ function Base.push!(pc::ParticleContainer, p::Particle) push!(pc.vals, p) push!(pc.logWs, 0.0) - push!(pc.logpseq,0.0) return pc end @@ -38,7 +38,6 @@ end function Base.empty!(pc::ParticleContainer) pc.vals = eltype(pc.vals)[] pc.logWs = Float64[] - pc.logpseq = Float64[] return pc end @@ -48,8 +47,7 @@ function Base.copy(pc::ParticleContainer, utility_functions::AbstractPFUtilitFun vals = eltype(pc.vals)[fork(p, utility_functions.copy) for p in pc.vals] # copy weights logWs = copy(pc.logWs) - logpseq = copy(pc.logpseq) - ParticleContainer( vals, logWs, logpseq, pc.logE, pc.n_consume) + ParticleContainer( vals, logWs, pc.logE, pc.n_consume) end # run particle filter for one step, return incremental likelihood @@ -63,13 +61,10 @@ function Libtask.consume(pc::ParticleContainer) for i=1:n p = particles[i] score = Libtask.consume(p) - if hasproperty(typeof(p.taskinfo), logpseq) - set_logpseq!(pc, i, p.taskinfo.logpseq) - end if score isa Real score += p.taskinfo.logp reset_logp!(p.taskinfo) - increase_logweight(pc, i, Float64(score)) + increase_logweight!(pc, i, Float64(score)) elseif score == Val{:done} num_done += 1 else @@ -85,7 +80,7 @@ function Libtask.consume(pc::ParticleContainer) else # update incremental likelihoods z2 = logZ(pc) - res = increase_logevidence(pc, z2 - z1) + res = increase_logevidence!(pc, z2 - z1) pc.n_consume += 1 # res = increase_loglikelihood(pc, z2 - z1) end @@ -94,7 +89,7 @@ end # compute the normalized weights -weights(pc::ParticleContainer) = softmax(pc.logWs) +weights(pc::ParticleContainer) = softmax!(copy(pc.logWs)) # There exists only softmax! # compute the log-likelihood estimate, ignoring constant term ``- \log num_particles`` logZ(pc::ParticleContainer) = logsumexp(pc.logWs) @@ -107,6 +102,4 @@ end increase_logweight!(pc::ParticleContainer, t::Int, logw::Float64) = (pc.logWs[t] += logw) -set_logpseq!(pc::ParticleContainer, t::Int, logp::Float64) = (pc.logpseq[t] = logp) - increase_logevidence!(pc::ParticleContainer, logw::Float64) = (pc.logE += logw) diff --git a/src/trace.jl b/src/Core/Container/Trace.jl similarity index 94% rename from src/trace.jl rename to src/Core/Container/Trace.jl index bc87bd8a..a53a2776 100644 --- a/src/trace.jl +++ b/src/Core/Container/Trace.jl @@ -10,12 +10,12 @@ mutable struct Trace{Tvi, TInfo} <: AbstractTrace where {Tvi, TInfo <: Abstract end function Base.copy(trace::Trace{Tvi,TInfo}, copy_vi::Function) where {Tvi, TInfo <: AbstractTaskInfo} - return Trace(copy_vi(trace.vi), copy(trace.task), copy(trace.taskinfo)) + return Trace(trace.vi, trace.task, trace.taskinfo,copy_vi) end # The procedure passes a function which is specified by the model. -function Trace(f::Function, vi, taskinfo, copy_vi::Function) +function Trace( vi, f::Function, taskinfo, copy_vi::Function) task = CTask( () -> begin res=f(); produce(Val{:done}); res; end ) res = Trace(copy_vi(vi), task, copy(taskinfo)) diff --git a/src/resample.jl b/src/Core/Resample/resample.jl similarity index 100% rename from src/resample.jl rename to src/Core/Resample/resample.jl diff --git a/src/resample_functions.jl b/src/Core/Resample/resample_functions.jl similarity index 100% rename from src/resample_functions.jl rename to src/Core/Resample/resample_functions.jl diff --git a/src/Core/Utilities/UtilityFunctions.jl b/src/Core/Utilities/UtilityFunctions.jl new file mode 100644 index 00000000..7657408d --- /dev/null +++ b/src/Core/Utilities/UtilityFunctions.jl @@ -0,0 +1,23 @@ + + +abstract type AbstractSMCUtilitFunctions <: AbstractPFUtilitFunctions end +abstract type AbstractPGASUtilityFunctions <: AbstractSMCUtilitFunctions end + + +struct SMCUtilityFunctions<:AbstractSMCUtilitFunctions + copy :: Function + set_retained_vns_del_by_spl! :: Function + empty! :: Function + to_named_tuple :: Function +end + +const PGUtilityFunctions = SMCUtilityFunctions + +struct PGASUtilityFunctions{AP}<:AbstractPGASUtilityFunctions + copy :: Function + set_retained_vns_del_by_spl! :: Function + merge_traj :: Function + empty! :: Function + to_named_tuple :: Functino + +end diff --git a/src/Inference/Inference.jl b/src/Inference/Inference.jl new file mode 100644 index 00000000..9ed63075 --- /dev/null +++ b/src/Inference/Inference.jl @@ -0,0 +1,84 @@ + + +function AbstractMCMC.sample( + model::ModelType, + alg::AlgType, + uf::UF, + vi::T, + N::Integer; + kwargs... +) where { + ModelType<:AbstractPFModel, + AlgType<: AbstractPFAlgorithm, + UF<:AbstractPFUtilitFunctions +} + return sample(model, Sampler(alg, uf, vi), N; progress=PROGRESS[], kwargs...) +end + + +function AbstractMCMC.bundle_samples( + rng::AbstractRNG, + model::ModelType, + spl::AbstractPFSampler, + N::Integer, + ts::Vector{T}; + discard_adapt::Bool=true, + save_state=false, + kwargs... +) where {ModelType<:AbstractPFModel, T<:AbstractPFTransition} + + # Convert transitions to array format. + # Also retrieve the variable names. + nms, vals = _params_to_array(ts) + + + # Get the values of the extra parameters in each Transition struct. + + extra_params, extra_values = get_transition_extras(ts) + # Extract names & construct param array. + nms = string.(vcat(nms..., string.(extra_params)...)) + parray = hcat(vals, extra_values) + + + # Get the values of the extra parameters in each Transition struct. + extra_params, extra_values = get_transition_extras(ts) + le = missing + # Set up the info tuple. + info = NamedTuple() + # Chain construction. + return Chains( + parray, + string.(nms), + deepcopy(TURING_INTERNAL_VARS); + evidence=le, + info=info + ) +end + + +function _params_to_array(ts::Vector{T}) where {T<:AbstractTransition} + names = Set{String}() + dicts = Vector{Dict{String, Any}}() + # Extract the parameter names and values from each transition. + for t in ts + nms, vs = flatten_namedtuple(t.θ) + push!(names, nms...) + # Convert the names and values to a single dictionary. + d = Dict{String, Any}() + for (k, v) in zip(nms, vs) + d[k] = v + end + push!(dicts, d) + end + # Convert the set to an ordered vector so the parameter ordering + # is deterministic. + ordered_names = collect(names) + vals = Matrix{Union{Real, Missing}}(undef, length(ts), length(ordered_names)) + # Place each element of all dicts into the returned value matrix. + for i in eachindex(dicts) + for (j, key) in enumerate(ordered_names) + vals[i,j] = get(dicts[i], key, missing) + end + end + return ordered_names, vals +end diff --git a/src/Inference/Model.jl b/src/Inference/Model.jl new file mode 100644 index 00000000..a6a74150 --- /dev/null +++ b/src/Inference/Model.jl @@ -0,0 +1,25 @@ + +### + +### The model + +### + + +struct PFModel <: AbstractPFModel + task::Task +end + +function PFModel(f::Function, args) + task = create_task(f,args) + PFModel(task) +end + + +function create_task(f::Function, args=nothing) + if args === nothing + return CTask(() -> begin new_vi=f(); produce(Val{:done}); new_vi; end ) + else + return CTask(() -> begin new_vi=f(args...); produce(Val{:done}); new_vi; end ) + end +end diff --git a/src/Inference/Sampler.jl b/src/Inference/Sampler.jl new file mode 100644 index 00000000..7245999f --- /dev/null +++ b/src/Inference/Sampler.jl @@ -0,0 +1,42 @@ + +abstract type AbstractPFSampler <: AbstractSampler end + +# The particle container is our state, + +struct SMCSampler{PC, ALG, UF, C} <: AbstractPFSampler where {PC<:ParticleContainer, ALG<:SMCAlgorithm, UF<:SMCUtilityFunctions} + pc :: PC + alg :: ALG + uf :: UF + vi :: C +end + + +function Sampler(alg:: ALG, uf::UF, vi::T) where { + ALG<: SMCAlgorithm, + UF<: SMCUtilityFunctions, +} + pc = ParticleContainer(Trace{typeof(vi),SMCTaskInfo}[]) + SMCSampler(pc, alg, uf, uf.empty!(vi)) +end + + + + +struct PGSampler{T, ALG, UF, C} <: AbstractPFSampler where { + T <:Particle, + ALG<:SMCAlgorithm, + UF<:AbstractUtilityFunctions +} + + alg :: ALG + uf :: UF + ref_traj :: Union{T, Nothing} + vi :: C +end + + +Sampler(alg:: ALG, uf::UF, vi::T) where { + ALG<: PGAlgorithm, + UF<: AbstractUtilityFunctions, + M <:AbstractPFModel +} = PGSampler(pc, alg, uf, nothing) diff --git a/src/Inference/Transitions.jl b/src/Inference/Transitions.jl new file mode 100644 index 00000000..e3cb0c96 --- /dev/null +++ b/src/Inference/Transitions.jl @@ -0,0 +1,14 @@ + + + +struct PFTransition{T, F<:AbstractFloat} <: AbstractPFTransition + θ::T + lp::F + le::F + weight::Vector{F} +end + +transition_type(spl::Sampler{<:ParticleInference}) = ParticleTransition +function additional_parameters(::Type{<:ParticleTransition}) + return [:lp,:le, :weight] +end diff --git a/src/Inference/sample_init.jl b/src/Inference/sample_init.jl new file mode 100644 index 00000000..5153880d --- /dev/null +++ b/src/Inference/sample_init.jl @@ -0,0 +1,16 @@ +function sample_init!( + rng::AbstractRNG, + ℓ::ModelType, + s::SamplerType, + N::Integer; + debug::Bool=false, + kwargs... +) where {ModelType<:AbstractPFModel, SamplerType<:SMCSampler} + + T = Trace{typeof(spl.vi),SMCTaskInfo} + particles = T[ Trace(vi, task, SMCTaskInfo(), alg.copy) for _ =1:N] + spl.pc = APS.ParticleContainer{typeof(particles[1])}(particles,zeros(N),zeros(N),0,0) + + sample!(spl.pc, spl.alg, spl.uf, nothing) + +end diff --git a/src/samplers.jl b/src/Inference/step.jl similarity index 75% rename from src/samplers.jl rename to src/Inference/step.jl index ccf7507b..a8000de0 100644 --- a/src/samplers.jl +++ b/src/Inference/step.jl @@ -1,59 +1,46 @@ -struct SMCAlgorithm{RT, UF<:AbstractSMCUtilitFunctions} <: AbstractPFAlgorithm where RT<:AbstractFloat - resampler :: Function - resampler_threshold :: RT - utility_functiions :: UF -end +#SMC step +function step!( + ::AbstractRNG, + model::Turing.Model, + spl::T, + i::Integer; + kwargs... + ) where T <: SMCSampler -struct PGAlgorithm{RT, UF<:AbstractSMCUtilitFunctions} <: AbstractPFAlgorithm where RT<:AbstractFloat - resampler :: Function - resampler_threshold :: RT - utility_functiions :: UF + particle = spl.pc[i] + params = spl.uf.to_named_tuple(spl.pc[i]) + return PFTransition(params, particle.taskinfo.logp, pc.logE, Ws[i]) end -function sample!(pc::ParticleContainer, spl::SMCAlgorithm) - while consume(pc) != Val{:done} - ess = effectiveSampleSize(pc) - if ess <= spl.resampler_threshold * length(pc) - # compute weights - Ws = weights(pc) - # check that weights are not NaN - @assert !any(isnan, Ws) - # sample ancestor indices - n = length(pc) - nresamples = n - indx = spl.resampler(Ws, nresamples) - resample!(pc, spl.utility_functions, indx) - end - end -end +# PG step +function step!( + ::AbstractRNG, + model::Turing.Model, + spl::T, + ::Integer; + kwargs... + ) where T <: PGSampler -# The resampler threshold is only imprtant for the first step! -function sample!(pc::ParticleContainer, spl::PGAlgorithm, ref_traj::Union{Particle,Nothing}=nothing) + n = alg.n + T = Trace{typeof(spl.vi),SMCTaskInfo} - if spl.ref_traj === nothing - # We do not have a reference trajectory yet, therefore, perform normal SMC! - # At this point it is important that we have this hirarchical structure for the utility function struct. - sampleSMC!(pc, SMCAlgorithm(spl.resampler, spl.resampler_threshold, spl.utility_functions)) + if hasfield(spl,:ref_traj) && spl.ref_traj !== nothing + particles = T[ Trace(vi, task, SMCTaskInfo(), alg.copy) for _ =1:n-1] + pc = APS.ParticleContainer{typeof(particles[1])}(particles,zeros(n),zeros(n),0,0) + push!(pc, spl.ref_traj) else - while consume(pc) != Val{:done} - # compute weights - Ws = weights(pc) - # check that weights are not NaN - @assert !any(isnan, Ws) - # sample ancestor indices - n = length(pc) - # Ancestor trajectory is not sampled - nresamples = n-1 - indx = resampler(Ws, nresamples) - # We add ancestor trajectory to the path. - # For ancestor sampling, we would change n at this point. - push!(indx,n) - resample!(pc, spl.utility_functions, indx, ref_traj) - end + particles = T[ Trace(vi, task, SMCTaskInfo(), alg.copy) for _ =1:n] + pc = APS.ParticleContainer{typeof(particles[1])}(particles,zeros(n),zeros(n),0,0) end + sample!(pc, spl.alg, spl.uf, spl.ref_traj) + + indx = AdvancedPS.randcat(weights(pc)) + particle = spl.ref_traj = pc[indx] + params = spl.uf.to_named_tuple(spl.pc[i]) + return PFTransition(params, particle.taskinfo.logp, pc.logE, Ws[i]) end # diff --git a/src/UtilityFunctions.jl b/src/UtilityFunctions.jl deleted file mode 100644 index d8a36533..00000000 --- a/src/UtilityFunctions.jl +++ /dev/null @@ -1,14 +0,0 @@ - -struct SMCUtilityFunctions<:AbstractSMCUtilitFunctions - copy :: Function - set_retained_vns_del_by_spl :: Function -end - -const PGUtilityFunctions = SMCUtilityFunctions - -struct PGASUtilityFunctions{AP}<:AbstractPGASUtilityFunctions where AP<:Union{Function,Nothing} - copy :: Function - set_retained_vns_del_by_spl :: Function - merge_traj :: Function - ancestor_proposal :: AP -end From 8636a86522f68767bf5b62c5d37df58e1efc0f0b Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Thu, 5 Dec 2019 22:41:16 +0000 Subject: [PATCH 22/25] corrections --- Example/demonstrate_with_Turing_VI.jl | 108 +++++++++++++ Example/turing_interface.jl | 144 +++++++++++++++--- {Example => Old_Examples}/demonstarte.jl | 0 {Example => Old_Examples}/demonstartePGAS.jl | 4 +- .../demonstrate_turing_vi.jl | 0 {Example => Old_Examples}/test_interface.jl | 0 {Example => Old_Examples}/testfile.jl | 0 Tests/test_utils/AllUtils.jl | 9 ++ Tests/test_utils/ad_utils.jl | 77 ++++++++++ Tests/test_utils/models.jl | 53 +++++++ Tests/test_utils/numerical_tests.jl | 66 ++++++++ Tests/test_utils/random_measure_utils.jl | 36 +++++ Tests/test_utils/staging.jl | 52 +++++++ Tests/test_utils/testing_functions.jl | 73 +++++++++ Tests/tests.jl | 16 +- src/AdvancedPS.jl | 33 ++-- src/Core/Algorithms/Algorithms.jl | 10 +- src/Core/Algorithms/sample.jl | 12 +- src/Core/Container/Trace.jl | 24 +-- src/Core/Utilities/UtilityFunctions.jl | 9 +- src/Inference/Inference.jl | 49 ++---- src/Inference/Sampler.jl | 25 +-- src/Inference/Transitions.jl | 122 ++++++++++++++- src/Inference/sample_init.jl | 10 +- src/Inference/step.jl | 46 +++--- 25 files changed, 851 insertions(+), 127 deletions(-) create mode 100644 Example/demonstrate_with_Turing_VI.jl rename {Example => Old_Examples}/demonstarte.jl (100%) rename {Example => Old_Examples}/demonstartePGAS.jl (98%) rename {Example => Old_Examples}/demonstrate_turing_vi.jl (100%) rename {Example => Old_Examples}/test_interface.jl (100%) rename {Example => Old_Examples}/testfile.jl (100%) create mode 100644 Tests/test_utils/AllUtils.jl create mode 100644 Tests/test_utils/ad_utils.jl create mode 100644 Tests/test_utils/models.jl create mode 100644 Tests/test_utils/numerical_tests.jl create mode 100644 Tests/test_utils/random_measure_utils.jl create mode 100644 Tests/test_utils/staging.jl create mode 100644 Tests/test_utils/testing_functions.jl diff --git a/Example/demonstrate_with_Turing_VI.jl b/Example/demonstrate_with_Turing_VI.jl new file mode 100644 index 00000000..f6151e32 --- /dev/null +++ b/Example/demonstrate_with_Turing_VI.jl @@ -0,0 +1,108 @@ + +## It is not yet a package... +using Distributions +using Turing.Core.RandomVariables +import Turing.Core: @varname +import Turing.Utilities: vectorize +using Turing +using Revise +using AdvancedPS +import Turing.Core: tonamedtuple +include("AdvancedPS/Example/turing_interface.jl") +# Define a short model. +# The syntax is rather simple. Observations need to be reported with report_observation. +# Transitions must be reported using report_transition. +# The trace contains the variables which we want to infer using particle gibbs. +# Thats all! +n = 20 + +y = Vector{Float64}(undef,n-1) +for i =1:n-1 + y[i] = 0 +end + +function task_f() + var = initialize() + x = Vector{Float64}(undef,n) + vn = @varname x[1] + x[1] = update_var(var, vn, rand(Normal())) + report_transition!(var,0.0,0.0) + for i = 2:n + # Sampling + r = rand(Normal()) + vn = @varname x[i] + x[i] = update_var(var, vn, r) + logγ = logpdf(Normal(),x[i]) #γ(x_t|x_t-1) + logp = logpdf(Normal(),x[i]) # p(x_t|x_t-1) + report_transition!(var,logp,logγ) + #Proposal and Resampling + logpy = logpdf(Normal(x[i], 1.0), y[i-1]) + var = report_observation!(var,logpy) + end +end + + + +task = create_task(task_f) + +model = PFModel(task) + +tonamedtuple(vi::UntypedVarInfo) = tonamedtuple(TypedVarInfo(vi)) + +alg = AdvancedPS.SMCAlgorithm() +uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) +container = VarInfo() + +chn =sample(model, alg, uf, container, 2000) + + + + +alg = AdvancedPS.PGAlgorithm(5) +uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) +n = 5 +T = Trace{typeof(container), PGTaskInfo{Float64}} + +# if hasfield(typeof(container),:ref_traj) && spl.ref_traj !== nothing +# particles = T[ Trace(container, model.task, SMCTaskInfo(), uf.copy) for _ =1:n-1] +# pc = ParticleContainer{typeof(particles[1])}(particles,zeros(n),0.0,0) +# +# # Reset Task +# spl.ref_traj.task = copy(model.task) +# +# push!(pc, .ref_traj) +# else +particles = T[ Trace(container, model.task, PGTaskInfo(), uf.copy) for _ =1:n] +pc = ParticleContainer{typeof(particles[1])}(particles,zeros(n),0.0,0) + + +AdvancedPS.sample!(pc, alg, uf, nothing) + +ref_traj = pc[1] + +particles = T[ Trace(container, model.task, PGTaskInfo(), uf.copy) for _ =1:n-1] +pc = ParticleContainer{typeof(particles[1])}(particles,zeros(n),0.0,0) + +# Reset Task +ref_traj = AdvancedPS.forkr(ref_traj, uf.copy) + +push!(pc, ref_traj) +pc + +AdvancedPS.sample!(pc, alg, uf, ref_traj) + +container +vn = @varname vi +push!(container,vn,3.0,Normal()) +container.metadata.flags + + + +pc + + + +indx = AdvancedPS.randcat(weights(pc)) +particle = spl.ref_traj = pc[indx] +params = spl.uf.tonamedtuple(particle.vi) +return PFTransition(params, particle.taskinfo.logp, pc.logE, weights(pc)[indx]) diff --git a/Example/turing_interface.jl b/Example/turing_interface.jl index 2aec914d..145f0c7d 100644 --- a/Example/turing_interface.jl +++ b/Example/turing_interface.jl @@ -1,10 +1,6 @@ -## Some function which make the model easier to define. - - - - # This is important for initalizaiton const initialize = AdvancedPS.current_trace +const TypedVarInfo = VarInfo{<:NamedTuple} function report_observation!(trace, logp::Float64) trace.taskinfo.logp += logp @@ -19,23 +15,16 @@ function report_transition!(trace, logp::Float64, logγ::Float64) end - -function in(sym::Symbol, vns::Vector{VarName})::Bool - -end - - # We obtain the new value for our variable # If the distribution is not specified, we simply set it to be Normal function update_var(trace, vn, val, dist= Normal()) - # check if the symbol is contained in the varinfo... if haskey(trace.vi,vn) if is_flagged(trace.vi, vn, "del") - unset_flag!(vi, vn, "del") - trace.vi[vn] = val - setgid!(vi, spl.selector, vn) - setorder!(vi, vn, vi.num_produce) + unset_flag!(trace.vi, vn, "del") + trace.vi[vn] = vectorize(dist,val) + setgid!(trace.vi, vn) + setorder!(trace.vi, vn, trace.vi.num_produce) return val else val = trace.vi[vn] @@ -44,7 +33,6 @@ function update_var(trace, vn, val, dist= Normal()) #We do not specify the distribution... Thats why we set it to be Normal() push!(trace.vi, vn, val, dist) end - return val end @@ -52,3 +40,125 @@ end function create_task(f::Function) return CTask(() -> begin new_vi=f(); produce(Val{:done}); new_vi; end ) end + + + +######################################################################### +# This is copied from turing compiler3.0, but we need to extract the # +# sampler form set_retained_vns_del_by_spl! # +######################################################################### + + +# Get all indices of variables belonging to SampleFromPrior: + +# if the gid/selector of a var is an empty Set, then that var is assumed to be assigned to + +# the SampleFromPrior sampler + + +""" +`getidx(vi::UntypedVarInfo, vn::VarName)` + + +Returns the index of `vn` in `vi.metadata.vns`. + +""" +getidx(vi::UntypedVarInfo, vn::VarName) = vi.idcs[vn] + + +""" + +`getidx(vi::TypedVarInfo, vn::VarName{sym})` + +Returns the index of `vn` in `getfield(vi.metadata, sym).vns`. + +""" + +function getidx(vi::TypedVarInfo, vn::VarName{sym}) where sym + getfield(vi.metadata, sym).idcs[vn] +end + +setgid!(vi::UntypedVarInfo, vn::VarName) = push!(vi.gids[getidx(vi, vn)], Turing.Selector(1,:PS)) + +function setgid!(vi::TypedVarInfo, vn::VarName{sym}) where sym + push!(getfield(vi.metadata, sym).gids[getidx(vi, vn)], Turing.Selector(1,:PS)) +end + + +@inline function _getidcs(vi::UntypedVarInfo) + return filter(i -> isempty(vi.gids[i]) , 1:length(vi.gids)) +end + +# Get a NamedTuple of all the indices belonging to SampleFromPrior, one for each symbol +@inline function _getidcs(vi::TypedVarInfo) + return _getidcs(vi.metadata) +end + +@generated function _getidcs(metadata::NamedTuple{names}) where {names} + exprs = [] + for f in names + push!(exprs, :($f = findinds(metadata.$f))) + end + length(exprs) == 0 && return :(NamedTuple()) + return :($(exprs...),) +end + + + + + + + +""" +`set_retained_vns_del_by_spl!(vi::VarInfo, spl::Sampler)` + +Sets the `"del"` flag of variables in `vi` with `order > vi.num_produce` to `true`. + +""" + +function set_retained_vns_del_by_spl!(vi::UntypedVarInfo) + # Get the indices of `vns` that belong to `spl` as a vector + gidcs = _getidcs(vi) + if vi.num_produce == 0 + for i = length(gidcs):-1:1 + vi.flags["del"][gidcs[i]] = true + end + else + for i in 1:length(vi.orders) + if i in gidcs && vi.orders[i] > vi.num_produce + vi.flags["del"][i] = true + end + end + end + return nothing +end + +function set_retained_vns_del_by_spl!(vi::TypedVarInfo) + # Get the indices of `vns` that belong to `spl` as a NamedTuple, one entry for each symbol + gidcs = _getidcs(vi) + return _set_retained_vns_del_by_spl!(vi.metadata, gidcs, vi.num_produce) +end + +@generated function _set_retained_vns_del_by_spl!(metadata, gidcs::NamedTuple{names}, num_produce) where {names} + expr = Expr(:block) + for f in names + f_gidcs = :(gidcs.$f) + f_orders = :(metadata.$f.orders) + f_flags = :(metadata.$f.flags) + push!(expr.args, quote + # Set the flag for variables with symbol `f` + if num_produce == 0 + for i = length($f_gidcs):-1:1 + $f_flags["del"][$f_gidcs[i]] = true + end + else + for i in 1:length($f_orders) + if i in $f_gidcs && $f_orders[i] > num_produce + $f_flags["del"][i] = true + end + end + end + end) + end + return expr +end diff --git a/Example/demonstarte.jl b/Old_Examples/demonstarte.jl similarity index 100% rename from Example/demonstarte.jl rename to Old_Examples/demonstarte.jl diff --git a/Example/demonstartePGAS.jl b/Old_Examples/demonstartePGAS.jl similarity index 98% rename from Example/demonstartePGAS.jl rename to Old_Examples/demonstartePGAS.jl index 78bcfdcf..600c53af 100644 --- a/Example/demonstartePGAS.jl +++ b/Old_Examples/demonstartePGAS.jl @@ -4,13 +4,13 @@ using Libtask APS = AdvancedPS using Distributions n = 20 - +using ## Our states -vi = APS.Container(zeros(n),0) +vi = Container(zeros(n),0) ## Our observations y = Vector{Float64}(undef,n) for i = 1:n-1 diff --git a/Example/demonstrate_turing_vi.jl b/Old_Examples/demonstrate_turing_vi.jl similarity index 100% rename from Example/demonstrate_turing_vi.jl rename to Old_Examples/demonstrate_turing_vi.jl diff --git a/Example/test_interface.jl b/Old_Examples/test_interface.jl similarity index 100% rename from Example/test_interface.jl rename to Old_Examples/test_interface.jl diff --git a/Example/testfile.jl b/Old_Examples/testfile.jl similarity index 100% rename from Example/testfile.jl rename to Old_Examples/testfile.jl diff --git a/Tests/test_utils/AllUtils.jl b/Tests/test_utils/AllUtils.jl new file mode 100644 index 00000000..a850d6c2 --- /dev/null +++ b/Tests/test_utils/AllUtils.jl @@ -0,0 +1,9 @@ +using Turing, Random, Test + +# Import utility functions and reused models. +include("staging.jl") +include("numerical_tests.jl") +include("ad_utils.jl") +include("models.jl") +include("random_measure_utils.jl") +include("testing_functions.jl") diff --git a/Tests/test_utils/ad_utils.jl b/Tests/test_utils/ad_utils.jl new file mode 100644 index 00000000..1e5798d3 --- /dev/null +++ b/Tests/test_utils/ad_utils.jl @@ -0,0 +1,77 @@ +using Turing: gradient_logp_forward, gradient_logp_reverse +using Test + +function test_ad(f, at = 0.5; rtol = 1e-6, atol = 1e-6) + isarr = isa(at, AbstractArray) + reverse = Tracker.data(Tracker.gradient(f, at)[1]) + if isarr + forward = ForwardDiff.gradient(f, at) + @test isapprox(reverse, forward, rtol=rtol, atol=atol) + else + forward = ForwardDiff.derivative(f, at) + finite_diff = central_fdm(5,1)(f, at) + @test isapprox(reverse, forward, rtol=rtol, atol=atol) + @test isapprox(reverse, finite_diff, rtol=rtol, atol=atol) + end +end + +""" + test_reverse_mode_ad(forward, f, ȳ, x...; rtol=1e-6, atol=1e-6) + +Check that the reverse-mode sensitivities produced by an AD library are correct for `f` +at `x...`, given sensitivity `ȳ` w.r.t. `y = f(x...)` up to `rtol` and `atol`. +`forward` should be either `Tracker.forward` or `Zygote.forward`. +""" +function test_reverse_mode_ad(forward, f, ȳ, x...; rtol=1e-6, atol=1e-6) + + # Perform a regular forwards-pass. + y = f(x...) + + # Use tracker to compute reverse-mode sensitivities. + y_tracker, back = forward(f, x...) + x̄s_tracker = back(ȳ) + + # Use finite differencing to compute reverse-mode sensitivities. + x̄s_fdm = FDM.j′vp(central_fdm(5, 1), f, ȳ, x...) + + # Check that forwards-pass produces the correct answer. + @test isapprox(y, Tracker.data(y_tracker), atol=atol, rtol=rtol) + + # Check that reverse-mode sensitivities are correct. + @test all(zip(x̄s_tracker, x̄s_fdm)) do (x̄_tracker, x̄_fdm) + isapprox(Tracker.data(x̄_tracker), x̄_fdm; atol=atol, rtol=rtol) + end +end + +# See `test_reverse_mode_ad` for details. +function test_tracker_ad(f, ȳ, x...; rtol=1e-6, atol=1e-6) + return test_reverse_mode_ad(Tracker.forward, f, ȳ, x...; rtol=rtol, atol=atol) +end + +function test_model_ad(model, f, syms::Vector{Symbol}) + # Set up VI. + vi = Turing.VarInfo(model) + + # Collect symbols. + vnms = Vector(undef, length(syms)) + vnvals = Vector{Float64}() + for i in 1:length(syms) + s = syms[i] + vnms[i] = getfield(vi.metadata, s).vns[1] + + vals = getval(vi, vnms[i]) + for i in eachindex(vals) + push!(vnvals, vals[i]) + end + end + + spl = SampleFromPrior() + _, ∇E = gradient_logp_forward(vi[spl], vi, model) + grad_Turing = sort(∇E) + + # Call ForwardDiff's AD + grad_FWAD = sort(ForwardDiff.gradient(f, vec(vnvals))) + + # Compare result + @test grad_Turing ≈ grad_FWAD atol=1e-9 +end diff --git a/Tests/test_utils/models.jl b/Tests/test_utils/models.jl new file mode 100644 index 00000000..af207621 --- /dev/null +++ b/Tests/test_utils/models.jl @@ -0,0 +1,53 @@ +# The old-gdemo model. +@model gdemo(x, y) = begin + s ~ InverseGamma(2, 3) + m ~ Normal(0, sqrt(s)) + x ~ Normal(m, sqrt(s)) + y ~ Normal(m, sqrt(s)) + return s, m +end + +@model gdemo_d() = begin + s ~ InverseGamma(2, 3) + m ~ Normal(0, sqrt(s)) + 1.5 ~ Normal(m, sqrt(s)) + 2.0 ~ Normal(m, sqrt(s)) + return s, m +end + +gdemo_default = gdemo_d() + +@model MoGtest(D) = begin + mu1 ~ Normal(1, 1) + mu2 ~ Normal(4, 1) + z1 ~ Categorical(2) + if z1 == 1 + D[1] ~ Normal(mu1, 1) + else + D[1] ~ Normal(mu2, 1) + end + z2 ~ Categorical(2) + if z2 == 1 + D[2] ~ Normal(mu1, 1) + else + D[2] ~ Normal(mu2, 1) + end + z3 ~ Categorical(2) + if z3 == 1 + D[3] ~ Normal(mu1, 1) + else + D[3] ~ Normal(mu2, 1) + end + z4 ~ Categorical(2) + if z4 == 1 + D[4] ~ Normal(mu1, 1) + else + D[4] ~ Normal(mu2, 1) + end + z1, z2, z3, z4, mu1, mu2 +end + +MoGtest_default = MoGtest([1.0 1.0 4.0 4.0]) + +# Declare empty model to make the Sampler constructor work. +@model empty_model() = begin x = 1; end diff --git a/Tests/test_utils/numerical_tests.jl b/Tests/test_utils/numerical_tests.jl new file mode 100644 index 00000000..88e2b4e9 --- /dev/null +++ b/Tests/test_utils/numerical_tests.jl @@ -0,0 +1,66 @@ +function check_dist_numerical(dist, chn; mean_tol = 0.1, var_atol = 1.0, var_tol = 0.5) + @testset "numerical" begin + # Extract values. + chn_xs = chn[1:2:end, :x, :].value + + # Check means. + dist_mean = mean(dist) + mean_shape = size(dist_mean) + if !all(isnan, dist_mean) && !all(isinf, dist_mean) + chn_mean = Array(mean(chn_xs, dims=1)) + chn_mean = length(chn_mean) == 1 ? + chn_mean[1] : + reshape(chn_mean, mean_shape) + atol_m = length(chn_mean) > 1 ? + mean_tol * length(chn_mean) : + max(mean_tol, mean_tol * chn_mean) + @test chn_mean ≈ dist_mean atol=atol_m + end + + # Check variances. + # var() for Distributions.MatrixDistribution is not defined + if !(dist isa Distributions.MatrixDistribution) + # Variance + dist_var = var(dist) + var_shape = size(dist_var) + if !all(isnan, dist_var) && !all(isinf, dist_var) + chn_var = Array(var(chn_xs, dims=1)) + chn_var = length(chn_var) == 1 ? + chn_var[1] : + reshape(chn_var, var_shape) + atol_v = length(chn_mean) > 1 ? + mean_tol * length(chn_mean) : + max(mean_tol, mean_tol * chn_mean) + @test chn_mean ≈ dist_mean atol=atol_v + end + end + end +end + +# Helper function for numerical tests +function check_numerical(chain, + symbols::Vector, + exact_vals::Vector; + atol=0.2, + rtol=0.0) + for (sym, val) in zip(symbols, exact_vals) + E = val isa Real ? + mean(chain[sym].value) : + vec(mean(chain[sym].value, dims=[1])) + @info (symbol=sym, exact=val, evaluated=E) + @test E ≈ val atol=atol rtol=rtol + end +end + +# Wrapper function to quickly check gdemo accuracy. +function check_gdemo(chain; atol=0.2, rtol=0.0) + check_numerical(chain, [:s, :m], [49/24, 7/6], atol=atol, rtol=rtol) +end + +# Wrapper function to check MoGtest. +function check_MoGtest_default(chain; atol=0.2, rtol=0.0) + check_numerical(chain, + [:z1, :z2, :z3, :z4, :mu1, :mu2], + [1.0, 1.0, 2.0, 2.0, 1.0, 4.0], + atol=atol, rtol=rtol) +end diff --git a/Tests/test_utils/random_measure_utils.jl b/Tests/test_utils/random_measure_utils.jl new file mode 100644 index 00000000..868bbd31 --- /dev/null +++ b/Tests/test_utils/random_measure_utils.jl @@ -0,0 +1,36 @@ +using SpecialFunctions + +function compute_log_joint(observations, partition, tau0, tau1, sigma, theta) + n = length(observations) + k = length(partition) + prob = k*log(sigma) + lgamma(theta) + lgamma(theta/sigma + k) - lgamma(theta/sigma) - lgamma(theta + n) + for cluster in partition + prob += lgamma(length(cluster) - sigma) - lgamma(1 - sigma) + prob += compute_log_conditonal_observations(observations, cluster, tau0, tau1) + end + prob +end + +function compute_log_conditonal_observations(observations, cluster, tau0, tau1) + nl = length(cluster) + prob = (nl/2)*log(tau1) - (nl/2)*log(2*pi) + 0.5*log(tau0) + 0.5*log(tau0+nl) + prob += -tau1/2*(sum(observations)) + 0.5*(tau0*mu_0+tau1*sum(observations[cluster]))^2/(tau0+nl*tau1) + prob +end + +# Test of similarity between distributions +function correct_posterior(empirical_probs, data, partitions, τ0, τ1, σ, θ) + true_log_probs = map(p -> compute_log_joint(data, p, τ0, τ1, σ, θ), partitions) + true_probs = exp.(true_log_probs) + true_probs /= sum(true_probs) + + empirical_probs /= sum(empirical_probs) + + # compare distribitions + # L2 + L2 = sum((empirical_probs - true_probs).^2) + + # Discrepancy + discr = maximum(abs.(empirical_probs - true_probs)) + return L2, discr +end diff --git a/Tests/test_utils/staging.jl b/Tests/test_utils/staging.jl new file mode 100644 index 00000000..15d5853d --- /dev/null +++ b/Tests/test_utils/staging.jl @@ -0,0 +1,52 @@ +function get_stage() + # Appveyor uses "True" for non-Ubuntu images. + if get(ENV, "APPVEYOR", "") == "True" || get(ENV, "APPVEYOR", "") == "true" + return "nonnumeric" + end + + # Handle Travis and Github Actions specially. + if get(ENV, "TRAVIS", "") == "true" || get(ENV, "GITHUB_ACTIONS", "") == "true" + if "STAGE" in keys(ENV) + return ENV["STAGE"] + else + return "all" + end + end + + return "all" +end + +function do_test(stage_str) + stg = get_stage() + + # If the tests are being run by Appveyor, don't run + # any numerical tests. + if stg == "nonnumeric" + if stage_str == "numerical" + return false + else + return true + end + end + + # Otherwise run the regular testing procedure. + if stg == "all" || stg == stage_str + return true + end + + return false +end + +macro stage_testset(stage_string::String, args...) + if do_test(stage_string) + return esc(:(@testset($(args...)))) + end +end + +macro numerical_testset(args...) + esc(:(@stage_testset "numerical" $(args...))) +end + +macro turing_testset(args...) + esc(:(@stage_testset "test" $(args...))) +end diff --git a/Tests/test_utils/testing_functions.jl b/Tests/test_utils/testing_functions.jl new file mode 100644 index 00000000..809a62a6 --- /dev/null +++ b/Tests/test_utils/testing_functions.jl @@ -0,0 +1,73 @@ +using Turing + +GKernel(var) = (x) -> Normal(x, sqrt.(var)) + +varname(s::Symbol) = nothing, s +function varname(expr::Expr) + # Initialize an expression block to store the code for creating uid + local sym + @assert expr.head == :ref "expr needs to be an indexing expression, e.g. :(x[1])" + indexing_ex = Expr(:block) + # Add the initialization statement for uid + push!(indexing_ex.args, quote indexing_list = [] end) + # Initialize a local container for parsing and add the expr to it + to_eval = []; pushfirst!(to_eval, expr) + # Parse the expression and creating the code for creating uid + find_head = false + while length(to_eval) > 0 + evaling = popfirst!(to_eval) # get the current expression to deal with + if isa(evaling, Expr) && evaling.head == :ref && ~find_head + # Add all the indexing arguments to the left + pushfirst!(to_eval, "[", insdelim(evaling.args[2:end])..., "]") + # Add first argument depending on its type + # If it is an expression, it means it's a nested array calling + # Otherwise it's the symbol for the calling + if isa(evaling.args[1], Expr) + pushfirst!(to_eval, evaling.args[1]) + else + # push!(indexing_ex.args, quote pushfirst!(indexing_list, $(string(evaling.args[1]))) end) + push!(indexing_ex.args, quote sym = Symbol($(string(evaling.args[1]))) end) # store symbol in runtime + find_head = true + sym = evaling.args[1] # store symbol in compilation time + end + else + # Evaluting the concrete value of the indexing variable + push!(indexing_ex.args, quote push!(indexing_list, string($evaling)) end) + end + end + push!(indexing_ex.args, quote indexing = reduce(*, indexing_list) end) + return indexing_ex, sym +end + +genvn(sym::Symbol) = VarName(gensym(), sym, "", 1) +function genvn(expr::Expr) + ex, sym = varname(expr) + VarName(gensym(), sym, eval(ex), 1) +end + +function randr(vi::Turing.VarInfo, + vn::Turing.VarName, + dist::Distribution, + spl::Turing.Sampler, + count::Bool = false) + if ~haskey(vi, vn) + r = rand(dist) + Turing.push!(vi, vn, r, dist, spl) + return r + elseif is_flagged(vi, vn, "del") + unset_flag!(vi, vn, "del") + r = rand(dist) + Turing.RandomVariables.setval!(vi, Turing.vectorize(dist, r), vn) + return r + else + if count Turing.checkindex(vn, vi, spl) end + Turing.updategid!(vi, vn, spl) + return vi[vn] + end +end + + + +function insdelim(c, deli=",") + return reduce((e, res) -> append!(e, [res, deli]), c; init = [])[1:end-1] +end diff --git a/Tests/tests.jl b/Tests/tests.jl index 0fe852b7..c800e069 100644 --- a/Tests/tests.jl +++ b/Tests/tests.jl @@ -1,5 +1,5 @@ using Test - +using AdvancedPS dir = splitdir(splitdir(pathof(Turing))[1])[1] @@ -7,6 +7,20 @@ dir = splitdir(splitdir(pathof(Turing))[1])[1] include(dir*"/test/test_utils/AllUtils.jl") + +@turing_testset "AdvancedPS/src/Core/Container/Trace.jl" begin + vi = Array{Float64}(undef,10) + Trace() + + + +end + + +include("AdvancedPS/src/Core/Container/Trace.jl" ) + + +asdf @testset "container.jl" begin @turing_testset "copy particle container" begin pc = ParticleContainer(x -> x * x, Trace[]) diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index f01394f2..cd2c5148 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -6,7 +6,7 @@ module AdvancedPS using Libtask using StatsFuns: logsumexp, softmax! - using AdvancedMCMC + using AbstractMCMC import MCMCChains: Chains import Base.copy @@ -24,15 +24,24 @@ module AdvancedPS abstract type AbstractPFModel <: AbstractModel end + export AbstractTaskInfo, + AbstractParticleContainer, + AbstractTrace, + AbstractPFAlgorithm, + AbstractPFUtilitFunctions, + AbstractPFTransition, + AbstractPFSampler, + AbstractSMCUtilitFunctions, + AbstractPGASUtilityFunctions, + AbstractPFModel - - include("Core/Container/trace.jl") + include("Core/Algorithms/Algorithms.jl") + include("Core/Container/Trace.jl") include("Core/Container/ParticleContainer.jl") include("Core/Resample/resample.jl") - include("Core/Algorithms/taskinfo.jl") + include("Core/Algorithms/Taskinfo.jl") include("Core/Algorithms/sample.jl") include("Core/Resample/resample_functions.jl") - include("Core/Algorithms/Algorithms.jl") include("Core/Utilities/UtilityFunctions.jl") export ParticleContainer, @@ -44,6 +53,7 @@ module AdvancedPS empty!, resample!, PGTaskInfo, + SMCTaskInfo, PGASTaskInfo sample!, resample, @@ -56,13 +66,15 @@ module AdvancedPS PGUtilityFunctions, PGASUtilityFunctions, SMCAlgorithm, - PGAlgorithm + PGAlgorithm, + update_task! + include("Inference/Model.jl") - include("Inference/sample_init.jl") - include("Inference/Sampler.jl") - inlcude("Inference/step.jl") include("Inference/Transitions.jl") + include("Inference/Sampler.jl") + include("Inference/sample_init.jl") + include("Inference/step.jl") include("Inference/Inference.jl") export PFModel, @@ -71,6 +83,7 @@ module AdvancedPS sample_init!, step!, Sampler, - PFTransition + PFTransition, + transition_type end # module diff --git a/src/Core/Algorithms/Algorithms.jl b/src/Core/Algorithms/Algorithms.jl index ed9a9bac..bc4d67d0 100644 --- a/src/Core/Algorithms/Algorithms.jl +++ b/src/Core/Algorithms/Algorithms.jl @@ -9,8 +9,16 @@ function SMCAlgorithm() SMCAlgorithm(resample_systematic, 0.5) end +struct PGAlgorithm{RT} <: AbstractPFAlgorithm where RT<:AbstractFloat + resampler :: Function + resampler_threshold :: RT + n :: Int64 -const PGAlgorithm = SMCAlgorithm +end + +function PGAlgorithm(n::Int64) + PGAlgorithm(resample_systematic, 0.5, n) +end struct PGASAlgorithm{RT} <: AbstractPFAlgorithm diff --git a/src/Core/Algorithms/sample.jl b/src/Core/Algorithms/sample.jl index 5779cb48..d3c7325c 100644 --- a/src/Core/Algorithms/sample.jl +++ b/src/Core/Algorithms/sample.jl @@ -3,12 +3,13 @@ ## This is all we need for Turing! function sample!(pc::PC, alg::ALG, utility_functions::AbstractSMCUtilitFunctions, ref_traj::T) where { - PC <: ParticleContainer, - ALG <: Union{SMCAlgorithm, PGAlgoirhtm} - T <: Trace + PC <:ParticleContainer, + ALG <:Union{SMCAlgorithm, PGAlgorithm}, + T <:Union{Trace,Nothing} } n = length(pc.vals) while consume(pc) != Val{:done} + ess = effectiveSampleSize(pc) if ref_traj !== nothing || ess <= alg.resampler_threshold * length(pc) # compute weights Ws = weights(pc) @@ -16,11 +17,12 @@ function sample!(pc::PC, alg::ALG, utility_functions::AbstractSMCUtilitFunctions @assert !any(isnan, Ws) # sample ancestor indices # Ancestor trajectory is not sampled - nresamples = n-1 + ref_traj !== nothing ? nresamples = n-1 : nresamples = n + indx = alg.resampler(Ws, nresamples) # We add ancestor trajectory to the path. # For ancestor sampling, we would change n at this point. - push!(indx,n) + ref_traj !== nothing ? push!(indx,n) : nothing resample!(pc, utility_functions, indx, ref_traj) end end diff --git a/src/Core/Container/Trace.jl b/src/Core/Container/Trace.jl index a53a2776..bff4f72f 100644 --- a/src/Core/Container/Trace.jl +++ b/src/Core/Container/Trace.jl @@ -10,14 +10,13 @@ mutable struct Trace{Tvi, TInfo} <: AbstractTrace where {Tvi, TInfo <: Abstract end function Base.copy(trace::Trace{Tvi,TInfo}, copy_vi::Function) where {Tvi, TInfo <: AbstractTaskInfo} - return Trace(trace.vi, trace.task, trace.taskinfo,copy_vi) + return Trace(trace.vi, trace.task, trace.taskinfo, copy_vi) end # The procedure passes a function which is specified by the model. -function Trace( vi, f::Function, taskinfo, copy_vi::Function) +function Trace(vi, f::Function, taskinfo, copy_vi::Function) task = CTask( () -> begin res=f(); produce(Val{:done}); res; end ) - res = Trace(copy_vi(vi), task, copy(taskinfo)) # CTask(()->f()); if res.task.storage === nothing @@ -29,8 +28,6 @@ end ## We need to design the task in the Turing wrapper. function Trace(vi, task::Task, taskinfo, copy_vi::Function) - - res = Trace(copy_vi(vi), Libtask.copy(task), copy(taskinfo)) # CTask(()->f()); if res.task.storage === nothing @@ -41,7 +38,18 @@ function Trace(vi, task::Task, taskinfo, copy_vi::Function) end +# NOTE: this function is called by `forkr` +function Trace(vi, f::Function, taskinfo, copy_vi::Function) + # CTask(()->f()); + task = CTask( () -> begin res=f(); produce(Val{:done}); res; end ) + res = Trace(copy_vi(vi), task, copy(taskinfo)) + if res.task.storage === nothing + res.task.storage = IdDict() + end + res.task.storage[:turing_trace] = res # create a backward reference in task_local_storage + return res +end # step to the next observe statement, return log likelihood Libtask.consume(t::Trace) = (t.vi.num_produce += 1; consume(t.task)) @@ -56,11 +64,9 @@ function fork(trace::Trace, copy_vi::Function, is_ref::Bool = false, set_retaine return newtrace end -# PG requires keeping all randomness for the reference particle -# Create new task and copy randomness -function forkr(trace::Trace, copy_vi::Function) - newtrace = Trace(trace.vi,trace.task ,trace.taskinfo,copy_vi) +function forkr(trace::Trace, copy_vi::Function) + newtrace = Trace(trace.vi, trace.task.code, trace.taskinfo, copy_vi) newtrace.vi.num_produce = 0 return newtrace end diff --git a/src/Core/Utilities/UtilityFunctions.jl b/src/Core/Utilities/UtilityFunctions.jl index 7657408d..e30d209a 100644 --- a/src/Core/Utilities/UtilityFunctions.jl +++ b/src/Core/Utilities/UtilityFunctions.jl @@ -1,16 +1,15 @@ -abstract type AbstractSMCUtilitFunctions <: AbstractPFUtilitFunctions end -abstract type AbstractPGASUtilityFunctions <: AbstractSMCUtilitFunctions end - struct SMCUtilityFunctions<:AbstractSMCUtilitFunctions copy :: Function set_retained_vns_del_by_spl! :: Function empty! :: Function - to_named_tuple :: Function + tonamedtuple :: Function end + + const PGUtilityFunctions = SMCUtilityFunctions struct PGASUtilityFunctions{AP}<:AbstractPGASUtilityFunctions @@ -18,6 +17,6 @@ struct PGASUtilityFunctions{AP}<:AbstractPGASUtilityFunctions set_retained_vns_del_by_spl! :: Function merge_traj :: Function empty! :: Function - to_named_tuple :: Functino + tonamedtuple :: Function end diff --git a/src/Inference/Inference.jl b/src/Inference/Inference.jl index 9ed63075..66fe383a 100644 --- a/src/Inference/Inference.jl +++ b/src/Inference/Inference.jl @@ -1,13 +1,16 @@ +const PROGRESS = Ref(true) + function AbstractMCMC.sample( model::ModelType, alg::AlgType, uf::UF, - vi::T, + vi::C, N::Integer; kwargs... ) where { + C, ModelType<:AbstractPFModel, AlgType<: AbstractPFAlgorithm, UF<:AbstractPFUtilitFunctions @@ -15,23 +18,25 @@ function AbstractMCMC.sample( return sample(model, Sampler(alg, uf, vi), N; progress=PROGRESS[], kwargs...) end +const ACVANCEDPS_INTERNAL_VARS = (internals = [ + "lp", + "weight", + "le", +],) function AbstractMCMC.bundle_samples( rng::AbstractRNG, - model::ModelType, + model::AbstractPFModel, spl::AbstractPFSampler, N::Integer, ts::Vector{T}; - discard_adapt::Bool=true, - save_state=false, kwargs... -) where {ModelType<:AbstractPFModel, T<:AbstractPFTransition} +) where {T<:AbstractPFTransition} # Convert transitions to array format. # Also retrieve the variable names. nms, vals = _params_to_array(ts) - # Get the values of the extra parameters in each Transition struct. extra_params, extra_values = get_transition_extras(ts) @@ -40,8 +45,6 @@ function AbstractMCMC.bundle_samples( parray = hcat(vals, extra_values) - # Get the values of the extra parameters in each Transition struct. - extra_params, extra_values = get_transition_extras(ts) le = missing # Set up the info tuple. info = NamedTuple() @@ -49,36 +52,8 @@ function AbstractMCMC.bundle_samples( return Chains( parray, string.(nms), - deepcopy(TURING_INTERNAL_VARS); + deepcopy(ACVANCEDPS_INTERNAL_VARS); evidence=le, info=info ) end - - -function _params_to_array(ts::Vector{T}) where {T<:AbstractTransition} - names = Set{String}() - dicts = Vector{Dict{String, Any}}() - # Extract the parameter names and values from each transition. - for t in ts - nms, vs = flatten_namedtuple(t.θ) - push!(names, nms...) - # Convert the names and values to a single dictionary. - d = Dict{String, Any}() - for (k, v) in zip(nms, vs) - d[k] = v - end - push!(dicts, d) - end - # Convert the set to an ordered vector so the parameter ordering - # is deterministic. - ordered_names = collect(names) - vals = Matrix{Union{Real, Missing}}(undef, length(ts), length(ordered_names)) - # Place each element of all dicts into the returned value matrix. - for i in eachindex(dicts) - for (j, key) in enumerate(ordered_names) - vals[i,j] = get(dicts[i], key, missing) - end - end - return ordered_names, vals -end diff --git a/src/Inference/Sampler.jl b/src/Inference/Sampler.jl index 7245999f..375c2a87 100644 --- a/src/Inference/Sampler.jl +++ b/src/Inference/Sampler.jl @@ -1,9 +1,12 @@ -abstract type AbstractPFSampler <: AbstractSampler end # The particle container is our state, -struct SMCSampler{PC, ALG, UF, C} <: AbstractPFSampler where {PC<:ParticleContainer, ALG<:SMCAlgorithm, UF<:SMCUtilityFunctions} +mutable struct SMCSampler{PC, ALG, UF, C} <: AbstractPFSampler where { + PC<:ParticleContainer, + ALG<:SMCAlgorithm, + UF<:SMCUtilityFunctions +} pc :: PC alg :: ALG uf :: UF @@ -11,23 +14,23 @@ struct SMCSampler{PC, ALG, UF, C} <: AbstractPFSampler where {PC<:ParticleContai end -function Sampler(alg:: ALG, uf::UF, vi::T) where { +function Sampler(alg:: ALG, uf::UF, vi::C) where { + C, ALG<: SMCAlgorithm, UF<: SMCUtilityFunctions, } - pc = ParticleContainer(Trace{typeof(vi),SMCTaskInfo}[]) + pc = ParticleContainer(Trace{typeof(vi),SMCTaskInfo{Float64}}[]) SMCSampler(pc, alg, uf, uf.empty!(vi)) end -struct PGSampler{T, ALG, UF, C} <: AbstractPFSampler where { +mutable struct PGSampler{T, ALG, UF, C} <: AbstractPFSampler where { T <:Particle, - ALG<:SMCAlgorithm, - UF<:AbstractUtilityFunctions + ALG<:PGAlgorithm, + UF<:PGUtilityFunctions } - alg :: ALG uf :: UF ref_traj :: Union{T, Nothing} @@ -36,7 +39,7 @@ end Sampler(alg:: ALG, uf::UF, vi::T) where { + T, ALG<: PGAlgorithm, - UF<: AbstractUtilityFunctions, - M <:AbstractPFModel -} = PGSampler(pc, alg, uf, nothing) + UF<: PGUtilityFunctions +} = PGSampler{Trace{typeof(vi),PGTaskInfo{Float64}},typeof(alg),typeof(uf),typeof(vi)}(alg, uf, nothing, vi) diff --git a/src/Inference/Transitions.jl b/src/Inference/Transitions.jl index e3cb0c96..3d89cba2 100644 --- a/src/Inference/Transitions.jl +++ b/src/Inference/Transitions.jl @@ -5,10 +5,126 @@ struct PFTransition{T, F<:AbstractFloat} <: AbstractPFTransition θ::T lp::F le::F - weight::Vector{F} + weight::F end -transition_type(spl::Sampler{<:ParticleInference}) = ParticleTransition -function additional_parameters(::Type{<:ParticleTransition}) +AbstractMCMC.transition_type(spl::S) where S<:AbstractPFSampler = PFTransition +function additional_parameters(::Type{<:PFTransition}) return [:lp,:le, :weight] end + + + +function get_transition_extras(ts::Vector{T}) where T<:AbstractTransition + # Get the extra field names from the sampler state type. + # This handles things like :lp or :weight. + extra_params = additional_parameters(T) + # Get the values of the extra parameters. + local extra_names + all_vals = [] + # Iterate through each transition. + for t in ts + extra_names = String[] + vals = [] + # Iterate through each of the additional field names + # in the struct. + for p in extra_params + # Check whether the field contains a NamedTuple, + # in which case we need to iterate through each + # key/value pair. + prop = getproperty(t, p) + if prop isa NamedTuple + for (k, v) in pairs(prop) + push!(extra_names, string(k)) + push!(vals, v) + end + else + push!(extra_names, string(p)) + push!(vals, prop) + end + end + push!(all_vals, vals) + end + # Convert the vector-of-vectors to a matrix. + valmat = [all_vals[i][j] for i in 1:length(ts), j in 1:length(all_vals[1])] + return extra_names, valmat +end + + + + + +function flatten(names, value :: AbstractArray, k :: String, v) + if isa(v, Number) + name = k + push!(value, v) + push!(names, name) + elseif isa(v, Array) + for i = eachindex(v) + if isa(v[i], Number) + name = string(ind2sub(size(v), i)) + name = replace(name, "(" => "["); + name = replace(name, ",)" => "]"); + name = replace(name, ")" => "]"); + name = k * name + isa(v[i], Nothing) && println(v, i, v[i]) + push!(value, v[i]) + push!(names, name) + elseif isa(v[i], AbstractArray) + name = k * string(ind2sub(size(v), i)) + flatten(names, value, name, v[i]) + else + error("Unknown var type: typeof($v[i])=$(typeof(v[i]))") + end + end + else + error("Unknown var type: typeof($v)=$(typeof(v))") + end + return +end + +function flatten_namedtuple(nt::NamedTuple{pnames}) where {pnames} + vals = Vector{Real}() + names = Vector{AbstractString}() + for k in pnames + v = nt[k] + if length(v) == 1 + flatten(names, vals, string(k), v) + else + for (vnval, vn) in zip(v[1], v[2]) + flatten(names, vals, vn, vnval) + end + end + end + return names, vals +end + + + + +function _params_to_array(ts::Vector{T}) where {T<:AbstractTransition} + names = Set{String}() + dicts = Vector{Dict{String, Any}}() + # Extract the parameter names and values from each transition. + for t in ts + nms, vs = flatten_namedtuple(t.θ) + push!(names, nms...) + # Convert the names and values to a single dictionary. + d = Dict{String, Any}() + for (k, v) in zip(nms, vs) + d[k] = v + end + push!(dicts, d) + end + # Convert the set to an ordered vector so the parameter ordering + # is deterministic. + ordered_names = collect(names) + vals = Matrix{Union{Real, Missing}}(undef, length(ts), length(ordered_names)) + # Place each element of all dicts into the returned value matrix. + for i in eachindex(dicts) + for (j, key) in enumerate(ordered_names) + vals[i,j] = get(dicts[i], key, missing) + end + end + return ordered_names, vals +end diff --git a/src/Inference/sample_init.jl b/src/Inference/sample_init.jl index 5153880d..c45f03c9 100644 --- a/src/Inference/sample_init.jl +++ b/src/Inference/sample_init.jl @@ -1,15 +1,15 @@ -function sample_init!( +function AbstractMCMC.sample_init!( rng::AbstractRNG, ℓ::ModelType, - s::SamplerType, + spl::SamplerType, N::Integer; debug::Bool=false, kwargs... ) where {ModelType<:AbstractPFModel, SamplerType<:SMCSampler} - T = Trace{typeof(spl.vi),SMCTaskInfo} - particles = T[ Trace(vi, task, SMCTaskInfo(), alg.copy) for _ =1:N] - spl.pc = APS.ParticleContainer{typeof(particles[1])}(particles,zeros(N),zeros(N),0,0) + T = Trace{typeof(spl.vi),SMCTaskInfo{Float64}} + particles = T[ Trace(spl.vi, ℓ.task, SMCTaskInfo(), spl.uf.copy) for _ =1:N] + spl.pc = ParticleContainer{typeof(particles[1])}(particles,zeros(N),0.0,0) sample!(spl.pc, spl.alg, spl.uf, nothing) diff --git a/src/Inference/step.jl b/src/Inference/step.jl index a8000de0..f39a9786 100644 --- a/src/Inference/step.jl +++ b/src/Inference/step.jl @@ -1,46 +1,50 @@ #SMC step -function step!( +function AbstractMCMC.step!( ::AbstractRNG, - model::Turing.Model, - spl::T, - i::Integer; + model::AbstractPFModel, + spl::SPL, + ::Integer; + iteration::Integer, kwargs... - ) where T <: SMCSampler + ) where SPL <: SMCSampler + + particle = spl.pc[iteration] - particle = spl.pc[i] - params = spl.uf.to_named_tuple(spl.pc[i]) - return PFTransition(params, particle.taskinfo.logp, pc.logE, Ws[i]) + params = spl.uf.tonamedtuple(particle.vi) + return PFTransition(params, particle.taskinfo.logp, spl.pc.logE, weights(spl.pc)[iteration]) end # PG step -function step!( +function AbstractMCMC.step!( ::AbstractRNG, - model::Turing.Model, - spl::T, + model::AbstractPFModel, + spl::SPL, ::Integer; kwargs... - ) where T <: PGSampler + ) where SPL <: PGSampler - n = alg.n - T = Trace{typeof(spl.vi),SMCTaskInfo} + n = spl.alg.n + T = Trace{typeof(spl.vi), PGTaskInfo{Float64}} - if hasfield(spl,:ref_traj) && spl.ref_traj !== nothing - particles = T[ Trace(vi, task, SMCTaskInfo(), alg.copy) for _ =1:n-1] - pc = APS.ParticleContainer{typeof(particles[1])}(particles,zeros(n),zeros(n),0,0) + if hasfield(typeof(spl),:ref_traj) && spl.ref_traj !== nothing + particles = T[ Trace(spl.vi, model.task, PGTaskInfo(), spl.uf.copy) for _ =1:n-1] + pc = ParticleContainer{typeof(particles[1])}(particles,zeros(n),0.0,0) + # Reset Task + spl.ref_traj = forkr(spl.ref_traj, uf.copy) push!(pc, spl.ref_traj) else - particles = T[ Trace(vi, task, SMCTaskInfo(), alg.copy) for _ =1:n] - pc = APS.ParticleContainer{typeof(particles[1])}(particles,zeros(n),zeros(n),0,0) + particles = T[ Trace(spl.vi, model.task, PGTaskInfo(), spl.uf.copy) for _ =1:n] + pc = ParticleContainer{typeof(particles[1])}(particles,zeros(n),0.0,0) end sample!(pc, spl.alg, spl.uf, spl.ref_traj) indx = AdvancedPS.randcat(weights(pc)) particle = spl.ref_traj = pc[indx] - params = spl.uf.to_named_tuple(spl.pc[i]) - return PFTransition(params, particle.taskinfo.logp, pc.logE, Ws[i]) + params = spl.uf.tonamedtuple(particle.vi) + return PFTransition(params, particle.taskinfo.logp, pc.logE, weights(pc)[indx]) end # From 4a1dd88fefe24c36d0dcfce53121a17b45b916e5 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Fri, 6 Dec 2019 16:16:50 +0000 Subject: [PATCH 23/25] Running --- Example/Custom_Container/demonstarte.jl | 73 +++++++++++++ .../Custom_Container}/test_interface.jl | 14 +++ Example/Turing_baseline.jl | 20 ---- Example/Using_Turing_VI/Turing_baseline.jl | 21 ++++ .../demonstrate_with_Turing_VI.jl | 62 ++++------- .../{ => Using_Turing_VI}/turing_interface.jl | 102 +++++++++++++++--- Old_Examples/demonstarte.jl | 94 ---------------- Tests/test_utils/staging.jl | 2 +- Tests/tests.jl | 3 - src/AdvancedPS.jl | 3 +- src/Core/Algorithms/sample.jl | 2 +- src/Core/Container/Trace.jl | 6 +- src/Core/Resample/resample.jl | 3 + src/Inference/step.jl | 7 +- 14 files changed, 232 insertions(+), 180 deletions(-) create mode 100644 Example/Custom_Container/demonstarte.jl rename {Old_Examples => Example/Custom_Container}/test_interface.jl (85%) delete mode 100644 Example/Turing_baseline.jl create mode 100644 Example/Using_Turing_VI/Turing_baseline.jl rename Example/{ => Using_Turing_VI}/demonstrate_with_Turing_VI.jl (54%) rename Example/{ => Using_Turing_VI}/turing_interface.jl (62%) delete mode 100644 Old_Examples/demonstarte.jl diff --git a/Example/Custom_Container/demonstarte.jl b/Example/Custom_Container/demonstarte.jl new file mode 100644 index 00000000..eb77c96b --- /dev/null +++ b/Example/Custom_Container/demonstarte.jl @@ -0,0 +1,73 @@ + +## It is not yet a package... +using Distributions +using Revise +using AdvancedPS +include("AdvancedPS/Example/Custom_Container/test_interface.jl") +# Define a short model. +# The syntax is rather simple. Observations need to be reported with report_observation. +# Transitions must be reported using report_transition. +# The trace contains the variables which we want to infer using particle gibbs. +# Thats all! +n = 3000 + +y = Vector{Float64}(undef,n-1) +for i =1:n-1 + y[i] = 0 +end + +function task_f() + var = initialize() + x = Vector{Float64}(undef,n) + vn = @varname x[1] + x[1] = update_var(var, vn, rand(Normal())) + report_transition!(var,0.0,0.0) + for i = 2:n + # Sampling + r = rand(Normal()) + vn = @varname x[i] + x[i] = update_var(var, vn, r) + logγ = logpdf(Normal(),x[i]) #γ(x_t|x_t-1) + logp = logpdf(Normal(),x[i]) # p(x_t|x_t-1) + report_transition!(var,logp,logγ) + #Proposal and Resampling + logpy = logpdf(Normal(x[i], 1.0), y[i-1]) + var = report_observation!(var,logpy) + end +end + +function task_f2() + var = initialize() + x = Vector{Float64}(undef,n) + vn = @varname x[1] + x[1] = update_var(var, vn, rand(Normal())) + report_transition!(var,0.0,0.0) + for i = 2:n + # Sampling + r = rand(Normal()) + vn = @varname x[i] + x[i] = update_var(var, vn, r) + logγ = 0.0 + logp = 0.0 + report_transition!(var,logp,logγ) + #Proposal and Resampling + logpy = logpdf(Normal(x[i], 1.0), y[i-1]) + var = report_observation!(var,logpy) + end +end + + + + +task = create_task(task_f2) +model = PFModel(task) +tcontainer = Container(zeros(n),Vector{Bool}(undef,n),zeros(n),0) + +alg = AdvancedPS.SMCAlgorithm() +uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) +@elapsed sample(model, alg, uf, tcontainer, 10) + + +alg = AdvancedPS.PGAlgorithm(AdvancedPS.resample_systematic, 1.0, 10) +uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) +@elapsed chn2 =sample(model, alg, uf, tcontainer, 5) diff --git a/Old_Examples/test_interface.jl b/Example/Custom_Container/test_interface.jl similarity index 85% rename from Old_Examples/test_interface.jl rename to Example/Custom_Container/test_interface.jl index 2327b3f2..aed3584f 100644 --- a/Old_Examples/test_interface.jl +++ b/Example/Custom_Container/test_interface.jl @@ -15,6 +15,11 @@ mutable struct Container num_produce::Float64 end +function Base.deepcopy(vi::Container) + return Container(deepcopy(vi.x),deepcopy(vi.marked),deepcopy(vi.produced_at),deepcopy(vi.num_produce)) +end + + function set_retained_vns_del_by_spl!(container::Container) for i in 1:length(container.marked) if container.marked[i] @@ -67,3 +72,12 @@ end function create_task(f::Function) return CTask(() -> begin new_vi=f(); produce(Val{:done}); new_vi; end ) end + +function tonamedtuple(vi::Container) + return NamedTuple() +end +function Base.empty!(vi::Container) + for i in 1:length(vi.marked) + vi.marked[i] = false + end +end diff --git a/Example/Turing_baseline.jl b/Example/Turing_baseline.jl deleted file mode 100644 index 31868bdc..00000000 --- a/Example/Turing_baseline.jl +++ /dev/null @@ -1,20 +0,0 @@ -using Turing - -n = 20 - -y = Vector{Float64}(undef,n-1) -for i =1:n-1 - y[i] = sin(0.1*i) -end - -@model demo(y) = begin - x = Vector{Float64}(undef,n) - vn = @varname x[1] - x[1] ~ Normal() - for i = 2:n - x[i] ~ Normal(x[i-1],0.2) - y[i-1] ~ Normal(x[i],0.2) - end -end - -sample(demo(y),SMC(),10) diff --git a/Example/Using_Turing_VI/Turing_baseline.jl b/Example/Using_Turing_VI/Turing_baseline.jl new file mode 100644 index 00000000..7baf6a85 --- /dev/null +++ b/Example/Using_Turing_VI/Turing_baseline.jl @@ -0,0 +1,21 @@ +using Turing + +n = 3000 + +y = Vector{Float64}(undef,n-1) +for i =1:n-1 + y[i] = 0 +end + +@model demo() = begin + x = Vector{Float64}(undef,n) + x[1] ~ Normal() + for i = 2:n + x[i] ~ Normal() + y[i-1] ~ Normal(x[i],1.0) + end +end + +@elapsed sample(demo(),PG(10),5) + +@elapsed sample(demo(),SMC(),10) diff --git a/Example/demonstrate_with_Turing_VI.jl b/Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl similarity index 54% rename from Example/demonstrate_with_Turing_VI.jl rename to Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl index f6151e32..88348647 100644 --- a/Example/demonstrate_with_Turing_VI.jl +++ b/Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl @@ -8,13 +8,13 @@ using Turing using Revise using AdvancedPS import Turing.Core: tonamedtuple -include("AdvancedPS/Example/turing_interface.jl") +include("AdvancedPS/Example/Using_Turing_VI/turing_interface.jl") # Define a short model. # The syntax is rather simple. Observations need to be reported with report_observation. # Transitions must be reported using report_transition. # The trace contains the variables which we want to infer using particle gibbs. # Thats all! -n = 20 +n = 3000 y = Vector{Float64}(undef,n-1) for i =1:n-1 @@ -44,65 +44,49 @@ end task = create_task(task_f) - model = PFModel(task) - tonamedtuple(vi::UntypedVarInfo) = tonamedtuple(TypedVarInfo(vi)) + + + alg = AdvancedPS.SMCAlgorithm() uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) -container = VarInfo() -chn =sample(model, alg, uf, container, 2000) +# Get type stability!! +untypedcontainer = VarInfo() +T = Trace{typeof(untypedcontainer),SMCTaskInfo{Float64}} +particles = T[ Trace(untypedcontainer, task, SMCTaskInfo(), uf.copy) for _ =1:1] +pc = ParticleContainer{typeof(particles[1])}(particles,zeros(1),0.0,0) +@elapsed AdvancedPS.sample!(pc, alg, uf, nothing) -alg = AdvancedPS.PGAlgorithm(5) -uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) -n = 5 -T = Trace{typeof(container), PGTaskInfo{Float64}} -# if hasfield(typeof(container),:ref_traj) && spl.ref_traj !== nothing -# particles = T[ Trace(container, model.task, SMCTaskInfo(), uf.copy) for _ =1:n-1] -# pc = ParticleContainer{typeof(particles[1])}(particles,zeros(n),0.0,0) -# -# # Reset Task -# spl.ref_traj.task = copy(model.task) -# -# push!(pc, .ref_traj) -# else -particles = T[ Trace(container, model.task, PGTaskInfo(), uf.copy) for _ =1:n] -pc = ParticleContainer{typeof(particles[1])}(particles,zeros(n),0.0,0) -AdvancedPS.sample!(pc, alg, uf, nothing) +container = uf.empty!(TypedVarInfo(pc[1].vi)) +tcontainer = container -ref_traj = pc[1] -particles = T[ Trace(container, model.task, PGTaskInfo(), uf.copy) for _ =1:n-1] -pc = ParticleContainer{typeof(particles[1])}(particles,zeros(n),0.0,0) -# Reset Task -ref_traj = AdvancedPS.forkr(ref_traj, uf.copy) +# typeof(tcontainer) <: TypedVarInfo +# tcontainer = VarInfo() +# typeof(tcontainer) <: TypedVarInfo +# -push!(pc, ref_traj) -pc -AdvancedPS.sample!(pc, alg, uf, ref_traj) -container -vn = @varname vi -push!(container,vn,3.0,Normal()) -container.metadata.flags +alg = AdvancedPS.SMCAlgorithm() +uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) +@elapsed sample(model, alg, uf, tcontainer, 10) -pc -indx = AdvancedPS.randcat(weights(pc)) -particle = spl.ref_traj = pc[indx] -params = spl.uf.tonamedtuple(particle.vi) -return PFTransition(params, particle.taskinfo.logp, pc.logE, weights(pc)[indx]) +alg = AdvancedPS.PGAlgorithm(AdvancedPS.resample_systematic, 1.0, 10) +uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) +@elapsed chn2 =sample(model, alg, uf, tcontainer, 5) diff --git a/Example/turing_interface.jl b/Example/Using_Turing_VI/turing_interface.jl similarity index 62% rename from Example/turing_interface.jl rename to Example/Using_Turing_VI/turing_interface.jl index 145f0c7d..16ddee5d 100644 --- a/Example/turing_interface.jl +++ b/Example/Using_Turing_VI/turing_interface.jl @@ -1,6 +1,8 @@ # This is important for initalizaiton const initialize = AdvancedPS.current_trace const TypedVarInfo = VarInfo{<:NamedTuple} +const Selector = Turing.Selector +const BASE_SELECTOR = Selector(:PS) function report_observation!(trace, logp::Float64) trace.taskinfo.logp += logp @@ -23,10 +25,11 @@ function update_var(trace, vn, val, dist= Normal()) if is_flagged(trace.vi, vn, "del") unset_flag!(trace.vi, vn, "del") trace.vi[vn] = vectorize(dist,val) - setgid!(trace.vi, vn) + setgid!(trace.vi, BASE_SELECTOR, vn) setorder!(trace.vi, vn, trace.vi.num_produce) return val else + updategid!(trace.vi, BASE_SELECTOR, vn) val = trace.vi[vn] end else @@ -56,13 +59,18 @@ end # the SampleFromPrior sampler + + """ + `getidx(vi::UntypedVarInfo, vn::VarName)` + Returns the index of `vn` in `vi.metadata.vns`. """ + getidx(vi::UntypedVarInfo, vn::VarName) = vi.idcs[vn] @@ -70,30 +78,55 @@ getidx(vi::UntypedVarInfo, vn::VarName) = vi.idcs[vn] `getidx(vi::TypedVarInfo, vn::VarName{sym})` + + Returns the index of `vn` in `getfield(vi.metadata, sym).vns`. """ function getidx(vi::TypedVarInfo, vn::VarName{sym}) where sym + getfield(vi.metadata, sym).idcs[vn] + end -setgid!(vi::UntypedVarInfo, vn::VarName) = push!(vi.gids[getidx(vi, vn)], Turing.Selector(1,:PS)) -function setgid!(vi::TypedVarInfo, vn::VarName{sym}) where sym - push!(getfield(vi.metadata, sym).gids[getidx(vi, vn)], Turing.Selector(1,:PS)) -end + +""" + +`setgid!(vi::VarInfo, gid::Selector, vn::VarName)` -@inline function _getidcs(vi::UntypedVarInfo) - return filter(i -> isempty(vi.gids[i]) , 1:length(vi.gids)) + +Adds `gid` to the set of sampler selectors associated with `vn` in `vi`. + +""" + +setgid!(vi::UntypedVarInfo, gid::Selector, vn::VarName) = push!(vi.gids[getidx(vi, vn)], gid) + +function setgid!(vi::TypedVarInfo, gid::Selector, vn::VarName{sym}) where sym + + push!(getfield(vi.metadata, sym).gids[getidx(vi, vn)], gid) + end -# Get a NamedTuple of all the indices belonging to SampleFromPrior, one for each symbol -@inline function _getidcs(vi::TypedVarInfo) - return _getidcs(vi.metadata) +""" + +`updategid!(vi::VarInfo, vn::VarName, spl::Sampler)` + + + +If `vn` doesn't have a sampler selector linked and `vn`'s symbol is in the space of + +`spl`, this function will set `vn`'s `gid` to `Set([spl.selector])`. + +""" + +function updategid!(vi::AbstractVarInfo, sel::Selector, vn::VarName) + setgid!(vi, sel, vn) end + @generated function _getidcs(metadata::NamedTuple{names}) where {names} exprs = [] for f in names @@ -103,22 +136,63 @@ end return :($(exprs...),) end +# Get all indices of variables belonging to a given sampler +@inline function _getidcs(vi::UntypedVarInfo, s::Selector) + findinds(vi, s) +end +@inline function _getidcs(vi::TypedVarInfo, s::Selector) + return _getidcs(vi.metadata, s) +end +# Get a NamedTuple for all the indices belonging to a given selector for each symbol + +@generated function _getidcs(metadata::NamedTuple{names}, s::Selector) where {names} + exprs = [] + # Iterate through each varname in metadata. + for f in names + # If the varname is in the sampler space + # or the sample space is empty (all variables) + # then return the indices for that variable. + push!(exprs, :($f = findinds(metadata.$f, s))) + end + length(exprs) == 0 && return :(NamedTuple()) + return :($(exprs...),) +end + +@inline function findinds(f_meta, s::Selector) + + # Get all the idcs of the vns in `space` and that belong to the selector `s` + return filter((i) -> + (s in f_meta.gids[i] || isempty(f_meta.gids[i])), 1:length(f_meta.gids)) +end + +@inline function findinds(f_meta) + # Get all the idcs of the vns + return filter((i) -> isempty(f_meta.gids[i]), 1:length(f_meta.gids)) +end """ + `set_retained_vns_del_by_spl!(vi::VarInfo, spl::Sampler)` + + Sets the `"del"` flag of variables in `vi` with `order > vi.num_produce` to `true`. """ -function set_retained_vns_del_by_spl!(vi::UntypedVarInfo) +function set_retained_vns_del_by_spl!(vi::AbstractVarInfo) + return set_retained_vns_del_by_spl!(vi, BASE_SELECTOR) +end + + +function set_retained_vns_del_by_spl!(vi::UntypedVarInfo, sel::Selector) # Get the indices of `vns` that belong to `spl` as a vector - gidcs = _getidcs(vi) + gidcs = _getidcs(vi, sel) if vi.num_produce == 0 for i = length(gidcs):-1:1 vi.flags["del"][gidcs[i]] = true @@ -133,9 +207,9 @@ function set_retained_vns_del_by_spl!(vi::UntypedVarInfo) return nothing end -function set_retained_vns_del_by_spl!(vi::TypedVarInfo) +function set_retained_vns_del_by_spl!(vi::TypedVarInfo, sel::Selector) # Get the indices of `vns` that belong to `spl` as a NamedTuple, one entry for each symbol - gidcs = _getidcs(vi) + gidcs = _getidcs(vi, sel) return _set_retained_vns_del_by_spl!(vi.metadata, gidcs, vi.num_produce) end diff --git a/Old_Examples/demonstarte.jl b/Old_Examples/demonstarte.jl deleted file mode 100644 index f75f2ada..00000000 --- a/Old_Examples/demonstarte.jl +++ /dev/null @@ -1,94 +0,0 @@ - -## It is not yet a package... - -using AdvancedPS -using Libtask -const APS = AdvancedPS -using Distributions -using Plots - -n = 20 -include("AdvancedPS.jl/Example/test_interface.jl") - - -## Our states -vi = Container(zeros(n),[false for i =1:n] ,zeros(n), 0) - - -## Our observations -y = Vector{Float64}(undef,n) -for i = 1:n-1 - y[i] = -i -end - -# Define a short model. -# The syntax is rather simple. Observations need to be reported with report_observation. -# Transitions must be reported using report_transition. -# The trace contains the variables which we want to infer using particle gibbs. -# Thats all! -function task_f() - - var = current_trace() - set_x!(var,1,rand(Normal())) # We sample - report_transition!(var,0.0,0.0) - - for i = 2:n - # Sampling - r = rand(Normal(get_x(var,i-1)-1,0.8)) - set_x!(var,i,r) # We sample from proposal - logγ = logpdf(Normal(get_x(var,i-1)-1,0.8),get_x(var,i)) #γ(x_t|x_t-1) - logp = logpdf(Normal(),get_x(var,i)) # p(x_t|x_t-1) - - - report_transition!(var,logp,logγ) - - #Proposal and Resampling - logpy = logpdf(Normal(get_x(var,i),0.4),y[i-1]) - - var = report_observation!(var,logpy) - - end - - -end - -m = 10 -task = create_task(task_f) - -particlevec = [Trace(vi, task, PGTaskInfo(0.0,0.0),deepcopy) for i =1:m] -particlevec -ws = [0 for i =1:m] - -particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),deepcopy(ws),0,0) - -Algo = APS.SMCAlgorithm(APS.resample_systematic,0.0,APS.SMCUtilityFunctions(copy_container,set_retained_vns_del_by_spl!)) -## Do one SMC step. -vi -APS.sample!(particles,Algo) - - - -particles - -means = [mean([sum(particles.vals[i].vi.x[2:20]-y[1:19]) for i =1:m])] - - - - - - -particles - - - - -particlevec = [Trace(vi, task, PGTaskInfo(0.0,0.0),deepcopy) for i =1:m-1] -push!(particlevec,Trace(particles.vals[1].vi,task,PGTaskInfo(0.0,0.0),deepcopy)) -ws = [0 for i =1:m] -particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),deepcopy(ws),0,0) - -Algo = APS.PGAlgorithm(APS.resample_systematic,0.0,APS.PGUtilityFunctions(copy_container,set_retained_vns_del_by_spl!)) -## Do one SMC step. -APS.sample!(particles,Algo) - -means = [mean([sum(particles.vals[i].vi.x[2:20]-y[1:19]) for i =1:m])] diff --git a/Tests/test_utils/staging.jl b/Tests/test_utils/staging.jl index 15d5853d..68b0b29a 100644 --- a/Tests/test_utils/staging.jl +++ b/Tests/test_utils/staging.jl @@ -47,6 +47,6 @@ macro numerical_testset(args...) esc(:(@stage_testset "numerical" $(args...))) end -macro turing_testset(args...) +macro apf_testset(args...) esc(:(@stage_testset "test" $(args...))) end diff --git a/Tests/tests.jl b/Tests/tests.jl index c800e069..42768788 100644 --- a/Tests/tests.jl +++ b/Tests/tests.jl @@ -11,9 +11,6 @@ include(dir*"/test/test_utils/AllUtils.jl") @turing_testset "AdvancedPS/src/Core/Container/Trace.jl" begin vi = Array{Float64}(undef,10) Trace() - - - end diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index cd2c5148..8f7eb843 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -67,8 +67,7 @@ module AdvancedPS PGASUtilityFunctions, SMCAlgorithm, PGAlgorithm, - update_task! - + forkr include("Inference/Model.jl") include("Inference/Transitions.jl") diff --git a/src/Core/Algorithms/sample.jl b/src/Core/Algorithms/sample.jl index d3c7325c..14e39452 100644 --- a/src/Core/Algorithms/sample.jl +++ b/src/Core/Algorithms/sample.jl @@ -13,12 +13,12 @@ function sample!(pc::PC, alg::ALG, utility_functions::AbstractSMCUtilitFunctions if ref_traj !== nothing || ess <= alg.resampler_threshold * length(pc) # compute weights Ws = weights(pc) + # check that weights are not NaN @assert !any(isnan, Ws) # sample ancestor indices # Ancestor trajectory is not sampled ref_traj !== nothing ? nresamples = n-1 : nresamples = n - indx = alg.resampler(Ws, nresamples) # We add ancestor trajectory to the path. # For ancestor sampling, we would change n at this point. diff --git a/src/Core/Container/Trace.jl b/src/Core/Container/Trace.jl index bff4f72f..a62bb127 100644 --- a/src/Core/Container/Trace.jl +++ b/src/Core/Container/Trace.jl @@ -15,7 +15,7 @@ end # The procedure passes a function which is specified by the model. -function Trace(vi, f::Function, taskinfo, copy_vi::Function) +function Trace(vi::Tvi, f::Function, taskinfo::TInfo, copy_vi::Function) where {Tvi,TInfo<:AbstractTaskInfo} task = CTask( () -> begin res=f(); produce(Val{:done}); res; end ) res = Trace(copy_vi(vi), task, copy(taskinfo)) # CTask(()->f()); @@ -27,7 +27,7 @@ function Trace(vi, f::Function, taskinfo, copy_vi::Function) end ## We need to design the task in the Turing wrapper. -function Trace(vi, task::Task, taskinfo, copy_vi::Function) +function Trace(vi::Tvi, task::Task, taskinfo::TInfo, copy_vi::Function) where {Tvi,TInfo<:AbstractTaskInfo} res = Trace(copy_vi(vi), Libtask.copy(task), copy(taskinfo)) # CTask(()->f()); if res.task.storage === nothing @@ -40,7 +40,7 @@ end # NOTE: this function is called by `forkr` -function Trace(vi, f::Function, taskinfo, copy_vi::Function) +function Trace(vi::Tvi, f::Function, taskinfo::TInfo, copy_vi::Function) where {Tvi,TInfo<:AbstractTaskInfo} # CTask(()->f()); task = CTask( () -> begin res=f(); produce(Val{:done}); res; end ) res = Trace(copy_vi(vi), task, copy(taskinfo)) diff --git a/src/Core/Resample/resample.jl b/src/Core/Resample/resample.jl index 968b4f51..d61c7922 100644 --- a/src/Core/Resample/resample.jl +++ b/src/Core/Resample/resample.jl @@ -10,10 +10,13 @@ function resample!( n = length(pc.vals) # count number of children for each particle num_children = zeros(Int, n) + @inbounds for i in indx num_children[i] += 1 end + + # fork particles particles = collect(pc) children = similar(particles) diff --git a/src/Inference/step.jl b/src/Inference/step.jl index f39a9786..93991a6e 100644 --- a/src/Inference/step.jl +++ b/src/Inference/step.jl @@ -26,13 +26,14 @@ function AbstractMCMC.step!( ) where SPL <: PGSampler n = spl.alg.n + T = Trace{typeof(spl.vi), PGTaskInfo{Float64}} - if hasfield(typeof(spl),:ref_traj) && spl.ref_traj !== nothing + if spl.ref_traj !== nothing particles = T[ Trace(spl.vi, model.task, PGTaskInfo(), spl.uf.copy) for _ =1:n-1] - pc = ParticleContainer{typeof(particles[1])}(particles,zeros(n),0.0,0) + pc = ParticleContainer{typeof(particles[1])}(particles,zeros(n-1),0.0,0) # Reset Task - spl.ref_traj = forkr(spl.ref_traj, uf.copy) + spl.ref_traj = forkr(spl.ref_traj, spl.uf.copy) push!(pc, spl.ref_traj) else particles = T[ Trace(spl.vi, model.task, PGTaskInfo(), spl.uf.copy) for _ =1:n] From f8e91708d6574f26c821546173b475463b93c1d7 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Fri, 6 Dec 2019 20:32:28 +0000 Subject: [PATCH 24/25] Good running model --- Example/Custom_Container/demonstarte.jl | 52 +++++++------------ Example/Custom_Container/test_interface.jl | 35 +++++++------ Example/Using_Turing_VI/Turing_baseline.jl | 10 ++-- .../demonstrate_with_Turing_VI.jl | 42 ++++++--------- Example/Using_Turing_VI/turing_interface.jl | 7 +-- Tests/tests.jl | 5 +- src/Inference/Transitions.jl | 2 + 7 files changed, 66 insertions(+), 87 deletions(-) diff --git a/Example/Custom_Container/demonstarte.jl b/Example/Custom_Container/demonstarte.jl index eb77c96b..e84cc607 100644 --- a/Example/Custom_Container/demonstarte.jl +++ b/Example/Custom_Container/demonstarte.jl @@ -1,8 +1,10 @@ ## It is not yet a package... using Distributions -using Revise using AdvancedPS +using Libtask +using BenchmarkTools +using NamedTupleTools include("AdvancedPS/Example/Custom_Container/test_interface.jl") # Define a short model. # The syntax is rather simple. Observations need to be reported with report_observation. @@ -16,58 +18,40 @@ for i =1:n-1 y[i] = 0 end -function task_f() +function task_f(y) var = initialize() - x = Vector{Float64}(undef,n) - vn = @varname x[1] - x[1] = update_var(var, vn, rand(Normal())) + x = zeros(n,1) + r = vectorize(Normal(), rand(Normal())) + x[1,:] = update_var(var, 1, r) report_transition!(var,0.0,0.0) for i = 2:n # Sampling - r = rand(Normal()) - vn = @varname x[i] - x[i] = update_var(var, vn, r) - logγ = logpdf(Normal(),x[i]) #γ(x_t|x_t-1) - logp = logpdf(Normal(),x[i]) # p(x_t|x_t-1) + r = vectorize(Normal(),rand(Normal())) + x[i,:] = update_var(var, i, r) + logγ = logpdf(Normal(),x[i,1]) #γ(x_t|x_t-1) + logp = logpdf(Normal(),x[i,1]) # p(x_t|x_t-1) report_transition!(var,logp,logγ) #Proposal and Resampling - logpy = logpdf(Normal(x[i], 1.0), y[i-1]) + logpy = logpdf(Normal(x[i,1], 1.0), y[i-1]) var = report_observation!(var,logpy) end end -function task_f2() - var = initialize() - x = Vector{Float64}(undef,n) - vn = @varname x[1] - x[1] = update_var(var, vn, rand(Normal())) - report_transition!(var,0.0,0.0) - for i = 2:n - # Sampling - r = rand(Normal()) - vn = @varname x[i] - x[i] = update_var(var, vn, r) - logγ = 0.0 - logp = 0.0 - report_transition!(var,logp,logγ) - #Proposal and Resampling - logpy = logpdf(Normal(x[i], 1.0), y[i-1]) - var = report_observation!(var,logpy) - end -end -task = create_task(task_f2) +task = create_task(task_f, y) model = PFModel(task) -tcontainer = Container(zeros(n),Vector{Bool}(undef,n),zeros(n),0) +tcontainer = Container(zeros(n,1),Vector{Bool}(undef,n),zeros(n),0) + + alg = AdvancedPS.SMCAlgorithm() uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) -@elapsed sample(model, alg, uf, tcontainer, 10) +@btime sample(model, alg, uf, tcontainer, 100) alg = AdvancedPS.PGAlgorithm(AdvancedPS.resample_systematic, 1.0, 10) uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) -@elapsed chn2 =sample(model, alg, uf, tcontainer, 5) +@btime chn2 =sample(model, alg, uf, tcontainer, 5) diff --git a/Example/Custom_Container/test_interface.jl b/Example/Custom_Container/test_interface.jl index aed3584f..ca27f06d 100644 --- a/Example/Custom_Container/test_interface.jl +++ b/Example/Custom_Container/test_interface.jl @@ -9,7 +9,7 @@ # This is a very shallow container solely for testing puropose mutable struct Container - x::Array{Float64,1} + x::Array{Float64,2} marked::Vector{Bool} produced_at::Vector{Int64} num_produce::Float64 @@ -49,35 +49,38 @@ function report_transition!(trace,logp::Float64,logγ::Float64) trace.taskinfo.logpseq += logp end -function get_x(trace,indx) - @assert trace.vi.marked[indx] "[Interface] This should already be marked!" - return trace.vi.x[indx] -end - -# We only set it if it is not yet marked -function set_x!(trace,indx,val) - if !trace.vi.marked[indx] - trace.vi.x[indx] = val - trace.vi.marked[indx] = true - trace.vi.produced_at[indx] = trace.vi.num_produce +function update_var(trace, vn::Int64, r::Vector{Float64}) + if !trace.vi.marked[vn] + trace.vi.x[vn,:] = r + trace.vi.marked[vn] = true + trace.vi.produced_at[vn] = trace.vi.num_produce + return r end - trace + return trace.vi.x[vn,:] end + # The reason for this is that we need to pass it! function copy_container(vi::Container) Container(deepcopy(vi.x),deepcopy(vi.marked),deepcopy(vi.produced_at),copy(vi.num_produce)) end -function create_task(f::Function) - return CTask(() -> begin new_vi=f(); produce(Val{:done}); new_vi; end ) +function create_task(f::Function, args...) + return CTask(() -> begin new_vi=f(args...); produce(Val{:done}); new_vi; end ) end function tonamedtuple(vi::Container) - return NamedTuple() + tnames = Tuple([Symbol("x$i") for i in 1:size(vi.x)[1]]) + tvalues = Tuple([vi.x[i,:] for i in 1:size(vi.x)[1]]) + return namedtuple(tnames, tvalues) end function Base.empty!(vi::Container) for i in 1:length(vi.marked) vi.marked[i] = false end + vi end +ind2sub(v, i) = Tuple(CartesianIndices(v)[i]) +vectorize(d::UnivariateDistribution, r::Real) = [r] +vectorize(d::MultivariateDistribution, r::AbstractVector{<:Real}) = copy(r) +vectorize(d::MatrixDistribution, r::AbstractMatrix{<:Real}) = copy(vec(r)) diff --git a/Example/Using_Turing_VI/Turing_baseline.jl b/Example/Using_Turing_VI/Turing_baseline.jl index 7baf6a85..6d288b63 100644 --- a/Example/Using_Turing_VI/Turing_baseline.jl +++ b/Example/Using_Turing_VI/Turing_baseline.jl @@ -1,5 +1,5 @@ using Turing - +using BenchmarkTools n = 3000 y = Vector{Float64}(undef,n-1) @@ -7,7 +7,7 @@ for i =1:n-1 y[i] = 0 end -@model demo() = begin +@model demo(y) = begin x = Vector{Float64}(undef,n) x[1] ~ Normal() for i = 2:n @@ -16,6 +16,8 @@ end end end -@elapsed sample(demo(),PG(10),5) -@elapsed sample(demo(),SMC(),10) + + +#@elapsed sample(demo(),PG(10),5) +chn = @btime sample(demo(y), SMC(), 100) diff --git a/Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl b/Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl index 88348647..ddd37c01 100644 --- a/Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl +++ b/Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl @@ -2,26 +2,25 @@ ## It is not yet a package... using Distributions using Turing.Core.RandomVariables -import Turing.Core: @varname +import Turing.Core: @varname, tonamedtuple import Turing.Utilities: vectorize using Turing using Revise using AdvancedPS -import Turing.Core: tonamedtuple include("AdvancedPS/Example/Using_Turing_VI/turing_interface.jl") # Define a short model. # The syntax is rather simple. Observations need to be reported with report_observation. # Transitions must be reported using report_transition. # The trace contains the variables which we want to infer using particle gibbs. # Thats all! -n = 3000 +n = 500 y = Vector{Float64}(undef,n-1) for i =1:n-1 y[i] = 0 end -function task_f() +function task_f(y) var = initialize() x = Vector{Float64}(undef,n) vn = @varname x[1] @@ -33,7 +32,7 @@ function task_f() vn = @varname x[i] x[i] = update_var(var, vn, r) logγ = logpdf(Normal(),x[i]) #γ(x_t|x_t-1) - logp = logpdf(Normal(),x[i]) # p(x_t|x_t-1) + logp = logγ # p(x_t|x_t-1) report_transition!(var,logp,logγ) #Proposal and Resampling logpy = logpdf(Normal(x[i], 1.0), y[i-1]) @@ -43,46 +42,35 @@ end -task = create_task(task_f) -model = PFModel(task) -tonamedtuple(vi::UntypedVarInfo) = tonamedtuple(TypedVarInfo(vi)) -alg = AdvancedPS.SMCAlgorithm() -uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) +task = create_task(task_f, y) +model = PFModel(task) +tonamedtuple(vi::UntypedVarInfo) = tonamedtuple(TypedVarInfo(vi)) +tonamedtuple(vi::TypedVarInfo) = Turing.tonamedtuple() -# Get type stability!! +################################################################# +# Get type stability!! # +################################################################# +alg = AdvancedPS.SMCAlgorithm() +uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) untypedcontainer = VarInfo() T = Trace{typeof(untypedcontainer),SMCTaskInfo{Float64}} particles = T[ Trace(untypedcontainer, task, SMCTaskInfo(), uf.copy) for _ =1:1] pc = ParticleContainer{typeof(particles[1])}(particles,zeros(1),0.0,0) - -@elapsed AdvancedPS.sample!(pc, alg, uf, nothing) - - - - - +AdvancedPS.sample!(pc, alg, uf, nothing) container = uf.empty!(TypedVarInfo(pc[1].vi)) tcontainer = container -# typeof(tcontainer) <: TypedVarInfo -# tcontainer = VarInfo() -# typeof(tcontainer) <: TypedVarInfo -# - - - - alg = AdvancedPS.SMCAlgorithm() uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) -@elapsed sample(model, alg, uf, tcontainer, 10) +@btime sample(model, alg, uf, tcontainer, 100) diff --git a/Example/Using_Turing_VI/turing_interface.jl b/Example/Using_Turing_VI/turing_interface.jl index 16ddee5d..99c39212 100644 --- a/Example/Using_Turing_VI/turing_interface.jl +++ b/Example/Using_Turing_VI/turing_interface.jl @@ -4,6 +4,8 @@ const TypedVarInfo = VarInfo{<:NamedTuple} const Selector = Turing.Selector const BASE_SELECTOR = Selector(:PS) + + function report_observation!(trace, logp::Float64) trace.taskinfo.logp += logp produce(logp) @@ -40,8 +42,8 @@ function update_var(trace, vn, val, dist= Normal()) end # The reason for this is that we need to pass it! -function create_task(f::Function) - return CTask(() -> begin new_vi=f(); produce(Val{:done}); new_vi; end ) +function create_task(f::Function, args...) + return CTask(() -> begin new_vi=f(args...); produce(Val{:done}); new_vi; end ) end @@ -97,7 +99,6 @@ end `setgid!(vi::VarInfo, gid::Selector, vn::VarName)` - Adds `gid` to the set of sampler selectors associated with `vn` in `vi`. """ diff --git a/Tests/tests.jl b/Tests/tests.jl index 42768788..aa8a1beb 100644 --- a/Tests/tests.jl +++ b/Tests/tests.jl @@ -2,13 +2,12 @@ using Test using AdvancedPS -dir = splitdir(splitdir(pathof(Turing))[1])[1] -include(dir*"/test/test_utils/AllUtils.jl") +include("AdvancedPS/Test/test_utils/AllUtils.jl") -@turing_testset "AdvancedPS/src/Core/Container/Trace.jl" begin +@apf_testset "AdvancedPS/src/Core/Container/Trace.jl" begin vi = Array{Float64}(undef,10) Trace() end diff --git a/src/Inference/Transitions.jl b/src/Inference/Transitions.jl index 3d89cba2..7dd56b83 100644 --- a/src/Inference/Transitions.jl +++ b/src/Inference/Transitions.jl @@ -128,3 +128,5 @@ function _params_to_array(ts::Vector{T}) where {T<:AbstractTransition} end return ordered_names, vals end + +ind2sub(v, i) = Tuple(CartesianIndices(v)[i]) From 7229c88042a56001c1ec09e628cdfd882e9c7417 Mon Sep 17 00:00:00 2001 From: donhausk <42518029+donhausk@users.noreply.github.com> Date: Sat, 7 Dec 2019 15:07:04 +0000 Subject: [PATCH 25/25] Ready for PR --- .../demonstarte.jl | 2 +- .../test_interface.jl | 3 - Example/Using_Turing_VI/Turing_baseline.jl | 6 +- .../demonstrate_with_Turing_VI.jl | 11 +- Example/Using_Turing_VI/turing_interface.jl | 11 +- Old_Examples/demonstartePGAS.jl | 77 --------- Old_Examples/demonstrate_turing_vi.jl | 119 ------------- Old_Examples/testfile.jl | 28 --- .../create_data_from_old_turing_model.jl | 25 --- .../test_against_previous_model.jl | 32 ---- Tests/Using_Turing_VI/numerical_tests.jl | 159 ++++++++++++++++++ Tests/test_resample.jl | 19 +++ Tests/test_utils/models.jl | 105 +++++++----- Tests/test_utils/numerical_tests.jl | 21 ++- Tests/tests.jl | 90 ---------- Tests/tests_container.jl | 117 +++++++++++++ src/AdvancedPS.jl | 3 +- 17 files changed, 401 insertions(+), 427 deletions(-) rename Example/{Custom_Container => Using_Custom_VI}/demonstarte.jl (96%) rename Example/{Custom_Container => Using_Custom_VI}/test_interface.jl (95%) delete mode 100644 Old_Examples/demonstartePGAS.jl delete mode 100644 Old_Examples/demonstrate_turing_vi.jl delete mode 100644 Old_Examples/testfile.jl delete mode 100644 Test_with_Turing/create_data_from_old_turing_model.jl delete mode 100644 Test_with_Turing/test_against_previous_model.jl create mode 100644 Tests/Using_Turing_VI/numerical_tests.jl create mode 100644 Tests/test_resample.jl delete mode 100644 Tests/tests.jl create mode 100644 Tests/tests_container.jl diff --git a/Example/Custom_Container/demonstarte.jl b/Example/Using_Custom_VI/demonstarte.jl similarity index 96% rename from Example/Custom_Container/demonstarte.jl rename to Example/Using_Custom_VI/demonstarte.jl index e84cc607..6c3b3d9e 100644 --- a/Example/Custom_Container/demonstarte.jl +++ b/Example/Using_Custom_VI/demonstarte.jl @@ -5,7 +5,7 @@ using AdvancedPS using Libtask using BenchmarkTools using NamedTupleTools -include("AdvancedPS/Example/Custom_Container/test_interface.jl") +include("AdvancedPS/Example/Using_Custom_VI/test_interface.jl") # Define a short model. # The syntax is rather simple. Observations need to be reported with report_observation. # Transitions must be reported using report_transition. diff --git a/Example/Custom_Container/test_interface.jl b/Example/Using_Custom_VI/test_interface.jl similarity index 95% rename from Example/Custom_Container/test_interface.jl rename to Example/Using_Custom_VI/test_interface.jl index ca27f06d..45e98465 100644 --- a/Example/Custom_Container/test_interface.jl +++ b/Example/Using_Custom_VI/test_interface.jl @@ -37,8 +37,6 @@ end const initialize = current_trace function report_observation!(trace, logp::Float64) - trace.taskinfo.logp += logp - trace.vi.num_produce += 1 produce(logp) trace = current_trace() end @@ -80,7 +78,6 @@ function Base.empty!(vi::Container) end vi end -ind2sub(v, i) = Tuple(CartesianIndices(v)[i]) vectorize(d::UnivariateDistribution, r::Real) = [r] vectorize(d::MultivariateDistribution, r::AbstractVector{<:Real}) = copy(r) vectorize(d::MatrixDistribution, r::AbstractMatrix{<:Real}) = copy(vec(r)) diff --git a/Example/Using_Turing_VI/Turing_baseline.jl b/Example/Using_Turing_VI/Turing_baseline.jl index 6d288b63..1c9a8f5a 100644 --- a/Example/Using_Turing_VI/Turing_baseline.jl +++ b/Example/Using_Turing_VI/Turing_baseline.jl @@ -1,6 +1,6 @@ using Turing using BenchmarkTools -n = 3000 +n = 500 y = Vector{Float64}(undef,n-1) for i =1:n-1 @@ -8,7 +8,7 @@ for i =1:n-1 end @model demo(y) = begin - x = Vector{Float64}(undef,n) + x = TArray{Float64}(undef,n) x[1] ~ Normal() for i = 2:n x[i] ~ Normal() @@ -21,3 +21,5 @@ end #@elapsed sample(demo(),PG(10),5) chn = @btime sample(demo(y), SMC(), 100) + +chn.na.axes diff --git a/Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl b/Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl index ddd37c01..b17048da 100644 --- a/Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl +++ b/Example/Using_Turing_VI/demonstrate_with_Turing_VI.jl @@ -1,12 +1,9 @@ ## It is not yet a package... using Distributions -using Turing.Core.RandomVariables -import Turing.Core: @varname, tonamedtuple -import Turing.Utilities: vectorize -using Turing -using Revise using AdvancedPS +using BenchmarkTools +using Libtask include("AdvancedPS/Example/Using_Turing_VI/turing_interface.jl") # Define a short model. # The syntax is rather simple. Observations need to be reported with report_observation. @@ -22,7 +19,7 @@ end function task_f(y) var = initialize() - x = Vector{Float64}(undef,n) + x = TArray{Float64}(undef,n) vn = @varname x[1] x[1] = update_var(var, vn, rand(Normal())) report_transition!(var,0.0,0.0) @@ -48,8 +45,6 @@ end task = create_task(task_f, y) model = PFModel(task) -tonamedtuple(vi::UntypedVarInfo) = tonamedtuple(TypedVarInfo(vi)) -tonamedtuple(vi::TypedVarInfo) = Turing.tonamedtuple() ################################################################# diff --git a/Example/Using_Turing_VI/turing_interface.jl b/Example/Using_Turing_VI/turing_interface.jl index 99c39212..4ae8ef10 100644 --- a/Example/Using_Turing_VI/turing_interface.jl +++ b/Example/Using_Turing_VI/turing_interface.jl @@ -1,3 +1,10 @@ + +using Turing.Core.RandomVariables +import Turing.Core: @varname +import Turing.Utilities: vectorize +using Turing +using AdvancedPS + # This is important for initalizaiton const initialize = AdvancedPS.current_trace const TypedVarInfo = VarInfo{<:NamedTuple} @@ -7,7 +14,6 @@ const BASE_SELECTOR = Selector(:PS) function report_observation!(trace, logp::Float64) - trace.taskinfo.logp += logp produce(logp) trace = AdvancedPS.current_trace() end @@ -60,7 +66,8 @@ end # the SampleFromPrior sampler - +tonamedtuple(vi::TypedVarInfo) = Turing.tonamedtuple(vi) +tonamedtuple(vi::UntypedVarInfo) = tonamedtuple(TypedVarInfo(vi)) """ diff --git a/Old_Examples/demonstartePGAS.jl b/Old_Examples/demonstartePGAS.jl deleted file mode 100644 index 600c53af..00000000 --- a/Old_Examples/demonstartePGAS.jl +++ /dev/null @@ -1,77 +0,0 @@ -## It is not yet a package... -using AdvancedPS -using Libtask -APS = AdvancedPS -using Distributions -n = 20 -using - - - - -## Our states -vi = Container(zeros(n),0) -## Our observations -y = Vector{Float64}(undef,n) -for i = 1:n-1 - y[i] = -i -end - -# Define a short model. -# The syntax is rather simple. Observations need to be reported with report_observation. -# Transitions must be reported using report_transition. -# The trace contains the variables which we want to infer using particle gibbs. -# Thats all! -function task_f() - var = APS.init() - APS.set_x(var,1,rand(Normal())) # We sample - APS.report_transition(var,0.0,0.0) - for i = 2:n - # Sampling - APS.set_x(var,i, rand(Normal(APS.get_x(var,i-1)-1,0.8))) # We sample from proposal - logγ = logpdf(Normal(APS.get_x(var,i-1)-1,0.8),APS.get_x(var,i)) #γ(x_t|x_t-1) - logp = logpdf(Normal(),APS.get_x(var,i)) # p(x_t|x_t-1) - - - APS.report_transition(var,logp,logγ) - - #Proposal and Resampling - logpy = logpdf(Normal(APS.get_x(var,i),0.4),y[i-1]) - - var = APS.report_observation(var,logpy) - if var.taskinfo.hold - produce(0.0) - end - - end - -end - - - -particles = APS.ParticleContainer{typeof(vi),APS.PGASTaskInfo }() - - -m = 10 -task = APS.create_task(task_f) - - -APS.extend!(particles, 10, vi, task, APS.PGASTaskInfo(0.0,0.0)) -## Do one SMC step. -APS.samplePGAS!(particles) - -particles2 = APS.ParticleContainer{typeof(vi),APS.PGASTaskInfo }() - - -m = 10 -task = APS.create_task(task_f) - - -APS.extend!(particles2, 9, vi, task, APS.PGASTaskInfo(0.0,0.0,true)) -APS.extend!(particles2, 1, particles[1].vi, task, APS.PGASTaskInfo(0.0,0.0,true)) - -particles2.manipulators["merge_traj"] = (x,y,i=0) -> y # Obviously this function is wrong! -## Do one SMC step. -APS.samplePGAS!(particles2,APS.resample_systematic,particles2[m]) - -particles2 diff --git a/Old_Examples/demonstrate_turing_vi.jl b/Old_Examples/demonstrate_turing_vi.jl deleted file mode 100644 index f89d5c8e..00000000 --- a/Old_Examples/demonstrate_turing_vi.jl +++ /dev/null @@ -1,119 +0,0 @@ - -## It is not yet a package... - -using AdvancedPS -using Libtask -const APS = AdvancedPS -using Distributions -using Plots -using Turing.Core.RandomVariables -import Turing.Core: @varname -include("AdvancedPS.jl/Example/turing_interface.jl") -import Turing: SampleFromPrior -using StatsFuns: softmax! -using AbstractMCMC -# Define a short model. -# The syntax is rather simple. Observations need to be reported with report_observation. -# Transitions must be reported using report_transition. -# The trace contains the variables which we want to infer using particle gibbs. -# Thats all! -n = 20 - -y = Vector{Float64}(undef,n-1) -for i =1:n-1 - y[i] = sin(0.1*i) -end - -function task_f() - var = initialize() - x = Vector{Float64}(undef,n) - vn = @varname x[1] - x[1] = update_var(var, vn, rand(Normal())) - report_transition!(var,0.0,0.0) - for i = 2:n - # Sampling - r = rand(Normal(x[i-1],0.2)) - vn = @varname x[i] - x[i] = update_var(var, vn, r) - logγ = logpdf(Normal(x[i-1],0.2),x[i]) #γ(x_t|x_t-1) - logp = logpdf(Normal(),x[i]) # p(x_t|x_t-1) - report_transition!(var,logp,logγ) - #Proposal and Resampling - logpy = logpdf(Normal(x[i],0.2),y[i-1]) - var = report_observation!(var,logpy) - end -end - - -task = create_task(task_f) - - - - -### This is for initilaization!! -m = 1 -vi_c = VarInfo() -task -particlevec = [APS.Trace(vi_c, task, PGTaskInfo(0.0,0.0),deepcopy) for i =1:m] -particlevec -ws = [0 for i =1:m] - -particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),zeros(m),0,0) - - -Algo = APS.SMCAlgorithm(APS.resample_systematic,1.0,APS.SMCUtilityFunctions(deepcopy,set_retained_vns_del_by_spl!)) -## Do one SMC step. - -@elapsed APS.sample!(particles,Algo) - -vi_c = empty!(VarInfo{<:NamedTuple}(particles[1].vi)) - -const vi_ct = vi_c - - -m = 200 -task = create_task(task_f) -task -particlevec = [APS.Trace(vi_c, task, PGTaskInfo(0.0,0.0),deepcopy) for i =1:m] -particlevec -ws = [0 for i =1:m] - -particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),zeros(m),0,0) - - -Algo = APS.SMCAlgorithm(APS.resample_systematic,0.5,APS.SMCUtilityFunctions(deepcopy,set_retained_vns_del_by_spl!)) -## Do one SMC step. - - -@elapsed APS.sample!(particles,Algo) - - - -particles - -#means = [mean([sum(particles.vals[i].vi.vals[2:20]-y[1:19])^2 for i =1:m])] - - -set_retained_vns_del_by_spl!(vi_c, SampleFromPrior()) -for l = 2:10 - global particles - indx = AdvancedPS.randcat(softmax!(copy(particles.logWs))) - - println(indx) - particlevec = [Trace(vi_c, task, PGTaskInfo(0.0,0.0),deepcopy) for i =1:m] - - #push!(particlevec,Trace(particles.vals[indx].vi, task, PGTaskInfo(0.0,0.0), deepcopy)) - ws = [0 for i =1:m] - particles = APS.ParticleContainer{typeof(particlevec[1])}(particlevec,deepcopy(ws),deepcopy(ws),0,0) - Algo = APS.PGAlgorithm(APS.resample_systematic,1.0,APS.SMCUtilityFunctions(deepcopy,set_retained_vns_del_by_spl!)) - ## Do one SMC step. - APS.sample!(particles,Algo,particles[indx]) - - push!(means, mean([sum(particles.vals[i].vi.vals[2:20]-y[1:19])^2 for i =1:m])) -end - -particles - -plot(means) - -means diff --git a/Old_Examples/testfile.jl b/Old_Examples/testfile.jl deleted file mode 100644 index ab2b1be3..00000000 --- a/Old_Examples/testfile.jl +++ /dev/null @@ -1,28 +0,0 @@ - -v = VarInfo() - -vn = @varname hi - -push!(v,vn,0.8,Normal()) - -v[vn] = [1.2] - -v2 = VarInfo(v) - -typeof(v) <: UntypedVarInfo -typeof(v2) <:UntypedVarInfo - -v2.num_produce = 0 - -fl = Vector{Float64}(undef,1) -fl[1] = 4.0 -setindex!(v2,fl,vn) -v2 - -v2 = VarInfo{<:NamedTuple}(v) - -v2 = TypedVarInfo(v) - -v2[vn] = [3.0] - -empty!(v2) diff --git a/Test_with_Turing/create_data_from_old_turing_model.jl b/Test_with_Turing/create_data_from_old_turing_model.jl deleted file mode 100644 index b4b5fcd7..00000000 --- a/Test_with_Turing/create_data_from_old_turing_model.jl +++ /dev/null @@ -1,25 +0,0 @@ -# Import packages. -using Turing -using Random; Random.seed!(1) -using Distributed -using DistributionsAD -# Define a simple Normal model with unknown mean and variance. -@model gdemo(y) = begin - s ~ Exponential(0.2) - m ~ Normal(0, sqrt(s)) - x = Vector{Real}(undef,10) - x[1] ~ Normal(m,s) - y[1] ~ Normal(x[1],0.5) - for i = 2:10 - x[i] ~ Normal(x[i-1],s) - y[i] ~ Normal(x[i],0.5) - end - -end - -y = Vector{Float64}(1:10) - -chn1 = sample(gdemo(y),SMC(),1000) -write("Old_Model_SMC.jls", chn1) -chn2 = sample(gdemo(y),PG(100),100) -write("Old_Model_PG.jls",chn2) diff --git a/Test_with_Turing/test_against_previous_model.jl b/Test_with_Turing/test_against_previous_model.jl deleted file mode 100644 index 0bc720a7..00000000 --- a/Test_with_Turing/test_against_previous_model.jl +++ /dev/null @@ -1,32 +0,0 @@ -# Import packages. - -using Turing -using Random; Random.seed!(1) -using Distributed -using DistributionsAD -# Define a simple Normal model with unknown mean and variance. -@Turing.model gdemo(y) = begin - s ~ Exponential(0.2) - m ~ Normal(0, sqrt(s)) - x = Vector{Real}(undef,10) - x[1] ~ Normal(m,s) - y[1] ~ Normal(x[1],0.5) - for i = 2:10 - x[i] ~ Normal(x[i-1],s) - y[i] ~ Normal(x[i],0.5) - end - -end - -y = Vector{Float64}(1:10) - -chn1 = Turing.sample(gdemo(y),Turing.SMC(),1000) -write("tmp.jls", chn1) -chn2 = Turing.sample(gdemo(y),Turing.PG(100),100) -write("tmp2.jls",chn2) - -chn1_old = read("Old_Model_SMC.jls", Chains) -chn2_old = read("Old_Model_PG.jls", Chains) - -sum(chn1.value.data - chn1_old.value.data) -sum(chn2.value.data- chn2_old.value.data) diff --git a/Tests/Using_Turing_VI/numerical_tests.jl b/Tests/Using_Turing_VI/numerical_tests.jl new file mode 100644 index 00000000..abb5f878 --- /dev/null +++ b/Tests/Using_Turing_VI/numerical_tests.jl @@ -0,0 +1,159 @@ +using Random +using Test +using Distributions +using Turing +using AdvancedPS + +dir = splitdir(splitdir(pathof(AdvancedPS))[1])[1] + +include(dir*"/Example/Using_Turing_VI/turing_interface.jl") +include(dir*"/Tests/test_utils/AllUtils.jl") + +import Turing.Core: tonamedtuple +tonamedtuple(vi::UntypedVarInfo) = tonamedtuple(TypedVarInfo(vi)) + + +@testset "apf.jl" begin + @apf_testset "apf constructor" begin + N = 200 + f = aps_gdemo_default + task = create_task(f) + model = PFModel(task) + tcontainer = VarInfo() + + alg = AdvancedPS.SMCAlgorithm() + uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, N) + + alg = AdvancedPS.PGAlgorithm(5) + uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, N) + + end + @numerical_testset "apf inference" begin + N = 2000 + f = aps_gdemo_default + task = create_task(f) + model = PFModel(task) + tcontainer = VarInfo() + + alg = AdvancedPS.SMCAlgorithm() + uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, N) + check_gdemo(caps1, atol = 0.1) + + alg = AdvancedPS.PGAlgorithm(5) + uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, N) + check_gdemo(caps1, atol = 0.1) + end +end + + + + + +@testset "test_against_turing.jl" begin + + @numerical_testset "apf inference" begin + N = 5000 + + + ## Testset1 + y = [0 for i in 1:10] + + # We use the Turing model as baseline + chn_base = sample(large_demo(y),PG(20),N) + + ############################################# + # Using a Proposal # + ############################################# + f = large_demo_apf_proposal + task = create_task(f,y) + model = PFModel(task) + tcontainer = VarInfo() + + ##SMC + alg = AdvancedPS.SMCAlgorithm() + uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, 5*N) + check_numerical(caps1,chn_base, 0.1, 0.2) + + # PG + alg = AdvancedPS.PGAlgorithm(20) + uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, N) + check_numerical(caps1,chn_base,0.1, 0.2) + + + + ############################################# + # No Proposal # + ############################################# + f = large_demo_apf + task = create_task(f,y) + model = PFModel(task) + tcontainer = VarInfo() + + #SMC + alg = AdvancedPS.SMCAlgorithm() + uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, 5*N) + check_numerical(caps1,chn_base, 0.1, 0.2) + + # PG + alg = AdvancedPS.PGAlgorithm(20) + uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, N) + check_numerical(caps1,chn_base,0.1, 0.2) + + + + ## Testset2 + y = [-0.1*i for i in 1:10] + + # We use the Turing model as baseline + chn_base = sample(large_demo(y),PG(20),N) + + ############################################# + # Using a Proposal # + ############################################# + f = large_demo_apf_proposal + task = create_task(f,y) + model = PFModel(task) + tcontainer = VarInfo() + + #SMC + alg = AdvancedPS.SMCAlgorithm() + uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, 5*N) + check_numerical(caps1,chn_base,0.1, 0.2) + + # PG + alg = AdvancedPS.PGAlgorithm(20) + uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, N) + check_numerical(caps1,chn_base, 0.1, 0.2) + + + ############################################# + # No Proposal # + ############################################# + f = large_demo_apf + task = create_task(f,y) + model = PFModel(task) + tcontainer = VarInfo() + + #SMC + alg = AdvancedPS.SMCAlgorithm() + uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, 5*N) + check_numerical(caps1,chn_base,0.21, 0.2) + + # PG + alg = AdvancedPS.PGAlgorithm(20) + uf = AdvancedPS.PGUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + caps1 = sample(model, alg, uf, tcontainer, N) + check_numerical(caps1,chn_base, 0.1, 0.2) + end +end diff --git a/Tests/test_resample.jl b/Tests/test_resample.jl new file mode 100644 index 00000000..54a000e7 --- /dev/null +++ b/Tests/test_resample.jl @@ -0,0 +1,19 @@ + + + +@apf_testset "resample.jl" begin + D = [0.3, 0.4, 0.3] + num_samples = Int(1e6) + resSystematic = AdvancedPS.resample_systematic(D, num_samples ) + resStratified = AdvancedPS.resample_stratified(D, num_samples ) + resMultinomial= AdvancedPS.resample_multinomial(D, num_samples ) + resResidual = AdvancedPS.resample_residual(D, num_samples ) + AdvancedPS.resample(D) + resSystematic2=AdvancedPS.resample(D, num_samples ) + + @test sum(resSystematic .== 2) ≈ (num_samples * 0.4) atol=1e-3*num_samples + @test sum(resSystematic2 .== 2) ≈ (num_samples * 0.4) atol=1e-3*num_samples + @test sum(resStratified .== 2) ≈ (num_samples * 0.4) atol=1e-3*num_samples + @test sum(resMultinomial .== 2) ≈ (num_samples * 0.4) atol=1e-2*num_samples + @test sum(resResidual .== 2) ≈ (num_samples * 0.4) atol=1e-2*num_samples +end diff --git a/Tests/test_utils/models.jl b/Tests/test_utils/models.jl index af207621..0327f1ea 100644 --- a/Tests/test_utils/models.jl +++ b/Tests/test_utils/models.jl @@ -1,11 +1,3 @@ -# The old-gdemo model. -@model gdemo(x, y) = begin - s ~ InverseGamma(2, 3) - m ~ Normal(0, sqrt(s)) - x ~ Normal(m, sqrt(s)) - y ~ Normal(m, sqrt(s)) - return s, m -end @model gdemo_d() = begin s ~ InverseGamma(2, 3) @@ -17,37 +9,74 @@ end gdemo_default = gdemo_d() -@model MoGtest(D) = begin - mu1 ~ Normal(1, 1) - mu2 ~ Normal(4, 1) - z1 ~ Categorical(2) - if z1 == 1 - D[1] ~ Normal(mu1, 1) - else - D[1] ~ Normal(mu2, 1) - end - z2 ~ Categorical(2) - if z2 == 1 - D[2] ~ Normal(mu1, 1) - else - D[2] ~ Normal(mu2, 1) - end - z3 ~ Categorical(2) - if z3 == 1 - D[3] ~ Normal(mu1, 1) - else - D[3] ~ Normal(mu2, 1) - end - z4 ~ Categorical(2) - if z4 == 1 - D[4] ~ Normal(mu1, 1) - else - D[4] ~ Normal(mu2, 1) +function gdemo_d_apf() + var = initialize() + r = rand(InverseGamma(2, 3)) + vn = @varname s + s = update_var(var, vn, r) + + r = rand(Normal(0, sqrt(s))) + vn = @varname m + m = update_var(var, vn, r) + + logp = logpdf(Normal(m, sqrt(s)), 1.5) + report_observation!(var,logp) + + logp = logpdf(Normal(m, sqrt(s)), 2.0) + report_observation!(var,logp) +end + + +aps_gdemo_default = gdemo_d_apf + +@model large_demo(y) = begin + x = TArray{Float64}(undef,11) + x[1] ~ Normal() + for i = 2:11 + x[i] ~ Normal(0.1*x[i-1]-0.1,0.5) + y[i-1] ~ Normal(x[i],0.3) end - z1, z2, z3, z4, mu1, mu2 end -MoGtest_default = MoGtest([1.0 1.0 4.0 4.0]) +function large_demo_apf(y) + var = initialize() + x = TArray{Float64}(undef,11) + vn = @varname x[1] + x[1] = update_var(var, vn, rand(Normal())) + # there is nothing to report since we are not using proposal sampling + report_transition!(var,0.0,0.0) + for i = 2:11 + # Sampling + r = rand( Normal(0.1*x[i-1]-0.1,0.5)) + vn = @varname x[i] + x[i] = update_var(var, vn, r) + logγ = logpdf( Normal(0.1*x[i-1]-0.1,0.5),x[i]) #γ(x_t|x_t-1) + logp = logγ # p(x_t|x_t-1) + report_transition!(var,logp,logγ) + #Proposal and Resampling + logpy = logpdf(Normal(x[i], 0.3), y[i-1]) + var = report_observation!(var,logpy) + end +end -# Declare empty model to make the Sampler constructor work. -@model empty_model() = begin x = 1; end +## Here, we even have a proposal distributin +function large_demo_apf_proposal(y) + var = initialize() + x = TArray{Float64}(undef,11) + vn = @varname x[1] + x[1] = update_var(var, vn, rand(Normal())) + # there is nothing to report since we are not using proposal sampling + report_transition!(var,0.0,0.0) + for i = 2:11 + # Sampling + r = rand(Normal(0.1*x[i-1],0.5)) + vn = @varname x[i] + x[i] = update_var(var, vn, r) + logγ = logpdf(Normal(0.1*x[i-1],0.5),x[i]) #γ(x_t|x_t-1) + logp = logpdf( Normal(0.1*x[i-1]-0.1,0.5),x[i]) # p(x_t|x_t-1) + report_transition!(var,logp,logγ) + #Proposal and Resampling + logpy = logpdf(Normal(x[i], 0.3), y[i-1]) + var = report_observation!(var,logpy) + end +end diff --git a/Tests/test_utils/numerical_tests.jl b/Tests/test_utils/numerical_tests.jl index 88e2b4e9..3e981fbd 100644 --- a/Tests/test_utils/numerical_tests.jl +++ b/Tests/test_utils/numerical_tests.jl @@ -52,11 +52,30 @@ function check_numerical(chain, end end +# Compare two chains +function check_numerical(chain1, + chain2, + atol=0.2, + rtol=0.0) + for name in zip(chain2.name_map.parameters) + sym = Symbol(name[1]) + val = chain2[sym].value[1] isa Real ? + mean(chain2[sym].value) : + vec(mean(chain2[sym].value, dims=[1])) + E = val isa Real ? + mean(chain1[sym].value) : + vec(mean(chain1[sym].value, dims=[1])) + @info (symbol=sym, exact=val, evaluated=E) + @test E ≈ val atol=atol rtol=rtol + end +end + + + # Wrapper function to quickly check gdemo accuracy. function check_gdemo(chain; atol=0.2, rtol=0.0) check_numerical(chain, [:s, :m], [49/24, 7/6], atol=atol, rtol=rtol) end - # Wrapper function to check MoGtest. function check_MoGtest_default(chain; atol=0.2, rtol=0.0) check_numerical(chain, diff --git a/Tests/tests.jl b/Tests/tests.jl deleted file mode 100644 index aa8a1beb..00000000 --- a/Tests/tests.jl +++ /dev/null @@ -1,90 +0,0 @@ -using Test -using AdvancedPS - - - -include("AdvancedPS/Test/test_utils/AllUtils.jl") - - - -@apf_testset "AdvancedPS/src/Core/Container/Trace.jl" begin - vi = Array{Float64}(undef,10) - Trace() -end - - -include("AdvancedPS/src/Core/Container/Trace.jl" ) - - -asdf -@testset "container.jl" begin - @turing_testset "copy particle container" begin - pc = ParticleContainer(x -> x * x, Trace[]) - newpc = copy(pc) - @test newpc.logE == pc.logE - @test newpc.logWs == pc.logWs - @test newpc.n_consume == pc.n_consume - @test typeof(pc) === typeof(newpc) - end - @turing_testset "particle container" begin - n = Ref(0) - alg = PG(5) - spl = Turing.Sampler(alg, empty_model()) - dist = Normal(0, 1) - function fpc() - t = TArray(Float64, 1); - t[1] = 0; - while true - ct = current_trace() - vn = VarName(gensym(), :x, "[$n]", 1) - Turing.assume(spl, dist, vn, ct.vi); n[] += 1; - produce(0) - vn = VarName(gensym(), :x, "[$n]", 1) - Turing.assume(spl, dist, vn, ct.vi); n[] += 1; - t[1] = 1 + t[1] - end - end - model = Turing.Model{(:x,),()}(fpc, NamedTuple(), NamedTuple()) - particles = [Trace(fpc, model, spl, Turing.VarInfo()) for _ in 1:3] - pc = ParticleContainer(fpc, particles) - @test weights(pc) == [1/3, 1/3, 1/3] - @test logZ(pc) ≈ log(3) - @test pc.logE ≈ log(1) - @test consume(pc) == log(1) - resample!(pc) - @test weights(pc) == [1/3, 1/3, 1/3] - @test logZ(pc) ≈ log(3) - @test pc.logE ≈ log(1) - @test effectiveSampleSize(pc) == 3 - @test consume(pc) ≈ log(1) - resample!(pc) - @test consume(pc) ≈ log(1) - end - @turing_testset "trace" begin - n = Ref(0) - alg = PG(5) - spl = Turing.Sampler(alg, empty_model()) - dist = Normal(0, 1) - function f2() - t = Turray(Int, 1); - t[1] = 0; - while true - ct = current_trace() - vn = VarName(gensym(), :x, "[$n]", 1) - Turing.assume(spl, dist, vn, ct.vi); n[] += 1; - produce(t[1]); - vn = VarName(gensym(), :x, "[$n]", 1) - Turing.assume(spl, dist, vn, ct.vi); n[] += 1; - t[1] = 1 + t[1] - end - end - # Test task copy version of trace - model = Turing.Model{(:x,),()}(f2, NamedTuple(), NamedTuple()) - tr = Trace(f2, model, spl, Turing.VarInfo()) - consume(tr); consume(tr) - a = fork(tr); - consume(a); consume(a) - @test consume(tr) == 2 - @test consume(a) == 4 - end -end diff --git a/Tests/tests_container.jl b/Tests/tests_container.jl new file mode 100644 index 00000000..d91b3662 --- /dev/null +++ b/Tests/tests_container.jl @@ -0,0 +1,117 @@ +using Test +using AdvancedPS + + +using Random +using Test +using Distributions +using Turing +using AdvancedPS + +dir = splitdir(splitdir(pathof(AdvancedPS))[1])[1] + +include(dir*"/Example/Using_Turing_VI/turing_interface.jl") +include(dir*"/Tests/test_utils/AllUtils.jl") +import Turing.Core: tonamedtuple +tonamedtuple(vi::UntypedVarInfo) = tonamedtuple(TypedVarInfo(vi)) + + + + + + + + + +@testset "Particle.jl" begin + @apf_testset "copy particle container" begin + pc = ParticleContainer(Trace[], Float64[], 0.0, 0) + uf = AdvancedPS.SMCUtilityFunctions(deepcopy, () -> (), () -> (), () ->()) + newpc = copy(pc, uf) + @test newpc.logE == pc.logE + @test newpc.logWs == pc.logWs + @test newpc.n_consume == pc.n_consume + @test typeof(pc) === typeof(newpc) + end + + @apf_testset "particle container" begin + n = Ref(0) + alg = AdvancedPS.PGAlgorithm(5) + uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + + dist = Normal(0, 1) + + function fpc() + t = TArray(Float64, 1); + var = initialize() + t[1] = 0; + while true + vn = @varname x[n] + r = rand(dist) + update_var(var, vn, r) + n[] += 1 + produce(0) + var = current_trace() + r = rand(dist) + update_var(var, vn, r) + t[1] = 1 + t[1] + end + end + + task = create_task(fpc) + model = PFModel(task) + particles = [Trace(Turing.VarInfo(), fpc, PGTaskInfo(0.0, 0.0), deepcopy) for _ in 1:3] + pc = ParticleContainer(particles) + + @test weights(pc) == [1/3, 1/3, 1/3] + @test logZ(pc) ≈ log(3) + @test pc.logE ≈ log(1) + @test consume(pc) == log(1) + + Ws = weights(pc) + indx = alg.resampler(Ws, length(pc)) + resample!(pc, uf, indx, nothing) + @test weights(pc) == [1/3, 1/3, 1/3] + @test logZ(pc) ≈ log(3) + @test pc.logE ≈ log(1) + @test AdvancedPS.effectiveSampleSize(pc) == 3 + @test consume(pc) ≈ log(1) + Ws = weights(pc) + indx = alg.resampler(Ws, length(pc)) + resample!(pc, uf, indx, nothing) + @test consume(pc) ≈ log(1) + end + + + @apf_testset "trace" begin + n = Ref(0) + alg = AdvancedPS.PGAlgorithm(5) + uf = AdvancedPS.SMCUtilityFunctions(deepcopy, set_retained_vns_del_by_spl!, empty!, tonamedtuple) + dist = Normal(0, 1) + function f2() + t = TArray(Float64, 1); + var = initialize() + t[1] = 0; + while true + vn = @varname x[n] + r = rand(dist) + update_var(var, vn, r) + produce(t[1]); + var = current_trace() + vn = @varname x[n] + r = rand(dist) + update_var(var, vn, r) + t[1] = 1 + t[1] + end + end + task = create_task(fpc) + model = PFModel(task) + tr = Trace(Turing.VarInfo(), f2, PGTaskInfo(0.0, 0.0), deepcopy) + consume(tr); consume(tr) + a = AdvancedPS.fork(tr, deepcopy); + consume(a); consume(a) + @test consume(tr) == 2 + @test consume(a) == 4 + + end +end diff --git a/src/AdvancedPS.jl b/src/AdvancedPS.jl index 8f7eb843..0d558a7d 100644 --- a/src/AdvancedPS.jl +++ b/src/AdvancedPS.jl @@ -9,7 +9,8 @@ module AdvancedPS using AbstractMCMC import MCMCChains: Chains import Base.copy - + using Distributions + abstract type AbstractTaskInfo end abstract type AbstractParticleContainer end abstract type AbstractTrace end