1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum LossType {
9 MSE,
11 MAE,
13 CrossEntropy,
15 Huber,
17 Hinge,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Reduction {
24 Mean,
26 Sum,
28 None,
30}
31
32#[derive(Debug, Clone)]
34pub struct LossConfig {
35 pub loss_type: LossType,
37 pub huber_delta: f64,
39 pub epsilon: f64,
41 pub reduction: Reduction,
43}
44
45impl Default for LossConfig {
46 fn default() -> Self {
47 Self {
48 loss_type: LossType::MSE,
49 huber_delta: 1.0,
50 epsilon: 1e-7,
51 reduction: Reduction::Mean,
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct LossFunctionStats {
59 pub loss_type: LossType,
61 pub computations: u64,
63}
64
65pub struct TensorLossFunction {
86 config: LossConfig,
87 computations: u64,
88}
89
90impl TensorLossFunction {
91 pub fn new(config: LossConfig) -> Self {
93 Self {
94 config,
95 computations: 0,
96 }
97 }
98
99 pub fn compute(&mut self, predictions: &[f64], targets: &[f64]) -> Result<Vec<f64>, String> {
103 if predictions.len() != targets.len() {
104 return Err(format!(
105 "length mismatch: predictions={} vs targets={}",
106 predictions.len(),
107 targets.len()
108 ));
109 }
110
111 let eps = self.config.epsilon;
112 let delta = self.config.huber_delta;
113
114 let losses: Vec<f64> = predictions
115 .iter()
116 .zip(targets.iter())
117 .map(|(&p, &t)| match self.config.loss_type {
118 LossType::MSE => {
119 let d = p - t;
120 d * d
121 }
122 LossType::MAE => (p - t).abs(),
123 LossType::CrossEntropy => -(t * (p + eps).ln() + (1.0 - t) * (1.0 - p + eps).ln()),
124 LossType::Huber => {
125 let d = (p - t).abs();
126 if d <= delta {
127 0.5 * d * d
128 } else {
129 delta * (d - 0.5 * delta)
130 }
131 }
132 LossType::Hinge => {
133 let margin = 1.0 - t * p;
134 if margin > 0.0 {
135 margin
136 } else {
137 0.0
138 }
139 }
140 })
141 .collect();
142
143 self.computations += losses.len() as u64;
144 Ok(losses)
145 }
146
147 pub fn reduce(&self, losses: &[f64]) -> f64 {
149 match self.config.reduction {
150 Reduction::Sum => losses.iter().sum(),
151 Reduction::Mean => {
152 if losses.is_empty() {
153 0.0
154 } else {
155 let sum: f64 = losses.iter().sum();
156 sum / losses.len() as f64
157 }
158 }
159 Reduction::None => {
160 losses.iter().sum()
163 }
164 }
165 }
166
167 pub fn forward(&mut self, predictions: &[f64], targets: &[f64]) -> Result<f64, String> {
169 let losses = self.compute(predictions, targets)?;
170 Ok(self.reduce(&losses))
171 }
172
173 pub fn gradient(&mut self, predictions: &[f64], targets: &[f64]) -> Result<Vec<f64>, String> {
177 if predictions.len() != targets.len() {
178 return Err(format!(
179 "length mismatch: predictions={} vs targets={}",
180 predictions.len(),
181 targets.len()
182 ));
183 }
184
185 let eps = self.config.epsilon;
186 let delta = self.config.huber_delta;
187
188 let grads: Vec<f64> = predictions
189 .iter()
190 .zip(targets.iter())
191 .map(|(&p, &t)| match self.config.loss_type {
192 LossType::MSE => 2.0 * (p - t),
193 LossType::MAE => {
194 let d = p - t;
195 if d > 0.0 {
196 1.0
197 } else if d < 0.0 {
198 -1.0
199 } else {
200 0.0
201 }
202 }
203 LossType::CrossEntropy => -(t / (p + eps)) + (1.0 - t) / (1.0 - p + eps),
204 LossType::Huber => {
205 let d = p - t;
206 let abs_d = d.abs();
207 if abs_d <= delta {
208 d
209 } else if d > 0.0 {
210 delta
211 } else {
212 -delta
213 }
214 }
215 LossType::Hinge => {
216 if t * p < 1.0 {
217 -t
218 } else {
219 0.0
220 }
221 }
222 })
223 .collect();
224
225 self.computations += grads.len() as u64;
226 Ok(grads)
227 }
228
229 pub fn mse(a: &[f64], b: &[f64]) -> f64 {
233 if a.len() != b.len() || a.is_empty() {
234 return 0.0;
235 }
236 let sum: f64 = a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum();
237 sum / a.len() as f64
238 }
239
240 pub fn mae(a: &[f64], b: &[f64]) -> f64 {
244 if a.len() != b.len() || a.is_empty() {
245 return 0.0;
246 }
247 let sum: f64 = a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()).sum();
248 sum / a.len() as f64
249 }
250
251 pub fn stats(&self) -> LossFunctionStats {
253 LossFunctionStats {
254 loss_type: self.config.loss_type,
255 computations: self.computations,
256 }
257 }
258}
259
260#[cfg(test)]
265mod tests {
266 use super::*;
267
268 fn cfg(loss_type: LossType) -> LossConfig {
269 LossConfig {
270 loss_type,
271 ..LossConfig::default()
272 }
273 }
274
275 fn cfg_with_reduction(loss_type: LossType, reduction: Reduction) -> LossConfig {
276 LossConfig {
277 loss_type,
278 reduction,
279 ..LossConfig::default()
280 }
281 }
282
283 #[test]
286 fn mse_basic() {
287 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
288 let losses = f.compute(&[1.0, 2.0, 3.0], &[1.0, 2.0, 3.0]).expect("ok");
289 assert!(losses.iter().all(|&v| v.abs() < 1e-15));
290 }
291
292 #[test]
293 fn mse_nonzero() {
294 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
295 let losses = f.compute(&[1.0, 2.0], &[2.0, 4.0]).expect("ok");
296 assert!((losses[0] - 1.0).abs() < 1e-15);
297 assert!((losses[1] - 4.0).abs() < 1e-15);
298 }
299
300 #[test]
301 fn mse_forward_mean() {
302 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
303 let val = f.forward(&[1.0, 2.0, 3.0], &[1.5, 2.5, 3.5]).expect("ok");
304 assert!((val - 0.25).abs() < 1e-10);
305 }
306
307 #[test]
308 fn mse_gradient() {
309 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
310 let g = f.gradient(&[3.0], &[1.0]).expect("ok");
311 assert!((g[0] - 4.0).abs() < 1e-15); }
313
314 #[test]
317 fn mae_basic() {
318 let mut f = TensorLossFunction::new(cfg(LossType::MAE));
319 let losses = f.compute(&[1.0, 5.0], &[3.0, 2.0]).expect("ok");
320 assert!((losses[0] - 2.0).abs() < 1e-15);
321 assert!((losses[1] - 3.0).abs() < 1e-15);
322 }
323
324 #[test]
325 fn mae_gradient_positive() {
326 let mut f = TensorLossFunction::new(cfg(LossType::MAE));
327 let g = f.gradient(&[5.0], &[3.0]).expect("ok");
328 assert!((g[0] - 1.0).abs() < 1e-15);
329 }
330
331 #[test]
332 fn mae_gradient_negative() {
333 let mut f = TensorLossFunction::new(cfg(LossType::MAE));
334 let g = f.gradient(&[1.0], &[3.0]).expect("ok");
335 assert!((g[0] - (-1.0)).abs() < 1e-15);
336 }
337
338 #[test]
339 fn mae_gradient_zero() {
340 let mut f = TensorLossFunction::new(cfg(LossType::MAE));
341 let g = f.gradient(&[3.0], &[3.0]).expect("ok");
342 assert!(g[0].abs() < 1e-15);
343 }
344
345 #[test]
348 fn cross_entropy_perfect_prediction() {
349 let mut f = TensorLossFunction::new(cfg(LossType::CrossEntropy));
350 let losses = f.compute(&[0.9999999], &[1.0]).expect("ok");
352 assert!(losses[0] < 0.001);
353 }
354
355 #[test]
356 fn cross_entropy_bad_prediction() {
357 let mut f = TensorLossFunction::new(cfg(LossType::CrossEntropy));
358 let losses = f.compute(&[0.01], &[1.0]).expect("ok");
360 assert!(losses[0] > 1.0);
361 }
362
363 #[test]
364 fn cross_entropy_gradient() {
365 let mut f = TensorLossFunction::new(cfg(LossType::CrossEntropy));
366 let eps = 1e-7;
367 let p = 0.7;
368 let t = 1.0;
369 let g = f.gradient(&[p], &[t]).expect("ok");
370 let expected = -(t / (p + eps)) + (1.0 - t) / (1.0 - p + eps);
371 assert!((g[0] - expected).abs() < 1e-10);
372 }
373
374 #[test]
375 fn cross_entropy_symmetry() {
376 let mut f = TensorLossFunction::new(cfg(LossType::CrossEntropy));
378 let losses = f.compute(&[0.01], &[0.0]).expect("ok");
379 assert!(losses[0] < 0.02); }
381
382 #[test]
385 fn huber_quadratic_region() {
386 let mut f = TensorLossFunction::new(cfg(LossType::Huber));
387 let losses = f.compute(&[1.5], &[1.0]).expect("ok");
389 assert!((losses[0] - 0.125).abs() < 1e-15);
390 }
391
392 #[test]
393 fn huber_linear_region() {
394 let mut f = TensorLossFunction::new(cfg(LossType::Huber));
395 let losses = f.compute(&[3.0], &[1.0]).expect("ok");
397 assert!((losses[0] - 1.5).abs() < 1e-15);
398 }
399
400 #[test]
401 fn huber_transition_at_delta() {
402 let mut f = TensorLossFunction::new(cfg(LossType::Huber));
404 let losses = f.compute(&[2.0], &[1.0]).expect("ok");
405 assert!((losses[0] - 0.5).abs() < 1e-15);
408 }
409
410 #[test]
411 fn huber_custom_delta() {
412 let config = LossConfig {
413 loss_type: LossType::Huber,
414 huber_delta: 0.5,
415 ..LossConfig::default()
416 };
417 let mut f = TensorLossFunction::new(config);
418 let losses = f.compute(&[2.0], &[1.0]).expect("ok");
420 assert!((losses[0] - 0.375).abs() < 1e-15);
421 }
422
423 #[test]
424 fn huber_gradient_quadratic() {
425 let mut f = TensorLossFunction::new(cfg(LossType::Huber));
426 let g = f.gradient(&[1.3], &[1.0]).expect("ok");
427 assert!((g[0] - 0.3).abs() < 1e-14);
428 }
429
430 #[test]
431 fn huber_gradient_linear_positive() {
432 let mut f = TensorLossFunction::new(cfg(LossType::Huber));
433 let g = f.gradient(&[5.0], &[1.0]).expect("ok");
434 assert!((g[0] - 1.0).abs() < 1e-15); }
436
437 #[test]
438 fn huber_gradient_linear_negative() {
439 let mut f = TensorLossFunction::new(cfg(LossType::Huber));
440 let g = f.gradient(&[1.0], &[5.0]).expect("ok");
441 assert!((g[0] - (-1.0)).abs() < 1e-15);
442 }
443
444 #[test]
447 fn hinge_correct_large_margin() {
448 let mut f = TensorLossFunction::new(cfg(LossType::Hinge));
449 let losses = f.compute(&[2.0], &[1.0]).expect("ok");
451 assert!(losses[0].abs() < 1e-15);
452 }
453
454 #[test]
455 fn hinge_violation() {
456 let mut f = TensorLossFunction::new(cfg(LossType::Hinge));
457 let losses = f.compute(&[0.5], &[1.0]).expect("ok");
459 assert!((losses[0] - 0.5).abs() < 1e-15);
460 }
461
462 #[test]
463 fn hinge_negative_target() {
464 let mut f = TensorLossFunction::new(cfg(LossType::Hinge));
465 let losses = f.compute(&[-2.0], &[-1.0]).expect("ok");
467 assert!(losses[0].abs() < 1e-15);
468 }
469
470 #[test]
471 fn hinge_gradient_active() {
472 let mut f = TensorLossFunction::new(cfg(LossType::Hinge));
473 let g = f.gradient(&[0.5], &[1.0]).expect("ok");
474 assert!((g[0] - (-1.0)).abs() < 1e-15); }
476
477 #[test]
478 fn hinge_gradient_inactive() {
479 let mut f = TensorLossFunction::new(cfg(LossType::Hinge));
480 let g = f.gradient(&[2.0], &[1.0]).expect("ok");
481 assert!(g[0].abs() < 1e-15);
482 }
483
484 #[test]
487 fn reduction_sum() {
488 let mut f = TensorLossFunction::new(cfg_with_reduction(LossType::MSE, Reduction::Sum));
489 let val = f.forward(&[1.0, 2.0], &[2.0, 4.0]).expect("ok");
490 assert!((val - 5.0).abs() < 1e-15); }
492
493 #[test]
494 fn reduction_mean() {
495 let mut f = TensorLossFunction::new(cfg_with_reduction(LossType::MSE, Reduction::Mean));
496 let val = f.forward(&[1.0, 2.0], &[2.0, 4.0]).expect("ok");
497 assert!((val - 2.5).abs() < 1e-15); }
499
500 #[test]
501 fn reduction_none_returns_sum_in_forward() {
502 let mut f = TensorLossFunction::new(cfg_with_reduction(LossType::MSE, Reduction::None));
503 let val = f.forward(&[1.0, 2.0], &[2.0, 4.0]).expect("ok");
504 assert!((val - 5.0).abs() < 1e-15);
506 }
507
508 #[test]
509 fn reduce_empty() {
510 let f = TensorLossFunction::new(cfg(LossType::MSE));
511 assert!(f.reduce(&[]).abs() < 1e-15);
512 }
513
514 #[test]
517 fn compute_length_mismatch() {
518 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
519 let res = f.compute(&[1.0, 2.0], &[1.0]);
520 assert!(res.is_err());
521 let msg = res.expect_err("should fail");
522 assert!(msg.contains("length mismatch"));
523 }
524
525 #[test]
526 fn gradient_length_mismatch() {
527 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
528 let res = f.gradient(&[1.0], &[1.0, 2.0]);
529 assert!(res.is_err());
530 }
531
532 #[test]
533 fn forward_length_mismatch() {
534 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
535 let res = f.forward(&[1.0, 2.0, 3.0], &[1.0]);
536 assert!(res.is_err());
537 }
538
539 #[test]
542 fn all_zeros() {
543 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
544 let losses = f.compute(&[0.0, 0.0], &[0.0, 0.0]).expect("ok");
545 assert!(losses.iter().all(|&v| v.abs() < 1e-15));
546 }
547
548 #[test]
549 fn all_ones_mse() {
550 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
551 let losses = f.compute(&[1.0, 1.0], &[1.0, 1.0]).expect("ok");
552 assert!(losses.iter().all(|&v| v.abs() < 1e-15));
553 }
554
555 #[test]
556 fn empty_inputs() {
557 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
558 let losses = f.compute(&[], &[]).expect("ok");
559 assert!(losses.is_empty());
560 }
561
562 #[test]
563 fn single_element() {
564 let mut f = TensorLossFunction::new(cfg(LossType::MAE));
565 let val = f.forward(&[5.0], &[3.0]).expect("ok");
566 assert!((val - 2.0).abs() < 1e-15);
567 }
568
569 #[test]
572 fn static_mse() {
573 let val = TensorLossFunction::mse(&[1.0, 2.0], &[2.0, 4.0]);
574 assert!((val - 2.5).abs() < 1e-15);
575 }
576
577 #[test]
578 fn static_mae() {
579 let val = TensorLossFunction::mae(&[1.0, 2.0], &[3.0, 5.0]);
580 assert!((val - 2.5).abs() < 1e-15);
581 }
582
583 #[test]
584 fn static_mse_length_mismatch() {
585 let val = TensorLossFunction::mse(&[1.0], &[1.0, 2.0]);
586 assert!(val.abs() < 1e-15);
587 }
588
589 #[test]
590 fn static_mae_empty() {
591 let val = TensorLossFunction::mae(&[], &[]);
592 assert!(val.abs() < 1e-15);
593 }
594
595 #[test]
598 fn stats_initial() {
599 let f = TensorLossFunction::new(cfg(LossType::Huber));
600 let s = f.stats();
601 assert_eq!(s.loss_type, LossType::Huber);
602 assert_eq!(s.computations, 0);
603 }
604
605 #[test]
606 fn stats_after_compute() {
607 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
608 let _ = f.compute(&[1.0, 2.0, 3.0], &[0.0, 0.0, 0.0]);
609 assert_eq!(f.stats().computations, 3);
610 }
611
612 #[test]
613 fn stats_accumulate() {
614 let mut f = TensorLossFunction::new(cfg(LossType::MSE));
615 let _ = f.compute(&[1.0, 2.0], &[0.0, 0.0]);
616 let _ = f.gradient(&[1.0, 2.0, 3.0], &[0.0, 0.0, 0.0]);
617 assert_eq!(f.stats().computations, 5); }
619
620 #[test]
623 fn numerical_gradient_mse() {
624 verify_numerical_gradient(LossType::MSE, &[1.5, 2.5, 0.3], &[1.0, 3.0, 0.1]);
625 }
626
627 #[test]
628 fn numerical_gradient_mae() {
629 verify_numerical_gradient(LossType::MAE, &[1.5, 2.5, 0.3], &[1.0, 3.0, 0.1]);
631 }
632
633 #[test]
634 fn numerical_gradient_cross_entropy() {
635 verify_numerical_gradient(LossType::CrossEntropy, &[0.7, 0.3, 0.9], &[1.0, 0.0, 1.0]);
636 }
637
638 #[test]
639 fn numerical_gradient_huber() {
640 verify_numerical_gradient(LossType::Huber, &[1.5, 4.0, 0.3], &[1.0, 1.0, 0.1]);
641 }
642
643 fn verify_numerical_gradient(loss_type: LossType, preds: &[f64], targets: &[f64]) {
645 let config = cfg(loss_type);
646 let mut f = TensorLossFunction::new(config.clone());
647 let analytical = f.gradient(preds, targets).expect("ok");
648
649 let h = 1e-5;
650 for i in 0..preds.len() {
651 let mut p_plus = preds.to_vec();
652 let mut p_minus = preds.to_vec();
653 p_plus[i] += h;
654 p_minus[i] -= h;
655
656 let mut f1 = TensorLossFunction::new(config.clone());
657 let mut f2 = TensorLossFunction::new(config.clone());
658 let l_plus = f1.compute(&p_plus, targets).expect("ok");
659 let l_minus = f2.compute(&p_minus, targets).expect("ok");
660
661 let numerical = (l_plus[i] - l_minus[i]) / (2.0 * h);
662 let tol = 1e-4;
663 assert!(
664 (analytical[i] - numerical).abs() < tol,
665 "{:?} grad[{}]: analytical={}, numerical={}",
666 loss_type,
667 i,
668 analytical[i],
669 numerical
670 );
671 }
672 }
673}