diff --git a/README.md b/README.md index 2ecbbf0..659d02b 100644 --- a/README.md +++ b/README.md @@ -15,23 +15,29 @@ notebooks and an API. using AlgebraicInference using Catlab.Programs -wd = @relation (x,) begin +wd = @relation (x,) where (x::X, y::Y) begin prior(x) likelihood(x, y) evidence(y) end -hm = Dict( +hom_map = Dict( :prior => normal(0, 1), # x ~ N(0, 1) :likelihood => kernel([1], 0, 1), # y | x ~ N(x, 1) :evidence => normal(2, 0)) # y = 2 +ob_map = Dict( + :X => 1, # x ∈ ℝ¹ + :Y => 1) # y ∈ ℝ¹ + +ob_attr = :junction_type + # Solve directly. -Σ = oapply(wd, hm) +Σ = oapply(wd, hom_map, ob_map; ob_attr) # Solve using belief propagation. T = DenseGaussianSystem{Float64} -Σ = solve(InferenceProblem{T}(wd, hm), MinFill()) +Σ = solve(InferenceProblem{T, Int}(wd, hom_map, ob_map; ob_attr), MinFill()) ``` ![inference](./inference.svg) diff --git a/docs/literate/kalman.jl b/docs/literate/kalman.jl index 9dd90b9..545835a 100644 --- a/docs/literate/kalman.jl +++ b/docs/literate/kalman.jl @@ -2,7 +2,6 @@ using AlgebraicInference using BenchmarkTools using Catlab.Graphics, Catlab.Programs, Catlab.WiringDiagrams -using Catlab.WiringDiagrams.MonoidalUndirectedWiringDiagrams: UntypedHypergraphDiagram using Distributions using FillArrays using LinearAlgebra @@ -27,6 +26,7 @@ B = [ 1.3 0.0 0.0 0.7 ] + P = [ 0.05 0.0 0.0 0.05 @@ -53,21 +53,17 @@ end; # observations of ``(z_1, \dots, z_n)``. The function `kalman` constructs a wiring diagram # that represents the filtering problem. function kalman_step(i) - kf = UntypedHypergraphDiagram{String}(2) - add_box!(kf, 2; name="state") - add_box!(kf, 4; name="predict") - add_box!(kf, 4; name="measure") - add_box!(kf, 2; name="z$i") + kf = HypergraphDiagram{String, String}(["X"]) + add_box!(kf, ["X"]; name="state") + add_box!(kf, ["X", "X"]; name="predict") + add_box!(kf, ["X", "Z"]; name="measure") + add_box!(kf, ["Z"]; name="z$i") add_wires!(kf, [ - (0, 1) => (2, 3), - (0, 2) => (2, 4), + (0, 1) => (2, 2), (1, 1) => (2, 1), (1, 1) => (3, 1), - (1, 2) => (2, 2), - (1, 2) => (3, 2), - (3, 3) => (4, 1), - (3, 4) => (4, 2)]) + (3, 2) => (4, 1)]) kf end @@ -80,19 +76,26 @@ to_graphviz(kalman(5), box_labels=:name; implicit_junctions=true) # We generate ``100`` points of data and solve the filtering problem. n = 100; kf = kalman(n); data = generate_data(n) -dm = Dict("z$i" => normal(data[i], Zeros(2, 2)) for i in 1:n) +evidence = Dict("z$i" => normal(data[i], Zeros(2, 2)) for i in 1:n) -hm = Dict( - dm..., +hom_map = Dict( + evidence..., "state" => normal(Zeros(2), 100I(2)), "predict" => kernel(A, Zeros(2), P), "measure" => kernel(B, Zeros(2), Q)) -mean(oapply(kf, hm)) +ob_map = Dict( + "X" => 2, + "Z" => 2) + +ob_attr = :junction_type + +mean(oapply(kf, hom_map, ob_map; ob_attr)) # -@benchmark oapply(kf, hm) +@benchmark oapply(kf, hom_map, ob_map; ob_attr) # Since the filtering problem is large, we may wish to solve it using belief propagation. -ip = InferenceProblem{DenseGaussianSystem{Float64}}(kf, hm) +T = DenseGaussianSystem{Float64} +ip = InferenceProblem{T, Int}(kf, hom_map, ob_map; ob_attr) is = init(ip, MinFill()) mean(solve(is)) diff --git a/docs/literate/regression.jl b/docs/literate/regression.jl index 6eca4cc..41a087e 100644 --- a/docs/literate/regression.jl +++ b/docs/literate/regression.jl @@ -46,28 +46,34 @@ Q = I - X * pinv(X) β̂ = pinv(X) * (I - pinv(Q * W * Q) * Q * W)' * y # To solve for ``\hat{\beta}`` using AlgebraicInference.jl, we construct an undirected # wiring diagram. -wd = @relation (a₁, a₂) begin - X(a₁, a₂, b₁, b₂, b₃) - +(b₁, b₂, b₃, c₁, c₂, c₃, d₁, d₂, d₃) - ϵ(c₁, c₂, c₃) - y(d₁, d₂, d₃) +wd = @relation (a,) where (a::m, b::n, c::n, d::n) begin + X(a, b) + +(b, c, d) + ϵ(c) + y(d) end to_graphviz(wd; box_labels=:name, implicit_junctions=true) # Then we assign values to the boxes in `wd` and compute the result. -P = [ +P = [ 1 0 0 1 0 0 0 1 0 0 1 0 0 0 1 0 0 1 ] -hm = Dict( +hom_map = Dict( :X => kernel(X, Zeros(3), Zeros(3, 3)), :+ => kernel(P, Zeros(3), Zeros(3, 3)), :ϵ => normal(Zeros(3), W), :y => normal(y, Zeros(3, 3))) -β̂ = mean(oapply(wd, hm)) +ob_map = Dict( + :m => 2, + :n => 3) + +ob_attr = :junction_type + +β̂ = mean(oapply(wd, hom_map, ob_map; ob_attr)) # ## Bayesian Linear Regression # Let ``\rho = \mathcal{N}(m, V)`` be our prior belief about ``\beta``. Then our posterior # belief ``\hat{\rho}`` is a bivariate normal distribution with mean @@ -93,26 +99,32 @@ m̂ = m - V * X' * pinv(X * V * X' + W) * (X * m - y) V̂ = V - V * X' * pinv(X * V * X' + W) * X * V # To solve for ``\hat{\rho}`` using AlgebraicInference.jl, we construct an undirected # wiring diagram. -wd = @relation (a₁, a₂) begin - ρ(a₁, a₂) - X(a₁, a₂, b₁, b₂, b₃) - +(b₁, b₂, b₃, c₁, c₂, c₃, d₁, d₂, d₃) - ϵ(c₁, c₂, c₃) - y(d₁, d₂, d₃) +wd = @relation (a,) where (a::m, b::n, c::n, d::n) begin + ρ(a) + X(a, b) + +(b, c, d) + ϵ(c) + y(d) end to_graphviz(wd; box_labels=:name, implicit_junctions=true) # Then we assign values to the boxes in `wd` and compute the result. -hm = Dict( +hom_map = Dict( :ρ => normal(m, V), :X => kernel(X, Zeros(3), Zeros(3, 3)), :+ => kernel(P, Zeros(3), Zeros(3, 3)), :ϵ => normal(Zeros(3), W), :y => normal(y, Zeros(3, 3))) -m̂ = mean(oapply(wd, hm)) +ob_map = Dict( + :m => 2, + :n => 3) + +ob_attr = :junction_type + +m̂ = mean(oapply(wd, hom_map, ob_map; ob_attr)) # -V̂ = cov(oapply(wd, hm)) +V̂ = cov(oapply(wd, hom_map, ob_map; ob_attr)) # covellipse!(m, V, aspect_ratio=:equal, label="prior") covellipse!(m̂, V̂, aspect_ratio=:equal, label="posterior") diff --git a/docs/src/api.md b/docs/src/api.md index a91f47f..5470157 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -3,7 +3,7 @@ ```@docs GaussianSystem -GaussianSystem(::AbstractMatrix, ::AbstractMatrix, ::AbstractVector, ::AbstractVector, ::Any) +GaussianSystem(::AbstractMatrix, ::AbstractMatrix, ::AbstractVector, ::AbstractVector, ::Real) normal kernel @@ -14,7 +14,7 @@ invcov(::GaussianSystem) var(::GaussianSystem) mean(::GaussianSystem) -oapply(::AbstractUWD, ::AbstractVector{<:GaussianSystem}) +oapply(::AbstractUWD, ::AbstractVector{<:GaussianSystem}, ::AbstractVector) ``` ## Problems @@ -23,8 +23,8 @@ InferenceProblem MinDegree MinFill -InferenceProblem{T}(::AbstractUWD, ::AbstractDict, ::Union{Nothing, AbstractDict}) where T -InferenceProblem{T}(::AbstractUWD, ::AbstractVector, ::Union{Nothing, AbstractVector}) where T +InferenceProblem{T₁, T₂}(::AbstractUWD, ::AbstractDict, ::AbstractDict) where {T₁, T₂} +InferenceProblem{T₁, T₂}(::AbstractUWD, ::AbstractVector, ::AbstractVector) where {T₁, T₂} solve(::InferenceProblem, alg) init(::InferenceProblem, alg) diff --git a/docs/src/generated/kalman.md b/docs/src/generated/kalman.md index 69accc2..855185f 100644 --- a/docs/src/generated/kalman.md +++ b/docs/src/generated/kalman.md @@ -8,7 +8,6 @@ EditURL = "/literate/kalman.jl" using AlgebraicInference using BenchmarkTools using Catlab.Graphics, Catlab.Programs, Catlab.WiringDiagrams -using Catlab.WiringDiagrams.MonoidalUndirectedWiringDiagrams: UntypedHypergraphDiagram using Distributions using FillArrays using LinearAlgebra @@ -37,6 +36,7 @@ B = [ 1.3 0.0 0.0 0.7 ] + P = [ 0.05 0.0 0.0 0.05 @@ -68,21 +68,17 @@ that represents the filtering problem. ````@example kalman function kalman_step(i) - kf = UntypedHypergraphDiagram{String}(2) - add_box!(kf, 2; name="state") - add_box!(kf, 4; name="predict") - add_box!(kf, 4; name="measure") - add_box!(kf, 2; name="z$i") + kf = HypergraphDiagram{String, String}(["X"]) + add_box!(kf, ["X"]; name="state") + add_box!(kf, ["X", "X"]; name="predict") + add_box!(kf, ["X", "Z"]; name="measure") + add_box!(kf, ["Z"]; name="z$i") add_wires!(kf, [ - (0, 1) => (2, 3), - (0, 2) => (2, 4), + (0, 1) => (2, 2), (1, 1) => (2, 1), (1, 1) => (3, 1), - (1, 2) => (2, 2), - (1, 2) => (3, 2), - (3, 3) => (4, 1), - (3, 4) => (4, 2)]) + (3, 2) => (4, 1)]) kf end @@ -99,25 +95,32 @@ We generate ``100`` points of data and solve the filtering problem. ````@example kalman n = 100; kf = kalman(n); data = generate_data(n) -dm = Dict("z$i" => normal(data[i], Zeros(2, 2)) for i in 1:n) +evidence = Dict("z$i" => normal(data[i], Zeros(2, 2)) for i in 1:n) -hm = Dict( - dm..., +hom_map = Dict( + evidence..., "state" => normal(Zeros(2), 100I(2)), "predict" => kernel(A, Zeros(2), P), "measure" => kernel(B, Zeros(2), Q)) -mean(oapply(kf, hm)) +ob_map = Dict( + "X" => 2, + "Z" => 2) + +ob_attr = :junction_type + +mean(oapply(kf, hom_map, ob_map; ob_attr)) ```` ````@example kalman -@benchmark oapply(kf, hm) +@benchmark oapply(kf, hom_map, ob_map; ob_attr) ```` Since the filtering problem is large, we may wish to solve it using belief propagation. ````@example kalman -ip = InferenceProblem{DenseGaussianSystem{Float64}}(kf, hm) +T = DenseGaussianSystem{Float64} +ip = InferenceProblem{T, Int}(kf, hom_map, ob_map; ob_attr) is = init(ip, MinFill()) mean(solve(is)) diff --git a/docs/src/generated/regression.md b/docs/src/generated/regression.md index f1f640b..0708a75 100644 --- a/docs/src/generated/regression.md +++ b/docs/src/generated/regression.md @@ -60,11 +60,11 @@ To solve for ``\hat{\beta}`` using AlgebraicInference.jl, we construct an undire wiring diagram. ````@example regression -wd = @relation (a₁, a₂) begin - X(a₁, a₂, b₁, b₂, b₃) - +(b₁, b₂, b₃, c₁, c₂, c₃, d₁, d₂, d₃) - ϵ(c₁, c₂, c₃) - y(d₁, d₂, d₃) +wd = @relation (a,) where (a::m, b::n, c::n, d::n) begin + X(a, b) + +(b, c, d) + ϵ(c) + y(d) end to_graphviz(wd; box_labels=:name, implicit_junctions=true) @@ -79,13 +79,19 @@ P = [ 0 0 1 0 0 1 ] -hm = Dict( +hom_map = Dict( :X => kernel(X, Zeros(3), Zeros(3, 3)), :+ => kernel(P, Zeros(3), Zeros(3, 3)), :ϵ => normal(Zeros(3), W), :y => normal(y, Zeros(3, 3))) -β̂ = mean(oapply(wd, hm)) +ob_map = Dict( + :m => 2, + :n => 3) + +ob_attr = :junction_type + +β̂ = mean(oapply(wd, hom_map, ob_map; ob_attr)) ```` ## Bayesian Linear Regression @@ -121,12 +127,12 @@ To solve for ``\hat{\rho}`` using AlgebraicInference.jl, we construct an undirec wiring diagram. ````@example regression -wd = @relation (a₁, a₂) begin - ρ(a₁, a₂) - X(a₁, a₂, b₁, b₂, b₃) - +(b₁, b₂, b₃, c₁, c₂, c₃, d₁, d₂, d₃) - ϵ(c₁, c₂, c₃) - y(d₁, d₂, d₃) +wd = @relation (a,) where (a::m, b::n, c::n, d::n) begin + ρ(a) + X(a, b) + +(b, c, d) + ϵ(c) + y(d) end to_graphviz(wd; box_labels=:name, implicit_junctions=true) @@ -135,18 +141,24 @@ to_graphviz(wd; box_labels=:name, implicit_junctions=true) Then we assign values to the boxes in `wd` and compute the result. ````@example regression -hm = Dict( +hom_map = Dict( :ρ => normal(m, V), :X => kernel(X, Zeros(3), Zeros(3, 3)), :+ => kernel(P, Zeros(3), Zeros(3, 3)), :ϵ => normal(Zeros(3), W), :y => normal(y, Zeros(3, 3))) -m̂ = mean(oapply(wd, hm)) +ob_map = Dict( + :m => 2, + :n => 3) + +ob_attr = :junction_type + +m̂ = mean(oapply(wd, hom_map, ob_map; ob_attr)) ```` ````@example regression -V̂ = cov(oapply(wd, hm)) +V̂ = cov(oapply(wd, hom_map, ob_map; ob_attr)) ```` ````@example regression diff --git a/src/problems.jl b/src/problems.jl index 7167e87..ba9a20f 100644 --- a/src/problems.jl +++ b/src/problems.jl @@ -28,29 +28,30 @@ algorithm. Variables are eliminated according to the "minimum fill" heuristic. struct MinFill end """ - InferenceProblem{T}(wd::AbstractUWD, hom_map::AbstractDict, - ob_map::Union{Nothing, AbstractDict}=nothing; - hom_attr=:name, ob_attr=:variable) where T + InferenceProblem{T₁, T₂}(wd::AbstractUWD, hom_map::AbstractDict, ob_map::AbstractDict; + hom_attr=:name, ob_attr=:variable) where {T₁, T₂} Construct an inference problem that performs undirected composition. Before being composed, -the values of `hom_map` are converted to type `T`. +the values of `hom_map` are converted to type `T₁`, and the values of `ob_map` are converted +to type `T₂`. """ -function InferenceProblem{T₁, T₂}(wd::AbstractUWD, hom_map::AbstractDict, - ob_map::Union{Nothing, AbstractDict}=nothing; +function InferenceProblem{T₁, T₂}(wd::AbstractUWD, hom_map::AbstractDict, ob_map::AbstractDict; hom_attr=:name, ob_attr=:variable) where {T₁, T₂} homs = [hom_map[x] for x in subpart(wd, hom_attr)] - obs = isnothing(ob_map) ? nothing : [ob_map[x] for x in subpart(wd, ob_attr)] + obs = [ob_map[x] for x in subpart(wd, ob_attr)] InferenceProblem{T₁, T₂}(wd, homs, obs) end """ - InferenceProblem{T}(wd::AbstractUWD, homs::AbstractVector, - obs::Union{Nothing, AbstractVector}=nothing) where T + InferenceProblem{T₁, T₂}(wd::AbstractUWD, homs::AbstractVector, + obs::AbstractVector) where {T₁, T₂} Construct an inference problem that performs undirected composition. Before being composed, -the elements of `homs` are converted to type `T`. +the elements of `homs` are converted to type `T₁`, and the elements of `obs` are converted +to type `T₂`. """ -function InferenceProblem{T₁, T₂}(wd::AbstractUWD, homs::AbstractVector, obs::AbstractVector) where {T₁, T₂} +function InferenceProblem{T₁, T₂}(wd::AbstractUWD, homs::AbstractVector, + obs::AbstractVector) where {T₁, T₂} @assert nboxes(wd) == length(homs) @assert njunctions(wd) == length(obs) query = collect(subpart(wd, :outer_junction))