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