Skip to main content

optirs_learned/transformer/architecture/
feedforward.rs

1use std::fmt::Debug;
2// Feed-forward network components for transformer layers
3//
4// This module implements the feed-forward network (FFN) layers used in
5// transformer encoder/decoder blocks, including various activation functions
6// and output projection layers.
7
8use scirs2_core::ndarray::{Array1, Array2};
9use scirs2_core::numeric::Float;
10
11use crate::error::{OptimError, Result};
12
13/// Output transformation types
14#[derive(Debug, Clone, Copy)]
15pub enum OutputTransformation {
16    /// Linear transformation
17    Linear,
18    /// Tanh activation
19    Tanh,
20    /// Sigmoid activation
21    Sigmoid,
22    /// Learned activation
23    LearnedActivation,
24    /// Parameter-specific scaling
25    ParameterScaling,
26}
27
28/// Output projection layer for final transformer output
29#[derive(Debug, Clone)]
30pub struct OutputProjectionLayer<T: Float + Debug + Send + Sync + 'static> {
31    /// Projection weights
32    weights: Array2<T>,
33
34    /// Projection bias
35    bias: Array1<T>,
36
37    /// Output transformation
38    transformation: OutputTransformation,
39}
40
41/// Input embedding layer for transformer input processing
42#[derive(Debug, Clone)]
43pub struct InputEmbedding<T: Float + Debug + Send + Sync + 'static> {
44    /// Embedding weights
45    weights: Array2<T>,
46
47    /// Input dimension
48    input_dim: usize,
49
50    /// Model dimension
51    modeldim: usize,
52}
53
54impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> OutputProjectionLayer<T> {
55    /// Create new output projection layer with Xavier/Glorot uniform weights,
56    /// limit `sqrt(6 / (fan_in + fan_out))`.
57    pub fn new(input_dim: usize, output_dim: usize) -> Result<Self> {
58        if input_dim == 0 || output_dim == 0 {
59            return Err(OptimError::InvalidConfig(
60                "Output projection dimensions must be positive".to_string(),
61            ));
62        }
63
64        let mut rng = scirs2_core::random::thread_rng();
65        let mut weights = Array2::zeros((input_dim, output_dim));
66
67        let bound = (6.0 / (input_dim + output_dim) as f64).sqrt();
68        for elem in weights.iter_mut() {
69            *elem = scirs2_core::numeric::NumCast::from((rng.random::<f64>() - 0.5) * 2.0 * bound)
70                .unwrap_or_else(|| T::zero());
71        }
72
73        let bias = Array1::zeros(output_dim);
74
75        Ok(Self {
76            weights,
77            bias,
78            transformation: OutputTransformation::Linear,
79        })
80    }
81
82    /// Create with specific transformation
83    pub fn new_with_transformation(
84        input_dim: usize,
85        output_dim: usize,
86        transformation: OutputTransformation,
87    ) -> Result<Self> {
88        let mut layer = Self::new(input_dim, output_dim)?;
89        layer.transformation = transformation;
90        Ok(layer)
91    }
92
93    /// Forward pass through output projection
94    pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
95        let (seq_len, input_dim) = input.dim();
96        let (weight_in, weight_out) = self.weights.dim();
97
98        if input_dim != weight_in {
99            return Err(OptimError::InvalidConfig(
100                "Input dimension doesn't match weight matrix".to_string(),
101            ));
102        }
103
104        let _ = seq_len;
105        let mut output = input.dot(&self.weights) + &self.bias;
106
107        // Apply output transformation
108        match self.transformation {
109            OutputTransformation::Linear => {
110                // No additional transformation
111            }
112            OutputTransformation::Tanh => {
113                output.mapv_inplace(|x| x.tanh());
114            }
115            OutputTransformation::Sigmoid => {
116                output.mapv_inplace(|x| T::one() / (T::one() + (-x).exp()));
117            }
118            OutputTransformation::LearnedActivation => {
119                // Softplus-gated identity: smooth, monotone and bounded below.
120                output.mapv_inplace(|x| x * (T::one() + (-x.abs()).exp()));
121            }
122            OutputTransformation::ParameterScaling => {
123                // Apply a fixed, dimension-dependent scaling per output feature.
124                for j in 0..weight_out {
125                    let scale: T =
126                        scirs2_core::numeric::NumCast::from(1.0 + 0.1 * (j as f64).sin())
127                            .unwrap_or_else(|| T::one());
128                    for i in 0..output.nrows() {
129                        output[[i, j]] = output[[i, j]] * scale;
130                    }
131                }
132            }
133        }
134
135        Ok(output)
136    }
137
138    /// Get output transformation type
139    pub fn transformation(&self) -> OutputTransformation {
140        self.transformation
141    }
142
143    /// Set output transformation type
144    pub fn set_transformation(&mut self, transformation: OutputTransformation) {
145        self.transformation = transformation;
146    }
147
148    /// Get projection weights
149    pub fn weights(&self) -> &Array2<T> {
150        &self.weights
151    }
152
153    /// Get projection bias
154    pub fn bias(&self) -> &Array1<T> {
155        &self.bias
156    }
157
158    /// Update weights and bias
159    pub fn update_parameters(&mut self, weights: Array2<T>, bias: Array1<T>) -> Result<()> {
160        let (weight_in, weight_out) = weights.dim();
161        let bias_dim = bias.len();
162
163        if weight_out != bias_dim {
164            return Err(OptimError::InvalidConfig(
165                "Weight output dimension doesn't match bias dimension".to_string(),
166            ));
167        }
168
169        // Update internal dimensions if they match
170        if (weight_in, weight_out) == self.weights.dim() && bias_dim == self.bias.len() {
171            self.weights = weights;
172            self.bias = bias;
173            Ok(())
174        } else {
175            Err(OptimError::InvalidConfig(
176                "New parameter dimensions don't match current layer dimensions".to_string(),
177            ))
178        }
179    }
180}
181
182impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> InputEmbedding<T> {
183    /// Create new input embedding layer with Xavier/Glorot uniform weights.
184    pub fn new(input_dim: usize, model_dim: usize) -> Result<Self> {
185        if input_dim == 0 || model_dim == 0 {
186            return Err(OptimError::InvalidConfig(
187                "Input embedding dimensions must be positive".to_string(),
188            ));
189        }
190
191        let mut rng = scirs2_core::random::thread_rng();
192        let mut weights = Array2::zeros((input_dim, model_dim));
193
194        let bound = (6.0 / (input_dim + model_dim) as f64).sqrt();
195        for elem in weights.iter_mut() {
196            *elem = scirs2_core::numeric::NumCast::from((rng.random::<f64>() - 0.5) * 2.0 * bound)
197                .unwrap_or_else(|| T::zero());
198        }
199
200        Ok(Self {
201            weights,
202            input_dim,
203            modeldim: model_dim,
204        })
205    }
206
207    /// Forward pass through input embedding
208    pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
209        let (seq_len, input_dim) = input.dim();
210
211        if input_dim != self.input_dim {
212            return Err(OptimError::InvalidConfig(format!(
213                "Input dimension {} doesn't match embedding input dimension {}",
214                input_dim, self.input_dim
215            )));
216        }
217
218        let _ = seq_len;
219        Ok(input.dot(&self.weights))
220    }
221
222    /// Get input dimension
223    pub fn input_dim(&self) -> usize {
224        self.input_dim
225    }
226
227    /// Get model dimension
228    pub fn model_dim(&self) -> usize {
229        self.modeldim
230    }
231
232    /// Get embedding weights
233    pub fn weights(&self) -> &Array2<T> {
234        &self.weights
235    }
236
237    /// Update embedding weights
238    pub fn update_weights(&mut self, weights: Array2<T>) -> Result<()> {
239        let (weight_in, weight_out) = weights.dim();
240
241        if weight_in != self.input_dim || weight_out != self.modeldim {
242            return Err(OptimError::InvalidConfig(
243                "New weight dimensions don't match embedding dimensions".to_string(),
244            ));
245        }
246
247        self.weights = weights;
248        Ok(())
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn input_embedding_produces_nonzero_output() {
258        let embedding = InputEmbedding::<f64>::new(6, 4).expect("embedding creation");
259        let input = Array2::<f64>::ones((3, 6));
260        let output = embedding.forward(&input).expect("forward");
261        assert_eq!(output.shape(), &[3, 4]);
262        assert!(
263            output.iter().any(|&v| v != 0.0),
264            "Xavier-initialized embedding produced all zeros"
265        );
266    }
267
268    #[test]
269    fn output_projection_produces_nonzero_output() {
270        let projection = OutputProjectionLayer::<f64>::new(6, 4).expect("projection creation");
271        let input = Array2::<f64>::ones((2, 6));
272        let output = projection.forward(&input).expect("forward");
273        assert_eq!(output.shape(), &[2, 4]);
274        assert!(
275            output.iter().any(|&v| v != 0.0),
276            "Xavier-initialized projection produced all zeros"
277        );
278    }
279
280    #[test]
281    fn projection_matches_manual_matmul() {
282        let mut projection = OutputProjectionLayer::<f64>::new(2, 2).expect("projection creation");
283        let weights =
284            Array2::<f64>::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).expect("valid shape");
285        let bias = Array1::<f64>::from_vec(vec![0.5, -0.5]);
286        projection
287            .update_parameters(weights, bias)
288            .expect("parameter update");
289
290        let input = Array2::<f64>::from_shape_vec((1, 2), vec![1.0, 1.0]).expect("valid shape");
291        let output = projection.forward(&input).expect("forward");
292        assert!((output[[0, 0]] - 4.5).abs() < 1e-12);
293        assert!((output[[0, 1]] - 5.5).abs() < 1e-12);
294    }
295
296    #[test]
297    fn dimension_mismatches_are_errors() {
298        let embedding = InputEmbedding::<f64>::new(6, 4).expect("embedding creation");
299        let input = Array2::<f64>::ones((3, 5));
300        assert!(embedding.forward(&input).is_err());
301
302        assert!(InputEmbedding::<f64>::new(0, 4).is_err());
303        assert!(OutputProjectionLayer::<f64>::new(4, 0).is_err());
304    }
305
306    #[test]
307    fn tanh_transformation_bounds_the_output() {
308        let mut projection = OutputProjectionLayer::<f64>::new(4, 4).expect("projection creation");
309        projection.set_transformation(OutputTransformation::Tanh);
310        let input = Array2::<f64>::from_elem((2, 4), 100.0);
311        let output = projection.forward(&input).expect("forward");
312        assert!(output.iter().all(|v| v.abs() <= 1.0));
313    }
314}