use crate::error::Error;
use crate::neural_network::Tensor;
use crate::neural_network::losses::{
clip_probabilities, stable_log_softmax_softmax, validate_same_shape,
};
use crate::neural_network::traits::Loss;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CategoricalCrossEntropy {
from_logits: bool,
}
impl CategoricalCrossEntropy {
pub fn new(from_logits: bool) -> Self {
Self { from_logits }
}
}
fn batch_and_classes(t: &Tensor) -> (usize, usize) {
let batch = t.shape()[0];
let classes: usize = t.shape()[1..].iter().product();
(batch, classes)
}
fn validate_shapes(y_true: &Tensor, y_pred: &Tensor) -> Result<(), Error> {
if y_true.is_empty() {
return Err(Error::empty_input(
"CategoricalCrossEntropy expects non-empty y_true",
));
}
if y_true.ndim() < 2 {
return Err(Error::invalid_input(format!(
"CategoricalCrossEntropy expects at least 2D tensors [batch, classes], got {}D",
y_true.ndim()
)));
}
validate_same_shape(y_true, y_pred)
}
impl Loss for CategoricalCrossEntropy {
fn compute_loss(&self, y_true: &Tensor, y_pred: &Tensor) -> Result<f32, Error> {
validate_shapes(y_true, y_pred)?;
let n = y_true.shape()[0] as f32;
if self.from_logits {
let (b, c) = batch_and_classes(y_pred);
let logits = y_pred
.to_shape((b, c))
.map_err(|e| Error::computation(format!("CCE logits reshape failed: {e}")))?;
let labels = y_true
.to_shape((b, c))
.map_err(|e| Error::computation(format!("CCE labels reshape failed: {e}")))?;
let (log_sm, _) = stable_log_softmax_softmax(&logits.view());
return Ok(-(&labels * &log_sm).sum() / n);
}
let y_pred_clipped = clip_probabilities(y_pred);
let losses = y_true * &y_pred_clipped.mapv(|y_p| y_p.ln());
Ok(-losses.sum() / n)
}
fn compute_grad(&self, y_true: &Tensor, y_pred: &Tensor) -> Result<Tensor, Error> {
validate_shapes(y_true, y_pred)?;
let n = y_true.shape()[0] as f32;
if self.from_logits {
let (b, c) = batch_and_classes(y_pred);
let logits = y_pred
.to_shape((b, c))
.map_err(|e| Error::computation(format!("CCE logits reshape failed: {e}")))?;
let labels = y_true
.to_shape((b, c))
.map_err(|e| Error::computation(format!("CCE labels reshape failed: {e}")))?;
let (_, sm) = stable_log_softmax_softmax(&logits.view());
let grad2d = (&sm - &labels) / n;
let grad = grad2d
.into_shape_with_order(y_pred.raw_dim())
.map_err(|e| Error::computation(format!("CCE gradient reshape failed: {e}")))?;
return Ok(grad);
}
let y_pred_clipped = clip_probabilities(y_pred);
let grad = -y_true / &y_pred_clipped;
Ok(grad / n)
}
}