-
Notifications
You must be signed in to change notification settings - Fork 37
Reparameterizations #220
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
Reparameterizations #220
Conversation
|
I like the general idea 👍 I have to admit though that I did not get what x ~ transform(f, Normal())or x ~ f ∘ Normal()would be more intuitive. And I noticed that you already mentioned such an alternative at the end of your post 🙂 Additionally, I felt it would be more consistent with the typical use of |
Yeah the more I look at it, the more I think there ought to be a better approach. But IMO it needs to be something more special than just So I'm more a fan of the x ~ f Normal()would just do it, since
Very okay with this! I think And regarding the |
|
Hmm yes, I guess x ~ Normal() transform=fx ~ f @∘ Normal()x ~ @transform f Normal()x ~ @reparam f Normal()I am not completely sure anymore though to what extent the proposal here would actually address the issues and user requests mentioned in the OP. IIRC usually users wanted to track the transformed Regarding the names, I wanted to suggest |
I honestly kind of like this! Explicit and simple.
Yes and no. It indeed doesn't solve that particular part of the issue, but IMO it's a step towards it. Of course we can always do this by hand, but it quickly becomes annoying.
Ah, good:) Yeah, we can see what people think. EDIT: Actually, I wonder if maybe it would be a good idea to introduce a tracking mechanism while I'm at it. I'll have to think a bit. |
This doesn't work btw. We need a |
|
Wait..why don't we just use your EDIT: Nvm, I guess it messes up stuff like HMC sampling since HMC will attempt to change the parameter. |
At the moment we will actually call `generate_mainbody!` on inputs to macros inside the model, e.g. in a model `@mymacro x ~ Normal()` will actually result in code `@mymacro $(generate_mainbody!(:(x ~ Normal())))` (or something, you get the idea). IMO, this shouldn't be done for the following reasons: 1. Breaks with what you'd expect in Julia, IMO, which is that a macro eats the "raw" code. 2. Means that if we want to do stuff like `@reparam` from #220 (and a bunch of others, see #221 for a small list of possibilities), we need touch the compiler rather than just make a small macro that will perform transformations *after* the compiler has done it's job (referring to DynamicPPL compiler here). 3. If the user wants to use a macro on some variables, but they want the actual variable rather than messing around with the sample-statement, they can just separate it into two lines, e.g. `x ~ Normal(); @mymacro ...`. Also, to be completely honest, for the longest time I've just assumed that I'm not even allowed to do `@mymacro x ~ Normal()` and have things work 😅 I bet a lot of people have the same impression by default (though this might of course just not be true:) )
At the moment we will actually call `generate_mainbody!` on inputs to macros inside the model, e.g. in a model `@mymacro x ~ Normal()` will actually result in code `@mymacro $(generate_mainbody!(:(x ~ Normal())))` (or something, you get the idea). IMO, this shouldn't be done for the following reasons: 1. Breaks with what you'd expect in Julia, IMO, which is that a macro eats the "raw" code. 2. Means that if we want to do stuff like `@reparam` from #220 (and a bunch of others, see #221 for a small list of possibilities), we need touch the compiler rather than just make a small macro that will perform transformations *after* the compiler has done it's job (referring to DynamicPPL compiler here). 3. If the user wants to use a macro on some variables, but they want the actual variable rather than messing around with the sample-statement, they can just separate it into two lines, e.g. `x ~ Normal(); @mymacro ...`. Also, to be completely honest, for the longest time I've just assumed that I'm not even allowed to do `@mymacro x ~ Normal()` and have things work 😅 I bet a lot of people have the same impression by default (though this might of course just not be true:) )
## Overview At the moment, we perform a check at model-expansion as to whether or not `vsym(left) in args`, where `args` is the arguments of the model. 1. If `true`, we return a block of code which uses `DynamicPPL.isassumption` to check whether or not to call `assume` or `observe` for the the variable present in `args`. 2. Otherwise, we generate a block which is identical to the `assume` block in the if-statement mentioned in (1). The thing is, `DynamicPPL.isassumption` performs exactly the same check as above but using `DynamicPPL.inargnames`, i.e. at runtime. So if we're using `TypedVarInfo`, the check at macro-expansion vs. at runtime is completely redundant since all the information necessary to determine `DynamicPPL.inargnames` is available at compile-time. Therefore I suggest we remove this check at model-expansion, and simply handle it using `DynamicPPL.isassumption`. ## Pros & cons Pros: - No need to pass `args` around everywhere - `generate_tilde` and `generate_dot_tilde` are much simpler: two possible blocks we can generate, either a) assume/observe, or b) observe literal. Cons: - We need to perform _one_ more check at runtime when using `UntypedVarInfo`. **IMO, this is really worth it.** ## Motivation (sort of) The main motivation behind this PR is simplification, but there's a different reason why I came across this. I came to this because I was thinking about trying to "customize" the behavior of `~`, and I was thinking of using a macro to do it, e.g. `@mymacro x ~ Normal()`. Atm we're actually performing model-expansion on the code passed to the macro and thus trying to alter the way DynamicPPL treats `~` using a macro is veeeery difficult since you actually have to work with the *expanded* code, but let's ignore that issue for now (and take that discussion somewhere else, because IMO we shouldn't do this). Suppose we didn't perform model-expansions of the code fed to the macros, then you can just copy-paste `generate_tilde`, customize it do what you want, and BAM, you got yourself a working `@mymacro x ~ Normal()` which can do neat stuff! This is *not* possible atm because we don't have access to `args`, and so you have to take the approach in this PR to get there. That means that it's of course possible to do atm, but it's a bit icky since it ends up looking fundamentally different from `generate_tilde` rather than just slightly different. Then we can implement things like a `@tilde` which will expand to `generate_tilde` which can be used *internally* in functions (if the "internal" variables are present in the functions of course, but we can also simplify this in different ways), actually allowing people to modularize their models a bit, and `@reparam` from #220 using very similar pieces of code, a `@track` macro can be introduced to deal with the explicit tracking of variables rather than putting this directly in the compiler, etc. Endless opportunities! (Of course, I'm not suggesting we add these, but this makes it a bit easier to explore.) Co-authored-by: David Widmann <[email protected]>
|
Closing this as there are now much easier ways to add this in:) |
EDIT: This PR needs some rework as the process can be simplifed significantly now that we have proper macroexpansion within models.
It's Saturday. Saturday is the day to be a bit wild and "let loose" as the kids say.
Therefore I tried coming up with a decent solution to this problem, and I think I've arrived at something that will at least leave some hair on @devmotion's head.
Result
Now it's possible to do stuff like
where the internal variable which is kept track of is denoted by
_:And for the sake of completeness:
To see the above example's expanded code, see the bottom of this PR.
What does the people want?
Desire for such a feature have been expressed several times, e.g. #94 TuringLang/Turing.jl#1444 and loads of times in our Slack channel.
The people want the ability to:
x ~ Normal(μ, σ)they want to writex_, since this can make the geometry of the posterior nicer (i.e. more numerically stable and/or better suited for the metric chosen in something like HMC).Normal()in the above case), i.e. only when the transformationfis invertible. What's that you say? Come on now, everyone say it together. 1...2...3..A BIJECTOR! Very good. So we can do this for transformations present in Bijectors.jl.transformed(dist, bijector)and observe using this?". Sometimes people have variables present in the arguments of the model which are not always observed; sometimes it's insteadmissing. In those case you want to sample, and we're back to case (1).Why isn't
TransformedDistributionenough?absain't invertible.assumeand whatnot to make this possible, but we'd still have the issue that we could not support non-bijective transformations.Why not introduce a
ReparamDistributionwhich allows non-invertible transformations?logpdf, and so representing it as aDistributionwill be disingenious.assumeand deal withlinkandinvlink.Solution?
Why not just introduce a
@reparammacro that isn't even a macro?:)Wait! I know what you're thinking "We literally went through this before, where we replaced all those fake macros like
@varinfowith 'internal variables', e.g._varinfo_." Yeah, yeah, I know. BUT this is different! Kind of.By introducing a "fake" macro we really don't have to do much to make things just work. The idea is to take the following representation of a model:
and convert into
This means that we won't have to worry about
assume,link,invlink, etc. These are all handled as usual for the "base" distribution.Caveats
@reparamisn't really a macro since it will have to be captured inDynamicPPL.generate_mainbody!before it's expanded.pymc3, so users will likely be familiar with the idea of transformed variables ending up in the chain with an underscore behind it.Alternatives/To discuss
@reparam, e.g. usex ~ f distor something. So what the user sees is def something we can discuss more. We could even go a bit crazy and doTODOs
The todo's are straight-forward but I just didn't bother until we've discussed the change.
generate_dot_tilde_with_reparam(super-easy)Example of expanded model: