torsh_cli/commands/model/
validation.rs1#![allow(dead_code)]
11
12use anyhow::Result;
13use tracing::{debug, info, warn};
14
15use scirs2_core::random::{thread_rng, Distribution, Uniform};
17
18use torsh::core::device::DeviceType;
20use torsh::tensor::Tensor;
21
22use super::types::{LayerInfo, TorshModel};
23
24#[derive(Debug, Clone)]
26pub struct ValidationResult {
27 pub passed: bool,
29 pub accuracy: Option<f64>,
31 pub top5_accuracy: Option<f64>,
33 pub num_samples: usize,
35 pub successful_inferences: usize,
37 pub failed_inferences: usize,
39 pub avg_inference_time_ms: f64,
41 pub peak_memory_mb: f64,
43 pub gradient_check_passed: Option<bool>,
45 pub numerical_stability: f64,
47 pub errors: Vec<String>,
49 pub warnings: Vec<String>,
51}
52
53#[derive(Debug, Clone)]
55pub struct GradientCheckResult {
56 pub passed: bool,
58 pub max_relative_error: f64,
60 pub avg_relative_error: f64,
62 pub num_gradients_checked: usize,
64 pub failed_locations: Vec<String>,
66}
67
68#[derive(Debug, Clone)]
70pub struct StabilityAnalysis {
71 pub has_nan: bool,
73 pub has_inf: bool,
75 pub has_large_values: bool,
77 pub has_tiny_values: bool,
79 pub gradient_magnitude: Option<GradientStatistics>,
82 pub activation_stats: ActivationStatistics,
84}
85
86#[derive(Debug, Clone)]
88pub struct GradientStatistics {
89 pub mean: f64,
90 pub std: f64,
91 pub min: f64,
92 pub max: f64,
93 pub vanishing_percentage: f64,
95 pub exploding_percentage: f64,
97}
98
99#[derive(Debug, Clone)]
101pub struct ActivationStatistics {
102 pub mean: f64,
103 pub std: f64,
104 pub min: f64,
105 pub max: f64,
106 pub dead_neurons_percentage: f64,
108}
109
110pub async fn validate_model(
112 model: &TorshModel,
113 num_samples: usize,
114 check_gradients: bool,
115) -> Result<ValidationResult> {
116 info!(
117 "Validating model with {} samples (gradient check: {})",
118 num_samples, check_gradients
119 );
120
121 let mut errors = Vec::new();
122 let mut warnings = Vec::new();
123
124 if let Err(e) = validate_model_structure(model) {
126 errors.push(format!("Model structure validation failed: {}", e));
127 }
128
129 let (successful, failed, avg_time, peak_memory) =
131 run_inference_tests(model, num_samples).await?;
132
133 let gradient_check_result = if check_gradients {
135 match perform_gradient_check(model).await {
136 Ok(result) => Some(result.passed),
137 Err(e) => {
138 warnings.push(format!("Gradient check failed: {}", e));
139 None
140 }
141 }
142 } else {
143 None
144 };
145
146 let stability = analyze_numerical_stability(model).await?;
148 let numerical_stability = calculate_stability_score(&stability);
149
150 if stability.has_nan {
151 errors.push("Model contains NaN values".to_string());
152 }
153 if stability.has_inf {
154 errors.push("Model contains Inf values".to_string());
155 }
156
157 if let Some(gradient_magnitude) = &stability.gradient_magnitude {
158 if gradient_magnitude.vanishing_percentage > 50.0 {
159 warnings.push(format!(
160 "High vanishing gradient rate: {:.1}%",
161 gradient_magnitude.vanishing_percentage
162 ));
163 }
164
165 if gradient_magnitude.exploding_percentage > 10.0 {
166 warnings.push(format!(
167 "High exploding gradient rate: {:.1}%",
168 gradient_magnitude.exploding_percentage
169 ));
170 }
171 }
172
173 let passed = errors.is_empty() && successful > 0;
174
175 Ok(ValidationResult {
176 passed,
177 accuracy: None, top5_accuracy: None,
179 num_samples,
180 successful_inferences: successful,
181 failed_inferences: failed,
182 avg_inference_time_ms: avg_time,
183 peak_memory_mb: peak_memory,
184 gradient_check_passed: gradient_check_result,
185 numerical_stability,
186 errors,
187 warnings,
188 })
189}
190
191fn validate_model_structure(model: &TorshModel) -> Result<()> {
193 debug!("Validating model structure");
194
195 if model.layers.is_empty() {
197 anyhow::bail!("Model has no layers");
198 }
199
200 for layer in &model.layers {
202 if layer.input_shape.is_empty() {
203 anyhow::bail!("Layer {} has empty input shape", layer.name);
204 }
205 if layer.output_shape.is_empty() {
206 anyhow::bail!("Layer {} has empty output shape", layer.name);
207 }
208
209 if layer.trainable {
211 let weight_name = format!("{}.weight", layer.name);
212 if !model.weights.contains_key(&weight_name) {
213 anyhow::bail!("Trainable layer {} missing weight tensor", layer.name);
214 }
215 }
216 }
217
218 for i in 0..model.layers.len() - 1 {
220 let current = &model.layers[i];
221 let next = &model.layers[i + 1];
222
223 if current.output_shape != next.input_shape {
224 warn!(
225 "Shape mismatch between layers {} and {}: {:?} != {:?}",
226 current.name, next.name, current.output_shape, next.input_shape
227 );
228 }
229 }
230
231 Ok(())
232}
233
234async fn run_inference_tests(
236 model: &TorshModel,
237 num_samples: usize,
238) -> Result<(usize, usize, f64, f64)> {
239 info!("Running {} inference tests", num_samples);
240
241 let input_shape = model
242 .layers
243 .first()
244 .map(|l| l.input_shape.clone())
245 .unwrap_or_else(|| vec![784]);
246
247 let mut successful = 0;
248 let mut failed = 0;
249 let mut total_time = 0.0;
250 let mut peak_memory = 0.0f64;
251
252 for i in 0..num_samples {
253 let input = create_random_input(&input_shape)?;
254
255 let start = std::time::Instant::now();
256
257 match perform_forward_pass(model, &input).await {
258 Ok(output) => {
259 successful += 1;
260 total_time += start.elapsed().as_secs_f64() * 1000.0;
261
262 let memory = estimate_inference_memory(model, &output);
264 peak_memory = peak_memory.max(memory);
265
266 debug!(
267 "Inference {}: successful, output shape: {:?}",
268 i,
269 output.shape().dims()
270 );
271 }
272 Err(e) => {
273 failed += 1;
274 warn!("Inference {} failed: {}", i, e);
275 }
276 }
277 }
278
279 let avg_time = if successful > 0 {
280 total_time / successful as f64
281 } else {
282 0.0
283 };
284
285 Ok((successful, failed, avg_time, peak_memory))
286}
287
288fn create_random_input(shape: &[usize]) -> Result<Tensor<f32>> {
290 let mut rng = thread_rng();
291 let uniform = Uniform::new(-1.0f64, 1.0f64)?;
292
293 let num_elements: usize = shape.iter().product();
294 let data: Vec<f32> = (0..num_elements)
295 .map(|_| uniform.sample(&mut rng) as f32)
296 .collect();
297
298 Ok(Tensor::from_data(data, shape.to_vec(), DeviceType::Cpu)?)
299}
300
301async fn perform_forward_pass(model: &TorshModel, input: &Tensor<f32>) -> Result<Tensor<f32>> {
307 debug!("Performing forward pass");
308 super::tensor_integration::forward_pass(model, input)
309}
310
311fn estimate_layer_flops(layer: &LayerInfo) -> u64 {
313 let input_size: u64 = layer.input_shape.iter().map(|&x| x as u64).product();
314 let output_size: u64 = layer.output_shape.iter().map(|&x| x as u64).product();
315
316 match layer.layer_type.as_str() {
317 "Linear" | "Dense" => 2 * input_size * output_size,
318 "Conv2d" => {
319 let kernel_size = 9; 2 * kernel_size * output_size
321 }
322 "ReLU" | "Sigmoid" | "Tanh" => output_size,
323 _ => output_size,
324 }
325}
326
327fn estimate_inference_memory(model: &TorshModel, _output: &Tensor<f32>) -> f64 {
329 let param_memory: u64 = model
330 .weights
331 .values()
332 .map(|t| {
333 let elements: usize = t.shape.iter().product();
334 (elements * t.dtype.size_bytes()) as u64
335 })
336 .sum();
337
338 let activation_memory: u64 = model
339 .layers
340 .iter()
341 .map(|l| {
342 let output_elements: u64 = l.output_shape.iter().map(|&x| x as u64).product();
343 output_elements * 4 })
345 .sum();
346
347 (param_memory + activation_memory) as f64 / (1024.0 * 1024.0)
348}
349
350async fn perform_gradient_check(model: &TorshModel) -> Result<GradientCheckResult> {
361 info!("Performing gradient check");
362
363 let input_shape = model
364 .layers
365 .first()
366 .map(|l| l.input_shape.clone())
367 .unwrap_or_else(|| vec![784]);
368 let input = create_random_input(&input_shape)?;
369
370 let passed = super::tensor_integration::gradient_check(model, &input, 1e-5)?;
373
374 Ok(GradientCheckResult {
375 passed,
376 max_relative_error: 0.0,
377 avg_relative_error: 0.0,
378 num_gradients_checked: model.weights.len(),
379 failed_locations: Vec::new(),
380 })
381}
382
383async fn analyze_numerical_stability(model: &TorshModel) -> Result<StabilityAnalysis> {
392 info!("Analyzing numerical stability");
393
394 let input_shape = model
395 .layers
396 .first()
397 .map(|l| l.input_shape.clone())
398 .unwrap_or_else(|| vec![784]);
399 let input = create_random_input(&input_shape)?;
400 let output = super::tensor_integration::forward_pass(model, &input)?;
401 let values: Vec<f32> = output.to_vec()?;
402
403 let mut has_nan = false;
404 let mut has_inf = false;
405 let mut has_large_values = false;
406 let mut has_tiny_values = false;
407 for &val in &values {
408 if val.is_nan() {
409 has_nan = true;
410 }
411 if val.is_infinite() {
412 has_inf = true;
413 }
414 if val.abs() > 1e6 {
415 has_large_values = true;
416 }
417 if val != 0.0 && val.abs() < 1e-6 {
418 has_tiny_values = true;
419 }
420 }
421
422 let activation_stats = compute_activation_statistics(&values);
423
424 Ok(StabilityAnalysis {
425 has_nan,
426 has_inf,
427 has_large_values,
428 has_tiny_values,
429 gradient_magnitude: None,
430 activation_stats,
431 })
432}
433
434fn compute_activation_statistics(activations: &[f32]) -> ActivationStatistics {
436 if activations.is_empty() {
437 return ActivationStatistics {
438 mean: 0.0,
439 std: 0.0,
440 min: 0.0,
441 max: 0.0,
442 dead_neurons_percentage: 0.0,
443 };
444 }
445
446 let count = activations.len() as f64;
447 let mean = activations.iter().map(|&x| x as f64).sum::<f64>() / count;
448 let variance = activations
449 .iter()
450 .map(|&x| (x as f64 - mean).powi(2))
451 .sum::<f64>()
452 / count;
453 let std = variance.sqrt();
454 let min = activations.iter().copied().fold(f32::INFINITY, f32::min) as f64;
455 let max = activations
456 .iter()
457 .copied()
458 .fold(f32::NEG_INFINITY, f32::max) as f64;
459
460 let dead_count = activations.iter().filter(|&&x| x == 0.0).count();
461 let dead_neurons_percentage = (dead_count as f64 / count) * 100.0;
462
463 ActivationStatistics {
464 mean,
465 std,
466 min,
467 max,
468 dead_neurons_percentage,
469 }
470}
471
472fn calculate_stability_score(analysis: &StabilityAnalysis) -> f64 {
474 let mut score = 1.0f64;
475
476 if analysis.has_nan {
478 score -= 0.5;
479 }
480 if analysis.has_inf {
481 score -= 0.5;
482 }
483
484 if analysis.has_large_values {
486 score -= 0.1;
487 }
488 if analysis.has_tiny_values {
489 score -= 0.05;
490 }
491
492 if let Some(gradient_magnitude) = &analysis.gradient_magnitude {
494 if gradient_magnitude.vanishing_percentage > 50.0 {
495 score -= 0.2;
496 }
497 if gradient_magnitude.exploding_percentage > 10.0 {
498 score -= 0.2;
499 }
500 }
501
502 if analysis.activation_stats.dead_neurons_percentage > 50.0 {
504 score -= 0.1;
505 }
506
507 score.max(0.0)
508}
509
510pub fn format_validation_result(result: &ValidationResult) -> String {
512 let mut output = String::new();
513
514 output.push_str("╔═══════════════════════════════════════════════════════════════════════╗\n");
515 output.push_str("║ MODEL VALIDATION REPORT ║\n");
516 output
517 .push_str("╚═══════════════════════════════════════════════════════════════════════╝\n\n");
518
519 let status = if result.passed {
521 "✅ PASSED"
522 } else {
523 "❌ FAILED"
524 };
525 output.push_str(&format!("Status: {}\n\n", status));
526
527 output.push_str("📊 Inference Testing\n");
529 output.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
530 output.push_str(&format!(" Samples tested: {}\n", result.num_samples));
531 output.push_str(&format!(
532 " Successful: {}\n",
533 result.successful_inferences
534 ));
535 output.push_str(&format!(
536 " Failed: {}\n",
537 result.failed_inferences
538 ));
539 output.push_str(&format!(
540 " Avg inference time: {:.2} ms\n",
541 result.avg_inference_time_ms
542 ));
543 output.push_str(&format!(
544 " Peak memory: {:.2} MB\n",
545 result.peak_memory_mb
546 ));
547
548 if let Some(acc) = result.accuracy {
549 output.push_str(&format!(" Accuracy: {:.2}%\n", acc * 100.0));
550 }
551 if let Some(top5) = result.top5_accuracy {
552 output.push_str(&format!(" Top-5 Accuracy: {:.2}%\n", top5 * 100.0));
553 }
554
555 output.push_str("\n");
556
557 if let Some(grad_passed) = result.gradient_check_passed {
559 output.push_str("🔍 Gradient Checking\n");
560 output
561 .push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
562 output.push_str(&format!(
563 " Status: {}\n",
564 if grad_passed {
565 "✅ PASSED"
566 } else {
567 "❌ FAILED"
568 }
569 ));
570 output.push_str("\n");
571 }
572
573 output.push_str("📈 Numerical Stability\n");
575 output.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
576 output.push_str(&format!(
577 " Stability score: {:.2}/1.00\n",
578 result.numerical_stability
579 ));
580 output.push_str("\n");
581
582 if !result.errors.is_empty() {
584 output.push_str("❌ Errors\n");
585 output
586 .push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
587 for error in &result.errors {
588 output.push_str(&format!(" • {}\n", error));
589 }
590 output.push_str("\n");
591 }
592
593 if !result.warnings.is_empty() {
595 output.push_str("⚠️ Warnings\n");
596 output
597 .push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
598 for warning in &result.warnings {
599 output.push_str(&format!(" • {}\n", warning));
600 }
601 output.push_str("\n");
602 }
603
604 output
605}
606
607#[cfg(test)]
608mod tests {
609 use super::super::tensor_integration::create_real_model;
610 use super::*;
611
612 #[tokio::test]
613 async fn test_model_validation() {
614 let model = create_real_model("test", 3, DeviceType::Cpu)
615 .expect("create real model should succeed");
616 let result = validate_model(&model, 10, false)
617 .await
618 .expect("operation should succeed");
619
620 assert!(result.num_samples == 10);
621 assert!(result.successful_inferences > 0);
622 }
623
624 #[test]
625 fn test_structure_validation() {
626 let model = create_real_model("test", 2, DeviceType::Cpu)
627 .expect("create real model should succeed");
628 assert!(validate_model_structure(&model).is_ok());
629 }
630
631 #[tokio::test]
632 async fn test_gradient_check_is_honest_error() {
633 let model = create_real_model("test", 2, DeviceType::Cpu)
634 .expect("create real model should succeed");
635 assert!(perform_gradient_check(&model).await.is_err());
638 }
639
640 #[tokio::test]
641 async fn test_stability_analysis() {
642 let model = create_real_model("test", 2, DeviceType::Cpu)
643 .expect("create real model should succeed");
644 let analysis = analyze_numerical_stability(&model)
645 .await
646 .expect("operation should succeed");
647
648 assert!(!analysis.has_nan);
649 assert!(!analysis.has_inf);
650 }
651
652 #[test]
653 fn test_validation_formatting() {
654 let result = ValidationResult {
655 passed: true,
656 accuracy: Some(0.95),
657 top5_accuracy: Some(0.99),
658 num_samples: 100,
659 successful_inferences: 98,
660 failed_inferences: 2,
661 avg_inference_time_ms: 5.5,
662 peak_memory_mb: 125.3,
663 gradient_check_passed: Some(true),
664 numerical_stability: 0.92,
665 errors: vec![],
666 warnings: vec!["High memory usage".to_string()],
667 };
668
669 let formatted = format_validation_result(&result);
670 assert!(formatted.contains("VALIDATION REPORT"));
671 assert!(formatted.contains("PASSED"));
672 }
673}