use super::{GradPair, Objective, weighted_label_mean};
#[derive(Debug, Clone, Copy, Default)]
pub struct SquaredErrorObjective;
impl Objective for SquaredErrorObjective {
fn name(&self) -> &'static str {
"reg:squarederror"
}
fn gradient(
&self,
preds: &[f32],
labels: &[f32],
weights: Option<&[f32]>,
out: &mut [GradPair],
) {
super::check_gradient_inputs(labels.len(), 1, preds, labels, weights, out);
super::rowwise_gradient(
labels.len(),
1,
preds,
labels,
weights,
out,
|preds, labels, weights, out| {
for i in 0..preds.len() {
let w = weights.map_or(1.0, |ws| ws[i]);
out[i] = GradPair::new((preds[i] - labels[i]) * w, w);
}
},
);
}
fn const_hess(&self) -> bool {
true
}
fn base_margins(
&self,
labels: &[f32],
weights: Option<&[f32]>,
_group: Option<&crate::data::GroupInfo>,
) -> Vec<f32> {
vec![weighted_label_mean(labels, weights)]
}
fn default_metric(&self) -> String {
"rmse".to_string()
}
}
#[derive(Debug, Clone, Copy)]
pub struct PseudoHuberObjective {
slope: f32,
}
impl PseudoHuberObjective {
pub fn new(slope: f32) -> Self {
PseudoHuberObjective { slope }
}
}
impl Default for PseudoHuberObjective {
fn default() -> Self {
PseudoHuberObjective { slope: 1.0 }
}
}
impl Objective for PseudoHuberObjective {
fn name(&self) -> &'static str {
"reg:pseudohubererror"
}
fn gradient(
&self,
preds: &[f32],
labels: &[f32],
weights: Option<&[f32]>,
out: &mut [GradPair],
) {
super::check_gradient_inputs(labels.len(), 1, preds, labels, weights, out);
let slope_sq = self.slope * self.slope;
super::rowwise_gradient(
labels.len(),
1,
preds,
labels,
weights,
out,
|preds, labels, weights, out| {
for i in 0..preds.len() {
let w = weights.map_or(1.0, |ws| ws[i]);
let z = preds[i] - labels[i];
let scale_sqrt = (1.0 + z * z / slope_sq).sqrt();
let scale = slope_sq + z * z;
out[i] =
GradPair::new((z / scale_sqrt) * w, (slope_sq / (scale * scale_sqrt)) * w);
}
},
);
}
fn default_metric(&self) -> String {
"mae".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gradient_matches_closed_form() {
let obj = SquaredErrorObjective;
let preds = [2.0f32, 0.0, -1.0];
let labels = [1.0f32, 0.5, -3.0];
let mut out = vec![GradPair::default(); 3];
obj.gradient(&preds, &labels, None, &mut out);
assert_eq!(out[0], GradPair::new(1.0, 1.0)); assert_eq!(out[1], GradPair::new(-0.5, 1.0)); assert_eq!(out[2], GradPair::new(2.0, 1.0)); }
#[test]
fn weighted_gradient_scales() {
let obj = SquaredErrorObjective;
let preds = [2.0f32];
let labels = [1.0f32];
let w = [4.0f32];
let mut out = vec![GradPair::default(); 1];
obj.gradient(&preds, &labels, Some(&w), &mut out);
assert_eq!(out[0], GradPair::new(4.0, 4.0));
}
#[test]
fn base_margins_is_label_mean() {
let obj = SquaredErrorObjective;
assert_eq!(obj.base_margins(&[1.0, 2.0, 3.0], None, None), vec![2.0]);
}
#[test]
fn pseudo_huber_slope_scales_gradient() {
let obj = PseudoHuberObjective::new(2.0);
let mut out = vec![GradPair::default(); 1];
obj.gradient(&[2.0], &[0.0], None, &mut out);
let root2 = 2f32.sqrt();
assert!(
(out[0].grad - 2.0 / root2).abs() < 1e-6,
"grad {}",
out[0].grad
);
assert!(
(out[0].hess - 1.0 / (2.0 * root2)).abs() < 1e-6,
"hess {}",
out[0].hess
);
PseudoHuberObjective::default().gradient(&[2.0], &[0.0], None, &mut out);
let s = 5f32;
assert_eq!(out[0], GradPair::new(2.0 / s.sqrt(), 1.0 / (s * s.sqrt())));
}
#[test]
fn pseudo_huber_intercept_is_newton_step() {
let obj = PseudoHuberObjective::default();
let labels = [0.0f32, 4.0];
let margins = obj.base_margins(&labels, None, None);
let s = 17f32; let g1 = -4.0f32 / s.sqrt();
let h1 = 1.0f32 / (s * s.sqrt());
let expected = (-f64::from(g1) / (1.0 + f64::from(h1))) as f32;
assert_eq!(margins, vec![expected]);
assert!(
margins[0] < 1.0,
"Newton step {} should undershoot the mean",
margins[0]
);
}
}