-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow contraction of composite nodes #287
Comments
Hello, could you expand somewhat on what this task entails? I'd like to tackle it or a portion of it as a first issue, but I will likely need some hand-holding if that is okay. Anticipating difficulty will arise in 1. grasping the problem domain, and 2. familiarizing myself with the code base. But, overcoming both of those is the entire reason I would like to pursue this task. Let me know what I can do! |
I have done some reading based off of some keyword searches in this post. My assumption is that we are talking about graph contraction here. It looks like union-find would be sufficient for solving this, which is outlined here. Let me know if that understanding is correct. If so, I am fairly sure I've implemented this before in some algorithms class. I'll be checking my understanding in GraphPPL in the meantime. |
Hi @blolt , thanks for picking this up! This issue basically says that we want to be able to do the following: Let's say I build a custom submodel, called @model function gcv(κ, ω, z, x, y)
log_σ := κ * z + ω
y ~ NormalMeanVariance(x, exp(log_σ))
end and we want to embed this in some larger model, say a Hierarchical Gaussian Filter. Without going into too much detail, this model can be written by stacking a couple of @model function hgf(y)
# Specify priors
ξ ~ Gamma(1, 1)
ω_1 ~ NormalMeanVariance(0, 1)
ω_2 ~ NormalMeanVariance(0, 1)
κ_1 ~ NormalMeanVariance(0, 1)
κ_2 ~ NormalMeanVariance(0, 1)
x_1[1] ~ NormalMeanVariance(0, 1)
x_2[1] ~ NormalMeanVariance(0, 1)
x_3[1] ~ NormalMeanVariance(0, 1)
y[1] ~ NormalMeanVariance(x_1[1], 1)
# Specify generative model
for i in 2:(length(y) + 1)
x_3[i] ~ NormalMeanVariance(μ = x_3[i - 1], τ = ξ)
x_2[i] ~ gcv(x = x_2[i - 1], z = x_3[i], ω = ω_2, κ = κ_2)
x_1[i] ~ gcv(x = x_1[i - 1], z = x_2[i], ω = ω_1, κ = κ_1)
y[i] ~ NormalMeanVariance(x_1[i], 1)
end
end Now, if we materialize this entire model, there are some nasty messages inside the During the construction of a node/submodel (essentially, when resolving a
Note that the user would need to write their own custom In short, this is a perfect way to dive into some nitty gritty details of |
Hey @wouterwln ! Looks like my intuition was way off! Thanks for the links and the incredibly thorough explanation. I looked through some of the relevant code this morning and will make some time to work through this more later this evening (EST). Thanks again! |
Hey @wouterwln! I have had some more time to look at RxInfer and GraphPPL, as well as learning more about Julia. I have some questions and ideas about the proposed approach for this issue that I am hoping to discuss with you. Mostly, I just have questions about the use of parametric types in RxInfer.
I noticed that parameterization like this seems to be common throughout the reactiveBayes packages, but I am having difficulty understanding why (for example, 18 parametric types on the RxInferenceEngine struct seems like overkill). I believe the backend we are looking to parameterize is defined here, but please let me know if this is wrong. Here is my understanding of how the implementation you recommend would work. # new parameterized struct
struct ReactiveMPGraphPPLBackend{T}
contract::T
end
# multiple dispatch implementation of NodeType provided by RxInfer user
function GraphPPL.NodeType(::ReactiveMPGraphPPLBackend{True}, ::typeof(gcv))
# custom behaviour defined by user
return GraphPPL.Atomic()
end
# ... infer changes (haven't gotten this far. The flow of data between infer and the graphPPL backend is not fully clear to me, but it seems to go through the model macro for each time we call infer via ~). Is this correct to your suggestion? This is an odd usage of a "generic" (parametric) type that I have not seen before. It seems like we are only interested in using the generic type as boolean, and like it is being used for specific functionality. Why not simply use a boolean value within the struct, then, like so? struct ReactiveMPGraphPPLBackend
contract::Bool
ReactiveMPGraphPPLBackend() = new(true)
end Parameterizing this struct has the downside of breaking 55 existing unit tests, which expect an unparameterized interface, whereas adding a boolean maintains the interface and allows all existing unit tests to pass. In general, it seems like good practice to be explicit about the types we are using where we can be, too. I would appreciate better understanding why we see parameterization + multiple dispatch as the best approach here. Also, I hope this does not come across as pedantic. I understand that these are mostly implementation details that will be covered in review, but I think they get at some of the philosophy behind how this package is written, so I would appreciate understanding them from that perspective. |
Hey @blolt , this is because of type stability issues. Let's look at the case that we put a function GraphPPL.NodeType(backend::ReactiveMPGraphPPLBackend, ::typeof(gcv))
if backend.contract
return GraphPPL.Atomic()
else
return GraphPPL.Composite()
end
end Now this function is type unstable, because we cannot statically determine what the return type of this function will be. Type instability is quite a strange thing to get your head around when you first start using Julia (I know for one that I had some issues understanding it), but it boils down to this: The Julia compiler, before executing any of your code, tries to statically determine the return type of any function. Note that we do not have access to the internal variables here, only the type of the parameters. This is done because if Julia knows all types of all inputs, the compiler can compile away the multiple dispatch (if all types are known, there should only be 1 candidate implementation we can run, and we should run that one), instead of having to search for a candidate function during runtime. If we put Now, this type parametrization is not a ReactiveBayes specific design pattern, but a common Julia practice. What we do is, when the type of one of the fields of a struct influences the behaviour of the object, we parameterize the type. The difference is this: When we have the following:
Whenever we create an instance of this type, the value of More info on type stability: Package I use to track type instabilities: Hope this helps! |
Hey @wouterwln! I really appreciate your thorough answers here. The need for type stability here makes sense to me. I have spent a good amount of time reading through the RxInfer and GraphPPL code now, but I remain blocked on understanding how to allow the user to toggle backend functionality through the infer function. Here are the changes / new methods I have made. struct ReactiveMPGraphPPLBackend{T}
shouldCreateCompositeNodes::T
ReactiveMPGraphPPLBackend{T}() where {T} = new{T}(nothing)
ReactiveMPGraphPPLBackend{T}(val::Bool) where {T} = new{T}(val)
end
macro model(model_specification)
return esc(GraphPPL.model_macro_interior(ReactiveMPGraphPPLBackend{true}, model_specification))
end
function GraphPPL.instantiate(::Type{ReactiveMPGraphPPLBackend{True}})
return ReactiveMPGraphPPLBackend{True}(true)
end It seems like this should allow the user to implement something like, GraphPPL.NodeType(::ReactiveMPGraphPPLBackend{True}, ::typeof(gcv)) = GraphPPL.Atomic() Of course, it doesn't allow for the user to define any behavior when the type is Thank you! |
Hi @blolt, I see now that the change I propose is not as straightforward as I made it seem. I've looked into it a bit more, and let me explain: I don't think we should change the Now, a really sharp reader will say: But Wouter, this creation of the backend will be type unstable, since we still don't know the return type of By the way, I think the following should be enough: struct ReactiveMPGraphPPLBackend{T}
shouldCreateCompositeNodes::T
end to realize this change. @bvdmitri what do you think? I think with some smart dispatching we can make this entire procedure type stable, but it might put more of a burden on the user (for example, having to supply |
It is relatively simple: import RxInfer: ReactiveMPGraphPPLBackend
import GraphPPL: with_backend
using RxInfer, Random
n = 500 # Number of coin flips
p = 0.75 # Bias of a coin
distribution = Bernoulli(p)
dataset = rand(distribution, n)
@model function coin_model(y, a, b)
θ ~ Beta(a, b)
y .~ Bernoulli(θ)
end
result = infer(
model = with_backend(coin_model(a = 2.0, b = 7.0), ReactiveMPGraphPPLBackend()), # pass some extra args to the backend
data = (y = dataset, )
) or if you prefer piped operation result = infer(
model = coin_model(a = 2.0, b = 7.0) |> (m) -> with_backend(m, ReactiveMPGraphPPLBackend()),
data = (y = dataset, )
) Regarding type stability, writing julia> struct Hello{T} end
julia> foo(x) = x ? True() : False()
julia> bar(x) = Hello{foo(x)}()
bar (generic function with 1 method)
julia> bar(true)
Hello{static(true)}()
julia> using BenchmarkTools
julia> @btime bar(true)
0.791 ns (0 allocations: 0 bytes)
Hello{static(true)}() Even if type-unstable, it will only be so during creation time, and will have a concrete type when it reaches the infer function. This is a minor multiple dispatch issue and should not be a major concern. The actual constructor of course should be parametrised with GraphPPL.NodeType(::ReactiveMPGraphPPLBackend{True}, ::typeof(gcv)) = GraphPPL.Atomic() As a side note, you could implement it like this: allows_contraction(::ReactiveMPGraphPPLBackend{True}) = True()
allows_contraction(::ReactiveMPGraphPPLBackend{False}) = False()
GraphPPL.NodeType(backend::ReactiveMPGraphPPLBackend, node) = GraphPPL.NodeType(backend, allows_contraction(backend), node)
GraphPPL.NodeType(::ReactiveMPGraphPPLBackend, ::True, ::typeof(gcv)) = GraphPPL.Atomic() This way, changing the order of type parameters (e.g. if someone in the future would add some extra |
Thanks for the advice here guys, I think I was able to implement node contraction with everything provided. I have been spending some time since working to implement the HGF example from @wouterwln as a unit test. I have so far failed to get the example to work, but this is as much due to insufficient familiarity with RxInfer as it may be an incorrect node contraction implementation. I've got the changes sitting in a fork right now, but I could possibly submit it as a WIP pull request? I am a bit stumped here. |
Thanks for your work! Yeah, WIP PR is welcome, we can discuss and see your changes there and we will also be able to commit directly to your fork in this case. |
Should be very easy (parameterizing the backend) since all machinery is already there in
GraphPPL
, but we should expose (and document) the API to usersThe text was updated successfully, but these errors were encountered: