1use crate::autodiff::{Tape, Var};
16
17pub struct Mlp {
19 sizes: Vec<usize>,
20 params: Vec<f64>,
21 m: Vec<f64>,
23 v: Vec<f64>,
24 t: u64,
25}
26
27fn splitmix64(state: &mut u64) -> u64 {
28 *state = state.wrapping_add(0x9E3779B97F4A7C15);
29 let mut z = *state;
30 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
31 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
32 z ^ (z >> 31)
33}
34
35impl Mlp {
36 pub fn new(sizes: &[usize], seed: u64) -> Self {
39 assert!(sizes.len() >= 2, "need at least an input and an output layer");
40 let mut state = seed ^ 0xA5A5_5A5A_1234_5678;
41 let mut params = Vec::new();
42 for l in 0..sizes.len() - 1 {
43 let (ind, outd) = (sizes[l], sizes[l + 1]);
44 let r = (6.0 / (ind + outd) as f64).sqrt(); for _ in 0..ind * outd {
46 let u = (splitmix64(&mut state) as f64 / u64::MAX as f64) * 2.0 - 1.0;
47 params.push(u * r);
48 }
49 params.extend(std::iter::repeat_n(0.0, outd)); }
51 let n = params.len();
52 Mlp { sizes: sizes.to_vec(), params, m: vec![0.0; n], v: vec![0.0; n], t: 0 }
53 }
54
55 pub fn n_params(&self) -> usize {
57 self.params.len()
58 }
59
60 pub fn forward(&self, x: &[f64]) -> Vec<f64> {
62 let mut a = x.to_vec();
63 let mut off = 0;
64 let layers = self.sizes.len() - 1;
65 for l in 0..layers {
66 let (ind, outd) = (self.sizes[l], self.sizes[l + 1]);
67 let mut z = vec![0.0; outd];
68 for (o, zo) in z.iter_mut().enumerate() {
69 let mut s = self.params[off + ind * outd + o]; for (i, &ai) in a.iter().enumerate() {
71 s += self.params[off + o * ind + i] * ai;
72 }
73 *zo = if l + 1 < layers { s.tanh() } else { s };
74 }
75 off += ind * outd + outd;
76 a = z;
77 }
78 a
79 }
80
81 fn loss_and_grad(&self, xs: &[Vec<f64>], ys: &[Vec<f64>]) -> (f64, Vec<f64>) {
84 let tape = Tape::new();
85 let pv: Vec<Var> = self.params.iter().map(|&p| tape.var(p)).collect();
86 let layers = self.sizes.len() - 1;
87 let mut loss = tape.constant(0.0);
88 for (x, y) in xs.iter().zip(ys.iter()) {
89 let mut a: Vec<Var> = x.iter().map(|&xi| tape.constant(xi)).collect();
90 let mut off = 0;
91 for l in 0..layers {
92 let (ind, outd) = (self.sizes[l], self.sizes[l + 1]);
93 let mut z = Vec::with_capacity(outd);
94 for o in 0..outd {
95 let mut s = pv[off + ind * outd + o]; for i in 0..ind {
97 s = s + pv[off + o * ind + i] * a[i];
98 }
99 z.push(if l + 1 < layers { s.tanh() } else { s });
100 }
101 off += ind * outd + outd;
102 a = z;
103 }
104 for (o, out) in a.iter().enumerate() {
105 let e = *out - y[o];
106 loss = loss + e * e;
107 }
108 }
109 let n = (xs.len() * ys[0].len()) as f64;
110 let loss = loss * (1.0 / n);
111 let g = loss.backward();
112 let grad: Vec<f64> = pv.iter().map(|&p| g.wrt(p)).collect();
113 (loss.value(), grad)
114 }
115
116 pub fn train_step(&mut self, xs: &[Vec<f64>], ys: &[Vec<f64>], lr: f64) -> f64 {
118 let (loss, grad) = self.loss_and_grad(xs, ys);
119 self.t += 1;
120 let (b1, b2, eps) = (0.9_f64, 0.999_f64, 1e-8);
121 let bc1 = 1.0 - b1.powi(self.t as i32);
122 let bc2 = 1.0 - b2.powi(self.t as i32);
123 for (i, &gi) in grad.iter().enumerate() {
124 self.m[i] = b1 * self.m[i] + (1.0 - b1) * gi;
125 self.v[i] = b2 * self.v[i] + (1.0 - b2) * gi * gi;
126 let mhat = self.m[i] / bc1;
127 let vhat = self.v[i] / bc2;
128 self.params[i] -= lr * mhat / (vhat.sqrt() + eps);
129 }
130 loss
131 }
132
133 pub fn train(&mut self, xs: &[Vec<f64>], ys: &[Vec<f64>], epochs: usize, lr: f64) -> f64 {
135 let mut mse = f64::INFINITY;
136 for _ in 0..epochs {
137 mse = self.train_step(xs, ys, lr);
138 }
139 self.loss_and_grad(xs, ys).0.min(mse)
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn an_mlp_approximates_a_nonlinear_function() {
150 let xs: Vec<Vec<f64>> = (0..40).map(|i| vec![-1.0 + 2.0 * i as f64 / 39.0]).collect();
153 let ys: Vec<Vec<f64>> = xs.iter().map(|x| vec![(3.0 * x[0]).sin()]).collect();
154 let mut net = Mlp::new(&[1, 24, 24, 1], 7);
155 let mse = net.train(&xs, &ys, 1500, 0.02);
156 assert!(mse < 1e-3, "MLP should fit sin(3x): final MSE {mse}");
157 let p = net.forward(&[0.5])[0];
159 assert!((p - 1.5_f64.sin()).abs() < 0.1, "prediction at x=0.5: {p} vs {}", 1.5_f64.sin());
160 }
161
162 #[test]
163 fn an_mlp_learns_xor() {
164 let xs = vec![vec![0.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0], vec![1.0, 1.0]];
167 let ys = vec![vec![-1.0], vec![1.0], vec![1.0], vec![-1.0]];
168 let mut net = Mlp::new(&[2, 8, 1], 3);
169 let mse = net.train(&xs, &ys, 2000, 0.03);
170 assert!(mse < 1e-2, "MLP should learn XOR: final MSE {mse}");
171 for (x, y) in xs.iter().zip(ys.iter()) {
173 let p = net.forward(x)[0];
174 assert_eq!(p.signum(), y[0].signum(), "XOR({x:?}) predicted {p}, want sign {}", y[0]);
175 }
176 }
177
178 #[test]
179 fn the_loss_decreases_monotonically_early_on() {
180 let xs: Vec<Vec<f64>> = (0..10).map(|i| vec![i as f64 / 10.0]).collect();
182 let ys: Vec<Vec<f64>> = xs.iter().map(|x| vec![2.0 * x[0]]).collect();
183 let mut net = Mlp::new(&[1, 8, 1], 1);
184 let first = net.train_step(&xs, &ys, 0.05);
185 let mut last = first;
186 for _ in 0..50 {
187 last = net.train_step(&xs, &ys, 0.05);
188 }
189 assert!(last < first, "loss should drop: {first} → {last}");
190 }
191}