1use super::config::ActivationFunction;
4use crate::error::{OptimError, Result};
5use scirs2_core::ndarray::{Array1, Array2};
6use scirs2_core::numeric::Float;
7use std::fmt::Debug;
8
9fn xavier_uniform<
11 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
12>(
13 fan_in: usize,
14 fan_out: usize,
15) -> Array2<T> {
16 let mut rng = scirs2_core::random::thread_rng();
17 let bound = (6.0 / (fan_in + fan_out).max(1) as f64).sqrt();
18 let mut weights = Array2::zeros((fan_in, fan_out));
19 for elem in weights.iter_mut() {
20 *elem = scirs2_core::numeric::NumCast::from((rng.random::<f64>() - 0.5) * 2.0 * bound)
21 .unwrap_or_else(|| T::zero());
22 }
23 weights
24}
25
26pub struct EmbeddingLayer<
33 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
34> {
35 embedding_matrix: Array2<T>,
37
38 input_dimension: usize,
40
41 output_dimension: usize,
43}
44
45impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
46 EmbeddingLayer<T>
47{
48 pub fn new(input_dimension: usize, output_dimension: usize) -> Result<Self> {
50 if input_dimension == 0 || output_dimension == 0 {
51 return Err(OptimError::InvalidConfig(
52 "Embedding dimensions must be positive".to_string(),
53 ));
54 }
55
56 Ok(Self {
57 embedding_matrix: xavier_uniform(input_dimension, output_dimension),
58 input_dimension,
59 output_dimension,
60 })
61 }
62
63 pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
65 let (_sequence_length, width) = input.dim();
66 if width != self.input_dimension {
67 return Err(OptimError::InvalidConfig(format!(
68 "Embedding input width {width} does not match the expected {}",
69 self.input_dimension
70 )));
71 }
72
73 Ok(input.dot(&self.embedding_matrix))
74 }
75
76 pub fn parameter_count(&self) -> usize {
78 self.input_dimension * self.output_dimension
79 }
80
81 pub fn weights(&self) -> &Array2<T> {
83 &self.embedding_matrix
84 }
85
86 pub fn set_weights(&mut self, weights: Array2<T>) -> Result<()> {
88 if weights.dim() != (self.input_dimension, self.output_dimension) {
89 return Err(OptimError::InvalidConfig(format!(
90 "Weight shape {:?} does not match ({}, {})",
91 weights.dim(),
92 self.input_dimension,
93 self.output_dimension
94 )));
95 }
96 self.embedding_matrix = weights;
97 Ok(())
98 }
99
100 pub fn backward(
104 &mut self,
105 input: &Array2<T>,
106 grad_output: &Array2<T>,
107 learning_rate: T,
108 ) -> Result<Array2<T>> {
109 if input.ncols() != self.input_dimension || grad_output.ncols() != self.output_dimension {
110 return Err(OptimError::InvalidConfig(
111 "Embedding backward received mismatched shapes".to_string(),
112 ));
113 }
114 if input.nrows() != grad_output.nrows() {
115 return Err(OptimError::InvalidConfig(
116 "Embedding backward received mismatched row counts".to_string(),
117 ));
118 }
119
120 let grad_weights = input.t().dot(grad_output);
121 self.embedding_matrix = &self.embedding_matrix - &(grad_weights.clone() * learning_rate);
122 Ok(grad_weights)
123 }
124
125 pub fn reset(&mut self) -> Result<()> {
127 self.embedding_matrix = xavier_uniform(self.input_dimension, self.output_dimension);
128 Ok(())
129 }
130}
131
132pub struct LayerNormalization<
134 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
135> {
136 dimension: usize,
138
139 gamma: Array1<T>,
141
142 beta: Array1<T>,
144
145 epsilon: T,
147}
148
149impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
150 LayerNormalization<T>
151{
152 pub fn new(dimension: usize) -> Result<Self> {
154 let gamma = Array1::ones(dimension);
155 let beta = Array1::zeros(dimension);
156 let epsilon = scirs2_core::numeric::NumCast::from(1e-5).unwrap_or_else(|| T::zero());
157
158 Ok(Self {
159 dimension,
160 gamma,
161 beta,
162 epsilon,
163 })
164 }
165
166 pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
171 let (rows, width) = input.dim();
172 if width != self.dimension {
173 return Err(OptimError::InvalidConfig(format!(
174 "Layer norm input width {width} does not match its dimension {}",
175 self.dimension
176 )));
177 }
178 if width == 0 {
179 return Ok(input.clone());
180 }
181
182 let mut output = input.clone();
183 let width_t: T =
184 scirs2_core::numeric::NumCast::from(width as f64).unwrap_or_else(|| T::one());
185
186 for i in 0..rows {
187 let row = input.row(i);
188
189 let mean = row.sum() / width_t;
190 let variance = row
191 .iter()
192 .map(|&x| {
193 let diff = x - mean;
194 diff * diff
195 })
196 .fold(T::zero(), |acc, x| acc + x)
197 / width_t;
198
199 let std_dev = (variance + self.epsilon).sqrt();
200
201 for j in 0..self.dimension {
202 let normalized = (input[[i, j]] - mean) / std_dev;
203 output[[i, j]] = self.gamma[j] * normalized + self.beta[j];
204 }
205 }
206
207 Ok(output)
208 }
209
210 pub fn dimension(&self) -> usize {
212 self.dimension
213 }
214
215 pub fn backward(
223 &mut self,
224 input: &Array2<T>,
225 grad_output: &Array2<T>,
226 learning_rate: T,
227 ) -> Result<Array2<T>> {
228 if input.dim() != grad_output.dim() {
229 return Err(OptimError::InvalidConfig(format!(
230 "Layer norm backward shape mismatch: {:?} vs {:?}",
231 input.dim(),
232 grad_output.dim()
233 )));
234 }
235 let (rows, width) = input.dim();
236 if width != self.dimension {
237 return Err(OptimError::InvalidConfig(format!(
238 "Layer norm backward width {width} does not match dimension {}",
239 self.dimension
240 )));
241 }
242 if width == 0 {
243 return Ok(Array2::zeros(input.dim()));
244 }
245
246 let width_t: T =
247 scirs2_core::numeric::NumCast::from(width as f64).unwrap_or_else(|| T::one());
248 let mut grad_input = Array2::zeros(input.dim());
249 let mut grad_gamma = Array1::zeros(width);
250 let mut grad_beta = Array1::zeros(width);
251
252 for i in 0..rows {
253 let row = input.row(i);
254 let mean = row.sum() / width_t;
255 let variance = row
256 .iter()
257 .map(|&x| (x - mean) * (x - mean))
258 .fold(T::zero(), |acc, x| acc + x)
259 / width_t;
260 let sigma = (variance + self.epsilon).sqrt();
261
262 let mut xhat = Array1::zeros(width);
263 for j in 0..width {
264 xhat[j] = (input[[i, j]] - mean) / sigma;
265 }
266
267 let mut g_scaled = Array1::zeros(width);
268 for j in 0..width {
269 let g = grad_output[[i, j]];
270 grad_gamma[j] = grad_gamma[j] + g * xhat[j];
271 grad_beta[j] = grad_beta[j] + g;
272 g_scaled[j] = g * self.gamma[j];
273 }
274
275 let mean_g = g_scaled.iter().fold(T::zero(), |a, &b| a + b) / width_t;
276 let mean_gx = g_scaled
277 .iter()
278 .zip(xhat.iter())
279 .map(|(&g, &x)| g * x)
280 .fold(T::zero(), |a, b| a + b)
281 / width_t;
282
283 for j in 0..width {
284 grad_input[[i, j]] = (g_scaled[j] - mean_g - xhat[j] * mean_gx) / sigma;
285 }
286 }
287
288 self.gamma = &self.gamma - &(grad_gamma * learning_rate);
289 self.beta = &self.beta - &(grad_beta * learning_rate);
290
291 Ok(grad_input)
292 }
293
294 pub fn parameter_count(&self) -> usize {
296 2 * self.dimension }
298
299 pub fn reset(&mut self) -> Result<()> {
301 self.gamma.fill(T::one());
302 self.beta.fill(T::zero());
303 Ok(())
304 }
305}
306
307#[derive(Debug, Clone)]
310pub struct DropoutLayer {
311 dropout_rate: f64,
313
314 training: bool,
316
317 rng_state: u64,
319}
320
321impl DropoutLayer {
322 pub fn new(dropout_rate: f64) -> Self {
324 Self::with_seed(dropout_rate, 0x9E37_79B9_7F4A_7C15)
325 }
326
327 pub fn with_seed(dropout_rate: f64, seed: u64) -> Self {
329 Self {
330 dropout_rate: dropout_rate.clamp(0.0, 1.0),
331 training: false,
332 rng_state: seed | 1,
333 }
334 }
335
336 fn next_f64(&mut self) -> f64 {
337 let mut x = self.rng_state;
338 x ^= x >> 12;
339 x ^= x << 25;
340 x ^= x >> 27;
341 self.rng_state = x;
342 let value = x.wrapping_mul(0x2545_F491_4F6C_DD1D);
343 (value >> 11) as f64 / (1u64 << 53) as f64
344 }
345
346 pub fn forward<
349 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
350 >(
351 &mut self,
352 input: &Array2<T>,
353 ) -> Array2<T> {
354 if !self.training || self.dropout_rate <= 0.0 {
355 return input.clone();
356 }
357 if self.dropout_rate >= 1.0 {
358 return Array2::zeros(input.dim());
359 }
360
361 let mut output = input.clone();
362 let keep_prob = 1.0 - self.dropout_rate;
363 let scale: T =
364 scirs2_core::numeric::NumCast::from(1.0 / keep_prob).unwrap_or_else(|| T::one());
365
366 for elem in output.iter_mut() {
367 if self.next_f64() < self.dropout_rate {
368 *elem = T::zero();
369 } else {
370 *elem = *elem * scale;
371 }
372 }
373
374 output
375 }
376
377 pub fn set_training(&mut self, training: bool) {
379 self.training = training;
380 }
381
382 pub fn is_training(&self) -> bool {
384 self.training
385 }
386
387 pub fn dropout_rate(&self) -> f64 {
389 self.dropout_rate
390 }
391}
392
393pub struct OutputProjection<
395 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
396> {
397 weight: Array2<T>,
399
400 bias: Array1<T>,
402
403 input_dim: usize,
405
406 output_dim: usize,
408}
409
410impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
411 OutputProjection<T>
412{
413 pub fn new(input_dim: usize, output_dim: usize) -> Result<Self> {
415 if input_dim == 0 || output_dim == 0 {
416 return Err(OptimError::InvalidConfig(
417 "Output projection dimensions must be positive".to_string(),
418 ));
419 }
420
421 Ok(Self {
422 weight: xavier_uniform(input_dim, output_dim),
423 bias: Array1::zeros(output_dim),
424 input_dim,
425 output_dim,
426 })
427 }
428
429 pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
431 let (_rows, width) = input.dim();
432 if width != self.input_dim {
433 return Err(OptimError::InvalidConfig(format!(
434 "Projection input width {width} does not match the expected {}",
435 self.input_dim
436 )));
437 }
438
439 Ok(input.dot(&self.weight) + &self.bias)
440 }
441
442 pub fn parameter_count(&self) -> usize {
444 self.input_dim * self.output_dim + self.output_dim
445 }
446
447 pub fn weights(&self) -> &Array2<T> {
449 &self.weight
450 }
451
452 pub fn bias(&self) -> &Array1<T> {
454 &self.bias
455 }
456
457 pub fn set_parameters(&mut self, weight: Array2<T>, bias: Array1<T>) -> Result<()> {
459 if weight.dim() != (self.input_dim, self.output_dim) || bias.len() != self.output_dim {
460 return Err(OptimError::InvalidConfig(format!(
461 "Parameter shapes {:?}/{} do not match ({}, {})",
462 weight.dim(),
463 bias.len(),
464 self.input_dim,
465 self.output_dim
466 )));
467 }
468 self.weight = weight;
469 self.bias = bias;
470 Ok(())
471 }
472
473 pub fn backward(
477 &mut self,
478 input: &Array2<T>,
479 grad_output: &Array2<T>,
480 learning_rate: T,
481 ) -> Result<Array2<T>> {
482 if input.ncols() != self.input_dim || grad_output.ncols() != self.output_dim {
483 return Err(OptimError::InvalidConfig(
484 "Output projection backward received mismatched shapes".to_string(),
485 ));
486 }
487 if input.nrows() != grad_output.nrows() {
488 return Err(OptimError::InvalidConfig(
489 "Output projection backward received mismatched row counts".to_string(),
490 ));
491 }
492
493 let grad_weight = input.t().dot(grad_output);
494 let mut grad_bias = Array1::zeros(self.output_dim);
495 for j in 0..self.output_dim {
496 grad_bias[j] = grad_output.column(j).iter().fold(T::zero(), |a, &b| a + b);
497 }
498
499 let grad_input = grad_output.dot(&self.weight.t());
500
501 self.weight = &self.weight - &(grad_weight * learning_rate);
502 self.bias = &self.bias - &(grad_bias * learning_rate);
503
504 Ok(grad_input)
505 }
506
507 pub fn reset(&mut self) -> Result<()> {
509 self.weight = xavier_uniform(self.input_dim, self.output_dim);
510 self.bias.fill(T::zero());
511 Ok(())
512 }
513}
514
515pub struct ResidualConnections<
517 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
518> {
519 dimension: usize,
521
522 scale_factor: Option<T>,
524}
525
526impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
527 ResidualConnections<T>
528{
529 pub fn new(dimension: usize) -> Self {
531 Self {
532 dimension,
533 scale_factor: None,
534 }
535 }
536
537 pub fn new_with_scaling(dimension: usize, initial_scale: T) -> Self {
539 Self {
540 dimension,
541 scale_factor: Some(initial_scale),
542 }
543 }
544
545 pub fn dimension(&self) -> usize {
547 self.dimension
548 }
549
550 pub fn add(&self, input: &Array2<T>, residual: &Array2<T>) -> Result<Array2<T>> {
557 if input.shape() != residual.shape() {
558 return Err(crate::error::OptimError::Other(
559 "Shape mismatch in residual connection".to_string(),
560 ));
561 }
562 if input.ncols() != self.dimension {
563 return Err(crate::error::OptimError::InvalidConfig(format!(
564 "Residual connection was built for model dimension {} but received \
565 {} features",
566 self.dimension,
567 input.ncols()
568 )));
569 }
570
571 let mut output = input + residual;
572
573 if let Some(scale) = self.scale_factor {
574 output.mapv_inplace(|x| x * scale);
575 }
576
577 Ok(output)
578 }
579
580 pub fn set_scale_factor(&mut self, scale: T) {
582 self.scale_factor = Some(scale);
583 }
584
585 pub fn get_scale_factor(&self) -> Option<T> {
587 self.scale_factor
588 }
589}
590
591pub struct ActivationLayer;
593
594impl ActivationLayer {
595 pub fn apply<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
597 input: &Array2<T>,
598 activation: ActivationFunction,
599 ) -> Array2<T> {
600 match activation {
601 ActivationFunction::ReLU => Self::relu(input),
602 ActivationFunction::GELU => Self::gelu(input),
603 ActivationFunction::Swish => Self::swish(input),
604 ActivationFunction::Tanh => Self::tanh(input),
605 ActivationFunction::Sigmoid => Self::sigmoid(input),
606 ActivationFunction::LeakyReLU => Self::leaky_relu(
607 input,
608 scirs2_core::numeric::NumCast::from(0.01).unwrap_or_else(|| T::zero()),
609 ),
610 }
611 }
612
613 fn relu<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
615 input: &Array2<T>,
616 ) -> Array2<T> {
617 input.map(|&x| if x > T::zero() { x } else { T::zero() })
618 }
619
620 fn gelu<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
622 input: &Array2<T>,
623 ) -> Array2<T> {
624 input.map(|&x| {
625 let half = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::zero());
626 let one = T::one();
627 let sqrt_2_pi =
628 scirs2_core::numeric::NumCast::from(0.797884560802865).unwrap_or_else(|| T::zero()); let coeff = scirs2_core::numeric::NumCast::from(0.044715).unwrap_or_else(|| T::zero());
630
631 let tanh_arg = sqrt_2_pi * (x + coeff * x * x * x);
632 let tanh_val = tanh_arg.tanh();
633
634 half * x * (one + tanh_val)
635 })
636 }
637
638 fn swish<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
640 input: &Array2<T>,
641 ) -> Array2<T> {
642 input.map(|&x| x * Self::sigmoid_scalar(x))
643 }
644
645 fn tanh<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
647 input: &Array2<T>,
648 ) -> Array2<T> {
649 input.map(|&x| x.tanh())
650 }
651
652 fn sigmoid<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
654 input: &Array2<T>,
655 ) -> Array2<T> {
656 input.map(|&x| Self::sigmoid_scalar(x))
657 }
658
659 fn leaky_relu<
661 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
662 >(
663 input: &Array2<T>,
664 alpha: T,
665 ) -> Array2<T> {
666 input.map(|&x| if x > T::zero() { x } else { alpha * x })
667 }
668
669 fn sigmoid_scalar<
671 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
672 >(
673 x: T,
674 ) -> T {
675 let one = T::one();
676 if x >= T::zero() {
677 one / (one + (-x).exp())
678 } else {
679 let e = x.exp();
680 e / (one + e)
681 }
682 }
683
684 pub fn derivative<
686 T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
687 >(
688 pre_activation: &Array2<T>,
689 activation: ActivationFunction,
690 ) -> Array2<T> {
691 match activation {
692 ActivationFunction::ReLU => {
693 pre_activation.map(|&x| if x > T::zero() { T::one() } else { T::zero() })
694 }
695 ActivationFunction::LeakyReLU => {
696 let alpha: T =
697 scirs2_core::numeric::NumCast::from(0.01).unwrap_or_else(|| T::zero());
698 pre_activation.map(|&x| if x > T::zero() { T::one() } else { alpha })
699 }
700 ActivationFunction::Tanh => pre_activation.map(|&x| {
701 let t = x.tanh();
702 T::one() - t * t
703 }),
704 ActivationFunction::Sigmoid => pre_activation.map(|&x| {
705 let s = Self::sigmoid_scalar(x);
706 s * (T::one() - s)
707 }),
708 ActivationFunction::Swish => pre_activation.map(|&x| {
709 let s = Self::sigmoid_scalar(x);
710 s + x * s * (T::one() - s)
711 }),
712 ActivationFunction::GELU => pre_activation.map(|&x| {
713 let a: T = scirs2_core::numeric::NumCast::from(0.797_884_560_802_865)
715 .unwrap_or_else(|| T::one());
716 let c: T =
717 scirs2_core::numeric::NumCast::from(0.044715).unwrap_or_else(|| T::zero());
718 let half: T = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::one());
719 let three: T = scirs2_core::numeric::NumCast::from(3.0).unwrap_or_else(|| T::one());
720 let inner = a * (x + c * x * x * x);
721 let t = inner.tanh();
722 let sech2 = T::one() - t * t;
723 half * (T::one() + t) + half * x * sech2 * a * (T::one() + three * c * x * x)
724 }),
725 }
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732
733 #[test]
734 fn embedding_layer_projects_continuous_inputs() {
735 let embedding = EmbeddingLayer::<f64>::new(6, 4).expect("embedding creation");
736 assert_eq!(embedding.parameter_count(), 24);
737
738 let input = Array2::<f64>::from_shape_fn((3, 6), |(i, j)| 0.1 * (i + j) as f64 - 0.3);
740 let output = embedding.forward(&input).expect("forward");
741 assert_eq!(output.dim(), (3, 4));
742 assert!(
743 output.iter().any(|&v| v != 0.0),
744 "projection produced all zeros"
745 );
746
747 assert!(output.row(0) != output.row(2));
749 }
750
751 #[test]
752 fn embedding_layer_matches_a_manual_matmul() {
753 let mut embedding = EmbeddingLayer::<f64>::new(2, 2).expect("embedding creation");
754 embedding
755 .set_weights(
756 Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).expect("valid shape"),
757 )
758 .expect("weights");
759 let input = Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).expect("valid shape");
760 let output = embedding.forward(&input).expect("forward");
761 assert!((output[[0, 0]] - 4.0).abs() < 1e-12);
762 assert!((output[[0, 1]] - 6.0).abs() < 1e-12);
763 }
764
765 #[test]
766 fn embedding_reset_re_randomizes() {
767 let mut embedding = EmbeddingLayer::<f64>::new(8, 8).expect("embedding creation");
768 let before = embedding.weights().clone();
769 embedding.reset().expect("reset");
770 assert!(embedding.weights().iter().any(|&v| v != 0.0));
771 assert_ne!(*embedding.weights(), before);
772 }
773
774 #[test]
775 fn embedding_rejects_wrong_width() {
776 let embedding = EmbeddingLayer::<f64>::new(6, 4).expect("embedding creation");
777 assert!(embedding.forward(&Array2::<f64>::zeros((2, 5))).is_err());
778 assert!(EmbeddingLayer::<f64>::new(0, 4).is_err());
779 }
780
781 #[test]
782 fn output_projection_produces_nonzero_output() {
783 let projection = OutputProjection::<f64>::new(8, 4).expect("projection creation");
784 let input = Array2::<f64>::ones((2, 8));
785 let output = projection.forward(&input).expect("forward");
786 assert_eq!(output.dim(), (2, 4));
787 assert!(
788 output.iter().any(|&v| v != 0.0),
789 "zero-initialized projection produced all zeros"
790 );
791 assert_eq!(projection.parameter_count(), 8 * 4 + 4);
792 }
793
794 #[test]
795 fn output_projection_reset_re_randomizes() {
796 let mut projection = OutputProjection::<f64>::new(8, 4).expect("projection creation");
797 let before = projection.weights().clone();
798 projection.reset().expect("reset");
799 assert!(projection.weights().iter().any(|&v| v != 0.0));
800 assert_ne!(*projection.weights(), before);
801 }
802
803 #[test]
804 fn layer_normalization_standardizes_and_validates() {
805 let norm = LayerNormalization::<f64>::new(4).expect("layer norm creation");
806 let input =
807 Array2::<f64>::from_shape_vec((1, 4), vec![1.0, 2.0, 3.0, 4.0]).expect("valid shape");
808 let output = norm.forward(&input).expect("forward");
809 let mean: f64 = output.row(0).iter().sum::<f64>() / 4.0;
810 assert!(mean.abs() < 1e-9, "mean {mean}");
811 assert_eq!(norm.parameter_count(), 8);
812
813 assert!(norm.forward(&Array2::<f64>::zeros((2, 8))).is_err());
815 }
816
817 #[test]
818 fn dropout_defaults_to_inference_and_is_reproducible() {
819 let mut dropout = DropoutLayer::with_seed(0.5, 11);
820 let input = Array2::<f64>::ones((8, 8));
821
822 assert!(!dropout.is_training());
823 assert_eq!(dropout.forward(&input), input);
824
825 dropout.set_training(true);
826 let first = dropout.forward(&input);
827 let zeros = first.iter().filter(|&&v| v == 0.0).count();
828 assert!(zeros > 0 && zeros < input.len());
829 assert!(first.iter().all(|&v| v == 0.0 || (v - 2.0).abs() < 1e-12));
830
831 let mut replay = DropoutLayer::with_seed(0.5, 11);
832 replay.set_training(true);
833 assert_eq!(replay.forward(&input), first);
834 }
835
836 #[test]
837 fn residual_connections_validate_shapes() {
838 let residual = ResidualConnections::<f64>::new(4);
839 let input = Array2::<f64>::ones((2, 4));
840 let other = Array2::<f64>::from_elem((2, 4), 0.5);
841 let output = residual.add(&input, &other).expect("residual add");
842 assert!((output[[0, 0]] - 1.5).abs() < 1e-12);
843 assert!(residual.add(&input, &Array2::<f64>::ones((3, 4))).is_err());
844 }
845
846 #[test]
847 fn activation_functions_behave() {
848 let input =
849 Array2::<f64>::from_shape_vec((2, 2), vec![-1.0, 0.0, 0.5, 1.0]).expect("valid shape");
850
851 let relu = ActivationLayer::apply(&input, ActivationFunction::ReLU);
852 assert_eq!(relu[[0, 0]], 0.0);
853 assert_eq!(relu[[1, 1]], 1.0);
854
855 let gelu = ActivationLayer::apply(&input, ActivationFunction::GELU);
856 assert_eq!(gelu.shape(), input.shape());
857
858 let sigmoid = ActivationLayer::apply(&input, ActivationFunction::Sigmoid);
859 assert!(sigmoid.iter().all(|&x| (0.0..=1.0).contains(&x)));
860 }
861
862 #[test]
863 fn activations_stay_finite_for_extreme_inputs() {
864 let input =
865 Array2::<f64>::from_shape_vec((1, 3), vec![-1e6, 0.0, 1e6]).expect("valid shape");
866 for activation in [
867 ActivationFunction::ReLU,
868 ActivationFunction::GELU,
869 ActivationFunction::Swish,
870 ActivationFunction::Tanh,
871 ActivationFunction::Sigmoid,
872 ActivationFunction::LeakyReLU,
873 ] {
874 let output = ActivationLayer::apply(&input, activation);
875 assert!(
876 output.iter().all(|v| v.is_finite()),
877 "{activation:?} produced {output:?}"
878 );
879 }
880 }
881}