Skip to main content

sklears_neural/layers/
transformer.rs

1//! Transformer components including positional encoding and transformer blocks.
2//!
3//! This module provides implementations of transformer architecture components
4//! including positional encoding, multi-head attention, and transformer blocks.
5
6use crate::activation::Activation;
7use crate::weight_init::{InitStrategy, WeightInitializer};
8use crate::NeuralResult;
9use scirs2_core::ndarray::{s, Array1, Array2, Array3};
10use scirs2_core::numeric::NumCast;
11use scirs2_core::random::thread_rng;
12use sklears_core::error::SklearsError;
13use sklears_core::types::FloatBounds;
14
15/// Apply activation to 3D array (for transformer use)
16fn apply_activation_3d<T: FloatBounds>(activation: &Activation, input: &Array3<T>) -> Array3<T> {
17    match activation {
18        Activation::Identity => input.clone(),
19        Activation::Logistic => input.mapv(|val| {
20            let exp_neg = (-val).exp();
21            T::one() / (T::one() + exp_neg)
22        }),
23        Activation::Tanh => input.mapv(|val| val.tanh()),
24        Activation::Relu => input.mapv(|val| val.max(T::zero())),
25        _ => input.clone(), // For other activations, just return input for now
26    }
27}
28
29/// Positional encoding types
30#[derive(Debug, Clone, PartialEq)]
31pub enum PositionalEncodingType {
32    /// Sinusoidal positional encoding (original transformer)
33    Sinusoidal,
34    /// Learnable positional encoding
35    Learnable,
36    /// Relative positional encoding
37    Relative,
38}
39
40/// Positional encoding implementation for transformer architectures
41///
42/// Adds positional information to input embeddings to help the model
43/// understand the order of elements in a sequence.
44#[derive(Debug, Clone)]
45#[allow(dead_code)] // dropout_rate retained for future regularization; currently stored but not applied
46pub struct PositionalEncoding<T: FloatBounds> {
47    /// Maximum sequence length
48    max_seq_len: usize,
49    /// Model dimension (embedding dimension)
50    d_model: usize,
51    /// Type of positional encoding
52    encoding_type: PositionalEncodingType,
53    /// Dropout rate for regularization
54    dropout_rate: T,
55    /// Pre-computed sinusoidal encodings (for sinusoidal type)
56    sinusoidal_encodings: Option<Array2<T>>,
57    /// Learnable position embeddings (for learnable type)
58    position_embeddings: Option<Array2<T>>,
59    /// Whether to scale embeddings by sqrt(d_model)
60    scale_embeddings: bool,
61}
62
63impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand + From<f64>> PositionalEncoding<T> {
64    /// Create a new positional encoding layer
65    pub fn new(
66        max_seq_len: usize,
67        d_model: usize,
68        encoding_type: PositionalEncodingType,
69        dropout_rate: T,
70        scale_embeddings: bool,
71    ) -> NeuralResult<Self> {
72        let mut pe = Self {
73            max_seq_len,
74            d_model,
75            encoding_type: encoding_type.clone(),
76            dropout_rate,
77            sinusoidal_encodings: None,
78            position_embeddings: None,
79            scale_embeddings,
80        };
81
82        match encoding_type {
83            PositionalEncodingType::Sinusoidal => {
84                pe.sinusoidal_encodings = Some(pe.create_sinusoidal_encodings()?);
85            }
86            PositionalEncodingType::Learnable => {
87                pe.position_embeddings = Some(pe.create_learnable_embeddings()?);
88            }
89            PositionalEncodingType::Relative => {
90                // Relative positional encoding is handled differently
91                // and doesn't require pre-computed embeddings
92            }
93        }
94
95        Ok(pe)
96    }
97
98    /// Create sinusoidal positional encodings
99    fn create_sinusoidal_encodings(&self) -> NeuralResult<Array2<T>> {
100        let mut encodings = Array2::zeros((self.max_seq_len, self.d_model));
101
102        for pos in 0..self.max_seq_len {
103            for i in 0..(self.d_model / 2) {
104                let angle = pos as f64 / 10000_f64.powf(2.0 * i as f64 / self.d_model as f64);
105
106                // Even dimensions use sin
107                encodings[[pos, 2 * i]] = NumCast::from(angle.sin()).unwrap_or_else(T::zero);
108
109                // Odd dimensions use cos
110                if 2 * i + 1 < self.d_model {
111                    encodings[[pos, 2 * i + 1]] =
112                        NumCast::from(angle.cos()).unwrap_or_else(T::zero);
113                }
114            }
115        }
116
117        Ok(encodings)
118    }
119
120    /// Create learnable position embeddings
121    fn create_learnable_embeddings(&self) -> NeuralResult<Array2<T>> {
122        let mut rng = thread_rng();
123        let initializer = WeightInitializer::new(InitStrategy::Normal {
124            mean: 0.0,
125            std: 0.02,
126        });
127
128        initializer.initialize_2d(&mut rng, (self.max_seq_len, self.d_model))
129    }
130
131    /// Apply positional encoding to input embeddings
132    pub fn encode(&self, embeddings: &Array3<T>) -> NeuralResult<Array3<T>> {
133        let (batch_size, seq_len, d_model) = embeddings.dim();
134
135        if d_model != self.d_model {
136            return Err(SklearsError::InvalidParameter {
137                name: "d_model".to_string(),
138                reason: format!("expected {}, got {}", self.d_model, d_model),
139            });
140        }
141
142        if seq_len > self.max_seq_len {
143            return Err(SklearsError::InvalidParameter {
144                name: "seq_len".to_string(),
145                reason: format!(
146                    "sequence length {} exceeds maximum {}",
147                    seq_len, self.max_seq_len
148                ),
149            });
150        }
151
152        let mut output = embeddings.clone();
153
154        // Scale embeddings if requested
155        if self.scale_embeddings {
156            let scale_factor = NumCast::from((self.d_model as f64).sqrt()).unwrap_or(T::one());
157            output *= scale_factor;
158        }
159
160        // Add positional encodings based on type
161        match &self.encoding_type {
162            PositionalEncodingType::Sinusoidal => {
163                if let Some(ref encodings) = self.sinusoidal_encodings {
164                    for batch in 0..batch_size {
165                        for pos in 0..seq_len {
166                            for dim in 0..d_model {
167                                output[[batch, pos, dim]] += encodings[[pos, dim]];
168                            }
169                        }
170                    }
171                }
172            }
173            PositionalEncodingType::Learnable => {
174                if let Some(ref embeddings) = self.position_embeddings {
175                    for batch in 0..batch_size {
176                        for pos in 0..seq_len {
177                            for dim in 0..d_model {
178                                output[[batch, pos, dim]] += embeddings[[pos, dim]];
179                            }
180                        }
181                    }
182                }
183            }
184            PositionalEncodingType::Relative => {
185                // Relative positional encoding is typically handled in the attention mechanism
186                // For now, we'll just return the original embeddings
187            }
188        }
189
190        Ok(output)
191    }
192
193    /// Get positional encodings for a specific sequence length
194    pub fn get_encodings(&self, seq_len: usize) -> NeuralResult<Array2<T>> {
195        if seq_len > self.max_seq_len {
196            return Err(SklearsError::InvalidParameter {
197                name: "seq_len".to_string(),
198                reason: format!(
199                    "sequence length {} exceeds maximum {}",
200                    seq_len, self.max_seq_len
201                ),
202            });
203        }
204
205        match &self.encoding_type {
206            PositionalEncodingType::Sinusoidal => {
207                if let Some(ref encodings) = self.sinusoidal_encodings {
208                    Ok(encodings.slice(s![..seq_len, ..]).to_owned())
209                } else {
210                    Err(SklearsError::InvalidParameter {
211                        name: "position_encodings".to_string(),
212                        reason: "sinusoidal encodings not initialized".to_string(),
213                    })
214                }
215            }
216            PositionalEncodingType::Learnable => {
217                if let Some(ref embeddings) = self.position_embeddings {
218                    Ok(embeddings.slice(s![..seq_len, ..]).to_owned())
219                } else {
220                    Err(SklearsError::InvalidParameter {
221                        name: "position_embeddings".to_string(),
222                        reason: "learnable embeddings not initialized".to_string(),
223                    })
224                }
225            }
226            PositionalEncodingType::Relative => {
227                // Return zeros for relative encoding as it's handled differently
228                Ok(Array2::zeros((seq_len, self.d_model)))
229            }
230        }
231    }
232
233    /// Update learnable position embeddings (for gradient-based optimization)
234    pub fn update_position_embeddings(
235        &mut self,
236        gradients: &Array2<T>,
237        learning_rate: T,
238    ) -> NeuralResult<()> {
239        if let PositionalEncodingType::Learnable = self.encoding_type {
240            if let Some(ref mut embeddings) = self.position_embeddings {
241                *embeddings = embeddings.clone() - gradients * learning_rate;
242                Ok(())
243            } else {
244                Err(SklearsError::InvalidParameter {
245                    name: "position_embeddings".to_string(),
246                    reason: "learnable embeddings not initialized".to_string(),
247                })
248            }
249        } else {
250            Err(SklearsError::InvalidParameter {
251                name: "encoding_type".to_string(),
252                reason: "position embeddings are not learnable".to_string(),
253            })
254        }
255    }
256
257    /// Get the number of parameters
258    pub fn num_parameters(&self) -> usize {
259        match &self.encoding_type {
260            PositionalEncodingType::Sinusoidal => 0, // No learnable parameters
261            PositionalEncodingType::Learnable => {
262                if let Some(ref embeddings) = self.position_embeddings {
263                    embeddings.len()
264                } else {
265                    0
266                }
267            }
268            PositionalEncodingType::Relative => 0, // Handled in attention layers
269        }
270    }
271}
272
273/// Multi-head attention mechanism for transformer architectures
274///
275/// Implements scaled dot-product attention with multiple attention heads:
276/// Attention(Q, K, V) = softmax(QK^T / sqrt(d_k))V
277#[derive(Debug, Clone)]
278#[allow(dead_code)] // dropout_rate, use_bias retained for future regularization and bias enabling
279pub struct MultiHeadAttention<T: FloatBounds> {
280    /// Number of attention heads
281    num_heads: usize,
282    /// Model dimension
283    d_model: usize,
284    /// Dimension of each attention head
285    d_k: usize,
286    /// Query projection weights
287    w_q: Array2<T>,
288    /// Key projection weights
289    w_k: Array2<T>,
290    /// Value projection weights
291    w_v: Array2<T>,
292    /// Output projection weights
293    w_o: Array2<T>,
294    /// Query bias
295    b_q: Option<Array1<T>>,
296    /// Key bias
297    b_k: Option<Array1<T>>,
298    /// Value bias
299    b_v: Option<Array1<T>>,
300    /// Output bias
301    b_o: Option<Array1<T>>,
302    /// Dropout rate for attention weights
303    dropout_rate: T,
304    /// Whether to use bias terms
305    use_bias: bool,
306    /// Scaling factor for attention scores
307    scale_factor: T,
308    /// Cached attention weights for visualization/analysis
309    cached_attention_weights: Option<Array3<T>>,
310}
311
312impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand + From<f64>> MultiHeadAttention<T> {
313    /// Create a multi-head attention layer; `d_model` must be divisible by `num_heads`
314    pub fn new(
315        d_model: usize,
316        num_heads: usize,
317        dropout_rate: T,
318        use_bias: bool,
319    ) -> NeuralResult<Self> {
320        if !d_model.is_multiple_of(num_heads) {
321            return Err(SklearsError::InvalidParameter {
322                name: "d_model".to_string(),
323                reason: format!(
324                    "d_model ({}) must be divisible by num_heads ({})",
325                    d_model, num_heads
326                ),
327            });
328        }
329
330        let d_k = d_model / num_heads;
331        let scale_factor = NumCast::from(1.0 / (d_k as f64).sqrt()).unwrap_or(T::one());
332
333        // Initialize weights using Xavier initialization
334        let mut rng = thread_rng();
335        let initializer = WeightInitializer::new(InitStrategy::XavierUniform);
336
337        let w_q = initializer.initialize_2d(&mut rng, (d_model, d_model))?;
338        let w_k = initializer.initialize_2d(&mut rng, (d_model, d_model))?;
339        let w_v = initializer.initialize_2d(&mut rng, (d_model, d_model))?;
340        let w_o = initializer.initialize_2d(&mut rng, (d_model, d_model))?;
341
342        let (b_q, b_k, b_v, b_o) = if use_bias {
343            (
344                Some(Array1::zeros(d_model)),
345                Some(Array1::zeros(d_model)),
346                Some(Array1::zeros(d_model)),
347                Some(Array1::zeros(d_model)),
348            )
349        } else {
350            (None, None, None, None)
351        };
352
353        Ok(Self {
354            num_heads,
355            d_model,
356            d_k,
357            w_q,
358            w_k,
359            w_v,
360            w_o,
361            b_q,
362            b_k,
363            b_v,
364            b_o,
365            dropout_rate,
366            use_bias,
367            scale_factor,
368            cached_attention_weights: None,
369        })
370    }
371
372    /// Forward pass through multi-head attention
373    pub fn forward(
374        &mut self,
375        query: &Array3<T>,
376        key: &Array3<T>,
377        value: &Array3<T>,
378        mask: Option<&Array3<T>>,
379    ) -> NeuralResult<Array3<T>> {
380        let (batch_size, seq_len_q, _) = query.dim();
381        let (_, seq_len_k, _) = key.dim();
382        let (_, seq_len_v, _) = value.dim();
383
384        if seq_len_k != seq_len_v {
385            return Err(SklearsError::InvalidParameter {
386                name: "seq_len".to_string(),
387                reason: "key and value sequence lengths must match".to_string(),
388            });
389        }
390
391        // Linear projections for Q, K, V
392        let q = self.linear_projection(query, &self.w_q, self.b_q.as_ref())?;
393        let k = self.linear_projection(key, &self.w_k, self.b_k.as_ref())?;
394        let v = self.linear_projection(value, &self.w_v, self.b_v.as_ref())?;
395
396        // Reshape to (batch_size, num_heads, seq_len, d_k)
397        let q_heads = self.reshape_for_heads(&q, batch_size, seq_len_q)?;
398        let k_heads = self.reshape_for_heads(&k, batch_size, seq_len_k)?;
399        let v_heads = self.reshape_for_heads(&v, batch_size, seq_len_v)?;
400
401        // Scaled dot-product attention
402        let (attended_values, attention_weights) =
403            self.scaled_dot_product_attention(&q_heads, &k_heads, &v_heads, mask)?;
404
405        // Cache attention weights for analysis
406        self.cached_attention_weights = Some(attention_weights);
407
408        // Reshape back to (batch_size, seq_len, d_model)
409        let concatenated = self.reshape_from_heads(&attended_values, batch_size, seq_len_q)?;
410
411        // Final linear projection
412        let output = self.linear_projection(&concatenated, &self.w_o, self.b_o.as_ref())?;
413
414        Ok(output)
415    }
416
417    /// Apply linear projection (matrix multiplication + bias)
418    fn linear_projection(
419        &self,
420        input: &Array3<T>,
421        weight: &Array2<T>,
422        bias: Option<&Array1<T>>,
423    ) -> NeuralResult<Array3<T>> {
424        let (batch_size, seq_len, d_model) = input.dim();
425        let mut output = Array3::zeros((batch_size, seq_len, d_model));
426
427        for batch in 0..batch_size {
428            let input_2d = input.slice(s![batch, .., ..]);
429            let projected = input_2d.dot(weight);
430            output.slice_mut(s![batch, .., ..]).assign(&projected);
431
432            if let Some(bias_vec) = bias {
433                for seq in 0..seq_len {
434                    for dim in 0..d_model {
435                        output[[batch, seq, dim]] += bias_vec[dim];
436                    }
437                }
438            }
439        }
440
441        Ok(output)
442    }
443
444    /// Reshape tensor for multi-head processing
445    fn reshape_for_heads(
446        &self,
447        input: &Array3<T>,
448        batch_size: usize,
449        seq_len: usize,
450    ) -> NeuralResult<Array3<T>> {
451        // Input: (batch_size, seq_len, d_model)
452        // Output: (batch_size * num_heads, seq_len, d_k)
453        let mut output = Array3::zeros((batch_size * self.num_heads, seq_len, self.d_k));
454
455        for batch in 0..batch_size {
456            for head in 0..self.num_heads {
457                let head_idx = batch * self.num_heads + head;
458                let start_dim = head * self.d_k;
459                let _end_dim = start_dim + self.d_k;
460
461                for seq in 0..seq_len {
462                    for dim in 0..self.d_k {
463                        output[[head_idx, seq, dim]] = input[[batch, seq, start_dim + dim]];
464                    }
465                }
466            }
467        }
468
469        Ok(output)
470    }
471
472    /// Reshape tensor back from multi-head format
473    fn reshape_from_heads(
474        &self,
475        input: &Array3<T>,
476        batch_size: usize,
477        seq_len: usize,
478    ) -> NeuralResult<Array3<T>> {
479        // Input: (batch_size * num_heads, seq_len, d_k)
480        // Output: (batch_size, seq_len, d_model)
481        let mut output = Array3::zeros((batch_size, seq_len, self.d_model));
482
483        for batch in 0..batch_size {
484            for head in 0..self.num_heads {
485                let head_idx = batch * self.num_heads + head;
486                let start_dim = head * self.d_k;
487
488                for seq in 0..seq_len {
489                    for dim in 0..self.d_k {
490                        output[[batch, seq, start_dim + dim]] = input[[head_idx, seq, dim]];
491                    }
492                }
493            }
494        }
495
496        Ok(output)
497    }
498
499    /// Scaled dot-product attention
500    fn scaled_dot_product_attention(
501        &self,
502        q: &Array3<T>,
503        k: &Array3<T>,
504        v: &Array3<T>,
505        mask: Option<&Array3<T>>,
506    ) -> NeuralResult<(Array3<T>, Array3<T>)> {
507        let (batch_heads, seq_len_q, d_k) = q.dim();
508        let (_, seq_len_k, _) = k.dim();
509
510        // Compute attention scores: Q @ K^T
511        let mut scores = Array3::zeros((batch_heads, seq_len_q, seq_len_k));
512
513        for batch_head in 0..batch_heads {
514            let q_slice = q.slice(s![batch_head, .., ..]);
515            let k_slice = k.slice(s![batch_head, .., ..]);
516            let score_slice = q_slice.dot(&k_slice.t());
517            scores
518                .slice_mut(s![batch_head, .., ..])
519                .assign(&score_slice);
520        }
521
522        // Scale by sqrt(d_k)
523        scores *= self.scale_factor;
524
525        // Apply mask if provided
526        if let Some(mask_tensor) = mask {
527            // Apply mask by setting masked positions to large negative value
528            let neg_inf = NumCast::from(-1e9).unwrap_or(T::zero());
529            for batch_head in 0..batch_heads {
530                for i in 0..seq_len_q {
531                    for j in 0..seq_len_k {
532                        let mask_batch = batch_head % mask_tensor.dim().0;
533                        if mask_tensor[[mask_batch, i, j]] == T::zero() {
534                            scores[[batch_head, i, j]] = neg_inf;
535                        }
536                    }
537                }
538            }
539        }
540
541        // Apply softmax to get attention weights
542        let attention_weights = self.softmax_3d(&scores)?;
543
544        // Apply attention to values: Attention @ V
545        let mut output = Array3::zeros((batch_heads, seq_len_q, d_k));
546
547        for batch_head in 0..batch_heads {
548            let attn_slice = attention_weights.slice(s![batch_head, .., ..]);
549            let v_slice = v.slice(s![batch_head, .., ..]);
550            let out_slice = attn_slice.dot(&v_slice);
551            output.slice_mut(s![batch_head, .., ..]).assign(&out_slice);
552        }
553
554        Ok((output, attention_weights))
555    }
556
557    /// Apply softmax along the last dimension of a 3D tensor
558    fn softmax_3d(&self, input: &Array3<T>) -> NeuralResult<Array3<T>> {
559        let (dim0, dim1, dim2) = input.dim();
560        let mut output = Array3::zeros((dim0, dim1, dim2));
561
562        for i in 0..dim0 {
563            for j in 0..dim1 {
564                // Find max for numerical stability
565                let mut max_val = input[[i, j, 0]];
566                for k in 1..dim2 {
567                    if input[[i, j, k]] > max_val {
568                        max_val = input[[i, j, k]];
569                    }
570                }
571
572                // Compute exp(x - max) and sum
573                let mut sum = T::zero();
574                for k in 0..dim2 {
575                    let exp_val = (input[[i, j, k]] - max_val).exp();
576                    output[[i, j, k]] = exp_val;
577                    sum += exp_val;
578                }
579
580                // Normalize
581                for k in 0..dim2 {
582                    output[[i, j, k]] /= sum;
583                }
584            }
585        }
586
587        Ok(output)
588    }
589
590    /// Get the last computed attention weights
591    pub fn get_attention_weights(&self) -> Option<&Array3<T>> {
592        self.cached_attention_weights.as_ref()
593    }
594
595    /// Get the number of parameters
596    pub fn num_parameters(&self) -> usize {
597        let weight_params = self.w_q.len() + self.w_k.len() + self.w_v.len() + self.w_o.len();
598        let bias_params = if self.use_bias {
599            self.b_q.as_ref().map_or(0, |b| b.len())
600                + self.b_k.as_ref().map_or(0, |b| b.len())
601                + self.b_v.as_ref().map_or(0, |b| b.len())
602                + self.b_o.as_ref().map_or(0, |b| b.len())
603        } else {
604            0
605        };
606        weight_params + bias_params
607    }
608}
609
610/// Feed-forward network for transformer blocks
611#[derive(Debug, Clone)]
612#[allow(dead_code)] // dropout_rate and use_bias retained for future regularization and bias enabling
613pub struct FeedForward<T: FloatBounds> {
614    /// First linear layer weights
615    w1: Array2<T>,
616    /// Second linear layer weights
617    w2: Array2<T>,
618    /// First layer bias
619    b1: Option<Array1<T>>,
620    /// Second layer bias
621    b2: Option<Array1<T>>,
622    /// Activation function
623    activation: Activation,
624    /// Dropout rate
625    dropout_rate: T,
626    /// Whether to use bias
627    use_bias: bool,
628}
629
630impl<T: FloatBounds + scirs2_core::ndarray::ScalarOperand> FeedForward<T> {
631    /// Create a position-wise feed-forward network with the given model and expansion dimensions
632    pub fn new(
633        d_model: usize,
634        d_ff: usize,
635        activation: Activation,
636        dropout_rate: T,
637        use_bias: bool,
638    ) -> NeuralResult<Self> {
639        // Initialize weights
640        let mut rng = thread_rng();
641        let initializer = WeightInitializer::new(InitStrategy::XavierUniform);
642
643        let w1 = initializer.initialize_2d(&mut rng, (d_model, d_ff))?;
644        let w2 = initializer.initialize_2d(&mut rng, (d_ff, d_model))?;
645
646        let (b1, b2) = if use_bias {
647            (Some(Array1::zeros(d_ff)), Some(Array1::zeros(d_model)))
648        } else {
649            (None, None)
650        };
651
652        Ok(Self {
653            w1,
654            w2,
655            b1,
656            b2,
657            activation,
658            dropout_rate,
659            use_bias,
660        })
661    }
662
663    /// Forward pass through the feed-forward network
664    pub fn forward(&mut self, input: &Array3<T>) -> NeuralResult<Array3<T>> {
665        let (batch_size, seq_len, d_model) = input.dim();
666
667        // First linear layer
668        let mut hidden = Array3::zeros((batch_size, seq_len, self.w1.dim().1));
669
670        for batch in 0..batch_size {
671            let input_2d = input.slice(s![batch, .., ..]);
672            let hidden_2d = input_2d.dot(&self.w1);
673            hidden.slice_mut(s![batch, .., ..]).assign(&hidden_2d);
674
675            if let Some(ref bias) = self.b1 {
676                for seq in 0..seq_len {
677                    for dim in 0..bias.len() {
678                        hidden[[batch, seq, dim]] += bias[dim];
679                    }
680                }
681            }
682        }
683
684        // Apply activation
685        hidden = apply_activation_3d(&self.activation, &hidden);
686
687        // Second linear layer
688        let mut output = Array3::zeros((batch_size, seq_len, d_model));
689
690        for batch in 0..batch_size {
691            let hidden_2d = hidden.slice(s![batch, .., ..]);
692            let output_2d = hidden_2d.dot(&self.w2);
693            output.slice_mut(s![batch, .., ..]).assign(&output_2d);
694
695            if let Some(ref bias) = self.b2 {
696                for seq in 0..seq_len {
697                    for dim in 0..bias.len() {
698                        output[[batch, seq, dim]] += bias[dim];
699                    }
700                }
701            }
702        }
703
704        Ok(output)
705    }
706
707    /// Get the number of parameters
708    pub fn num_parameters(&self) -> usize {
709        let weight_params = self.w1.len() + self.w2.len();
710        let bias_params = if self.use_bias {
711            self.b1.as_ref().map_or(0, |b| b.len()) + self.b2.as_ref().map_or(0, |b| b.len())
712        } else {
713            0
714        };
715        weight_params + bias_params
716    }
717}
718
719#[allow(non_snake_case)]
720#[cfg(test)]
721mod tests {
722    use super::*;
723
724    #[test]
725    #[ignore]
726    fn test_sinusoidal_positional_encoding() {
727        let pe =
728            PositionalEncoding::<f64>::new(100, 64, PositionalEncodingType::Sinusoidal, 0.1, true)
729                .expect("operation should succeed");
730
731        assert_eq!(pe.max_seq_len, 100);
732        assert_eq!(pe.d_model, 64);
733        assert!(pe.sinusoidal_encodings.is_some());
734
735        let encodings = pe.get_encodings(10).expect("operation should succeed");
736        assert_eq!(encodings.dim(), (10, 64));
737    }
738
739    #[test]
740    #[ignore]
741    fn test_learnable_positional_encoding() {
742        let pe =
743            PositionalEncoding::<f64>::new(50, 32, PositionalEncodingType::Learnable, 0.1, false)
744                .expect("operation should succeed");
745
746        assert_eq!(pe.num_parameters(), 50 * 32);
747        assert!(pe.position_embeddings.is_some());
748
749        let encodings = pe.get_encodings(20).expect("operation should succeed");
750        assert_eq!(encodings.dim(), (20, 32));
751    }
752
753    #[test]
754    #[ignore]
755    fn test_positional_encoding_forward() {
756        let pe =
757            PositionalEncoding::<f64>::new(50, 16, PositionalEncodingType::Sinusoidal, 0.0, false)
758                .expect("operation should succeed");
759
760        let embeddings = Array3::zeros((2, 10, 16)); // batch=2, seq=10, d_model=16
761        let encoded = pe.encode(&embeddings).expect("operation should succeed");
762
763        assert_eq!(encoded.dim(), (2, 10, 16));
764    }
765
766    #[test]
767    #[ignore]
768    fn test_multi_head_attention_creation() {
769        let mha =
770            MultiHeadAttention::<f64>::new(512, 8, 0.1, true).expect("construction should succeed");
771
772        assert_eq!(mha.num_heads, 8);
773        assert_eq!(mha.d_model, 512);
774        assert_eq!(mha.d_k, 64);
775        assert!(mha.use_bias);
776    }
777
778    #[test]
779    #[ignore]
780    fn test_multi_head_attention_invalid_dimensions() {
781        let result = MultiHeadAttention::<f64>::new(511, 8, 0.1, true);
782        assert!(result.is_err()); // 511 is not divisible by 8
783    }
784
785    #[test]
786    #[ignore]
787    fn test_multi_head_attention_forward() {
788        let mut mha =
789            MultiHeadAttention::<f64>::new(64, 4, 0.0, false).expect("construction should succeed");
790
791        let query = Array3::zeros((2, 10, 64)); // batch=2, seq=10, d_model=64
792        let key = Array3::zeros((2, 15, 64)); // batch=2, seq=15, d_model=64
793        let value = Array3::zeros((2, 15, 64)); // batch=2, seq=15, d_model=64
794
795        let output = mha
796            .forward(&query, &key, &value, None)
797            .expect("forward pass should succeed");
798        assert_eq!(output.dim(), (2, 10, 64));
799
800        // Check that attention weights were cached
801        assert!(mha.get_attention_weights().is_some());
802    }
803
804    #[test]
805    #[ignore]
806    fn test_multi_head_attention_with_mask() {
807        let mut mha =
808            MultiHeadAttention::<f64>::new(32, 2, 0.0, false).expect("construction should succeed");
809
810        let query = Array3::ones((1, 5, 32));
811        let key = Array3::ones((1, 5, 32));
812        let value = Array3::ones((1, 5, 32));
813
814        // Create a causal mask (lower triangular)
815        let mut mask = Array3::zeros((1, 5, 5));
816        for i in 0..5 {
817            for j in 0..=i {
818                mask[[0, i, j]] = 1.0;
819            }
820        }
821
822        let output = mha
823            .forward(&query, &key, &value, Some(&mask))
824            .expect("forward pass should succeed");
825        assert_eq!(output.dim(), (1, 5, 32));
826    }
827
828    #[test]
829    #[ignore]
830    fn test_feed_forward_network() {
831        let mut ffn = FeedForward::<f64>::new(256, 1024, Activation::Relu, 0.1, true)
832            .expect("construction should succeed");
833
834        let input = Array3::zeros((2, 10, 256));
835        let output = ffn.forward(&input).expect("forward pass should succeed");
836
837        assert_eq!(output.dim(), (2, 10, 256));
838        assert_eq!(ffn.num_parameters(), 256 * 1024 + 1024 * 256 + 1024 + 256);
839    }
840
841    #[test]
842    #[ignore]
843    fn test_positional_encoding_sequence_length_validation() {
844        let pe =
845            PositionalEncoding::<f64>::new(20, 16, PositionalEncodingType::Sinusoidal, 0.0, false)
846                .expect("operation should succeed");
847
848        let long_embeddings = Array3::zeros((1, 25, 16)); // seq_len > max_seq_len
849        let result = pe.encode(&long_embeddings);
850        assert!(result.is_err());
851    }
852
853    #[test]
854    #[ignore]
855    fn test_positional_encoding_dimension_validation() {
856        let pe =
857            PositionalEncoding::<f64>::new(50, 16, PositionalEncodingType::Sinusoidal, 0.0, false)
858                .expect("operation should succeed");
859
860        let wrong_dim_embeddings = Array3::zeros((1, 10, 32)); // d_model mismatch
861        let result = pe.encode(&wrong_dim_embeddings);
862        assert!(result.is_err());
863    }
864
865    #[test]
866    #[ignore]
867    fn test_learnable_embedding_updates() {
868        let mut pe =
869            PositionalEncoding::<f64>::new(10, 8, PositionalEncodingType::Learnable, 0.0, false)
870                .expect("operation should succeed");
871
872        let gradients = Array2::ones((10, 8));
873        let learning_rate = 0.01;
874
875        let result = pe.update_position_embeddings(&gradients, learning_rate);
876        assert!(result.is_ok());
877    }
878
879    #[test]
880    #[ignore]
881    fn test_sinusoidal_encoding_properties() {
882        let pe =
883            PositionalEncoding::<f64>::new(100, 64, PositionalEncodingType::Sinusoidal, 0.0, false)
884                .expect("operation should succeed");
885
886        let encodings = pe
887            .sinusoidal_encodings
888            .as_ref()
889            .expect("operation should succeed");
890
891        // Check that even positions use sin and odd positions use cos
892        // This is a basic sanity check - the actual values depend on the formula
893        assert_eq!(encodings.dim(), (100, 64));
894
895        // Check that positions 0 and 1 have different patterns
896        let pos_0 = encodings.row(0);
897        let pos_1 = encodings.row(1);
898
899        let mut different = false;
900        for i in 0..64 {
901            if (pos_0[i] - pos_1[i]).abs() > 1e-6_f64 {
902                different = true;
903                break;
904            }
905        }
906        assert!(
907            different,
908            "Different positions should have different encodings"
909        );
910    }
911}