CoolFace
Apppublic

lerobot/robot-learning-tutorial

sourceHugging Faceupdated 1y agoView on Hugging Face
508likes
data-generator.js647 linesDownload Raw Back to core
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