optirs_learned/transformer/architecture/
feedforward.rs1use std::fmt::Debug;
2use scirs2_core::ndarray::{Array1, Array2};
9use scirs2_core::numeric::Float;
10
11use crate::error::{OptimError, Result};
12
13#[derive(Debug, Clone, Copy)]
15pub enum OutputTransformation {
16 Linear,
18 Tanh,
20 Sigmoid,
22 LearnedActivation,
24 ParameterScaling,
26}
27
28#[derive(Debug, Clone)]
30pub struct OutputProjectionLayer<T: Float + Debug + Send + Sync + 'static> {
31 weights: Array2<T>,
33
34 bias: Array1<T>,
36
37 transformation: OutputTransformation,
39}
40
41#[derive(Debug, Clone)]
43pub struct InputEmbedding<T: Float + Debug + Send + Sync + 'static> {
44 weights: Array2<T>,
46
47 input_dim: usize,
49
50 modeldim: usize,
52}
53
54impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> OutputProjectionLayer<T> {
55 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 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 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 match self.transformation {
109 OutputTransformation::Linear => {
110 }
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 output.mapv_inplace(|x| x * (T::one() + (-x.abs()).exp()));
121 }
122 OutputTransformation::ParameterScaling => {
123 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 pub fn transformation(&self) -> OutputTransformation {
140 self.transformation
141 }
142
143 pub fn set_transformation(&mut self, transformation: OutputTransformation) {
145 self.transformation = transformation;
146 }
147
148 pub fn weights(&self) -> &Array2<T> {
150 &self.weights
151 }
152
153 pub fn bias(&self) -> &Array1<T> {
155 &self.bias
156 }
157
158 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 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 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 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 pub fn input_dim(&self) -> usize {
224 self.input_dim
225 }
226
227 pub fn model_dim(&self) -> usize {
229 self.modeldim
230 }
231
232 pub fn weights(&self) -> &Array2<T> {
234 &self.weights
235 }
236
237 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}