use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::model::domain::Domain;
use super::inference::{argmax, entropy_bits};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Objective {
Entropy,
SquaredError,
AbsoluteError,
Decision {
loss: Vec<Vec<f64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
labels: Option<Vec<String>>,
},
}
impl Objective {
pub fn label(&self) -> &'static str {
match self {
Objective::Entropy => "entropy",
Objective::SquaredError => "squared_error",
Objective::AbsoluteError => "absolute_error",
Objective::Decision { .. } => "decision",
}
}
pub fn units(&self) -> &'static str {
match self {
Objective::Entropy => "bits",
Objective::SquaredError => "variance",
Objective::AbsoluteError => "abs. error",
Objective::Decision { .. } => "loss",
}
}
pub fn validate(&self, domain: &Domain) -> Result<()> {
match self {
Objective::Entropy => Ok(()),
Objective::SquaredError | Objective::AbsoluteError => match domain {
Domain::Continuous { .. } => Ok(()),
Domain::Categorical { .. } => Err(Error::Invalid(format!(
"{} objective needs a continuous slot",
self.label()
))),
},
Objective::Decision { loss, labels } => {
if loss.is_empty() {
return Err(Error::Invalid(
"decision objective needs at least one decision".into(),
));
}
let n = domain.n();
for (d, row) in loss.iter().enumerate() {
if row.len() != n {
return Err(Error::Invalid(format!(
"decision {d}: loss has {} entries, slot has {n} cells",
row.len()
)));
}
if row.iter().any(|x| !x.is_finite()) {
return Err(Error::Invalid(format!(
"decision {d}: loss entries must be finite"
)));
}
}
if let Some(l) = labels {
if l.len() != loss.len() {
return Err(Error::Invalid(format!(
"decision objective: {} labels for {} decisions",
l.len(),
loss.len()
)));
}
}
Ok(())
}
}
}
pub fn risk(&self, domain: &Domain, post: &[f64]) -> f64 {
match self {
Objective::Entropy => entropy_bits(post),
Objective::SquaredError => mean_variance(domain, post).1,
Objective::AbsoluteError => {
let center = domain.midpoint(median_cell(post));
post.iter()
.enumerate()
.map(|(i, p)| p * (domain.midpoint(i) - center).abs())
.sum()
}
Objective::Decision { loss, .. } => decision_risk(loss, post).0,
}
}
pub fn recommendation(&self, domain: &Domain, post: &[f64]) -> String {
match self {
Objective::Entropy => {
format!("report posterior (MAP {})", value_str(domain, argmax(post)))
}
Objective::SquaredError => format!("estimate {:.0}", mean_variance(domain, post).0),
Objective::AbsoluteError => {
format!("estimate {:.0}", domain.midpoint(median_cell(post)))
}
Objective::Decision { loss, labels } => {
let d = decision_risk(loss, post).1;
match labels {
Some(l) => l[d].clone(),
None => format!("decision #{d}"),
}
}
}
}
}
fn mean_variance(domain: &Domain, post: &[f64]) -> (f64, f64) {
let mut mean = 0.0;
let mut m2 = 0.0;
for (i, p) in post.iter().enumerate() {
let x = domain.midpoint(i);
mean += p * x;
m2 += p * x * x;
}
(mean, (m2 - mean * mean).max(0.0))
}
fn median_cell(post: &[f64]) -> usize {
let mut acc = 0.0;
for (i, p) in post.iter().enumerate() {
acc += p;
if acc >= 0.5 {
return i;
}
}
post.len() - 1
}
fn decision_risk(loss: &[Vec<f64>], post: &[f64]) -> (f64, usize) {
let mut best = (f64::INFINITY, 0);
for (d, row) in loss.iter().enumerate() {
let r: f64 = row.iter().zip(post).map(|(l, p)| l * p).sum();
if r < best.0 {
best = (r, d);
}
}
best
}
fn value_str(domain: &Domain, i: usize) -> String {
match domain {
Domain::Continuous { .. } => format!("{:.0}", domain.midpoint(i)),
Domain::Categorical { values } => values[i].clone(),
}
}