[02-assemble-training-loop]'s train_one_epoch returns a single average loss for that epoch, and you'd normally call it once per epoch, in a loop, for many epochs. Without recording every value it returns somewhere, you can only ever see the MOST RECENT epoch's loss, and lose the entire training history the moment training moves on to the next epoch, exactly the history you'd need to plot a loss curve, notice whether the model has started overfitting ([04-regularization/01-early-stopping]'s exact concern), or decide which checkpoint was actually the best one to keep.
Raw per-step or per-epoch loss values are also often genuinely noisy, jumping up and down step to step even while the overall trend is clearly decreasing, which is why loss curves in practice are almost always shown SMOOTHED (a moving average), rather than as the raw, jittery values.
Implement MetricTracker, with record(name, value) (append a value to that metric's history), get_history(name) (return the raw list), moving_average(name, window) (return a same-length list of smoothed values, using a "trailing window" of up to window most recent values at each point), and best(name, mode) (return the minimum or maximum ever recorded for that metric, mode="min" or mode="max").
moving_average must return a list the SAME LENGTH as the raw history, one smoothed value per raw value, not a shorter list that skips the first few entries.i where fewer than window values exist so far (i.e. i < window - 1), average over whatever IS available (values[0:i+1]), don't wait until a full window has accumulated.best(name, mode="min") returns the minimum ever recorded; mode="max" returns the maximum. Return None (not an error, not 0) if name was never recorded.self.history.setdefault(name, []).append(value): setdefault returns the existing list for name if one exists, or creates and returns a fresh empty list if this is the first time, either way you then .append(value) to it. get_history is just self.history.get(name, []).
For each index i in range(len(values)), the window START is max(0, i - window + 1) (clamped at 0 so early entries use a partial window), and the window is values[start : i+1]. Average that slice and append it to the result list.
min(values) if mode == "min" else max(values), but check if not values: return None FIRST, before calling min/max on a possibly-empty list (which would raise an error instead).
Click "Run Tests" to test your implementation