๐ŸŽ“ A1Learner
๐Ÿ”

PyTorch Logits vs Probabilities, the Classic Bug

Logits are the raw scores a model outputs before any squashing into probabilities. CrossEntropyLoss expects logits, and it applies its own log-softmax inside.

The buggy line

Softmax turns logits into probabilities. Probabilities are the right output for a person to read.

import torch

logits = torch.tensor([[2.0, 1.0, 0.0]])
probs = torch.softmax(logits, dim=1)
print(probs.round(decimals=4))

The same loss, two inputs

Cross-entropy scores the logits directly. Feeding it probabilities scores a different, softer input.

import torch

logits = torch.tensor([[2.0, 1.0, 0.0]])
target = torch.tensor([0])
loss_fn = torch.nn.CrossEntropyLoss()
print(f"{loss_fn(logits, target).item():.4f}")
probs = torch.softmax(logits, dim=1)
print(f"{loss_fn(probs, target).item():.4f}")

Softened twice

Cross-entropy applies log-softmax to its input. Probabilities fed in get squashed a second time and move closer together.

import torch

probs = torch.tensor([[0.6652, 0.2447, 0.0900]])
print(torch.softmax(probs, dim=1).round(decimals=4))

Example

import torch

torch.manual_seed(0)

# Three clusters of points, one per class.
centers = torch.tensor([[-3.0, 0.0], [3.0, -2.0], [0.0, 3.0]])
X = torch.cat([c + 0.8 * torch.randn(30, 2) for c in centers])
y = torch.cat([torch.full((30,), i) for i in range(3)])


def train(buggy):
    torch.manual_seed(1)
    model = torch.nn.Linear(2, 3)
    optimizer = torch.optim.SGD(model.parameters(), lr=0.3)
    loss_fn = torch.nn.CrossEntropyLoss()
    trace = []
    for step in range(61):
        logits = model(X)
        if buggy:
            fed = torch.softmax(logits, dim=1)
        else:
            fed = logits
        loss = loss_fn(fed, y)
        if step % 20 == 0:
            trace.append(loss.item())
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    return trace


correct = train(False)
buggy = train(True)
for step, c, b in zip([0, 20, 40, 60], correct, buggy):
    print(f"Step {step}: correct {c:.4f}  buggy {b:.4f}")

Key points

  • Logits: the raw scores from the last layer, before softmax
  • Probability: a score squashed by softmax so the scores sum to 1
  • Use nn.CrossEntropyLoss with logits, never with probabilities
  • Softmax before cross-entropy softens the scores twice, and learning slows down
  • The fix for the bug is deleting the softmax line, nothing more
  • Raw scores go into cross-entropy, and the loss does its own squashing inside

In this example, three clusters of points form three classes. Two models start from the same seed and train on the same data. The correct model feeds logits straight into CrossEntropyLoss. The buggy model applies softmax first. At step 0 the buggy loss reads lower, at 1.2890 against 2.2989. By step 20 the correct loss has fallen to 0.0563, and the buggy loss still sits at 0.6173. At step 60 the gap remains, at 0.0284 against 0.5770. The double-softened scores give the loss far less to learn from. The buggy model crawls while the correct model finishes.

Note

nn.CrossEntropyLoss combines log-softmax and negative log-likelihood in one step

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

A model ends with softmax, and its output goes into nn.CrossEntropyLoss. What is the fix?