use crate::error::Error;
use crate::neural_network::Tensor;
use crate::neural_network::losses::{clip_probabilities, stable_log_softmax_softmax};
use crate::neural_network::traits::Loss;
use ndarray::Ix2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SparseCategoricalCrossEntropy {
from_logits: bool,
}
impl SparseCategoricalCrossEntropy {
pub fn new(from_logits: bool) -> Self {
Self { from_logits }
}
}
fn validate_and_extract_labels(y_true: &Tensor, y_pred: &Tensor) -> Result<Vec<usize>, Error> {
if y_pred.ndim() != 2 {
return Err(Error::invalid_input(format!(
"SparseCategoricalCrossEntropy expects 2D predictions [batch, num_classes], got shape {:?}",
y_pred.shape()
)));
}
if y_true.ndim() != 2 || y_true.shape()[1] != 1 {
return Err(Error::invalid_input(format!(
"SparseCategoricalCrossEntropy expects integer labels of shape [batch, 1], got shape {:?}",
y_true.shape()
)));
}
if y_true.shape()[0] != y_pred.shape()[0] {
return Err(Error::dimension_mismatch(
y_true.shape()[0],
y_pred.shape()[0],
));
}
let batch_size = y_true.shape()[0];
let num_classes = y_pred.shape()[1];
(0..batch_size)
.map(|i| {
let label = y_true[[i, 0]];
if !label.is_finite() || label < 0.0 {
return Err(Error::invalid_input(format!(
"SparseCategoricalCrossEntropy label at sample {i} must be a non-negative integer, got {label}"
)));
}
let class_idx = label.round() as usize;
if class_idx >= num_classes {
return Err(Error::invalid_input(format!(
"SparseCategoricalCrossEntropy label {class_idx} at sample {i} is out of range for {num_classes} classes"
)));
}
Ok(class_idx)
})
.collect()
}
impl Loss for SparseCategoricalCrossEntropy {
fn compute_loss(&self, y_true: &Tensor, y_pred: &Tensor) -> Result<f32, Error> {
let class_indices = validate_and_extract_labels(y_true, y_pred)?;
let batch_size = class_indices.len();
if self.from_logits {
let logits = y_pred.view().into_dimensionality::<Ix2>().map_err(|_| {
Error::invalid_input("SparseCategoricalCrossEntropy expects 2D logits")
})?;
let (log_sm, _) = stable_log_softmax_softmax(&logits);
let total: f32 = class_indices
.iter()
.enumerate()
.map(|(i, &class_idx)| -log_sm[[i, class_idx]])
.sum();
return Ok(total / batch_size as f32);
}
let y_pred_clipped = clip_probabilities(y_pred);
let total_loss: f32 = class_indices
.iter()
.enumerate()
.map(|(i, &class_idx)| -y_pred_clipped[[i, class_idx]].ln())
.sum();
Ok(total_loss / batch_size as f32)
}
fn compute_grad(&self, y_true: &Tensor, y_pred: &Tensor) -> Result<Tensor, Error> {
let class_indices = validate_and_extract_labels(y_true, y_pred)?;
let batch_size = class_indices.len();
if self.from_logits {
let logits = y_pred.view().into_dimensionality::<Ix2>().map_err(|_| {
Error::invalid_input("SparseCategoricalCrossEntropy expects 2D logits")
})?;
let (_, mut grad) = stable_log_softmax_softmax(&logits);
for (i, &class_idx) in class_indices.iter().enumerate() {
grad[[i, class_idx]] -= 1.0;
}
grad /= batch_size as f32;
return Ok(grad.into_dyn());
}
let y_pred_clipped = clip_probabilities(y_pred);
let mut grad = Tensor::zeros(y_pred.raw_dim());
for (i, &class_idx) in class_indices.iter().enumerate() {
grad[[i, class_idx]] = -1.0 / y_pred_clipped[[i, class_idx]];
}
Ok(grad / batch_size as f32)
}
}