1use std::collections::HashMap;
8
9use super::types::{
10 ActivationHeatmap, AttentionVisualization, ClusteringResults, DriftInfo, HiddenStateAnalysis,
11 LayerActivationStats, LayerAnalysis, RepresentationStability, TemporalDynamics,
12 WeightDistribution,
13};
14
15#[derive(Debug)]
17pub struct LayerAnalyzer {
18 layer_activations: HashMap<String, Vec<LayerActivationStats>>,
20 config: LayerAnalysisConfig,
22 layer_states: HashMap<String, LayerState>,
24}
25
26#[derive(Debug, Clone)]
28pub struct LayerAnalysisConfig {
29 pub dead_neuron_threshold: f64,
31 pub saturated_neuron_threshold: f64,
33 pub max_activation_variance: f64,
35 pub min_health_score: f64,
37 pub history_length: usize,
39}
40
41impl Default for LayerAnalysisConfig {
42 fn default() -> Self {
43 Self {
44 dead_neuron_threshold: 0.1,
45 saturated_neuron_threshold: 0.1,
46 max_activation_variance: 2.0,
47 min_health_score: 0.7,
48 history_length: 100,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Default)]
55struct LayerState {
56 health_scores: Vec<f64>,
58 #[allow(dead_code)]
60 detected_issues: Vec<String>,
61 last_analysis_step: usize,
63}
64
65impl LayerAnalyzer {
66 pub fn new() -> Self {
68 Self {
69 layer_activations: HashMap::new(),
70 config: LayerAnalysisConfig::default(),
71 layer_states: HashMap::new(),
72 }
73 }
74
75 pub fn with_config(config: LayerAnalysisConfig) -> Self {
77 Self {
78 layer_activations: HashMap::new(),
79 config,
80 layer_states: HashMap::new(),
81 }
82 }
83
84 pub fn record_layer_activations(&mut self, layer_name: &str, stats: LayerActivationStats) {
86 let health_score = self.calculate_layer_health_score(&stats);
88
89 let layer_stats = self.layer_activations.entry(layer_name.to_string()).or_default();
90 layer_stats.push(stats);
91
92 if layer_stats.len() > self.config.history_length {
94 layer_stats.remove(0);
95 }
96
97 let layer_state = self.layer_states.entry(layer_name.to_string()).or_default();
99 layer_state.health_scores.push(health_score);
100
101 if layer_state.health_scores.len() > 50 {
102 layer_state.health_scores.remove(0);
103 }
104
105 layer_state.last_analysis_step += 1;
106 }
107
108 pub fn record_layer_stats(&mut self, stats: LayerActivationStats) {
110 let layer_name = stats.layer_name.clone();
111 self.record_layer_activations(&layer_name, stats);
112 }
113
114 pub fn get_layer_activations(&self, layer_name: &str) -> Option<&[LayerActivationStats]> {
116 self.layer_activations.get(layer_name).map(|v| v.as_slice())
117 }
118
119 pub fn perform_layer_by_layer_analysis(&self) -> Vec<LayerAnalysis> {
121 let mut analyses = Vec::new();
122
123 for (layer_name, stats_history) in &self.layer_activations {
124 if let Some(latest_stats) = stats_history.last() {
125 let analysis = self.analyze_single_layer(layer_name, latest_stats, stats_history);
126 analyses.push(analysis);
127 }
128 }
129
130 analyses.sort_by(|a, b| {
131 a.health_score.partial_cmp(&b.health_score).unwrap_or(std::cmp::Ordering::Equal)
132 });
133 analyses
134 }
135
136 pub fn analyze_single_layer(
138 &self,
139 layer_name: &str,
140 current_stats: &LayerActivationStats,
141 stats_history: &[LayerActivationStats],
142 ) -> LayerAnalysis {
143 let layer_type = self.infer_layer_type(layer_name);
144 let health_score = self.calculate_layer_health_score(current_stats);
145 let issues = self.identify_layer_issues(current_stats, stats_history);
146 let recommendations = self.generate_layer_recommendations(&issues, &layer_type);
147 let activation_summary = self.generate_activation_summary(current_stats);
148
149 LayerAnalysis {
150 layer_name: layer_name.to_string(),
151 layer_type,
152 health_score,
153 issues,
154 recommendations,
155 activation_summary,
156 }
157 }
158
159 pub fn calculate_layer_health_score(&self, stats: &LayerActivationStats) -> f64 {
161 let mut score = 1.0;
162
163 if stats.dead_neurons_ratio > self.config.dead_neuron_threshold {
165 score -= stats.dead_neurons_ratio * 0.5;
166 }
167
168 if stats.saturated_neurons_ratio > self.config.saturated_neuron_threshold {
170 score -= stats.saturated_neurons_ratio * 0.3;
171 }
172
173 let activation_range = stats.max_activation - stats.min_activation;
175 if activation_range > 10.0 {
176 score -= 0.2;
177 }
178
179 if stats.std_activation > self.config.max_activation_variance {
181 score -= 0.2;
182 }
183
184 if stats.sparsity > 0.1 && stats.sparsity < 0.8 {
186 score += 0.1;
187 }
188
189 score.max(0.0).min(1.0)
190 }
191
192 pub fn identify_layer_issues(
194 &self,
195 current_stats: &LayerActivationStats,
196 stats_history: &[LayerActivationStats],
197 ) -> Vec<String> {
198 let mut issues = Vec::new();
199
200 if current_stats.dead_neurons_ratio > self.config.dead_neuron_threshold {
202 issues.push(format!(
203 "High dead neuron ratio: {:.1}%",
204 current_stats.dead_neurons_ratio * 100.0
205 ));
206 }
207
208 if current_stats.saturated_neurons_ratio > self.config.saturated_neuron_threshold {
210 issues.push(format!(
211 "High saturated neuron ratio: {:.1}%",
212 current_stats.saturated_neurons_ratio * 100.0
213 ));
214 }
215
216 if current_stats.max_activation - current_stats.min_activation > 100.0 {
218 issues.push("Extremely wide activation range detected".to_string());
219 }
220
221 if current_stats.std_activation > self.config.max_activation_variance {
223 issues.push("High activation variance detected".to_string());
224 }
225
226 if stats_history.len() > 5 {
228 let variance_trend = self.analyze_variance_trend(stats_history);
229 if variance_trend > 0.1 {
230 issues.push("Increasing activation variance over time".to_string());
231 }
232 }
233
234 if current_stats.mean_activation.abs() < 1e-6 {
236 issues.push("Near-zero mean activation detected".to_string());
237 }
238
239 issues
240 }
241
242 pub fn generate_layer_recommendations(
244 &self,
245 issues: &[String],
246 layer_type: &str,
247 ) -> Vec<String> {
248 let mut recommendations = Vec::new();
249
250 for issue in issues {
251 if issue.contains("dead neuron") {
252 match layer_type {
253 "Linear" => recommendations
254 .push("Consider using LeakyReLU or ELU activation".to_string()),
255 "Convolutional" => recommendations.push(
256 "Consider batch normalization or different initialization".to_string(),
257 ),
258 _ => recommendations.push(
259 "Consider different activation function or initialization".to_string(),
260 ),
261 }
262 }
263
264 if issue.contains("saturated neuron") {
265 recommendations
266 .push("Consider gradient clipping or learning rate reduction".to_string());
267 recommendations.push("Consider batch normalization".to_string());
268 }
269
270 if issue.contains("activation range") {
271 recommendations.push("Consider activation clipping or normalization".to_string());
272 }
273
274 if issue.contains("variance") {
275 recommendations.push("Consider weight initialization adjustment".to_string());
276 recommendations.push("Consider adding regularization".to_string());
277 }
278
279 if issue.contains("zero activation") {
280 recommendations
281 .push("Check weight initialization and input preprocessing".to_string());
282 }
283 }
284
285 recommendations.dedup();
286 recommendations
287 }
288
289 pub fn analyze_weight_distributions(&self) -> HashMap<String, WeightDistribution> {
291 let mut distributions = HashMap::new();
292
293 for layer_name in self.layer_activations.keys() {
294 let distribution = self.analyze_layer_weight_distribution(layer_name);
295 distributions.insert(layer_name.clone(), distribution);
296 }
297
298 distributions
299 }
300
301 pub fn generate_activation_heatmaps(&self) -> HashMap<String, ActivationHeatmap> {
303 let mut heatmaps = HashMap::new();
304
305 for (layer_name, stats_history) in &self.layer_activations {
306 if let Some(latest_stats) = stats_history.last() {
307 let heatmap = self.create_activation_heatmap(layer_name, latest_stats);
308 heatmaps.insert(layer_name.clone(), heatmap);
309 }
310 }
311
312 heatmaps
313 }
314
315 pub fn generate_attention_visualizations(&self) -> HashMap<String, AttentionVisualization> {
317 let mut visualizations = HashMap::new();
318
319 for layer_name in self.layer_activations.keys() {
320 if self.infer_layer_type(layer_name) == "Attention" {
321 let visualization = self.create_attention_visualization(layer_name);
322 visualizations.insert(layer_name.clone(), visualization);
323 }
324 }
325
326 visualizations
327 }
328
329 pub fn analyze_hidden_states(&self) -> HashMap<String, HiddenStateAnalysis> {
331 let mut analyses = HashMap::new();
332
333 for layer_name in self.layer_activations.keys() {
334 let analysis = self.analyze_layer_hidden_states(layer_name);
335 analyses.insert(layer_name.clone(), analysis);
336 }
337
338 analyses
339 }
340
341 fn infer_layer_type(&self, layer_name: &str) -> String {
344 let name_lower = layer_name.to_lowercase();
345
346 if name_lower.contains("attention") || name_lower.contains("attn") {
347 "Attention".to_string()
348 } else if name_lower.contains("linear")
349 || name_lower.contains("dense")
350 || name_lower.contains("fc")
351 {
352 "Linear".to_string()
353 } else if name_lower.contains("conv") {
354 "Convolutional".to_string()
355 } else if name_lower.contains("norm")
356 || name_lower.contains("bn")
357 || name_lower.contains("ln")
358 {
359 "Normalization".to_string()
360 } else if name_lower.contains("dropout") {
361 "Dropout".to_string()
362 } else if name_lower.contains("embed") {
363 "Embedding".to_string()
364 } else {
365 "Unknown".to_string()
366 }
367 }
368
369 fn generate_activation_summary(&self, stats: &LayerActivationStats) -> String {
370 format!(
371 "Mean: {:.3}, Std: {:.3}, Range: [{:.3}, {:.3}], Dead: {:.1}%, Saturated: {:.1}%, Sparsity: {:.1}%",
372 stats.mean_activation,
373 stats.std_activation,
374 stats.min_activation,
375 stats.max_activation,
376 stats.dead_neurons_ratio * 100.0,
377 stats.saturated_neurons_ratio * 100.0,
378 stats.sparsity * 100.0
379 )
380 }
381
382 fn analyze_variance_trend(&self, stats_history: &[LayerActivationStats]) -> f64 {
383 if stats_history.len() < 2 {
384 return 0.0;
385 }
386
387 let variances: Vec<f64> = stats_history.iter().map(|s| s.std_activation.powi(2)).collect();
388 self.calculate_trend(&variances)
389 }
390
391 fn calculate_trend(&self, values: &[f64]) -> f64 {
392 if values.len() < 2 {
393 return 0.0;
394 }
395
396 let n = values.len() as f64;
397 let x_mean = (n - 1.0) / 2.0;
398 let y_mean = values.iter().sum::<f64>() / n;
399
400 let mut numerator = 0.0;
401 let mut denominator = 0.0;
402
403 for (i, &y) in values.iter().enumerate() {
404 let x = i as f64;
405 numerator += (x - x_mean) * (y - y_mean);
406 denominator += (x - x_mean).powi(2);
407 }
408
409 if denominator == 0.0 {
410 0.0
411 } else {
412 numerator / denominator
413 }
414 }
415
416 fn analyze_layer_weight_distribution(&self, layer_name: &str) -> WeightDistribution {
417 use scirs2_core::random::*; let mut rng = thread_rng();
419
420 let layer_type = self.infer_layer_type(layer_name);
422 let (mean, std_dev) = match layer_type.as_str() {
423 "Linear" => (rng.random_range(-0.1..0.1), rng.random_range(0.1..0.5)),
424 "Convolutional" => (rng.random_range(-0.05..0.05), rng.random_range(0.05..0.3)),
425 "Attention" => (rng.random_range(-0.02..0.02), rng.random_range(0.02..0.2)),
426 _ => (rng.random_range(-0.1..0.1), rng.random_range(0.1..0.4)),
427 };
428
429 let min = mean - 3.0 * std_dev;
430 let max = mean + 3.0 * std_dev;
431 let sparsity = rng.random_range(0.0..0.3);
432
433 WeightDistribution {
434 mean,
435 std_dev,
436 min,
437 max,
438 sparsity,
439 distribution_shape: "Normal".to_string(),
440 }
441 }
442
443 fn create_activation_heatmap(
444 &self,
445 layer_name: &str,
446 stats: &LayerActivationStats,
447 ) -> ActivationHeatmap {
448 use scirs2_core::random::*; let mut rng = thread_rng();
450
451 let (height, width) = if stats.output_shape.len() >= 2 {
453 (stats.output_shape[0].min(64), stats.output_shape[1].min(64))
454 } else {
455 (32, 32)
456 };
457
458 let data: Vec<Vec<f64>> = (0..height)
459 .map(|_| {
460 (0..width)
461 .map(|_| rng.random_range(stats.min_activation..stats.max_activation))
462 .collect()
463 })
464 .collect();
465
466 ActivationHeatmap {
467 data,
468 dimensions: (height, width),
469 value_range: (stats.min_activation, stats.max_activation),
470 interpretation: format!(
471 "Activation pattern for {} layer",
472 self.infer_layer_type(layer_name)
473 ),
474 }
475 }
476
477 fn create_attention_visualization(&self, _layer_name: &str) -> AttentionVisualization {
478 use scirs2_core::random::*; let mut rng = thread_rng();
480
481 let seq_length = rng.random_range(10..50);
482 let attention_weights: Vec<Vec<f64>> = (0..seq_length)
483 .map(|_| (0..seq_length).map(|_| rng.random_range(0.0..1.0)).collect())
484 .collect();
485
486 let input_tokens: Vec<String> = (0..seq_length).map(|i| format!("token_{}", i)).collect();
487
488 let output_tokens = input_tokens.clone();
489
490 let patterns = vec![
491 "Self-attention pattern detected".to_string(),
492 "Local attention focused".to_string(),
493 "Global attention pattern".to_string(),
494 ];
495
496 AttentionVisualization {
497 attention_weights,
498 input_tokens,
499 output_tokens,
500 patterns,
501 }
502 }
503
504 fn analyze_layer_hidden_states(&self, layer_name: &str) -> HiddenStateAnalysis {
505 use scirs2_core::random::*; let _rng = thread_rng();
507
508 let dimensionality = self.get_hidden_dimensions(layer_name);
509 let information_content = self.compute_information_content(layer_name);
510 let clustering_results = self.perform_clustering_analysis(layer_name);
511 let temporal_dynamics = self.analyze_temporal_dynamics(layer_name);
512 let representation_stability = self.assess_representation_stability(layer_name);
513
514 HiddenStateAnalysis {
515 dimensionality,
516 information_content,
517 clustering_results,
518 temporal_dynamics,
519 representation_stability,
520 }
521 }
522
523 fn get_hidden_dimensions(&self, layer_name: &str) -> usize {
524 if let Some(stats_history) = self.layer_activations.get(layer_name) {
525 if let Some(latest_stats) = stats_history.last() {
526 return latest_stats.output_shape.iter().product();
527 }
528 }
529 512 }
531
532 fn compute_information_content(&self, layer_name: &str) -> f64 {
533 use scirs2_core::random::*; let mut rng = thread_rng();
535
536 let layer_type = self.infer_layer_type(layer_name);
537 match layer_type.as_str() {
538 "Attention" => rng.random_range(0.6..0.9),
539 "Linear" => rng.random_range(0.4..0.7),
540 "Convolutional" => rng.random_range(0.3..0.6),
541 _ => rng.random_range(0.4..0.7),
542 }
543 }
544
545 fn perform_clustering_analysis(&self, layer_name: &str) -> ClusteringResults {
546 use scirs2_core::random::*; let mut rng = thread_rng();
548
549 let hidden_dims = self.get_hidden_dimensions(layer_name);
550 let num_clusters = rng.random_range(5..20);
551
552 let cluster_centers: Vec<Vec<f64>> = (0..num_clusters)
553 .map(|_| (0..hidden_dims.min(10)).map(|_| rng.random_range(-1.0..1.0)).collect())
554 .collect();
555
556 let cluster_assignments: Vec<usize> =
557 (0..100).map(|_| rng.random_range(0..num_clusters)).collect();
558
559 ClusteringResults {
560 num_clusters,
561 cluster_centers,
562 cluster_assignments,
563 silhouette_score: rng.random_range(0.2..0.8),
564 inertia: rng.random_range(100.0..1000.0),
565 }
566 }
567
568 fn analyze_temporal_dynamics(&self, _layer_name: &str) -> TemporalDynamics {
569 use scirs2_core::random::*; let mut rng = thread_rng();
571
572 let consistency = rng.random_range(0.5..0.9);
573 let change_rate = rng.random_range(0.01..0.1);
574
575 let num_windows = rng.random_range(3..8);
576 let stability_windows: Vec<(usize, usize)> = (0..num_windows)
577 .map(|i| {
578 let start = i * 100;
579 let end = start + rng.random_range(50..150);
580 (start, end)
581 })
582 .collect();
583
584 let drift_detected = rng.gen_bool(0.2);
585 let drift_info = DriftInfo {
586 drift_detected,
587 drift_magnitude: if drift_detected { rng.random_range(0.1..0.5) } else { 0.0 },
588 drift_direction: if drift_detected {
589 ["increasing", "decreasing", "oscillating"][rng.random_range(0..3)].to_string()
590 } else {
591 "stable".to_string()
592 },
593 onset_step: if drift_detected { Some(rng.random_range(100..1000)) } else { None },
594 };
595
596 TemporalDynamics {
597 temporal_consistency: consistency,
598 change_rate,
599 stability_windows,
600 drift_detection: drift_info,
601 }
602 }
603
604 fn assess_representation_stability(&self, layer_name: &str) -> RepresentationStability {
605 use scirs2_core::random::*; let mut rng = thread_rng();
607
608 let layer_type = self.infer_layer_type(layer_name);
609
610 let stability_score = match layer_type.as_str() {
611 "Normalization" => rng.random_range(0.8..0.95),
612 "Attention" => rng.random_range(0.6..0.85),
613 "Linear" => rng.random_range(0.5..0.8),
614 _ => rng.random_range(0.4..0.7),
615 };
616
617 RepresentationStability {
618 stability_score,
619 variance_across_batches: rng.random_range(0.01..0.1),
620 consistency_measure: rng.random_range(0.6..0.9),
621 robustness_to_noise: rng.random_range(0.3..0.8),
622 }
623 }
624
625 pub fn clear(&mut self) {
627 self.layer_activations.clear();
628 self.layer_states.clear();
629 }
630}
631
632impl Default for LayerAnalyzer {
633 fn default() -> Self {
634 Self::new()
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 fn create_test_layer_stats(layer_name: &str) -> LayerActivationStats {
643 LayerActivationStats {
644 layer_name: layer_name.to_string(),
645 mean_activation: 0.5,
646 std_activation: 0.2,
647 min_activation: 0.0,
648 max_activation: 1.0,
649 dead_neurons_ratio: 0.05,
650 saturated_neurons_ratio: 0.03,
651 sparsity: 0.3,
652 output_shape: vec![128, 256],
653 }
654 }
655
656 #[test]
657 fn test_layer_analyzer_creation() {
658 let analyzer = LayerAnalyzer::new();
659 assert_eq!(analyzer.layer_activations.len(), 0);
660 }
661
662 #[test]
663 fn test_record_layer_activations() {
664 let mut analyzer = LayerAnalyzer::new();
665 let stats = create_test_layer_stats("test_layer");
666
667 analyzer.record_layer_activations("test_layer", stats);
668 assert_eq!(analyzer.layer_activations.len(), 1);
669 assert!(analyzer.layer_activations.contains_key("test_layer"));
670 }
671
672 #[test]
673 fn test_layer_health_score_calculation() {
674 let analyzer = LayerAnalyzer::new();
675 let stats = create_test_layer_stats("test_layer");
676
677 let health_score = analyzer.calculate_layer_health_score(&stats);
678 assert!(health_score > 0.0 && health_score <= 1.0);
679 }
680
681 #[test]
682 fn test_layer_type_inference() {
683 let analyzer = LayerAnalyzer::new();
684
685 assert_eq!(analyzer.infer_layer_type("attention_layer"), "Attention");
686 assert_eq!(analyzer.infer_layer_type("linear_projection"), "Linear");
687 assert_eq!(analyzer.infer_layer_type("conv2d_layer"), "Convolutional");
688 assert_eq!(analyzer.infer_layer_type("batch_norm"), "Normalization");
689 }
690
691 #[test]
692 fn test_issue_identification() {
693 let analyzer = LayerAnalyzer::new();
694 let mut stats = create_test_layer_stats("test_layer");
695 stats.dead_neurons_ratio = 0.2; let issues = analyzer.identify_layer_issues(&stats, &[]);
698 assert!(!issues.is_empty());
699 assert!(issues[0].contains("dead neuron"));
700 }
701
702 #[test]
703 fn test_layer_analysis() {
704 let analyzer = LayerAnalyzer::new();
705 let stats = create_test_layer_stats("attention_layer");
706 let history = vec![stats.clone()];
707
708 let analysis = analyzer.analyze_single_layer("attention_layer", &stats, &history);
709 assert_eq!(analysis.layer_name, "attention_layer");
710 assert_eq!(analysis.layer_type, "Attention");
711 assert!(analysis.health_score > 0.0);
712 }
713}