trustformers_debug/model_diagnostics/
training.rs1use std::collections::VecDeque;
8
9use super::types::{
10 ConvergenceStatus, ModelPerformanceMetrics, OverfittingIndicator, PlateauInfo,
11 TrainingDynamics, TrainingStability, UnderfittingIndicator,
12};
13
14#[derive(Debug)]
16pub struct TrainingDynamicsAnalyzer {
17 metrics_history: VecDeque<ModelPerformanceMetrics>,
19 config: TrainingAnalysisConfig,
21 current_state: TrainingState,
23}
24
25#[derive(Debug, Clone)]
27pub struct TrainingAnalysisConfig {
28 pub convergence_window: usize,
30 pub min_improvement_threshold: f64,
32 pub max_variance_threshold: f64,
34 pub min_plateau_duration: usize,
36 pub overfitting_gap_threshold: f64,
38 pub min_learning_rate: f64,
40}
41
42impl Default for TrainingAnalysisConfig {
43 fn default() -> Self {
44 Self {
45 convergence_window: 20,
46 min_improvement_threshold: 0.001,
47 max_variance_threshold: 0.1,
48 min_plateau_duration: 10,
49 overfitting_gap_threshold: 0.05,
50 min_learning_rate: 1e-6,
51 }
52 }
53}
54
55#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
57pub struct TrainingState {
58 steps_since_improvement: usize,
60 best_loss: f64,
62 current_plateau: Option<PlateauInfo>,
64 convergence_history: VecDeque<ConvergenceStatus>,
66}
67
68impl Default for TrainingState {
69 fn default() -> Self {
70 Self {
71 steps_since_improvement: 0,
72 best_loss: f64::INFINITY,
73 current_plateau: None,
74 convergence_history: VecDeque::new(),
75 }
76 }
77}
78
79impl TrainingDynamicsAnalyzer {
80 pub fn new() -> Self {
82 Self {
83 metrics_history: VecDeque::new(),
84 config: TrainingAnalysisConfig::default(),
85 current_state: TrainingState::default(),
86 }
87 }
88
89 pub fn with_config(config: TrainingAnalysisConfig) -> Self {
91 Self {
92 metrics_history: VecDeque::new(),
93 config,
94 current_state: TrainingState::default(),
95 }
96 }
97
98 pub fn add_metrics(&mut self, metrics: ModelPerformanceMetrics) {
100 if metrics.loss < self.current_state.best_loss {
102 self.current_state.best_loss = metrics.loss;
103 self.current_state.steps_since_improvement = 0;
104 } else {
105 self.current_state.steps_since_improvement += 1;
106 }
107
108 self.metrics_history.push_back(metrics);
109
110 if self.metrics_history.len() > 1000 {
112 self.metrics_history.pop_front();
113 }
114
115 let status = self.detect_convergence_status();
117 self.current_state.convergence_history.push_back(status);
118 if self.current_state.convergence_history.len() > 50 {
119 self.current_state.convergence_history.pop_front();
120 }
121 }
122
123 pub fn record_training_dynamics(&mut self, _dynamics: TrainingDynamics) {
125 }
128
129 pub fn analyze_training_dynamics(&self) -> TrainingDynamics {
131 let convergence_status = self.detect_convergence_status();
132 let training_stability = self.assess_training_stability();
133 let learning_efficiency = self.calculate_learning_efficiency();
134 let overfitting_indicators = self.detect_overfitting_indicators();
135 let underfitting_indicators = self.detect_underfitting_indicators();
136 let plateau_detection = self.detect_plateau();
137
138 TrainingDynamics {
139 convergence_status,
140 training_stability,
141 learning_efficiency,
142 overfitting_indicators,
143 underfitting_indicators,
144 plateau_detection,
145 }
146 }
147
148 pub fn detect_convergence_status(&self) -> ConvergenceStatus {
150 if self.metrics_history.len() < self.config.convergence_window {
151 return ConvergenceStatus::Unknown;
152 }
153
154 let recent_metrics: Vec<_> =
155 self.metrics_history.iter().rev().take(self.config.convergence_window).collect();
156
157 let losses: Vec<f64> = recent_metrics.iter().map(|m| m.loss).collect();
158
159 if self.is_converged(&losses) {
161 ConvergenceStatus::Converged
162 } else if self.is_diverging(&losses) {
163 ConvergenceStatus::Diverging
164 } else if self.is_oscillating(&losses) {
165 ConvergenceStatus::Oscillating
166 } else if self.is_plateau(&losses) {
167 ConvergenceStatus::Plateau
168 } else if self.is_converging(&losses) {
169 ConvergenceStatus::Converging
170 } else {
171 ConvergenceStatus::Unknown
172 }
173 }
174
175 pub fn assess_training_stability(&self) -> TrainingStability {
177 if self.metrics_history.len() < 10 {
178 return TrainingStability::Unknown;
179 }
180
181 let recent_losses: Vec<f64> =
182 self.metrics_history.iter().rev().take(20).map(|m| m.loss).collect();
183
184 let variance = self.calculate_variance(&recent_losses);
185
186 if variance > self.config.max_variance_threshold {
187 TrainingStability::Unstable
188 } else if variance > self.config.max_variance_threshold / 2.0 {
189 TrainingStability::HighVariance
190 } else {
191 TrainingStability::Stable
192 }
193 }
194
195 pub fn calculate_learning_efficiency(&self) -> f64 {
197 if self.metrics_history.len() < 2 {
198 return 0.0;
199 }
200
201 let initial_loss = self.metrics_history.front().map(|m| m.loss).unwrap_or(0.0);
202 let current_loss = self.metrics_history.back().map(|m| m.loss).unwrap_or(0.0);
203 let steps = self.metrics_history.len();
204
205 if initial_loss <= current_loss {
206 return 0.0;
207 }
208
209 let improvement = (initial_loss - current_loss) / initial_loss;
210 let efficiency = improvement / (steps as f64).sqrt();
211
212 efficiency.min(1.0)
213 }
214
215 pub fn detect_overfitting_indicators(&self) -> Vec<OverfittingIndicator> {
217 let mut indicators = Vec::new();
218
219 if self.metrics_history.len() > 10 {
221 let recent_losses: Vec<f64> =
222 self.metrics_history.iter().rev().take(10).map(|m| m.loss).collect();
223
224 let avg_loss = recent_losses.iter().sum::<f64>() / recent_losses.len() as f64;
226 if avg_loss < 0.01 {
227 indicators.push(OverfittingIndicator::PerfectTrainingAccuracy);
228 }
229
230 let variance = self.calculate_variance(&recent_losses);
232 if variance > 0.05 {
233 indicators.push(OverfittingIndicator::HighVarianceInValidation);
234 }
235 }
236
237 indicators
238 }
239
240 pub fn detect_underfitting_indicators(&self) -> Vec<UnderfittingIndicator> {
242 let mut indicators = Vec::new();
243
244 if let Some(current_metrics) = self.metrics_history.back() {
245 if current_metrics.loss > 1.0 {
247 indicators.push(UnderfittingIndicator::HighTrainingLoss {
248 loss: current_metrics.loss,
249 threshold: 1.0,
250 });
251 }
252
253 if let Some(accuracy) = current_metrics.accuracy {
255 if accuracy < 0.5 {
256 indicators.push(UnderfittingIndicator::LowTrainingAccuracy {
257 accuracy,
258 threshold: 0.5,
259 });
260 }
261 }
262
263 if self.current_state.steps_since_improvement > 50 {
265 indicators.push(UnderfittingIndicator::SlowConvergence {
266 steps_taken: self.metrics_history.len(),
267 expected: self.metrics_history.len() / 2,
268 });
269 }
270
271 if self.current_state.steps_since_improvement > 100 {
273 indicators.push(UnderfittingIndicator::NoLearning {
274 steps_without_improvement: self.current_state.steps_since_improvement,
275 });
276 }
277 }
278
279 indicators
280 }
281
282 pub fn detect_plateau(&self) -> Option<PlateauInfo> {
284 if self.metrics_history.len() < self.config.min_plateau_duration {
285 return None;
286 }
287
288 let recent_losses: Vec<f64> = self
289 .metrics_history
290 .iter()
291 .rev()
292 .take(self.config.min_plateau_duration)
293 .map(|m| m.loss)
294 .collect();
295
296 let variance = self.calculate_variance(&recent_losses);
297 let mean_loss = recent_losses.iter().sum::<f64>() / recent_losses.len() as f64;
298
299 if variance < self.config.min_improvement_threshold {
301 let start_step = self.metrics_history.len() - self.config.min_plateau_duration;
302 Some(PlateauInfo {
303 start_step,
304 duration_steps: self.config.min_plateau_duration,
305 plateau_value: mean_loss,
306 variance,
307 })
308 } else {
309 None
310 }
311 }
312
313 pub fn generate_training_recommendations(&self) -> Vec<TrainingRecommendation> {
315 let mut recommendations = Vec::new();
316 let dynamics = self.analyze_training_dynamics();
317
318 match dynamics.convergence_status {
319 ConvergenceStatus::Diverging => {
320 recommendations.push(TrainingRecommendation {
321 category: "Convergence".to_string(),
322 priority: TrainingRecommendationPriority::Critical,
323 description: "Training is diverging".to_string(),
324 action: "Reduce learning rate immediately".to_string(),
325 expected_impact: 0.8,
326 });
327 },
328 ConvergenceStatus::Plateau => {
329 recommendations.push(TrainingRecommendation {
330 category: "Convergence".to_string(),
331 priority: TrainingRecommendationPriority::High,
332 description: "Training has reached a plateau".to_string(),
333 action: "Consider learning rate scheduling or data augmentation".to_string(),
334 expected_impact: 0.6,
335 });
336 },
337 _ => {},
338 }
339
340 if let TrainingStability::Unstable = dynamics.training_stability {
341 recommendations.push(TrainingRecommendation {
342 category: "Stability".to_string(),
343 priority: TrainingRecommendationPriority::High,
344 description: "Training is unstable".to_string(),
345 action: "Reduce learning rate or add gradient clipping".to_string(),
346 expected_impact: 0.7,
347 });
348 }
349
350 if dynamics.learning_efficiency < 0.3 {
351 recommendations.push(TrainingRecommendation {
352 category: "Efficiency".to_string(),
353 priority: TrainingRecommendationPriority::Medium,
354 description: "Low learning efficiency detected".to_string(),
355 action: "Consider architecture changes or hyperparameter tuning".to_string(),
356 expected_impact: 0.5,
357 });
358 }
359
360 recommendations
361 }
362
363 fn is_converged(&self, losses: &[f64]) -> bool {
365 if losses.len() < 5 {
366 return false;
367 }
368
369 let recent_variance = self.calculate_variance(&losses[..5]);
370 recent_variance < self.config.min_improvement_threshold && losses[0] < 0.01
371 }
372
373 fn is_diverging(&self, losses: &[f64]) -> bool {
374 if losses.len() < 3 {
375 return false;
376 }
377
378 let (Some(&first), Some(&last)) = (losses.first(), losses.last()) else {
379 return false;
380 };
381 losses.windows(2).all(|w| w[1] >= w[0]) && (last / first) > 1.1
383 }
384
385 fn is_oscillating(&self, losses: &[f64]) -> bool {
386 if losses.len() < 6 {
387 return false;
388 }
389
390 let mut direction_changes = 0;
392 for window in losses.windows(3) {
393 let trend1 = window[1] - window[0];
394 let trend2 = window[2] - window[1];
395 if trend1.signum() != trend2.signum() {
396 direction_changes += 1;
397 }
398 }
399
400 direction_changes > losses.len() / 3
401 }
402
403 fn is_plateau(&self, losses: &[f64]) -> bool {
404 let variance = self.calculate_variance(losses);
405 variance < self.config.min_improvement_threshold
406 }
407
408 fn is_converging(&self, losses: &[f64]) -> bool {
409 if losses.len() < 3 {
410 return false;
411 }
412
413 let trend = self.calculate_trend(losses);
415 trend < -self.config.min_improvement_threshold
416 }
417
418 fn calculate_variance(&self, values: &[f64]) -> f64 {
419 if values.len() < 2 {
420 return 0.0;
421 }
422
423 let mean = values.iter().sum::<f64>() / values.len() as f64;
424 let variance =
425 values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
426 variance
427 }
428
429 fn calculate_trend(&self, values: &[f64]) -> f64 {
430 if values.len() < 2 {
431 return 0.0;
432 }
433
434 let n = values.len() as f64;
435 let x_mean = (n - 1.0) / 2.0;
436 let y_mean = values.iter().sum::<f64>() / n;
437
438 let mut numerator = 0.0;
439 let mut denominator = 0.0;
440
441 for (i, &y) in values.iter().enumerate() {
442 let x = i as f64;
443 numerator += (x - x_mean) * (y - y_mean);
444 denominator += (x - x_mean).powi(2);
445 }
446
447 if denominator == 0.0 {
448 0.0
449 } else {
450 numerator / denominator
451 }
452 }
453
454 pub fn clear(&mut self) {
456 self.metrics_history.clear();
457 self.current_state = TrainingState::default();
458 }
459
460 pub fn get_training_state(&self) -> &TrainingState {
462 &self.current_state
463 }
464
465 pub async fn generate_report(&self) -> anyhow::Result<TrainingDynamicsReport> {
467 let training_dynamics = self.analyze_training_dynamics();
468 let recommendations = self.generate_recommendations();
469
470 Ok(TrainingDynamicsReport {
471 training_dynamics,
472 recommendations,
473 current_state: self.current_state.clone(),
474 })
475 }
476
477 fn generate_recommendations(&self) -> Vec<TrainingRecommendation> {
479 let mut recommendations = Vec::new();
480
481 recommendations.push(TrainingRecommendation {
483 category: "General".to_string(),
484 description: "Continue monitoring training dynamics".to_string(),
485 action: "Monitor training progress and adjust parameters as needed".to_string(),
486 priority: TrainingRecommendationPriority::Low,
487 expected_impact: 0.1,
488 });
489
490 recommendations
491 }
492}
493
494impl Default for TrainingDynamicsAnalyzer {
495 fn default() -> Self {
496 Self::new()
497 }
498}
499
500#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
502pub struct TrainingRecommendation {
503 pub category: String,
505 pub priority: TrainingRecommendationPriority,
507 pub description: String,
509 pub action: String,
511 pub expected_impact: f64,
513}
514
515#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
517pub enum TrainingRecommendationPriority {
518 Low,
520 Medium,
522 High,
524 Critical,
526}
527
528#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
530pub struct TrainingDynamicsReport {
531 pub training_dynamics: TrainingDynamics,
533 pub recommendations: Vec<TrainingRecommendation>,
535 pub current_state: TrainingState,
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use chrono::Utc;
543
544 fn create_test_metrics(step: usize, loss: f64) -> ModelPerformanceMetrics {
545 ModelPerformanceMetrics {
546 training_step: step,
547 loss,
548 accuracy: Some(0.8),
549 learning_rate: 0.001,
550 batch_size: 32,
551 throughput_samples_per_sec: 100.0,
552 memory_usage_mb: 1000.0,
553 gpu_utilization: Some(0.9),
554 timestamp: Utc::now(),
555 }
556 }
557
558 #[test]
559 fn test_training_dynamics_analyzer_creation() {
560 let analyzer = TrainingDynamicsAnalyzer::new();
561 assert_eq!(analyzer.metrics_history.len(), 0);
562 }
563
564 #[test]
565 fn test_add_metrics() {
566 let mut analyzer = TrainingDynamicsAnalyzer::new();
567 let metrics = create_test_metrics(1, 0.5);
568
569 analyzer.add_metrics(metrics);
570 assert_eq!(analyzer.metrics_history.len(), 1);
571 assert_eq!(analyzer.current_state.best_loss, 0.5);
572 }
573
574 #[test]
575 fn test_convergence_detection() {
576 let mut analyzer = TrainingDynamicsAnalyzer::new();
577
578 for i in 1..=25 {
580 let loss = 1.0 / (i as f64);
581 let metrics = create_test_metrics(i, loss);
582 analyzer.add_metrics(metrics);
583 }
584
585 let status = analyzer.detect_convergence_status();
586 matches!(
587 status,
588 ConvergenceStatus::Converging | ConvergenceStatus::Converged
589 );
590 }
591
592 #[test]
593 fn test_learning_efficiency_calculation() {
594 let mut analyzer = TrainingDynamicsAnalyzer::new();
595
596 analyzer.add_metrics(create_test_metrics(1, 1.0));
597 analyzer.add_metrics(create_test_metrics(2, 0.5));
598 analyzer.add_metrics(create_test_metrics(3, 0.25));
599
600 let efficiency = analyzer.calculate_learning_efficiency();
601 assert!(efficiency > 0.0);
602 }
603
604 #[test]
605 fn test_plateau_detection() {
606 let mut analyzer = TrainingDynamicsAnalyzer::new();
607
608 for i in 1..=15 {
610 let metrics = create_test_metrics(i, 0.1); analyzer.add_metrics(metrics);
612 }
613
614 let plateau = analyzer.detect_plateau();
615 assert!(plateau.is_some());
616 }
617
618 #[test]
619 fn test_training_stability_assessment() {
620 let mut analyzer = TrainingDynamicsAnalyzer::new();
621
622 for i in 1..=20 {
624 let loss = 0.5 + (i as f64 * 0.001); let metrics = create_test_metrics(i, loss);
626 analyzer.add_metrics(metrics);
627 }
628
629 let stability = analyzer.assess_training_stability();
630 matches!(stability, TrainingStability::Stable);
631 }
632}