-
Notifications
You must be signed in to change notification settings - Fork 37
Different approach to how observations/missings are stored in the model #268
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
Conversation
| # Generate main body | ||
| modelinfo[:body] = generate_mainbody(mod, modelinfo[:modeldef][:body], warn) | ||
| # Generate main body and find all variable symbols | ||
| modelinfo[:body], modelinfo[:varnames] = generate_mainbody( |
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.
Should generate_mainbody get a new name? I figured there is no need to traverse the expression twice, so I just added the varname extraction here...
|
I am confused, isn't this wrong? Missing values are no observations but have to be sampled. julia> m = demo2(missing, [1.0], 1.0)
Model demo2 given
constants θ, zz
observations x, y
julia> m2 = demo2(missing, [missing], 1.0)
Model demo2 given
constants θ, zz
observations x, yIt's a bit difficult to parse all changes, it seems e.g.
|
Co-authored-by: Tor Erlend Fjelde <[email protected]>
Right, that previously used a syntactic/static notion of "observation" (intersection of argument and variable names). I have fixed the printing now.
I think that handling model arguments this way makes things much clearer. They are mostly bookkeeping, with some probabilistic meaning attached, and this should become clearer with moving away from all the focus on
Only the existence of the defaults, currently, which affects the
Your're right, we don't need to export them. I have removed most of them.
Somewhat. But IMHO this way things look much clearer, don't you think? |
|
Just to illustrate what this currently can do: julia> @model function testmodel1(x; θ = 0.0)
s ~ InverseGamma(2, 3)
m ~ Normal(θ, sqrt(s))
x[1] ~ Normal(m, sqrt(s))
x[2] ~ Normal(m, sqrt(s))
end
testmodel1 (generic function with 2 methods)
julia> m = testmodel1(randn(2))
Model testmodel1 given
constants θ
observations x
julia> m = testmodel1([missing, 0.5])
Model testmodel1 given
constants θ
observations x[2]For now, that's just printing. I think these abilites can enable even nicer things in the future. And additionally, in my opinion the code is clearer. |
|
The prob macro now works, too! And given how smooth the change was, I believe this is a step in the right direction and towards condition/decondition. Besides a lot of warnings, there is only one of the Turing integration tests failing, because somewhere in loglikelihoods.jl: Test Failed at /home/philipp/.julia/dev/DynamicPPL/test/turing/loglikelihoods.jl:35
Expression: logpdf(Normal(m, √s), xs[1]) == var_to_likelihoods["xs[1]"]
Evaluated: -1.5235817173338913 == [-1.5235817173338913]
Stacktrace:
[1] top-level scope at /home/philipp/.julia/dev/DynamicPPL/test/turing/loglikelihoods.jl:35
[2] top-level scope at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.5/Test/src/Test.jl:1115
[3] top-level scope at /home/philipp/.julia/dev/DynamicPPL/test/turing/loglikelihoods.jl:2Is this a known thing? CC @yebai |
| if argname in leftnames | ||
| push!(argvals, :(deepcopy(left.$argname))) | ||
| push!(missings, argname) | ||
| push!(argvals, :(Constant(deepcopy(left.$argname)))) |
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 guess the old default arguments should be preserved, too:
| push!(argvals, :(Constant(deepcopy(left.$argname)))) | |
| push!(argvals, :(Constant(deepcopy(left.$argname), getdefault(model, $argname))) |
But the current implementation also has this feature though.
I'm a bit confused as to why we're distinguishing between Also, making kwargs to be
I think also the behavior of kwargs as mentioend above.
But if we're going to introduce We could even make |
| A `Model` struct with model evaluation function of type `F`, and arguments `arguments`. | ||
| """ | ||
| Model(name::Symbol, f, args::NamedTuple[, defaults::NamedTuple = ()]) | ||
| struct Model{F, argumentnames, Targs, internalnames, Tinternals} <: AbstractProbabilisticProgram |
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.
[JuliaFormatter] reported by reviewdog 🐶
| struct Model{F, argumentnames, Targs, internalnames, Tinternals} <: AbstractProbabilisticProgram | |
| struct Model{F,argumentnames,Targs,internalnames,Tinternals} <: AbstractProbabilisticProgram |
| model::Model{<:Any,argnames,Targs}, | ||
| ::Type{T} |
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.
[JuliaFormatter] reported by reviewdog 🐶
| model::Model{<:Any,argnames,Targs}, | |
| ::Type{T} | |
| model::Model{<:Any,argnames,Targs}, ::Type{T} |
| isdefined(ntr, arg) || | ||
| isdefined(defaults, arg) && getfield(defaults, arg) !== missing | ||
| @assert all(getargumentnames(model)) do arg | ||
| isdefined(ntl, arg) || isdefined(ntr, arg) || |
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.
[JuliaFormatter] reported by reviewdog 🐶
| isdefined(ntl, arg) || isdefined(ntr, arg) || | |
| isdefined(ntl, arg) || | |
| isdefined(ntr, arg) || |
| hasdefault(model, arg) && | ||
| !ismissing(getdefault(model, arg)) |
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.
[JuliaFormatter] reported by reviewdog 🐶
| hasdefault(model, arg) && | |
| !ismissing(getdefault(model, arg)) | |
| hasdefault(model, arg) && !ismissing(getdefault(model, arg)) |
Yeah but this knowledge is hidden in generated code (by @model function test(x, y)
x ~ Normal(10, sqrt(y))
end
m = test(x, y)there is no way the user can find out that Now that I also have added internal variables, we have this: julia> @model function gdemo(x, y, m0 = 0.0)
s ~ InverseGamma(2, 3)
m ~ Normal(m0, sqrt(s))
x ~ filldist(Normal(m, sqrt(s)), length(y))
for i in 1:length(y)
y[i] ~ Normal(x[i], sqrt(s))
end
end
gdemo (generic function with 2 methods)
julia> model = gdemo(rand(3), rand(3), 10)
Model gdemo given
constants: m0
observed variables: x, y
latent variables: s, m
julia> model = gdemo(rand(3), [0.5, missing, 0.5])
Model gdemo given
constants: m0
observed variables: x, y[1], y[3]
latent variables: y[2], s, m
julia> model = gdemo(missing, [0.5, missing, 0.5])
Model gdemo given
constants: m0
observed variables: y[1], y[3]
latent variables: x, y[2], s, m
Constants are things that are not variables, i.e., not on the LHS of any tilde. They have no probabilistic meaning.
They aren't all constants... obviously there might be bugs, but I do intend to match all of the old semantics: julia> @model function test2(x; y = 1)
y ~ Normal(10, sqrt(x))
end
test2 (generic function with 1 method)
julia> test2(1)
Model test2 given
constants: x
observed variables: y
latent variables:
julia> test2(1; y = missing)
Model test2 given
constants: x
observed variables:
latent variables: y
This is something I and Hong are currently still investigating, and I am not really sure. You might want to take a look about how I updated the prob macro (OK, this actually buttresses your point, since the contexts there are still the same...). I need to think about this more.
I think I am essentially doing that in this implementation. The new |
| Model(model.name, | ||
| model.evaluator, | ||
| $(to_namedtuple_expr(argnames, argvals)), | ||
| model.internal_variables) |
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.
[JuliaFormatter] reported by reviewdog 🐶
| Model(model.name, | |
| model.evaluator, | |
| $(to_namedtuple_expr(argnames, argvals)), | |
| model.internal_variables) | |
| Model( | |
| model.name, | |
| model.evaluator, | |
| $(to_namedtuple_expr(argnames, argvals)), | |
| model.internal_variables, | |
| ) |
| model.name, | ||
| model.evaluator, | ||
| $(to_namedtuple_expr(argnames, argvals)), | ||
| model.internal_variables)) |
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.
[JuliaFormatter] reported by reviewdog 🐶
| model.internal_variables)) | |
| model.internal_variables, | |
| )) |
torfjelde
left a comment
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.
Yeah but this knowledge is hidden in generated code (by
isassumption), and only treated negatively. For@model function test(x, y) x ~ Normal(10, sqrt(y)) end m = test(x, y)there is no way the user can find out that
yis "constant", and not on the LHS of a tilde, by inspectingm. The only way is looking at the output on aVarInfo(and with stochastic control flow, that can even be a nondeterministic observation!)
Completely agree with this being a pro of this PR 👍
Constants are things that are not variables, i.e., not on the LHS of any tilde. They have no probabilistic meaning.
Yeah I follow that. My question was more towards "why do I need this separation?", in particular since, as you say, we potentially have stochastic control flow. But I guess in most cases it will make sense to just considered everything that could be an observation as an observation.
They aren't all constants... obviously there might be bugs, but I do intend to match all of the old semantics:
Okay, good 👍 I think I misunderstood:)
We could even make
isassumptiona function that takes__model__,vnandleftrather thanexpr(could even add__context__too so that something likeConditionContextcan change whether something is treated as observation or not), since it should still be possible to evaluate at compile-time. I've always been a bit puzzled about the currentisassumptionand why it needs to act onexpr, so maybe I'm missing something.I think I am essentially doing that in this implementation. The new
isobservationtakes aVarName, which has a symbol type parameter. Your idea forisassumptionwould require the same kind of approach to work at compile time.
Yeah I realized this is one of those things that I just forget why we have it, and then when I start messing with it, I realized why 😅 See #271 where we end up back at a macro for the check (though also usable outside of the model, which can be nice).
Btw, I've left some comments in the code too, one which is of particular importance: how can the current approach work with @submodel? That is somewhat unclear to me atm 😕
| [missing, 42]` -- then `x[1]` is not an observation, but `x[2]` is.) | ||
| """ | ||
| function isobservation(vn::VarName{s}, model::Model{<:Any,argnames}) where {s,argnames} | ||
| return (s in argnames) && isobservation(vn, getproperty(model.arguments, s)) |
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.
Is the s in argnames needed if this approach is taken? E.g. why is it not okay for me to do something like Observation(x) ~ Normal() when x is not in the argnames?
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.
E.g. could we do
return (
(s in argnames && isobservation(vn, getproperty(model.arguments, s))) ||
(s in internalnames && isobservation(vn, getproperty(model.internal_variables, s)))
)if we already have internal_variables available? This would allow:
Observation(x) ~ Normal()of some internal variablexto force it to be an observation. People have sometimes asked for something along those lines before, though it's already possible to do using@addlogprob!.- Seems like something like this is necessary to implement
conditionanddeconditionfor latent variables, if we're not going the context-route?
Of course this still leaves the issue of "what do we do when left isn't defined yet, as in generate_tilde?". But then how do we then implement condition and decondition?
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.
Observation(x) ~ Normal() seems like a really nice idea. You'd use that when you transforme passed-in data, right?
|
|
||
| # extract observations from that | ||
| modelinfo[:obsnames] = modelinfo[:allargs_syms] ∩ modelinfo[:varnames] | ||
| modelinfo[:latentnames] = setdiff(modelinfo[:varnames], modelinfo[:allargs_syms]) |
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.
Btw, this will be incorrect for submodels and other similar non-top-level assume statements 😕
| function Base.show(io::IO, ::MIME"text/plain", model::Model) | ||
| println(io, "Model ", model.name, " given") | ||
| print(io, " constants: ") | ||
| join(io, getconstants(model), ", ") | ||
| println(io) | ||
| print(io, " observed variables: ") | ||
| join(io, getobservedvariables(model), ", ") | ||
| println(io) | ||
| print(io, " latent variables: ") | ||
| join(io, getlatentvariables(model), ", ") | ||
| return nothing | ||
| 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.
This really needs some truncation, as the number of variables can be insanely large.
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 would actually say that all the getobservedvariables, etc. should also be truncated or at least be an iterator, because there are models were you might be working with 10^5 variables, and thus mistakenly calling show on the model could be very slow (this is a huge pain with e.g. MCMCChains).
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. I was planning to replace it with some form of "compression" later, that, e.g., can collect x[1], ..., x[4], x[6], ..., x[10] into x[1:4], x[6:10].
...which only makes it better with "low-entropy missing patterns", though (like every other variable being missing). An iterator result and truncation seem like a good idea.
torfjelde
left a comment
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.
Cross-posting from Slack:
The issue is as follows:
@model inner(x) = x ~ Normal()
@model function outer(x)
@submodel y = inner(x)
endHow do I specify that x within inner is the missing using that API without just making the input x of outer missing? This is effectively what we need a solution to if we want to support condition and decondition.
And even worse:
function @model inner(x)
m ~ Normal(x, 1)
end
@model function outer(x)
for i = 1:2
@submodel $(Symbol("myprefix_$i)")) y = inner(x)
end
endHow can I condition on myprefix_1.x? Doing this statically, i.e. trying to capture it from within the DPPL "compiler" as is done there, seems infeasible.
The last example also highlights the fact that the way the variable is referenced within the model isn't necessarily how it's referenced in the VarInfo, e.g. NamedDist.
| isobservation(::VarName, ::Constant) = false | ||
| isobservation(vn::VarName, obs::Variable{Missing}) = false | ||
| isobservation(vn::VarName, obs::Variable) = !ismissing(_getindex(obs.value, vn.indexing)) |
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.
Since this requires runtime to evaluate anyways, we might as well also fix some bugs, e.g. x[:, i] ~ MvNormal(...) currently doesn't work even though all the entries of X are missing:
isobservation(::VarName, value) = true
isobservation(::VarName, ::Constant) = false
isobservation(vn::VarName, obs::Variable{Missing}) = false
isobservation(vn::VarName, obs::Variable) = isobservation(vn, _getindex(obs.value, vn.indexing))
isobservation(vn::VarName, value::Missing) = false
function isobservation(vn::VarName, value::AbstractArray{>:Missing})
ismissings = (value .=== missing)
if all(ismissings)
# All are `missing` => assumptions.
return false
elseif any(ismissings)
# Only SOME are `missing` => don't know how to handle this.
error("$(vn) have some `missing` and some not; this is currently not supported")
else
# None are `missing` => observations.
return true
end
endThere 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.
Hm. But:
- Isn't that intermingling two different things, the actual predicate and an error check in the model? Even if we can't handle mixed missings in some cases, a non-missing element of an array still is an observation, logically.
- The check as you wrote it rules out too much, doesn't it? If the elements are accessed in a loop, and each tilda uses
Normal, it should work. We can't detect the error case just from the observation being mixed, we have to take into account the distribution. That should be a separate check before or within the tilde calls.
| return :(NamedTuple{$filtered_argnames}(($(values...),))) | ||
| end | ||
|
|
||
| hasargument(model::Model, argname::Symbol) = isdefined(model.arguments, argname) |
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.
Would we want to do this at compile-time? E.g. an impl for Val or VarName?
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.
Indeed, I think we do want to.
| [missing, 42]` -- then `x[1]` is not an observation, but `x[2]` is.) | ||
| """ | ||
| function isobservation(vn::VarName{s}, model::Model{<:Any,argnames}) where {s,argnames} | ||
| return (s in argnames) && isobservation(vn, getproperty(model.arguments, s)) |
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.
E.g. could we do
return (
(s in argnames && isobservation(vn, getproperty(model.arguments, s))) ||
(s in internalnames && isobservation(vn, getproperty(model.internal_variables, s)))
)if we already have internal_variables available? This would allow:
Observation(x) ~ Normal()of some internal variablexto force it to be an observation. People have sometimes asked for something along those lines before, though it's already possible to do using@addlogprob!.- Seems like something like this is necessary to implement
conditionanddeconditionfor latent variables, if we're not going the context-route?
Of course this still leaves the issue of "what do we do when left isn't defined yet, as in generate_tilde?". But then how do we then implement condition and decondition?
Now there you raise a real problem... And I seriously don't know out of hand how to deal with it. Admittedly I completely ignored submodels, but they mess with my assumption that you can't add variable names at runtime. I need to go thinking again.
True, that's a bit fuzzy. I believe "LHS of a tilde" is still a honest qualification to "potentially in the trace type", as the trace type for stoch. control flow is the union over all possible execution paths -- and we don't distinguish between the paths anyway. (To be exact, we only deal with "trace shapes", but the argument is the same.) |
|
bors try |
tryTimed out. |
|
Will stall this for now; this approach has nice outputs for many models, but doesn't generalize to submodels and similar cases. Ideas of it will probably become part of a new implementation based on #279. |
Here's a less (but still somewhat) radical approach, in preparation to implement AbstractPPL's
condition/decondition.The most important user-facing outcome is probably this:
The essence of the implementation is that we can match model arguments and tildes at compile time:
Notes:
getobservationsandisobservationare not congruent withmissings present. Any idea for better naming?