Skip to content

Commit 7bc6ca1

Browse files
committed
made optimization switchable between true RMSE and robustified loss
1 parent e8e2842 commit 7bc6ca1

4 files changed

Lines changed: 112 additions & 28 deletions

File tree

examples/4_performance_analysis.ipynb

Lines changed: 91 additions & 19 deletions
Large diffs are not rendered by default.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""This module implements constant-derivative model-based smoothers based on Kalman filtering and its generalization.
22
"""
3-
from ._kalman_smooth import kalman_filter, rts_smooth, rtsdiff, constant_velocity, constant_acceleration, constant_jerk, robustdiff, convex_smooth
3+
from ._kalman_smooth import kalman_filter, rts_smooth, rtsdiff, constant_velocity, constant_acceleration, constant_jerk, robustdiff, convex_smooth, robustdiffclassic

pynumdiff/kalman_smooth/_kalman_smooth.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@ def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=0):
307307
return x_states[0], x_states[1]
308308

309309

310+
def robustdiffclassic(x, dt, order, log_qr_ratio, huberM):
311+
return robustdiff(x, dt, order, 4, 4 - log_qr_ratio, huberM, huberM)
312+
313+
310314
def convex_smooth(y, A, Q, C, R, proc_huberM, meas_huberM):
311315
"""Solve the optimization problem for robust smoothing using CVXPY. Note this currently assumes constant dt
312316
but could be extended to handle variable step sizes by finding discrete-time A and Q for requisite gaps.

pynumdiff/optimize/_optimize.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ..polynomial_fit import polydiff, savgoldiff, splinediff
1515
from ..basis_fit import spectraldiff, rbfdiff
1616
from ..total_variation_regularization import tvrdiff, velocity, acceleration, jerk, iterative_velocity, smooth_acceleration, jerk_sliding
17-
from ..kalman_smooth import rtsdiff, constant_velocity, constant_acceleration, constant_jerk, robustdiff
17+
from ..kalman_smooth import rtsdiff, constant_velocity, constant_acceleration, constant_jerk, robustdiff, robustdiffclassic
1818
from ..linear_model import lineardiff
1919

2020
# Map from method -> (search_space, bounds_low_hi)
@@ -96,6 +96,11 @@
9696
'log_r': (-5, 16),
9797
'proc_huberM': (0, 6),
9898
'meas_huberM': (0, 6)}),
99+
robustdiffclassic: ({'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_qr_ratio': [k for k in range(-1, 16, 4)],
101+
'huberM': [0., 5, 20]}, # 0. so type is float. Good choices here really depend on the data scale
102+
{'log_qr_ratio': (-1, 18),
103+
'huberM': (0, 1e2)}), # really only want to use l2 norm when nearby
99104
lineardiff: ({'kernel': 'gaussian',
100105
'order': 3,
101106
'gamma': [1e-1, 1, 10, 100],
@@ -117,7 +122,7 @@
117122

118123
# This function has to be at the top level for multiprocessing but is only used by optimize.
119124
def _objective_function(point, func, x, dt, singleton_params, categorical_params, search_space_types,
120-
dxdt_truth, metric, tvgamma, padding, cache):
125+
dxdt_truth, metric, tvgamma, padding, cache, huberM):
121126
"""Function minimized by scipy.optimize.minimize, needs to have the form: (point, *args) -> float
122127
This is mildly complicated, because "point" controls the settings of a differentiation function, but
123128
the method may have numerical and non-numerical parameters, and all such parameters are now passed by
@@ -152,14 +157,15 @@ def _objective_function(point, func, x, dt, singleton_params, categorical_params
152157
elif metric == 'error_correlation':
153158
ec = evaluate.error_correlation(dxdt_truth, dxdt_hat, padding=padding)
154159
cache[key] = ec; return ec
155-
else: # then minimize sqrt{2*Mean(Huber((x_hat- x)/sigma))}*sigma + gamma*TV(dxdt_hat)
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)
160+
else: # then minimize (RMSE(x_hat - x) || sqrt{2*Mean(Huber((x_hat- x)/sigma, M))}*sigma) + gamma*TV(dxdt_hat)
161+
# rubust_rme(,inf) = rmse(), so just use the simpler function in that case
162+
cost = evaluate.rmse(x, x_hat, padding=padding) if huberM == float('inf') else evaluate.robust_rme(x, x_hat, padding=padding, M=huberM)
163+
cost += tvgamma*evaluate.total_variation(dxdt_hat, padding=padding)
158164
cache[key] = cost; return cost
159165

160166

161167
def optimize(func, x, dt, dxdt_truth=None, tvgamma=1e-2, search_space_updates={}, metric='rmse',
162-
padding=0, opt_method='Nelder-Mead', maxiter=10, parallel=True):
168+
padding=0, opt_method='Nelder-Mead', maxiter=10, parallel=True, huberM=float('inf')):
163169
"""Find the optimal hyperparameters for a given differentiation method.
164170
165171
:param function func: differentiation method to optimize parameters for, e.g. linear_model.savgoldiff
@@ -182,6 +188,8 @@ def optimize(func, x, dt, dxdt_truth=None, tvgamma=1e-2, search_space_updates={}
182188
:param bool parallel: whether to use multiple processes to optimize, typically faster for single optimizations.
183189
For experiments, it is a usually a better use of resources to parallelize at that level, meaning
184190
each must run in its own process, since spawned processes are not allowed to further spawn.
191+
:param float huberM: For ground-truth-less situation, if :math:`M < \\infty`, use outlier-robust, Huber-based accuracy
192+
metric in loss function.
185193
186194
:return: - **opt_params** (dict) -- best parameter settings for the differentation method
187195
- **opt_value** (float) -- lowest value found for objective function
@@ -220,15 +228,15 @@ def optimize(func, x, dt, dxdt_truth=None, tvgamma=1e-2, search_space_updates={}
220228
# wrap the objective and scipy.optimize.minimize to pass kwargs and a host of other things that remain the same
221229
_obj_fun = partial(_objective_function, func=func, x=x, dt=dt, singleton_params=singleton_params,
222230
categorical_params=categorical_combo, search_space_types=search_space_types, dxdt_truth=dxdt_truth,
223-
metric=metric, tvgamma=tvgamma, padding=padding, cache=cache)
231+
metric=metric, tvgamma=tvgamma, padding=padding, cache=cache, huberM=huberM)
224232
_minimize = partial(scipy.optimize.minimize, _obj_fun, method=opt_method, bounds=bounds, options={'maxiter':maxiter})
225233
results += pool.map(_minimize, starting_points) # returns a bunch of OptimizeResult objects
226234
else: # For experiments, where I want to parallelize optimization calls and am not allowed to have each spawn further processes
227235
cache = dict()
228236
for categorical_combo in categorical_combos:
229237
_obj_fun = partial(_objective_function, func=func, x=x, dt=dt, singleton_params=singleton_params,
230238
categorical_params=categorical_combo, search_space_types=search_space_types, dxdt_truth=dxdt_truth,
231-
metric=metric, tvgamma=tvgamma, padding=padding, cache=cache)
239+
metric=metric, tvgamma=tvgamma, padding=padding, cache=cache, huberM=huberM)
232240
_minimize = partial(scipy.optimize.minimize, _obj_fun, method=opt_method, bounds=bounds, options={'maxiter':maxiter})
233241
results += [_minimize(p) for p in starting_points]
234242

0 commit comments

Comments
 (0)