Skip to main content

topos/facade/neural/
mlp.rs

1use static_assertions::assert_impl_all;
2
3use crate::{Element, Shape, Symbol, Tape, Tensor, Value};
4
5use super::{Activation, Linear, Module, Segment, Visitor};
6
7// Entry-time thread-safety contract; the anchor rationale is documented
8// in `network.rs`.
9assert_impl_all!(Mlp<f64>: Send, Sync);
10
11/// A multilayer perceptron: affine stages chained by topology, the
12/// convenience constructor over [`Linear`] and [`Activation`].
13///
14/// A topology such as `[3, 4, 4, 1]` defines three stages that map a
15/// `[batch, 3]` input to a `[batch, 1]` output. Hidden stages apply
16/// the caller's [`Activation`] after their affine transform; the
17/// output stage is affine alone. The contained stages retain
18/// parameter [`Symbol`]s, so the perceptron records in each
19/// compatible generation.
20#[derive(Debug, Clone)]
21pub struct Mlp<E> {
22    stages: Vec<Linear<E>>,
23    activation: Activation,
24}
25
26impl<E: Element> Mlp<E> {
27    /// Allocates the perceptron's stages on `tape` and returns it.
28    ///
29    /// `sizes` lists the value widths from the input width to the
30    /// output width. `activation` is applied after every hidden
31    /// stage's affine transform — caller-owned, like every
32    /// hyperparameter, with no default. `initializer` produces the
33    /// initial payload for each parameter from its shape — `[inputs,
34    /// outputs]` weights and `[outputs]` biases, stage by stage. The
35    /// initializer is responsible for returning payloads with the
36    /// requested shapes, and callers control details such as fan-in
37    /// scaling, randomness, and symmetry breaking.
38    ///
39    /// # Panics
40    /// Panics if `sizes` has fewer than two entries. It also propagates
41    /// [`Linear::new`] validation failures if initialized weights and
42    /// biases do not form valid parameter shapes.
43    pub fn new(
44        tape: &Tape<E>,
45        sizes: &[usize],
46        activation: Activation,
47        mut initializer: impl FnMut(&Shape) -> Tensor<E>,
48    ) -> Self {
49        assert!(
50            sizes.len() >= 2,
51            "an MLP topology needs an input and an output width"
52        );
53        let stages = sizes
54            .windows(2)
55            .map(|pair| {
56                let weights = initializer(&Shape::new([pair[0], pair[1]]));
57                let bias = initializer(&Shape::new([pair[1]]));
58                Linear::new(tape, weights, bias)
59            })
60            .collect();
61        Self { stages, activation }
62    }
63
64    /// Returns the symbols of all parameters, stage by stage: each
65    /// stage's weights, then its bias.
66    pub fn parameters(&self) -> impl Iterator<Item = Symbol> + '_ {
67        super::parameters(self).into_iter()
68    }
69}
70
71impl<E: Element> Module<E> for Mlp<E> {
72    /// Records the perceptron's expression over the `[batch, inputs]`
73    /// value `input` and returns the `[batch, outputs]` output value.
74    ///
75    /// # Panics
76    /// Panics if the parameters or `input` are not allocated on the
77    /// input's tape, or if `input` and the initialized stage shapes
78    /// are incompatible.
79    fn express<'tape>(&self, input: Value<'tape, E>) -> Value<'tape, E> {
80        let last = self.stages.len() - 1;
81        self.stages
82            .iter()
83            .enumerate()
84            .fold(input, |value, (index, stage)| {
85                let affine = stage.express(value);
86                if index == last {
87                    affine
88                } else {
89                    self.activation.express(affine)
90                }
91            })
92    }
93
94    fn visit(&self, visitor: &mut dyn Visitor) {
95        for (index, stage) in self.stages.iter().enumerate() {
96            visitor.enter(Segment::Index(index));
97            stage.visit(visitor);
98            visitor.leave();
99        }
100    }
101}
102
103#[cfg(test)]
104#[path = "tests/mlp_tests.rs"]
105mod tests;