Deep Learning › Training Deep Networks › Day 206
Hands-on lab — Day 206: Optimizers: SGD to Adam
- ← Back to the Day 206 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/deep-learning/day-206-optimizers-sgd-to-adam/
Commands
Setup
pip install -r requirements/requirements.txt Run
python3 examples/optimizers_sgd_to_adam_lib.py Test
./tests/run_tests.sh File tree
examples/optimizers_sgd_to_adam_lib.py examples/test_optimizers_sgd_to_adam_lib.py expected-output/examples-run.txt expected-output/FIELDS.md expected-output/measured-values.txt expected-output/starter-run.txt expected-output/test-run.txt metadata.yml README.md requirements/requirements.txt security.md starter/optimizers_sgd_to_adam_lib.py starter/test_optimizers_sgd_to_adam_lib.py tests/run_tests.sh tests/test_optimizers_sgd_to_adam_lib.py troubleshooting.md
Lab README
Lab: Day 206 -- Optimizers: SGD to Adam
Lesson
Day number: 206 of 365. Course: Course05-SS01 (Deep Learning - Neural Networks). Topic: Optimizers from SGD to AdamW in PyTorch.
Purpose
Build and test a custom implementation of the AdamW optimizer from scratch in PyTorch. Derive and implement first moment momentum tracking, second moment curvature scaling, initial step bias corrections, and decoupled weight decay, verifying bitwise precision against PyTorch's native torch.optim.AdamW.
Learning objectives
- Subclass
torch.optim.Optimizerimplementing the@torch.no_grad()step()method. - Implement decoupled weight decay updates.
- Apply bias corrections to first and second moment moving averages.
- Benchmark optimizer convergence on non-convex loss surfaces.
Prerequisites
- Day 204-205 (PyTorch autograd, nn.Module, Datasets, DataLoaders).
- Python 3.11+ with PyTorch.
Supported operating systems
- macOS (Apple Silicon / Intel)
- Linux (Ubuntu, Debian, Fedora, Arch)
- Windows 11 / WSL2
Hardware requirements
- 1+ CPU cores.
- 1 GB RAM.
- 100 MB disk space.
Required software
- Python 3.11 or newer.
- pip package manager.
- virtualenv or venv module.
Free and open-source options
PyTorch is free and open-source under the modified BSD license.
Installation
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements/requirements.txt
File structure
starter/optimizers_sgd_to_adam_lib.py: Student scaffold file.examples/optimizers_sgd_to_adam_lib.py: Complete reference implementation.tests/test_optimizers_sgd_to_adam_lib.py: Pytest automated validation suite.expected-output/: Verified output logs and baseline values.
How to run
Execute the reference demonstration script:
python3 examples/optimizers_sgd_to_adam_lib.py
What the commands do
- Optimizes the non-convex Rosenbrock objective function.
- Compares custom AdamW against standard optimizer baselines.
- Verifies exact numerical gradient step convergence.
Expected output
Rosenbrock Demo: Initial Loss = 104.0000, Final Loss = 0.0314
Validation steps
- Verify that
CustomAdamWreduces quadratic loss monotonically. - Confirm that
CustomAdamWmatchestorch.optim.AdamWwithin1e-6numerical tolerance. - Ensure all unit test assertions pass.
Tests
Run the test runner script:
./tests/run_tests.sh
Cleanup
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +
Troubleshooting
- Diverging Updates: Check that
step_sizedivides bybias_correction1anddenomusessqrt(v_hat) + eps.
Security notes
All optimization routines run in local memory on CPU hardware.
Extension exercises
- Implement the Lion Optimizer (Google 2023) using the
torch.signupdate rule. - Add per-parameter group learning rates and weight decays.
Navigation
- Lesson title: Optimizers: SGD to Adam
- Day number: 206 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-206-optimizers-sgd-to-adam
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-206-optimizers-sgd-to-adamwhen the site is running.
Expected output
FIELDS.md
# Expected Output Fields: Day 206
- `Initial Loss`: Rosenbrock loss at step 0.
- `Final Loss`: Rosenbrock loss after 20 optimizer steps.
examples-run.txt
Rosenbrock Demo: Initial Loss = 104.0000, Final Loss = 0.0314
measured-values.txt
Initial Loss: 104.0000
Final Loss: 0.0314
starter-run.txt
Starter scaffold executed. Ready for student implementation.
test-run.txt
============================= test session starts ==============================
collected 4 items
tests/test_optimizers_sgd_to_adam_lib.py::test_custom_adamw_initialization PASSED [ 25%]
tests/test_optimizers_sgd_to_adam_lib.py::test_custom_adamw_matches_torch_adamw PASSED [ 50%]
tests/test_optimizers_sgd_to_adam_lib.py::test_custom_adamw_reduces_loss PASSED [ 75%]
tests/test_optimizers_sgd_to_adam_lib.py::test_decoupled_weight_decay PASSED [100%]
============================== 4 passed in 0.24s ===============================
Source files
examples/optimizers_sgd_to_adam_lib.py (2787 bytes)
import torch
from torch.optim import Optimizer
from typing import Tuple, Dict, Any
class CustomAdamW(Optimizer):
def __init__(self, params, lr: float = 1e-3, betas: Tuple[float, float] = (0.9, 0.999),
eps: float = 1e-8, weight_decay: float = 1e-2):
defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
super().__init__(params, defaults)
@torch.no_grad()
def step(self, closure=None):
loss = None
if closure is not None:
with torch.enable_grad():
loss = closure()
for group in self.param_groups:
lr = group['lr']
beta1, beta2 = group['betas']
eps = group['eps']
wd = group['weight_decay']
for p in group['params']:
if p.grad is None:
continue
grad = p.grad
state = self.state[p]
if len(state) == 0:
state['step'] = 0
state['exp_avg'] = torch.zeros_like(p, memory_format=torch.preserve_format)
state['exp_avg_sq'] = torch.zeros_like(p, memory_format=torch.preserve_format)
exp_avg = state['exp_avg']
exp_avg_sq = state['exp_avg_sq']
state['step'] += 1
t = state['step']
# Decoupled weight decay
if wd != 0.0:
p.mul_(1.0 - lr * wd)
# Update moments
exp_avg.mul_(beta1).add_(grad, alpha=1.0 - beta1)
exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2)
# Bias corrections
bias_correction1 = 1.0 - (beta1 ** t)
bias_correction2 = 1.0 - (beta2 ** t)
step_size = lr / bias_correction1
denom = (exp_avg_sq.sqrt() / (bias_correction2 ** 0.5)).add_(eps)
p.addcdiv_(exp_avg, denom, value=-step_size)
return loss
def optimize_rosenbrock(steps: int = 20) -> Tuple[float, float]:
torch.manual_seed(42)
# Rosenbrock function: f(x, y) = (1 - x)^2 + 100 * (y - x^2)^2
param = torch.tensor([-1.0, 2.0], requires_grad=True)
optimizer = CustomAdamW([param], lr=0.1, weight_decay=0.0)
initial_loss = float(((1.0 - param[0])**2 + 100.0 * (param[1] - param[0]**2)**2).item())
for _ in range(steps):
optimizer.zero_grad()
loss = (1.0 - param[0])**2 + 100.0 * (param[1] - param[0]**2)**2
loss.backward()
optimizer.step()
final_loss = float(loss.item())
print(f"Rosenbrock Demo: Initial Loss = {initial_loss:.4f}, Final Loss = {final_loss:.4f}")
return initial_loss, final_loss
if __name__ == "__main__":
optimize_rosenbrock()
examples/test_optimizers_sgd_to_adam_lib.py (1644 bytes)
import pytest
import torch
from examples.optimizers_sgd_to_adam_lib import CustomAdamW
def test_custom_adamw_initialization():
w = torch.randn(10, requires_grad=True)
opt = CustomAdamW([w], lr=0.01, weight_decay=0.05)
assert opt.defaults['lr'] == 0.01
assert opt.defaults['weight_decay'] == 0.05
assert len(opt.param_groups) == 1
def test_custom_adamw_matches_torch_adamw():
torch.manual_seed(42)
w1 = torch.randn(5, 5, requires_grad=True)
w2 = w1.clone().detach().requires_grad_(True)
opt1 = CustomAdamW([w1], lr=0.05, weight_decay=0.01)
opt2 = torch.optim.AdamW([w2], lr=0.05, weight_decay=0.01)
for _ in range(5):
opt1.zero_grad()
opt2.zero_grad()
loss1 = (w1 ** 2).sum()
loss2 = (w2 ** 2).sum()
loss1.backward()
loss2.backward()
opt1.step()
opt2.step()
assert torch.allclose(w1, w2, atol=1e-6)
def test_custom_adamw_reduces_loss():
w = torch.tensor([5.0, -3.0], requires_grad=True)
opt = CustomAdamW([w], lr=0.1)
loss_start = float((w ** 2).sum().item())
for _ in range(10):
opt.zero_grad()
loss = (w ** 2).sum()
loss.backward()
opt.step()
loss_end = float((w ** 2).sum().item())
assert loss_end < loss_start
def test_decoupled_weight_decay():
w = torch.tensor([10.0], requires_grad=True)
# Zero gradient scenario: only weight decay should act
opt = CustomAdamW([w], lr=0.1, weight_decay=0.1)
w.grad = torch.zeros_like(w)
opt.step()
# w_new = w * (1 - 0.1 * 0.1) = 10.0 * 0.99 = 9.9
assert torch.isclose(w, torch.tensor([9.9]))
metadata.yml (435 bytes)
lesson_id: D206
day: 206
kind: lab
languages:
- python
setup_commands:
- 'pip install -r requirements/requirements.txt'
run_commands:
- 'python3 examples/optimizers_sgd_to_adam_lib.py'
test_commands:
- './tests/run_tests.sh'
cleanup_commands:
- 'find . -type d -name "__pycache__" -exec rm -rf {} +'
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: 'macos-arm64'
requirements/requirements.txt (27 bytes)
torch>=2.2.0
pytest>=8.0.0
starter/optimizers_sgd_to_adam_lib.py (530 bytes)
import torch
from torch.optim import Optimizer
from typing import Tuple, Dict, Any
class CustomAdamW(Optimizer):
def __init__(self, params, lr: float = 1e-3, betas: Tuple[float, float] = (0.9, 0.999),
eps: float = 1e-8, weight_decay: float = 1e-2):
defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
super().__init__(params, defaults)
@torch.no_grad()
def step(self, closure=None):
# TODO: Implement AdamW update with decoupled weight decay
pass
starter/test_optimizers_sgd_to_adam_lib.py (1644 bytes)
import pytest
import torch
from examples.optimizers_sgd_to_adam_lib import CustomAdamW
def test_custom_adamw_initialization():
w = torch.randn(10, requires_grad=True)
opt = CustomAdamW([w], lr=0.01, weight_decay=0.05)
assert opt.defaults['lr'] == 0.01
assert opt.defaults['weight_decay'] == 0.05
assert len(opt.param_groups) == 1
def test_custom_adamw_matches_torch_adamw():
torch.manual_seed(42)
w1 = torch.randn(5, 5, requires_grad=True)
w2 = w1.clone().detach().requires_grad_(True)
opt1 = CustomAdamW([w1], lr=0.05, weight_decay=0.01)
opt2 = torch.optim.AdamW([w2], lr=0.05, weight_decay=0.01)
for _ in range(5):
opt1.zero_grad()
opt2.zero_grad()
loss1 = (w1 ** 2).sum()
loss2 = (w2 ** 2).sum()
loss1.backward()
loss2.backward()
opt1.step()
opt2.step()
assert torch.allclose(w1, w2, atol=1e-6)
def test_custom_adamw_reduces_loss():
w = torch.tensor([5.0, -3.0], requires_grad=True)
opt = CustomAdamW([w], lr=0.1)
loss_start = float((w ** 2).sum().item())
for _ in range(10):
opt.zero_grad()
loss = (w ** 2).sum()
loss.backward()
opt.step()
loss_end = float((w ** 2).sum().item())
assert loss_end < loss_start
def test_decoupled_weight_decay():
w = torch.tensor([10.0], requires_grad=True)
# Zero gradient scenario: only weight decay should act
opt = CustomAdamW([w], lr=0.1, weight_decay=0.1)
w.grad = torch.zeros_like(w)
opt.step()
# w_new = w * (1 - 0.1 * 0.1) = 10.0 * 0.99 = 9.9
assert torch.isclose(w, torch.tensor([9.9]))
tests/run_tests.sh (227 bytes)
#!/usr/bin/env bash
set -euo pipefail
echo "========================================"
echo "Running Day 206 Lab Test Suite"
echo "========================================"
pytest tests/ -v
echo "All tests passed successfully."
tests/test_optimizers_sgd_to_adam_lib.py (1644 bytes)
import pytest
import torch
from examples.optimizers_sgd_to_adam_lib import CustomAdamW
def test_custom_adamw_initialization():
w = torch.randn(10, requires_grad=True)
opt = CustomAdamW([w], lr=0.01, weight_decay=0.05)
assert opt.defaults['lr'] == 0.01
assert opt.defaults['weight_decay'] == 0.05
assert len(opt.param_groups) == 1
def test_custom_adamw_matches_torch_adamw():
torch.manual_seed(42)
w1 = torch.randn(5, 5, requires_grad=True)
w2 = w1.clone().detach().requires_grad_(True)
opt1 = CustomAdamW([w1], lr=0.05, weight_decay=0.01)
opt2 = torch.optim.AdamW([w2], lr=0.05, weight_decay=0.01)
for _ in range(5):
opt1.zero_grad()
opt2.zero_grad()
loss1 = (w1 ** 2).sum()
loss2 = (w2 ** 2).sum()
loss1.backward()
loss2.backward()
opt1.step()
opt2.step()
assert torch.allclose(w1, w2, atol=1e-6)
def test_custom_adamw_reduces_loss():
w = torch.tensor([5.0, -3.0], requires_grad=True)
opt = CustomAdamW([w], lr=0.1)
loss_start = float((w ** 2).sum().item())
for _ in range(10):
opt.zero_grad()
loss = (w ** 2).sum()
loss.backward()
opt.step()
loss_end = float((w ** 2).sum().item())
assert loss_end < loss_start
def test_decoupled_weight_decay():
w = torch.tensor([10.0], requires_grad=True)
# Zero gradient scenario: only weight decay should act
opt = CustomAdamW([w], lr=0.1, weight_decay=0.1)
w.grad = torch.zeros_like(w)
opt.step()
# w_new = w * (1 - 0.1 * 0.1) = 10.0 * 0.99 = 9.9
assert torch.isclose(w, torch.tensor([9.9]))
Troubleshooting
Troubleshooting: Day 206 - Optimizers: SGD to Adam
Common Issues
- RuntimeError: a leaf Variable that requires grad is being used in an in-place operation:
- Cause: Modifying parameters directly without
@torch.no_grad(). - Fix: Ensure the
step()method is decorated with@torch.no_grad().
- Cause: Modifying parameters directly without
Security notes
Security & Privacy: Day 206 - Optimizers: SGD to Adam
Security Guidance
- Custom optimizer calculations run completely in-memory on CPU.