Skip to main content

rust_lstm/layers/
linear.rs

1use crate::optimizers::Optimizer;
2use ndarray::Array2;
3use ndarray_rand::rand_distr::Uniform;
4use ndarray_rand::RandomExt;
5
6/// Holds gradients for linear layer parameters during backpropagation
7#[derive(Clone, Debug)]
8pub struct LinearGradients {
9    pub weight: Array2<f64>,
10    pub bias: Array2<f64>,
11}
12
13/// A fully connected (linear/dense) layer for neural networks
14///
15/// Performs the transformation: output = input * weight^T + bias
16/// where weight has shape (output_size, input_size) and bias has shape (output_size, 1)
17#[derive(Clone, Debug)]
18pub struct LinearLayer {
19    pub weight: Array2<f64>, // (output_size, input_size)
20    pub bias: Array2<f64>,   // (output_size, 1)
21    pub input_size: usize,
22    pub output_size: usize,
23    input_cache: Option<Array2<f64>>, // Cache input for backward pass
24}
25
26impl LinearLayer {
27    /// Create a new linear layer with random initialization
28    ///
29    /// # Arguments
30    /// * `input_size` - Size of input features
31    /// * `output_size` - Size of output features
32    ///
33    /// # Returns
34    /// * New LinearLayer with Xavier/Glorot initialization
35    pub fn new(input_size: usize, output_size: usize) -> Self {
36        // Xavier/Glorot initialization: scale by sqrt(2 / (input_size + output_size))
37        let scale = (2.0 / (input_size + output_size) as f64).sqrt();
38        let weight_range = scale;
39
40        let weight = Array2::random(
41            (output_size, input_size),
42            Uniform::new(-weight_range, weight_range),
43        );
44        let bias = Array2::zeros((output_size, 1));
45
46        Self {
47            weight,
48            bias,
49            input_size,
50            output_size,
51            input_cache: None,
52        }
53    }
54
55    /// Create a new linear layer with zero initialization
56    pub fn new_zeros(input_size: usize, output_size: usize) -> Self {
57        let weight = Array2::zeros((output_size, input_size));
58        let bias = Array2::zeros((output_size, 1));
59
60        Self {
61            weight,
62            bias,
63            input_size,
64            output_size,
65            input_cache: None,
66        }
67    }
68
69    /// Create a new linear layer with custom initialization
70    pub fn from_weights(weight: Array2<f64>, bias: Array2<f64>) -> Self {
71        let (output_size, input_size) = weight.dim();
72        assert_eq!(
73            bias.shape(),
74            &[output_size, 1],
75            "Bias shape must be (output_size, 1)"
76        );
77
78        Self {
79            weight,
80            bias,
81            input_size,
82            output_size,
83            input_cache: None,
84        }
85    }
86
87    /// Forward pass through the linear layer
88    ///
89    /// # Arguments
90    /// * `input` - Input tensor of shape (input_size, batch_size)
91    ///
92    /// # Returns
93    /// * Output tensor of shape (output_size, batch_size)
94    pub fn forward(&mut self, input: &Array2<f64>) -> Array2<f64> {
95        let (input_features, _batch_size) = input.dim();
96        assert_eq!(
97            input_features, self.input_size,
98            "Input size {} doesn't match layer input size {}",
99            input_features, self.input_size
100        );
101
102        // Cache input for backward pass
103        self.input_cache = Some(input.clone());
104
105        // output = weight @ input + bias (bias broadcasts automatically)
106        &self.weight.dot(input) + &self.bias
107    }
108
109    /// Backward pass through the linear layer
110    ///
111    /// # Arguments
112    /// * `grad_output` - Gradient w.r.t. output of shape (output_size, batch_size)
113    ///
114    /// # Returns
115    /// * Tuple of (gradients, input_gradient)
116    ///   - gradients: LinearGradients containing weight and bias gradients
117    ///   - input_gradient: Gradient w.r.t. input of shape (input_size, batch_size)
118    pub fn backward(&self, grad_output: &Array2<f64>) -> (LinearGradients, Array2<f64>) {
119        let input = self
120            .input_cache
121            .as_ref()
122            .expect("Input cache not found for backward pass");
123        let (output_features, batch_size) = grad_output.dim();
124        let (input_features, input_batch_size) = input.dim();
125
126        assert_eq!(
127            output_features, self.output_size,
128            "Gradient output size mismatch"
129        );
130        assert_eq!(input_features, self.input_size, "Input size mismatch");
131        assert_eq!(batch_size, input_batch_size, "Batch size mismatch");
132
133        // Gradient w.r.t. weight: grad_output @ input^T
134        let weight_grad = grad_output.dot(&input.t());
135
136        // Gradient w.r.t. bias: sum over batch dimension, keep as column vector
137        let bias_grad = grad_output
138            .sum_axis(ndarray::Axis(1))
139            .insert_axis(ndarray::Axis(1));
140
141        // Gradient w.r.t. input: weight^T @ grad_output
142        let input_grad = self.weight.t().dot(grad_output);
143
144        let gradients = LinearGradients {
145            weight: weight_grad,
146            bias: bias_grad,
147        };
148
149        (gradients, input_grad)
150    }
151
152    /// Update parameters using the provided optimizer
153    pub fn update_parameters<O: Optimizer>(
154        &mut self,
155        gradients: &LinearGradients,
156        optimizer: &mut O,
157        prefix: &str,
158    ) {
159        optimizer.update(
160            &format!("{}_weight", prefix),
161            &mut self.weight,
162            &gradients.weight,
163        );
164        optimizer.update(&format!("{}_bias", prefix), &mut self.bias, &gradients.bias);
165    }
166
167    /// Initialize zero gradients for accumulation
168    pub fn zero_gradients(&self) -> LinearGradients {
169        LinearGradients {
170            weight: Array2::zeros(self.weight.raw_dim()),
171            bias: Array2::zeros(self.bias.raw_dim()),
172        }
173    }
174
175    /// Get the number of parameters in this layer
176    pub fn num_parameters(&self) -> usize {
177        self.weight.len() + self.bias.len()
178    }
179
180    /// Get layer dimensions
181    pub fn dimensions(&self) -> (usize, usize) {
182        (self.input_size, self.output_size)
183    }
184
185    /// Set the layer to training mode
186    pub fn train(&mut self) {
187        // Linear layer has no specific training mode behavior like dropout
188    }
189
190    /// Set the layer to evaluation mode
191    pub fn eval(&mut self) {
192        // Linear layer has no specific evaluation mode behavior
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::optimizers::SGD;
200    use ndarray::arr2;
201
202    #[test]
203    fn test_linear_layer_creation() {
204        let layer = LinearLayer::new(10, 5);
205        assert_eq!(layer.input_size, 10);
206        assert_eq!(layer.output_size, 5);
207        assert_eq!(layer.weight.shape(), &[5, 10]);
208        assert_eq!(layer.bias.shape(), &[5, 1]);
209    }
210
211    #[test]
212    fn test_linear_layer_forward() {
213        let mut layer = LinearLayer::new_zeros(3, 2);
214        let input = arr2(&[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]); // (3, 2)
215
216        let output = layer.forward(&input);
217        assert_eq!(output.shape(), &[2, 2]); // (output_size, batch_size)
218
219        // With zero weights and bias, output should be zero
220        assert!(output.iter().all(|&x| x == 0.0));
221    }
222
223    #[test]
224    fn test_linear_layer_backward() {
225        let mut layer = LinearLayer::new(3, 2);
226        let input = arr2(&[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]); // (3, 2)
227        let grad_output = arr2(&[[1.0, 1.0], [1.0, 1.0]]); // (2, 2)
228
229        // Forward pass first to cache input
230        let _output = layer.forward(&input);
231
232        let (gradients, input_grad) = layer.backward(&grad_output);
233
234        assert_eq!(gradients.weight.shape(), &[2, 3]);
235        assert_eq!(gradients.bias.shape(), &[2, 1]);
236        assert_eq!(input_grad.shape(), &[3, 2]);
237    }
238
239    #[test]
240    fn test_linear_layer_with_optimizer() {
241        let mut layer = LinearLayer::new(2, 1);
242        let mut optimizer = SGD::new(0.1);
243
244        let input = arr2(&[[1.0], [2.0]]); // (2, 1)
245        let target = arr2(&[[3.0]]); // (1, 1)
246
247        // Forward pass
248        let output = layer.forward(&input);
249
250        // Simple loss gradient (output - target)
251        let grad_output = &output - &target;
252
253        // Backward pass
254        let (gradients, _) = layer.backward(&grad_output);
255
256        // Update parameters
257        layer.update_parameters(&gradients, &mut optimizer, "linear");
258
259        // Parameters should have changed
260        assert!(layer.weight.iter().any(|&x| x != 0.0) || layer.bias.iter().any(|&x| x != 0.0));
261    }
262
263    #[test]
264    fn test_linear_layer_dimensions() {
265        let layer = LinearLayer::new(128, 10);
266        assert_eq!(layer.dimensions(), (128, 10));
267        assert_eq!(layer.num_parameters(), 128 * 10 + 10); // weights + bias
268    }
269
270    #[test]
271    fn test_from_weights() {
272        let weight = arr2(&[[1.0, 2.0], [3.0, 4.0]]);
273        let bias = arr2(&[[0.5], [-0.5]]);
274
275        let layer = LinearLayer::from_weights(weight.clone(), bias.clone());
276        assert_eq!(layer.weight, weight);
277        assert_eq!(layer.bias, bias);
278        assert_eq!(layer.input_size, 2);
279        assert_eq!(layer.output_size, 2);
280    }
281}