Optimizer choice controls how a PyTorch training loop turns gradients into parameter updates. Adam keeps running averages of gradients and squared gradients, so it is a common starting point for small models, noisy losses, and custom loops that need adaptive learning rates instead of plain SGD.
torch.optim.Adam receives model parameters and stores per-parameter optimizer state after the first successful step() call. Build it after the model has been created and moved to the target device, then run it in the usual training order of clearing old gradients, calculating loss, backpropagating, and stepping the optimizer.
A CPU smoke run with a tiny linear model makes the optimizer state visible without accelerator-specific setup. A changed weight, state_step=1, non-zero exp_avg_norm, and lower loss confirm that Adam updated the model and initialized its first-moment state.
import torch from torch import nn torch.manual_seed(7) features = torch.tensor( [ [-1.0, 0.5, 0.25], [0.0, -0.25, 0.75], [0.5, 0.75, -0.5], [1.0, -0.5, -0.25], ], dtype=torch.float32, ) targets = features @ torch.tensor([[0.6], [-0.4], [0.2]]) + 0.1 model = nn.Sequential(nn.Linear(3, 1)) loss_fn = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.05) first_weight = model[0].weight weight_before = first_weight.detach().clone() optimizer.zero_grad(set_to_none=True) prediction = model(features) loss_before = loss_fn(prediction, targets) loss_before.backward() gradient_norm = torch.linalg.vector_norm(first_weight.grad).item() optimizer.step() with torch.no_grad(): loss_after = loss_fn(model(features), targets) state = optimizer.state[first_weight] weight_changed = not torch.equal(weight_before, first_weight.detach()) print(f"torch_version={torch.__version__}") print(f"optimizer={optimizer.__class__.__name__}") print(f"learning_rate={optimizer.param_groups[0]['lr']}") print(f"loss_before={loss_before.item():.6f}") print(f"gradient_norm={gradient_norm:.6f}") print(f"state_step={int(state['step'].item())}") print(f"exp_avg_norm={torch.linalg.vector_norm(state['exp_avg']).item():.6f}") print(f"weight_changed={weight_changed}") print(f"loss_after={loss_after.item():.6f}") print(f"loss_decreased={loss_after.item() < loss_before.item()}")
lr=0.05 is intentionally high for a tiny smoke test. Use a project-specific learning rate, often near 1e-3, before tuning a real model.
$ python adam_optimizer_step.py torch_version=2.12.1+cpu optimizer=Adam learning_rate=0.05 loss_before=0.179021 gradient_norm=0.690800 state_step=1 exp_avg_norm=0.069080 weight_changed=True loss_after=0.131637 loss_decreased=True
weight_changed=True confirms that optimizer.step() updated a model parameter. state_step=1 and exp_avg_norm confirm that Adam created optimizer state for the first tracked parameter.
model = build_model().to(device) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
Pass model.parameters() directly when every trainable parameter uses the same options. Use parameter groups only when different layers need different learning rates or weight decay values.
optimizer.zero_grad(set_to_none=True)
set_to_none=True lets parameters that receive no gradient stay at None, and torch.optim skips those parameters during the step.
Related: How to zero gradients in PyTorch
predictions = model(batch_features) loss = loss_fn(predictions, batch_targets) loss.backward()
optimizer.step()
Call learning-rate schedulers after the optimizer update unless a specific scheduler documents another order.
Related: How to set a learning rate scheduler in PyTorch
first_parameter = next(model.parameters()) before = first_parameter.detach().clone() optimizer.zero_grad(set_to_none=True) predictions = model(batch_features) loss = loss_fn(predictions, batch_targets) loss.backward() optimizer.step() weight_changed = not torch.equal(before, first_parameter.detach()) print(f"weight_changed={weight_changed}")
If weight_changed is False, inspect whether the loss used model outputs, whether gradients are None, and whether the optimizer was constructed with the same parameters used in the forward pass.
$ rm adam_optimizer_step.py