Skip to main content

trustformers_debug/
behavior_analysis.rs

1//! Behavior Analysis
2//!
3//! Advanced analysis tools for understanding neural network behavior including
4//! input sensitivity, feature importance, and neuron activation patterns.
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9
10/// Configuration for behavior analysis
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct BehaviorAnalysisConfig {
13    /// Enable input sensitivity analysis
14    pub enable_input_sensitivity: bool,
15    /// Enable feature importance calculations
16    pub enable_feature_importance: bool,
17    /// Enable neuron activation pattern analysis
18    pub enable_activation_patterns: bool,
19    /// Enable dead neuron detection
20    pub enable_dead_neuron_detection: bool,
21    /// Enable correlation analysis
22    pub enable_correlation_analysis: bool,
23    /// Threshold for dead neuron detection (activation below this value)
24    pub dead_neuron_threshold: f32,
25    /// Number of samples for sensitivity analysis
26    pub sensitivity_samples: usize,
27    /// Perturbation magnitude for sensitivity analysis
28    pub perturbation_magnitude: f32,
29    /// Correlation threshold for significance
30    pub correlation_threshold: f32,
31}
32
33impl Default for BehaviorAnalysisConfig {
34    fn default() -> Self {
35        Self {
36            enable_input_sensitivity: true,
37            enable_feature_importance: true,
38            enable_activation_patterns: true,
39            enable_dead_neuron_detection: true,
40            enable_correlation_analysis: true,
41            dead_neuron_threshold: 1e-6,
42            sensitivity_samples: 100,
43            perturbation_magnitude: 0.01,
44            correlation_threshold: 0.5,
45        }
46    }
47}
48
49/// Input sensitivity analysis results
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct InputSensitivity {
52    pub input_dimension: usize,
53    pub sensitivity_score: f32,
54    pub gradient_magnitude: f32,
55    pub perturbation_impact: f32,
56    pub rank: usize,
57}
58
59/// Feature importance analysis results
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct FeatureImportance {
62    pub feature_id: String,
63    pub importance_score: f32,
64    pub attribution_method: AttributionMethod,
65    pub confidence: f32,
66    pub rank: usize,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub enum AttributionMethod {
71    GradientBased,
72    PermutationImportance,
73    ShapleySampling,
74    IntegratedGradients,
75    LimeApproximation,
76}
77
78/// Neuron activation pattern information
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct NeuronActivationPattern {
81    pub layer_id: String,
82    pub neuron_id: usize,
83    pub activation_statistics: ActivationStatistics,
84    pub pattern_type: ActivationPatternType,
85    pub stability_score: f32,
86    pub selectivity_score: f32,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ActivationStatistics {
91    pub mean: f32,
92    pub std: f32,
93    pub min: f32,
94    pub max: f32,
95    pub percentile_25: f32,
96    pub percentile_75: f32,
97    pub skewness: f32,
98    pub kurtosis: f32,
99    pub sparsity: f32, // Fraction of near-zero activations
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub enum ActivationPatternType {
104    Normal,
105    Saturated,
106    Dead,
107    Oscillating,
108    Sparse,
109    Dense,
110    Bipolar,
111}
112
113/// Dead neuron detection results
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct DeadNeuronInfo {
116    pub layer_id: String,
117    pub neuron_id: usize,
118    pub activation_level: f32,
119    pub dead_probability: f32,
120    pub suggested_action: NeuronRepairAction,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub enum NeuronRepairAction {
125    Reinitialize,
126    AdjustLearningRate,
127    ChangeActivationFunction,
128    AddNoise,
129    Skip, // Neuron is functioning normally
130}
131
132/// Correlation analysis results
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct CorrelationAnalysis {
135    pub correlation_matrix: Vec<Vec<f32>>,
136    pub significant_correlations: Vec<CorrelationPair>,
137    pub redundant_features: Vec<FeatureGroup>,
138    pub independent_features: Vec<usize>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct CorrelationPair {
143    pub feature_a: usize,
144    pub feature_b: usize,
145    pub correlation: f32,
146    /// Two-sided p-value for `H0: rho = 0`, from the standard
147    /// `t = r * sqrt((n - 2) / (1 - r^2))` statistic on `n - 2` degrees of
148    /// freedom, where `n` is the number of paired samples the correlation was
149    /// computed from.
150    ///
151    /// `None` when fewer than three samples are available (no residual degrees
152    /// of freedom) or when `|r| == 1` exactly. Previously the literal `0.01`
153    /// for every reported pair, marked "Simplified p-value".
154    pub p_value: Option<f32>,
155    pub relationship_type: CorrelationType,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub enum CorrelationType {
160    Strong,
161    Moderate,
162    Weak,
163    None,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct FeatureGroup {
168    pub features: Vec<usize>,
169    /// Mean absolute pairwise correlation within the group.
170    pub average_correlation: f32,
171}
172
173// `FeatureGroup::group_importance` was removed: it was assigned
174// `average_correlation` verbatim (under the comment "Simplified importance"),
175// so a consumer saw two field names carrying one number and could reasonably
176// read them as independent evidence. Nothing here measures group importance.
177
178/// Comprehensive behavior analysis report
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct BehaviorAnalysisReport {
181    pub input_sensitivities: Vec<InputSensitivity>,
182    pub feature_importances: Vec<FeatureImportance>,
183    pub activation_patterns: Vec<NeuronActivationPattern>,
184    pub dead_neurons: Vec<DeadNeuronInfo>,
185    pub correlation_analysis: Option<CorrelationAnalysis>,
186    pub behavior_summary: BehaviorSummary,
187    pub recommendations: Vec<BehaviorRecommendation>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct BehaviorSummary {
192    pub total_neurons_analyzed: usize,
193    pub dead_neuron_percentage: f32,
194    pub average_activation_sparsity: f32,
195    pub feature_distribution_entropy: f32,
196    pub model_stability_score: f32,
197    pub interpretability_score: f32,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct BehaviorRecommendation {
202    pub category: RecommendationCategory,
203    pub priority: Priority,
204    pub description: String,
205    pub implementation: String,
206    pub expected_impact: f32,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub enum RecommendationCategory {
211    Architecture,
212    Training,
213    Initialization,
214    Regularization,
215    DataPreprocessing,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub enum Priority {
220    Critical,
221    High,
222    Medium,
223    Low,
224}
225
226/// Behavior analyzer
227#[derive(Debug)]
228pub struct BehaviorAnalyzer {
229    config: BehaviorAnalysisConfig,
230    activation_history: HashMap<String, Vec<Vec<f32>>>,
231    input_gradients: HashMap<String, Vec<f32>>,
232    feature_attributions: HashMap<String, FeatureImportance>,
233    analysis_cache: HashMap<String, BehaviorAnalysisReport>,
234}
235
236impl BehaviorAnalyzer {
237    /// Create a new behavior analyzer
238    pub fn new(config: BehaviorAnalysisConfig) -> Self {
239        Self {
240            config,
241            activation_history: HashMap::new(),
242            input_gradients: HashMap::new(),
243            feature_attributions: HashMap::new(),
244            analysis_cache: HashMap::new(),
245        }
246    }
247
248    /// Record neuron activations for analysis
249    pub fn record_activations(&mut self, layer_id: String, activations: Vec<f32>) {
250        self.activation_history.entry(layer_id).or_default().push(activations);
251    }
252
253    /// Record input gradients for sensitivity analysis
254    pub fn record_input_gradients(&mut self, input_id: String, gradients: Vec<f32>) {
255        self.input_gradients.insert(input_id, gradients);
256    }
257
258    /// Perform comprehensive behavior analysis
259    pub async fn analyze(&mut self) -> Result<BehaviorAnalysisReport> {
260        let mut report = BehaviorAnalysisReport {
261            input_sensitivities: Vec::new(),
262            feature_importances: Vec::new(),
263            activation_patterns: Vec::new(),
264            dead_neurons: Vec::new(),
265            correlation_analysis: None,
266            behavior_summary: BehaviorSummary {
267                total_neurons_analyzed: 0,
268                dead_neuron_percentage: 0.0,
269                average_activation_sparsity: 0.0,
270                feature_distribution_entropy: 0.0,
271                model_stability_score: 0.0,
272                interpretability_score: 0.0,
273            },
274            recommendations: Vec::new(),
275        };
276
277        if self.config.enable_input_sensitivity {
278            report.input_sensitivities = self.analyze_input_sensitivity().await?;
279        }
280
281        if self.config.enable_feature_importance {
282            report.feature_importances = self.calculate_feature_importance().await?;
283        }
284
285        if self.config.enable_activation_patterns {
286            report.activation_patterns = self.analyze_activation_patterns().await?;
287        }
288
289        if self.config.enable_dead_neuron_detection {
290            report.dead_neurons = self.detect_dead_neurons().await?;
291        }
292
293        if self.config.enable_correlation_analysis {
294            report.correlation_analysis = Some(self.perform_correlation_analysis().await?);
295        }
296
297        self.generate_behavior_summary(&mut report);
298        self.generate_recommendations(&mut report);
299
300        Ok(report)
301    }
302
303    /// Analyze input sensitivity using gradient-based methods
304    async fn analyze_input_sensitivity(&self) -> Result<Vec<InputSensitivity>> {
305        let mut sensitivities = Vec::new();
306
307        for gradients in self.input_gradients.values() {
308            for (dim, &gradient) in gradients.iter().enumerate() {
309                let sensitivity_score = gradient.abs();
310                let gradient_magnitude = gradient.abs();
311
312                // First-order (Taylor) estimate from the real gradient; see
313                // `estimate_perturbation_impact` for its error bound.
314                let perturbation_impact = self.estimate_perturbation_impact(gradient, dim);
315
316                sensitivities.push(InputSensitivity {
317                    input_dimension: dim,
318                    sensitivity_score,
319                    gradient_magnitude,
320                    perturbation_impact,
321                    rank: 0, // Will be set after sorting
322                });
323            }
324        }
325
326        // Sort by sensitivity score and assign ranks
327        sensitivities.sort_by(|a, b| {
328            b.sensitivity_score
329                .partial_cmp(&a.sensitivity_score)
330                .unwrap_or(std::cmp::Ordering::Equal)
331        });
332        for (rank, sensitivity) in sensitivities.iter_mut().enumerate() {
333            sensitivity.rank = rank + 1;
334        }
335
336        Ok(sensitivities)
337    }
338
339    /// First-order estimate of `|f(x + eps*e_i) - f(x)|` for a perturbation of
340    /// size [`BehaviorAnalysisConfig::perturbation_magnitude`] along dimension
341    /// `i`.
342    ///
343    /// This is the exact first-order Taylor term `|df/dx_i| * eps`, computed
344    /// from the real recorded input gradient. It is an *approximation* of the
345    /// true impact with error `O(eps^2 * |d2f/dx_i^2|)`, so it is accurate for
346    /// small `eps` and understates the impact wherever the model is strongly
347    /// curved along that dimension. Measuring the true impact would require
348    /// re-evaluating the model at the perturbed input, which this analyzer --
349    /// which receives gradients, not a model handle -- cannot do.
350    fn estimate_perturbation_impact(&self, gradient: f32, _dimension: usize) -> f32 {
351        gradient.abs() * self.config.perturbation_magnitude
352    }
353
354    /// Calculate feature importance using multiple methods
355    async fn calculate_feature_importance(&self) -> Result<Vec<FeatureImportance>> {
356        let mut importances = Vec::new();
357
358        // Gradient-based importance
359        for (input_id, gradients) in &self.input_gradients {
360            let total_gradient = gradients.iter().map(|g| g.abs()).sum::<f32>();
361            let importance_score = total_gradient / gradients.len() as f32;
362
363            importances.push(FeatureImportance {
364                feature_id: input_id.clone(),
365                importance_score,
366                attribution_method: AttributionMethod::GradientBased,
367                confidence: self.calculate_attribution_confidence(importance_score),
368                rank: 0,
369            });
370        }
371
372        // Sort by importance and assign ranks
373        importances.sort_by(|a, b| {
374            b.importance_score
375                .partial_cmp(&a.importance_score)
376                .unwrap_or(std::cmp::Ordering::Equal)
377        });
378        for (rank, importance) in importances.iter_mut().enumerate() {
379            importance.rank = rank + 1;
380        }
381
382        Ok(importances)
383    }
384
385    /// Calculate confidence in attribution score
386    fn calculate_attribution_confidence(&self, score: f32) -> f32 {
387        // Simple confidence based on score magnitude
388        (score.tanh() * 0.5 + 0.5).min(1.0)
389    }
390
391    /// Analyze neuron activation patterns
392    async fn analyze_activation_patterns(&self) -> Result<Vec<NeuronActivationPattern>> {
393        let mut patterns = Vec::new();
394
395        for (layer_id, activation_history) in &self.activation_history {
396            if activation_history.is_empty() {
397                continue;
398            }
399
400            let neuron_count = activation_history[0].len();
401
402            for neuron_id in 0..neuron_count {
403                let neuron_activations: Vec<f32> = activation_history
404                    .iter()
405                    .map(|batch| batch.get(neuron_id).copied().unwrap_or(0.0))
406                    .collect();
407
408                let statistics = self.compute_activation_statistics(&neuron_activations);
409                let pattern_type = self.classify_activation_pattern(&statistics);
410                let stability_score = self.compute_stability_score(&neuron_activations);
411                let selectivity_score = self.compute_selectivity_score(&neuron_activations);
412
413                patterns.push(NeuronActivationPattern {
414                    layer_id: layer_id.clone(),
415                    neuron_id,
416                    activation_statistics: statistics,
417                    pattern_type,
418                    stability_score,
419                    selectivity_score,
420                });
421            }
422        }
423
424        Ok(patterns)
425    }
426
427    /// Compute detailed activation statistics
428    fn compute_activation_statistics(&self, activations: &[f32]) -> ActivationStatistics {
429        if activations.is_empty() {
430            return ActivationStatistics {
431                mean: 0.0,
432                std: 0.0,
433                min: 0.0,
434                max: 0.0,
435                percentile_25: 0.0,
436                percentile_75: 0.0,
437                skewness: 0.0,
438                kurtosis: 0.0,
439                sparsity: 1.0,
440            };
441        }
442
443        let mean = activations.iter().sum::<f32>() / activations.len() as f32;
444        let variance =
445            activations.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / activations.len() as f32;
446        let std = variance.sqrt();
447
448        let mut sorted_activations = activations.to_vec();
449        sorted_activations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
450
451        let min = sorted_activations[0];
452        let max = sorted_activations[sorted_activations.len() - 1];
453        let percentile_25 = sorted_activations[sorted_activations.len() / 4];
454        let percentile_75 = sorted_activations[3 * sorted_activations.len() / 4];
455
456        // Calculate skewness and kurtosis
457        let skewness = if std > 0.0 {
458            activations.iter().map(|&x| ((x - mean) / std).powi(3)).sum::<f32>()
459                / activations.len() as f32
460        } else {
461            0.0
462        };
463
464        let kurtosis = if std > 0.0 {
465            activations.iter().map(|&x| ((x - mean) / std).powi(4)).sum::<f32>()
466                / activations.len() as f32
467                - 3.0
468        } else {
469            0.0
470        };
471
472        // Calculate sparsity (fraction of near-zero activations)
473        let near_zero_count = activations
474            .iter()
475            .filter(|&&x| x.abs() < self.config.dead_neuron_threshold)
476            .count();
477        let sparsity = near_zero_count as f32 / activations.len() as f32;
478
479        ActivationStatistics {
480            mean,
481            std,
482            min,
483            max,
484            percentile_25,
485            percentile_75,
486            skewness,
487            kurtosis,
488            sparsity,
489        }
490    }
491
492    /// Classify activation pattern type
493    fn classify_activation_pattern(&self, stats: &ActivationStatistics) -> ActivationPatternType {
494        if stats.sparsity > 0.9 {
495            ActivationPatternType::Dead
496        } else if stats.sparsity > 0.7 {
497            ActivationPatternType::Sparse
498        } else if stats.max > 0.95 && stats.mean > 0.8 {
499            ActivationPatternType::Saturated
500        } else if stats.std / stats.mean.abs().max(1e-8) > 2.0 {
501            ActivationPatternType::Oscillating
502        } else if stats.mean.abs() > 0.1 && stats.mean * stats.min < 0.0 {
503            ActivationPatternType::Bipolar
504        } else if stats.sparsity < 0.3 {
505            ActivationPatternType::Dense
506        } else {
507            ActivationPatternType::Normal
508        }
509    }
510
511    /// Compute stability score for neuron activations
512    fn compute_stability_score(&self, activations: &[f32]) -> f32 {
513        if activations.len() < 2 {
514            return 0.0;
515        }
516
517        let mean = activations.iter().sum::<f32>() / activations.len() as f32;
518        let variance =
519            activations.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / activations.len() as f32;
520
521        // Stability is inverse of coefficient of variation
522        if mean.abs() > 1e-8 {
523            1.0 / (1.0 + variance.sqrt() / mean.abs())
524        } else {
525            0.0
526        }
527    }
528
529    /// Compute selectivity score (how selective the neuron is)
530    fn compute_selectivity_score(&self, activations: &[f32]) -> f32 {
531        if activations.is_empty() {
532            return 0.0;
533        }
534
535        // Selectivity based on activation distribution
536        let max_activation = activations.iter().fold(0.0f32, |a, &b| a.max(b.abs()));
537        let mean_activation =
538            activations.iter().map(|x| x.abs()).sum::<f32>() / activations.len() as f32;
539
540        if max_activation > 1e-8 {
541            1.0 - (mean_activation / max_activation)
542        } else {
543            0.0
544        }
545    }
546
547    /// Detect dead neurons
548    async fn detect_dead_neurons(&self) -> Result<Vec<DeadNeuronInfo>> {
549        let mut dead_neurons = Vec::new();
550
551        for (layer_id, activation_history) in &self.activation_history {
552            if activation_history.is_empty() {
553                continue;
554            }
555
556            let neuron_count = activation_history[0].len();
557
558            for neuron_id in 0..neuron_count {
559                let neuron_activations: Vec<f32> = activation_history
560                    .iter()
561                    .map(|batch| batch.get(neuron_id).copied().unwrap_or(0.0))
562                    .collect();
563
564                let activation_level = neuron_activations.iter().map(|x| x.abs()).sum::<f32>()
565                    / neuron_activations.len() as f32;
566
567                let dead_probability = if activation_level < self.config.dead_neuron_threshold {
568                    1.0 - (activation_level / self.config.dead_neuron_threshold)
569                } else {
570                    0.0
571                };
572
573                if dead_probability > 0.5 {
574                    let suggested_action =
575                        self.suggest_neuron_repair_action(activation_level, &neuron_activations);
576
577                    dead_neurons.push(DeadNeuronInfo {
578                        layer_id: layer_id.clone(),
579                        neuron_id,
580                        activation_level,
581                        dead_probability,
582                        suggested_action,
583                    });
584                }
585            }
586        }
587
588        Ok(dead_neurons)
589    }
590
591    /// Suggest repair action for dead neurons
592    fn suggest_neuron_repair_action(
593        &self,
594        activation_level: f32,
595        activations: &[f32],
596    ) -> NeuronRepairAction {
597        if activation_level < self.config.dead_neuron_threshold * 0.1 {
598            NeuronRepairAction::Reinitialize
599        } else if activation_level < self.config.dead_neuron_threshold * 0.5 {
600            let variance =
601                activations.iter().map(|&x| x.powi(2)).sum::<f32>() / activations.len() as f32;
602            if variance < 1e-10 {
603                NeuronRepairAction::AddNoise
604            } else {
605                NeuronRepairAction::AdjustLearningRate
606            }
607        } else {
608            NeuronRepairAction::ChangeActivationFunction
609        }
610    }
611
612    /// Perform correlation analysis
613    async fn perform_correlation_analysis(&self) -> Result<CorrelationAnalysis> {
614        // Correlations between input gradients stand in for feature
615        // interactions: two inputs whose gradients move together influence the
616        // output together. This is a real correlation of real gradients, not a
617        // second-derivative (Hessian) interaction term, which would need
618        // double backpropagation the analyzer does not receive.
619        let gradient_vectors: Vec<&Vec<f32>> = self.input_gradients.values().collect();
620
621        if gradient_vectors.len() < 2 {
622            return Ok(CorrelationAnalysis {
623                correlation_matrix: Vec::new(),
624                significant_correlations: Vec::new(),
625                redundant_features: Vec::new(),
626                independent_features: Vec::new(),
627            });
628        }
629
630        let n = gradient_vectors.len();
631        let mut correlation_matrix = vec![vec![0.0; n]; n];
632        let mut significant_correlations = Vec::new();
633
634        // Compute correlation matrix
635        for i in 0..n {
636            for j in i..n {
637                let correlation =
638                    self.compute_correlation(gradient_vectors[i], gradient_vectors[j]);
639                correlation_matrix[i][j] = correlation;
640                correlation_matrix[j][i] = correlation;
641
642                if i != j && correlation.abs() > self.config.correlation_threshold {
643                    let correlation_type = if correlation.abs() > 0.8 {
644                        CorrelationType::Strong
645                    } else if correlation.abs() > 0.5 {
646                        CorrelationType::Moderate
647                    } else {
648                        CorrelationType::Weak
649                    };
650
651                    significant_correlations.push(CorrelationPair {
652                        feature_a: i,
653                        feature_b: j,
654                        correlation,
655                        p_value: correlation_p_value(correlation, gradient_vectors[i].len()),
656                        relationship_type: correlation_type,
657                    });
658                }
659            }
660        }
661
662        // Find redundant features (groups of highly correlated features)
663        let redundant_features = self.find_redundant_feature_groups(&correlation_matrix);
664
665        // Find independent features
666        let independent_features = self.find_independent_features(&correlation_matrix);
667
668        Ok(CorrelationAnalysis {
669            correlation_matrix,
670            significant_correlations,
671            redundant_features,
672            independent_features,
673        })
674    }
675
676    /// Compute Pearson correlation coefficient
677    /// See [`correlation_p_value`].
678    fn compute_correlation(&self, x: &[f32], y: &[f32]) -> f32 {
679        if x.len() != y.len() || x.is_empty() {
680            return 0.0;
681        }
682
683        let n = x.len() as f32;
684        let mean_x = x.iter().sum::<f32>() / n;
685        let mean_y = y.iter().sum::<f32>() / n;
686
687        let numerator: f32 =
688            x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi - mean_x) * (yi - mean_y)).sum();
689
690        let sum_sq_x: f32 = x.iter().map(|&xi| (xi - mean_x).powi(2)).sum();
691        let sum_sq_y: f32 = y.iter().map(|&yi| (yi - mean_y).powi(2)).sum();
692
693        let denominator = (sum_sq_x * sum_sq_y).sqrt();
694
695        if denominator > 1e-8 {
696            numerator / denominator
697        } else {
698            0.0
699        }
700    }
701
702    /// Find groups of redundant features
703    fn find_redundant_feature_groups(&self, correlation_matrix: &[Vec<f32>]) -> Vec<FeatureGroup> {
704        let mut groups = Vec::new();
705        let mut visited = HashSet::new();
706
707        for i in 0..correlation_matrix.len() {
708            if visited.contains(&i) {
709                continue;
710            }
711
712            let mut group = vec![i];
713            let mut group_correlations = Vec::new();
714
715            for j in (i + 1)..correlation_matrix.len() {
716                if correlation_matrix[i][j].abs() > 0.7 {
717                    group.push(j);
718                    group_correlations.push(correlation_matrix[i][j].abs());
719                    visited.insert(j);
720                }
721            }
722
723            if group.len() > 1 {
724                let average_correlation =
725                    group_correlations.iter().sum::<f32>() / group_correlations.len() as f32;
726                groups.push(FeatureGroup {
727                    features: group,
728                    average_correlation,
729                });
730            }
731
732            visited.insert(i);
733        }
734
735        groups
736    }
737
738    /// Find independent features
739    fn find_independent_features(&self, correlation_matrix: &[Vec<f32>]) -> Vec<usize> {
740        let mut independent = Vec::new();
741
742        for i in 0..correlation_matrix.len() {
743            let max_correlation = correlation_matrix[i]
744                .iter()
745                .enumerate()
746                .filter(|(j, _)| *j != i)
747                .map(|(_, &corr)| corr.abs())
748                .fold(0.0f32, |a, b| a.max(b));
749
750            if max_correlation < self.config.correlation_threshold {
751                independent.push(i);
752            }
753        }
754
755        independent
756    }
757
758    /// Generate behavior summary
759    fn generate_behavior_summary(&self, report: &mut BehaviorAnalysisReport) {
760        let total_neurons = report.activation_patterns.len();
761        let dead_neurons = report.dead_neurons.len();
762
763        report.behavior_summary.total_neurons_analyzed = total_neurons;
764        report.behavior_summary.dead_neuron_percentage = if total_neurons > 0 {
765            (dead_neurons as f32 / total_neurons as f32) * 100.0
766        } else {
767            0.0
768        };
769
770        if !report.activation_patterns.is_empty() {
771            report.behavior_summary.average_activation_sparsity = report
772                .activation_patterns
773                .iter()
774                .map(|p| p.activation_statistics.sparsity)
775                .sum::<f32>()
776                / report.activation_patterns.len() as f32;
777
778            report.behavior_summary.model_stability_score =
779                report.activation_patterns.iter().map(|p| p.stability_score).sum::<f32>()
780                    / report.activation_patterns.len() as f32;
781        }
782
783        // Simple entropy calculation for feature distribution
784        if !report.feature_importances.is_empty() {
785            let total_importance: f32 =
786                report.feature_importances.iter().map(|f| f.importance_score).sum();
787
788            if total_importance > 0.0 {
789                let entropy: f32 = report
790                    .feature_importances
791                    .iter()
792                    .map(|f| {
793                        let p = f.importance_score / total_importance;
794                        if p > 0.0 {
795                            -p * p.log2()
796                        } else {
797                            0.0
798                        }
799                    })
800                    .sum();
801                report.behavior_summary.feature_distribution_entropy = entropy;
802            }
803        }
804
805        // Overall interpretability score
806        report.behavior_summary.interpretability_score =
807            (report.behavior_summary.model_stability_score * 0.4
808                + (1.0 - report.behavior_summary.dead_neuron_percentage / 100.0) * 0.3
809                + (1.0 - report.behavior_summary.average_activation_sparsity) * 0.3)
810                .max(0.0)
811                .min(1.0);
812    }
813
814    /// Generate behavior recommendations
815    fn generate_recommendations(&self, report: &mut BehaviorAnalysisReport) {
816        // Dead neuron recommendations
817        if report.behavior_summary.dead_neuron_percentage > 20.0 {
818            report.recommendations.push(BehaviorRecommendation {
819                category: RecommendationCategory::Training,
820                priority: Priority::Critical,
821                description: format!("High percentage of dead neurons detected ({:.1}%)",
822                                   report.behavior_summary.dead_neuron_percentage),
823                implementation: "Consider reducing learning rate, changing initialization, or adding batch normalization".to_string(),
824                expected_impact: 0.8,
825            });
826        }
827
828        // Sparsity recommendations
829        if report.behavior_summary.average_activation_sparsity > 0.8 {
830            report.recommendations.push(BehaviorRecommendation {
831                category: RecommendationCategory::Architecture,
832                priority: Priority::High,
833                description: "Very sparse activations detected, model may be under-utilized".to_string(),
834                implementation: "Consider reducing model capacity or adjusting activation functions".to_string(),
835                expected_impact: 0.6,
836            });
837        }
838
839        // Stability recommendations
840        if report.behavior_summary.model_stability_score < 0.5 {
841            report.recommendations.push(BehaviorRecommendation {
842                category: RecommendationCategory::Training,
843                priority: Priority::High,
844                description: "Low model stability detected".to_string(),
845                implementation: "Consider adding regularization, reducing learning rate, or using gradient clipping".to_string(),
846                expected_impact: 0.7,
847            });
848        }
849
850        // Feature importance recommendations
851        if report.feature_importances.len() > 10 {
852            let top_features = &report.feature_importances[..5];
853            let bottom_features =
854                &report.feature_importances[report.feature_importances.len() - 5..];
855
856            let top_importance: f32 = top_features.iter().map(|f| f.importance_score).sum();
857            let bottom_importance: f32 = bottom_features.iter().map(|f| f.importance_score).sum();
858
859            if top_importance > bottom_importance * 10.0 {
860                report.recommendations.push(BehaviorRecommendation {
861                    category: RecommendationCategory::DataPreprocessing,
862                    priority: Priority::Medium,
863                    description: "Highly imbalanced feature importance detected".to_string(),
864                    implementation: "Consider feature selection or dimensionality reduction"
865                        .to_string(),
866                    expected_impact: 0.5,
867                });
868            }
869        }
870    }
871
872    /// Generate a comprehensive report
873    pub async fn generate_report(&self) -> Result<BehaviorAnalysisReport> {
874        let mut temp_analyzer = BehaviorAnalyzer {
875            config: self.config.clone(),
876            activation_history: self.activation_history.clone(),
877            input_gradients: self.input_gradients.clone(),
878            feature_attributions: self.feature_attributions.clone(),
879            analysis_cache: HashMap::new(),
880        };
881
882        temp_analyzer.analyze().await
883    }
884
885    /// Clear all recorded data
886    pub fn clear(&mut self) {
887        self.activation_history.clear();
888        self.input_gradients.clear();
889        self.feature_attributions.clear();
890        self.analysis_cache.clear();
891    }
892
893    /// Get summary of current analysis state
894    pub fn get_analysis_summary(&self) -> AnalysisSummary {
895        AnalysisSummary {
896            total_layers_tracked: self.activation_history.len(),
897            total_activation_samples: self
898                .activation_history
899                .values()
900                .map(|history| history.len())
901                .sum(),
902            total_inputs_tracked: self.input_gradients.len(),
903            // Coverage would need the model's total layer count to divide by,
904            // which this analyzer is never told. It used to report a flat
905            // `1.0` -- "100% covered" -- as soon as a single layer had been
906            // tracked.
907            analysis_coverage: None,
908        }
909    }
910}
911
912/// Summary of analysis state
913#[derive(Debug, Clone, Serialize, Deserialize)]
914pub struct AnalysisSummary {
915    pub total_layers_tracked: usize,
916    pub total_activation_samples: usize,
917    pub total_inputs_tracked: usize,
918    /// Fraction of the model's layers this analysis covers.
919    ///
920    /// Always `None`: the analyzer only ever sees the layers a caller chose to
921    /// record, and is never told how many the model has, so there is no
922    /// denominator. Previously a flat `1.0` whenever anything at all had been
923    /// tracked.
924    pub analysis_coverage: Option<f32>,
925}
926
927#[cfg(test)]
928#[path = "behavior_analysis_tests.rs"]
929mod behavior_analysis_tests;
930
931/// Two-sided p-value for a Pearson correlation `r` computed from `n` paired
932/// samples, via `t = r * sqrt((n - 2) / (1 - r^2))` on `n - 2` degrees of
933/// freedom.
934///
935/// `None` when `n < 3` (no residual degrees of freedom) or `|r| >= 1` (the
936/// statistic diverges). This replaces the constant `0.01` that every reported
937/// correlation pair used to carry.
938fn correlation_p_value(correlation: f32, sample_count: usize) -> Option<f32> {
939    if sample_count < 3 {
940        return None;
941    }
942    let r = f64::from(correlation);
943    let denominator = 1.0 - r * r;
944    if denominator <= 0.0 {
945        return None;
946    }
947    let degrees_of_freedom = sample_count as f64 - 2.0;
948    let t_statistic = r * (degrees_of_freedom / denominator).sqrt();
949    trustformers_core::statistics::student_t_two_sided_p_value(t_statistic, degrees_of_freedom)
950        .map(|p| p as f32)
951}