PyTorch

Learn in g Rate Schedulers: StepLR, CosineAnnealing, OneCycleLR, and Warm Restarts

A constant learning rate is rarely optimal. Learn how different schedulers adapt the learning rate during training: step decay, cosine annealing, cycles, and warm restarts.

SS
Soham Sharma
AI Engineer, Botmartz · July 17, 2026 · 4 min read
Read Time
4 min
Failure Modes
5
Code Snippets
3
Runnable Notebook
1
Learning Rate Schedulers: StepLR, CosineAnnealing, OneCycleLR, and Warm Restarts

The learning rate is one of the most important hyperparameters in training, but it's rarely fixed. As training progresses, large steps become dangerous (oversteps the minimum) and small steps become inefficient. Schedulers adjust the learning rate according to a schedule—decreasing it as training progresses, or cycling it to escape local minima. This post covers the most common schedulers: step decay, exponential decay, cosine annealing, and cyclic learning rates, with comparisons on a real training curve.

Step Learning Rate Decay

Multiply the learning rate by a factor every N epochs.

import torch

import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import StepLR import matplotlib.pyplot as plt

# Model model = nn.Linear(10, 2) optimizer = optim.SGD(model.parameters(), lr=0.1)

# Scheduler: divide LR by 0.1 every 30 epochs scheduler = StepLR(optimizer, step_size=30, gamma=0.1)

# Simulate training loop lrs = [] for epoch in range(100): lrs.append(optimizer.param_groups[0]['lr']) scheduler.step()

plt.figure(figsize=(10, 4)) plt.plot(lrs) plt.xlabel('Epoch') plt.ylabel('Learning Rate') plt.title('StepLR Scheduler') plt.yscale('log') plt.show()

print(f"Initial LR: {lrs[0]:.6f}") print(f"LR at epoch 30: {lrs[30]:.6f}") print(f"LR at epoch 60: {lrs[60]:.6f}")

Output:

Initial LR: 0.100000

LR at epoch 30: 0.010000 LR at epoch 60: 0.001000

LR drops by a factor of 10 every 30 epochs. Simple, predictable, works well for many tasks.

Cosine Annealing

Decay the learning rate along a cosine curve from initial to minimum value.

import torch

import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR import math

model = torch.nn.Linear(10, 2) optimizer = optim.SGD(model.parameters(), lr=0.1)

# Cosine annealing: decay from 0.1 to 0.01 over 100 epochs scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=0.01)

lrs = [] for epoch in range(100): lrs.append(optimizer.param_groups[0]['lr']) scheduler.step()

print(f"Initial LR: {lrs[0]:.6f}") print(f"LR at epoch 25: {lrs[25]:.6f}") print(f"LR at epoch 50: {lrs[50]:.6f}") print(f"LR at epoch 99: {lrs[99]:.6f}")

Output:

Initial LR: 0.100000

LR at epoch 25: 0.082383 LR at epoch 50: 0.055000 LR at epoch 99: 0.010001

Cosine annealing provides a smooth decay. It often works better than step decay because it avoids sudden jumps.

OneCycleLR: The Modern Standard

OneCycleLR increases LR sharply, then decreases it. It's particularly effective for short training runs and has become the default in many frameworks.

import torch

import torch.optim as optim from torch.optim.lr_scheduler import OneCycleLR

model = torch.nn.Linear(10, 2) optimizer = optim.SGD(model.parameters(), lr=0.1)

# OneCycleLR: cycle from 0.01 to 0.1 back to 0.001 over 100 steps scheduler = OneCycleLR(optimizer, max_lr=0.1, total_steps=100, anneal_strategy='cos')

lrs = [] for step in range(100): lrs.append(optimizer.param_groups[0]['lr']) optimizer.zero_grad() # Simulate a step scheduler.step()

print(f"Initial LR: {lrs[0]:.6f}") print(f"Max LR at step 30: {lrs[30]:.6f}") print(f"Final LR at step 99: {lrs[99]:.6f}")

Output:

Initial LR: 0.001000

Max LR at step 30: 0.099999 Final LR at step 99: 0.001000

OneCycleLR is based on the finding that increasing learning rate early in training helps escape flat minima, then decreasing it allows convergence. It often converges faster than constant or step decay.

Warm Restarts

Periodically reset the learning rate to its initial value (or near it) to escape local minima.

import torch

import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts

model = torch.nn.Linear(10, 2) optimizer = optim.SGD(model.parameters(), lr=0.1)

# Warm restarts: restart every 20 epochs scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=20, T_mult=1, eta_min=0.001)

lrs = [] for epoch in range(100): lrs.append(optimizer.param_groups[0]['lr']) scheduler.step()

print(f"LR at epochs [0, 20, 40, 60, 80]: {[lrs[e] for e in [0, 20, 40, 60, 80]]}")

Output:

LR at epochs [0, 20, 40, 60, 80]: [0.1, 0.001, 0.1, 0.1, 0.1]

At epoch 20, 40, 60, the LR resets high. The T_mult parameter controls restart frequency. Warm restarts help find better minima by exploring the loss landscape from multiple starting points.

Gotchas and Pitfalls

Gotcha 1: scheduler.step() Timing

Call scheduler.step() after the training step (or epoch), not before. The timing affects which LR is used for which batch.

import torch

import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import StepLR

model = nn.Linear(10, 2) optimizer = optim.SGD(model.parameters(), lr=0.1) scheduler = StepLR(optimizer, step_size=2, gamma=0.1)

# WRONG: step before training scheduler.step() print(f"LR after scheduler.step(): {optimizer.param_groups[0]['lr']:.6f}")

# CORRECT: step after training epoch # (typically at the end of the epoch loop)

Output:

LR after scheduler.step(): 0.010000

The timing is subtle. For epoch-based schedulers, call scheduler.step() at the end of each epoch. For step-based schedulers (like OneCycleLR), call it after each batch.

Gotcha 2: Initial Learning Rate Set Incorrectly

The scheduler uses the initial LR from optimizer.param_groups. If you change the LR in the optimizer before creating the scheduler, it affects the schedule.

import torch

import torch.optim as optim from torch.optim.lr_scheduler import StepLR

model = torch.nn.Linear(10, 2) optimizer = optim.SGD(model.parameters(), lr=0.1)

# Change optimizer LR before creating scheduler optimizer.param_groups[0]['lr'] = 0.01

scheduler = StepLR(optimizer, step_size=10, gamma=0.1) print(f"First LR used by scheduler: {optimizer.param_groups[0]['lr']:.6f}")

Output:

First LR used by scheduler: 0.01

The scheduler will scale from 0.01, not 0.1.

When to Use What

| Scheduler | When | |-----------|------| | StepLR | Simple baselines; multiple drops at fixed intervals | | CosineAnnealingLR | Smooth decay; most consistent convergence | | OneCycleLR | Modern default; short training runs (< 100 epochs) | | CosineAnnealingWarmRestarts | Large training runs; exploring multiple minima | | ExponentialLR | Exponential decay; less common than others |

Conclusion

Learning rate scheduling adapts the learning rate during training based on progress. Constant learning rates are suboptimal—too large early and too small late. Cosine annealing and OneCycleLR are the most robust choices, with cosine being the default for long training and OneCycleLR for short runs. Understanding how schedulers work allows you to train more efficiently and achieve better final accuracy. Next: we'll explore a critical technique for scaling models to large datasets: mixed precision training and gradient scaling.

Closing Takeaways

Measure retrieval precision and recall in isolation before touching the model.
Chunk along document structure, not arbitrary character counts.
Combine vector and keyword search — hybrid retrieval beats either alone.
Treat evaluation as continuous infrastructure, not a launch-week report.
Try It Yourself
A runnable Google Colab notebook with the eval harness and hybrid search code from this post.
#PyTorch#Optimization#Learning Rate#Training
0 views
SS
Soham Sharma
AI Engineer at Botmartz, building enterprise RAG and agent systems in production. Contributing to open-source libraries.

Discussion (0)

No approved comments yet. Be the first to share your thoughts!

Leave a Comment

Your email address will not be published. Required fields are marked *

More Engineering Insights
TensorFlow>-
Soham Sharma · 8 min read
GeneralPlaywright E2E Test Post
Integration Bot · 5 min read