use anyhow::Result;
use ndarray::{s, Array1, Array2};
pub trait LossFunction {
fn compute(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<f32>;
fn gradient(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<Array2<f32>>;
fn name(&self) -> &'static str;
}
pub struct CTCLoss {
blank_token: usize,
reduction: ReductionType,
}
#[derive(Debug, Clone)]
pub enum ReductionType {
Mean,
Sum,
None,
}
impl CTCLoss {
pub fn new(blank_token: usize) -> Self {
Self {
blank_token,
reduction: ReductionType::Mean,
}
}
pub fn with_reduction(mut self, reduction: ReductionType) -> Self {
self.reduction = reduction;
self
}
fn compute_ctc_loss(
&self,
log_probs: &Array2<f32>,
targets: &Array1<usize>,
input_lengths: &Array1<usize>,
target_lengths: &Array1<usize>,
) -> Result<f32> {
let batch_size = log_probs.shape()[0];
let mut total_loss = 0.0;
for i in 0..batch_size {
let input_len = input_lengths[i];
let target_len = target_lengths[i];
let sequence_log_probs = log_probs
.slice_axis(ndarray::Axis(0), ndarray::Slice::from(i..i + 1))
.slice_axis(ndarray::Axis(1), ndarray::Slice::from(0..input_len))
.slice_axis(ndarray::Axis(2), ndarray::Slice::from(..))
.to_owned();
let target_sequence = targets
.slice_axis(ndarray::Axis(0), ndarray::Slice::from(i..i + 1))
.slice_axis(ndarray::Axis(1), ndarray::Slice::from(0..target_len))
.to_owned();
let loss = self.forward_algorithm(&sequence_log_probs, &target_sequence)?;
total_loss += loss;
}
match self.reduction {
ReductionType::Mean => Ok(total_loss / batch_size as f32),
ReductionType::Sum => Ok(total_loss),
ReductionType::None => Ok(total_loss),
}
}
fn forward_algorithm(&self, log_probs: &Array2<f32>, target: &Array1<usize>) -> Result<f32> {
let t = log_probs.shape()[0];
let s = target.len() * 2 + 1;
let mut extended_target = vec![self.blank_token; s];
for (i, &token) in target.iter().enumerate() {
extended_target[i * 2 + 1] = token;
}
let mut alpha = Array2::<f32>::zeros((t, s));
alpha[[0, 0]] = log_probs[[0, self.blank_token]];
if s > 1 {
alpha[[0, 1]] = log_probs[[0, extended_target[1]]];
}
for t_idx in 1..t {
for s_idx in 0..s {
let current_token = extended_target[s_idx];
let mut sum = alpha[[t_idx - 1, s_idx]] + log_probs[[t_idx, current_token]];
if s_idx > 0 {
sum =
sum.max(alpha[[t_idx - 1, s_idx - 1]] + log_probs[[t_idx, current_token]]);
}
if s_idx > 1
&& extended_target[s_idx] != self.blank_token
&& extended_target[s_idx] != extended_target[s_idx - 2]
{
sum =
sum.max(alpha[[t_idx - 1, s_idx - 2]] + log_probs[[t_idx, current_token]]);
}
alpha[[t_idx, s_idx]] = sum;
}
}
let final_loss = if s > 0 {
alpha[[t - 1, s - 1]].max(alpha[[t - 1, s - 2]])
} else {
alpha[[t - 1, 0]]
};
Ok(-final_loss)
}
}
impl LossFunction for CTCLoss {
fn compute(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<f32> {
let batch_size = predictions.shape()[0];
let mut target_indices = Vec::new();
let mut target_lengths = Vec::new();
let mut input_lengths = Vec::new();
for i in 0..batch_size {
let target_row = targets.slice(s![i, ..]);
let non_zero_indices: Vec<usize> = target_row
.iter()
.enumerate()
.filter(|&(_, &val)| val != 0.0)
.map(|(idx, _)| idx)
.collect();
target_indices.extend(non_zero_indices.clone());
target_lengths.push(non_zero_indices.len());
input_lengths.push(predictions.shape()[1]);
}
let target_array = Array1::from(target_indices);
let target_lengths_array = Array1::from(target_lengths);
let input_lengths_array = Array1::from(input_lengths);
self.compute_ctc_loss(
predictions,
&target_array,
&input_lengths_array,
&target_lengths_array,
)
}
fn gradient(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<Array2<f32>> {
let mut gradients = predictions.clone();
gradients -= targets;
Ok(gradients)
}
fn name(&self) -> &'static str {
"CTC Loss"
}
}
pub struct CrossEntropyLoss {
reduction: ReductionType,
label_smoothing: f32,
}
impl CrossEntropyLoss {
pub fn new() -> Self {
Self {
reduction: ReductionType::Mean,
label_smoothing: 0.0,
}
}
pub fn with_label_smoothing(mut self, smoothing: f32) -> Self {
self.label_smoothing = smoothing;
self
}
pub fn with_reduction(mut self, reduction: ReductionType) -> Self {
self.reduction = reduction;
self
}
}
impl LossFunction for CrossEntropyLoss {
fn compute(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<f32> {
let batch_size = predictions.shape()[0];
let num_classes = predictions.shape()[1];
let mut total_loss = 0.0;
for i in 0..batch_size {
let pred_row = predictions.slice(s![i, ..]);
let target_row = targets.slice(s![i, ..]);
let max_val = pred_row.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let exp_pred: Array1<f32> = pred_row.mapv(|x| (x - max_val).exp());
let sum_exp: f32 = exp_pred.sum();
let softmax_pred = exp_pred.mapv(|x| x / sum_exp);
let mut loss = 0.0;
for j in 0..num_classes {
if target_row[j] > 0.0 {
loss += -target_row[j] * softmax_pred[j].ln();
}
}
total_loss += loss;
}
match self.reduction {
ReductionType::Mean => Ok(total_loss / batch_size as f32),
ReductionType::Sum => Ok(total_loss),
ReductionType::None => Ok(total_loss),
}
}
fn gradient(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<Array2<f32>> {
let batch_size = predictions.shape()[0];
let num_classes = predictions.shape()[1];
let mut gradients = Array2::<f32>::zeros((batch_size, num_classes));
for i in 0..batch_size {
let pred_row = predictions.slice(s![i, ..]);
let target_row = targets.slice(s![i, ..]);
let max_val = pred_row.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let exp_pred: Array1<f32> = pred_row.mapv(|x| (x - max_val).exp());
let sum_exp: f32 = exp_pred.sum();
let softmax_pred = exp_pred.mapv(|x| x / sum_exp);
for j in 0..num_classes {
gradients[[i, j]] = softmax_pred[j] - target_row[j];
}
}
Ok(gradients)
}
fn name(&self) -> &'static str {
"Cross-Entropy Loss"
}
}
pub struct FocalLoss {
alpha: f32,
gamma: f32,
reduction: ReductionType,
}
impl FocalLoss {
pub fn new(alpha: f32, gamma: f32) -> Self {
Self {
alpha,
gamma,
reduction: ReductionType::Mean,
}
}
pub fn with_reduction(mut self, reduction: ReductionType) -> Self {
self.reduction = reduction;
self
}
}
impl LossFunction for FocalLoss {
fn compute(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<f32> {
let batch_size = predictions.shape()[0];
let num_classes = predictions.shape()[1];
let mut total_loss = 0.0;
for i in 0..batch_size {
let pred_row = predictions.slice(s![i, ..]);
let target_row = targets.slice(s![i, ..]);
let max_val = pred_row.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let exp_pred: Array1<f32> = pred_row.mapv(|x| (x - max_val).exp());
let sum_exp: f32 = exp_pred.sum();
let softmax_pred = exp_pred.mapv(|x| x / sum_exp);
let mut loss = 0.0;
for j in 0..num_classes {
if target_row[j] > 0.0 {
let pt = softmax_pred[j];
let focal_weight = self.alpha * (1.0 - pt).powf(self.gamma);
loss += -target_row[j] * focal_weight * pt.ln();
}
}
total_loss += loss;
}
match self.reduction {
ReductionType::Mean => Ok(total_loss / batch_size as f32),
ReductionType::Sum => Ok(total_loss),
ReductionType::None => Ok(total_loss),
}
}
fn gradient(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<Array2<f32>> {
let mut gradients = predictions.clone();
gradients -= targets;
Ok(gradients)
}
fn name(&self) -> &'static str {
"Focal Loss"
}
}
pub struct AttentionLoss {
reduction: ReductionType,
}
impl AttentionLoss {
pub fn new() -> Self {
Self {
reduction: ReductionType::Mean,
}
}
pub fn with_reduction(mut self, reduction: ReductionType) -> Self {
self.reduction = reduction;
self
}
}
impl LossFunction for AttentionLoss {
fn compute(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<f32> {
let ce_loss = CrossEntropyLoss::new().with_reduction(ReductionType::None);
let ce_value = ce_loss.compute(predictions, targets)?;
let attention_penalty = 0.1 * predictions.sum();
let total_loss = ce_value + attention_penalty;
match self.reduction {
ReductionType::Mean => Ok(total_loss / predictions.shape()[0] as f32),
ReductionType::Sum => Ok(total_loss),
ReductionType::None => Ok(total_loss),
}
}
fn gradient(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<Array2<f32>> {
let ce_loss = CrossEntropyLoss::new();
let mut gradients = ce_loss.gradient(predictions, targets)?;
gradients += 0.1;
Ok(gradients)
}
fn name(&self) -> &'static str {
"Attention Loss"
}
}
pub struct CombinedLoss {
losses: Vec<Box<dyn LossFunction + Send + Sync>>,
weights: Vec<f32>,
}
impl CombinedLoss {
pub fn new() -> Self {
Self {
losses: Vec::new(),
weights: Vec::new(),
}
}
pub fn add_loss(mut self, loss: Box<dyn LossFunction + Send + Sync>, weight: f32) -> Self {
self.losses.push(loss);
self.weights.push(weight);
self
}
}
impl LossFunction for CombinedLoss {
fn compute(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<f32> {
let mut total_loss = 0.0;
for (loss, &weight) in self.losses.iter().zip(self.weights.iter()) {
let loss_value = loss.compute(predictions, targets)?;
total_loss += weight * loss_value;
}
Ok(total_loss)
}
fn gradient(&self, predictions: &Array2<f32>, targets: &Array2<f32>) -> Result<Array2<f32>> {
let mut total_gradient = Array2::<f32>::zeros(predictions.raw_dim());
for (loss, &weight) in self.losses.iter().zip(self.weights.iter()) {
let gradient = loss.gradient(predictions, targets)?;
total_gradient = &total_gradient + &(weight * &gradient);
}
Ok(total_gradient)
}
fn name(&self) -> &'static str {
"Combined Loss"
}
}