Skip to main content

trustformers_debug/model_diagnostics/
layers.rs

1//! Layer-level analysis and activation monitoring.
2//!
3//! This module provides comprehensive layer-level diagnostics including
4//! activation analysis, weight distribution monitoring, attention visualization,
5//! and layer health assessment for deep learning models.
6// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
7// are retained for the data model, serialization completeness, and future consumers that
8// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
9#![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/// Layer analyzer for monitoring and analyzing individual layer behavior.
20#[derive(Debug)]
21pub struct LayerAnalyzer {
22    /// Layer activation statistics history
23    layer_activations: HashMap<String, Vec<LayerActivationStats>>,
24    /// Layer health monitoring configuration
25    config: LayerAnalysisConfig,
26    /// Current layer states
27    layer_states: HashMap<String, LayerState>,
28}
29
30/// Configuration for layer analysis.
31#[derive(Debug, Clone)]
32pub struct LayerAnalysisConfig {
33    /// Threshold for dead neuron detection
34    pub dead_neuron_threshold: f64,
35    /// Threshold for saturated neuron detection
36    pub saturated_neuron_threshold: f64,
37    /// Maximum acceptable activation variance
38    pub max_activation_variance: f64,
39    /// Minimum acceptable layer health score
40    pub min_health_score: f64,
41    /// History length for temporal analysis
42    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/// Current state information for a layer.
58#[derive(Debug, Clone, Default)]
59struct LayerState {
60    /// Health score history
61    health_scores: Vec<f64>,
62    /// Issues detected in the layer
63    detected_issues: Vec<String>,
64    /// Last analysis timestamp
65    last_analysis_step: usize,
66}
67
68impl LayerAnalyzer {
69    /// Create a new layer analyzer.
70    pub fn new() -> Self {
71        Self {
72            layer_activations: HashMap::new(),
73            config: LayerAnalysisConfig::default(),
74            layer_states: HashMap::new(),
75        }
76    }
77
78    /// Create a new layer analyzer with custom configuration.
79    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    /// Record layer activation statistics.
88    pub fn record_layer_activations(&mut self, layer_name: &str, stats: LayerActivationStats) {
89        // Calculate health score before mutable borrow
90        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        // Maintain reasonable history length
96        if layer_stats.len() > self.config.history_length {
97            layer_stats.remove(0);
98        }
99
100        // Update layer state
101        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    /// Record layer statistics (extracts layer name and calls record_layer_activations).
112    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    /// Get layer activation statistics for a specific layer.
118    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    /// Perform comprehensive layer-by-layer analysis.
123    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    /// Analyze a single layer comprehensively.
140    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    /// Calculate layer health score.
163    pub fn calculate_layer_health_score(&self, stats: &LayerActivationStats) -> f64 {
164        let mut score = 1.0;
165
166        // Penalize dead neurons
167        if stats.dead_neurons_ratio > self.config.dead_neuron_threshold {
168            score -= stats.dead_neurons_ratio * 0.5;
169        }
170
171        // Penalize saturated neurons
172        if stats.saturated_neurons_ratio > self.config.saturated_neuron_threshold {
173            score -= stats.saturated_neurons_ratio * 0.3;
174        }
175
176        // Penalize extreme activation ranges
177        let activation_range = stats.max_activation - stats.min_activation;
178        if activation_range > 10.0 {
179            score -= 0.2;
180        }
181
182        // Penalize high variance
183        if stats.std_activation > self.config.max_activation_variance {
184            score -= 0.2;
185        }
186
187        // Bonus for good sparsity
188        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    /// Identify issues in a layer.
196    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        // Dead neuron issues
204        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        // Saturated neuron issues
212        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        // Activation range issues
220        if current_stats.max_activation - current_stats.min_activation > 100.0 {
221            issues.push("Extremely wide activation range detected".to_string());
222        }
223
224        // Variance issues
225        if current_stats.std_activation > self.config.max_activation_variance {
226            issues.push("High activation variance detected".to_string());
227        }
228
229        // Temporal issues (if history is available)
230        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        // Zero activation issues
238        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    /// Generate recommendations for layer improvement.
246    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    /// Analyze weight distributions for all layers.
293    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    /// Generate activation heatmaps for visualization.
305    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    /// Generate attention visualizations for attention layers.
319    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    /// Analyze hidden states for representational quality.
333    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    // Helper methods
345
346    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::*; // SciRS2 Integration Policy
421        let mut rng = thread_rng();
422
423        // Simulate weight distribution analysis
424        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::*; // SciRS2 Integration Policy
452        let mut rng = thread_rng();
453
454        // Create simulated heatmap data based on layer output shape
455        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::*; // SciRS2 Integration Policy
482        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::*; // SciRS2 Integration Policy
509        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 // Default dimension
533    }
534
535    fn compute_information_content(&self, layer_name: &str) -> f64 {
536        use scirs2_core::random::*; // SciRS2 Integration Policy
537        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::*; // SciRS2 Integration Policy
550        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::*; // SciRS2 Integration Policy
573        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::*; // SciRS2 Integration Policy
609        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    /// Clear all layer analysis data.
629    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; // High dead neuron ratio
699
700        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}