1use crate::activation_functions::ActivationFunction;
2use crate::layer::Layer;
3use crate::ModelError;
4use nalgebra::DVector;
5use rand::{rng, Rng};
6use std::fmt;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84#[serde(into = "NetworkData", try_from = "NetworkData")]
85pub struct NeuralNetwork<const IN: usize, const OUT: usize> {
86 layers: Vec<Layer>,
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub(crate) struct NetworkData {
96 pub(crate) layers: Vec<LayerData>,
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub(crate) struct LayerData {
102 pub(crate) activation: ActivationFunction,
103 pub(crate) weights: Vec<Vec<f64>>,
105 pub(crate) biases: Vec<f64>,
106}
107
108impl<const IN: usize, const OUT: usize> NeuralNetwork<IN, OUT> {
109 pub fn new(hidden: &[usize]) -> Self {
133 Self::new_with_rng(hidden, &mut rng())
134 }
135
136 pub fn new_with_rng<R: Rng>(hidden: &[usize], rng: &mut R) -> Self {
145 Self::build(hidden, |neurons, inputs| {
146 Layer::random(neurons, inputs, ActivationFunction::default(), rng)
147 })
148 }
149
150 pub fn from_parameters(hidden: &[usize], parameters: &[f64]) -> Self {
175 let mut network = Self::build(hidden, |neurons, inputs| {
176 Layer::zeros(neurons, inputs, ActivationFunction::default())
177 });
178 network.set_parameters(parameters);
179 network
180 }
181
182 fn build(hidden: &[usize], mut make: impl FnMut(usize, usize) -> Layer) -> Self {
184 let layers = Self::layer_shapes(hidden)
185 .map(|(neurons, inputs)| make(neurons, inputs))
186 .collect();
187
188 Self { layers }
189 }
190
191 fn layer_shapes(hidden: &[usize]) -> impl Iterator<Item = (usize, usize)> + '_ {
193 const { assert!(IN > 0, "a network needs at least one input neuron") };
194 const { assert!(OUT > 0, "a network needs at least one output neuron") };
195 assert!(
196 hidden.iter().all(|&size| size > 0),
197 "every hidden layer must have at least one neuron, got {hidden:?}"
198 );
199
200 let inputs = std::iter::once(IN).chain(hidden.iter().copied());
201 let neurons = hidden.iter().copied().chain(std::iter::once(OUT));
202 neurons.zip(inputs)
203 }
204
205 pub fn feed_forward(&self, inputs: &[f64; IN]) -> [f64; OUT] {
222 let mut outputs = DVector::from_row_slice(inputs);
223
224 for layer in &self.layers {
225 outputs = layer.forward(&outputs);
226 }
227
228 <[f64; OUT]>::try_from(outputs.as_slice())
231 .expect("output layer width should match the OUT type parameter")
232 }
233
234 pub fn parameter_count(&self) -> usize {
237 self.layers.iter().map(Layer::parameter_count).sum()
238 }
239
240 pub fn parameter_count_for(hidden: &[usize]) -> usize {
255 Self::layer_shapes(hidden)
256 .map(|(neurons, inputs)| Layer::parameter_count_for(neurons, inputs))
257 .sum()
258 }
259
260 pub fn parameters(&self) -> Vec<f64> {
263 let mut parameters = Vec::with_capacity(self.parameter_count());
264 for layer in &self.layers {
265 layer.extend_parameters(&mut parameters);
266 }
267 parameters
268 }
269
270 pub fn set_parameters(&mut self, parameters: &[f64]) {
278 let expected = self.parameter_count();
279 assert_eq!(
280 parameters.len(),
281 expected,
282 "Incompatible parameter count: expected {expected} parameters, got {}",
283 parameters.len()
284 );
285
286 let mut rest = parameters;
287 for layer in &mut self.layers {
288 rest = layer.take_parameters(rest);
289 }
290 }
291
292 pub fn set_layer_weights<R: AsRef<[f64]>>(&mut self, layer: usize, weights: &[R]) {
319 self.layer_mut(layer).set_weights(weights);
320 }
321
322 pub fn layer_weights(&self, layer: usize) -> Vec<Vec<f64>> {
329 self.layer(layer).weight_rows()
330 }
331
332 pub fn set_layer_biases(&mut self, layer: usize, biases: &[f64]) {
339 self.layer_mut(layer).set_biases(biases);
340 }
341
342 pub fn layer_biases(&self, layer: usize) -> &[f64] {
348 self.layer(layer).biases().as_slice()
349 }
350
351 pub fn set_weight(&mut self, layer: usize, neuron: usize, input: usize, weight: f64) {
354 self.layer_mut(layer).set_weight(neuron, input, weight);
355 }
356
357 pub fn get_weight(&self, layer: usize, neuron: usize, input: usize) -> f64 {
360 self.layer(layer).weights()[(neuron, input)]
361 }
362
363 pub fn set_bias(&mut self, layer: usize, neuron: usize, bias: f64) {
366 self.layer_mut(layer).set_bias(neuron, bias);
367 }
368
369 pub fn get_bias(&self, layer: usize, neuron: usize) -> f64 {
372 self.layer(layer).biases()[neuron]
373 }
374
375 fn layer(&self, layer: usize) -> &Layer {
377 if layer == 0 {
378 panic!("Invalid layer index");
379 }
380 &self.layers[layer - 1]
381 }
382
383 fn layer_mut(&mut self, layer: usize) -> &mut Layer {
384 if layer == 0 {
385 panic!("Invalid layer index");
386 }
387 &mut self.layers[layer - 1]
388 }
389
390 pub fn num_layers(&self) -> usize {
392 self.layers.len() + 1
393 }
394
395 pub fn layer_size(&self, layer: usize) -> usize {
397 if layer == 0 {
398 return IN;
399 }
400 self.layer(layer).size()
401 }
402
403 pub fn hidden_layer_sizes(&self) -> Vec<usize> {
414 let hidden = &self.layers[..self.layers.len() - 1];
415 hidden.iter().map(Layer::size).collect()
416 }
417
418 pub fn layer_activation(&self, layer: usize) -> ActivationFunction {
425 self.layer(layer).activation()
426 }
427
428 pub fn set_layer_activation(&mut self, layer: usize, activation_function: ActivationFunction) {
435 self.layer_mut(layer).set_activation(activation_function);
436 }
437
438 pub fn set_activation_function(&mut self, activation_function: ActivationFunction) {
455 for layer in &mut self.layers {
456 layer.set_activation(activation_function);
457 }
458 }
459
460 pub fn set_output_activation(&mut self, activation_function: ActivationFunction) {
462 self.set_layer_activation(self.num_layers() - 1, activation_function);
463 }
464
465 pub fn output_activation(&self) -> ActivationFunction {
467 self.layer_activation(self.num_layers() - 1)
468 }
469
470 pub fn print(&self) {
471 for layer in &self.layers {
472 println!("{:?} {} {}", layer.activation(), layer.weights(), layer.biases());
473 }
474 }
475}
476
477impl<const IN: usize, const OUT: usize> From<NeuralNetwork<IN, OUT>> for NetworkData {
478 fn from(network: NeuralNetwork<IN, OUT>) -> Self {
479 NetworkData::from(&network)
480 }
481}
482
483impl<const IN: usize, const OUT: usize> From<&NeuralNetwork<IN, OUT>> for NetworkData {
484 fn from(network: &NeuralNetwork<IN, OUT>) -> Self {
485 let layers = network
486 .layers
487 .iter()
488 .map(|layer| LayerData {
489 activation: layer.activation(),
490 weights: layer.weight_rows(),
491 biases: layer.biases().iter().copied().collect(),
492 })
493 .collect();
494
495 NetworkData { layers }
496 }
497}
498
499impl<const IN: usize, const OUT: usize> TryFrom<NetworkData> for NeuralNetwork<IN, OUT> {
500 type Error = ModelError;
501
502 fn try_from(data: NetworkData) -> Result<Self, Self::Error> {
505 let first = data.layers.first().ok_or(ModelError::EmptyNetwork)?;
506 if let Some(row) = first.weights.first() {
507 if row.len() != IN {
508 return Err(ModelError::DimensionMismatch {
509 end: "input",
510 expected: IN,
511 found: row.len(),
512 });
513 }
514 }
515
516 let mut inputs = IN;
517 let mut layers = Vec::with_capacity(data.layers.len());
518 for (index, layer) in data.layers.iter().enumerate() {
519 let layer = Layer::from_parts(&layer.weights, &layer.biases, layer.activation, inputs)
520 .map_err(|reason| ModelError::InconsistentLayer {
521 layer: index + 1,
522 reason,
523 })?;
524 inputs = layer.size();
525 layers.push(layer);
526 }
527
528 if inputs != OUT {
529 return Err(ModelError::DimensionMismatch {
530 end: "output",
531 expected: OUT,
532 found: inputs,
533 });
534 }
535
536 Ok(Self { layers })
537 }
538}
539
540impl<const IN: usize, const OUT: usize> fmt::Display for NeuralNetwork<IN, OUT> {
541 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542 writeln!(f, "Neural Network")?;
543 writeln!(f)?;
544 writeln!(f, "Input Layer Size: {IN}")?;
545 writeln!(f)?;
546 for (index, layer) in self.layers.iter().enumerate() {
547 writeln!(f, "Layer {}: {}", index + 1, layer)?;
548 }
549 Ok(())
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::activation_functions::{binary_step, identity, relu, sigmoid, tanh};
557
558 const EPSILON: f64 = 1e-12;
559
560 fn assert_all_close(actual: &[f64], expected: &[f64]) {
561 assert_eq!(actual.len(), expected.len(), "length mismatch");
562 for (i, (a, e)) in actual.iter().zip(expected).enumerate() {
563 assert!((a - e).abs() < EPSILON, "at index {i}: expected {e}, got {a}");
564 }
565 }
566
567 fn fixed_network() -> NeuralNetwork<2, 1> {
569 let mut nn = NeuralNetwork::<2, 1>::new(&[]);
570 nn.set_layer_weights(1, &[[0.5, -0.25]]);
571 nn.set_layer_biases(1, &[0.1]);
572 nn
573 }
574
575 #[test]
576 fn new_reports_layer_count_and_sizes() {
577 let nn = NeuralNetwork::<3, 2>::new(&[5]);
578
579 assert_eq!(nn.num_layers(), 3);
580 assert_eq!(nn.layer_size(0), 3);
581 assert_eq!(nn.layer_size(1), 5);
582 assert_eq!(nn.layer_size(2), 2);
583 }
584
585 #[test]
588 fn new_without_hidden_layers_wires_input_straight_to_output() {
589 let nn = NeuralNetwork::<3, 2>::new(&[]);
590
591 assert_eq!(nn.num_layers(), 2);
592 assert_eq!(nn.layer_size(0), 3);
593 assert_eq!(nn.layer_size(1), 2);
594 }
595
596 #[test]
597 #[should_panic(expected = "at least one neuron")]
598 fn new_rejects_a_zero_sized_hidden_layer() {
599 NeuralNetwork::<2, 1>::new(&[0]);
600 }
601
602 #[test]
603 fn new_with_rng_is_reproducible_for_the_same_seed() {
604 use rand::SeedableRng;
605
606 let mut first_rng = rand::rngs::StdRng::seed_from_u64(42);
607 let mut second_rng = rand::rngs::StdRng::seed_from_u64(42);
608
609 let first = NeuralNetwork::<3, 2>::new_with_rng(&[4], &mut first_rng);
610 let second = NeuralNetwork::<3, 2>::new_with_rng(&[4], &mut second_rng);
611
612 for layer in 1..first.num_layers() {
613 for neuron in 0..first.layer_size(layer) {
614 for input in 0..first.layer_size(layer - 1) {
615 assert_eq!(
616 first.get_weight(layer, neuron, input),
617 second.get_weight(layer, neuron, input),
618 "weight differs at layer {layer}, neuron {neuron}, input {input}"
619 );
620 }
621 }
622 }
623 }
624
625 #[test]
626 fn feed_forward_applies_weights_bias_and_activation() {
627 let nn = fixed_network();
628
629 let output = nn.feed_forward(&[1.0, 2.0]);
631
632 assert_all_close(&output, &[sigmoid(0.1)]);
633 }
634
635 #[test]
636 fn feed_forward_defaults_to_sigmoid() {
637 let nn = fixed_network();
638
639 assert_eq!(nn.layer_activation(1), ActivationFunction::Sigmoid);
640 assert_all_close(&nn.feed_forward(&[1.0, 2.0]), &[sigmoid(0.1)]);
641 }
642
643 #[test]
646 fn feed_forward_honours_every_activation_function() {
647 let cases = [
648 (ActivationFunction::Sigmoid, sigmoid as fn(f64) -> f64),
649 (ActivationFunction::Tanh, tanh),
650 (ActivationFunction::ReLU, relu),
651 (ActivationFunction::BinaryStep, binary_step),
652 (ActivationFunction::Identity, identity),
653 ];
654
655 for (variant, expected) in cases {
656 let mut nn = fixed_network();
657 nn.set_activation_function(variant);
658
659 assert_eq!(nn.layer_activation(1), variant);
660 assert_all_close(&nn.feed_forward(&[1.0, 2.0]), &[expected(0.1)]);
661 }
662 }
663
664 #[test]
666 fn binary_step_does_not_panic() {
667 let mut nn = fixed_network();
668 nn.set_activation_function(ActivationFunction::BinaryStep);
669
670 assert_all_close(&nn.feed_forward(&[1.0, 2.0]), &[1.0]);
671 assert_all_close(&nn.feed_forward(&[-1.0, 2.0]), &[0.0]);
672 }
673
674 #[test]
675 fn set_and_get_weight_round_trip() {
676 let mut nn = NeuralNetwork::<2, 2>::new(&[]);
677 nn.set_weight(1, 1, 0, 0.75);
678
679 assert_eq!(nn.get_weight(1, 1, 0), 0.75);
680 }
681
682 #[test]
683 #[should_panic(expected = "Invalid layer index")]
684 fn set_layer_weights_rejects_layer_zero() {
685 let mut nn = NeuralNetwork::<2, 1>::new(&[]);
686 nn.set_layer_weights(0, &[[0.5, 0.5]]);
687 }
688
689 #[test]
690 #[should_panic(expected = "Incompatible weights matrix size")]
691 fn set_layer_weights_rejects_a_mismatched_matrix() {
692 let mut nn = NeuralNetwork::<2, 1>::new(&[]);
693 nn.set_layer_weights(1, &[[0.5, 0.5, 0.5]]);
694 }
695
696 #[test]
697 #[should_panic(expected = "Incompatible biases vector size")]
698 fn set_layer_biases_rejects_a_mismatched_vector() {
699 let mut nn = NeuralNetwork::<2, 1>::new(&[]);
700 nn.set_layer_biases(1, &[0.1, 0.2]);
701 }
702
703 #[test]
705 fn display_names_the_activation_function() {
706 let mut nn = fixed_network();
707 nn.set_activation_function(ActivationFunction::ReLU);
708
709 let rendered = nn.to_string();
710
711 assert!(
712 rendered.contains("Activation Function: ReLU"),
713 "unexpected output:\n{rendered}"
714 );
715 assert!(
716 !rendered.contains("0x"),
717 "output leaked a pointer address:\n{rendered}"
718 );
719 }
720
721 #[test]
722 fn display_reports_the_input_layer_size() {
723 let nn = NeuralNetwork::<4, 1>::new(&[2]);
724
725 assert!(nn.to_string().contains("Input Layer Size: 4"));
726 }
727
728 #[test]
729 fn layer_weights_and_biases_round_trip_in_row_major_order() {
730 let mut nn = NeuralNetwork::<2, 3>::new(&[]);
731 let weights = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]];
732
733 nn.set_layer_weights(1, &weights);
734 nn.set_layer_biases(1, &[0.7, 0.8, 0.9]);
735
736 assert_eq!(nn.layer_weights(1), weights.map(Vec::from).to_vec());
737 assert_eq!(nn.layer_biases(1), &[0.7, 0.8, 0.9]);
738 assert_eq!(nn.get_weight(1, 2, 0), 0.5);
740 }
741
742 #[test]
743 fn set_layer_weights_accepts_weights_built_at_runtime() {
744 let mut nn = NeuralNetwork::<2, 1>::new(&[]);
745 let weights: Vec<Vec<f64>> = vec![vec![0.5, -0.25]];
746
747 nn.set_layer_weights(1, &weights);
748
749 assert_eq!(nn.layer_weights(1), weights);
750 }
751
752 #[test]
753 #[should_panic(expected = "Incompatible weights matrix size")]
754 fn set_layer_weights_rejects_ragged_rows() {
755 let mut nn = NeuralNetwork::<2, 2>::new(&[]);
756 let ragged: Vec<Vec<f64>> = vec![vec![0.1, 0.2], vec![0.3]];
757
758 nn.set_layer_weights(1, &ragged);
759 }
760
761 #[test]
762 #[should_panic(expected = "Incompatible weights matrix size")]
763 fn set_layer_weights_rejects_the_wrong_number_of_rows() {
764 let mut nn = NeuralNetwork::<2, 2>::new(&[]);
765 nn.set_layer_weights(1, &[[0.1, 0.2]]);
766 }
767
768 #[test]
769 fn set_and_get_bias_round_trip() {
770 let mut nn = NeuralNetwork::<2, 2>::new(&[]);
771 nn.set_bias(1, 1, -0.3);
772
773 assert_eq!(nn.get_bias(1, 1), -0.3);
774 assert_eq!(nn.layer_biases(1), &[0.0, -0.3]);
775 }
776
777 #[test]
778 #[should_panic(expected = "Invalid layer index")]
779 fn layer_biases_rejects_layer_zero() {
780 let nn = NeuralNetwork::<2, 1>::new(&[]);
781 nn.layer_biases(0);
782 }
783
784 fn numbered_network() -> NeuralNetwork<2, 1> {
786 let mut nn = NeuralNetwork::<2, 1>::new(&[2]);
787 nn.set_layer_weights(1, &[[1.0, 2.0],
788 [3.0, 4.0]]);
789 nn.set_layer_biases(1, &[5.0, 6.0]);
790 nn.set_layer_weights(2, &[[7.0, 8.0]]);
791 nn.set_layer_biases(2, &[9.0]);
792 nn
793 }
794
795 #[test]
796 fn hidden_and_output_layers_can_use_different_activations() {
797 let mut nn = NeuralNetwork::<1, 1>::new(&[1]);
798 nn.set_layer_weights(1, &[[1.0]]);
799 nn.set_layer_weights(2, &[[1.0]]);
800 nn.set_activation_function(ActivationFunction::ReLU);
801 nn.set_output_activation(ActivationFunction::Tanh);
802
803 assert_all_close(&nn.feed_forward(&[-2.0]), &[0.0]);
805 assert_all_close(&nn.feed_forward(&[2.0]), &[tanh(2.0)]);
806 assert_eq!(nn.layer_activation(1), ActivationFunction::ReLU);
807 assert_eq!(nn.output_activation(), ActivationFunction::Tanh);
808 }
809
810 #[test]
811 fn set_layer_activation_changes_only_that_layer() {
812 let mut nn = NeuralNetwork::<2, 1>::new(&[3, 3]);
813 nn.set_layer_activation(2, ActivationFunction::Identity);
814
815 assert_eq!(nn.layer_activation(1), ActivationFunction::Sigmoid);
816 assert_eq!(nn.layer_activation(2), ActivationFunction::Identity);
817 assert_eq!(nn.layer_activation(3), ActivationFunction::Sigmoid);
818 }
819
820 #[test]
821 #[should_panic(expected = "Invalid layer index")]
822 fn the_input_layer_has_no_activation() {
823 NeuralNetwork::<2, 1>::new(&[]).layer_activation(0);
824 }
825
826 #[test]
827 fn parameters_follow_the_documented_order() {
828 let nn = numbered_network();
829
830 assert_eq!(
831 nn.parameters(),
832 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
833 );
834 }
835
836 #[test]
837 fn parameter_count_matches_the_shape() {
838 let nn = NeuralNetwork::<10, 3>::new(&[8]);
839
840 assert_eq!(nn.parameter_count(), 11 * 8 + 9 * 3);
841 assert_eq!(nn.parameters().len(), nn.parameter_count());
842 assert_eq!(NeuralNetwork::<10, 3>::parameter_count_for(&[8]), nn.parameter_count());
843 assert_eq!(NeuralNetwork::<10, 3>::parameter_count_for(&[]), 11 * 3);
844 }
845
846 #[test]
847 fn set_parameters_writes_the_documented_order() {
848 let mut nn = NeuralNetwork::<2, 1>::new(&[2]);
849 nn.set_parameters(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
850
851 assert_eq!(nn, numbered_network());
852 assert_eq!(nn.get_weight(1, 1, 0), 3.0);
853 assert_eq!(nn.get_bias(2, 0), 9.0);
854 }
855
856 #[test]
857 fn set_parameters_keeps_activation_functions() {
858 let mut nn = numbered_network();
859 nn.set_output_activation(ActivationFunction::Tanh);
860
861 nn.set_parameters(&[0.0; 9]);
862
863 assert_eq!(nn.output_activation(), ActivationFunction::Tanh);
864 }
865
866 #[test]
867 fn parameters_round_trip_through_from_parameters() {
868 use rand::SeedableRng;
869 let mut rng = rand::rngs::StdRng::seed_from_u64(7);
870 let original = NeuralNetwork::<3, 2>::new_with_rng(&[4, 5], &mut rng);
871
872 let rebuilt = NeuralNetwork::<3, 2>::from_parameters(
873 &original.hidden_layer_sizes(),
874 &original.parameters(),
875 );
876
877 assert_eq!(rebuilt, original);
878 assert_all_close(
879 &rebuilt.feed_forward(&[0.1, -0.2, 0.3]),
880 &original.feed_forward(&[0.1, -0.2, 0.3]),
881 );
882 }
883
884 #[test]
885 #[should_panic(expected = "expected 9 parameters, got 8")]
886 fn set_parameters_rejects_a_short_list() {
887 numbered_network().set_parameters(&[0.0; 8]);
888 }
889
890 #[test]
891 #[should_panic(expected = "expected 9 parameters, got 10")]
892 fn from_parameters_rejects_a_long_list() {
893 NeuralNetwork::<2, 1>::from_parameters(&[2], &[0.0; 10]);
894 }
895
896 #[test]
897 #[should_panic(expected = "at least one neuron")]
898 fn parameter_count_for_rejects_a_zero_sized_hidden_layer() {
899 NeuralNetwork::<2, 1>::parameter_count_for(&[3, 0]);
900 }
901
902 #[test]
903 fn hidden_layer_sizes_lists_only_hidden_layers() {
904 assert_eq!(NeuralNetwork::<2, 1>::new(&[]).hidden_layer_sizes(), Vec::<usize>::new());
905 assert_eq!(NeuralNetwork::<2, 1>::new(&[5, 3]).hidden_layer_sizes(), vec![5, 3]);
906 }
907
908 #[test]
910 fn networks_can_be_shared_across_threads() {
911 fn assert_send_sync<T: Send + Sync>() {}
912 assert_send_sync::<NeuralNetwork<10, 3>>();
913 }
914}