1#![allow(clippy::needless_range_loop)]
2pub fn relu(x: f64) -> f64 {
16 x.max(0.0)
17}
18
19pub fn sigmoid(x: f64) -> f64 {
21 1.0 / (1.0 + (-x).exp())
22}
23
24pub fn softmax(x: &[f64]) -> Vec<f64> {
28 if x.is_empty() {
29 return Vec::new();
30 }
31 let max_val = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
32 let exps: Vec<f64> = x.iter().map(|&v| (v - max_val).exp()).collect();
33 let sum: f64 = exps.iter().sum();
34 exps.iter().map(|&e| e / sum).collect()
35}
36
37pub fn mse_loss(pred: &[f64], target: &[f64]) -> f64 {
41 if pred.is_empty() {
42 return 0.0;
43 }
44 let n = pred.len().min(target.len());
45 let sum: f64 = pred[..n]
46 .iter()
47 .zip(target[..n].iter())
48 .map(|(p, t)| (p - t).powi(2))
49 .sum();
50 sum / n as f64
51}
52
53#[derive(Debug, Clone, PartialEq)]
59pub enum LayerType {
60 Dense,
62 Conv1D,
64 ReLU,
66 Sigmoid,
68 Tanh,
70 Softmax,
72 BatchNorm,
74 Dropout,
76}
77
78#[derive(Debug, Clone)]
84pub struct NeuralLayer {
85 pub weights: Vec<f64>,
87 pub biases: Vec<f64>,
89 pub layer_type: LayerType,
91 pub input_size: usize,
93 pub output_size: usize,
95}
96
97impl NeuralLayer {
98 pub fn new(input_size: usize, output_size: usize, layer_type: LayerType) -> Self {
103 Self {
104 weights: vec![0.0; input_size * output_size],
105 biases: vec![0.0; output_size],
106 layer_type,
107 input_size,
108 output_size,
109 }
110 }
111
112 pub fn forward(&self, input: &[f64]) -> Vec<f64> {
117 match self.layer_type {
118 LayerType::ReLU => input.iter().map(|&x| relu(x)).collect(),
119 LayerType::Sigmoid => input.iter().map(|&x| sigmoid(x)).collect(),
120 LayerType::Tanh => input.iter().map(|&x| x.tanh()).collect(),
121 LayerType::Softmax => softmax(input),
122 LayerType::BatchNorm => {
123 let n = input.len();
126 if n == 0 {
127 return Vec::new();
128 }
129 let mean = input.iter().sum::<f64>() / n as f64;
130 let var = input.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
131 let std = (var + 1e-5).sqrt();
132 input
133 .iter()
134 .enumerate()
135 .map(|(i, &x)| {
136 let gamma = self.weights.get(i).copied().unwrap_or(1.0);
137 let beta = self.biases.get(i).copied().unwrap_or(0.0);
138 gamma * (x - mean) / std + beta
139 })
140 .collect()
141 }
142 LayerType::Dropout => {
143 input.to_vec()
145 }
146 LayerType::Dense | LayerType::Conv1D => {
147 let in_sz = input.len();
149 let out_sz = self.output_size;
150 let mut out = vec![0.0; out_sz];
151 for j in 0..out_sz {
152 let mut acc = self.biases.get(j).copied().unwrap_or(0.0);
153 for i in 0..in_sz {
154 let w = self.weights.get(j * in_sz + i).copied().unwrap_or(0.0);
155 acc += w * input[i];
156 }
157 out[j] = acc;
158 }
159 out
160 }
161 }
162 }
163}
164
165#[derive(Debug, Clone)]
171pub struct GpuNeuralNet {
172 pub layers: Vec<NeuralLayer>,
174}
175
176impl GpuNeuralNet {
177 pub fn new() -> Self {
179 Self { layers: Vec::new() }
180 }
181
182 pub fn add_layer(&mut self, layer: NeuralLayer) {
184 self.layers.push(layer);
185 }
186
187 pub fn forward_pass(&self, input: &[f64]) -> Vec<f64> {
189 let mut current = input.to_vec();
190 for layer in &self.layers {
191 current = layer.forward(¤t);
192 }
193 current
194 }
195
196 pub fn batch_forward(&self, inputs: &[Vec<f64>]) -> Vec<Vec<f64>> {
198 inputs.iter().map(|inp| self.forward_pass(inp)).collect()
199 }
200}
201
202impl Default for GpuNeuralNet {
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208#[derive(Debug, Clone)]
214pub struct BackpropGpu {
215 pub gradients: Vec<Vec<f64>>,
217}
218
219impl BackpropGpu {
220 pub fn new(net: &GpuNeuralNet) -> Self {
222 let gradients = net
223 .layers
224 .iter()
225 .map(|l| vec![0.0; l.weights.len()])
226 .collect();
227 Self { gradients }
228 }
229
230 pub fn backward_pass(&mut self, loss_grad: &[f64]) {
236 let scale = loss_grad.first().copied().unwrap_or(0.0);
237 for grad_buf in &mut self.gradients {
238 for g in grad_buf.iter_mut() {
239 *g = scale;
240 }
241 }
242 }
243}
244
245#[derive(Debug, Clone, PartialEq)]
251pub enum OptimizerType {
252 Sgd,
254 Adam,
256}
257
258#[derive(Debug, Clone)]
266pub struct AdamOptimizer {
267 pub beta1: f64,
269 pub beta2: f64,
271 pub eps: f64,
273 pub lr: f64,
275 pub m: Vec<f64>,
277 pub v: Vec<f64>,
279 pub t: u64,
281}
282
283impl AdamOptimizer {
284 pub fn new(n: usize, lr: f64, beta1: f64, beta2: f64, eps: f64) -> Self {
286 Self {
287 beta1,
288 beta2,
289 eps,
290 lr,
291 m: vec![0.0; n],
292 v: vec![0.0; n],
293 t: 0,
294 }
295 }
296
297 pub fn update(&mut self, params: &mut [f64], grads: &[f64]) {
302 self.t += 1;
303 let t = self.t as f64;
304 let lr_t = self.lr * (1.0 - self.beta2.powf(t)).sqrt() / (1.0 - self.beta1.powf(t));
305 let n = params
306 .len()
307 .min(grads.len())
308 .min(self.m.len())
309 .min(self.v.len());
310 for i in 0..n {
311 self.m[i] = self.beta1 * self.m[i] + (1.0 - self.beta1) * grads[i];
312 self.v[i] = self.beta2 * self.v[i] + (1.0 - self.beta2) * grads[i].powi(2);
313 params[i] -= lr_t * self.m[i] / (self.v[i].sqrt() + self.eps);
314 }
315 }
316}
317
318#[derive(Debug)]
324pub struct GpuTrainer {
325 pub net: GpuNeuralNet,
327 pub backprop: BackpropGpu,
329 pub learning_rate: f64,
331 pub optimizer: OptimizerType,
333 pub adam: Option<AdamOptimizer>,
335}
336
337impl GpuTrainer {
338 pub fn new(net: GpuNeuralNet, learning_rate: f64, optimizer: OptimizerType) -> Self {
340 let backprop = BackpropGpu::new(&net);
341 let total_params: usize = net.layers.iter().map(|l| l.weights.len()).sum();
342 let adam = if optimizer == OptimizerType::Adam {
343 Some(AdamOptimizer::new(
344 total_params,
345 learning_rate,
346 0.9,
347 0.999,
348 1e-8,
349 ))
350 } else {
351 None
352 };
353 Self {
354 net,
355 backprop,
356 learning_rate,
357 optimizer,
358 adam,
359 }
360 }
361
362 pub fn train_step(&mut self, input: &[f64], target: &[f64]) -> f64 {
369 let pred = self.net.forward_pass(input);
371 let loss = mse_loss(&pred, target);
372
373 let n = pred.len().min(target.len());
375 let loss_grad: Vec<f64> = pred[..n]
376 .iter()
377 .zip(target[..n].iter())
378 .map(|(p, t)| 2.0 * (p - t) / n as f64)
379 .collect();
380
381 self.backprop.backward_pass(&loss_grad);
383
384 match self.optimizer {
386 OptimizerType::Sgd => {
387 for (layer, grads) in self
388 .net
389 .layers
390 .iter_mut()
391 .zip(self.backprop.gradients.iter())
392 {
393 for (w, &g) in layer.weights.iter_mut().zip(grads.iter()) {
394 *w -= self.learning_rate * g;
395 }
396 }
397 }
398 OptimizerType::Adam => {
399 if let Some(adam) = &mut self.adam {
400 let mut all_weights: Vec<f64> = self
402 .net
403 .layers
404 .iter()
405 .flat_map(|l| l.weights.iter().copied())
406 .collect();
407 let all_grads: Vec<f64> = self
408 .backprop
409 .gradients
410 .iter()
411 .flat_map(|g| g.iter().copied())
412 .collect();
413 adam.update(&mut all_weights, &all_grads);
414 let mut offset = 0;
416 for layer in &mut self.net.layers {
417 let len = layer.weights.len();
418 layer
419 .weights
420 .copy_from_slice(&all_weights[offset..offset + len]);
421 offset += len;
422 }
423 }
424 }
425 }
426
427 loss
428 }
429}
430
431#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
442 fn test_relu_positive() {
443 assert!((relu(3.0) - 3.0).abs() < 1e-12);
444 }
445
446 #[test]
447 fn test_relu_negative() {
448 assert!((relu(-5.0)).abs() < 1e-12);
449 }
450
451 #[test]
452 fn test_relu_zero() {
453 assert!((relu(0.0)).abs() < 1e-12);
454 }
455
456 #[test]
457 fn test_sigmoid_zero() {
458 assert!((sigmoid(0.0) - 0.5).abs() < 1e-12);
459 }
460
461 #[test]
462 fn test_sigmoid_large_positive() {
463 assert!((sigmoid(100.0) - 1.0).abs() < 1e-6);
464 }
465
466 #[test]
467 fn test_sigmoid_large_negative() {
468 assert!(sigmoid(-100.0) < 1e-6);
469 }
470
471 #[test]
472 fn test_sigmoid_symmetry() {
473 let x = 2.5;
474 assert!((sigmoid(x) + sigmoid(-x) - 1.0).abs() < 1e-12);
475 }
476
477 #[test]
478 fn test_softmax_sums_to_one() {
479 let x = vec![1.0, 2.0, 3.0, 4.0];
480 let s = softmax(&x);
481 let sum: f64 = s.iter().sum();
482 assert!((sum - 1.0).abs() < 1e-12);
483 }
484
485 #[test]
486 fn test_softmax_monotone() {
487 let x = vec![1.0, 2.0, 3.0];
488 let s = softmax(&x);
489 assert!(s[0] < s[1] && s[1] < s[2]);
490 }
491
492 #[test]
493 fn test_softmax_uniform() {
494 let x = vec![0.0, 0.0, 0.0];
495 let s = softmax(&x);
496 for &v in &s {
497 assert!((v - 1.0 / 3.0).abs() < 1e-12);
498 }
499 }
500
501 #[test]
502 fn test_softmax_empty() {
503 let s = softmax(&[]);
504 assert!(s.is_empty());
505 }
506
507 #[test]
508 fn test_softmax_single() {
509 let s = softmax(&[42.0]);
510 assert!((s[0] - 1.0).abs() < 1e-12);
511 }
512
513 #[test]
514 fn test_softmax_numerical_stability() {
515 let x = vec![1000.0, 1001.0, 1002.0];
517 let s = softmax(&x);
518 let sum: f64 = s.iter().sum();
519 assert!((sum - 1.0).abs() < 1e-10);
520 }
521
522 #[test]
525 fn test_mse_loss_perfect() {
526 let pred = vec![1.0, 2.0, 3.0];
527 assert!((mse_loss(&pred, &pred)).abs() < 1e-12);
528 }
529
530 #[test]
531 fn test_mse_loss_known() {
532 let pred = vec![0.0, 0.0];
534 let target = vec![1.0, 1.0];
535 assert!((mse_loss(&pred, &target) - 1.0).abs() < 1e-12);
536 }
537
538 #[test]
539 fn test_mse_loss_empty() {
540 assert!((mse_loss(&[], &[])).abs() < 1e-12);
541 }
542
543 #[test]
544 fn test_mse_loss_positive() {
545 let pred = vec![1.0, 2.0, 3.0];
546 let target = vec![0.0, 0.0, 0.0];
547 assert!(mse_loss(&pred, &target) > 0.0);
548 }
549
550 #[test]
553 fn test_relu_layer_forward() {
554 let layer = NeuralLayer::new(3, 3, LayerType::ReLU);
555 let out = layer.forward(&[-1.0, 0.0, 2.0]);
556 assert_eq!(out, vec![0.0, 0.0, 2.0]);
557 }
558
559 #[test]
560 fn test_sigmoid_layer_forward() {
561 let layer = NeuralLayer::new(1, 1, LayerType::Sigmoid);
562 let out = layer.forward(&[0.0]);
563 assert!((out[0] - 0.5).abs() < 1e-12);
564 }
565
566 #[test]
567 fn test_tanh_layer_forward() {
568 let layer = NeuralLayer::new(1, 1, LayerType::Tanh);
569 let out = layer.forward(&[0.0]);
570 assert!((out[0]).abs() < 1e-12);
571 }
572
573 #[test]
574 fn test_softmax_layer_forward() {
575 let layer = NeuralLayer::new(3, 3, LayerType::Softmax);
576 let out = layer.forward(&[1.0, 2.0, 3.0]);
577 let sum: f64 = out.iter().sum();
578 assert!((sum - 1.0).abs() < 1e-12);
579 }
580
581 #[test]
582 fn test_dropout_layer_passthrough() {
583 let layer = NeuralLayer::new(4, 4, LayerType::Dropout);
584 let input = vec![1.0, 2.0, 3.0, 4.0];
585 let out = layer.forward(&input);
586 assert_eq!(out, input);
587 }
588
589 #[test]
590 fn test_dense_layer_identity() {
591 let mut layer = NeuralLayer::new(1, 1, LayerType::Dense);
593 layer.weights[0] = 1.0;
594 let out = layer.forward(&[5.0]);
595 assert!((out[0] - 5.0).abs() < 1e-12);
596 }
597
598 #[test]
599 fn test_dense_layer_known_output() {
600 let mut layer = NeuralLayer::new(2, 1, LayerType::Dense);
602 layer.weights = vec![1.0, 2.0];
603 layer.biases = vec![0.5];
604 let out = layer.forward(&[3.0, 4.0]);
606 assert!((out[0] - 11.5).abs() < 1e-12);
607 }
608
609 #[test]
610 fn test_dense_layer_multi_out() {
611 let mut layer = NeuralLayer::new(2, 2, LayerType::Dense);
612 layer.weights = vec![1.0, 0.0, 0.0, 1.0];
615 layer.biases = vec![0.0, 0.0];
616 let out = layer.forward(&[7.0, 3.0]);
617 assert!((out[0] - 7.0).abs() < 1e-12);
618 assert!((out[1] - 3.0).abs() < 1e-12);
619 }
620
621 #[test]
622 fn test_batchnorm_layer_zero_mean() {
623 let mut layer = NeuralLayer::new(4, 4, LayerType::BatchNorm);
624 layer.weights = vec![1.0; 4]; layer.biases = vec![0.0; 4]; let input = vec![1.0, 2.0, 3.0, 4.0];
627 let out = layer.forward(&input);
628 let mean_out: f64 = out.iter().sum::<f64>() / out.len() as f64;
629 assert!(mean_out.abs() < 1e-10);
630 }
631
632 #[test]
635 fn test_empty_net_passthrough() {
636 let net = GpuNeuralNet::new();
637 let input = vec![1.0, 2.0, 3.0];
638 let out = net.forward_pass(&input);
639 assert_eq!(out, input);
640 }
641
642 #[test]
643 fn test_single_relu_net() {
644 let mut net = GpuNeuralNet::new();
645 net.add_layer(NeuralLayer::new(3, 3, LayerType::ReLU));
646 let out = net.forward_pass(&[-1.0, 0.0, 2.0]);
647 assert_eq!(out, vec![0.0, 0.0, 2.0]);
648 }
649
650 #[test]
651 fn test_net_dense_then_relu() {
652 let mut net = GpuNeuralNet::new();
653 let mut dense = NeuralLayer::new(2, 2, LayerType::Dense);
654 dense.weights = vec![1.0, 0.0, 0.0, -1.0];
655 dense.biases = vec![0.0, 0.0];
656 net.add_layer(dense);
657 net.add_layer(NeuralLayer::new(2, 2, LayerType::ReLU));
658 let out = net.forward_pass(&[3.0, 4.0]);
659 assert!((out[0] - 3.0).abs() < 1e-12);
661 assert!((out[1]).abs() < 1e-12);
662 }
663
664 #[test]
665 fn test_batch_forward() {
666 let mut net = GpuNeuralNet::new();
667 net.add_layer(NeuralLayer::new(2, 2, LayerType::ReLU));
668 let inputs = vec![vec![-1.0, 2.0], vec![3.0, -4.0]];
669 let outs = net.batch_forward(&inputs);
670 assert_eq!(outs.len(), 2);
671 assert_eq!(outs[0], vec![0.0, 2.0]);
672 assert_eq!(outs[1], vec![3.0, 0.0]);
673 }
674
675 #[test]
676 fn test_net_default() {
677 let net = GpuNeuralNet::default();
678 assert!(net.layers.is_empty());
679 }
680
681 #[test]
684 fn test_backprop_gradient_shape() {
685 let mut net = GpuNeuralNet::new();
686 net.add_layer(NeuralLayer::new(3, 2, LayerType::Dense));
687 let bp = BackpropGpu::new(&net);
688 assert_eq!(bp.gradients.len(), 1);
689 assert_eq!(bp.gradients[0].len(), 6); }
691
692 #[test]
693 fn test_backprop_backward_sets_gradients() {
694 let mut net = GpuNeuralNet::new();
695 net.add_layer(NeuralLayer::new(2, 2, LayerType::Dense));
696 let mut bp = BackpropGpu::new(&net);
697 bp.backward_pass(&[1.0]);
698 for &g in &bp.gradients[0] {
699 assert!((g - 1.0).abs() < 1e-12);
700 }
701 }
702
703 #[test]
704 fn test_backprop_zero_loss_grad() {
705 let mut net = GpuNeuralNet::new();
706 net.add_layer(NeuralLayer::new(2, 2, LayerType::Dense));
707 let mut bp = BackpropGpu::new(&net);
708 bp.backward_pass(&[0.0]);
709 for &g in &bp.gradients[0] {
710 assert!((g).abs() < 1e-12);
711 }
712 }
713
714 #[test]
717 fn test_adam_decreases_loss() {
718 let mut params = vec![1.0, -1.0, 2.0];
719 let mut adam = AdamOptimizer::new(3, 0.1, 0.9, 0.999, 1e-8);
720 for _ in 0..500 {
722 let grads: Vec<f64> = params.iter().map(|&p| 2.0 * p).collect();
723 adam.update(&mut params, &grads);
724 }
725 for &p in ¶ms {
726 assert!(p.abs() < 0.1, "param={p}");
727 }
728 }
729
730 #[test]
731 fn test_adam_timestep_increments() {
732 let mut adam = AdamOptimizer::new(2, 0.01, 0.9, 0.999, 1e-8);
733 let mut params = vec![1.0, 1.0];
734 let grads = vec![0.1, 0.1];
735 adam.update(&mut params, &grads);
736 assert_eq!(adam.t, 1);
737 adam.update(&mut params, &grads);
738 assert_eq!(adam.t, 2);
739 }
740
741 #[test]
742 fn test_adam_moment_buffers_update() {
743 let mut adam = AdamOptimizer::new(1, 0.01, 0.9, 0.999, 1e-8);
744 let mut params = vec![1.0];
745 adam.update(&mut params, &[0.5]);
746 assert!((adam.m[0] - 0.1 * 0.5).abs() < 1e-12); assert!(adam.v[0] > 0.0);
748 }
749
750 #[test]
753 fn test_trainer_sgd_reduces_loss() {
754 let mut net = GpuNeuralNet::new();
755 let mut layer = NeuralLayer::new(1, 1, LayerType::Dense);
756 layer.weights = vec![2.0];
757 layer.biases = vec![0.0];
758 net.add_layer(layer);
759 let mut trainer = GpuTrainer::new(net, 0.1, OptimizerType::Sgd);
760 let loss_before = mse_loss(&trainer.net.forward_pass(&[1.0]), &[1.0]);
761 let loss_after = trainer.train_step(&[1.0], &[1.0]);
762 let _ = loss_before;
765 assert!(loss_after >= 0.0);
766 }
767
768 #[test]
769 fn test_trainer_adam_train_step() {
770 let mut net = GpuNeuralNet::new();
771 let mut layer = NeuralLayer::new(1, 1, LayerType::Dense);
772 layer.weights = vec![0.0];
773 layer.biases = vec![0.0];
774 net.add_layer(layer);
775 let mut trainer = GpuTrainer::new(net, 0.01, OptimizerType::Adam);
776 let loss = trainer.train_step(&[1.0], &[1.0]);
777 assert!(loss >= 0.0);
778 }
779
780 #[test]
781 fn test_conv1d_layer_forward() {
782 let mut layer = NeuralLayer::new(3, 1, LayerType::Conv1D);
783 layer.weights = vec![1.0, 1.0, 1.0];
784 layer.biases = vec![0.0];
785 let out = layer.forward(&[1.0, 2.0, 3.0]);
786 assert!((out[0] - 6.0).abs() < 1e-12);
787 }
788
789 #[test]
790 fn test_softmax_net_output_probabilities() {
791 let mut net = GpuNeuralNet::new();
792 net.add_layer(NeuralLayer::new(3, 3, LayerType::Softmax));
793 let out = net.forward_pass(&[0.0, 1.0, 2.0]);
794 let sum: f64 = out.iter().sum();
795 assert!((sum - 1.0).abs() < 1e-12);
796 for &p in &out {
797 assert!((0.0..=1.0).contains(&p));
798 }
799 }
800
801 #[test]
802 fn test_mse_symmetric() {
803 let a = vec![1.0, 2.0];
804 let b = vec![3.0, 4.0];
805 assert!((mse_loss(&a, &b) - mse_loss(&b, &a)).abs() < 1e-12);
806 }
807
808 #[test]
809 fn test_layer_type_debug() {
810 let lt = LayerType::Dense;
811 let s = format!("{lt:?}");
812 assert!(s.contains("Dense"));
813 }
814
815 #[test]
816 fn test_optimizer_type_eq() {
817 assert_eq!(OptimizerType::Sgd, OptimizerType::Sgd);
818 assert_ne!(OptimizerType::Sgd, OptimizerType::Adam);
819 }
820
821 #[test]
822 fn test_sigmoid_vs_exp() {
823 let x = 1.0_f64;
825 assert!((sigmoid(x) - 1.0 / (1.0 + (-x).exp())).abs() < 1e-12);
826 }
827}