Skip to main content

ipfrs_tensorlogic/
weight_initializer.rs

1//! Weight initialization strategies for tensor operations.
2//!
3//! Provides common initialization methods used in deep learning:
4//! - Xavier/Glorot (uniform and normal)
5//! - He/Kaiming (uniform and normal)
6//! - LeCun (uniform and normal)
7//! - Orthogonal initialization
8//! - Sparse initialization
9//! - Truncated normal
10//! - Constant/zeros/ones
11//!
12//! Uses a custom xorshift64 PRNG to avoid external random number dependencies.
13
14use std::f64::consts::PI;
15
16// ---------------------------------------------------------------------------
17// Enums
18// ---------------------------------------------------------------------------
19
20/// Fan mode for calculating fan-in/fan-out
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum FanMode {
23    /// Use fan-in (number of input units)
24    FanIn,
25    /// Use fan-out (number of output units)
26    FanOut,
27    /// Use average of fan-in and fan-out
28    Average,
29}
30
31/// Distribution type for initialization
32#[derive(Debug, Clone, PartialEq)]
33pub enum InitDistribution {
34    /// Uniform distribution
35    Uniform,
36    /// Normal (Gaussian) distribution
37    Normal,
38}
39
40/// Weight initialization strategy
41#[derive(Debug, Clone, PartialEq)]
42pub enum InitStrategy {
43    /// All weights set to zero
44    Zeros,
45    /// All weights set to one
46    Ones,
47    /// All weights set to a constant value
48    Constant(f64),
49    /// Xavier/Glorot initialization: Var = 2/(fan_in + fan_out)
50    Xavier(InitDistribution),
51    /// He initialization: Var = 2/fan
52    He(FanMode, InitDistribution),
53    /// Kaiming initialization (alias for He)
54    Kaiming(FanMode, InitDistribution),
55    /// LeCun initialization: Var = 1/fan_in
56    LeCun(InitDistribution),
57    /// Orthogonal initialization with gain
58    Orthogonal(f64),
59    /// Sparse initialization with sparsity ratio (fraction of zeros)
60    Sparse(f64),
61    /// Truncated normal distribution clamped to [a, b]
62    TruncatedNormal {
63        /// Mean of the normal distribution
64        mean: f64,
65        /// Standard deviation
66        std: f64,
67        /// Lower bound
68        a: f64,
69        /// Upper bound
70        b: f64,
71    },
72}
73
74// ---------------------------------------------------------------------------
75// Config / Shape / Stats
76// ---------------------------------------------------------------------------
77
78/// Configuration for weight initialization.
79#[derive(Debug, Clone)]
80pub struct WeightInitConfig {
81    /// The initialization strategy to use
82    pub strategy: InitStrategy,
83    /// Seed for the PRNG
84    pub seed: u64,
85    /// Gain factor (used by Xavier, He, etc.)
86    pub gain: f64,
87}
88
89impl Default for WeightInitConfig {
90    fn default() -> Self {
91        Self {
92            strategy: InitStrategy::Xavier(InitDistribution::Uniform),
93            seed: 42,
94            gain: 1.0,
95        }
96    }
97}
98
99/// Shape information for computing fan-in/fan-out.
100#[derive(Debug, Clone)]
101pub struct TensorShape {
102    /// Dimensions of the tensor
103    pub dims: Vec<usize>,
104}
105
106impl TensorShape {
107    /// Create a new `TensorShape` from the given dimensions.
108    pub fn new(dims: Vec<usize>) -> Self {
109        Self { dims }
110    }
111
112    /// Total number of elements in the tensor.
113    pub fn numel(&self) -> usize {
114        self.dims.iter().product()
115    }
116}
117
118/// Statistics about initialised weights.
119#[derive(Debug, Clone)]
120pub struct InitStats {
121    /// Total number of parameters
122    pub total_params: u64,
123    /// Mean of the weights
124    pub mean: f64,
125    /// Variance of the weights
126    pub variance: f64,
127    /// Minimum weight value
128    pub min: f64,
129    /// Maximum weight value
130    pub max: f64,
131}
132
133// ---------------------------------------------------------------------------
134// WeightInitializer
135// ---------------------------------------------------------------------------
136
137/// Weight initializer that generates tensors according to a chosen strategy.
138pub struct WeightInitializer {
139    config: WeightInitConfig,
140    rng_state: u64,
141    initialized_count: u64,
142}
143
144impl WeightInitializer {
145    /// Create a new initializer from the given configuration.
146    pub fn new(config: WeightInitConfig) -> Self {
147        let seed = if config.seed == 0 { 1 } else { config.seed };
148        Self {
149            rng_state: seed,
150            config,
151            initialized_count: 0,
152        }
153    }
154
155    // -- Public API ----------------------------------------------------------
156
157    /// Generate weights for the given tensor shape.
158    pub fn initialize(&mut self, shape: &TensorShape) -> Vec<f64> {
159        let n = shape.numel();
160        let (fan_in, fan_out) = Self::compute_fan(shape);
161        let gain = self.config.gain;
162
163        let strategy = self.config.strategy.clone();
164        let weights = match strategy {
165            InitStrategy::Zeros => vec![0.0; n],
166            InitStrategy::Ones => vec![1.0; n],
167            InitStrategy::Constant(v) => vec![v; n],
168            InitStrategy::Xavier(ref dist) => self.xavier_init(n, fan_in, fan_out, gain, dist),
169            InitStrategy::He(ref mode, ref dist) | InitStrategy::Kaiming(ref mode, ref dist) => {
170                self.he_init(n, fan_in, fan_out, gain, mode, dist)
171            }
172            InitStrategy::LeCun(ref dist) => self.lecun_init(n, fan_in, gain, dist),
173            InitStrategy::Orthogonal(g) => self.orthogonal_init(shape, g),
174            InitStrategy::Sparse(sparsity) => self.sparse_init(n, sparsity),
175            InitStrategy::TruncatedNormal { mean, std, a, b } => {
176                self.truncated_normal_init(n, mean, std, a, b)
177            }
178        };
179
180        self.initialized_count += n as u64;
181        weights
182    }
183
184    /// Compute (fan_in, fan_out) from a tensor shape.
185    ///
186    /// - 1D: `(dims[0], dims[0])`
187    /// - 2D: `(dims[1], dims[0])`
188    /// - 3D+: `(dims[1] * receptive, dims[0] * receptive)` where
189    ///   `receptive = product of dims[2..]`
190    pub fn compute_fan(shape: &TensorShape) -> (usize, usize) {
191        match shape.dims.len() {
192            0 => (1, 1),
193            1 => (shape.dims[0], shape.dims[0]),
194            2 => (shape.dims[1], shape.dims[0]),
195            _ => {
196                let receptive: usize = shape.dims[2..].iter().product();
197                let fan_in = shape.dims[1] * receptive;
198                let fan_out = shape.dims[0] * receptive;
199                (fan_in, fan_out)
200            }
201        }
202    }
203
204    /// Compute the Xavier/Glorot bound: `gain * sqrt(6 / (fan_in + fan_out))`.
205    pub fn xavier_bound(fan_in: usize, fan_out: usize, gain: f64) -> f64 {
206        gain * (6.0 / (fan_in + fan_out) as f64).sqrt()
207    }
208
209    /// Compute the He/Kaiming bound: `gain * sqrt(3 / fan)`.
210    pub fn he_bound(fan: usize, gain: f64) -> f64 {
211        gain * (3.0 / fan as f64).sqrt()
212    }
213
214    /// Generate a uniform random value in `[low, high)` using xorshift64.
215    pub fn next_uniform(&mut self, low: f64, high: f64) -> f64 {
216        let u = self.next_unit();
217        low + u * (high - low)
218    }
219
220    /// Generate a normally distributed value via the Box-Muller transform.
221    pub fn next_normal(&mut self, mean: f64, std: f64) -> f64 {
222        let u1 = self.next_unit().max(1e-300); // avoid log(0)
223        let u2 = self.next_unit();
224        let z = (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos();
225        mean + std * z
226    }
227
228    /// Compute basic statistics for a slice of weights.
229    pub fn compute_stats(weights: &[f64]) -> InitStats {
230        if weights.is_empty() {
231            return InitStats {
232                total_params: 0,
233                mean: 0.0,
234                variance: 0.0,
235                min: 0.0,
236                max: 0.0,
237            };
238        }
239
240        let n = weights.len() as f64;
241        let sum: f64 = weights.iter().sum();
242        let mean = sum / n;
243
244        let var_sum: f64 = weights.iter().map(|w| (w - mean).powi(2)).sum();
245        let variance = var_sum / n;
246
247        let min = weights.iter().copied().fold(f64::INFINITY, f64::min);
248        let max = weights.iter().copied().fold(f64::NEG_INFINITY, f64::max);
249
250        InitStats {
251            total_params: weights.len() as u64,
252            mean,
253            variance,
254            min,
255            max,
256        }
257    }
258
259    /// Reset the PRNG seed.
260    pub fn reset_seed(&mut self, seed: u64) {
261        self.rng_state = if seed == 0 { 1 } else { seed };
262    }
263
264    /// Return the total number of weights initialised so far.
265    pub fn initialized_count(&self) -> u64 {
266        self.initialized_count
267    }
268
269    // -- Private helpers -----------------------------------------------------
270
271    /// xorshift64 — returns a value in [0, 1).
272    fn next_unit(&mut self) -> f64 {
273        let mut x = self.rng_state;
274        x ^= x << 13;
275        x ^= x >> 7;
276        x ^= x << 17;
277        self.rng_state = x;
278        // Map to [0, 1)
279        (x as f64) / (u64::MAX as f64)
280    }
281
282    fn xavier_init(
283        &mut self,
284        n: usize,
285        fan_in: usize,
286        fan_out: usize,
287        gain: f64,
288        dist: &InitDistribution,
289    ) -> Vec<f64> {
290        match dist {
291            InitDistribution::Uniform => {
292                let bound = Self::xavier_bound(fan_in, fan_out, gain);
293                (0..n).map(|_| self.next_uniform(-bound, bound)).collect()
294            }
295            InitDistribution::Normal => {
296                let std = gain * (2.0 / (fan_in + fan_out) as f64).sqrt();
297                (0..n).map(|_| self.next_normal(0.0, std)).collect()
298            }
299        }
300    }
301
302    fn he_init(
303        &mut self,
304        n: usize,
305        fan_in: usize,
306        fan_out: usize,
307        gain: f64,
308        mode: &FanMode,
309        dist: &InitDistribution,
310    ) -> Vec<f64> {
311        let fan = match mode {
312            FanMode::FanIn => fan_in,
313            FanMode::FanOut => fan_out,
314            FanMode::Average => (fan_in + fan_out) / 2,
315        };
316        match dist {
317            InitDistribution::Uniform => {
318                let bound = Self::he_bound(fan, gain);
319                (0..n).map(|_| self.next_uniform(-bound, bound)).collect()
320            }
321            InitDistribution::Normal => {
322                let std = gain * (2.0 / fan as f64).sqrt();
323                (0..n).map(|_| self.next_normal(0.0, std)).collect()
324            }
325        }
326    }
327
328    fn lecun_init(
329        &mut self,
330        n: usize,
331        fan_in: usize,
332        gain: f64,
333        dist: &InitDistribution,
334    ) -> Vec<f64> {
335        match dist {
336            InitDistribution::Uniform => {
337                let bound = gain * (3.0 / fan_in as f64).sqrt();
338                (0..n).map(|_| self.next_uniform(-bound, bound)).collect()
339            }
340            InitDistribution::Normal => {
341                let std = gain * (1.0 / fan_in as f64).sqrt();
342                (0..n).map(|_| self.next_normal(0.0, std)).collect()
343            }
344        }
345    }
346
347    /// Orthogonal initialization via QR-like decomposition of a random matrix.
348    ///
349    /// For a 2-D shape (rows, cols) we generate a random matrix and then
350    /// produce an orthonormal basis via a simplified Gram-Schmidt process,
351    /// scaled by `gain`.  For shapes with more than two dimensions we treat
352    /// them as (dims[0], product-of-rest).
353    fn orthogonal_init(&mut self, shape: &TensorShape, gain: f64) -> Vec<f64> {
354        let (rows, cols) = match shape.dims.len() {
355            0 => (1, 1),
356            1 => (shape.dims[0], 1),
357            2 => (shape.dims[0], shape.dims[1]),
358            _ => {
359                let rest: usize = shape.dims[1..].iter().product();
360                (shape.dims[0], rest)
361            }
362        };
363
364        let n = rows.max(cols);
365        let m = rows.min(cols);
366
367        // Generate random n x m matrix
368        let mut mat: Vec<Vec<f64>> = (0..n)
369            .map(|_| (0..m).map(|_| self.next_normal(0.0, 1.0)).collect())
370            .collect();
371
372        // Modified Gram-Schmidt
373        for j in 0..m {
374            // Normalise column j
375            let norm = col_norm(&mat, j, n);
376            if norm > 1e-15 {
377                for row in mat.iter_mut().take(n) {
378                    row[j] /= norm;
379                }
380            }
381            // Subtract projection from remaining columns
382            for k in (j + 1)..m {
383                let dot = col_dot(&mat, j, k, n);
384                for row in mat.iter_mut().take(n) {
385                    row[k] -= dot * row[j];
386                }
387            }
388        }
389
390        // Extract the result (rows x cols), applying gain
391        let mut result = Vec::with_capacity(rows * cols);
392        if rows <= cols {
393            // Take first `rows` rows of the n x m matrix
394            for row in mat.iter().take(rows) {
395                for val in row.iter().take(cols.min(m)) {
396                    result.push(val * gain);
397                }
398                // If cols > m pad with zeros (shouldn't happen with our setup)
399                if cols > m {
400                    result.extend(std::iter::repeat_n(0.0, cols - m));
401                }
402            }
403        } else {
404            // n == rows, m == cols
405            for row in mat.iter().take(rows) {
406                for val in row.iter().take(cols) {
407                    result.push(val * gain);
408                }
409            }
410        }
411
412        result
413    }
414
415    /// Sparse initialisation: a fraction `sparsity` of the weights are zero,
416    /// the rest are drawn from a standard normal.
417    fn sparse_init(&mut self, n: usize, sparsity: f64) -> Vec<f64> {
418        let sparsity = sparsity.clamp(0.0, 1.0);
419        (0..n)
420            .map(|_| {
421                let u = self.next_unit();
422                if u < sparsity {
423                    0.0
424                } else {
425                    self.next_normal(0.0, 1.0)
426                }
427            })
428            .collect()
429    }
430
431    /// Truncated normal: rejection-sample values in [a, b].
432    fn truncated_normal_init(&mut self, n: usize, mean: f64, std: f64, a: f64, b: f64) -> Vec<f64> {
433        (0..n)
434            .map(|_| {
435                // Rejection sampling — cap iterations to avoid infinite loop on
436                // extremely narrow bounds.
437                for _ in 0..1000 {
438                    let v = self.next_normal(mean, std);
439                    if v >= a && v <= b {
440                        return v;
441                    }
442                }
443                // Fallback: clamp
444                self.next_normal(mean, std).clamp(a, b)
445            })
446            .collect()
447    }
448}
449
450// ---------------------------------------------------------------------------
451// Free helpers for orthogonal init
452// ---------------------------------------------------------------------------
453
454/// L2 norm of column `j` across all rows.
455fn col_norm(mat: &[Vec<f64>], j: usize, rows: usize) -> f64 {
456    let s: f64 = mat.iter().take(rows).map(|row| row[j] * row[j]).sum();
457    s.sqrt()
458}
459
460/// Dot product of columns `j` and `k`.
461fn col_dot(mat: &[Vec<f64>], j: usize, k: usize, rows: usize) -> f64 {
462    mat.iter().take(rows).map(|row| row[j] * row[k]).sum()
463}
464
465// ---------------------------------------------------------------------------
466// Tests
467// ---------------------------------------------------------------------------
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    // -- helpers -------------------------------------------------------------
474
475    fn make_init(strategy: InitStrategy) -> WeightInitializer {
476        WeightInitializer::new(WeightInitConfig {
477            strategy,
478            seed: 12345,
479            gain: 1.0,
480        })
481    }
482
483    fn shape2d(r: usize, c: usize) -> TensorShape {
484        TensorShape::new(vec![r, c])
485    }
486
487    // -- Zeros / Ones / Constant --------------------------------------------
488
489    #[test]
490    fn test_zeros() {
491        let mut init = make_init(InitStrategy::Zeros);
492        let w = init.initialize(&shape2d(3, 4));
493        assert_eq!(w.len(), 12);
494        assert!(w.iter().all(|&v| v == 0.0));
495    }
496
497    #[test]
498    fn test_ones() {
499        let mut init = make_init(InitStrategy::Ones);
500        let w = init.initialize(&shape2d(3, 4));
501        assert_eq!(w.len(), 12);
502        assert!(w.iter().all(|&v| v == 1.0));
503    }
504
505    #[test]
506    fn test_constant() {
507        let mut init = make_init(InitStrategy::Constant(std::f64::consts::PI));
508        let w = init.initialize(&shape2d(2, 5));
509        assert_eq!(w.len(), 10);
510        assert!(w.iter().all(|&v| (v - std::f64::consts::PI).abs() < 1e-12));
511    }
512
513    // -- Fan computation ----------------------------------------------------
514
515    #[test]
516    fn test_fan_1d() {
517        let s = TensorShape::new(vec![128]);
518        let (fi, fo) = WeightInitializer::compute_fan(&s);
519        assert_eq!(fi, 128);
520        assert_eq!(fo, 128);
521    }
522
523    #[test]
524    fn test_fan_2d() {
525        let s = shape2d(64, 128);
526        let (fi, fo) = WeightInitializer::compute_fan(&s);
527        assert_eq!(fi, 128);
528        assert_eq!(fo, 64);
529    }
530
531    #[test]
532    fn test_fan_4d() {
533        // Conv2D: [out_channels=32, in_channels=16, kH=3, kW=3]
534        let s = TensorShape::new(vec![32, 16, 3, 3]);
535        let (fi, fo) = WeightInitializer::compute_fan(&s);
536        assert_eq!(fi, 16 * 9);
537        assert_eq!(fo, 32 * 9);
538    }
539
540    #[test]
541    fn test_fan_0d() {
542        let s = TensorShape::new(vec![]);
543        let (fi, fo) = WeightInitializer::compute_fan(&s);
544        assert_eq!(fi, 1);
545        assert_eq!(fo, 1);
546    }
547
548    #[test]
549    fn test_fan_3d() {
550        // [out, in, kernel]
551        let s = TensorShape::new(vec![64, 32, 5]);
552        let (fi, fo) = WeightInitializer::compute_fan(&s);
553        assert_eq!(fi, 32 * 5);
554        assert_eq!(fo, 64 * 5);
555    }
556
557    // -- Xavier -------------------------------------------------------------
558
559    #[test]
560    fn test_xavier_uniform_variance() {
561        let mut init = WeightInitializer::new(WeightInitConfig {
562            strategy: InitStrategy::Xavier(InitDistribution::Uniform),
563            seed: 42,
564            gain: 1.0,
565        });
566        let shape = shape2d(512, 256);
567        let w = init.initialize(&shape);
568
569        let stats = WeightInitializer::compute_stats(&w);
570        // Theoretical variance for uniform[-a,a]: a^2/3
571        // a = sqrt(6/(512+256)), Var = 6/(3*(512+256)) = 2/(512+256)
572        let expected_var = 2.0 / (512.0 + 256.0);
573        let rel_err = (stats.variance - expected_var).abs() / expected_var;
574        assert!(
575            rel_err < 0.1,
576            "Xavier uniform var {:.6} vs expected {:.6}",
577            stats.variance,
578            expected_var
579        );
580    }
581
582    #[test]
583    fn test_xavier_normal_variance() {
584        let mut init = WeightInitializer::new(WeightInitConfig {
585            strategy: InitStrategy::Xavier(InitDistribution::Normal),
586            seed: 42,
587            gain: 1.0,
588        });
589        let shape = shape2d(512, 256);
590        let w = init.initialize(&shape);
591
592        let stats = WeightInitializer::compute_stats(&w);
593        let expected_var = 2.0 / (512.0 + 256.0);
594        let rel_err = (stats.variance - expected_var).abs() / expected_var;
595        assert!(
596            rel_err < 0.15,
597            "Xavier normal var {:.6} vs expected {:.6}",
598            stats.variance,
599            expected_var
600        );
601    }
602
603    // -- He / Kaiming -------------------------------------------------------
604
605    #[test]
606    fn test_he_fan_in_uniform() {
607        let mut init = WeightInitializer::new(WeightInitConfig {
608            strategy: InitStrategy::He(FanMode::FanIn, InitDistribution::Uniform),
609            seed: 77,
610            gain: 1.0,
611        });
612        let shape = shape2d(256, 512);
613        let w = init.initialize(&shape);
614
615        let stats = WeightInitializer::compute_stats(&w);
616        // Var = bound^2 / 3 = (sqrt(3/fan))^2/3 = 3/(3*fan) = 1/fan
617        // He: gain^2 * 2/fan  -> bound = gain*sqrt(3*2/fan) hmm
618        // Actually he_bound = gain * sqrt(3/fan), Var(uniform[-b,b]) = b^2/3
619        // For He fan_in: fan = 512, bound = sqrt(3/512)
620        // Var = 3/(3*512) = 1/512
621        // But He init uses gain * sqrt(3/fan) -> b^2 = 3/512 -> Var = b^2/3 = 1/512
622        // However the theory for He is Var = 2/fan_in. The uniform variant
623        // uses bound = sqrt(6/fan) so Var = 6/(3*fan) = 2/fan.
624        // Our implementation uses he_bound = gain * sqrt(3/fan). Let me check:
625        // That means Var = (3/fan)/3 = 1/fan. Let me re-verify against the code.
626        // The uniform case: bound = he_bound(fan, gain) = gain * sqrt(3/fan)
627        // Var(U[-b,b]) = b^2/3 = gain^2 * 3/(3*fan) = gain^2/fan
628        // For gain=sqrt(2): Var = 2/fan. But we use gain=1 here.
629        // So with gain=1, Var = 1/fan_in = 1/512.
630        let expected_var = 1.0 / 512.0;
631        let rel_err = (stats.variance - expected_var).abs() / expected_var;
632        assert!(
633            rel_err < 0.15,
634            "He fan_in uniform var {:.6} vs expected {:.6}",
635            stats.variance,
636            expected_var
637        );
638    }
639
640    #[test]
641    fn test_he_fan_in_normal() {
642        let mut init = WeightInitializer::new(WeightInitConfig {
643            strategy: InitStrategy::He(FanMode::FanIn, InitDistribution::Normal),
644            seed: 99,
645            gain: 1.0,
646        });
647        let shape = shape2d(256, 512);
648        let w = init.initialize(&shape);
649        let stats = WeightInitializer::compute_stats(&w);
650        // std = gain * sqrt(2/fan_in), Var = gain^2 * 2/fan_in
651        let expected_var = 2.0 / 512.0;
652        let rel_err = (stats.variance - expected_var).abs() / expected_var;
653        assert!(
654            rel_err < 0.15,
655            "He fan_in normal var {:.6} vs expected {:.6}",
656            stats.variance,
657            expected_var
658        );
659    }
660
661    #[test]
662    fn test_kaiming_is_he_alias() {
663        let seed = 555;
664        let shape = shape2d(64, 128);
665
666        let mut init_he = WeightInitializer::new(WeightInitConfig {
667            strategy: InitStrategy::He(FanMode::FanIn, InitDistribution::Uniform),
668            seed,
669            gain: 1.0,
670        });
671        let mut init_kaiming = WeightInitializer::new(WeightInitConfig {
672            strategy: InitStrategy::Kaiming(FanMode::FanIn, InitDistribution::Uniform),
673            seed,
674            gain: 1.0,
675        });
676
677        let w1 = init_he.initialize(&shape);
678        let w2 = init_kaiming.initialize(&shape);
679        assert_eq!(w1, w2, "Kaiming must produce the same weights as He");
680    }
681
682    #[test]
683    fn test_he_fan_out() {
684        let mut init = WeightInitializer::new(WeightInitConfig {
685            strategy: InitStrategy::He(FanMode::FanOut, InitDistribution::Normal),
686            seed: 42,
687            gain: 1.0,
688        });
689        let shape = shape2d(256, 512);
690        let w = init.initialize(&shape);
691        let stats = WeightInitializer::compute_stats(&w);
692        // fan_out = 256, Var = 2/256
693        let expected_var = 2.0 / 256.0;
694        let rel_err = (stats.variance - expected_var).abs() / expected_var;
695        assert!(
696            rel_err < 0.15,
697            "He fan_out normal var {:.6} vs expected {:.6}",
698            stats.variance,
699            expected_var
700        );
701    }
702
703    #[test]
704    fn test_he_average_mode() {
705        let mut init = WeightInitializer::new(WeightInitConfig {
706            strategy: InitStrategy::He(FanMode::Average, InitDistribution::Normal),
707            seed: 42,
708            gain: 1.0,
709        });
710        let shape = shape2d(200, 400);
711        let w = init.initialize(&shape);
712        let stats = WeightInitializer::compute_stats(&w);
713        // fan = (400+200)/2 = 300, Var = 2/300
714        let expected_var = 2.0 / 300.0;
715        let rel_err = (stats.variance - expected_var).abs() / expected_var;
716        assert!(
717            rel_err < 0.15,
718            "He average normal var {:.6} vs expected {:.6}",
719            stats.variance,
720            expected_var
721        );
722    }
723
724    // -- LeCun --------------------------------------------------------------
725
726    #[test]
727    fn test_lecun_normal_variance() {
728        let mut init = WeightInitializer::new(WeightInitConfig {
729            strategy: InitStrategy::LeCun(InitDistribution::Normal),
730            seed: 42,
731            gain: 1.0,
732        });
733        let shape = shape2d(256, 512);
734        let w = init.initialize(&shape);
735        let stats = WeightInitializer::compute_stats(&w);
736        // std = gain * sqrt(1/fan_in), Var = 1/fan_in
737        let expected_var = 1.0 / 512.0;
738        let rel_err = (stats.variance - expected_var).abs() / expected_var;
739        assert!(
740            rel_err < 0.15,
741            "LeCun normal var {:.6} vs expected {:.6}",
742            stats.variance,
743            expected_var
744        );
745    }
746
747    #[test]
748    fn test_lecun_uniform_variance() {
749        let mut init = WeightInitializer::new(WeightInitConfig {
750            strategy: InitStrategy::LeCun(InitDistribution::Uniform),
751            seed: 42,
752            gain: 1.0,
753        });
754        let shape = shape2d(256, 512);
755        let w = init.initialize(&shape);
756        let stats = WeightInitializer::compute_stats(&w);
757        // bound = gain * sqrt(3/fan_in), Var = bound^2/3 = 1/fan_in
758        let expected_var = 1.0 / 512.0;
759        let rel_err = (stats.variance - expected_var).abs() / expected_var;
760        assert!(
761            rel_err < 0.15,
762            "LeCun uniform var {:.6} vs expected {:.6}",
763            stats.variance,
764            expected_var
765        );
766    }
767
768    // -- TruncatedNormal ----------------------------------------------------
769
770    #[test]
771    fn test_truncated_normal_bounds() {
772        let mut init = make_init(InitStrategy::TruncatedNormal {
773            mean: 0.0,
774            std: 1.0,
775            a: -1.5,
776            b: 1.5,
777        });
778        let w = init.initialize(&TensorShape::new(vec![10000]));
779        assert!(w.iter().all(|&v| (-1.5..=1.5).contains(&v)));
780    }
781
782    #[test]
783    fn test_truncated_normal_mean_near_zero() {
784        let mut init = make_init(InitStrategy::TruncatedNormal {
785            mean: 0.0,
786            std: 1.0,
787            a: -2.0,
788            b: 2.0,
789        });
790        let w = init.initialize(&TensorShape::new(vec![50000]));
791        let stats = WeightInitializer::compute_stats(&w);
792        assert!(
793            stats.mean.abs() < 0.05,
794            "truncated normal mean should be near 0: {}",
795            stats.mean
796        );
797    }
798
799    // -- Orthogonal ---------------------------------------------------------
800
801    #[test]
802    fn test_orthogonal_semi_orthogonal() {
803        let mut init = make_init(InitStrategy::Orthogonal(1.0));
804        let shape = shape2d(4, 4);
805        let w = init.initialize(&shape);
806        assert_eq!(w.len(), 16);
807
808        // Check that Q^T Q ≈ I (columns are orthonormal)
809        let rows = 4;
810        let cols = 4;
811        for i in 0..cols {
812            for j in 0..cols {
813                let dot: f64 = (0..rows).map(|r| w[r * cols + i] * w[r * cols + j]).sum();
814                if i == j {
815                    assert!(
816                        (dot - 1.0).abs() < 0.1,
817                        "diagonal ({},{}) should be ~1.0, got {}",
818                        i,
819                        j,
820                        dot,
821                    );
822                } else {
823                    assert!(
824                        dot.abs() < 0.1,
825                        "off-diagonal ({},{}) should be ~0.0, got {}",
826                        i,
827                        j,
828                        dot,
829                    );
830                }
831            }
832        }
833    }
834
835    #[test]
836    fn test_orthogonal_rectangular() {
837        let mut init = make_init(InitStrategy::Orthogonal(1.0));
838        let shape = shape2d(8, 4);
839        let w = init.initialize(&shape);
840        assert_eq!(w.len(), 32);
841
842        // For tall matrix (rows > cols), Q^T Q ≈ I (cols x cols identity)
843        let cols = 4;
844        let rows = 8;
845        for i in 0..cols {
846            for j in 0..cols {
847                let dot: f64 = (0..rows).map(|r| w[r * cols + i] * w[r * cols + j]).sum();
848                if i == j {
849                    assert!(
850                        (dot - 1.0).abs() < 0.15,
851                        "orthogonal rect diag ({},{}) = {}",
852                        i,
853                        j,
854                        dot,
855                    );
856                } else {
857                    assert!(
858                        dot.abs() < 0.15,
859                        "orthogonal rect off-diag ({},{}) = {}",
860                        i,
861                        j,
862                        dot,
863                    );
864                }
865            }
866        }
867    }
868
869    #[test]
870    fn test_orthogonal_with_gain() {
871        let mut init = make_init(InitStrategy::Orthogonal(2.0));
872        let shape = shape2d(4, 4);
873        let w = init.initialize(&shape);
874
875        // Column norms should be approximately `gain = 2.0`
876        for j in 0..4 {
877            let norm: f64 = (0..4).map(|i| w[i * 4 + j].powi(2)).sum::<f64>().sqrt();
878            assert!(
879                (norm - 2.0).abs() < 0.3,
880                "column {} norm should be ~2.0, got {}",
881                j,
882                norm,
883            );
884        }
885    }
886
887    // -- Sparse -------------------------------------------------------------
888
889    #[test]
890    fn test_sparse_ratio() {
891        let mut init = make_init(InitStrategy::Sparse(0.8));
892        let w = init.initialize(&TensorShape::new(vec![10000]));
893        let zeros = w.iter().filter(|&&v| v == 0.0).count();
894        let ratio = zeros as f64 / w.len() as f64;
895        assert!(
896            (ratio - 0.8).abs() < 0.05,
897            "sparse ratio {:.3} vs expected 0.8",
898            ratio,
899        );
900    }
901
902    #[test]
903    fn test_sparse_zero_sparsity() {
904        let mut init = make_init(InitStrategy::Sparse(0.0));
905        let w = init.initialize(&TensorShape::new(vec![1000]));
906        let zeros = w.iter().filter(|&&v| v == 0.0).count();
907        assert_eq!(zeros, 0, "sparsity=0 should produce no zeros");
908    }
909
910    #[test]
911    fn test_sparse_full_sparsity() {
912        let mut init = make_init(InitStrategy::Sparse(1.0));
913        let w = init.initialize(&TensorShape::new(vec![1000]));
914        assert!(
915            w.iter().all(|&v| v == 0.0),
916            "sparsity=1 should be all zeros"
917        );
918    }
919
920    // -- Stats --------------------------------------------------------------
921
922    #[test]
923    fn test_stats_basic() {
924        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
925        let stats = WeightInitializer::compute_stats(&data);
926        assert_eq!(stats.total_params, 5);
927        assert!((stats.mean - 3.0).abs() < 1e-12);
928        assert!((stats.variance - 2.0).abs() < 1e-12);
929        assert!((stats.min - 1.0).abs() < 1e-12);
930        assert!((stats.max - 5.0).abs() < 1e-12);
931    }
932
933    #[test]
934    fn test_stats_empty() {
935        let stats = WeightInitializer::compute_stats(&[]);
936        assert_eq!(stats.total_params, 0);
937        assert_eq!(stats.mean, 0.0);
938        assert_eq!(stats.variance, 0.0);
939    }
940
941    #[test]
942    fn test_stats_single() {
943        let stats = WeightInitializer::compute_stats(&[7.0]);
944        assert_eq!(stats.total_params, 1);
945        assert!((stats.mean - 7.0).abs() < 1e-12);
946        assert!((stats.variance).abs() < 1e-12);
947        assert!((stats.min - 7.0).abs() < 1e-12);
948        assert!((stats.max - 7.0).abs() < 1e-12);
949    }
950
951    // -- Seed reproducibility -----------------------------------------------
952
953    #[test]
954    fn test_same_seed_same_weights() {
955        let shape = shape2d(32, 64);
956        let mut a = WeightInitializer::new(WeightInitConfig {
957            strategy: InitStrategy::Xavier(InitDistribution::Normal),
958            seed: 999,
959            gain: 1.0,
960        });
961        let mut b = WeightInitializer::new(WeightInitConfig {
962            strategy: InitStrategy::Xavier(InitDistribution::Normal),
963            seed: 999,
964            gain: 1.0,
965        });
966        assert_eq!(a.initialize(&shape), b.initialize(&shape));
967    }
968
969    #[test]
970    fn test_different_seed_different_weights() {
971        let shape = shape2d(32, 64);
972        let mut a = WeightInitializer::new(WeightInitConfig {
973            strategy: InitStrategy::Xavier(InitDistribution::Normal),
974            seed: 111,
975            gain: 1.0,
976        });
977        let mut b = WeightInitializer::new(WeightInitConfig {
978            strategy: InitStrategy::Xavier(InitDistribution::Normal),
979            seed: 222,
980            gain: 1.0,
981        });
982        assert_ne!(a.initialize(&shape), b.initialize(&shape));
983    }
984
985    #[test]
986    fn test_reset_seed() {
987        let shape = shape2d(16, 16);
988        let mut init = WeightInitializer::new(WeightInitConfig {
989            strategy: InitStrategy::He(FanMode::FanIn, InitDistribution::Uniform),
990            seed: 42,
991            gain: 1.0,
992        });
993        let w1 = init.initialize(&shape);
994        init.reset_seed(42);
995        let w2 = init.initialize(&shape);
996        assert_eq!(w1, w2, "reset_seed should reproduce identical weights");
997    }
998
999    // -- Large tensors ------------------------------------------------------
1000
1001    #[test]
1002    fn test_large_tensor() {
1003        let mut init = make_init(InitStrategy::Xavier(InitDistribution::Uniform));
1004        let shape = TensorShape::new(vec![100, 100]);
1005        let w = init.initialize(&shape);
1006        assert_eq!(w.len(), 10_000);
1007        let stats = WeightInitializer::compute_stats(&w);
1008        assert!(
1009            stats.mean.abs() < 0.05,
1010            "large Xavier mean should be near zero"
1011        );
1012    }
1013
1014    #[test]
1015    fn test_very_large_tensor() {
1016        let mut init = make_init(InitStrategy::He(FanMode::FanIn, InitDistribution::Normal));
1017        let shape = TensorShape::new(vec![50, 50]);
1018        let w = init.initialize(&shape);
1019        assert_eq!(w.len(), 2500);
1020        assert!(w.iter().all(|v| v.is_finite()));
1021    }
1022
1023    // -- initialized_count ---------------------------------------------------
1024
1025    #[test]
1026    fn test_initialized_count() {
1027        let mut init = make_init(InitStrategy::Zeros);
1028        assert_eq!(init.initialized_count(), 0);
1029        init.initialize(&shape2d(3, 4));
1030        assert_eq!(init.initialized_count(), 12);
1031        init.initialize(&TensorShape::new(vec![10]));
1032        assert_eq!(init.initialized_count(), 22);
1033    }
1034
1035    // -- Xavier bound / He bound helpers ------------------------------------
1036
1037    #[test]
1038    fn test_xavier_bound_value() {
1039        let b = WeightInitializer::xavier_bound(256, 512, 1.0);
1040        let expected = (6.0 / 768.0_f64).sqrt();
1041        assert!((b - expected).abs() < 1e-12);
1042    }
1043
1044    #[test]
1045    fn test_he_bound_value() {
1046        let b = WeightInitializer::he_bound(512, 1.0);
1047        let expected = (3.0 / 512.0_f64).sqrt();
1048        assert!((b - expected).abs() < 1e-12);
1049    }
1050
1051    // -- Gain factor --------------------------------------------------------
1052
1053    #[test]
1054    fn test_xavier_with_gain() {
1055        let mut init = WeightInitializer::new(WeightInitConfig {
1056            strategy: InitStrategy::Xavier(InitDistribution::Normal),
1057            seed: 42,
1058            gain: 2.0,
1059        });
1060        let shape = shape2d(256, 256);
1061        let w = init.initialize(&shape);
1062        let stats = WeightInitializer::compute_stats(&w);
1063        // Var = gain^2 * 2/(fan_in+fan_out) = 4 * 2/512 = 8/512
1064        let expected_var = 4.0 * 2.0 / 512.0;
1065        let rel_err = (stats.variance - expected_var).abs() / expected_var;
1066        assert!(
1067            rel_err < 0.15,
1068            "Xavier gain=2 var {:.6} vs {:.6}",
1069            stats.variance,
1070            expected_var
1071        );
1072    }
1073
1074    // -- numel --------------------------------------------------------------
1075
1076    #[test]
1077    fn test_numel() {
1078        assert_eq!(TensorShape::new(vec![3, 4, 5]).numel(), 60);
1079        assert_eq!(TensorShape::new(vec![]).numel(), 1); // empty product = 1
1080        assert_eq!(TensorShape::new(vec![7]).numel(), 7);
1081    }
1082
1083    // -- Default config -----------------------------------------------------
1084
1085    #[test]
1086    fn test_default_config() {
1087        let cfg = WeightInitConfig::default();
1088        assert_eq!(cfg.seed, 42);
1089        assert!((cfg.gain - 1.0).abs() < 1e-12);
1090        assert_eq!(
1091            cfg.strategy,
1092            InitStrategy::Xavier(InitDistribution::Uniform)
1093        );
1094    }
1095}