#[cfg(test)]
mod tests {
use super::super::*;
#[test]
fn falsify_ce_001_non_negativity() {
let criterion = CrossEntropyLoss::with_reduction(Reduction::None);
let test_cases: Vec<(Vec<f32>, Vec<f32>, usize)> = vec![
(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![0.0, 2.0], 3),
(vec![0.0, 0.0, 0.0, 0.0], vec![0.0, 1.0], 2),
(vec![-10.0, 10.0, -10.0, 10.0], vec![1.0, 1.0], 2),
(vec![100.0, -100.0, 0.0], vec![0.0], 3),
];
for (i, (logits_data, targets_data, nc)) in test_cases.iter().enumerate() {
let batch = targets_data.len();
let logits = Tensor::new(logits_data, &[batch, *nc]);
let targets = Tensor::new(targets_data, &[batch]);
let loss = criterion.forward(&logits, &targets);
for (j, &val) in loss.data().iter().enumerate() {
assert!(
val >= -1e-6,
"FALSIFIED CE-001 case {i}[{j}]: CE = {val} < 0"
);
}
}
}
#[test]
fn falsify_ce_006_perfect_prediction() {
let criterion = CrossEntropyLoss::with_reduction(Reduction::None);
let logits = Tensor::new(&[50.0, -50.0, -50.0, -50.0, 50.0, -50.0], &[2, 3]);
let targets = Tensor::new(&[0.0, 1.0], &[2]);
let loss = criterion.forward(&logits, &targets);
for (i, &val) in loss.data().iter().enumerate() {
assert!(
val < 1e-3,
"FALSIFIED CE-006: CE for near-perfect prediction [{i}] = {val}, expected ≈ 0"
);
}
}
#[test]
fn falsify_ce_003_numerical_stability() {
let criterion = CrossEntropyLoss::new();
let test_cases: Vec<(Vec<f32>, Vec<f32>, usize)> = vec![
(vec![1000.0, -1000.0, 0.0], vec![0.0], 3),
(vec![-500.0, -500.0, -500.0], vec![1.0], 3),
(vec![0.0, 0.0, 0.0, 0.0], vec![2.0], 4),
];
for (i, (logits_data, targets_data, nc)) in test_cases.iter().enumerate() {
let batch = targets_data.len();
let logits = Tensor::new(logits_data, &[batch, *nc]);
let targets = Tensor::new(targets_data, &[batch]);
let loss = criterion.forward(&logits, &targets);
let val = loss.data()[0];
assert!(
val.is_finite(),
"FALSIFIED CE-003 case {i}: CE = {val} (not finite)"
);
}
}
#[test]
fn falsify_ce_001b_uniform_logits() {
let criterion = CrossEntropyLoss::with_reduction(Reduction::None);
for &num_classes in &[2_usize, 3, 5, 10] {
let logits_data = vec![1.0; num_classes];
let logits = Tensor::new(&logits_data, &[1, num_classes]);
let targets = Tensor::new(&[0.0], &[1]);
let loss = criterion.forward(&logits, &targets);
let expected = (num_classes as f32).ln();
let val = loss.data()[0];
let diff = (val - expected).abs();
assert!(
diff < 1e-4,
"FALSIFIED CE-001b: CE(uniform, C={num_classes}) = {val}, expected log({num_classes}) = {expected}"
);
}
}
#[test]
fn falsify_ce_002_log_softmax_upper_bound() {
let criterion = CrossEntropyLoss::with_reduction(Reduction::None);
for &nc in &[2_usize, 3, 5, 10, 50] {
let logits_data = vec![0.0; nc];
let logits = Tensor::new(&logits_data, &[1, nc]);
let targets = Tensor::new(&[0.0], &[1]);
let loss = criterion.forward(&logits, &targets);
let val = loss.data()[0];
assert!(
val >= -1e-6,
"FALSIFIED CE-002: CE for uniform logits C={nc} = {val} < 0"
);
}
}
fn softmax_minus_onehot(
logits: &[f32],
targets: &[usize],
batch: usize,
nc: usize,
) -> Vec<f32> {
let mut out = Vec::with_capacity(batch * nc);
for b in 0..batch {
let row = &logits[b * nc..(b + 1) * nc];
let max = row.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = row.iter().map(|&x| (x - max).exp()).collect();
let sum: f32 = exps.iter().sum();
for (i, &e) in exps.iter().enumerate() {
let mut g = e / sum;
if i == targets[b] {
g -= 1.0;
}
out.push(g);
}
}
out
}
#[test]
fn falsify_ce_reduction_001_sum_grad_not_divided_by_batch() {
let logits_data = vec![1.0_f32, 2.0, 0.5, 0.1, 3.0, 0.2];
let targets_idx = vec![1_usize, 2_usize];
let batch = 2;
let nc = 3;
let criterion = CrossEntropyLoss::with_reduction(Reduction::Sum);
let logits = Tensor::new(&logits_data, &[batch, nc]).requires_grad();
let logits_id = logits.id();
let targets = Tensor::new(&[1.0_f32, 2.0], &[batch]);
let loss = criterion.forward(&logits, &targets);
loss.backward();
let grad = crate::autograd::get_grad(logits_id).expect("logits must have a gradient");
let expected = softmax_minus_onehot(&logits_data, &targets_idx, batch, nc);
for (i, (&g, &e)) in grad.data().iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-6,
"FALSIFIED CE-REDUCTION-001 (Sum)[{i}]: grad = {g}, expected (softmax-onehot) = {e}; \
buggy main divides by batch → {} (half)",
e / batch as f32
);
}
}
#[test]
fn falsify_ce_reduction_001_mean_grad_divided_by_batch() {
let logits_data = vec![1.0_f32, 2.0, 0.5, 0.1, 3.0, 0.2];
let targets_idx = vec![1_usize, 2_usize];
let batch = 2;
let nc = 3;
let criterion = CrossEntropyLoss::with_reduction(Reduction::Mean);
let logits = Tensor::new(&logits_data, &[batch, nc]).requires_grad();
let logits_id = logits.id();
let targets = Tensor::new(&[1.0_f32, 2.0], &[batch]);
let loss = criterion.forward(&logits, &targets);
loss.backward();
let grad = crate::autograd::get_grad(logits_id).expect("logits must have a gradient");
let raw = softmax_minus_onehot(&logits_data, &targets_idx, batch, nc);
for (i, (&g, &r)) in grad.data().iter().zip(raw.iter()).enumerate() {
let expected = r / batch as f32;
assert!(
(g - expected).abs() < 1e-6,
"FALSIFIED CE-REDUCTION-001 (Mean)[{i}]: grad = {g}, expected (softmax-onehot)/batch = {expected}"
);
}
}
#[test]
fn falsify_ce_reduction_001_none_per_sample_upstream() {
let logits_data = vec![1.0_f32, 2.0, 0.5, 0.1, 3.0, 0.2];
let targets_idx = vec![1_usize, 2_usize];
let batch = 2;
let nc = 3;
let criterion = CrossEntropyLoss::with_reduction(Reduction::None);
let logits = Tensor::new(&logits_data, &[batch, nc]).requires_grad();
let logits_id = logits.id();
let targets = Tensor::new(&[1.0_f32, 2.0], &[batch]);
let loss = criterion.forward(&logits, &targets);
let upstream = Tensor::new(&[2.0_f32, 5.0], &[batch]);
loss.backward_with_grad(upstream);
let grad = crate::autograd::get_grad(logits_id).expect("logits must have a gradient");
let raw = softmax_minus_onehot(&logits_data, &targets_idx, batch, nc);
let per_sample = [2.0_f32, 5.0];
for b in 0..batch {
for i in 0..nc {
let idx = b * nc + i;
let expected = raw[idx] * per_sample[b];
let g = grad.data()[idx];
assert!(
(g - expected).abs() < 1e-6,
"FALSIFIED CE-REDUCTION-001 (None)[{b},{i}]: grad = {g}, \
expected (softmax-onehot)*upstream[{b}] = {expected}"
);
}
}
}
mod ce_proptest_falsify {
use super::super::super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn falsify_ce_001_prop_non_negativity(
nc in 2..=10usize,
target in 0..10usize,
seed in 0..1000u32,
) {
let target = target % nc;
let logits_data: Vec<f32> = (0..nc)
.map(|i| ((i as f32 + seed as f32) * 0.37).sin() * 10.0)
.collect();
let criterion = CrossEntropyLoss::with_reduction(Reduction::None);
let logits = Tensor::new(&logits_data, &[1, nc]);
let targets = Tensor::new(&[target as f32], &[1]);
let loss = criterion.forward(&logits, &targets);
let val = loss.data()[0];
prop_assert!(
val >= -1e-6,
"FALSIFIED CE-001-prop: CE = {} < 0 (nc={}, target={})",
val, nc, target
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn falsify_ce_003_prop_finite_output(
nc in 2..=10usize,
target in 0..10usize,
scale in 0.1f32..100.0,
seed in 0..1000u32,
) {
let target = target % nc;
let logits_data: Vec<f32> = (0..nc)
.map(|i| ((i as f32 + seed as f32) * 0.73).cos() * scale)
.collect();
let criterion = CrossEntropyLoss::new();
let logits = Tensor::new(&logits_data, &[1, nc]);
let targets = Tensor::new(&[target as f32], &[1]);
let loss = criterion.forward(&logits, &targets);
let val = loss.data()[0];
prop_assert!(
val.is_finite(),
"FALSIFIED CE-003-prop: CE = {} (not finite), nc={}, scale={}",
val, nc, scale
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn falsify_ce_006_prop_perfect_prediction(
nc in 2..=10usize,
target in 0..10usize,
) {
let target = target % nc;
let mut logits_data = vec![-50.0; nc];
logits_data[target] = 50.0;
let criterion = CrossEntropyLoss::with_reduction(Reduction::None);
let logits = Tensor::new(&logits_data, &[1, nc]);
let targets = Tensor::new(&[target as f32], &[1]);
let loss = criterion.forward(&logits, &targets);
let val = loss.data()[0];
prop_assert!(
val < 1e-3,
"FALSIFIED CE-006-prop: CE = {} for dominant logit, expected ≈ 0",
val
);
}
}
}
}