Learning-rate schedules: warm-up, decay
Read these first if they are new to you
- Gradient descent Intermediate
- Reading Python Basic
The idea in one paragraph
A learning-rate schedule changes the learning rate as training goes on, by a plan fixed before training starts. The most common plan has two parts. During warm-up, the rate rises in a straight line from near zero to its peak over the first few thousand steps. During decay, it falls back toward zero by the last step, often along a cosine curve. Small early steps keep the random starting weights from being thrown off; small late steps let the weights settle.
No single learning rate suits the whole run
In gradient descent, the learning rate η scales every step. A large rate makes fast progress but overshoots and bounces around a low point. A small rate is precise but slow. A learning-rate schedule uses different rates at different times: a function that gives η for each step number t.
- Warm-up starts small. At the start the weights are random, so gradients are large and point in changing directions. Optimizers such as AdamW also have only a few gradients in their running averages. Large steps now can throw the weights into a region where training fails.
- Decay ends small. Late in training, each step's gradient comes from a small random batch of examples, so it is noisy. With a large rate the weights keep jumping around near a low point. Shrinking the rate lets them settle into it.
Warm-up is a straight rise; cosine decay is half a wave down
Write W for the number of warm-up steps, T for the total number of steps, and ηpeak for the highest rate. During warm-up (t < W):
ηt = ηpeak × t / W
After warm-up, let p = (t − W) / (T − W) be the fraction of the decay that is done, from 0 to 1. Cosine decay to zero is:
ηt = ηpeak × ½(1 + cos(πp))
The factor ½(1 + cos(πp)) falls smoothly from 1 at p = 0 to 0 at p = 1. It falls slowly at first, fastest in the middle, and slowly again at the end. (A cosine is a sine wave shifted by a quarter turn.) Some schedules stop at a small floor ηmin instead of 0.
Take ηpeak = 0.001, W = 1,000 and T = 10,000.
- Step 500 is in warm-up: 0.001 × 500 / 1,000 = 0.0005.
- Step 1,000 ends warm-up: 0.001 × 1,000 / 1,000 = 0.001, the peak.
- Step 5,500: p = 4,500 / 9,000 = 0.5. Factor ½(1 + cos(π/2)) = ½(1 + 0) = 0.5. Rate 0.0005.
- Step 10,000: p = 1. Factor ½(1 + cos π) = ½(1 − 1) = 0. Rate 0.
Your turn. What is the rate at step 7,750? Use cos(3π/4) ≈ −0.707.
Answer
p = 6,750 / 9,000 = 0.75. Factor ½(1 − 0.707) ≈ 0.146. Rate ≈ 0.000146.
The warm-up length and the decay shape are settings to choose
Try it. The figure draws the schedule over 10,000 steps with a peak of 0.001. Predict first: with no warm-up at all, what rate does the very first step use? Then find the warm-up length at which step 500 uses half the peak rate.
Answer
With no warm-up, the first step uses the full peak, 0.001. Step 500 uses half the peak when warm-up lasts 1,000 steps.
Other schedules exist. Step decay multiplies the rate by a fixed factor, such as 0.1, at chosen steps. The original Transformer paper used warm-up followed by a fall in proportion to 1 / √t. All of them share the same two aims: careful at the start, settled at the end.
That warm-up and decay help is well established in practice. Why warm-up helps is still studied, and the reasons given above are the common explanations, not proofs. The warm-up length, the peak and the shape are tuned for each problem. In PyTorch, AdamW's weight decay is multiplied by the current rate, so the schedule changes it too.
In PyTorch, a scheduler sets the rate before each step
LambdaLR multiplies the optimizer's rate by a function of the step count. Call
sched.step() once after each opt.step(). If you call it once per pass over the data
instead, the function's t counts passes, not steps.
schedule_torch.py
import math
import torch
peak, warmup, total = 0.001, 1000, 10_000
def factor(step): # multiplies the peak rate
if step < warmup:
return step / warmup # straight rise from 0
p = (step - warmup) / (total - warmup)
return 0.5 * (1 + math.cos(math.pi * p)) # cosine fall to 0
w = torch.zeros(1, requires_grad=True)
opt = torch.optim.AdamW([w], lr=peak) # the optimizer that updates w, with rate peak
sched = torch.optim.lr_scheduler.LambdaLR(opt, factor)
for step in range(total):
if step in (0, 500, 1000, 5500, 7750): # print only at these steps
print(step, f"{opt.param_groups[0]['lr']:.6f}") # the optimizer's current rate
w.grad = torch.zeros(1) # stands in for loss.backward()
opt.step()
sched.step() # after each optimizer step
0 0.000000
500 0.000500
1000 0.001000
5500 0.000500
7750 0.000146
With this function, step 0 has a rate of exactly 0, so the first step changes nothing. Some code uses (t + 1) / W instead to avoid that wasted step.
Check yourself
-
A run has ηpeak = 0.002 and 1,000 warm-up steps. What is the rate at step 250?
Answer
0.002 × 250 / 1,000 = 0.0005.
-
With cosine decay, what fraction of the peak is left a quarter of the way through the decay (p = 0.25)? Use cos(π/4) ≈ 0.707.
Answer
½(1 + 0.707) ≈ 0.854, so about 85% of the peak. Cosine decay stays high for a while, then falls faster.
-
Someone writes the schedule above but calls
sched.step()once per pass over a data set that takes 200 steps per pass. What happens to warm-up?Answer
Warm-up now lasts 1,000 passes, which is 200,000 steps. The run will likely end long before the rate reaches its peak.
-
Why end training with a small learning rate instead of the peak?
Answer
Each step's gradient is noisy, so at a high rate the weights keep jumping around a low point of the loss. A small rate lets them settle into it, which usually lowers the final loss.
Go further
Optional background, for readers who want more than this page.
-
Plots a cosine schedule with a straight-line warm-up and trains a network with it. Compare its first epochs with the run without warm-up in the section just before, which covers cosine decay.
-
The scheduler used above; the sidebar lists PyTorch's ready-made schedules.
-
The paper that made gradual warm-up standard; its warm-up section explains the problem it solves.
Where this shows up
Units that build on this one
- The training loop Intermediate