Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
*.jl.cov
*.jl.*.cov
*.jl.mem
/Manifest.toml
Manifest.toml
/test/Manifest.toml
1 change: 0 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ Random = "<0.0.1, 1"
Random123 = "1.3"
Requires = "1.0"
StatsFuns = "0.9, 1"
SSMProblems = "0.1"
julia = "1.7"

[extras]
Expand Down
102 changes: 52 additions & 50 deletions examples/gaussian-process/script.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,76 @@ using Distributions
using Libtask
using SSMProblems

Parameters = @NamedTuple begin
a::Float64
q::Float64
kernel
struct GaussianProcessDynamics{T<:Real,KT<:Kernel} <: LatentDynamics{T,T}
proc::GP{ZeroMean{T},KT}
function GaussianProcessDynamics(::Type{T}, kernel::KT) where {T<:Real,KT<:Kernel}
return new{T,KT}(GP(ZeroMean{T}(), kernel))
end
end

mutable struct GPSSM <: SSMProblems.AbstractStateSpaceModel
X::Vector{Float64}
observations::Vector{Float64}
θ::Parameters

GPSSM(params::Parameters) = new(Vector{Float64}(), params)
GPSSM(y::Vector{Float64}, params::Parameters) = new(Vector{Float64}(), y, params)
struct LinearGaussianDynamics{T<:Real} <: LatentDynamics{T,T}
a::T
b::T
q::T
end

seed = 1
T = 100
Nₚ = 20
Nₛ = 250
a = 0.9
q = 0.5

params = Parameters((a, q, SqExponentialKernel()))
function SSMProblems.distribution(proc::LinearGaussianDynamics{T}) where {T<:Real}
return Normal(zero(T), proc.q)
end

f(θ::Parameters, x, t) = Normal(θ.a * x, θ.q)
h(θ::Parameters) = Normal(0, θ.q)
g(θ::Parameters, x, t) = Normal(0, exp(0.5 * x)^2)
function SSMProblems.distribution(proc::LinearGaussianDynamics, ::Int, state)
return Normal(proc.a * state + proc.b, proc.q)
end

rng = Random.MersenneTwister(seed)
struct StochasticVolatility{T<:Real} <: ObservationProcess{T,T} end

x = zeros(T)
y = similar(x)
x[1] = rand(rng, h(params))
for t in 1:T
if t < T
x[t + 1] = rand(rng, f(params, x[t], t))
end
y[t] = rand(rng, g(params, x[t], t))
function SSMProblems.distribution(::StochasticVolatility{T}, ::Int, state) where {T<:Real}
return Normal(zero(T), exp((1 / 2) * state))
end

function gp_update(model::GPSSM, state, step)
gp = GP(model.θ.kernel)
prior = gp(1:(step - 1))
post = posterior(prior, model.X[1:(step - 1)])
μ, σ = mean_and_cov(post, [step])
return Normal(μ[1], σ[1])
function LinearGaussianStochasticVolatilityModel(a::T, q::T) where {T<:Real}
dyn = LinearGaussianDynamics(a, zero(T), q)
obs = StochasticVolatility{T}()
return SSMProblems.StateSpaceModel(dyn, obs)
end

SSMProblems.transition!!(rng::AbstractRNG, model::GPSSM) = rand(rng, h(model.θ))
function SSMProblems.transition!!(rng::AbstractRNG, model::GPSSM, state, step)
return rand(rng, gp_update(model, state, step))
function GaussianProcessStateSpaceModel(::Type{T}, kernel::KT) where {T<:Real,KT<:Kernel}
dyn = GaussianProcessDynamics(T, kernel)
obs = StochasticVolatility{T}()
return SSMProblems.StateSpaceModel(dyn, obs)
end

function SSMProblems.emission_logdensity(model::GPSSM, state, step)
return logpdf(g(model.θ, state, step), model.observations[step])
end
function SSMProblems.transition_logdensity(model::GPSSM, prev_state, current_state, step)
return logpdf(gp_update(model, prev_state, step), current_state)
const GPSSM{T,KT<:Kernel} = SSMProblems.StateSpaceModel{
T,
GaussianProcessDynamics{T,KT},
StochasticVolatility{T}
};

# for non-markovian models, we can redefine dynamics to reference the trajectory
function AdvancedPS.dynamics(
ssm::AdvancedPS.TracedSSM{<:GPSSM{T},T,T}, step::Int
) where {T<:Real}
prior = ssm.model.dyn.proc(1:(step - 1))
post = posterior(prior, ssm.X[1:(step - 1)])
μ, σ = mean_and_cov(post, [step])
return LinearGaussianDynamics(zero(T), μ[1], sqrt(σ[1]))
end

AdvancedPS.isdone(::GPSSM, step) = step > T
# Everything is now ready to simulate some data.
rng = MersenneTwister(1234);
true_model = LinearGaussianStochasticVolatilityModel(0.9, 0.5);
_, x, y = sample(rng, true_model, 100);

model = GPSSM(y, params)
pg = AdvancedPS.PGAS(Nₚ)
chains = sample(rng, model, pg, Nₛ)
# Create the model and run the sampler
gpssm = GaussianProcessStateSpaceModel(Float64, SqExponentialKernel());
model = gpssm(y);
pg = AdvancedPS.PGAS(20);
chains = sample(rng, model, pg, 250; progress=false);
#md nothing #hide

particles = hcat([chain.trajectory.model.X for chain in chains]...)
particles = hcat([chain.trajectory.model.X for chain in chains]...);
mean_trajectory = mean(particles; dims=2);
#md nothing #hide

scatter(particles; label=false, opacity=0.01, color=:black, xlabel="t", ylabel="state")
plot!(x; color=:darkorange, label="Original Trajectory")
Expand Down
88 changes: 31 additions & 57 deletions examples/gaussian-ssm/script.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,81 +28,55 @@ using SSMProblems
# as well as the initial distribution $f_0(x) = \mathcal{N}(0, q^2/(1-a^2))$.

# To use `AdvancedPS` we first need to define a model type that subtypes `AdvancedPS.AbstractStateSpaceModel`.
Parameters = @NamedTuple begin
a::Float64
q::Float64
r::Float64
mutable struct Parameters{T<:Real}
a::T
q::T
r::T
end

mutable struct LinearSSM <: SSMProblems.AbstractStateSpaceModel
X::Vector{Float64}
observations::Vector{Float64}
θ::Parameters
LinearSSM(θ::Parameters) = new(Vector{Float64}(), θ)
LinearSSM(y::Vector, θ::Parameters) = new(Vector{Float64}(), y, θ)
struct LinearGaussianDynamics{T<:Real} <: SSMProblems.LatentDynamics{T,T}
a::T
q::T
end

# and the densities defined above.
f(θ::Parameters, state, t) = Normal(θ.a * state, θ.q) # Transition density
g(θ::Parameters, state, t) = Normal(state, θ.r) # Observation density
f₀(θ::Parameters) = Normal(0, θ.q^2 / (1 - θ.a^2)) # Initial state density
#md nothing #hide
function SSMProblems.distribution(dyn::LinearGaussianDynamics{T}; kwargs...) where {T<:Real}
return Normal(zero(T), sqrt(dyn.q^2 / (1 - dyn.a^2)))
end

# We also need to specify the dynamics of the system through the transition equations:
# - `AdvancedPS.initialization`: the initial state density
# - `AdvancedPS.transition`: the state transition density
# - `AdvancedPS.observation`: the observation score given the observed data
# - `AdvancedPS.isdone`: signals the end of the execution for the model
SSMProblems.transition!!(rng::AbstractRNG, model::LinearSSM) = rand(rng, f₀(model.θ))
function SSMProblems.transition!!(
rng::AbstractRNG, model::LinearSSM, state::Float64, step::Int
)
return rand(rng, f(model.θ, state, step))
function SSMProblems.distribution(dyn::LinearGaussianDynamics, step::Int, state; kwargs...)
return Normal(dyn.a * state, dyn.q)
end

function SSMProblems.emission_logdensity(modeL::LinearSSM, state::Float64, step::Int)
return logpdf(g(model.θ, state, step), model.observations[step])
struct LinearGaussianObservation{T<:Real} <: SSMProblems.ObservationProcess{T,T}
r::T
end
function SSMProblems.transition_logdensity(
model::LinearSSM, prev_state, current_state, step

function SSMProblems.distribution(
obs::LinearGaussianObservation, step::Int, state; kwargs...
)
return logpdf(f(model.θ, prev_state, step), current_state)
return Normal(state, obs.r)
end

# We need to think seriously about how the data is handled
AdvancedPS.isdone(::LinearSSM, step) = step > Tₘ
function LinearGaussianStateSpaceModel(θ::Parameters)
dyn = LinearGaussianDynamics(θ.a, θ.q)
obs = LinearGaussianObservation(θ.r)
return SSMProblems.StateSpaceModel(dyn, obs)
end

# Everything is now ready to simulate some data.
a = 0.9 # Scale
q = 0.32 # State variance
r = 1 # Observation variance
Tₘ = 200 # Number of observation
Nₚ = 20 # Number of particles
Nₛ = 500 # Number of samples
seed = 1 # Reproduce everything

θ₀ = Parameters((a, q, r))
rng = Random.MersenneTwister(seed)

x = zeros(Tₘ)
y = zeros(Tₘ)
x[1] = rand(rng, f₀(θ₀))
for t in 1:Tₘ
if t < Tₘ
x[t + 1] = rand(rng, f(θ₀, x[t], t))
end
y[t] = rand(rng, g(θ₀, x[t], t))
end
rng = Random.MersenneTwister(1234)
θ = Parameters(0.9, 0.32, 1.0)
true_model = LinearGaussianStateSpaceModel(θ)
_, x, y = sample(rng, true_model, 200);

# Here are the latent and obseravation timeseries
plot(x; label="x", xlabel="t")
plot!(y; seriestype=:scatter, label="y", xlabel="t", mc=:red, ms=2, ma=0.5)

# `AdvancedPS` subscribes to the `AbstractMCMC` API. To sample we just need to define a Particle Gibbs kernel
# and a model interface.
model = LinearSSM(y, θ₀)
pgas = AdvancedPS.PGAS(Nₚ)
chains = sample(rng, model, pgas, Nₛ; progress=false);
pgas = AdvancedPS.PGAS(20)
chains = sample(rng, true_model(y), pgas, 500; progress=false);
#md nothing #hide

#
Expand All @@ -118,7 +92,7 @@ plot!(mean_trajectory; color=:dodgerblue, label="Mean trajectory", opacity=0.9)
# We used a particle gibbs kernel with the ancestor updating step which should help with the particle
# degeneracy problem and improve the mixing.
# We can compute the update rate of $x_t$ vs $t$ defined as the proportion of times $t$ where $x_t$ gets updated:
update_rate = sum(abs.(diff(particles; dims=2)) .> 0; dims=2) / Nₛ
update_rate = sum(abs.(diff(particles; dims=2)) .> 0; dims=2) / length(chains)
#md nothing #hide

# and compare it to the theoretical value of $1 - 1/Nₚ$.
Expand All @@ -130,4 +104,4 @@ plot(
xlabel="Iteration",
ylabel="Update rate",
)
hline!([1 - 1 / Nₚ]; label="N: $(Nₚ)")
hline!([1 - 1 / length(chains)]; label="N: $(length(chains))")
Loading
Loading