Skip to main content

trustformers_debug/
weight_analyzer.rs

1//! Weight distribution analysis tools
2//!
3//! This module provides tools to analyze weight distributions in neural networks,
4//! including dead neuron detection, initialization validation, and statistical analysis.
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::Path;
10
11/// Weight distribution analyzer for model inspection
12#[derive(Debug)]
13pub struct WeightAnalyzer {
14    /// Stored weight analyses by layer name
15    analyses: HashMap<String, WeightAnalysis>,
16    /// Configuration
17    config: WeightAnalyzerConfig,
18}
19
20/// Configuration for weight analysis
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct WeightAnalyzerConfig {
23    /// Threshold for dead neuron detection (absolute value)
24    pub dead_neuron_threshold: f64,
25    /// Number of histogram bins
26    pub num_bins: usize,
27    /// Check for initialization issues
28    pub check_initialization: bool,
29    /// Expected initialization schemes to validate against
30    pub expected_init_schemes: Vec<InitializationScheme>,
31}
32
33impl Default for WeightAnalyzerConfig {
34    fn default() -> Self {
35        Self {
36            dead_neuron_threshold: 1e-8,
37            num_bins: 50,
38            check_initialization: true,
39            expected_init_schemes: vec![
40                InitializationScheme::XavierUniform,
41                InitializationScheme::HeNormal,
42            ],
43        }
44    }
45}
46
47/// Initialization schemes for validation
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49pub enum InitializationScheme {
50    /// Xavier/Glorot uniform initialization
51    XavierUniform,
52    /// Xavier/Glorot normal initialization
53    XavierNormal,
54    /// He uniform initialization
55    HeUniform,
56    /// He normal initialization
57    HeNormal,
58    /// LeCun normal initialization
59    LeCunNormal,
60    /// Orthogonal initialization
61    Orthogonal,
62    /// Uniform initialization
63    Uniform,
64    /// Normal initialization
65    Normal,
66}
67
68/// Analysis results for a layer's weights
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct WeightAnalysis {
71    /// Layer name
72    pub layer_name: String,
73    /// Weight statistics
74    pub statistics: WeightStatistics,
75    /// Dead neurons detected
76    pub dead_neurons: Vec<usize>,
77    /// Histogram data
78    pub histogram: WeightHistogram,
79    /// Likely initialization scheme
80    pub likely_init_scheme: Option<InitializationScheme>,
81    /// Initialization warnings
82    pub init_warnings: Vec<String>,
83}
84
85/// Statistical summary of weights
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct WeightStatistics {
88    /// Mean weight value
89    pub mean: f64,
90    /// Standard deviation
91    pub std_dev: f64,
92    /// Minimum value
93    pub min: f64,
94    /// Maximum value
95    pub max: f64,
96    /// Median value
97    pub median: f64,
98    /// 25th percentile
99    pub q25: f64,
100    /// 75th percentile
101    pub q75: f64,
102    /// Skewness
103    pub skewness: f64,
104    /// Kurtosis
105    pub kurtosis: f64,
106    /// L1 norm
107    pub l1_norm: f64,
108    /// L2 norm
109    pub l2_norm: f64,
110    /// Number of zero weights
111    pub num_zeros: usize,
112    /// Sparsity ratio
113    pub sparsity: f64,
114}
115
116/// Histogram of weight distribution
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct WeightHistogram {
119    /// Bin edges
120    pub bin_edges: Vec<f64>,
121    /// Bin counts
122    pub bin_counts: Vec<usize>,
123    /// Total count
124    pub total_count: usize,
125}
126
127impl WeightAnalyzer {
128    /// Create a new weight analyzer
129    ///
130    /// # Example
131    ///
132    /// ```
133    /// use trustformers_debug::WeightAnalyzer;
134    ///
135    /// let analyzer = WeightAnalyzer::new();
136    /// ```
137    pub fn new() -> Self {
138        Self {
139            analyses: HashMap::new(),
140            config: WeightAnalyzerConfig::default(),
141        }
142    }
143
144    /// Create a weight analyzer with custom configuration
145    pub fn with_config(config: WeightAnalyzerConfig) -> Self {
146        Self {
147            analyses: HashMap::new(),
148            config,
149        }
150    }
151
152    /// Analyze weights from a layer
153    ///
154    /// # Arguments
155    ///
156    /// * `layer_name` - Name of the layer
157    /// * `weights` - Weight values
158    ///
159    /// # Example
160    ///
161    /// ```
162    /// # use trustformers_debug::WeightAnalyzer;
163    /// # let mut analyzer = WeightAnalyzer::new();
164    /// let weights = vec![0.1, 0.2, 0.05, 0.15, 0.3];
165    /// let analysis = analyzer.analyze("layer1", &weights).unwrap();
166    /// ```
167    pub fn analyze(&mut self, layer_name: &str, weights: &[f64]) -> Result<&WeightAnalysis> {
168        let statistics = self.compute_statistics(weights)?;
169        let dead_neurons = self.detect_dead_neurons(weights);
170        let histogram = self.compute_histogram(weights)?;
171        let (likely_init_scheme, init_warnings) = if self.config.check_initialization {
172            self.check_initialization(&statistics)
173        } else {
174            (None, Vec::new())
175        };
176
177        let analysis = WeightAnalysis {
178            layer_name: layer_name.to_string(),
179            statistics,
180            dead_neurons,
181            histogram,
182            likely_init_scheme,
183            init_warnings,
184        };
185
186        self.analyses.insert(layer_name.to_string(), analysis);
187        self.analyses
188            .get(layer_name)
189            .ok_or_else(|| anyhow::anyhow!("analysis should exist after insert"))
190    }
191
192    /// Compute statistics for weights
193    fn compute_statistics(&self, weights: &[f64]) -> Result<WeightStatistics> {
194        if weights.is_empty() {
195            anyhow::bail!("Cannot compute statistics for empty weight array");
196        }
197
198        let n = weights.len() as f64;
199        let mean = weights.iter().sum::<f64>() / n;
200
201        let variance = weights
202            .iter()
203            .map(|&x| {
204                let diff = x - mean;
205                diff * diff
206            })
207            .sum::<f64>()
208            / n;
209        let std_dev = variance.sqrt();
210
211        let mut sorted = weights.to_vec();
212        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
213
214        let min = sorted[0];
215        let max = sorted[sorted.len() - 1];
216        let median = percentile(&sorted, 50.0);
217        let q25 = percentile(&sorted, 25.0);
218        let q75 = percentile(&sorted, 75.0);
219
220        // Compute skewness
221        let skewness = if std_dev > 0.0 {
222            weights
223                .iter()
224                .map(|&x| {
225                    let z = (x - mean) / std_dev;
226                    z * z * z
227                })
228                .sum::<f64>()
229                / n
230        } else {
231            0.0
232        };
233
234        // Compute kurtosis
235        let kurtosis = if std_dev > 0.0 {
236            weights
237                .iter()
238                .map(|&x| {
239                    let z = (x - mean) / std_dev;
240                    z * z * z * z
241                })
242                .sum::<f64>()
243                / n
244                - 3.0
245        } else {
246            0.0
247        };
248
249        let l1_norm = weights.iter().map(|x| x.abs()).sum::<f64>();
250        let l2_norm = weights.iter().map(|x| x * x).sum::<f64>().sqrt();
251
252        let num_zeros = weights.iter().filter(|&&x| x.abs() < 1e-10).count();
253        let sparsity = num_zeros as f64 / n;
254
255        Ok(WeightStatistics {
256            mean,
257            std_dev,
258            min,
259            max,
260            median,
261            q25,
262            q75,
263            skewness,
264            kurtosis,
265            l1_norm,
266            l2_norm,
267            num_zeros,
268            sparsity,
269        })
270    }
271
272    /// Detect dead neurons (weights close to zero)
273    fn detect_dead_neurons(&self, weights: &[f64]) -> Vec<usize> {
274        weights
275            .iter()
276            .enumerate()
277            .filter_map(
278                |(i, &w)| {
279                    if w.abs() < self.config.dead_neuron_threshold {
280                        Some(i)
281                    } else {
282                        None
283                    }
284                },
285            )
286            .collect()
287    }
288
289    /// Compute histogram of weight distribution
290    fn compute_histogram(&self, weights: &[f64]) -> Result<WeightHistogram> {
291        if weights.is_empty() {
292            anyhow::bail!("Cannot compute histogram for empty weight array");
293        }
294
295        let min = weights.iter().fold(f64::INFINITY, |a, &b| a.min(b));
296        let max = weights.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
297
298        let bin_width = (max - min) / self.config.num_bins as f64;
299        let mut bin_counts = vec![0; self.config.num_bins];
300
301        for &weight in weights {
302            let bin_idx =
303                if bin_width > 0.0 { ((weight - min) / bin_width).floor() as usize } else { 0 };
304            let bin_idx = bin_idx.min(self.config.num_bins - 1);
305            bin_counts[bin_idx] += 1;
306        }
307
308        let bin_edges: Vec<f64> =
309            (0..=self.config.num_bins).map(|i| min + i as f64 * bin_width).collect();
310
311        Ok(WeightHistogram {
312            bin_edges,
313            bin_counts,
314            total_count: weights.len(),
315        })
316    }
317
318    /// Check initialization scheme and detect issues
319    fn check_initialization(
320        &self,
321        stats: &WeightStatistics,
322    ) -> (Option<InitializationScheme>, Vec<String>) {
323        let mut warnings = Vec::new();
324        let mut likely_scheme = None;
325
326        // Check if weights are all zero (not initialized)
327        if stats.sparsity > 0.99 {
328            warnings.push("Weights appear to be uninitialized (all zeros)".to_string());
329            return (None, warnings);
330        }
331
332        // Check if weights are too large
333        if stats.std_dev > 1.0 {
334            warnings.push(format!(
335                "Weights have high variance (std_dev={:.4}), may cause gradient explosion",
336                stats.std_dev
337            ));
338        }
339
340        // Check if weights are too small
341        if stats.std_dev < 0.001 {
342            warnings.push(format!(
343                "Weights have very low variance (std_dev={:.4}), may cause gradient vanishing",
344                stats.std_dev
345            ));
346        }
347
348        // Infer initialization scheme based on statistics
349        // Xavier: mean ~ 0, std ~ sqrt(2 / (fan_in + fan_out))
350        // He: mean ~ 0, std ~ sqrt(2 / fan_in)
351        // Normal: mean ~ 0, std ~ some constant
352
353        if stats.mean.abs() < 0.01 {
354            // Mean is close to zero, likely a good initialization
355            if stats.std_dev > 0.01 && stats.std_dev < 0.2 {
356                // Check distribution shape
357                if stats.skewness.abs() < 0.5 && stats.kurtosis.abs() < 1.0 {
358                    likely_scheme = Some(InitializationScheme::XavierNormal);
359                } else {
360                    likely_scheme = Some(InitializationScheme::Normal);
361                }
362            } else if stats.std_dev < 0.01 {
363                likely_scheme = Some(InitializationScheme::Uniform);
364            }
365        }
366
367        (likely_scheme, warnings)
368    }
369
370    /// Get analysis for a specific layer
371    pub fn get_analysis(&self, layer_name: &str) -> Option<&WeightAnalysis> {
372        self.analyses.get(layer_name)
373    }
374
375    /// Get all layer names with analyses
376    pub fn get_layer_names(&self) -> Vec<String> {
377        self.analyses.keys().cloned().collect()
378    }
379
380    /// Print summary of all analyses
381    pub fn print_summary(&self) -> String {
382        let mut output = String::new();
383        output.push_str("Weight Distribution Summary\n");
384        output.push_str(&"=".repeat(80));
385        output.push('\n');
386
387        for (layer_name, analysis) in &self.analyses {
388            output.push_str(&format!("\nLayer: {}\n", layer_name));
389            output.push_str(&format!("  Mean: {:.6}\n", analysis.statistics.mean));
390            output.push_str(&format!("  Std Dev: {:.6}\n", analysis.statistics.std_dev));
391            output.push_str(&format!(
392                "  Range: [{:.6}, {:.6}]\n",
393                analysis.statistics.min, analysis.statistics.max
394            ));
395            output.push_str(&format!("  Median: {:.6}\n", analysis.statistics.median));
396            output.push_str(&format!(
397                "  Sparsity: {:.2}%\n",
398                analysis.statistics.sparsity * 100.0
399            ));
400            output.push_str(&format!(
401                "  Dead Neurons: {} ({:.2}%)\n",
402                analysis.dead_neurons.len(),
403                analysis.dead_neurons.len() as f64 / analysis.histogram.total_count as f64 * 100.0
404            ));
405
406            if let Some(scheme) = analysis.likely_init_scheme {
407                output.push_str(&format!("  Likely Init: {:?}\n", scheme));
408            }
409
410            if !analysis.init_warnings.is_empty() {
411                output.push_str("  Warnings:\n");
412                for warning in &analysis.init_warnings {
413                    output.push_str(&format!("    - {}\n", warning));
414                }
415            }
416        }
417
418        output
419    }
420
421    /// Export analysis to JSON
422    pub fn export_to_json(&self, layer_name: &str, output_path: &Path) -> Result<()> {
423        let analysis = self
424            .analyses
425            .get(layer_name)
426            .ok_or_else(|| anyhow::anyhow!("Layer {} not found", layer_name))?;
427
428        let json = serde_json::to_string_pretty(analysis)?;
429        std::fs::write(output_path, json)?;
430
431        Ok(())
432    }
433
434    /// Plot weight distribution as ASCII histogram
435    pub fn plot_distribution_ascii(&self, layer_name: &str) -> Result<String> {
436        let analysis = self
437            .analyses
438            .get(layer_name)
439            .ok_or_else(|| anyhow::anyhow!("Layer {} not found", layer_name))?;
440
441        let histogram = &analysis.histogram;
442        let max_count = histogram.bin_counts.iter().max().unwrap_or(&0);
443        let scale = if *max_count > 0 { 50.0 / *max_count as f64 } else { 1.0 };
444
445        let mut output = String::new();
446        output.push_str(&format!("Weight Distribution: {}\n", layer_name));
447        output.push_str(&"=".repeat(60));
448        output.push('\n');
449
450        for i in 0..histogram.bin_counts.len() {
451            let bar_length = (histogram.bin_counts[i] as f64 * scale) as usize;
452            let bar = "█".repeat(bar_length);
453            output.push_str(&format!(
454                "{:8.3} - {:8.3} | {} ({})\n",
455                histogram.bin_edges[i],
456                histogram.bin_edges[i + 1],
457                bar,
458                histogram.bin_counts[i]
459            ));
460        }
461
462        output.push_str("\nStatistics:\n");
463        output.push_str(&format!("  Mean: {:.6}\n", analysis.statistics.mean));
464        output.push_str(&format!("  Std Dev: {:.6}\n", analysis.statistics.std_dev));
465        output.push_str(&format!(
466            "  Skewness: {:.6}\n",
467            analysis.statistics.skewness
468        ));
469        output.push_str(&format!(
470            "  Kurtosis: {:.6}\n",
471            analysis.statistics.kurtosis
472        ));
473
474        Ok(output)
475    }
476
477    /// Clear all stored analyses
478    pub fn clear(&mut self) {
479        self.analyses.clear();
480    }
481
482    /// Get number of analyzed layers
483    pub fn num_layers(&self) -> usize {
484        self.analyses.len()
485    }
486}
487
488impl Default for WeightAnalyzer {
489    fn default() -> Self {
490        Self::new()
491    }
492}
493
494/// Helper function to compute percentile
495fn percentile(sorted_values: &[f64], p: f64) -> f64 {
496    if sorted_values.is_empty() {
497        return 0.0;
498    }
499
500    let index = (p / 100.0 * (sorted_values.len() - 1) as f64).round() as usize;
501    sorted_values[index.min(sorted_values.len() - 1)]
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use std::env;
508
509    #[test]
510    fn test_weight_analyzer_creation() {
511        let analyzer = WeightAnalyzer::new();
512        assert_eq!(analyzer.num_layers(), 0);
513    }
514
515    #[test]
516    fn test_analyze_weights() {
517        let mut analyzer = WeightAnalyzer::new();
518        let weights = vec![0.1, 0.2, 0.15, 0.3, 0.25];
519
520        let analysis = analyzer.analyze("layer1", &weights).expect("operation failed in test");
521        assert_eq!(analysis.layer_name, "layer1");
522        assert!(analysis.statistics.mean > 0.0);
523        assert!(analysis.statistics.std_dev > 0.0);
524    }
525
526    #[test]
527    fn test_dead_neuron_detection() {
528        let mut analyzer = WeightAnalyzer::new();
529        let weights = vec![0.1, 0.0, 0.2, 0.0, 0.3]; // Two dead neurons
530
531        let analysis = analyzer.analyze("layer1", &weights).expect("operation failed in test");
532        assert_eq!(analysis.dead_neurons.len(), 2);
533    }
534
535    #[test]
536    fn test_compute_histogram() {
537        let analyzer = WeightAnalyzer::new();
538        let weights: Vec<f64> = (0..100).map(|x| x as f64 / 100.0).collect();
539
540        let histogram = analyzer.compute_histogram(&weights).expect("operation failed in test");
541        assert_eq!(histogram.bin_edges.len(), analyzer.config.num_bins + 1);
542        assert_eq!(histogram.total_count, 100);
543    }
544
545    #[test]
546    fn test_weight_statistics() {
547        let analyzer = WeightAnalyzer::new();
548        let weights = vec![1.0, 2.0, 3.0, 4.0, 5.0];
549
550        let stats = analyzer.compute_statistics(&weights).expect("operation failed in test");
551        assert_eq!(stats.mean, 3.0);
552        assert!(stats.std_dev > 0.0);
553        assert_eq!(stats.min, 1.0);
554        assert_eq!(stats.max, 5.0);
555    }
556
557    #[test]
558    fn test_initialization_check() {
559        let analyzer = WeightAnalyzer::new();
560
561        // Simulate Xavier-like initialization
562        let stats = WeightStatistics {
563            mean: 0.001,
564            std_dev: 0.05,
565            min: -0.15,
566            max: 0.15,
567            median: 0.0,
568            q25: -0.03,
569            q75: 0.03,
570            skewness: 0.1,
571            kurtosis: 0.2,
572            l1_norm: 10.0,
573            l2_norm: 5.0,
574            num_zeros: 0,
575            sparsity: 0.0,
576        };
577
578        let (scheme, warnings) = analyzer.check_initialization(&stats);
579        assert!(scheme.is_some());
580        assert!(warnings.is_empty() || warnings.len() <= 1);
581    }
582
583    #[test]
584    fn test_export_to_json() {
585        let temp_dir = env::temp_dir();
586        let output_path = temp_dir.join("weight_analysis.json");
587
588        let mut analyzer = WeightAnalyzer::new();
589        analyzer.analyze("layer1", &[1.0, 2.0, 3.0]).expect("operation failed in test");
590
591        analyzer
592            .export_to_json("layer1", &output_path)
593            .expect("operation failed in test");
594        assert!(output_path.exists());
595
596        // Clean up
597        let _ = std::fs::remove_file(output_path);
598    }
599
600    #[test]
601    fn test_plot_distribution_ascii() {
602        let mut analyzer = WeightAnalyzer::new();
603        let weights: Vec<f64> = (0..100).map(|x| x as f64 / 100.0).collect();
604
605        analyzer.analyze("layer1", &weights).expect("operation failed in test");
606
607        let ascii_plot =
608            analyzer.plot_distribution_ascii("layer1").expect("operation failed in test");
609        assert!(ascii_plot.contains("Weight Distribution"));
610        assert!(ascii_plot.contains("layer1"));
611        assert!(ascii_plot.contains("Statistics"));
612    }
613
614    #[test]
615    fn test_print_summary() {
616        let mut analyzer = WeightAnalyzer::new();
617
618        analyzer.analyze("layer1", &[1.0, 2.0, 3.0]).expect("operation failed in test");
619        analyzer.analyze("layer2", &[0.5, 1.0, 1.5]).expect("operation failed in test");
620
621        let summary = analyzer.print_summary();
622        assert!(summary.contains("layer1"));
623        assert!(summary.contains("layer2"));
624        assert!(summary.contains("Mean"));
625        assert!(summary.contains("Std Dev"));
626    }
627
628    #[test]
629    fn test_sparsity_calculation() {
630        let analyzer = WeightAnalyzer::new();
631        let weights = vec![0.0, 0.0, 0.0, 1.0, 0.0];
632
633        let stats = analyzer.compute_statistics(&weights).expect("operation failed in test");
634        assert_eq!(stats.num_zeros, 4);
635        assert_eq!(stats.sparsity, 0.8);
636    }
637
638    #[test]
639    fn test_clear_analyses() {
640        let mut analyzer = WeightAnalyzer::new();
641
642        analyzer.analyze("layer1", &[1.0]).expect("operation failed in test");
643        analyzer.analyze("layer2", &[2.0]).expect("operation failed in test");
644
645        assert_eq!(analyzer.num_layers(), 2);
646
647        analyzer.clear();
648        assert_eq!(analyzer.num_layers(), 0);
649    }
650}