Skip to content

Commit b37abbb

Browse files
committed
added Chebyshev and MSLE metrics
1 parent 5c8f1f2 commit b37abbb

11 files changed

Lines changed: 236 additions & 63 deletions

src/app/formula.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,13 @@ fn latex_to_plain_text(text: &str) -> String {
568568
output.push_str("\\frac");
569569
}
570570
"quad" => output.push(' '),
571+
"begin" | "end" => {
572+
// Окружения нужны только для LaTeX-верстки и не должны
573+
// попадать в человекочитаемый plain-text.
574+
if matches!(chars.next(), Some('{')) {
575+
let _ = read_braced_group(&mut chars);
576+
}
577+
}
571578
"text" => {
572579
if matches!(chars.next(), Some('{')) {
573580
let content = read_braced_group(&mut chars);

src/app/i18n.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,11 @@ pub(super) fn optimization_loss_metric_label(
156156
metric: OptimizationLossMetric,
157157
) -> &'static str {
158158
match metric {
159-
OptimizationLossMetric::Mse => "MSE",
160-
OptimizationLossMetric::Mae => "MAE",
159+
OptimizationLossMetric::Mse => "MSE (L2)",
160+
OptimizationLossMetric::Mae => "MAE (L1)",
161161
OptimizationLossMetric::SoftL1 => "soft_l1",
162+
OptimizationLossMetric::Chebyshev => "Chebyshev",
163+
OptimizationLossMetric::Msle => "MSLE",
162164
}
163165
}
164166

src/app/result_export.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,9 +429,11 @@ fn optimizer_method_id(method: OptimizerMethod) -> &'static str {
429429

430430
fn objective_metric_name(metric: OptimizationLossMetric) -> &'static str {
431431
match metric {
432-
OptimizationLossMetric::Mse => "Mean Squared Error",
433-
OptimizationLossMetric::Mae => "Mean Absolute Error",
432+
OptimizationLossMetric::Mse => "Mean Squared Error (L2)",
433+
OptimizationLossMetric::Mae => "Mean Absolute Error (L1)",
434434
OptimizationLossMetric::SoftL1 => "Soft L1",
435+
OptimizationLossMetric::Chebyshev => "Chebyshev Distance (Linf)",
436+
OptimizationLossMetric::Msle => "Mean Squared Logarithmic Error",
435437
}
436438
}
437439

src/app/tests/init_optimizer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ fn fit_export_spline_record_includes_expected_payload() {
228228
assert_eq!(value["optimizer"]["loss_metric"]["id"], "mae");
229229
assert_eq!(
230230
value["optimizer"]["loss_metric"]["name"],
231-
"Mean Absolute Error"
231+
"Mean Absolute Error (L1)"
232232
);
233233
assert!(
234234
value["optimizer"]

src/app/ui/diagnostics_panel.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,12 @@ fn ui_loss_diagnostics(
141141
);
142142
if app.fit_loss_metric != OptimizationLossMetric::Mse {
143143
plot_ui.line(
144-
Line::new("MSE", PlotPoints::from_iter(mse_points.iter().copied()))
145-
.id(egui::Id::new(DIAGNOSTICS_SERIES_ID_MSE))
146-
.width(1.5_f32),
144+
Line::new(
145+
"MSE (L2)",
146+
PlotPoints::from_iter(mse_points.iter().copied()),
147+
)
148+
.id(egui::Id::new(DIAGNOSTICS_SERIES_ID_MSE))
149+
.width(1.5_f32),
147150
);
148151
}
149152
plot_ui.line(
@@ -153,9 +156,12 @@ fn ui_loss_diagnostics(
153156
);
154157
if app.fit_loss_metric != OptimizationLossMetric::Mae {
155158
plot_ui.line(
156-
Line::new("MAE", PlotPoints::from_iter(mae_points.iter().copied()))
157-
.id(egui::Id::new(DIAGNOSTICS_SERIES_ID_MAE))
158-
.width(1.5_f32),
159+
Line::new(
160+
"MAE (L1)",
161+
PlotPoints::from_iter(mae_points.iter().copied()),
162+
)
163+
.id(egui::Id::new(DIAGNOSTICS_SERIES_ID_MAE))
164+
.width(1.5_f32),
159165
);
160166
}
161167
if app.fit_loss_metric != OptimizationLossMetric::SoftL1 {

src/app/ui/status_panel.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ pub(super) fn ui_optimization_metric(app: &mut CurveFitApp, ui: &mut egui::Ui) {
1313
metric_response,
1414
tr(
1515
language,
16-
"Optimization metric\n- This metric is minimized during fitting\n- Diagnostics shows it as loss(metric)\n- MSE: smooth gradients, MAE: more robust to outliers, soft_l1: compromise",
17-
"Метрика оптимизации\n- Эта метрика минимизируется во время фитинга\n- В диагностике она отображается как loss(metric)\n- MSE: более гладкие градиенты, MAE: устойчивее к выбросам, soft_l1: компромисс",
16+
"Optimization metric\n- This metric is minimized during fitting\n- Diagnostics shows it as loss(metric)\n- MSE (L2): smooth gradients\n- MAE (L1): more robust to outliers\n- soft_l1: compromise\n- Chebyshev: minimizes max absolute residual\n- MSLE: emphasizes relative/log-scaled deviations",
17+
"Метрика оптимизации\n- Эта метрика минимизируется во время фитинга\n- В диагностике она отображается как loss(metric)\n- MSE (L2): более гладкие градиенты\n- MAE (L1): устойчивее к выбросам\n- soft_l1: компромисс\n- Chebyshev: минимизирует максимальный модуль остатка\n- MSLE: акцент на относительных/логарифмических отклонениях",
1818
),
1919
);
2020
});

src/fit/metrics.rs

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,49 @@ pub enum OptimizationLossMetric {
77
Mse,
88
Mae,
99
SoftL1,
10+
Chebyshev,
11+
Msle,
1012
}
1113

1214
impl OptimizationLossMetric {
1315
/// Полный список вариантов для UI и переборов.
14-
pub const ALL: [Self; 3] = [Self::Mse, Self::Mae, Self::SoftL1];
16+
pub const ALL: [Self; 5] = [
17+
Self::Mse,
18+
Self::Mae,
19+
Self::SoftL1,
20+
Self::Chebyshev,
21+
Self::Msle,
22+
];
1523

1624
/// Короткое имя метрики для подписи в легенде.
1725
pub fn id(self) -> &'static str {
1826
match self {
1927
Self::Mse => "mse",
2028
Self::Mae => "mae",
2129
Self::SoftL1 => "soft_l1",
30+
Self::Chebyshev => "chebyshev",
31+
Self::Msle => "msle",
32+
}
33+
}
34+
35+
#[inline]
36+
pub(super) fn simd_fast_path_supported(self) -> bool {
37+
matches!(self, Self::Mse | Self::Mae | Self::SoftL1)
38+
}
39+
40+
#[inline]
41+
pub(super) fn requires_numerical_hessian(self) -> bool {
42+
matches!(self, Self::Mae | Self::Chebyshev)
43+
}
44+
45+
#[inline]
46+
fn signum_or_zero(value: f64) -> f64 {
47+
if value > 0.0 {
48+
1.0
49+
} else if value < 0.0 {
50+
-1.0
51+
} else {
52+
0.0
2253
}
2354
}
2455

@@ -27,32 +58,52 @@ impl OptimizationLossMetric {
2758
Self::Mse => residual * residual,
2859
Self::Mae => residual.abs(),
2960
Self::SoftL1 => 2.0 * ((1.0 + residual * residual).sqrt() - 1.0),
61+
Self::Chebyshev => residual.abs(),
62+
Self::Msle => {
63+
let log_term = (1.0 + residual.abs()).ln();
64+
log_term * log_term
65+
}
3066
}
3167
}
3268

3369
pub(super) fn residual_derivative(self, residual: f64) -> f64 {
3470
match self {
3571
Self::Mse => 2.0 * residual,
36-
Self::Mae => {
37-
if residual > 0.0 {
38-
1.0
39-
} else if residual < 0.0 {
40-
-1.0
41-
} else {
42-
0.0
43-
}
44-
}
72+
Self::Mae | Self::Chebyshev => Self::signum_or_zero(residual),
4573
Self::SoftL1 => 2.0 * residual / (1.0 + residual * residual).sqrt(),
74+
Self::Msle => {
75+
let abs_residual = residual.abs();
76+
let log_term = (1.0 + abs_residual).ln();
77+
Self::signum_or_zero(residual) * (2.0 * log_term / (1.0 + abs_residual))
78+
}
4679
}
4780
}
4881

4982
pub(super) fn residual_second_derivative(self, residual: f64) -> f64 {
5083
match self {
5184
Self::Mse => 2.0,
52-
Self::Mae => 0.0,
85+
Self::Mae | Self::Chebyshev => 0.0,
5386
Self::SoftL1 => 2.0 / (1.0 + residual * residual).powf(1.5),
87+
Self::Msle => {
88+
let abs_residual = residual.abs();
89+
let one_plus_abs = 1.0 + abs_residual;
90+
let log_term = one_plus_abs.ln();
91+
2.0 * (1.0 - log_term) / (one_plus_abs * one_plus_abs)
92+
}
5493
}
5594
}
95+
96+
pub(super) fn value_from_prediction(self, prediction: f64, target: f64) -> f64 {
97+
self.value_from_residual(prediction - target)
98+
}
99+
100+
pub(super) fn prediction_derivative(self, prediction: f64, target: f64) -> f64 {
101+
self.residual_derivative(prediction - target)
102+
}
103+
104+
pub(super) fn prediction_second_derivative(self, prediction: f64, target: f64) -> f64 {
105+
self.residual_second_derivative(prediction - target)
106+
}
56107
}
57108

58109
/// Значение по умолчанию для числа знаков после запятой в режиме квантизации метрик.
@@ -125,11 +176,6 @@ impl ResidualQuantizer {
125176
Self::Enabled { scale } => (value * scale).round() / scale,
126177
}
127178
}
128-
129-
#[inline]
130-
pub(super) fn residual(self, predicted: f64, observed: f64) -> f64 {
131-
self.quantize_value(predicted) - self.quantize_value(observed)
132-
}
133179
}
134180

135181
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -211,6 +257,7 @@ pub(super) struct ScalarMetrics {
211257
pub(super) rmse: f64,
212258
pub(super) mae: f64,
213259
pub(super) soft_l1: f64,
260+
pub(super) msle: f64,
214261
pub(super) r2: f64,
215262
pub(super) max_abs_error: f64,
216263
}
@@ -223,6 +270,8 @@ pub(super) fn scalar_loss_value(
223270
OptimizationLossMetric::Mse => metrics.mse,
224271
OptimizationLossMetric::Mae => metrics.mae,
225272
OptimizationLossMetric::SoftL1 => metrics.soft_l1,
273+
OptimizationLossMetric::Chebyshev => metrics.max_abs_error,
274+
OptimizationLossMetric::Msle => metrics.msle,
226275
}
227276
}
228277

@@ -246,13 +295,17 @@ where
246295
let mut sse = 0.0;
247296
let mut sae = 0.0;
248297
let mut soft_l1_sum = 0.0;
298+
let mut msle_sum = 0.0;
249299
let mut max_abs_error = 0.0_f64;
250300
for point in points.as_slice() {
251-
let residual = quantizer.residual(evaluate(point.x()), point.y());
301+
let predicted = quantizer.quantize_value(evaluate(point.x()));
302+
let observed = quantizer.quantize_value(point.y());
303+
let residual = predicted - observed;
252304
let abs_residual = residual.abs();
253305
sse += residual * residual;
254306
sae += abs_residual;
255307
soft_l1_sum += OptimizationLossMetric::SoftL1.value_from_residual(residual);
308+
msle_sum += OptimizationLossMetric::Msle.value_from_prediction(predicted, observed);
256309
max_abs_error = max_abs_error.max(abs_residual);
257310
}
258311

@@ -268,6 +321,7 @@ where
268321
let rmse = mse.sqrt();
269322
let mae = sae / sample_count;
270323
let soft_l1 = soft_l1_sum / sample_count;
324+
let msle = msle_sum / sample_count;
271325
let r2 = if sst <= 1e-15 {
272326
if sse <= 1e-15 { 1.0 } else { 0.0 }
273327
} else {
@@ -279,6 +333,7 @@ where
279333
rmse,
280334
mae,
281335
soft_l1,
336+
msle,
282337
r2,
283338
max_abs_error,
284339
}
@@ -289,6 +344,7 @@ pub(super) struct EvaluatorMetrics {
289344
pub(super) rmse: f64,
290345
pub(super) mae: f64,
291346
pub(super) soft_l1: f64,
347+
pub(super) msle: f64,
292348
pub(super) r2: f64,
293349
pub(super) max_abs_error: f64,
294350
pub(super) residuals: Vec<[f64; 2]>,
@@ -316,6 +372,7 @@ where
316372
rmse: scalar.rmse,
317373
mae: scalar.mae,
318374
soft_l1: scalar.soft_l1,
375+
msle: scalar.msle,
319376
r2: scalar.r2,
320377
max_abs_error: scalar.max_abs_error,
321378
residuals,

0 commit comments

Comments
 (0)