-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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 copy and deepcopy of PYMC models #7492
Conversation
] |
Hey @ricardoV94, sorry if this is a silly question. Do I need to do anything to get the |
CI has to be manually approved for first time contributors, doing it now |
Okay! Thank you very much!! |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7492 +/- ##
==========================================
+ Coverage 92.15% 92.44% +0.28%
==========================================
Files 103 103
Lines 17208 17119 -89
==========================================
- Hits 15858 15825 -33
+ Misses 1350 1294 -56
|
pymc/model/core.py
Outdated
check_for_gp_vars = [ | ||
k for x in ["_rotated_", "_hsgp_coeffs_"] for k in self.named_vars.keys() if x in k | ||
] | ||
if len(check_for_gp_vars) > 0: | ||
raise Exception("Unable to clone Gaussian Process 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.
This should be in the low-level utility used by clone_model
, fgraph_from_model
? Also I think it should be a warning, because we are not sure something with _rotated_
is a GP or just a user variable name for a vanilla variable.
Also @bwengals any other names we could look for to detect GPs in the model? This is not perfect, but avoiding as much surprise as possible would be great.
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.
@ricardoV94 Yes, you are absolutely correct. This should be a warning and yes, I can move it to the lower-level utility used by clone_model
. Do you know if there is a way that I can check model variables to discern the type of the variable it is? Something like type(model.named_vars)
and it would say it was a GP
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.
Nope, no way to know it was a GP, that's the unfortunate bit. GPs produce vanilla MvNormals or deterministics
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 for the very delayed response. No, there isn't. What I've done to smuggle GPs out with the model in the past is to do something like:
with pm.Model() as model:
# model code here
gp1 = pm.gp.Latent(...)
gp2 = pm.gp.HSGP(...)
model.gps = {"gp1": gp1, "gp2": gp2, ...}
If GPs added themselves to the model context automatically they could be tracked. They don't have names though, so need a little thought for how to key that dictionary, although maybe putting them in a list is OK.
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.
Hey @bwengals, if I understand you correctly, you are saying that we can add GPs to the Model class as an attribute and that would happen in core.py
in the section after line 539 and then we can pull those variables just like the rest of the variables in fgraph_from_model()
in the section after line 169 inside of the fgraph.py
file.
(sorry for the line number references I couldn't figure out how to highlight the code in the comment. I recently just started contributing to open source)
tests/model/test_core.py
Outdated
@@ -1761,3 +1762,52 @@ def test_graphviz_call_function(self, var_names, filenames) -> None: | |||
figsize=None, | |||
dpi=300, | |||
) | |||
|
|||
|
|||
class TestModelCopy(unittest.TestCase): |
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.
We use pytest, any reason we need to subclass unittest
?
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.
Oh, I must have made a mistake. I am sorry I am new to using pytest. I will fix that as well!
@Dekermanjian looks great! I left some suggestions, let me know if something does not make sense |
Thank you @ricardoV94 for your feedback! I appreciate it! |
pymc/model/fgraph.py
Outdated
@@ -391,6 +393,11 @@ def clone_model(model: Model) -> Model: | |||
z = pm.Deterministic("z", clone_x + 1) | |||
|
|||
""" | |||
check_for_gp_vars = [ |
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 be in the even lower fgraph_from_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.
Oh! my bad, I will fix that now. Thank you @ricardoV94!
…ed doc example, updated pytest
Hey @ricardoV94, is there anything you'd like me to modify/add to this pull request? |
pymc/model/fgraph.py
Outdated
check_for_gp_vars = [ | ||
k for x in ["_rotated_", "_hsgp_coeffs_"] for k in model.named_vars.keys() if x in k | ||
] | ||
if len(check_for_gp_vars) > 0: | ||
warnings.warn("Unable to clone Gaussian Process Variables", UserWarning) |
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 can be simplified
check_for_gp_vars = [ | |
k for x in ["_rotated_", "_hsgp_coeffs_"] for k in model.named_vars.keys() if x in k | |
] | |
if len(check_for_gp_vars) > 0: | |
warnings.warn("Unable to clone Gaussian Process Variables", UserWarning) | |
if any(name in ("_rotated_", "_hsgp_coeffs_") for name in model.named_vars): | |
warnings.warn("Unable to clone Gaussian Process Variables", UserWarning) |
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, that looks better. I will make that change. Thank you!
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.
Oh, hey @ricardoV94. Since we moved that check to the fgraph_from_model()
function maybe the warning should say something different instead of mentioning cloning? Is this function only used for cloning?
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.
Hey, one more thing. I wasn't able to get the above suggestion to work. I think that we need to check if the string subset ("rotated" & "hsgp_coeffs") are in the name of the variable. I was able to get it to work by making a little change:
if any(gp_name in var_name for gp_name in ["_rotated_", "_hsgp_coeffs_"] for var_name in model.named_vars):
warnings.warn("Unable to clone Gaussian Process Variables", UserWarning)
Is that okay with you?
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.
Oh sorry of course. Since there are only two options I would do an or
statement then: if '_rotated_' in var_name or '_hsgp_coeffs_' in var_name for var_name in ...
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.
Hey @ricardoV94, I couldn't figure out how to get it to work by only looping through model.named_vars one time like you have in your example. I had to do a loop in each one of the or
cases like this:
if any("_rotated_" in var_name for var_name in model.named_vars) or any("_hsgp_coeffs_" in var_name for var_name in model.named_vars):
warnings.warn("Unable to clone Gaussian Process Variables", UserWarning)
Am I missing something?
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 any(("_rotated_" in var_name or "_hsgp_coeffs_" in var_name) for var_name in model.named_vars)
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 let's give a more informative warning. Something like: "Detected variables likely created by GP objects. Further use of these old GP objects should be avoided as it may reintroduce variables from the old model. See issue: https://github.com/pymc-devs/pymc/issues/6883"
.
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.
Thanks @ricardoV94, I have made the suggestions and pushed the changes.
pymc/model/fgraph.py
Outdated
@@ -369,7 +377,7 @@ def clone_model(model: Model) -> Model: | |||
|
|||
Recreates a PyMC model with clones of the original variables. | |||
Shared variables will point to the same container but be otherwise different objects. | |||
Constants are not cloned. | |||
Constants are not cloned and if guassian process variables are detected then a warning will be triggered. |
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 not mention it, this is just a temporary thing we want to fix
Constants are not cloned and if guassian process variables are detected then a warning will be triggered. | |
Constants are not cloned. |
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.
Okay, I will make that change as well!
…ntation for clone_model function
9b031dd
to
88fde25
Compare
Thank you @ricardoV94 for your help with this and for your patience. I really appreciate 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.
Hi, I had another pass and I have a couple minor suggestions. I think this should be it!
Thanks for your patience as well 🙏
tests/model/test_core.py
Outdated
pm.Normal("y", f * 2) | ||
return gp_model | ||
|
||
def test_copy_model(self) -> None: |
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.
These tests have a lot of duplicated code that we can avoid if you use pytest.mark.parametrize("copy_method", (copy.copy, copy.deepcopy))
tests/model/test_core.py
Outdated
|
||
class TestModelCopy: | ||
@staticmethod | ||
def simple_model() -> pm.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.
No need to use staticmethods as these aren't used in more than one single test?
tests/model/test_core.py
Outdated
deepcopy_simple_model = copy.deepcopy(simple_model) | ||
|
||
with simple_model: | ||
simple_model_prior_predictive = pm.sample_prior_predictive(random_seed=42) |
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.
Taking a single draw should be enough.
I would also test that adding a deterministic to the copy model does not introduce one in the original model (basically the example you had in the docstrings)
tests/model/test_core.py
Outdated
|
||
def test_guassian_process_copy_failure(self) -> None: | ||
gaussian_process_model = self.gp_model() | ||
with pytest.warns(UserWarning): |
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.
Add a match kwarg to check the UserWarning is actually the one we care about
with pytest.warns(UserWarning): | |
with pytest.warns(UserWarning match=...): |
…inistics to clone model, added copy method to Model class
Hey Ricardo, I made the suggested changes. The tests look a lot better now. Thank you for reviewing it! |
pymc/model/core.py
Outdated
Clone a pymc model by overiding the python copy method using the clone_model method from fgraph. | ||
Constants are not cloned and if guassian process variables are detected then a warning will be triggered. |
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.
Clone a pymc model by overiding the python copy method using the clone_model method from fgraph. | |
Constants are not cloned and if guassian process variables are detected then a warning will be triggered. | |
Clone the model. | |
To access variables in the cloned model use `cloned_model["var_name"]`. |
tests/model/test_core.py
Outdated
simple_model_prior_predictive_mean = simple_model_prior_predictive["prior"]["y"].mean( | ||
("chain", "draw") | ||
) | ||
copy_simple_model_prior_predictive_mean = copy_simple_model_prior_predictive["prior"][ | ||
"y" | ||
].mean(("chain", "draw")) |
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.
No need to take them mean, now that it's a single value. Just retrieve it with simple_model_prior_preictive.prior["y"].values
tests/model/test_core.py
Outdated
copy_method(gaussian_process_model) | ||
|
||
@pytest.mark.parametrize("copy_method", (copy.copy, copy.deepcopy)) | ||
def test_adding_deterministics_to_clone(self, copy_method) -> None: |
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 check can be done in the first test. That way you can also confirm the prior_predictive["z"] draw is what you expect (and only exists in the cloned model)
tests/model/test_core.py
Outdated
"y" | ||
].mean(("chain", "draw")) | ||
|
||
assert np.isclose( |
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.
You can check exact equality, since the draws are exactly the same
Hey Ricardo, I made the changes you suggested. There is one part in the test I wasn't sure if there is a simpler way to do. When I sample a prior predictive, if I include the deterministic the first values for y between the copy model and the original model would not be the same. So I have to sample prior predictives twice one without the deterministic and another time with the deterministic. Anyway that can be simplified? |
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.
Very minor nitpicks to reduce test verbosity.
tests/model/test_core.py
Outdated
with copy_simple_model: | ||
z = pm.Deterministic("z", copy_simple_model["alpha"] + 1) | ||
copy_simple_model_prior_predictive = pm.sample_prior_predictive( | ||
samples=1, random_seed=42 | ||
) |
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.
You can do this above, and call sample_prior_predictive
only once for the copy_simple_model
tests/model/test_core.py
Outdated
simple_model_prior_predictive_val = simple_model_prior_predictive["prior"]["y"].values | ||
copy_simple_model_prior_predictive_val = copy_simple_model_prior_predictive["prior"][ |
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.
Just compare directly, no need to assign to separate variables, that are almost as verbose as the way they are accessed
It seems I keep trying to make the tests smaller. Very sorry! Let me know if it feels too much :) |
Ah... I didn't expect that, it shouldn't be the case in theory. Let me have a look, but we can leave as is if true |
Not a problem at all. It is for the best to remove any redundancies. |
I tried this again. When I sample prior predictive with the copy model that includes the deterministic the variable 'y' value no longer matches the value from the original model and the assert statement doesn't pass. |
You're right. I was testing with a simpler model and it worked fine. With multiple random variables the seeding will act differently. But with a single random variable its fine. I guess we can use a single RV for this example, since we are not even checking the others? import pymc as pm
from pymc.model.fgraph import clone_model
with pm.Model() as m:
x = pm.Normal("x")
print(pm.sample_prior_predictive(draws=1, random_seed=1).prior["x"].values)
with clone_model(m) as new_m:
y = pm.Deterministic("y", new_m["x"] + 1)
print(pm.sample_prior_predictive(draws=1, random_seed=1).prior["x"].values)
|
I also notice that if my deterministic is a function of alpha or error it will change the value of the prior predictive. However, if my deterministic is a function of the likelihood (like in your example) then the prior values are exactly the same. |
Yes, I said so above, but maybe it was not obvious. With multiple random variables, the deterministic ends up changing which seed goes to which variable (after they are split internally). But your test does not need the extra random variables right? The simpler, single random variable should be enough. It doesn't matter if it's observed or not for prior_predictive. |
Okay, this makes sense. This is because PyTensor gives each RV a unique seed correct? You are correct for the purposes of testing we don't need extra RVs. I made the simplification and pushed the changes. Thank you, Ricardo. |
Thanks a ton @Dekermanjian 😊 |
@ricardoV94 Happy to help! :) Ricardo, I have a quick question for you. I am trying to learn the lower level code for PyTensor so that I can contribute in a more meaningful way. I have walked through the PyTensor Dev tutorials but the tutorials are a little bit terse. I am wondering if there are more resources to learning the low level code. Maybe an example somewhere that shows how a simple PYMC model is constructed without using the high level abstractions. So defining the graph, the logp, and the sampling algorithm from scratch. Is something like that available? |
@Dekermanjian have you seen this guide? https://www.pymc.io/projects/docs/en/stable/learn/core_notebooks/pymc_pytensor.html I have actually been thinking about doing a series on implementing the core of PyMC in a live stream, which you might have liked if it already existed ? :) |
@ricardoV94 I have not seen that one before! Thank you this will be very helpful.
Yes, that would be fantastic. PYMC is wonderful for users because of the abstractions but for those who are interested in contributing development efforts it is difficult to connect the pieces just by going through the code base. |
PRs pymc-devs#7508 and pymc-devs#7492 introduced incompatible changes but were not tested simultaneously. Deepcopying the steps in the tests leads to deepcopying the model which uses `clone_model`, which in turn does not support initvals.
PRs pymc-devs#7508 and pymc-devs#7492 introduced incompatible changes but were not tested simultaneously. Deepcopying the steps in the tests leads to deepcopying the model which uses `clone_model`, which in turn does not support initvals.
PRs pymc-devs#7508 and pymc-devs#7492 introduced incompatible changes but were not tested simultaneously. Deepcopying the steps in the tests leads to deepcopying the model which uses `clone_model`, which in turn does not support initvals.
Description
I added
__copy__
and__deepcopy__
methods to the Model class. Both will use theclone_model()
method frompymc.model.fgraph
and if any guassian process variables are detected in the model an exception is raised. I also created 2 unit tests to test both that the methods work and that the exception is raised upon detecting a gaussian process variableRelated Issue
__copy__
and__deepcopy__
model methods #6985Checklist
Type of change
📚 Documentation preview 📚: https://pymc--7492.org.readthedocs.build/en/7492/