Skip to content

Commit 9ef8dd5

Browse files
committed
Intentionally picked Huber M in the optimization loss to be 2, which appears not to hurt performance when there are no outliers but helps the optimization better balance fidelity with smoothness when there are outliers, reran notebook and improved some of the explanatory math
1 parent ab4c42f commit 9ef8dd5

11 files changed

Lines changed: 925 additions & 1350 deletions

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ The following heuristic works well for choosing `tvgamma`, where `cutoff_frequen
8383

8484
Much more extensive usage is demonstrated in Jupyter notebooks:
8585
* Differentiation with different methods: [1_basic_tutorial.ipynb](https://github.com/florisvb/PyNumDiff/blob/master/examples/1_basic_tutorial.ipynb)
86-
* Parameter Optimization with known ground truth (only for demonstration purpose): [2a_optimizing_parameters_with_dxdt_known.ipynb](https://github.com/florisvb/PyNumDiff/blob/master/examples/2a_optimizing_parameters_with_dxdt_known.ipynb)
87-
* Parameter Optimization with unknown ground truth: [2b_optimizing_parameters_with_dxdt_unknown.ipynb](https://github.com/florisvb/PyNumDiff/blob/master/examples/2b_optimizing_parameters_with_dxdt_unknown.ipynb)
86+
* Parameter Optimization: [2_optimizing_hyperparameters.ipynb](https://github.com/florisvb/PyNumDiff/blob/master/examples/2_optimizing_hyperparameters.ipynb)
8887
* Automatic method suggestion: [3_automatic_method_suggestion.ipynb](https://github.com/florisvb/PyNumDiff/blob/master/examples/3_automatic_method_suggestion.ipynb)
8988

9089
## Repo Structure

examples/1_basic_tutorial.ipynb

Lines changed: 23 additions & 39 deletions
Large diffs are not rendered by default.

examples/2_optimizing_hyperparameters.ipynb

Lines changed: 871 additions & 0 deletions
Large diffs are not rendered by default.

examples/2a_optimizing_parameters_with_dxdt_known.ipynb

Lines changed: 0 additions & 622 deletions
This file was deleted.

examples/2b_optimizing_parameters_with_dxdt_unknown.ipynb

Lines changed: 0 additions & 651 deletions
This file was deleted.

examples/4_performance_analysis.ipynb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
},
7575
{
7676
"cell_type": "code",
77-
"execution_count": 3,
77+
"execution_count": null,
7878
"id": "1eba51f1-bf31-4f84-b82b-176461319de6",
7979
"metadata": {},
8080
"outputs": [],
@@ -90,9 +90,7 @@
9090
"\t\t\t(tvrdiff, 'TVRDiff'),\n",
9191
"\t\t\t(smooth_acceleration, 'SmoothAccelTVR'),\n",
9292
"\t\t\t(rtsdiff, 'RTSDiff'),\n",
93-
"\t\t\t(robustdiff, 'RobustDiff'),\n",
94-
"\t\t (robustdiff, 'RobustDiff2'),\n",
95-
"\t\t (robustdiff, 'RobustDiff3')]\n",
93+
"\t\t\t(robustdiff, 'RobustDiff')]\n",
9694
"sims = [(pi_cruise_control, 'Cruise Control'),\n",
9795
"\t\t(sine, 'Sum of Sines'),\n",
9896
"\t\t(triangle, 'Triangles'),\n",

pynumdiff/kalman_smooth/_kalman_smooth.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=0):
276276
norm case, because :math:`c_2` approaches :math:`\\frac{\\sqrt{2}}{M}`, cancelling the :math:`M` multiplying :math:`|\\cdot|`
277277
and leaving behind :math:`\\sqrt{2}`, the proper normalization.
278278
279+
Note that :code:`log_q` and :code:`proc_huberM` are coupled, as are :code:`log_r` and :code:`meas_huberM`, via the relation
280+
:math:`\\text{Huber}(q^{-1/2}v, M) = q^{-1}\\text{Huber}(v, Mq^{-1/2})`, but they are still independent enough that for
281+
the purposes of optimization we cannot collapse them.
282+
279283
:param np.array[float] x: data series to differentiate
280284
:param float dt: step size
281285
:param int order: which derivative to stabilize in the constant-derivative model (1=velocity, 2=acceleration, 3=jerk)
@@ -337,7 +341,7 @@ def huber_const(M): # from https://jmlr.org/papers/volume14/aravkin13a/aravkin13
337341
else huber_const(meas_huberM)*cvxpy.sum(cvxpy.huber(meas_resids, meas_huberM)) # CVXPY quirk: norm(, 1) != sum(abs()) for matrices
338342

339343
problem = cvxpy.Problem(cvxpy.Minimize(objective))
340-
try: problem.solve(solver=cvxpy.CLARABEL); print("CLARABEL succeeded")
344+
try: problem.solve(solver=cvxpy.CLARABEL)
341345
except cvxpy.error.SolverError: pass # Could try another solver here, like SCS, but slows things down
342346

343347
if x_states.value is None: return np.full((A.shape[0], N), np.nan) # There can be solver failure, even without error

pynumdiff/optimize/_optimize.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@
6060
'pad_to_zero_dxdt': {True, False},
6161
'high_freq_cutoff': [1e-3, 5e-2, 1e-2, 5e-2, 1e-1]}, # give numerical params in a list to scipy.optimize over them
6262
{'high_freq_cutoff': (1e-5, 1-1e-5)}),
63-
rbfdiff: ({'sigma': [1e-3, 1e-2, 1e-1, 1],
63+
rbfdiff: ({'sigma': [1e-2, 1e-1, 1],
6464
'lmbd': [1e-3, 1e-2, 1e-1]},
65-
{'sigma': (1e-3, 1e3),
66-
'lmbd': (1e-4, 0.5)}),
65+
{'sigma': (1e-2, 1e3),
66+
'lmbd': (1e-3, 0.5)}),
6767
tvrdiff: ({'gamma': [1e-2, 1e-1, 1, 10, 100, 1000],
6868
'order': {1, 2, 3}}, # warning: order 1 hacks the loss function when tvgamma is used, tends to win but is usually suboptimal choice in terms of true RMSE
6969
{'gamma': (1e-4, 1e7)}),
@@ -81,29 +81,21 @@
8181
rtsdiff: ({'forwardbackward': {True, False},
8282
'order': {1, 2, 3}, # for this few options, the optimization works better if this is categorical
8383
'log_qr_ratio': [float(k) for k in range(-9, 10, 2)] + [12, 16]},
84-
{'log_qr_ratio': [-10, 20]}), # qr_ratio is usually >>1
84+
{'log_qr_ratio': (-10, 20)}), # qr_ratio is usually >>1
8585
constant_velocity: ({'q': [1e-8, 1e-4, 1e-1, 1e1, 1e4, 1e8], # Deprecated method
8686
'r': [1e-8, 1e-4, 1e-1, 1e1, 1e4, 1e8],
8787
'forwardbackward': {True, False}},
8888
{'q': (1e-10, 1e10),
8989
'r': (1e-10, 1e10)}),
90-
# robustdiff: ({'order': {1, 2, 3}, # warning: order 1 hacks the loss function when tvgamma is used, tends to win but is usually suboptimal choice in terms of true RMSE
91-
# 'log_q': [1., 4, 8, 12], # decimal after first entry ensure this is treated as float type
92-
# 'log_r': [-1., 1, 4, 8],
93-
# #'proc_huberM': [0., 2, 6], # 0 is l1 norm, 1.345 is Huber 95% "efficiency", 2 assumes about 5% outliers,
94-
# 'meas_huberM': [0., 2, 6]}, # and 6 assumes basically no outliers -> l2 norm. Try (1 - norm.cdf(M))*2 to see outlier portion
95-
# {'log_q': (-1, 18),
96-
# 'log_r': (-5, 18),
97-
# 'proc_huberM': (0, 6),
98-
# 'meas_huberM': (0, 6)}),
9990
robustdiff: ({'order': {1, 2, 3}, # warning: order 1 hacks the loss function when tvgamma is used, tends to win but is usually suboptimal choice in terms of true RMSE
100-
'log_q': [1., 4, 8, 12], # decimal after first entry ensure this is treated as float type
101-
'log_r': [-1., 1, 4, 8],
102-
#'qr_ratio': [10**k for k in range(-1, 16, 4)],
103-
'huberM': [0., 5, 10]}, # 0. so type is float. Good choices here really depend on the data scale
104-
{'log_q': (0, 18),
105-
'log_r': (-5, 10),
106-
'huberM': (0, 20)}), # really only want to use quadratic when nearby; 20sigma is a huge distance
91+
'log_q': [1., 4, 7, 10, 13], # decimal after first entry ensure this is treated as float type
92+
'log_r': [-1., 2, 5, 8, 11],
93+
'proc_huberM': [0., 2, 6], # 0 is l1 norm, 1.345 is Huber 95% "efficiency", 2 assumes about 5% outliers,
94+
'meas_huberM': [0., 2, 6]}, # 6 assumes basically no outliers per outlier_portion = (1 - norm.cdf(M))*2
95+
{'log_q': (-5, 16),
96+
'log_r': (-5, 16),
97+
'proc_huberM': (0, 6),
98+
'meas_huberM': (0, 6)}),
10799
lineardiff: ({'kernel': 'gaussian',
108100
'order': 3,
109101
'gamma': [1e-1, 1, 10, 100],
@@ -161,7 +153,8 @@ def _objective_function(point, func, x, dt, singleton_params, categorical_params
161153
ec = evaluate.error_correlation(dxdt_truth, dxdt_hat, padding=padding)
162154
cache[key] = ec; return ec
163155
else: # then minimize sqrt{2*Mean(Huber((x_hat- x)/sigma))}*sigma + gamma*TV(dxdt_hat)
164-
cost = evaluate.robust_rme(x, x_hat, padding=padding) + tvgamma*evaluate.total_variation(dxdt_hat, padding=padding)
156+
# Huber M=2 means more than 95% of inliers (assuming Gaussianity) are treated with RMSE, while
157+
cost = evaluate.robust_rme(x, x_hat, padding=padding, M=2) + tvgamma*evaluate.total_variation(dxdt_hat, padding=padding)
165158
cache[key] = cost; return cost
166159

167160

pynumdiff/tests/test_diff_methods.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def spline_irreg_step(*args, **kwargs): return splinediff(*args, **kwargs)
5151
(constant_acceleration, {'r':1e-3, 'q':1e4}), (constant_acceleration, [1e-3, 1e4]),
5252
(constant_jerk, {'r':1e-4, 'q':1e5}), (constant_jerk, [1e-4, 1e5]),
5353
(rtsdiff, {'order':2, 'log_qr_ratio':7, 'forwardbackward':True}),
54-
#(robustdiff, {'order':3, 'log_q':8, 'log_r':0}), # Add back when design stabilizes
54+
(robustdiff, {'order':3, 'log_q':7, 'log_r':2}),
5555
(velocity, {'gamma':0.5}), (velocity, [0.5]),
5656
(acceleration, {'gamma':1}), (acceleration, [1]),
5757
(jerk, {'gamma':10}), (jerk, [10]),
@@ -223,10 +223,10 @@ def spline_irreg_step(*args, **kwargs): return splinediff(*args, **kwargs)
223223
[(-2, -3), (0, 0), (0, -1), (1, 1)],
224224
[(-1, -2), (1, 1), (0, -1), (1, 1)],
225225
[(0, 0), (3, 3), (0, 0), (3, 3)]],
226-
robustdiff: [[(-14, -15), (-17, -17), (0, -1), (1, 1)],
227-
[(-14, -14), (-13, -13), (0, -1), (1, 1)],
228-
[(-14, -14), (-13, -13), (0, -1), (1, 1)],
229-
[(-12, -12), (-2, -2), (0, -1), (1, 1)],
226+
robustdiff: [[(-15, -15), (-14, -15), (0, -1), (1, 1)],
227+
[(-14, -15), (-13, -13), (0, -1), (1, 1)],
228+
[(-14, -15), (-13, -13), (0, -1), (1, 1)],
229+
[(-9, -9), (-2, -2), (0, -1), (1, 1)],
230230
[(0, 0), (2, 2), (0, 0), (2, 2)],
231231
[(1, 1), (3, 3), (1, 1), (3, 3)]],
232232
lineardiff: [[(-7, -8), (-14, -14), (0, -1), (0, 0)],

pynumdiff/tests/test_utils.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ def test_simulations(request):
101101
from matplotlib import pyplot
102102
fig, axes = pyplot.subplots(2, 3, figsize=(18,7), constrained_layout=True)
103103

104-
for i,(sim,title) in enumerate(zip(
105-
[pi_cruise_control, sine, triangle, pop_dyn, linear_autonomous, lorenz_x],
104+
for i,(sim,title) in enumerate(zip([pi_cruise_control, sine, triangle, pop_dyn, linear_autonomous, lorenz_x],
106105
["Cruise Control", "Sum of Sines", "Triangles", "Logistic Growth", "Linear Autonomous", "Lorenz First Dimension"])):
107106

108107
y, x, dxdt = sim(duration=4, dt=0.01, noise_type='normal', noise_parameters=[0,0.1])

0 commit comments

Comments
 (0)