1use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct BehaviorAnalysisConfig {
13 pub enable_input_sensitivity: bool,
15 pub enable_feature_importance: bool,
17 pub enable_activation_patterns: bool,
19 pub enable_dead_neuron_detection: bool,
21 pub enable_correlation_analysis: bool,
23 pub dead_neuron_threshold: f32,
25 pub sensitivity_samples: usize,
27 pub perturbation_magnitude: f32,
29 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#[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#[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#[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, }
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#[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, }
131
132#[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 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 pub average_correlation: f32,
171}
172
173#[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#[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 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 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 pub fn record_input_gradients(&mut self, input_id: String, gradients: Vec<f32>) {
255 self.input_gradients.insert(input_id, gradients);
256 }
257
258 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 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 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, });
323 }
324 }
325
326 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 fn estimate_perturbation_impact(&self, gradient: f32, _dimension: usize) -> f32 {
351 gradient.abs() * self.config.perturbation_magnitude
352 }
353
354 async fn calculate_feature_importance(&self) -> Result<Vec<FeatureImportance>> {
356 let mut importances = Vec::new();
357
358 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 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 fn calculate_attribution_confidence(&self, score: f32) -> f32 {
387 (score.tanh() * 0.5 + 0.5).min(1.0)
389 }
390
391 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 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 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 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 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 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 if mean.abs() > 1e-8 {
523 1.0 / (1.0 + variance.sqrt() / mean.abs())
524 } else {
525 0.0
526 }
527 }
528
529 fn compute_selectivity_score(&self, activations: &[f32]) -> f32 {
531 if activations.is_empty() {
532 return 0.0;
533 }
534
535 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 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 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 async fn perform_correlation_analysis(&self) -> Result<CorrelationAnalysis> {
614 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 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 let redundant_features = self.find_redundant_feature_groups(&correlation_matrix);
664
665 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 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 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 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 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 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 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 fn generate_recommendations(&self, report: &mut BehaviorAnalysisReport) {
816 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 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 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 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 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 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 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 analysis_coverage: None,
908 }
909 }
910}
911
912#[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 pub analysis_coverage: Option<f32>,
925}
926
927#[cfg(test)]
928#[path = "behavior_analysis_tests.rs"]
929mod behavior_analysis_tests;
930
931fn 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}