Learning Rate Schedulers
Use learning rate schedulers — Applied AI engineering.
Focus: use learning rate schedulers
You’ve just found the perfect learning rate—and then the model stalls after a few epochs. You tweak it, it explodes; you lower it, training crawls. This trial-and-error is the single most common frustration in applied AI engineering. The fix isn’t a better starting value—it’s learning rate schedulers: dynamic schedules that adjust the learning rate as training progresses. In this lesson, you’ll learn how to use them to stabilize training, escape local minima, and converge faster—without babysitting your hyperparameters.
The problem this lesson solves
A fixed learning rate is a compromise you’re forced to make before you even start training. Choose too high, and the loss curve will bounce around or diverge. Choose too low, and you’ll waste hours while the model creeps toward convergence. Even if you get lucky and pick a workable value, that single number is wrong for most of training: early steps need a large enough rate to make meaningful progress, while later steps need a small enough rate to fine-tune the weights without overshooting.
This is exactly why use learning rate schedulers matters in applied AI engineering. A scheduler changes the learning rate over time, usually by decreasing it after milestones or per step. The result is faster, more stable convergence and better final performance. Without a scheduler, you’re leaving accuracy on the table—and spending too long at the keyboard.
Core concept / mental model
Think of learning rate as the step size of a hiker descending a mountain. A fixed step size means you’ll either stumble down the steep upper slopes (too large) or crawl across the flat valley (too small). A good hiker takes large steps at the top, then shortens them as the terrain levels out. That’s a learning rate schedule: an algorithm that shrinks the step size as you approach the optimum.
Formally, a learning rate is a scalar that scales the gradient update in optimizers like SGD or Adam. A scheduler takes the current epoch or step and returns a new learning rate. It can be deterministic (e.g., Step, Exponential, Cosine) or adapt to training metrics (e.g., ReduceLROnPlateau). In PyTorch, you typically attach a scheduler to an optimizer after each epoch or iteration.
Here’s a mental model in words:
- Warmup: Start with a low learning rate to avoid early instability, then ramp up.
- Decay: Decrease the rate gradually so the model can settle into a good minimum.
- Plateau detection: If the loss stops improving, lower the rate to nudge the model further.
How it works step by step
Using a scheduler in PyTorch follows a consistent pattern. Here’s the high-level flow:
- Create an optimizer with an initial learning rate.
- Create a scheduler tied to that optimizer.
- In each training epoch: run the forward pass and backward pass, then call
optimizer.step(). - After each epoch (or after each batch, depending on scheduler), call
scheduler.step().
For ReduceLROnPlateau, you must pass the current validation loss to step(). For other schedulers like StepLR, step() needs no arguments.
The critical detail: always call optimizer.step() before scheduler.step() if you’re using pre-built schedulers. This order ensures the learning rate changes after the current update, not before.
If you’re training on multiple epochs, you can log the learning rate each epoch to see the schedule in action—this makes debugging much easier.
Hands-on walkthrough
Let’s implement a simple neural network on a synthetic dataset and compare training with and without a scheduler. We’ll use StepLR, which multiplies the learning rate by a factor every step_size epochs.
import torch
import torch.nn as nn
import torch.optim as optim
# Simple model
def create_model():
return nn.Sequential(
nn.Linear(10, 64),
nn.ReLU(),
nn.Linear(64, 1)
)
# Synthetic data
X = torch.randn(1000, 10)
y = torch.sin(X.sum(dim=1)).unsqueeze(1)
train_loader = torch.utils.data.DataLoader(
list(zip(X, y)), batch_size=64, shuffle=True
)
# Training function with optional scheduler
def train(with_scheduler: bool):
model = create_model()
optimizer = optim.SGD(model.parameters(), lr=0.1)
criterion = nn.MSELoss()
schedulers = {
True: optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5),
False: None
}
scheduler = schedulers[with_scheduler]
epochs = 30
for epoch in range(epochs):
model.train()
for xb, yb in train_loader:
optimizer.zero_grad()
loss = criterion(model(xb), yb)
loss.backward()
optimizer.step()
if scheduler:
scheduler.step()
current_lr = optimizer.param_groups[0]['lr']
print(f"Epoch {epoch+1:2d} | LR: {current_lr:.4f} | Loss: {loss.item():.4f}")
print("=== Without scheduler ===")
train(False)
print("\n=== With StepLR ===")
train(True)
Expected output:
=== Without scheduler ===
Epoch 1 | LR: 0.1000 | Loss: 0.4783
...
Epoch 30 | LR: 0.1000 | Loss: 0.0451
=== With StepLR ===
Epoch 1 | LR: 0.1000 | Loss: 0.4756
...
Epoch 10 | LR: 0.0500 | Loss: 0.0312
Epoch 20 | LR: 0.0250 | Loss: 0.0187
Epoch 30 | LR: 0.0125 | Loss: 0.0102
You’ll notice the scheduler version achieves lower loss—and the learning rate visibly decays.
Now, let’s try CosineAnnealingLR for a smoother decay:
scheduler = optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=30, eta_min=0.001
)
Swap this into the train function and watch the loss decrease more steadily. CosineAnnealing is a favorite in computer vision benchmarks because it reduces the learning rate smoothly from the initial value to eta_min.
Finally, a ReduceLROnPlateau example that reacts to validation loss:
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=5
)
for epoch in range(epochs):
train_loss = ...
val_loss = ...
scheduler.step(val_loss)
This scheduler monitors your metric and drops the learning rate if the metric stops improving, which is ideal when you don’t know in advance how many epochs you need.
Compare options / when to choose what
| Scheduler | When to use | Pros | Cons |
|---|---|---|---|
| StepLR | Quick experiments, baseline | Simple, predictable | Requires tuning step_size |
| ExponentialLR | Long training, steady decay | Smooth decay | May not adapt to local minima |
| CosineAnnealingLR | Modern CNNs/Transformers | Smooth, often best performance | Requires knowing total epochs |
| ReduceLROnPlateau | When you can’t fix epoch count | Adaptive to validation metric | Needs patience tuning |
| OneCycleLR | Short training, cycle schedules | Fast convergence, good for large batches | Complex to tune |
When to choose what:
- Start with StepLR for the simplest baseline.
- Move to CosineAnnealing if you want a smarter baseline that consistently improves results.
- Use ReduceLROnPlateau when your training length varies or you prefer a hands-off approach.
- Try OneCycleLR for one-shot training with a fixed budget.
Troubleshooting & edge cases
- Error:
KeyError: 'lr'— You’re likely accessingoptimizer.param_groups[0]['lr']before defining the optimizer, or using a scheduler without an optimizer. Fix: create the optimizer first, then the scheduler. - Learning rate jumps after epoch 0 — Some schedulers, like
StepLR, change the LR on the firststep()call. To avoid this, callscheduler.step()at the end of each epoch, and start from epoch 1. - Loss diverges with a scheduler — This often happens when the initial LR is too high. Reduce it by 10x or use warmup. Also, ensure you’re not calling
scheduler.step()multiple times per epoch accidentally. - Validation loss plateau but LR doesn’t drop — For
ReduceLROnPlateau, you must passval_losstostep(). If you callstep()without arguments, it defaults to'min'mode but sees no improvement, so it never changes. - Scheduler imported but not applied — Double-check that you call
scheduler.step()in the epoch loop. It’s easy to forget, especially in notebooks.
Pro tip: Always log your learning rate each epoch. Use
writer.add_scalar('lr', lr, epoch)if you use TensorBoard. It makes debug sessions 10x faster.
What you learned & what's next
You now can use learning rate schedulers to improve model training in applied AI projects. You understand why fixed LRs are suboptimal, how schedulers decay the rate over time, and you’ve compared StepLR, CosineAnnealingLR, and ReduceLROnPlateau with hands-on examples. You also know common pitfalls—like forgetting to call step() or passing the wrong metric—and how to fix them.
This skill directly benefits every future training script you write, from fine-tuning LLMs to training custom vision models. With schedulers, your models converge faster and reach higher accuracy with less manual tuning.
Next step: In the following lesson, you’ll learn how to combine learning rate schedulers with early stopping to automatically halt training when validation loss plateaus—saving hours of compute. With both techniques, you’ll build efficient, self-regulating training pipelines that run without constant human attention.
Now, try applying a scheduler to your own model and compare the loss curves with and without it. See the difference for yourself—then you’ll never train without a scheduler again.
Practice recap
Now it’s your turn: take any training script you have and add a learning rate scheduler. Start with StepLR and log the LR each epoch. Compare the validation loss with the non-scheduler baseline. Then try CosineAnnealingLR and note the difference. This 15-minute exercise will cement the pattern and you’ll see immediate gains.
Common mistakes
- Forgetting to call scheduler.step() after each epoch — the learning rate stays constant and you never get the benefit.
- Calling scheduler.step() before optimizer.step() — this changes the LR for the current update, often causing instability.
- Passing training loss to ReduceLROnPlateau instead of validation loss — the scheduler becomes less effective because training loss always improves.
- Using a huge initial LR with a scheduler — even with decay, early epochs can diverge. Start with a reasonable baseline, then decay.
- Not logging the learning rate during training — makes it nearly impossible to debug whether the scheduler actually worked.
Variations
- Use CosineWarmupLR (e.g., Hugging Face's get_cosine_schedule_with_warmup) for transformer fine-tuning — warms up then decays smoothly.
- Adopt OneCycleLR for a single cycle of warmup and decay, ideal for quick, powerful baselines.
- Try a manual scheduler: linearly decrease the learning rate in a loop for full control, common in research codebases.
Real-world use cases
- Fine-tuning an LLM on a custom dataset with a warmup schedule to avoid catastrophic loss spikes in early steps.
- Training a CNN image classifier with CosineAnnealingLR to squeeze out final accuracy gains on CIFAR-10.
- Using ReduceLROnPlateau in a production anomaly detection model to adapt to non-stationary data and auto-lower LR when validation falters.
Key takeaways
- Fixed learning rates are a compromise; schedulers adapt the step size over time to improve convergence.
- A scheduler attaches to an optimizer and changes the LR after each epoch or step.
- StepLR, CosineAnnealingLR, and ReduceLROnPlateau serve different needs: simple decay, smooth decay, and adaptive decay.
- Call optimizer.step() before scheduler.step(), and pass validation loss to ReduceLROnPlateau.
- Schedulers help escape local minima and fine-tune later epochs, leading to better performance.
- Log your learning rate each epoch to debug and verify your schedule is working.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.