Modern training runs rarely keep one learning rate from start to finish. They warm up, starting below the target rate and climbing to it over the first few updates so that the wild gradients of freshly initialised weights cannot wreck the run. Then they decay along a smooth cosine curve down to a small floor, so the end of training takes the fine, settling steps it needs.
Number the updates , where is total_steps, and let be warmup_steps.
Warmup, for . The rate climbs in equal increments and reaches the peak exactly on update :
Cosine decay, for . First measure how far through the decay phase this update is:
then ease from the peak down to the floor:
The final update, , has progress and lands exactly on the floor. The cosine takes its argument in radians.
Task: write warmup_cosine(peak_lr, min_lr, warmup_steps, total_steps), returning the learning rate of every update in order: a list of total_steps floats, each rounded to 4 decimal places.
You can rely on total_steps , warmup_steps total_steps and min_lr peak_lr. warmup_steps can be , meaning no warmup at all, and it can equal total_steps, meaning the run ends before any decay begins.
Worked through, warmup_cosine(0.4, 0.0, 2, 6):
| update | phase | progress | learning rate |
|---|---|---|---|
| 1 | warmup | ||
| 2 | warmup | ||
| 3 | decay | ||
| 4 | decay | ||
| 5 | decay | ||
| 6 | decay |
The answer is [0.2, 0.4, 0.3414, 0.2, 0.0586, 0.0].
A schedule that is slightly wrong still trains, which is exactly why schedule bugs survive for so long. Get the conventions right: where the ramp starts, what the decay's progress is measured against, and where the floor enters the formula.