train_epoch applies scheduler.get_lr(self.epoch) at the start of the
epoch when self.epoch == 0, then increments epoch to 1 at the end.
The test was asserting against get_lr(1) which is off by one.
Covers six areas that were implemented but had zero test coverage:
- CosineSchedule: start/mid/end values, monotonic decrease, past-end edge case
- clip_grad_norm: clipping, no-clip, multi-param global norm, direction preservation, zero-grad safety
- save/load_checkpoint: file creation, all keys present, epoch/step/history/weights restored, resumes training, creates parent dirs
- Trainer.evaluate: finite loss, float return types, eval mode set, weights unchanged, history recorded, classification accuracy (correct and wrong)
- Scheduler integration in train_epoch: LR recorded in history, optimizer.lr updated, decreases over epochs
- Gradient clipping integration in train_epoch: training completes, weights update, tight clip limits update size
- Train/eval mode switching: model.training flag correct after train_epoch, evaluate, and train→eval→train