Profile: add multistart optimization for profile points#1712
Conversation
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #1712 +/- ##
===========================================
+ Coverage 84.35% 84.37% +0.01%
===========================================
Files 164 164
Lines 14752 14799 +47
===========================================
+ Hits 12444 12486 +42
- Misses 2308 2313 +5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
We were changing the step size of profiling parameter to 0 by accident.
- explicit absolute/relative profile step-size options - deprecated old absolute step-size option names - per-parameter step-size family resolution from `ub - lb` - finite-bound validation before profiling - single upfront step-size resolution before profile walking - resolved step sizes passed through profiling tasks/proposals - lightweight many-steps precheck warning/error mode - focused profile tests for options, resolution, precheck
PaulJonasJost
left a comment
There was a problem hiding this comment.
Regarding open question: I am strongly against loosening bounds. They are there for a reason, even if they are set uninformed. Thus they should not be just changed, especially not mid workflow. Other view on this is that if I loosen the bounds, I do not guarantee at all anymore, that my optimum is actually an optimum, e.g. if in the optimisation we already hit the same bound.
| reg_order: int = 4, | ||
| adaptive_target_scaling_factor: float = 1.5, | ||
| whole_path: bool = False, | ||
| profile_n_starts: int = 6, |
There was a problem hiding this comment.
Wouldn`t keeping the default at one or two be better? In case everything works it is 1/6 (1/3)of the overall work and otherwise depending on how fatally it fails, we can add starts?
There was a problem hiding this comment.
True
This is the design choice of the default being either
- robust according to the benchmark we ran
- initial non-robust run but faster --> in models with non-identifiability will probably show many jumps.
Having less starts had a large impact on the amount of jumps the profiles showed for the benchmark models I tested.
Based on previous discussions we had in team meetings, it seemed that the default we would like to have would be the robust one -- so all settings are following that -> order 0 (next PR), medium step size (previous PR), 6 multistarts.
Would you vote for the efficient version rather the robust one?
| logger.warning( | ||
| "Profiling found lower function values than the supplied " | ||
| f"optimization result (fval = {global_opt:.6g}) for:\n" | ||
| f"{par_lines}\n" | ||
| "This means profiling was started from a suboptimal point, so the " | ||
| "profile ratios and confidence thresholds are not anchored to the " | ||
| "true global optimum. Re-optimization of the model is suggested. " | ||
| "The better profile points can be included as startpoints " | ||
| "through `x_guesses` of the optimization problem." | ||
| ) |
There was a problem hiding this comment.
If I get the message right it is notifying me when I find a point actually better then the best found point from optimization? Like the message, but I think it should also include how to get the best points out if it includes the x_guesses hint
| problem=problem, | ||
| x0=startpoint, | ||
| id=str(0), | ||
| optimize_options=OptimizeOptions(allow_failed_starts=True), |
There was a problem hiding this comment.
why allow failed starts?
There was a problem hiding this comment.
Actually in this one I tend to understand (even though it should be impossible. But In 76 it seems more weird.
There was a problem hiding this comment.
I'd also stick to False. Maybe make it configurable.
In my opinion, Objective.__call__ should never raise when called with admissible parameters. If it does, this is most likely a problem with the Objective and we should not cover that up.
There was a problem hiding this comment.
True
I put it there with something else in mind that doesn't apply. Will change to False
|
|
||
| for k, j_par in enumerate(problem.x_free_indices): | ||
| x_j = optimizer_result.x[j_par] | ||
| lb_j = problem.lb[k] | ||
| ub_j = problem.ub[k] | ||
| at_lb = abs(x_j - lb_j) <= 1e-8 | ||
| at_ub = abs(x_j - ub_j) <= 1e-8 | ||
| if (at_lb or at_ub) and j_par not in warned_at_bound: | ||
| warned_at_bound.add(j_par) | ||
| bound_val = lb_j if at_lb else ub_j | ||
| logger.warning( | ||
| f"Parameter '{problem.x_names[j_par]}' hit its " | ||
| f"{'lower' if at_lb else 'upper'} bound " | ||
| f"({bound_val:.4g}) while profiling " | ||
| f"'{problem.x_names[i_par]}'. " | ||
| "The profile may be constrained near this region." | ||
| ) | ||
|
|
There was a problem hiding this comment.
Not sure how informative that actually is, as it should become very apparent in the 2d plots right? Would potentially remove.
There was a problem hiding this comment.
True
Still wanted to add it as Jan mentioned it was in MATLAB previously, and it might just indicate something "might be happening" to users.
I don't think it hurts, as we also send it only once ever per parameter. So logs won't get cluttered with it.
| assert not precheck_warnings | ||
|
|
||
|
|
||
| def test_profile_multistart_optimize_uses_best_start(monkeypatch): |
There was a problem hiding this comment.
what does monkeyxpatch do?
| additional optimization startpoints from a Gaussian centered at the | ||
| point suggested by `profile_next_guess`, in the reduced parameter | ||
| space. The profiled parameter itself is already fixed and is therefore | ||
| not perturbed. |
There was a problem hiding this comment.
| not perturbed. | |
| not perturbed. Only relevant if `profile_n_starts > 1`. |
Or similar.
| adaptive_target_scaling_factor: float = 1.5, | ||
| whole_path: bool = False, | ||
| profile_n_starts: int = 6, | ||
| profile_sampling_sigma: float = 0.01, |
There was a problem hiding this comment.
Since this is an absolute sigma, it might be nice to support per-parameter settings, but I guess this can also easily be extended later.
| better_optima.append((problem.x_names[i_par], best_fval)) | ||
| if better_optima: | ||
| par_lines = "\n".join( | ||
| f" {name}: best fval = {fval:.6g}" for name, fval in better_optima |
There was a problem hiding this comment.
With the isclose thresholds above, this may display identical values, which might be confusing.
| problem=problem, | ||
| x0=startpoint, | ||
| id=str(0), | ||
| optimize_options=OptimizeOptions(allow_failed_starts=True), |
There was a problem hiding this comment.
I'd also stick to False. Maybe make it configurable.
In my opinion, Objective.__call__ should never raise when called with admissible parameters. If it does, this is most likely a problem with the Objective and we should not cover that up.
I added multistart optimization at each profile point.
Additional starts are sampled around the point suggested by
profile_next_guess, and the original suggested start is always included as well. Failed starts are tolerated, and the best finite result is kept.I also added warnings when profiling finds lower function values than the supplied optimum, and when non-profiled parameters hit bounds during profiling.
Also:
profile_n_startsandprofile_sampling_sigmatoProfileOptionsOpen question: when non-profiled parameters hit bounds during profiling, should we eventually support relaxing those bounds (of the parameters we're not profiling), or is warning-only the better behavior here? Allowing a relaxation of the bounds would make the plots actually show non-identifiable parameters when they are such. But we might risk some weird optimisation failures in some unusual cases.
Should be merged only after #1711 -- contains all changes of that PR currently, too.