1use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::Path;
10
11#[derive(Debug)]
13pub struct WeightAnalyzer {
14 analyses: HashMap<String, WeightAnalysis>,
16 config: WeightAnalyzerConfig,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct WeightAnalyzerConfig {
23 pub dead_neuron_threshold: f64,
25 pub num_bins: usize,
27 pub check_initialization: bool,
29 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49pub enum InitializationScheme {
50 XavierUniform,
52 XavierNormal,
54 HeUniform,
56 HeNormal,
58 LeCunNormal,
60 Orthogonal,
62 Uniform,
64 Normal,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct WeightAnalysis {
71 pub layer_name: String,
73 pub statistics: WeightStatistics,
75 pub dead_neurons: Vec<usize>,
77 pub histogram: WeightHistogram,
79 pub likely_init_scheme: Option<InitializationScheme>,
81 pub init_warnings: Vec<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct WeightStatistics {
88 pub mean: f64,
90 pub std_dev: f64,
92 pub min: f64,
94 pub max: f64,
96 pub median: f64,
98 pub q25: f64,
100 pub q75: f64,
102 pub skewness: f64,
104 pub kurtosis: f64,
106 pub l1_norm: f64,
108 pub l2_norm: f64,
110 pub num_zeros: usize,
112 pub sparsity: f64,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct WeightHistogram {
119 pub bin_edges: Vec<f64>,
121 pub bin_counts: Vec<usize>,
123 pub total_count: usize,
125}
126
127impl WeightAnalyzer {
128 pub fn new() -> Self {
138 Self {
139 analyses: HashMap::new(),
140 config: WeightAnalyzerConfig::default(),
141 }
142 }
143
144 pub fn with_config(config: WeightAnalyzerConfig) -> Self {
146 Self {
147 analyses: HashMap::new(),
148 config,
149 }
150 }
151
152 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 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 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 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 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 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 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 if stats.sparsity > 0.99 {
328 warnings.push("Weights appear to be uninitialized (all zeros)".to_string());
329 return (None, warnings);
330 }
331
332 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 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 if stats.mean.abs() < 0.01 {
354 if stats.std_dev > 0.01 && stats.std_dev < 0.2 {
356 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 pub fn get_analysis(&self, layer_name: &str) -> Option<&WeightAnalysis> {
372 self.analyses.get(layer_name)
373 }
374
375 pub fn get_layer_names(&self) -> Vec<String> {
377 self.analyses.keys().cloned().collect()
378 }
379
380 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 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 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 pub fn clear(&mut self) {
479 self.analyses.clear();
480 }
481
482 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
494fn 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]; 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 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 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}