-
Notifications
You must be signed in to change notification settings - Fork 37
condition and decondition using traits from #286 without ContextualModel
#294
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
condition and decondition using traits from #286 without ContextualModel
#294
Conversation
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…l into tor/conditioning
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…l into tor/conditioning
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm halfway through reviewing this PR; will add more comments after dinner.
src/DynamicPPL.jl
Outdated
| contextualize, | ||
| observations, | ||
| conditioned, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we actually want to export these functions?
| contextualize, | |
| observations, | |
| conditioned, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was intentional if that's what you mean, in particular I think people will want the observations / conditioned method. With that being said, maybe we shouldn't export in the next release but delay it until we've tried it out a bit and are happy with it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I think we want to apply the principle that we only export functions with a strong use-case. It would help to avoid frequent breaking changes and/or maintenance burdens since we are still experimenting with things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good 👍
| function contextual_isassumption(context::AbstractContext, vn) | ||
| return contextual_isassumption(NodeTrait(context), context, vn) | ||
| end | ||
| function contextual_isassumption(context::ConditionContext, vn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation of contextual_isassumption looks slightly inconsistent because it dispatches using both contexts and traits. I think this is fine for now, but we probably want to consider improving it in future PRs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit uncertain what you mean here 😕 This pattern we're using all over the place. Traits represents a default implementation, but we we still allow direct overloads, e.g. here ConditionContext and PrefixContext needs special handling but the rest of the contexts can use the trait-implementations.
Notice how even the trait-impls above call the impl without the trait argument, thus allowing these specific overloads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite sure - mixing traits and types kind of makes the code hard to read. But I'm happy to leave this as a future effort, given that the functionality is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure 👍
| # In this case, the below will return `true` since the first branch | ||
| # will be hit. | ||
| # 3. We are working with a `ConditionContext` _and_ it's in the model arguments, | ||
| # i.e. we're trying to override the value. This is currently NOT supported. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused by what will happen in the 3rd situation: will the current implementation error/warn, or it will fail/override silently?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, no this just means that in this scenario ConditionContext won't have any effect, i.e.
@model function demo(x)
x ~ Normal()
end
m = demo(missing)
(m | (x = 1.0))() # <= `x` is still considered `missing` because we can't change variables used in args through `condition`There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, let us add this to the docstring since it makes things clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do 👍
I'll also add tests now that we've landed on taking this approach.
Co-authored-by: Hong Ge <hg344@cam.ac.uk>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent work, I left many comments below. Overall, I think the PR implemented condition/deconditioning in a nice non-breaking way.
I think the mixed-use of traits and nested contexts requires some further discussions, but happy to do that in a separate effort.
src/DynamicPPL.jl
Outdated
| contextualize, | ||
| observations, | ||
| conditioned, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I think we want to apply the principle that we only export functions with a strong use-case. It would help to avoid frequent breaking changes and/or maintenance burdens since we are still experimenting with things.
| # In this case, the below will return `true` since the first branch | ||
| # will be hit. | ||
| # 3. We are working with a `ConditionContext` _and_ it's in the model arguments, | ||
| # i.e. we're trying to override the value. This is currently NOT supported. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, let us add this to the docstring since it makes things clearer.
| function contextual_isassumption(context::AbstractContext, vn) | ||
| return contextual_isassumption(NodeTrait(context), context, vn) | ||
| end | ||
| function contextual_isassumption(context::ConditionContext, vn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not quite sure - mixing traits and types kind of makes the code hard to read. But I'm happy to leave this as a future effort, given that the functionality is correct.
| require_particles(spl::Sampler) = false | ||
|
|
||
| _getindex(x, inds::Tuple) = _getindex(view(x, first(inds)...), Base.tail(inds)) | ||
| _getindex(x, inds::Tuple) = _getindex(Base.maybeview(x, first(inds)...), Base.tail(inds)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add some doc on what are the possible x types to improve clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All types are allowed with maybeview:) Also this is more properly addressed in #295
| end | ||
| function tilde_observe(context::MiniBatchContext, right, left, vname, vi) | ||
| return context.loglike_scalar * tilde_observe(context.context, right, left, vname, vi) | ||
| function tilde_observe(context::MiniBatchContext, sampler, right, left, vi) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that we are missing the argument vname here..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is actually because we never use tilde_observe with vname. See impl of tilde_observe!.
This is also why this passed through the tests; we have absolutely no tests for the vname-included versions (and some of contexts are missing impls for this even on master).
| values::NamedTuple{Names}, context::ConditionContext | ||
| ) where {Names} | ||
| # Note that this potentially overrides values from `context`, thus giving | ||
| # precedence to the outmost `ConditionContext`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we at least throw a warning if overrides happen here? It might cause very subtle bugs otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warning will slow things down + I don't think it should warn because what we're doing here is setting the conditioned value, right? And so if you're setting something, you expect that value to be the one that ends up in the model. This is the case with the above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also IMO, a seperate note, any such logic should go into condition and/or decondition, not ConditionContext constructor.
src/contexts.jl
Outdated
| getvalue(context, vn) | ||
| Return the value of the parameter corresponding to `vn` from `context`. | ||
| If `context` does not contain the value for `vn`, then `nothing` is returned, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
contextdoes not contain the value forvn, thennothingis returned,
Do we need to distinguish from the case where the value of a variable is missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah, getvalue will return missing if missing is indeed the value, not otherwise (and nor should it).
IMO we should actually just error here rather than return nothing 👍
| condition(; values...) = condition(DefaultContext(), (; values...)) | ||
| condition(values::NamedTuple) = condition(DefaultContext(), values) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor comment: maybe do not provide these convenient condition functions, but always requiring the user to pass a context instead. Semantically, it feels a bit odd to condition on a set of variables without a context/model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sort of view it as an alternative constructor of ConditionContext and I don't expect condition to be used externally on anything except with Model. But I agree that the semantics are a bit weird.
How about this? We make ConditionContext callable!
julia> @model function demo()
x ~ Normal()
return x
end
demo (generic function with 1 method)
julia> m = demo();
julia> m()
-0.5714991589000286
julia> cm = m |> condition(x=1.0);
julia> cm()
1.0This is actually pretty neat, because I can easily imagine a pattern where people have some specific conditioning they want to do on several instances of a model, e.g. different parameterizations, etc., in which case it makes sense to do something like
julia> conditioner = condition(x=1.0);
julia> conditioner(m)()
1.0| # is that the outermost `context` takes precendence, hence when resolving | ||
| # the `conditioned` variables we need to ensure that `context.values` takes | ||
| # precedence over decendants of `context`. | ||
| return merge(context.values, conditioned(childcontext(context))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's throw a warning here when overrides happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, it's a bit strange to warn only when the user try to retrieve the conditioned values, no? If we're going to warn, I prefer you previous suggestion of doing so in condition instead. I'm still a bit on the edge regarding that though because it means that if you intentially want to override a value, you'll see warnings everytime you use the context + it will have a significant performance impact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I.e. we really need a opt-out mechanism if we add it + I'm saying that condition is a setter and hence we shouldn't be surprised if a value is replaced.
| """ | ||
| hasvalue(context, vn) | ||
| Return `true` if `vn` is found in `context`. | ||
| """ | ||
| hasvalue(context, vn) = false | ||
|
|
||
| function hasvalue(context::ConditionContext{vars}, vn::VarName{sym}) where {vars,sym} | ||
| return sym in vars | ||
| end | ||
| function hasvalue( | ||
| context::ConditionContext{vars}, vn::AbstractArray{<:VarName{sym}} | ||
| ) where {vars,sym} | ||
| return sym in vars | ||
| end | ||
|
|
||
| """ | ||
| getvalue(context, vn) | ||
| Return value of `vn` in `context`. | ||
| """ | ||
| function getvalue(context::AbstractContext, vn) | ||
| return error("context $(context) does not contain value for $vn") | ||
| end | ||
| getvalue(context::ConditionContext, vn) = _getvalue(context.values, vn) | ||
|
|
||
| """ | ||
| hasvalue_nested(context, vn) | ||
| Return `true` if `vn` is found in `context` or any of its descendants. | ||
| """ | ||
| function hasvalue_nested(context::AbstractContext, vn) | ||
| return hasvalue_nested(NodeTrait(hasvalue_nested, context), context, vn) | ||
| end | ||
| hasvalue_nested(::IsLeaf, context, vn) = hasvalue(context, vn) | ||
| function hasvalue_nested(::IsParent, context, vn) | ||
| return hasvalue(context, vn) || hasvalue_nested(childcontext(context), vn) | ||
| end | ||
| function hasvalue_nested(context::PrefixContext, vn) | ||
| return hasvalue_nested(childcontext(context), prefix(context, vn)) | ||
| end | ||
|
|
||
| """ | ||
| getvalue_nested(context, vn) | ||
| Return the value of the parameter corresponding to `vn` from `context` or its descendants. | ||
| """ | ||
| function getvalue_nested(context::AbstractContext, vn) | ||
| return getvalue_nested(NodeTrait(getvalue_nested, context), context, vn) | ||
| end | ||
| function getvalue_nested(::IsLeaf, context, vn) | ||
| return error("context $(context) does not contain value for $vn") | ||
| end | ||
| function getvalue_nested(context::PrefixContext, vn) | ||
| return getvalue_nested(childcontext(context), prefix(context, vn)) | ||
| end | ||
| function getvalue_nested(::IsParent, context, vn) | ||
| return if hasvalue(context, vn) | ||
| getvalue(context, vn) | ||
| else | ||
| getvalue_nested(childcontext(context), vn) | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be possible to do away with the "nested" versions (though they might be useful in other case) if we make isassumption also return whatever value we're going to use. See my comment on isassumption for more info.
| if !($(DynamicPPL.inargnames)($vn, __model__)) || | ||
| $(DynamicPPL.inmissings)($vn, __model__) | ||
| true | ||
| else | ||
| $expr === missing | ||
| end | ||
| else | ||
| # Evaluate the LHS | ||
| $(maybe_view(expr)) === missing | ||
| false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we wanted to do away with the getvalue_nested and hasvalue_nested (again, these are convenient to have even if we're not using them here but obviously their utility is reduced significantly), we could make this return both the Bool corresponding to whether it's an assumption and maybe a value, i.e. nothing or the value we've extracted from context. It would end up looking something like
function isassumption(expr::Union{Symbol,Expr})
vn = gensym(:vn)
val = gensym(:vn)
return quote
let $vn = $(varname(expr)), $val = $(DynamicPPL.contextual_isassumption)(__context__, $vn)
if $val !== missing
if !($(DynamicPPL.inargnames)($vn, __model__)) ||
$(DynamicPPL.inmissings)($vn, __model__)
true
else
$expr === missing ? (true, missing) : (false, $val)
end
else
false, missing
end
end
end
endwith contextual_isassumption now returning missing if it's considered an assumption or anything else if it's considered an observation.
Similarly we'd have to add an additional variable in generate_tilde, etc. which is used if we hit the observe branch and the value return from isassumption is not missing.
All in all, IMO it ends up looking more complicated than the current implementation of getvalue_nested, etc. so I'm not in favour of doing it but wanted to let people know it's an option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good!
|
bors try |
tryBuild failed: |
|
Format will fail here because of domluna/JuliaFormatter.jl#449 (comment) |
|
Btw, I've left some responses to your comments. Also, I need to add some tests for |
|
It seems like bors mistakenly closed this issue o.O I'll push the branch to remote again, and complete it. |
No description provided.