DataLens

Loss functions and optimizers

Pick the right loss for regression, binary and multi-class problems, understand logits and BCEWithLogitsLoss, and let an optimizer like SGD or Adam update the weights.

Level: Intermediate · 7 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Training needs two more pieces:

PyTorch provides both, so the training step shrinks to a few standard lines.

Losses for each problem type

ProblemModel outputLossTarget
regressionone number per examplenn.MSELoss() (or nn.L1Loss() for MAE)float values
binary classificationone logit per examplenn.BCEWithLogitsLoss()0.0 / 1.0 floats
multi-class (k classes)k logits per examplenn.CrossEntropyLoss()class index 0..k−1 (int64)
import torch
from torch import nn

pred = torch.tensor([52.0, 61.0, 44.0])
actual = torch.tensor([50.0, 65.0, 44.0])
print(nn.MSELoss()(pred, actual), nn.L1Loss()(pred, actual))
tensor(6.6667) tensor(2.)

Loss objects are created once, then called like functions: loss_fn(predictions, targets).

Logits and BCEWithLogitsLoss

For binary problems, the model outputs a raw score (logit), and the loss applies the sigmoid itself:

logits = torch.tensor([2.0, -1.0, 0.5])      # raw model outputs
targets = torch.tensor([1.0, 0.0, 0.0])
loss_fn = nn.BCEWithLogitsLoss()
print(loss_fn(logits, targets))

probs = torch.sigmoid(logits)
manual = -(targets * torch.log(probs) + (1 - targets) * torch.log(1 - probs)).mean()
print(manual)
tensor(0.4714)
tensor(0.4714)

The same log loss as the ML track. Why not apply sigmoid in the model and use nn.BCELoss on probabilities? Because for very confident predictions, log(sigmoid(z)) loses precision (a probability rounds to exactly 1.0, and log(0) appears). BCEWithLogitsLoss combines the two steps in a numerically stable way. Rule: models return logits; losses take logits.

Multi-class: CrossEntropyLoss

For k classes (say, which of 5 product categories a review is about), the model returns k logits per example, and the target is the index of the right class:

logits = torch.tensor([[2.0, 0.5, -1.0],      # example 1: class 0 looks likely
                       [0.1, 0.2, 3.0]])      # example 2: class 2 looks likely
targets = torch.tensor([0, 2])                # correct class indices (int64)
print(nn.CrossEntropyLoss()(logits, targets))
print(torch.softmax(logits, dim=1).round(decimals=3))
tensor(0.1755)
tensor([[0.7860, 0.1750, 0.0390],
        [0.0490, 0.0540, 0.8960]])

CrossEntropyLoss applies softmax (which turns k logits into k probabilities summing to 1) and log loss in one stable step. Again: no softmax in the model.

Optimizers

An optimizer holds a model's parameters and updates them from their .grad:

torch.manual_seed(0)
model = nn.Linear(1, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)
x = torch.tensor([[1.0], [2.0], [3.0]])
y = torch.tensor([[3.0], [5.0], [7.0]])
loss_fn = nn.MSELoss()

for step in range(3):
    optimizer.zero_grad()            # 1. clear old gradients
    loss = loss_fn(model(x), y)      # 2. forward pass + loss
    loss.backward()                  # 3. compute gradients
    optimizer.step()                 # 4. update parameters
    print(step, round(loss.item(), 3))
0 22.744
1 4.525
2 0.911

Those four lines are the heart of every PyTorch training loop. Compare with the autograd module: zero_grad() replaces the manual .grad.zero_() calls, and step() replaces the update inside no_grad().

Which optimizer?

for name, opt_class in [("SGD", torch.optim.SGD), ("Adam", torch.optim.Adam)]:
    torch.manual_seed(0)
    model = nn.Linear(1, 1)
    optimizer = opt_class(model.parameters(), lr=0.05)
    for _ in range(200):
        optimizer.zero_grad()
        loss = loss_fn(model(x), y)
        loss.backward()
        optimizer.step()
    print(name, round(loss.item(), 5), round(model.weight.item(), 3), round(model.bias.item(), 3))
SGD 0.00014 1.986 1.031
Adam 0.04756 1.743 1.57

SGD reaches w = 2, b = 1 within 200 steps; Adam, with the same learning rate, is still on its way. Adam's per-parameter step sizes shine on big, messy problems with many parameters, not on a one-weight toy, and each optimizer has its own good range of learning rates. The learning rate is still the most important setting, whatever the optimizer.

Key idea

Choose the loss from the problem type (MSE for numbers, BCEWithLogits for yes/no, CrossEntropy for k classes), feed it logits, and drive training with zero_grad → loss → backward → step.

Why this matters for clean code

Losses and optimizers are configuration, not logic: create them once, near the model, from clear settings (lr, weight_decay). Mismatches are the classic silent bugs: softmax in the model and in CrossEntropyLoss (training works, but worse), integer targets for BCE, float targets for CrossEntropy. Keeping the "logits in, loss out" convention everywhere prevents them.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: The training loop, step by step →