use super::{
GradPair, Loss, OutputDomain, check_base_score_domain, check_label_domain, log_link,
newton_intercepts, weighted_label_mean,
};
use crate::K_RT_EPS_F32;
use crate::data::MetaInfo;
use crate::error::Result;
use crate::metric::EvalMetric;
use crate::objective::Tweedie;
fn poisson_deviance(margin: f32, label: f32) -> f64 {
let (m, y) = (f64::from(margin), f64::from(label));
let mu = m.exp();
if y > 0.0 {
y * (y.ln() - m) - (y - mu)
} else {
mu
}
}
fn log_label_mean(info: &MetaInfo) -> Vec<f32> {
let mut margin = [weighted_label_mean(info.label_values(), info.weights)];
log_link(&mut margin);
margin.to_vec()
}
macro_rules! log_link_objective {
() => {
fn pred_transform(&self, preds: &mut [f32]) {
crate::simd::exp_inplace(preds);
}
fn probs_to_margins(&self, scores: &mut [f32]) {
log_link(scores);
}
fn validate_base_score(&self, base_score: f64) -> Result<()> {
check_base_score_domain(base_score, OutputDomain::Positive)
}
};
}
#[derive(Debug, Clone, Copy)]
pub struct Poisson {
max_delta_step: f32,
}
impl Poisson {
pub fn new(max_delta_step: f32) -> Self {
Poisson { max_delta_step }
}
}
impl Default for Poisson {
fn default() -> Self {
Poisson {
max_delta_step: 0.7,
}
}
}
impl Loss for Poisson {
fn name(&self) -> &'static str {
"count:poisson"
}
fn gradient(
&self,
preds: &[f32],
labels: &[f32],
weights: Option<&[f32]>,
out: &mut [GradPair],
) {
let max_delta_step = self.max_delta_step;
super::rowwise_gradient(
labels.len(),
1,
preds,
labels,
weights,
out,
|p, l, w, o| {
crate::simd::poisson_gradient(p, l, w, max_delta_step, o);
},
);
}
log_link_objective!();
fn base_margins_info(&self, info: &MetaInfo) -> Vec<f32> {
log_label_mean(info)
}
fn pointwise_loss(&self) -> Option<super::PointwiseLoss<'_>> {
Some(Box::new(poisson_deviance))
}
fn validate_info(&self, info: &MetaInfo) -> Result<()> {
check_label_domain(info, |y| y < 0.0)
}
fn default_metric(&self) -> EvalMetric {
EvalMetric::PoissonNLogLik
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Gamma {
scale_pos_weight: f32,
}
impl Gamma {
pub(crate) fn new(scale_pos_weight: f32) -> Self {
Gamma { scale_pos_weight }
}
}
impl Default for Gamma {
fn default() -> Self {
Gamma::new(1.0)
}
}
impl Loss for Gamma {
fn name(&self) -> &'static str {
"reg:gamma"
}
fn gradient(
&self,
preds: &[f32],
labels: &[f32],
weights: Option<&[f32]>,
out: &mut [GradPair],
) {
let scale_pos_weight = self.scale_pos_weight;
super::rowwise_gradient(
labels.len(),
1,
preds,
labels,
weights,
out,
|p, l, w, o| {
crate::simd::gamma_gradient(p, l, w, scale_pos_weight, o);
},
);
}
log_link_objective!();
fn base_margins_info(&self, info: &MetaInfo) -> Vec<f32> {
if (self.scale_pos_weight - 1.0).abs() > K_RT_EPS_F32 {
return newton_intercepts(self, info);
}
log_label_mean(info)
}
fn pointwise_loss(&self) -> Option<super::PointwiseLoss<'_>> {
let scale_pos_weight = f64::from(self.scale_pos_weight);
Some(Box::new(move |margin, label| {
let (m, y) = (f64::from(margin), f64::from(label));
let weight = if label == 1.0 { scale_pos_weight } else { 1.0 };
weight * (y * (-m).exp() + m - y.ln() - 1.0)
}))
}
fn validate_info(&self, info: &MetaInfo) -> Result<()> {
check_label_domain(info, |y| y <= 0.0)
}
fn default_metric(&self) -> EvalMetric {
EvalMetric::GammaNLogLik
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct TweedieLoss {
param: Tweedie,
rho: f32,
}
impl TweedieLoss {
pub(crate) fn new(param: Tweedie) -> Self {
TweedieLoss {
param,
rho: param.variance_power() as f32,
}
}
}
impl Default for TweedieLoss {
fn default() -> Self {
TweedieLoss::new(Tweedie::default())
}
}
impl Loss for TweedieLoss {
fn name(&self) -> &'static str {
"reg:tweedie"
}
fn gradient(
&self,
preds: &[f32],
labels: &[f32],
weights: Option<&[f32]>,
out: &mut [GradPair],
) {
let rho = self.rho;
super::rowwise_gradient(
labels.len(),
1,
preds,
labels,
weights,
out,
|p, l, w, o| {
crate::simd::tweedie_gradient(p, l, w, rho, o);
},
);
}
log_link_objective!();
fn base_margins_info(&self, info: &MetaInfo) -> Vec<f32> {
log_label_mean(info)
}
fn pointwise_loss(&self) -> Option<super::PointwiseLoss<'_>> {
let rho = f64::from(self.rho);
if (rho - 1.0).abs() < 1e-9 {
return Some(Box::new(poisson_deviance));
}
let (a, b) = (1.0 - rho, 2.0 - rho);
Some(Box::new(move |margin, label| {
let (m, y) = (f64::from(margin), f64::from(label));
y.powf(b) / (a * b) - y * (a * m).exp() / a + (b * m).exp() / b
}))
}
fn validate_info(&self, info: &MetaInfo) -> Result<()> {
check_label_domain(info, |y| y < 0.0)
}
fn default_metric(&self) -> EvalMetric {
let trained = format!("{}", self.rho).parse::<f64>().map(Tweedie::new);
EvalMetric::TweedieNLogLik(match trained {
Ok(Ok(power)) => power,
_ => self.param,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::objective::{base_margins, gradient_pairs};
use approx::assert_relative_eq;
#[test]
fn tweedie_default_metric_uses_the_trained_power() {
let loss = TweedieLoss::new(Tweedie::new(1.000_000_04).unwrap());
let at_one = EvalMetric::TweedieNLogLik(Tweedie::new(1.0).unwrap());
assert_eq!(loss.default_metric(), at_one);
let (preds, labels) = ([1.0f32, 2.0], [1.0f32, 3.0]);
let score = |metric: EvalMetric| metric.build(1).unwrap().eval(&preds, &labels, None);
assert_eq!(
score(loss.default_metric()).to_bits(),
score(at_one).to_bits()
);
let default = TweedieLoss::new(Tweedie::default());
assert_eq!(default.default_metric().name(), "tweedie-nloglik@1.5");
}
#[test]
fn poisson_gradient_at_log_mean_is_zero_sum() {
let obj = Poisson::default();
let labels = [2.0f32, 5.0];
let preds = [2.0f32.ln(), 5.0f32.ln()];
let out = gradient_pairs(&obj, &preds, &labels, None);
assert_relative_eq!(out[0].grad, 0.0, epsilon = 1e-5);
assert_relative_eq!(out[1].grad, 0.0, epsilon = 1e-5);
assert!(out[0].hess > 0.0);
}
#[test]
fn gamma_gradient_zero_at_log_y() {
let obj = Gamma::default();
let labels = [3.0f32];
let preds = [3.0f32.ln()];
let out = gradient_pairs(&obj, &preds, &labels, None);
assert_relative_eq!(out[0].grad, 0.0, epsilon = 1e-5);
}
#[test]
fn gamma_scale_pos_weight_reweights_rows_labeled_one() {
let obj = Gamma::new(2.0);
let out = gradient_pairs(&obj, &[0.0, 0.0], &[1.0, 4.0], Some(&[3.0, 3.0]));
assert_eq!(out[0], GradPair::new(0.0, 6.0)); assert_eq!(out[1], GradPair::new(-9.0, 12.0)); let mut expected = [0.4f32];
obj.pred_transform(&mut expected);
obj.probs_to_margins(&mut expected);
assert_eq!(base_margins(&obj, &[1.0, 3.0], None), expected.to_vec());
}
#[test]
fn tweedie_transform_is_exp() {
let obj = TweedieLoss::default();
let mut p = [0.0f32, 1.0];
obj.pred_transform(&mut p);
assert_relative_eq!(p[0], 1.0, epsilon = 1e-6);
assert_relative_eq!(p[1], 1.0f32.exp(), epsilon = 1e-6);
}
#[test]
fn log_link_intercept_is_ln_of_mean() {
let obj = Poisson::default();
assert_eq!(base_margins(&obj, &[2.0, 6.0], None), vec![4f32.ln()]);
let w = [3.0f32, 1.0];
assert_eq!(base_margins(&obj, &[2.0, 6.0], Some(&w)), vec![3f32.ln()]);
assert_eq!(
base_margins(&Gamma::default(), &[0.0, 0.0], None),
vec![f32::NEG_INFINITY]
);
}
}