use crate::error::Result;
use crate::losses::Loss;
use scirs2_core::ndarray::{Array, IxDyn};
use scirs2_core::numeric::{Float, NumAssign};
use std::fmt::Debug;
#[derive(Debug, Clone, Copy)]
pub struct HuberLoss<F: Float + NumAssign> {
delta: F,
}
impl<F: Float + NumAssign> HuberLoss<F> {
pub fn new(delta: F) -> Self {
Self { delta }
}
pub fn default_delta() -> Self {
Self {
delta: F::from(1.0).unwrap_or(F::one()),
}
}
}
impl<F: Float + NumAssign> Default for HuberLoss<F> {
fn default() -> Self {
Self::default_delta()
}
}
impl<F: Float + Debug + NumAssign> Loss<F> for HuberLoss<F> {
fn forward(&self, predictions: &Array<F, IxDyn>, targets: &Array<F, IxDyn>) -> Result<F> {
let n = F::from(predictions.len()).unwrap_or(F::one());
let half = F::from(0.5).unwrap_or(F::one() / (F::one() + F::one()));
let loss_sum = predictions
.iter()
.zip(targets.iter())
.map(|(&p, &t)| {
let diff = p - t;
let abs_diff = diff.abs();
if abs_diff <= self.delta {
half * diff * diff
} else {
self.delta * (abs_diff - half * self.delta)
}
})
.fold(F::zero(), |acc, x| acc + x);
Ok(loss_sum / n)
}
fn backward(
&self,
predictions: &Array<F, IxDyn>,
targets: &Array<F, IxDyn>,
) -> Result<Array<F, IxDyn>> {
let n = F::from(predictions.len()).unwrap_or(F::one());
let gradients: Vec<F> = predictions
.iter()
.zip(targets.iter())
.map(|(&p, &t)| {
let diff = p - t;
let abs_diff = diff.abs();
if abs_diff <= self.delta {
diff / n
} else {
if diff > F::zero() {
self.delta / n
} else {
-self.delta / n
}
}
})
.collect();
Array::from_shape_vec(predictions.dim(), gradients).map_err(|e| {
crate::error::NeuralError::InferenceError(format!(
"Failed to create gradient array: {}",
e
))
})
}
}
pub type SmoothL1Loss<F> = HuberLoss<F>;
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array;
#[test]
fn test_huber_quadratic_region() {
let huber = HuberLoss::new(1.0);
let predictions = Array::from_vec(vec![1.0, 2.0, 3.0]).into_dyn();
let targets = Array::from_vec(vec![1.2, 1.8, 3.1]).into_dyn();
let loss = huber
.forward(&predictions, &targets)
.expect("Operation failed");
let expected = (0.5 * 0.04 + 0.5 * 0.04 + 0.5 * 0.01) / 3.0;
assert!((loss - expected).abs() < 1e-6);
}
#[test]
fn test_huber_linear_region() {
let huber = HuberLoss::new(1.0);
let predictions = Array::from_vec(vec![0.0, 5.0]).into_dyn();
let targets = Array::from_vec(vec![3.0, 0.0]).into_dyn();
let loss = huber
.forward(&predictions, &targets)
.expect("Operation failed");
let expected = (2.5 + 4.5) / 2.0;
assert!((loss - expected).abs() < 1e-6);
}
#[test]
fn test_huber_mixed_regions() {
let huber = HuberLoss::new(1.0);
let predictions = Array::from_vec(vec![0.0, 0.5]).into_dyn();
let targets = Array::from_vec(vec![3.0, 0.3]).into_dyn();
let loss = huber
.forward(&predictions, &targets)
.expect("Operation failed");
let expected = (2.5 + 0.02) / 2.0;
assert!((loss - expected).abs() < 1e-6);
}
#[test]
fn test_huber_perfect_predictions() {
let huber = HuberLoss::new(1.0);
let predictions = Array::from_vec(vec![1.0, 2.0, 3.0]).into_dyn();
let targets = Array::from_vec(vec![1.0, 2.0, 3.0]).into_dyn();
let loss = huber
.forward(&predictions, &targets)
.expect("Operation failed");
assert!(loss.abs() < 1e-10);
}
#[test]
fn test_huber_backward_quadratic() {
let huber = HuberLoss::new(1.0);
let predictions = Array::from_vec(vec![1.0, 2.0]).into_dyn();
let targets = Array::from_vec(vec![1.5, 1.5]).into_dyn();
let gradients = huber
.backward(&predictions, &targets)
.expect("Operation failed");
assert!((gradients[[0]] - (-0.25)).abs() < 1e-6);
assert!((gradients[[1]] - 0.25).abs() < 1e-6);
}
#[test]
fn test_huber_backward_linear() {
let huber = HuberLoss::new(1.0);
let predictions = Array::from_vec(vec![0.0, 5.0]).into_dyn();
let targets = Array::from_vec(vec![3.0, 0.0]).into_dyn();
let gradients = huber
.backward(&predictions, &targets)
.expect("Operation failed");
assert!((gradients[[0]] - (-0.5)).abs() < 1e-6);
assert!((gradients[[1]] - 0.5).abs() < 1e-6);
}
#[test]
fn test_huber_custom_delta() {
let huber = HuberLoss::new(0.5);
let predictions = Array::from_vec(vec![0.0]).into_dyn();
let targets = Array::from_vec(vec![1.0]).into_dyn();
let loss = huber
.forward(&predictions, &targets)
.expect("Operation failed");
assert!((loss - 0.375).abs() < 1e-6);
}
#[test]
fn test_huber_2d() {
let huber = HuberLoss::new(1.0);
let predictions = Array::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0])
.expect("Operation failed")
.into_dyn();
let targets = Array::from_shape_vec((2, 2), vec![1.0, 2.5, 6.0, 4.0])
.expect("Operation failed")
.into_dyn();
let loss = huber
.forward(&predictions, &targets)
.expect("Operation failed");
let expected = (0.0 + 0.125 + 2.5 + 0.0) / 4.0;
assert!((loss - expected).abs() < 1e-6);
}
#[test]
fn test_smooth_l1_alias() {
let smooth_l1: SmoothL1Loss<f64> = SmoothL1Loss::new(1.0);
let predictions = Array::from_vec(vec![1.0, 2.0]).into_dyn();
let targets = Array::from_vec(vec![1.5, 5.0]).into_dyn();
let loss = smooth_l1
.forward(&predictions, &targets)
.expect("Operation failed");
assert!(loss > 0.0);
}
}