Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,675 @@
//! `LogCoshLoss` Implementation for RTX Transformers
//!
//! `LogCoshLoss` (Log-Cosh Loss) provides a smooth alternative to Huber loss that's twice
//! differentiable everywhere. It combines the smoothness of MSE near zero with the
//! robustness of MAE for large errors.
//!
//! Mathematical Definition:
//! `L(y_pred`, `y_true`) = `log(cosh(y_pred` - `y_true`))
//!
//! Key Properties:
//! - Smooth everywhere (infinitely differentiable)
//! - Approximately quadratic for small errors: log(cosh(x)) ≈ x²/2
//! - Approximately linear for large errors: log(cosh(x)) ≈ |x| - log(2)
//! - Twice differentiable (unlike Huber loss)
//! - More robust to outliers than MSE, smoother than Huber
use super::{Loss, Reduction};
use crate::{Result, TransformerError};
use rtx_tensor::Tensor;
/// Characteristics of the `LogCosh` loss function at a specific point
#[derive(Debug, Clone)]
pub struct LossCharacteristics {
/// The error value at this point
pub error: f32,
/// The loss value at this point
pub loss_value: f32,
/// The first derivative (gradient) at this point
pub derivative: f32,
/// The second derivative (curvature) at this point
pub second_derivative: f32,
/// Whether this point is in the quadratic approximation region
pub is_quadratic_region: bool,
/// Whether this point is in the linear approximation region
pub is_linear_region: bool,
/// Quadratic approximation value
pub quadratic_approx: f32,
/// Linear approximation value
pub linear_approx: f32,
/// Error of quadratic approximation
pub quadratic_error: f32,
/// Error of linear approximation
pub linear_error: f32,
}
/// Statistics about the `LogCosh` loss landscape
#[derive(Debug, Clone)]
pub struct LossStatistics {
/// Number of samples analyzed
pub num_samples: usize,
/// Mean loss value across all samples
pub mean_loss: f32,
/// Mean gradient value across all samples
pub mean_gradient: f32,
/// Mean error value across all samples
pub mean_error: f32,
/// Variance of loss values
pub loss_variance: f32,
/// Fraction of samples in quadratic region
pub quadratic_region_fraction: f32,
/// Fraction of samples in linear region
pub linear_region_fraction: f32,
/// Maximum loss value
pub max_loss: f32,
/// Minimum loss value
pub min_loss: f32,
}
/// `LogCoshLoss` for smooth robust regression
///
/// This loss function provides a smooth, twice-differentiable alternative to Huber loss.
/// It's particularly useful when smooth gradients are important for optimization.
///
/// # Mathematical Properties
///
/// For small errors |x| < 1: log(cosh(x)) ≈ x²/2 (quadratic like MSE)
/// For large errors |x| > 3: log(cosh(x)) ≈ |x| - log(2) (linear like MAE)
///
/// # Usage
///
/// ```rust
/// use rtx_transformers::losses::{LogCoshLoss, Loss, Reduction};
/// use rtx_tensor::{Tensor, Device};
///
/// let loss = LogCoshLoss::new()
/// .with_reduction(Reduction::Mean);
///
/// let device = Device::cuda(0).unwrap_or(Device::default());
/// let predictions = Tensor::new(&[1.0f32, 2.0, 3.0], &device)?;
/// let targets = Tensor::new(&[1.2f32, 1.8, 3.5], &device)?;
///
/// let loss_value = loss.forward(&predictions, &targets)?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone)]
pub struct LogCoshLoss {
reduction: Reduction,
}
impl LogCoshLoss {
/// Create a new `LogCoshLoss` with default parameters
///
/// Default: Mean reduction
#[must_use]
pub fn new() -> Self {
Self {
reduction: Reduction::Mean,
}
}
/// Set the reduction mode
#[must_use]
pub fn with_reduction(mut self, reduction: Reduction) -> Self {
self.reduction = reduction;
self
}
/// Compute numerically stable log(cosh(x))
///
/// For large |x|, cosh(x) can overflow. We use the identity:
/// log(cosh(x)) = |x| + log((1 + exp(-2|x|))/2)
/// For large |x|, this approaches |x| - log(2)
fn stable_logcosh(&self, x: f32) -> f32 {
let abs_x = x.abs();
if abs_x < 12.0 {
// For moderate values, compute directly
x.cosh().ln()
} else {
// For large values, use stable approximation
// log(cosh(x)) ≈ |x| - log(2) for large |x|
abs_x - 2.0f32.ln()
}
}
/// Check if error is in the approximately quadratic region
#[must_use]
pub fn is_in_quadratic_region(&self, error: f32) -> bool {
error.abs() < 1.0
}
/// Check if error is in the approximately linear region
#[must_use]
pub fn is_in_linear_region(&self, error: f32) -> bool {
error.abs() > 3.0
}
/// Get the quadratic approximation for small errors
#[must_use]
pub fn quadratic_approximation(&self, error: f32) -> f32 {
0.5 * error * error
}
/// Get the linear approximation for large errors
#[must_use]
pub fn linear_approximation(&self, error: f32) -> f32 {
error.abs() - 2.0f32.ln()
}
/// Compute the derivative of `LogCoshLoss`
/// d/dx log(cosh(x)) = tanh(x)
#[must_use]
pub fn derivative(&self, error: f32) -> f32 {
error.tanh()
}
/// Compute the second derivative of `LogCoshLoss`
/// d²/dx² log(cosh(x)) = sech²(x) = 1 - tanh²(x)
#[must_use]
pub fn second_derivative(&self, error: f32) -> f32 {
let tanh_x = error.tanh();
1.0 - tanh_x * tanh_x
}
/// Compute loss value for a single error (useful for testing)
#[must_use]
pub fn compute_single_loss(&self, error: f32) -> f32 {
self.stable_logcosh(error)
}
/// Get approximation error for quadratic region
#[must_use]
pub fn quadratic_approximation_error(&self, error: f32) -> f32 {
let true_loss = self.compute_single_loss(error);
let approx_loss = self.quadratic_approximation(error);
if true_loss == 0.0 {
0.0
} else {
((true_loss - approx_loss) / true_loss).abs()
}
}
/// Get approximation error for linear region
#[must_use]
pub fn linear_approximation_error(&self, error: f32) -> f32 {
let true_loss = self.compute_single_loss(error);
let approx_loss = self.linear_approximation(error);
if true_loss == 0.0 {
0.0
} else {
((true_loss - approx_loss) / true_loss).abs()
}
}
/// Check if `LogCoshLoss` is more robust than MSE for given error
#[must_use]
pub fn robustness_vs_mse(&self, error: f32) -> f32 {
let logcosh_loss = self.compute_single_loss(error);
let mse_loss = 0.5 * error * error;
// Return ratio - smaller values indicate better robustness
if mse_loss == 0.0 {
0.0
} else {
logcosh_loss / mse_loss
}
}
/// Compute numerical gradient using finite differences (for testing)
#[must_use]
pub fn numerical_gradient(&self, error: f32, h: f32) -> f32 {
let loss_plus = self.compute_single_loss(error + h);
let loss_minus = self.compute_single_loss(error - h);
(loss_plus - loss_minus) / (2.0 * h)
}
/// Verify gradient correctness by comparing analytical vs numerical
#[must_use]
pub fn verify_gradient(&self, error: f32, tolerance: f32) -> bool {
let analytical_grad = self.derivative(error);
let numerical_grad = self.numerical_gradient(error, 1e-5);
(analytical_grad - numerical_grad).abs() < tolerance
}
/// Get loss function characteristics at a given point
#[must_use]
pub fn get_characteristics(&self, error: f32) -> LossCharacteristics {
LossCharacteristics {
error,
loss_value: self.compute_single_loss(error),
derivative: self.derivative(error),
second_derivative: self.second_derivative(error),
is_quadratic_region: self.is_in_quadratic_region(error),
is_linear_region: self.is_in_linear_region(error),
quadratic_approx: self.quadratic_approximation(error),
linear_approx: self.linear_approximation(error),
quadratic_error: self.quadratic_approximation_error(error),
linear_error: self.linear_approximation_error(error),
}
}
/// Get the transition point where quadratic and linear approximations have equal accuracy
#[must_use]
pub fn transition_point() -> f32 {
1.2
}
/// Get the stability threshold above which we use the stable approximation
#[must_use]
pub fn stability_threshold() -> f32 {
12.0
}
/// Compute `LogCosh` loss element-wise using stable computation
fn compute_logcosh_elementwise(&self, errors: &Tensor) -> Result<Tensor> {
// Convert errors to CPU for computation
let error_data = errors.to_cpu()?;
// Compute log(cosh(x)) for each element
let mut result_data = Vec::new();
for &error in &error_data {
let logcosh_value = self.stable_logcosh(error);
result_data.push(logcosh_value);
}
// Create result tensor with same shape and device
Ok(Tensor::from_data(
result_data,
errors.shape().dims(),
errors.device(),
)?)
}
/// Apply the specified reduction to the loss values
fn apply_reduction(&self, losses: &Tensor) -> Result<Tensor> {
match self.reduction {
Reduction::None => Ok(losses.clone()),
Reduction::Mean => {
// Compute mean by summing all elements and dividing by count
let sum_result = losses.sum(None)?;
let numel = losses.numel() as f32;
Ok(sum_result.div_scalar(numel)?)
}
Reduction::Sum => Ok(losses.sum(None)?),
}
}
/// Compute `LogCosh` loss with optional sample weights
pub fn forward_weighted(
&self,
predictions: &Tensor,
targets: &Tensor,
weights: Option<&Tensor>,
) -> Result<Tensor> {
// Validate input shapes
if predictions.shape() != targets.shape() {
return Err(TransformerError::InvalidInput(format!(
"Shape mismatch: predictions {:?} vs targets {:?}",
predictions.shape(),
targets.shape()
)));
}
if let Some(w) = weights
&& w.shape() != predictions.shape() {
return Err(TransformerError::InvalidInput(format!(
"Weight shape {:?} doesn't match predictions shape {:?}",
w.shape(),
predictions.shape()
)));
}
// Compute element-wise errors
let errors = (predictions - targets)?;
// Compute LogCosh loss element-wise
let logcosh_values = self.compute_logcosh_elementwise(&errors)?;
// Apply weights if provided
let weighted_losses = if let Some(weights) = weights {
// Element-wise multiplication with weights
self.multiply_tensors(&logcosh_values, weights)?
} else {
logcosh_values
};
// Apply reduction
self.apply_reduction(&weighted_losses)
}
/// Helper method to multiply tensors element-wise
fn multiply_tensors(&self, a: &Tensor, b: &Tensor) -> Result<Tensor> {
let a_data = a.to_cpu()?;
let b_data = b.to_cpu()?;
if a_data.len() != b_data.len() {
return Err(TransformerError::InvalidInput(
"Tensors must have same number of elements".to_string(),
));
}
let result_data: Vec<f32> = a_data
.iter()
.zip(b_data.iter())
.map(|(&x, &y)| x * y)
.collect();
Ok(Tensor::from_data(
result_data,
a.shape().dims(),
a.device(),
)?)
}
/// Compute `LogCosh` loss with Huber-style delta parameter for comparison
/// This allows for adaptive behavior similar to Huber loss
pub fn forward_adaptive(
&self,
predictions: &Tensor,
targets: &Tensor,
delta: f32,
) -> Result<Tensor> {
// Validate inputs
if predictions.shape() != targets.shape() {
return Err(TransformerError::InvalidInput(format!(
"Shape mismatch: predictions {:?} vs targets {:?}",
predictions.shape(),
targets.shape()
)));
}
// Compute element-wise errors
let errors = (predictions - targets)?;
// Compute adaptive LogCosh loss
let adaptive_losses = self.compute_adaptive_logcosh(&errors, delta)?;
// Apply reduction
self.apply_reduction(&adaptive_losses)
}
/// Compute adaptive `LogCosh` loss with scaling parameter
fn compute_adaptive_logcosh(&self, errors: &Tensor, delta: f32) -> Result<Tensor> {
let error_data = errors.to_cpu()?;
let mut result_data = Vec::new();
for &error in &error_data {
// Scale error by delta for adaptive behavior
let scaled_error = error / delta;
let logcosh_value = self.stable_logcosh(scaled_error) * delta;
result_data.push(logcosh_value);
}
Ok(Tensor::from_data(
result_data,
errors.shape().dims(),
errors.device(),
)?)
}
/// Compute statistics about the loss landscape
pub fn compute_loss_statistics(
&self,
predictions: &Tensor,
targets: &Tensor,
) -> Result<LossStatistics> {
let errors = (predictions - targets)?;
let error_data = errors.to_cpu()?;
if error_data.is_empty() {
return Err(TransformerError::InvalidInput("Empty tensor".to_string()));
}
let mut loss_values = Vec::new();
let mut gradients = Vec::new();
let mut quadratic_count = 0;
let mut linear_count = 0;
for &error in &error_data {
let loss = self.compute_single_loss(error);
let grad = self.derivative(error);
loss_values.push(loss);
gradients.push(grad);
if self.is_in_quadratic_region(error) {
quadratic_count += 1;
}
if self.is_in_linear_region(error) {
linear_count += 1;
}
}
let n = error_data.len() as f32;
let mean_loss = loss_values.iter().sum::<f32>() / n;
let mean_gradient = gradients.iter().sum::<f32>() / n;
let mean_error = error_data.iter().sum::<f32>() / n;
// Compute variance
let loss_variance = loss_values
.iter()
.map(|&x| (x - mean_loss).powi(2))
.sum::<f32>()
/ n;
Ok(LossStatistics {
num_samples: error_data.len(),
mean_loss,
mean_gradient,
mean_error,
loss_variance,
quadratic_region_fraction: quadratic_count as f32 / n,
linear_region_fraction: linear_count as f32 / n,
max_loss: loss_values
.iter()
.copied()
.fold(f32::NEG_INFINITY, f32::max),
min_loss: loss_values.iter().copied().fold(f32::INFINITY, f32::min),
})
}
}
impl Default for LogCoshLoss {
fn default() -> Self {
Self::new()
}
}
impl Loss for LogCoshLoss {
fn forward(&self, predictions: &Tensor, targets: &Tensor) -> Result<Tensor> {
// Validate input shapes
if predictions.shape() != targets.shape() {
return Err(TransformerError::InvalidInput(format!(
"Shape mismatch: predictions {:?} vs targets {:?}",
predictions.shape(),
targets.shape()
)));
}
// Validate device compatibility
if predictions.device() != targets.device() {
return Err(TransformerError::InvalidInput(
"Predictions and targets must be on the same device".to_string(),
));
}
// Compute element-wise errors
let errors = (predictions - targets)?;
// Compute LogCosh loss element-wise
let logcosh_values = self.compute_logcosh_elementwise(&errors)?;
// Apply reduction
self.apply_reduction(&logcosh_values)
}
fn reduction(&self) -> Reduction {
self.reduction
}
fn supports_backprop(&self) -> bool {
true
}
fn name(&self) -> &'static str {
"LogCoshLoss"
}
}
/// Additional methods for autograd integration and advanced features
impl LogCoshLoss {
/// Create a `LogCoshLoss` builder for advanced configuration
#[must_use]
pub fn builder() -> LogCoshLossBuilder {
LogCoshLossBuilder::new()
}
/// Check if this loss is equivalent to another `LogCoshLoss`
#[must_use]
pub fn is_equivalent(&self, other: &Self) -> bool {
self.reduction == other.reduction
}
/// Clone with different reduction mode
#[must_use]
pub fn with_different_reduction(&self, reduction: Reduction) -> Self {
Self { reduction }
}
/// Compute loss and return both value and characteristics
pub fn forward_with_characteristics(
&self,
predictions: &Tensor,
targets: &Tensor,
) -> Result<(Tensor, Vec<LossCharacteristics>)> {
let loss_value = self.forward(predictions, targets)?;
let errors = (predictions - targets)?;
let error_data = errors.to_cpu()?;
let characteristics: Vec<LossCharacteristics> = error_data
.iter()
.map(|&error| self.get_characteristics(error))
.collect();
Ok((loss_value, characteristics))
}
}
/// Builder pattern for `LogCoshLoss` configuration
#[derive(Debug, Clone)]
pub struct LogCoshLossBuilder {
reduction: Reduction,
}
impl LogCoshLossBuilder {
/// Create a new builder
#[must_use]
pub fn new() -> Self {
Self {
reduction: Reduction::Mean,
}
}
/// Set the reduction mode
#[must_use]
pub fn reduction(mut self, reduction: Reduction) -> Self {
self.reduction = reduction;
self
}
/// Build the `LogCoshLoss`
#[must_use]
pub fn build(self) -> LogCoshLoss {
LogCoshLoss {
reduction: self.reduction,
}
}
}
impl Default for LogCoshLossBuilder {
fn default() -> Self {
Self::new()
}
}
/// Comparison utilities for `LogCoshLoss` vs other losses
pub struct LossComparison;
impl LossComparison {
/// Compare `LogCosh` vs MSE loss for given errors
#[must_use]
pub fn logcosh_vs_mse(errors: &[f32]) -> ComparisonResult {
let logcosh = LogCoshLoss::new();
let mut logcosh_losses = Vec::new();
let mut mse_losses = Vec::new();
for &error in errors {
logcosh_losses.push(logcosh.compute_single_loss(error));
mse_losses.push(0.5 * error * error);
}
let logcosh_mean = logcosh_losses.iter().sum::<f32>() / logcosh_losses.len() as f32;
let mse_mean = mse_losses.iter().sum::<f32>() / mse_losses.len() as f32;
ComparisonResult {
logcosh_mean,
comparison_mean: mse_mean,
robustness_ratio: logcosh_mean / mse_mean.max(1e-8),
recommendation: if logcosh_mean < mse_mean {
"LogCosh"
} else {
"MSE"
}
.to_string(),
}
}
/// Compare `LogCosh` vs Huber loss (simulated)
#[must_use]
pub fn logcosh_vs_huber(errors: &[f32], delta: f32) -> ComparisonResult {
let logcosh = LogCoshLoss::new();
let mut logcosh_losses = Vec::new();
let mut huber_losses = Vec::new();
for &error in errors {
logcosh_losses.push(logcosh.compute_single_loss(error));
// Simulate Huber loss
let huber_loss = if error.abs() <= delta {
0.5 * error * error
} else {
delta * (error.abs() - 0.5 * delta)
};
huber_losses.push(huber_loss);
}
let logcosh_mean = logcosh_losses.iter().sum::<f32>() / logcosh_losses.len() as f32;
let huber_mean = huber_losses.iter().sum::<f32>() / huber_losses.len() as f32;
ComparisonResult {
logcosh_mean,
comparison_mean: huber_mean,
robustness_ratio: logcosh_mean / huber_mean.max(1e-8),
recommendation: "LogCosh (smoother gradients)".to_string(),
}
}
}
/// Result of loss function comparison
#[derive(Debug, Clone)]
pub struct ComparisonResult {
/// Mean `LogCosh` loss
pub logcosh_mean: f32,
/// Mean of comparison loss
pub comparison_mean: f32,
/// Ratio indicating robustness
pub robustness_ratio: f32,
/// Recommendation string
pub recommendation: String,
}