use crate::objective::ObjectiveFunction;
use crate::{metrics::evaluation::Metric, utils::fast_sum};
use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Deserialize, Serialize, Clone)]
pub struct LogLoss {}
impl ObjectiveFunction for LogLoss {
#[inline]
fn loss(&self, y: &[f64], yhat: &[f64], sample_weight: Option<&[f64]>, _group: Option<&[u64]>) -> Vec<f32> {
match sample_weight {
Some(sample_weight) => y
.iter()
.zip(yhat)
.zip(sample_weight)
.map(|((y_, yhat_), w_)| {
let p = 1.0_f64 / (1.0_f64 + (-*yhat_).exp());
(-(*y_ * p.ln() + (1.0_f64 - *y_) * ((1.0_f64 - p).ln())) * *w_) as f32
})
.collect(),
None => y
.iter()
.zip(yhat)
.map(|(y_, yhat_)| {
let p = 1.0_f64 / (1.0_f64 + (-*yhat_).exp());
(-(*y_ * p.ln() + (1.0_f64 - *y_) * ((1.0_f64 - p).ln()))) as f32
})
.collect(),
}
}
#[inline]
fn initial_value(&self, y: &[f64], sample_weight: Option<&[f64]>, _group: Option<&[u64]>) -> f64 {
match sample_weight {
Some(sample_weight) => {
let mut ytot: f64 = 0.;
let mut ntot: f64 = 0.;
for i in 0..y.len() {
ytot += sample_weight[i] * y[i];
ntot += sample_weight[i];
}
f64::ln(ytot / (ntot - ytot))
}
None => {
let ytot = fast_sum(y);
let ntot = y.len() as f64;
f64::ln(ytot / (ntot - ytot))
}
}
}
#[inline]
fn gradient(
&self,
y: &[f64],
yhat: &[f64],
sample_weight: Option<&[f64]>,
_group: Option<&[u64]>,
) -> (Vec<f32>, Option<Vec<f32>>) {
let len = y.len();
let mut g = Vec::with_capacity(len);
let mut h = Vec::with_capacity(len);
match sample_weight {
Some(w) => {
for i in 0..len {
let y_val = y[i] as f32;
let yhat_val = yhat[i] as f32;
let w_val = w[i] as f32;
let p = 1.0 / (1.0 + (-yhat_val).exp());
g.push((p - y_val) * w_val);
h.push(p * (1.0 - p) * w_val);
}
(g, Some(h))
}
None => {
for i in 0..len {
let y_val = y[i] as f32;
let yhat_val = yhat[i] as f32;
let p = 1.0 / (1.0 + (-yhat_val).exp());
g.push(p - y_val);
h.push(p * (1.0 - p));
}
(g, Some(h))
}
}
}
fn default_metric(&self) -> Metric {
Metric::LogLoss
}
fn gradient_and_loss(
&self,
y: &[f64],
yhat: &[f64],
sample_weight: Option<&[f64]>,
_group: Option<&[u64]>,
) -> (Vec<f32>, Option<Vec<f32>>, Vec<f32>) {
let len = y.len();
let mut g = Vec::with_capacity(len);
let mut h = Vec::with_capacity(len);
let mut l = Vec::with_capacity(len);
match sample_weight {
Some(w) => {
for i in 0..len {
let y_val = y[i] as f32;
let yhat_val = yhat[i] as f32;
let w_val = w[i] as f32;
let p = 1.0_f32 / (1.0 + (-yhat_val).exp());
g.push((p - y_val) * w_val);
h.push(p * (1.0 - p) * w_val);
let p64 = 1.0_f64 / (1.0 + (-yhat[i]).exp());
l.push((-(y[i] * p64.ln() + (1.0 - y[i]) * (1.0 - p64).ln()) * w[i]) as f32);
}
}
None => {
for i in 0..len {
let y_val = y[i] as f32;
let yhat_val = yhat[i] as f32;
let p = 1.0_f32 / (1.0 + (-yhat_val).exp());
g.push(p - y_val);
h.push(p * (1.0 - p));
let p64 = 1.0_f64 / (1.0 + (-yhat[i]).exp());
l.push((-(y[i] * p64.ln() + (1.0 - y[i]) * (1.0 - p64).ln())) as f32);
}
}
}
(g, Some(h), l)
}
fn gradient_and_loss_into(
&self,
y: &[f64],
yhat: &[f64],
sample_weight: Option<&[f64]>,
_group: Option<&[u64]>,
grad: &mut [f32],
hess: &mut Option<Vec<f32>>,
loss: &mut [f32],
) {
let len = y.len();
let h = hess.get_or_insert_with(|| vec![0.0; len]);
match sample_weight {
Some(w) => {
for i in 0..len {
let y_val = y[i] as f32;
let yhat_val = yhat[i] as f32;
let w_val = w[i] as f32;
let p = 1.0_f32 / (1.0 + (-yhat_val).exp());
grad[i] = (p - y_val) * w_val;
h[i] = p * (1.0 - p) * w_val;
let p64 = 1.0_f64 / (1.0 + (-yhat[i]).exp());
loss[i] = (-(y[i] * p64.ln() + (1.0 - y[i]) * (1.0 - p64).ln()) * w[i]) as f32;
}
}
None => {
for i in 0..len {
let y_val = y[i] as f32;
let yhat_val = yhat[i] as f32;
let p = 1.0_f32 / (1.0 + (-yhat_val).exp());
grad[i] = p - y_val;
h[i] = p * (1.0 - p);
let p64 = 1.0_f64 / (1.0 + (-yhat[i]).exp());
loss[i] = (-(y[i] * p64.ln() + (1.0 - y[i]) * (1.0 - p64).ln())) as f32;
}
}
}
}
fn requires_batch_evaluation(&self) -> bool {
false
}
}
impl LogLoss {
#[inline]
pub fn loss_single(&self, y: f64, yhat: f64, sample_weight: Option<f64>) -> f32 {
let p = 1.0_f64 / (1.0_f64 + (-yhat).exp());
let l = -(y * p.ln() + (1.0_f64 - y) * (1.0_f64 - p).ln());
match sample_weight {
Some(w) => (l * w) as f32,
None => l as f32,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_loss() {
let y = vec![1.0, 0.0];
let yhat = vec![1.0, -1.0]; let loss_fn = LogLoss::default();
let l = loss_fn.loss(&y, &yhat, None, None);
assert_eq!(l.len(), 2);
assert!((l[0] - 0.3132617).abs() < 1e-6);
let (g, h) = loss_fn.gradient(&y, &yhat, None, None);
let h = h.unwrap();
assert!((g[0] - (-0.2689414)).abs() < 1e-6);
assert!((h[0] - 0.1966119).abs() < 1e-6);
}
#[test]
fn test_log_loss_init() {
let y = vec![1.0, 1.0, 0.0]; let loss_fn = LogLoss::default();
assert!((loss_fn.initial_value(&y, None, None) - std::f64::consts::LN_2).abs() < 1e-6);
}
#[test]
fn test_log_loss_weighted() {
let y = vec![1.0, 0.0];
let yhat = vec![1.0, -1.0];
let w = vec![2.0, 1.0];
let loss_fn = LogLoss::default();
let l = loss_fn.loss(&y, &yhat, Some(&w), None);
assert_eq!(l.len(), 2);
assert!((l[0] - 0.3132617 * 2.0).abs() < 1e-5);
}
#[test]
fn test_log_loss_gradient_weighted() {
let y = vec![1.0, 0.0];
let yhat = vec![1.0, -1.0];
let w = vec![2.0, 1.0];
let loss_fn = LogLoss::default();
let (g, h) = loss_fn.gradient(&y, &yhat, Some(&w), None);
let h = h.unwrap();
assert_eq!(g.len(), 2);
assert_eq!(h.len(), 2);
assert!((g[0] - (-0.2689414 * 2.0)).abs() < 1e-5);
}
#[test]
fn test_log_loss_init_weighted() {
let y = vec![1.0, 1.0, 0.0];
let w = vec![1.0, 2.0, 1.0]; let loss_fn = LogLoss::default();
let init = loss_fn.initial_value(&y, Some(&w), None);
assert!((init - 3.0_f64.ln()).abs() < 1e-6);
}
#[test]
fn test_log_loss_gradient_and_loss() {
let y = vec![1.0, 0.0];
let yhat = vec![1.0, -1.0];
let loss_fn = LogLoss::default();
let (g, h, l) = loss_fn.gradient_and_loss(&y, &yhat, None, None);
let h = h.unwrap();
assert_eq!(g.len(), 2);
assert_eq!(h.len(), 2);
assert_eq!(l.len(), 2);
assert!((l[0] - 0.3132617).abs() < 1e-5);
assert!((g[0] - (-0.2689414)).abs() < 1e-5);
}
#[test]
fn test_log_loss_gradient_and_loss_weighted() {
let y = vec![1.0, 0.0];
let yhat = vec![1.0, -1.0];
let w = vec![2.0, 1.0];
let loss_fn = LogLoss::default();
let (g, h, l) = loss_fn.gradient_and_loss(&y, &yhat, Some(&w), None);
let h = h.unwrap();
assert_eq!(g.len(), 2);
assert_eq!(h.len(), 2);
assert_eq!(l.len(), 2);
}
#[test]
fn test_log_loss_gradient_and_loss_into() {
let y = vec![1.0, 0.0];
let yhat = vec![1.0, -1.0];
let loss_fn = LogLoss::default();
let mut grad = vec![0.0_f32; 2];
let mut hess: Option<Vec<f32>> = None;
let mut loss = vec![0.0_f32; 2];
loss_fn.gradient_and_loss_into(&y, &yhat, None, None, &mut grad, &mut hess, &mut loss);
assert!(hess.is_some());
assert!((grad[0] - (-0.2689414)).abs() < 1e-5);
assert!((loss[0] - 0.3132617).abs() < 1e-5);
}
#[test]
fn test_log_loss_gradient_and_loss_into_weighted() {
let y = vec![1.0, 0.0];
let yhat = vec![1.0, -1.0];
let w = vec![2.0, 1.0];
let loss_fn = LogLoss::default();
let mut grad = vec![0.0_f32; 2];
let mut hess: Option<Vec<f32>> = None;
let mut loss = vec![0.0_f32; 2];
loss_fn.gradient_and_loss_into(&y, &yhat, Some(&w), None, &mut grad, &mut hess, &mut loss);
assert!(hess.is_some());
}
#[test]
fn test_log_loss_single() {
let loss_fn = LogLoss::default();
let l1 = loss_fn.loss_single(1.0, 1.0, None);
assert!(l1 > 0.0 && l1 < 1.0);
let l2 = loss_fn.loss_single(1.0, 1.0, Some(2.0));
assert!((l2 - l1 * 2.0).abs() < 1e-5);
}
#[test]
fn test_log_loss_default_metric() {
let loss_fn = LogLoss::default();
assert!(matches!(loss_fn.default_metric(), Metric::LogLoss));
}
#[test]
fn test_log_loss_requires_batch() {
let loss_fn = LogLoss::default();
assert!(!loss_fn.requires_batch_evaluation());
}
}