Skip to content
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

Adding a helper method to check model gradients before optimization. #690

Merged
merged 20 commits into from
Jul 6, 2021

Conversation

stephanmg
Copy link
Contributor

@stephanmg stephanmg commented Jul 1, 2021

Check for the gradients of the model equations and returns False if gradients are incompatible / not well-suited for gradient-based optimizers otherwise returns True.

@codecov-commenter
Copy link

codecov-commenter commented Jul 1, 2021

Codecov Report

Merging #690 (50930d7) into develop (16639a4) will increase coverage by 55.52%.
The diff coverage is 48.83%.

Impacted file tree graph

@@             Coverage Diff              @@
##           develop     #690       +/-   ##
============================================
+ Coverage    34.58%   90.10%   +55.52%     
============================================
  Files           96       96               
  Lines         6466     6507       +41     
============================================
+ Hits          2236     5863     +3627     
+ Misses        4230      644     -3586     
Impacted Files Coverage Δ
pypesto/objective/base.py 81.95% <11.76%> (+29.14%) ⬆️
pypesto/objective/amici.py 82.69% <20.00%> (+62.98%) ⬆️
pypesto/petab/importer.py 85.08% <85.71%> (+85.08%) ⬆️
pypesto/predict/constants.py 100.00% <0.00%> (+4.34%) ⬆️
pypesto/optimize/optimize.py 94.00% <0.00%> (+6.00%) ⬆️
pypesto/startpoint/uniform.py 100.00% <0.00%> (+7.14%) ⬆️
pypesto/optimize/optimizer.py 90.09% <0.00%> (+7.37%) ⬆️
pypesto/engine/multi_process.py 100.00% <0.00%> (+7.40%) ⬆️
pypesto/optimize/options.py 100.00% <0.00%> (+10.52%) ⬆️
pypesto/ensemble/constants.py 91.83% <0.00%> (+12.24%) ⬆️
... and 66 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 16639a4...50930d7. Read the comment docs.

stephanmg and others added 3 commits July 1, 2021 12:42
Co-authored-by: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com>
Co-authored-by: Paul Jonas Jost <70631928+PaulJonasJost@users.noreply.github.com>
@stephanmg
Copy link
Contributor Author

stephanmg commented Jul 1, 2021

Added your suggestion @PaulJonasJost - thanks for the pointers.

@stephanmg stephanmg requested a review from dweindl July 1, 2021 14:38
Copy link
Member

@dweindl dweindl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally useful. I am only wondering if it might be better to move it to a different place . It doesn't really depend on PEtab. pypesto.Problem? @yannikschaelte ?

free_indices,
multi_eps=[1e-3, 1e-4, 1e-5],
mode=mode))
except (RuntimeError, ValueError):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Short comment why/when this is expected to happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in commit, see below.

@@ -78,6 +80,38 @@ def from_yaml(yaml_config: Union[dict, str],
output_folder=output_folder,
model_name=model_name)

def check_and_log_gradients(self, RTOL: float = 1e-2, ATOL: float = 1e-3):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use lower-case argument names

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected.

Comment on lines 84 to 85
"""Check and log if gradients are compatible with gradient-based optimizers
Parameters
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Check and log if gradients are compatible with gradient-based optimizers
Parameters
"""Check and log if gradients match finite differences
Parameters

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the documentation brief.

@dilpath
Copy link
Member

dilpath commented Jul 1, 2021

Generally useful. I am only wondering if it might be better to move it to a different place . It doesn't really depend on PEtab. pypesto.Problem? @yannikschaelte ?

Perhaps the ObjectiveBase class, where the gradient check methods are implemented? However, the convenience of automatically using petab_problem.x_nominal_scaled as the test vector would then be lost. This could still be possible by redefining check_and_log_gradients in AmiciObjective, as e.g.:

def check_and_log_gradients(self, x: np.ndarray = None, *args, **kwargs):
    if x is None and 'petab_problem' in dir(self.amici_object_builder):
        x = self.amici_object_builder.petab_problem.x_nominal_scaled
    return super(ObjectiveBase, self).check_and_log_gradients(x=x, *args, **kwargs)

Copy link
Member

@dilpath dilpath left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Mostly suggestions, feel free to ignore, thanks for the helper method!

try:
dfs.append(objective.check_grad_multi_eps(
free_indices,
multi_eps=[1e-3, 1e-4, 1e-5],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could remove multi_eps from here (check_grad_multi_eps already has a default multi_eps) and replace with *args and **kwargs, so that the user has more control over the gradient check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 100 to 101
modes = [MODE_FUN, MODE_RES]
for mode in modes:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop over modes could be by default, but also allow users to only specify one of MODE_FUN or MODE_RES, to reduce computation (perhaps not a significant issue except for large problems).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, default is to consider both modes but give the user control over it.

multi_eps=[1e-3, 1e-4, 1e-5],
mode=mode))
except (RuntimeError, ValueError):
dfs.append(dfs[0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, what does this achieve? Also, it will raise an error if dfs == [] too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is to ensure len(dfs) == 2 for the return statement, then see my suggestion there for an alternative.

Comment on lines 93 to 96
objective.amici_solver.setSensitivityMethod(
amici.SensitivityMethod_forward)
objective.amici_solver.setAbsoluteTolerance(1e-10)
objective.amici_solver.setRelativeTolerance(1e-12)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These hard-coded problem-dependent settings should instead be default values that the user can customize (some models will fail at these tolerances, or may be overly computationally-expensive with forward sensitivities). Alternatively, the function could accept an objective that is already customized by the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made these hardcoded values (1e-10 and 1e-12) a user input, allow for pre-configured objective function as user input as well.

RTOL: relative error tolerance (default 1e-2)
ATOL: absolute error tolerance (default 1e-3)
"""
par = np.asarray(self.petab_problem.x_nominal_scaled)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be by default but also accept a user-supplied vector.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made as default and allow user defined tolerances.

Comment on lines 110 to 113
return np.all((dfs[0].rel_err.values < RTOL) |
(dfs[0].abs_err.values < ATOL) |
(dfs[1].rel_err.values < RTOL) |
(dfs[1].abs_err.values < ATOL))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return np.all((dfs[0].rel_err.values < RTOL) |
(dfs[0].abs_err.values < ATOL) |
(dfs[1].rel_err.values < RTOL) |
(dfs[1].abs_err.values < ATOL))
# Gradients are correct if any of the error types are within their
# respective tolerance, for any of the modes.
return any([
any([
np.all(mode_df.rel_err.values < RTOL),
np.all(mode_df.abs_err.values < ATOL),
])
for mode_df in dfs
])

For another suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged.

@@ -76,6 +76,13 @@ def test_3_optimize(self):
self.assertTrue(np.isfinite(
result.optimize_result.get_for_key('fval')[0]))

def test_4_gradients(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_4_gradients(self):
def test_check_and_log_gradients(self):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I thought you want to enumerate the test in the class, as the previous tests are enumerated. Changed!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, didn't notice sorry, fine as before then.

@@ -1,4 +1,6 @@
from pypesto.objective.constants import MODE_FUN, MODE_RES
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from pypesto.objective.constants import MODE_FUN, MODE_RES
from ..objective.constants import MODE_FUN, MODE_RES

Relative imports are a few lines below, could move this there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned up imports.

@@ -78,6 +80,38 @@ def from_yaml(yaml_config: Union[dict, str],
output_folder=output_folder,
model_name=model_name)

def check_and_log_gradients(self, RTOL: float = 1e-2, ATOL: float = 1e-3):
"""Check and log if gradients are compatible with gradient-based optimizers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the log functionality of this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, the logging is implictly done if log level is specified to check_grad_multi_eps.

@stephanmg
Copy link
Contributor Author

@dilpath @dweindl @PaulJonasJost please compare if I added all your suggestions.

Copy link
Member

@yannikschaelte yannikschaelte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally useful. I am only wondering if it might be better to move it to a different place . It doesn't really depend on PEtab. pypesto.Problem? @yannikschaelte ?

Perhaps the ObjectiveBase class, where the gradient check methods are implemented? However, the convenience of automatically using petab_problem.x_nominal_scaled as the test vector would then be lost. This could still be possible by redefining check_and_log_gradients in AmiciObjective, as e.g.:

def check_and_log_gradients(self, x: np.ndarray = None, *args, **kwargs):
    if x is None and 'petab_problem' in dir(self.amici_object_builder):
        x = self.amici_object_builder.petab_problem.x_nominal_scaled
    return super(ObjectiveBase, self).check_and_log_gradients(x=x, *args, **kwargs)

sounds good to me, i.e. defining generically in the base class and for amici/petab allowing to use the reference parameter.

objAbsoluteTolerance: float = 1e-10,
objRelativeTolerance: float = 1e-12,
multi_eps=None,
**kwargs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**kwargs
**kwargs,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

modes = [mode]

if multi_eps is None:
multi_eps = [1e-3, 1e-4, 1e-5]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in ODE problems, we often find FDs of step size between 1e-1 and 1e-8 stable (see also https://github.com/ICB-DCM/pyPESTO/blob/develop/pypesto/objective/finite_difference.py#L66), so one could define a broader default regime.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed.

Comment on lines 96 to 97
"""Check if gradients match finite differences (FDs)
Parameters
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Check if gradients match finite differences (FDs)
Parameters
"""Check if gradients match finite differences (FDs)
Parameters

----------
rtol: relative error tolerance
atol: absolute error tolerance
mode: unction values or residuals
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mode: unction values or residuals
mode: function values or residuals

Comment on lines 91 to 92
objAbsoluteTolerance: float = 1e-10,
objRelativeTolerance: float = 1e-12,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
objAbsoluteTolerance: float = 1e-10,
objRelativeTolerance: float = 1e-12,
obj_absolute_tolerance: float = 1e-10,
obj_relative_tolerance: float = 1e-12,

We use snake_case for everything but class names (PEP8). Please also adapt below.


Returns
-------
bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer using return type hints in the function signature instead of specifying it in the docstring.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, call it "match" or something

rtol: float = 1e-2,
atol: float = 1e-3,
mode: Literal = None,
objective: Objective = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
objective: Objective = None,
objective: Optional[Objective] = None,

*args,
rtol: float = 1e-2,
atol: float = 1e-3,
mode: Literal = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mode: Literal = None,
mode: Optional[Literal] = None,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't use plain Literal in Optional.

# fails for specified tolerances in forward sensitivities
pass

return any([
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return any([
return all([

Shouldn't that be all? If it fails for one mode, I'd consider that a failure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it should be all, you are right.

except (RuntimeError, ValueError):
# Might happen in case PEtab problem not well defined or
# fails for specified tolerances in forward sensitivities
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will the function return in this case? Or will it raise during the check? I wouldn't not want it return True.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning False now instead of pass.

Comment on lines 146 to 147
np.all(mode_df.rel_err.values < rtol),
np.all(mode_df.abs_err.values < atol),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, if one row passes only the relative tolerance and another row passes only the absolute tolerance, this report failure, right? This would be rather unexpected for me.

Copy link
Contributor Author

@stephanmg stephanmg Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you suggest to make this an or statement?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
np.all(mode_df.rel_err.values < rtol),
np.all(mode_df.abs_err.values < atol),
np.all((mode_df.rel_err.values < rtol) | (mode_df.abs_err.values < atol)),

Copy link
Contributor Author

@stephanmg stephanmg Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's what I just added.

@stephanmg
Copy link
Contributor Author

To which class should the method be moved? ObjectiveBase?

@dweindl
Copy link
Member

dweindl commented Jul 2, 2021

To which class should the method be moved? ObjectiveBase?

Meanwhile it has an AMICI dependency, so ObjectiveBase is rather not suitable anymore. Either that part needs to be thrown out, or it has to go somewhere pypesto.objective.amici*. I think others have a better sense of where it fits than I do.

@stephanmg
Copy link
Contributor Author

To which class should the method be moved? ObjectiveBase?

Meanwhile it has an AMICI dependency, so ObjectiveBase is rather not suitable anymore. Either that part needs to be thrown out, or it has to go somewhere pypesto.objective.amici*. I think others have a better sense of where it fits than I do.

Okay, maybe @yannikschaelte could clarify? Then I will make the changes accordingly. Thank you!

@yannikschaelte
Copy link
Member

To which class should the method be moved? ObjectiveBase?

Meanwhile it has an AMICI dependency, so ObjectiveBase is rather not suitable anymore. Either that part needs to be thrown out, or it has to go somewhere pypesto.objective.amici*. I think others have a better sense of where it fits than I do.

Okay, maybe @yannikschaelte could clarify? Then I will make the changes accordingly. Thank you!

What amici dependency do you mean? The amici specific forward sensi + tolerance instruction obviously only makes sense for amici, so the base method would not have those, but the rest should work?

@dweindl
Copy link
Member

dweindl commented Jul 2, 2021

What amici dependency do you mean? The amici specific forward sensi + tolerance instruction obviously only makes sense for amici, so the base method would not have those, but the rest should work?

Exactly those. Breaks encapsulation and adds a cyclic dependency.

@stephanmg
Copy link
Contributor Author

stephanmg commented Jul 2, 2021

What amici dependency do you mean? The amici specific forward sensi + tolerance instruction obviously only makes sense for amici, so the base method would not have those, but the rest should work?

Exactly those. Breaks encapsulation and adds a cyclic dependency.

Yes that's what I noticed (cyclic dependency) as well.

@dweindl
Copy link
Member

dweindl commented Jul 2, 2021

P.S.: I'd rather remove the AMICI part. In my opinion, these settings should have been applied consciously by the user before running any checks here.

@stephanmg
Copy link
Contributor Author

stephanmg commented Jul 2, 2021

So remove the AMICI part and add a required argument for an AMICIObjective or ObjectiveBase instead to the method?

@dilpath
Copy link
Member

dilpath commented Jul 2, 2021

What amici dependency do you mean? The amici specific forward sensi + tolerance instruction obviously only makes sense for amici, so the base method would not have those, but the rest should work?

Exactly those. Breaks encapsulation and adds a cyclic dependency.

Yes that's what I noticed (cyclic dependency) as well.

This suggestion resolves this right?

#690 (review)

@stephanmg
Copy link
Contributor Author

stephanmg commented Jul 2, 2021

Removed the Amici dependencies, still have to move functionality to BaseObjective.

@stephanmg
Copy link
Contributor Author

Functionality now moved to BaseObjective, added delegate to amici.py for convenient usage.

Copy link
Member

@yannikschaelte yannikschaelte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me, thanks! feel free to merge when you think it's fine.


Returns
-------
bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, call it "match" or something

test/petab/test_petab_import.py Outdated Show resolved Hide resolved
objective.amici_solver.setAbsoluteTolerance(1e-10)
objective.amici_solver.setRelativeTolerance(1e-12)

self.assertFalse(petab_problem.check_gradients(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this False again? maybe add a scenario where it's true? this https://github.com/ICB-DCM/pyPESTO/blob/develop/test/util.py#L122 should be stable enough no matter what.

Co-authored-by: Yannik Schälte <31767307+yannikschaelte@users.noreply.github.com>
@stephanmg stephanmg merged commit c0df8d6 into develop Jul 6, 2021
@stephanmg stephanmg deleted the check_and_log_gradients branch July 6, 2021 06:58
@yannikschaelte yannikschaelte mentioned this pull request Aug 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants