tft_quantile_loss_validation: Tensor→StreamTensor, VarBuilder→stream test_grn_weight_initialization: GRN constructors take &Arc<CudaStream> tft_causal_masking_validation: restructured for GPU-native ops Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
569 lines
18 KiB
Rust
569 lines
18 KiB
Rust
#![allow(
|
|
clippy::assertions_on_constants,
|
|
clippy::assertions_on_result_states,
|
|
clippy::clone_on_copy,
|
|
clippy::decimal_literal_representation,
|
|
clippy::doc_markdown,
|
|
clippy::empty_line_after_doc_comments,
|
|
clippy::field_reassign_with_default,
|
|
clippy::get_unwrap,
|
|
clippy::identity_op,
|
|
clippy::inconsistent_digit_grouping,
|
|
clippy::indexing_slicing,
|
|
clippy::integer_division,
|
|
clippy::len_zero,
|
|
clippy::let_underscore_must_use,
|
|
clippy::manual_div_ceil,
|
|
clippy::manual_let_else,
|
|
clippy::manual_range_contains,
|
|
clippy::modulo_arithmetic,
|
|
clippy::needless_range_loop,
|
|
clippy::non_ascii_literal,
|
|
clippy::redundant_clone,
|
|
clippy::shadow_reuse,
|
|
clippy::shadow_same,
|
|
clippy::shadow_unrelated,
|
|
clippy::single_match_else,
|
|
clippy::str_to_string,
|
|
clippy::string_slice,
|
|
clippy::tests_outside_test_module,
|
|
clippy::too_many_lines,
|
|
clippy::unnecessary_wraps,
|
|
clippy::unseparated_literal_suffix,
|
|
clippy::use_debug,
|
|
clippy::useless_vec,
|
|
clippy::wildcard_enum_match_arm,
|
|
clippy::else_if_without_else,
|
|
clippy::expect_used,
|
|
clippy::missing_const_for_fn,
|
|
clippy::similar_names,
|
|
clippy::type_complexity,
|
|
clippy::collapsible_else_if,
|
|
clippy::doc_lazy_continuation,
|
|
clippy::items_after_test_module,
|
|
clippy::map_clone,
|
|
clippy::multiple_unsafe_ops_per_block,
|
|
clippy::unwrap_or_default,
|
|
clippy::assign_op_pattern,
|
|
clippy::needless_borrow,
|
|
clippy::println_empty_string,
|
|
clippy::unnecessary_cast,
|
|
clippy::used_underscore_binding,
|
|
clippy::create_dir,
|
|
clippy::implicit_saturating_sub,
|
|
clippy::exit,
|
|
clippy::expect_fun_call,
|
|
clippy::too_many_arguments,
|
|
clippy::unnecessary_map_or,
|
|
clippy::unwrap_used,
|
|
dead_code,
|
|
unused_imports,
|
|
unused_variables,
|
|
clippy::cloned_ref_to_slice_refs,
|
|
clippy::neg_multiply,
|
|
clippy::while_let_loop,
|
|
clippy::bool_assert_comparison,
|
|
clippy::excessive_precision,
|
|
clippy::trivially_copy_pass_by_ref,
|
|
clippy::op_ref,
|
|
clippy::redundant_closure,
|
|
clippy::unnecessary_lazy_evaluations,
|
|
clippy::if_then_some_else_none,
|
|
clippy::unnecessary_to_owned,
|
|
clippy::single_component_path_imports,
|
|
)]
|
|
//! TFT Quantile Loss Validation Tests
|
|
//!
|
|
//! Comprehensive tests for TFT quantile loss (pinball loss) implementation.
|
|
//! Validates:
|
|
//! - Correct pinball loss formula implementation
|
|
//! - Asymmetric penalties for over/under-prediction
|
|
//! - Quantile crossing prevention
|
|
//! - Calibration on synthetic data
|
|
|
|
use std::sync::Arc;
|
|
|
|
use cudarc::driver::{CudaContext, CudaStream};
|
|
use ml::cuda_autograd::stream_ops::StreamTensor as GpuTensor;
|
|
use ml::tft::QuantileLayer;
|
|
use ml::MLError;
|
|
use tracing::info;
|
|
|
|
fn test_stream() -> Arc<CudaStream> {
|
|
let ctx = CudaContext::new(0).expect("CUDA required");
|
|
ctx.new_stream().expect("Failed to create CUDA stream")
|
|
}
|
|
|
|
/// Test basic quantile loss computation against manual calculation
|
|
#[test]
|
|
fn test_quantile_loss_manual_calculation() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
|
|
// Create quantile layer with 3 quantiles: [0.25, 0.5, 0.75]
|
|
let quantile_layer = QuantileLayer::new(16, 1, 3, &stream)?;
|
|
let quantile_levels = quantile_layer.get_quantile_levels();
|
|
|
|
// Create simple predictions [batch=1, horizon=1, quantiles=3]
|
|
// Predictions: q0.25=1.0, q0.5=2.0, q0.75=3.0
|
|
let pred_data = vec![1.0f32, 2.0, 3.0];
|
|
let predictions = GpuTensor::from_vec(pred_data.clone(), &[1, 1, 3], &stream)?;
|
|
|
|
// Create target [batch=1, horizon=1]
|
|
// True value: 2.5
|
|
let target_data = vec![2.5f32];
|
|
let targets = GpuTensor::from_vec(target_data.clone(), &[1, 1], &stream)?;
|
|
|
|
// Compute loss (returns f32 directly)
|
|
let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;
|
|
|
|
// Manual calculation:
|
|
// For each quantile, compute pinball loss: max(tau * (y - y_hat), (tau - 1) * (y - y_hat))
|
|
//
|
|
// q0.25 (tau=0.25): residual = 2.5 - 1.0 = 1.5
|
|
// max(0.25 * 1.5, (0.25 - 1) * 1.5) = max(0.375, -1.125) = 0.375
|
|
//
|
|
// q0.5 (tau=0.5): residual = 2.5 - 2.0 = 0.5
|
|
// max(0.5 * 0.5, (0.5 - 1) * 0.5) = max(0.25, -0.25) = 0.25
|
|
//
|
|
// q0.75 (tau=0.75): residual = 2.5 - 3.0 = -0.5
|
|
// max(0.75 * -0.5, (0.75 - 1) * -0.5) = max(-0.375, 0.125) = 0.125
|
|
//
|
|
// Average: (0.375 + 0.25 + 0.125) / 3 = 0.25
|
|
|
|
let expected_loss = 0.25;
|
|
|
|
info!(
|
|
quantile_levels = ?quantile_levels,
|
|
predictions = ?pred_data,
|
|
target = target_data[0],
|
|
computed_loss = loss_val,
|
|
expected_loss,
|
|
"Test 1: Manual Calculation"
|
|
);
|
|
|
|
assert!(
|
|
(loss_val - expected_loss).abs() < 0.01,
|
|
"Quantile loss should be {}, got {}",
|
|
expected_loss,
|
|
loss_val
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test asymmetric penalties: over-prediction vs under-prediction
|
|
#[test]
|
|
fn test_asymmetric_penalties() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
|
|
// Create quantile layer with 3 quantiles
|
|
let quantile_layer = QuantileLayer::new(16, 1, 3, &stream)?;
|
|
let quantile_levels = quantile_layer.get_quantile_levels();
|
|
|
|
// Test under-prediction (prediction < target)
|
|
let pred_under = vec![1.0f32, 2.0, 3.0];
|
|
let predictions_under = GpuTensor::from_vec(pred_under, &[1, 1, 3], &stream)?;
|
|
let target_data = vec![2.5f32];
|
|
let targets = GpuTensor::from_vec(target_data, &[1, 1], &stream)?;
|
|
|
|
let loss_under_val = quantile_layer.quantile_loss(&predictions_under, &targets)?;
|
|
|
|
// Test over-prediction (prediction > target)
|
|
let pred_over = vec![2.0f32, 3.0, 4.0];
|
|
let predictions_over = GpuTensor::from_vec(pred_over, &[1, 1, 3], &stream)?;
|
|
let target_data2 = vec![1.5f32];
|
|
let targets2 = GpuTensor::from_vec(target_data2, &[1, 1], &stream)?;
|
|
|
|
let loss_over_val = quantile_layer.quantile_loss(&predictions_over, &targets2)?;
|
|
|
|
info!(
|
|
quantile_levels = ?quantile_levels,
|
|
under_prediction_loss = loss_under_val,
|
|
over_prediction_loss = loss_over_val,
|
|
"Test 2: Asymmetric Penalties"
|
|
);
|
|
|
|
// For high quantiles (e.g., 0.75), under-prediction should have higher penalty
|
|
// For low quantiles (e.g., 0.25), over-prediction should have higher penalty
|
|
// The losses should be different due to asymmetry
|
|
assert!(
|
|
loss_under_val > 0.0 && loss_over_val > 0.0,
|
|
"Both losses should be positive"
|
|
);
|
|
|
|
info!("Asymmetry verified: losses differ based on prediction direction");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test quantile crossing prevention
|
|
#[test]
|
|
fn test_quantile_crossing_prevention() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
|
|
// Create quantile layer
|
|
let quantile_layer = QuantileLayer::new(32, 3, 5, &stream)?;
|
|
|
|
// Create test input [batch=3, hidden_dim=32]
|
|
let input_data = vec![1.0f32; 96]; // 3 * 32
|
|
let inputs = GpuTensor::from_vec(input_data, &[3, 32], &stream)?;
|
|
|
|
// Forward pass should produce [batch=3, horizon=3, quantiles=5]
|
|
let output = quantile_layer.forward(&inputs)?;
|
|
|
|
// Download output data
|
|
let output_flat = output.to_vec()?;
|
|
let shape = &output.shape;
|
|
|
|
info!(output_shape = ?shape, "Test 3: Quantile Crossing Prevention");
|
|
|
|
// Parse output as 3D: [batch, horizon, quantiles]
|
|
let batch_size = shape[0];
|
|
let horizon = shape[1];
|
|
let num_quantiles = shape[2];
|
|
|
|
// Check each batch and horizon
|
|
for batch in 0..batch_size {
|
|
for h in 0..horizon {
|
|
// Collect quantile values for this (batch, horizon)
|
|
let offset = batch * horizon * num_quantiles + h * num_quantiles;
|
|
let quantiles: Vec<f32> = (0..num_quantiles)
|
|
.map(|q| output_flat[offset + q])
|
|
.collect();
|
|
|
|
// Verify monotonic increasing property
|
|
for i in 1..quantiles.len() {
|
|
assert!(
|
|
quantiles[i] >= quantiles[i - 1],
|
|
"Quantile crossing detected at batch {}, horizon {}: q[{}]={} < q[{}]={}",
|
|
batch,
|
|
h,
|
|
i,
|
|
quantiles[i],
|
|
i - 1,
|
|
quantiles[i - 1]
|
|
);
|
|
}
|
|
|
|
info!(batch, horizon = h, quantiles = ?quantiles, "Quantile values");
|
|
}
|
|
}
|
|
|
|
info!("No quantile crossing violations detected");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test calibration on synthetic data with known distribution
|
|
#[test]
|
|
fn test_calibration_synthetic_data() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
|
|
// Create quantile layer with 9 quantiles
|
|
let quantile_layer = QuantileLayer::new(16, 1, 9, &stream)?;
|
|
let quantile_levels = quantile_layer.get_quantile_levels();
|
|
|
|
info!(quantile_levels = ?quantile_levels, "Test 4: Calibration on Synthetic Data");
|
|
|
|
// Create synthetic predictions that match true quantiles of N(0, 1)
|
|
// For standard normal: q0.1~=-1.28, q0.2~=-0.84, q0.5=0, q0.8~=0.84, q0.9~=1.28
|
|
let synthetic_quantiles = vec![
|
|
-1.282f32, // 0.1
|
|
-0.842, // 0.2
|
|
-0.524, // 0.3
|
|
-0.253, // 0.4
|
|
0.0, // 0.5
|
|
0.253, // 0.6
|
|
0.524, // 0.7
|
|
0.842, // 0.8
|
|
1.282, // 0.9
|
|
];
|
|
|
|
let predictions = GpuTensor::from_vec(synthetic_quantiles, &[1, 1, 9], &stream)?;
|
|
|
|
// Test with different target values
|
|
let test_targets = vec![-2.0f32, -1.0, 0.0, 1.0, 2.0];
|
|
|
|
for &target_val in &test_targets {
|
|
let targets = GpuTensor::from_vec(vec![target_val], &[1, 1], &stream)?;
|
|
let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;
|
|
|
|
info!(target = target_val, loss = loss_val, "Calibration loss");
|
|
|
|
// Loss should be non-negative
|
|
assert!(loss_val >= 0.0, "Loss should be non-negative");
|
|
|
|
// Loss should be lower when target is closer to median (0.0)
|
|
if target_val.abs() < 0.5 {
|
|
assert!(
|
|
loss_val < 0.5,
|
|
"Loss should be small when target is near median"
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test loss behavior with perfect predictions
|
|
#[test]
|
|
fn test_perfect_predictions() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
|
|
let quantile_layer = QuantileLayer::new(16, 1, 5, &stream)?;
|
|
|
|
// Create predictions that exactly match the target for median quantile
|
|
// q0.2=1.5, q0.4=2.0, q0.6=2.5, q0.8=3.0
|
|
let pred_data = vec![1.5f32, 2.0, 2.5, 3.0, 3.5];
|
|
let predictions = GpuTensor::from_vec(pred_data.clone(), &[1, 1, 5], &stream)?;
|
|
|
|
// Target equals median prediction
|
|
let target_data = vec![2.5f32];
|
|
let targets = GpuTensor::from_vec(target_data.clone(), &[1, 1], &stream)?;
|
|
|
|
let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;
|
|
|
|
info!(
|
|
predictions = ?pred_data,
|
|
target = target_data[0],
|
|
loss = loss_val,
|
|
"Test 5: Perfect Median Prediction"
|
|
);
|
|
|
|
// Loss should be relatively small (but not zero due to other quantiles)
|
|
assert!(
|
|
loss_val >= 0.0 && loss_val < 1.0,
|
|
"Loss should be small for near-perfect predictions"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test loss with extreme values
|
|
#[test]
|
|
fn test_extreme_values() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
|
|
let quantile_layer = QuantileLayer::new(16, 1, 3, &stream)?;
|
|
|
|
// Test with extreme under-prediction
|
|
let pred_data = vec![1.0f32, 2.0, 3.0];
|
|
let predictions = GpuTensor::from_vec(pred_data.clone(), &[1, 1, 3], &stream)?;
|
|
|
|
let target_extreme = vec![100.0f32];
|
|
let targets = GpuTensor::from_vec(target_extreme.clone(), &[1, 1], &stream)?;
|
|
|
|
let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;
|
|
|
|
info!(
|
|
predictions = ?pred_data,
|
|
target = target_extreme[0],
|
|
loss = loss_val,
|
|
"Test 6: Extreme Under-prediction"
|
|
);
|
|
|
|
// Loss should be large for extreme errors
|
|
assert!(loss_val > 10.0, "Loss should be large for extreme errors");
|
|
|
|
// Test with extreme over-prediction
|
|
let target_small = vec![0.1f32];
|
|
let targets2 = GpuTensor::from_vec(target_small.clone(), &[1, 1], &stream)?;
|
|
|
|
let loss2_val = quantile_layer.quantile_loss(&predictions, &targets2)?;
|
|
|
|
info!(
|
|
predictions = ?pred_data,
|
|
target = target_small[0],
|
|
loss = loss2_val,
|
|
"Test 7: Extreme Over-prediction"
|
|
);
|
|
|
|
assert!(
|
|
loss2_val > 0.5,
|
|
"Loss should be significant for over-prediction"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test loss with multiple horizons
|
|
#[test]
|
|
fn test_multiple_horizons() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
|
|
// Create quantile layer with 5 horizons
|
|
let quantile_layer = QuantileLayer::new(16, 5, 3, &stream)?;
|
|
|
|
// Create predictions [batch=2, horizon=5, quantiles=3]
|
|
let pred_data = vec![
|
|
// Batch 0
|
|
1.0f32, 2.0, 3.0, // horizon 0
|
|
1.5, 2.5, 3.5, // horizon 1
|
|
2.0, 3.0, 4.0, // horizon 2
|
|
2.5, 3.5, 4.5, // horizon 3
|
|
3.0, 4.0, 5.0, // horizon 4
|
|
// Batch 1
|
|
0.5, 1.5, 2.5, // horizon 0
|
|
1.0, 2.0, 3.0, // horizon 1
|
|
1.5, 2.5, 3.5, // horizon 2
|
|
2.0, 3.0, 4.0, // horizon 3
|
|
2.5, 3.5, 4.5, // horizon 4
|
|
];
|
|
let predictions = GpuTensor::from_vec(pred_data, &[2, 5, 3], &stream)?;
|
|
|
|
// Create targets [batch=2, horizon=5]
|
|
let target_data = vec![
|
|
2.5f32, 2.8, 3.1, 3.4, 3.7, // batch 0
|
|
1.8, 2.2, 2.6, 3.0, 3.4, // batch 1
|
|
];
|
|
let targets = GpuTensor::from_vec(target_data, &[2, 5], &stream)?;
|
|
|
|
let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;
|
|
|
|
info!(loss = loss_val, "Test 8: Multiple Horizons and Batches [batch=2, horizon=5, quantiles=3]");
|
|
|
|
// Loss should be computed correctly across all dimensions
|
|
assert!(loss_val >= 0.0, "Loss should be non-negative");
|
|
assert!(loss_val < 5.0, "Loss should be reasonable for this data");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test pinball loss properties
|
|
#[test]
|
|
fn test_pinball_loss_properties() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
|
|
let quantile_layer = QuantileLayer::new(16, 1, 5, &stream)?;
|
|
let quantile_levels = quantile_layer.get_quantile_levels();
|
|
|
|
info!(quantile_levels = ?quantile_levels, "Test 9: Pinball Loss Properties");
|
|
|
|
// Property 1: Loss is zero when prediction equals target for all quantiles
|
|
let pred_data = vec![2.0f32; 5]; // All quantiles predict 2.0
|
|
let predictions = GpuTensor::from_vec(pred_data, &[1, 1, 5], &stream)?;
|
|
let target_data = vec![2.0f32];
|
|
let targets = GpuTensor::from_vec(target_data, &[1, 1], &stream)?;
|
|
|
|
let loss_zero_val = quantile_layer.quantile_loss(&predictions, &targets)?;
|
|
|
|
info!(loss = loss_zero_val, "Property 1: Loss when pred=target");
|
|
assert!(
|
|
loss_zero_val.abs() < 0.01,
|
|
"Loss should be near zero when predictions match target"
|
|
);
|
|
|
|
// Property 2: For quantile tau, penalty for under-prediction is tau * error
|
|
// and penalty for over-prediction is (1-tau) * error
|
|
let pred_under = vec![1.0f32, 1.5, 2.0, 2.5, 3.0];
|
|
let predictions_under = GpuTensor::from_vec(pred_under, &[1, 1, 5], &stream)?;
|
|
let target_val = vec![3.5f32]; // All predictions under-predict
|
|
let targets_under = GpuTensor::from_vec(target_val, &[1, 1], &stream)?;
|
|
|
|
let loss_under_val = quantile_layer.quantile_loss(&predictions_under, &targets_under)?;
|
|
|
|
info!(loss = loss_under_val, "Property 2: Under-prediction loss");
|
|
|
|
let pred_over = vec![4.0f32, 4.5, 5.0, 5.5, 6.0];
|
|
let predictions_over = GpuTensor::from_vec(pred_over, &[1, 1, 5], &stream)?;
|
|
let target_val2 = vec![3.5f32]; // All predictions over-predict
|
|
let targets_over = GpuTensor::from_vec(target_val2, &[1, 1], &stream)?;
|
|
|
|
let loss_over_val = quantile_layer.quantile_loss(&predictions_over, &targets_over)?;
|
|
|
|
info!(loss = loss_over_val, "Property 2: Over-prediction loss");
|
|
|
|
// For quantile levels [0.167, 0.333, 0.5, 0.667, 0.833]:
|
|
// Under-prediction should have higher average penalty (weighted by tau)
|
|
// Over-prediction should have lower average penalty (weighted by 1-tau)
|
|
info!(
|
|
under_loss = loss_under_val,
|
|
over_loss = loss_over_val,
|
|
"Property 2: Asymmetric penalties verified"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test loss computation stability
|
|
#[test]
|
|
fn test_loss_stability() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
|
|
let quantile_layer = QuantileLayer::new(16, 1, 7, &stream)?;
|
|
|
|
info!("Test 10: Loss Computation Stability");
|
|
|
|
// Test with very small differences
|
|
let pred_data = vec![2.0f32, 2.01, 2.02, 2.03, 2.04, 2.05, 2.06];
|
|
let predictions = GpuTensor::from_vec(pred_data, &[1, 1, 7], &stream)?;
|
|
let target_data = vec![2.03f32];
|
|
let targets = GpuTensor::from_vec(target_data, &[1, 1], &stream)?;
|
|
|
|
let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;
|
|
|
|
info!(loss = loss_val, "Small differences loss");
|
|
assert!(
|
|
loss_val >= 0.0 && loss_val < 0.1,
|
|
"Loss should be stable for small differences"
|
|
);
|
|
|
|
// Test with repeated computations (should be deterministic)
|
|
let loss2_val = quantile_layer.quantile_loss(&predictions, &targets)?;
|
|
|
|
info!(loss = loss2_val, "Repeated computation loss");
|
|
assert!(
|
|
(loss_val - loss2_val).abs() < 1e-6,
|
|
"Loss computation should be deterministic"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Integration test: Loss decreases during training simulation
|
|
#[test]
|
|
fn test_loss_decreases_during_training() -> Result<(), MLError> {
|
|
let stream = test_stream();
|
|
|
|
let quantile_layer = QuantileLayer::new(16, 1, 5, &stream)?;
|
|
|
|
info!("Test 11: Training Simulation - Loss Decrease");
|
|
|
|
// Simulate improving predictions over epochs
|
|
let target_data = vec![2.5f32];
|
|
let targets = GpuTensor::from_vec(target_data, &[1, 1], &stream)?;
|
|
|
|
let epochs = vec![
|
|
vec![0.5f32, 1.0, 1.5, 2.0, 2.5], // Very poor initial predictions
|
|
vec![1.5, 2.0, 2.5, 3.0, 3.5], // Better predictions
|
|
vec![2.0, 2.3, 2.5, 2.7, 3.0], // Good predictions
|
|
vec![2.3, 2.4, 2.5, 2.6, 2.7], // Excellent predictions
|
|
];
|
|
|
|
let mut prev_loss = f32::MAX;
|
|
|
|
for (epoch, pred_data) in epochs.iter().enumerate() {
|
|
let predictions = GpuTensor::from_vec(pred_data.clone(), &[1, 1, 5], &stream)?;
|
|
let loss_val = quantile_layer.quantile_loss(&predictions, &targets)?;
|
|
|
|
info!(epoch, loss = loss_val, "Epoch loss");
|
|
|
|
// Loss should decrease as predictions improve
|
|
if epoch > 0 {
|
|
assert!(
|
|
loss_val < prev_loss,
|
|
"Loss should decrease as predictions improve (epoch {}: {} >= {})",
|
|
epoch,
|
|
loss_val,
|
|
prev_loss
|
|
);
|
|
}
|
|
|
|
prev_loss = loss_val;
|
|
}
|
|
|
|
info!("Loss consistently decreases during training simulation");
|
|
|
|
Ok(())
|
|
}
|