lerobot/robot-learning-tutorial
508
1// Data generation utilities for synthetic training data2 3// ============================================================================4// RANDOM HELPERS - Make magic numbers explicit and readable5// ============================================================================6 7/**8 * Random utilities for ML training simulation9 */10export const Random = {11 // Basic random generators12 between: (min, max) => min + Math.random() * (max - min),13 intBetween: (min, max) => Math.floor(Random.between(min, max + 1)),14 15 // ML-specific generators16 learningRate: () => Random.between(0.02, 0.08),17 noiseAmplitude: (baseValue, reduction = 0.8) => (factor) => 18 (Random.between(-1, 1) * baseValue * (1 - reduction * factor)),19 20 // Training quality simulation21 trainingQuality: () => {22 const quality = Math.random();23 return {24 isGood: quality > 0.66,25 isPoor: quality < 0.33,26 isMedium: quality >= 0.33 && quality <= 0.66,27 score: quality28 };29 },30 31 // Learning phases (plateau, improvements, etc.)32 learningPhases: (maxSteps) => {33 const phases = Random.intBetween(1, 3);34 const marks = new Set();35 while (marks.size < phases - 1) {36 marks.add(Math.floor(Random.between(0.25, 0.75) * (maxSteps - 1)));37 }38 return [0, ...Array.from(marks).sort((a, b) => a - b), maxSteps - 1];39 },40 41 // Training steps count with realistic ML training ranges (with large dataset support)42 trainingSteps: () => {43 const rand = Math.random();44 45 // Distribution basée sur des patterns d'entraînement ML réels46 // Inclut maintenant des datasets plus larges pour tester le sampling47 if (rand < 0.05) {48 // 5% - Très court : Tests rapides, prototypage49 return Random.intBetween(5, 50);50 } else if (rand < 0.15) {51 // 10% - Court : Expérimentations rapides52 return Random.intBetween(50, 200);53 } else if (rand < 0.35) {54 // 20% - Moyen-court : Entraînements standards55 return Random.intBetween(200, 400);56 } else if (rand < 0.55) {57 // 20% - Moyen : La plupart des entraînements58 return Random.intBetween(400, 800);59 } else if (rand < 0.75) {60 // 20% - Long : Entraînements approfondis (déclenche le sampling)61 return Random.intBetween(800, 1500);62 } else if (rand < 0.90) {63 // 15% - Très long : Large-scale training64 return Random.intBetween(1500, 3000);65 } else if (rand < 0.98) {66 // 8% - Extrêmement long : Research-scale67 return Random.intBetween(3000, 5000);68 } else {69 // 2% - Massive : LLMs, très gros datasets (pour tester les limites)70 return Random.intBetween(5000, 10000);71 }72 },73 74 // Training steps with specific scenario75 trainingStepsForScenario: (scenario = 'mixed') => {76 switch (scenario) {77 case 'prototyping':78 return Random.intBetween(5, 100);79 case 'development':80 return Random.intBetween(100, 400);81 case 'production':82 return Random.intBetween(400, 800);83 case 'research':84 return Random.intBetween(800, 2000);85 case 'llm':86 return Random.intBetween(2000, 5000);87 case 'massive':88 // Nouveau scénario pour tester le sampling avec de très gros datasets89 return Random.intBetween(5000, 15000);90 default:91 return Random.trainingSteps();92 }93 }94};95 96/**97 * ML Training constants for realistic simulation98 */99export const TrainingConfig = {100 LOSS: {101 INITIAL_MIN: 2.0,102 INITIAL_MAX: 6.5,103 NOISE_FACTOR: 0.08,104 SPIKE_PROBABILITY: 0.02,105 SPIKE_AMPLITUDE: 0.15,106 DECAY_ACCELERATION: 1.6107 },108 109 ACCURACY: {110 INITIAL_MIN: 0.1,111 INITIAL_MAX: 0.45,112 GOOD_FINAL: { min: 0.92, max: 0.99 },113 POOR_FINAL: { min: 0.62, max: 0.76 },114 MEDIUM_FINAL: { min: 0.8, max: 0.9 },115 NOISE_AMPLITUDE: 0.04,116 PHASE_ACCELERATION: 1.4117 },118 119 OVERFITTING: {120 START_RATIO_GOOD: 0.85,121 START_RATIO_POOR: 0.7,122 RANDOMNESS: 0.15,123 ACCURACY_DEGRADATION: 0.03,124 LOSS_INCREASE: 0.12125 },126 127 VALIDATION_GAP: {128 ACCURACY_MIN: 0.02,129 ACCURACY_MAX: 0.06,130 LOSS_MIN: 0.05,131 LOSS_MAX: 0.15,132 FLUCTUATION: 0.06133 }134};135 136/**137 * Performance optimization helpers138 */139export const Performance = {140 // Smart sampling for large datasets to maintain performance141 smartSample: (totalSteps, maxPoints = 2000) => {142 if (totalSteps <= maxPoints) {143 return Array.from({length: totalSteps}, (_, i) => i + 1);144 }145 146 // For large datasets, sample intelligently:147 // - Always include start and end148 // - Keep more density at the beginning (where learning happens faster)149 // - Sample logarithmically for the middle section150 // - Always include some regular intervals151 152 const samples = new Set([1, totalSteps]); // Always include first and last153 const targetSamples = Math.min(maxPoints, totalSteps);154 155 // Add logarithmic sampling (more points early, fewer later)156 const logSamples = Math.floor(targetSamples * 0.6);157 for (let i = 0; i < logSamples; i++) {158 const progress = i / (logSamples - 1);159 const logProgress = Math.log(1 + progress * (Math.E - 1)) / Math.log(Math.E); // Normalized log160 const step = Math.floor(1 + logProgress * (totalSteps - 1));161 samples.add(step);162 }163 164 // Add regular intervals for the remaining points165 const remainingSamples = targetSamples - samples.size;166 const interval = Math.floor(totalSteps / remainingSamples);167 for (let i = interval; i < totalSteps; i += interval) {168 samples.add(i);169 if (samples.size >= targetSamples) break;170 }171 172 return Array.from(samples).sort((a, b) => a - b);173 }174};175 176// ============================================================================177// CURVE GENERATION HELPERS - Specific ML training behaviors178// ============================================================================179 180/**181 * Calculate target final loss based on training quality182 */183function calculateTargetLoss(initialLoss, quality) {184 if (quality.isGood) {185 return initialLoss * Random.between(0.12, 0.24);186 } else if (quality.isPoor) {187 return initialLoss * Random.between(0.35, 0.60);188 } else {189 return initialLoss * Random.between(0.22, 0.38);190 }191}192 193/**194 * Calculate target final accuracy based on training quality195 */196function calculateTargetAccuracy(quality) {197 if (quality.isGood) {198 return Random.between(TrainingConfig.ACCURACY.GOOD_FINAL.min, TrainingConfig.ACCURACY.GOOD_FINAL.max);199 } else if (quality.isPoor) {200 return Random.between(TrainingConfig.ACCURACY.POOR_FINAL.min, TrainingConfig.ACCURACY.POOR_FINAL.max);201 } else {202 return Random.between(TrainingConfig.ACCURACY.MEDIUM_FINAL.min, TrainingConfig.ACCURACY.MEDIUM_FINAL.max);203 }204}205 206/**207 * Generate loss curve with realistic ML training dynamics208 */209function generateLossCurve(steps, initialLoss, targetLoss, learningPhases, quality) {210 let learningRate = Random.learningRate();211 const loss = new Array(steps);212 213 for (let phaseIndex = 0; phaseIndex < learningPhases.length - 1; phaseIndex++) {214 const phaseStart = learningPhases[phaseIndex];215 const phaseEnd = learningPhases[phaseIndex + 1] || phaseStart + 1;216 217 for (let step = phaseStart; step <= phaseEnd; step++) {218 const phaseProgress = (step - phaseStart) / Math.max(1, phaseEnd - phaseStart);219 const phaseTarget = targetLoss * Math.pow(0.85, phaseIndex);220 221 // Exponential decay with phase blending222 let value = initialLoss * Math.exp(-learningRate * (step + 1));223 value = 0.6 * value + 0.4 * (initialLoss + (phaseTarget - initialLoss) * (phaseIndex + phaseProgress) / Math.max(1, learningPhases.length - 1));224 225 // Add realistic noise that decreases over time226 const noiseGen = Random.noiseAmplitude(TrainingConfig.LOSS.NOISE_FACTOR * initialLoss);227 value += noiseGen(step / (steps - 1));228 229 // Occasional loss spikes (common in training)230 if (Math.random() < TrainingConfig.LOSS.SPIKE_PROBABILITY) {231 value += TrainingConfig.LOSS.SPIKE_AMPLITUDE * initialLoss;232 }233 234 loss[step] = Math.max(0, value);235 }236 237 // Learning rate changes between phases238 learningRate *= TrainingConfig.LOSS.DECAY_ACCELERATION;239 }240 241 return loss;242}243 244/**245 * Generate accuracy curve with realistic ML training dynamics246 */247function generateAccuracyCurve(steps, targetAccuracy, learningPhases, quality) {248 const initialAccuracy = Random.between(TrainingConfig.ACCURACY.INITIAL_MIN, TrainingConfig.ACCURACY.INITIAL_MAX);249 let learningRate = Random.learningRate();250 const accuracy = new Array(steps);251 252 for (let step = 0; step < steps; step++) {253 // Asymptotic growth towards target accuracy254 let value = targetAccuracy - (targetAccuracy - initialAccuracy) * Math.exp(-learningRate * (step + 1));255 256 // Add realistic noise that decreases over time257 const noiseGen = Random.noiseAmplitude(TrainingConfig.ACCURACY.NOISE_AMPLITUDE);258 value += noiseGen(step / (steps - 1));259 260 accuracy[step] = Math.max(0, Math.min(1, value));261 262 // Accelerate learning at phase boundaries263 if (learningPhases.includes(step)) {264 learningRate *= TrainingConfig.ACCURACY.PHASE_ACCELERATION;265 }266 }267 268 return accuracy;269}270 271/**272 * Apply overfitting effects to training curves273 */274function applyOverfitting(trainCurve, steps, quality) {275 const validationCurve = new Array(steps);276 const gapConfig = TrainingConfig.VALIDATION_GAP;277 278 // Calculate when overfitting starts279 const overfittingStart = Math.floor(280 (quality.isGood ? TrainingConfig.OVERFITTING.START_RATIO_GOOD : TrainingConfig.OVERFITTING.START_RATIO_POOR) 281 * (steps - 1) + Random.between(-TrainingConfig.OVERFITTING.RANDOMNESS, TrainingConfig.OVERFITTING.RANDOMNESS) * steps282 );283 284 const clampedStart = Math.max(Math.floor(0.5 * (steps - 1)), Math.min(Math.floor(0.95 * (steps - 1)), overfittingStart));285 286 for (let step = 0; step < steps; step++) {287 const isAccuracy = trainCurve[step] <= 1; // Simple heuristic288 const baseGap = isAccuracy 289 ? Random.between(gapConfig.ACCURACY_MIN, gapConfig.ACCURACY_MAX)290 : Random.between(gapConfig.LOSS_MIN, gapConfig.LOSS_MAX);291 292 let validationValue = isAccuracy 293 ? trainCurve[step] - baseGap + Random.between(-gapConfig.FLUCTUATION/2, gapConfig.FLUCTUATION/2)294 : trainCurve[step] * (1 + baseGap) + Random.between(-0.1, 0.1);295 296 // Apply overfitting effects after the overfitting point297 if (step >= clampedStart && !quality.isPoor) {298 const overfittingProgress = (step - clampedStart) / Math.max(1, steps - 1 - clampedStart);299 300 if (isAccuracy) {301 validationValue -= TrainingConfig.OVERFITTING.ACCURACY_DEGRADATION * overfittingProgress;302 } else {303 validationValue += TrainingConfig.OVERFITTING.LOSS_INCREASE * overfittingProgress * trainCurve[step];304 }305 }306 307 validationCurve[step] = isAccuracy 308 ? Math.max(0, Math.min(1, validationValue))309 : Math.max(0, validationValue);310 }311 312 return validationCurve;313}314 315export function generateRunNames(count, stepsHint = null) {316 const adjectives = [317 'ancient', 'brave', 'calm', 'clever', 'crimson', 'daring', 'eager', 'fearless', 318 'gentle', 'glossy', 'golden', 'hidden', 'icy', 'jolly', 'lively', 'mighty', 319 'noble', 'proud', 'quick', 'silent', 'swift', 'tiny', 'vivid', 'wild'320 ];321 322 const nouns = [323 'river', 'mountain', 'harbor', 'forest', 'valley', 'ocean', 'meadow', 'desert', 324 'island', 'canyon', 'harbor', 'trail', 'summit', 'delta', 'lagoon', 'ridge', 325 'tundra', 'reef', 'plateau', 'prairie', 'grove', 'bay', 'dune', 'cliff'326 ];327 328 // Ajouter des préfixes selon la longueur de l'entraînement329 const getPrefix = (steps) => {330 if (!steps) return '';331 if (steps < 100) return 'rapid-';332 if (steps < 1000) return 'quick-';333 if (steps < 10000) return 'deep-';334 if (steps < 50000) return 'ultra-';335 return 'mega-';336 };337 338 const used = new Set();339 const names = [];340 const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];341 342 while (names.length < count) {343 const prefix = getPrefix(stepsHint);344 const adjective = pick(adjectives);345 const noun = pick(nouns);346 const suffix = Math.floor(1 + Math.random() * 99);347 const name = `${prefix}${adjective}-${noun}-${suffix}`;348 349 if (!used.has(name)) {350 used.add(name);351 names.push(name);352 }353 }354 return names;355}356 357/**358 * Generate training scenario description based on steps count359 */360export function getScenarioDescription(steps) {361 if (steps < 25) return '🚀 Rapid Prototyping';362 if (steps < 100) return '⚡ Quick Experiment';363 if (steps < 400) return '🔧 Development Phase';364 if (steps < 800) return '📊 Standard Training';365 if (steps < 1500) return '🎯 Production Training (Sampling Active)';366 if (steps < 3000) return '🏗️ Large-Scale Training (Smart Sampling)';367 if (steps < 5000) return '🌌 Research-Scale Training (Adaptive Sampling)';368 return '🚀 Massive Dataset (Advanced Sampling)';369}370 371/**372 * Generate a massive dataset for testing sampling performance373 * @param {number} steps - Number of steps (default: random large number)374 * @param {number} runs - Number of runs (default: 3)375 * @returns {Object} Large dataset for testing376 */377export function generateMassiveTestDataset(steps = null, runs = 3) {378 const actualSteps = steps || Random.trainingStepsForScenario('massive');379 const runNames = generateRunNames(runs, actualSteps);380 const dataByMetric = new Map();381 382 console.log(`🧪 Generating massive test dataset: ${actualSteps} steps × ${runs} runs = ${actualSteps * runs} total points`);383 384 const TARGET_METRICS = ['epoch', 'train_accuracy', 'train_loss', 'val_accuracy', 'val_loss'];385 386 // Initialize data structure387 TARGET_METRICS.forEach((metric) => {388 const map = {};389 runNames.forEach((r) => { map[r] = []; });390 dataByMetric.set(metric, map);391 });392 393 // Generate curves for each run394 runNames.forEach((run, runIndex) => {395 console.log(`🔄 Generating curves for run ${runIndex + 1}/${runs}: ${run}`);396 const curves = genCurves(actualSteps);397 398 for (let stepIndex = 0; stepIndex < actualSteps; stepIndex++) {399 const step = stepIndex + 1;400 dataByMetric.get('epoch')[run].push({ step, value: step });401 dataByMetric.get('train_accuracy')[run].push({ step, value: curves.accTrain[stepIndex] });402 dataByMetric.get('val_accuracy')[run].push({ step, value: curves.accVal[stepIndex] });403 dataByMetric.get('train_loss')[run].push({ step, value: curves.lossTrain[stepIndex] });404 dataByMetric.get('val_loss')[run].push({ step, value: curves.lossVal[stepIndex] });405 }406 });407 408 console.log(`✅ Massive dataset generated successfully`);409 410 return {411 dataByMetric,412 runNames,413 stepCount: actualSteps,414 totalPoints: actualSteps * runs * TARGET_METRICS.length,415 description: getScenarioDescription(actualSteps)416 };417}418 419/**420 * Generate realistic ML training curves with training/validation splits421 * @param {number} totalSteps - Number of training steps to simulate422 * @param {number} maxPoints - Maximum points to generate for performance (default: 2000)423 * @returns {Object} Object containing training and validation curves for accuracy and loss424 */425export function genCurves(totalSteps, maxPoints = 2000) {426 // 1. Smart sampling for performance - get the actual steps we'll compute427 const sampledSteps = Performance.smartSample(totalSteps, maxPoints);428 const actualPointsCount = sampledSteps.length;429 430 // 2. Determine overall training quality and characteristics431 const quality = Random.trainingQuality();432 433 // 3. Generate target metrics based on quality434 const initialLoss = Random.between(TrainingConfig.LOSS.INITIAL_MIN, TrainingConfig.LOSS.INITIAL_MAX);435 const targetLoss = calculateTargetLoss(initialLoss, quality);436 const targetAccuracy = calculateTargetAccuracy(quality);437 438 // 4. Generate learning phases (plateaus, rapid improvements, etc.)439 const learningPhases = Random.learningPhases(totalSteps);440 441 // 5. Generate realistic training curves (using sampled steps for computation)442 const trainLoss = generateLossCurveOptimized(sampledSteps, totalSteps, initialLoss, targetLoss, learningPhases, quality);443 const trainAccuracy = generateAccuracyCurveOptimized(sampledSteps, totalSteps, targetAccuracy, learningPhases, quality);444 445 // 6. Apply overfitting to create validation curves446 const validationLoss = applyOverfittingOptimized(trainLoss, sampledSteps, totalSteps, quality);447 const validationAccuracy = applyOverfittingOptimized(trainAccuracy, sampledSteps, totalSteps, quality);448 449 // Convert back to simple arrays for backward compatibility450 // Create arrays indexed by step position for the original step sequence451 const stepToIndex = new Map();452 sampledSteps.forEach((step, index) => {453 stepToIndex.set(step, index);454 });455 456 // Create full arrays with interpolation for missing steps457 const createCompatibleArray = (sampledData) => {458 const result = new Array(totalSteps);459 let lastValue = sampledData[0]?.value || 0;460 461 // Ensure initial value is valid462 if (!Number.isFinite(lastValue)) {463 lastValue = 0;464 }465 466 for (let i = 0; i < totalSteps; i++) {467 const step = i + 1;468 const sampledIndex = stepToIndex.get(step);469 470 if (sampledIndex !== undefined) {471 // We have data for this step472 const newValue = sampledData[sampledIndex].value;473 lastValue = Number.isFinite(newValue) ? newValue : lastValue;474 result[i] = lastValue;475 } else {476 // Use last known value477 result[i] = lastValue;478 }479 }480 481 return result;482 };483 484 const result = {485 // Training curves (what the model sees during training) - compatible format486 accTrain: createCompatibleArray(trainAccuracy),487 lossTrain: createCompatibleArray(trainLoss),488 489 // Validation curves (held-out data, shows generalization) - compatible format490 accVal: createCompatibleArray(validationAccuracy),491 lossVal: createCompatibleArray(validationLoss),492 493 // Metadata for debugging494 _meta: {495 totalSteps,496 sampledPoints: actualPointsCount,497 samplingRatio: actualPointsCount / totalSteps,498 quality: quality.score499 }500 };501 502 // Debug: Check for NaN values503 const hasNaN = (arr, name) => {504 const nanCount = arr.filter(v => !Number.isFinite(v)).length;505 if (nanCount > 0) {506 console.warn(`⚠️ Found ${nanCount} NaN values in ${name}`);507 }508 };509 510 if (totalSteps > 1000) { // Only debug large datasets511 hasNaN(result.accTrain, 'accTrain');512 hasNaN(result.lossTrain, 'lossTrain');513 hasNaN(result.accVal, 'accVal');514 hasNaN(result.lossVal, 'lossVal');515 }516 517 return result;518}519 520// ============================================================================521// OPTIMIZED CURVE GENERATION - For performance with large datasets522// ============================================================================523 524/**525 * Optimized loss curve generation using sampled steps526 */527function generateLossCurveOptimized(sampledSteps, totalSteps, initialLoss, targetLoss, learningPhases, quality) {528 let learningRate = Random.learningRate();529 const loss = [];530 531 // Create a mapping function from sampled steps to values532 sampledSteps.forEach((step, index) => {533 // Find which learning phase this step belongs to534 let phaseIndex = 0;535 for (let i = 0; i < learningPhases.length - 1; i++) {536 if (step >= learningPhases[i] && step < learningPhases[i + 1]) {537 phaseIndex = i;538 break;539 }540 }541 542 const phaseStart = learningPhases[phaseIndex];543 const phaseEnd = learningPhases[phaseIndex + 1] || totalSteps;544 const phaseProgress = (step - phaseStart) / Math.max(1, phaseEnd - phaseStart);545 const phaseTarget = targetLoss * Math.pow(0.85, phaseIndex);546 547 // Exponential decay with phase blending548 let value = initialLoss * Math.exp(-learningRate * (step / totalSteps) * 100);549 value = 0.6 * value + 0.4 * (initialLoss + (phaseTarget - initialLoss) * (phaseIndex + phaseProgress) / Math.max(1, learningPhases.length - 1));550 551 // Add realistic noise that decreases over time552 const noiseGen = Random.noiseAmplitude(TrainingConfig.LOSS.NOISE_FACTOR * initialLoss);553 value += noiseGen(step / totalSteps);554 555 // Occasional loss spikes (common in training)556 if (Math.random() < TrainingConfig.LOSS.SPIKE_PROBABILITY) {557 value += TrainingConfig.LOSS.SPIKE_AMPLITUDE * initialLoss;558 }559 560 // Ensure no NaN values561 const finalValue = Math.max(0, Number.isFinite(value) ? value : initialLoss * 0.1);562 loss.push({ step, value: finalValue });563 });564 565 return loss;566}567 568/**569 * Optimized accuracy curve generation using sampled steps570 */571function generateAccuracyCurveOptimized(sampledSteps, totalSteps, targetAccuracy, learningPhases, quality) {572 const initialAccuracy = Random.between(TrainingConfig.ACCURACY.INITIAL_MIN, TrainingConfig.ACCURACY.INITIAL_MAX);573 let learningRate = Random.learningRate();574 const accuracy = [];575 576 sampledSteps.forEach((step, index) => {577 // Asymptotic growth towards target accuracy578 let value = targetAccuracy - (targetAccuracy - initialAccuracy) * Math.exp(-learningRate * (step / totalSteps) * 100);579 580 // Add realistic noise that decreases over time581 const noiseGen = Random.noiseAmplitude(TrainingConfig.ACCURACY.NOISE_AMPLITUDE);582 value += noiseGen(step / totalSteps);583 584 // Ensure no NaN values585 const finalValue = Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0.1;586 accuracy.push({ step, value: finalValue });587 588 // Accelerate learning at phase boundaries589 if (learningPhases.includes(step)) {590 learningRate *= TrainingConfig.ACCURACY.PHASE_ACCELERATION;591 }592 });593 594 return accuracy;595}596 597/**598 * Optimized overfitting application using sampled steps599 */600function applyOverfittingOptimized(trainCurve, sampledSteps, totalSteps, quality) {601 const validationCurve = [];602 const gapConfig = TrainingConfig.VALIDATION_GAP;603 604 // Calculate when overfitting starts605 const overfittingStart = Math.floor(606 (quality.isGood ? TrainingConfig.OVERFITTING.START_RATIO_GOOD : TrainingConfig.OVERFITTING.START_RATIO_POOR) 607 * totalSteps + Random.between(-TrainingConfig.OVERFITTING.RANDOMNESS, TrainingConfig.OVERFITTING.RANDOMNESS) * totalSteps608 );609 610 const clampedStart = Math.max(Math.floor(0.5 * totalSteps), Math.min(Math.floor(0.95 * totalSteps), overfittingStart));611 612 trainCurve.forEach((trainPoint, index) => {613 const step = trainPoint.step;614 const isAccuracy = trainPoint.value <= 1; // Simple heuristic615 const baseGap = isAccuracy 616 ? Random.between(gapConfig.ACCURACY_MIN, gapConfig.ACCURACY_MAX)617 : Random.between(gapConfig.LOSS_MIN, gapConfig.LOSS_MAX);618 619 let validationValue = isAccuracy 620 ? trainPoint.value - baseGap + Random.between(-gapConfig.FLUCTUATION/2, gapConfig.FLUCTUATION/2)621 : trainPoint.value * (1 + baseGap) + Random.between(-0.1, 0.1);622 623 // Apply overfitting effects after the overfitting point624 if (step >= clampedStart && !quality.isPoor) {625 const overfittingProgress = (step - clampedStart) / Math.max(1, totalSteps - clampedStart);626 627 if (isAccuracy) {628 validationValue -= TrainingConfig.OVERFITTING.ACCURACY_DEGRADATION * overfittingProgress;629 } else {630 validationValue += TrainingConfig.OVERFITTING.LOSS_INCREASE * overfittingProgress * trainPoint.value;631 }632 }633 634 // Ensure no NaN values in validation curves635 const finalValue = Number.isFinite(validationValue) 636 ? (isAccuracy ? Math.max(0, Math.min(1, validationValue)) : Math.max(0, validationValue))637 : (isAccuracy ? 0.1 : trainPoint.value);638 639 validationCurve.push({640 step,641 value: finalValue642 });643 });644 645 return validationCurve;646}647 