QuiddityML

23 September 2026 · 13 min read · fundamentalsloss functionspytorch

What is a loss function? MSE, cross-entropy, and when to use which

A loss function turns a model's predictions into one number that says how wrong they are, and training is the process of pushing that number down. This post explains why models need one, which loss to use for regression, binary, multi-class, and multi-label problems (and for a few other tasks), and how to use the loss on a proper validation split to evaluate a model.

A loss function takes a model's predictions and the correct answers and returns one number that says how wrong the predictions are. A loss of 0 means every prediction was exactly right, and a higher loss means worse predictions. Training a model means changing its weights until this number is as small as possible, so the loss is how you tell the model what "good" means. With the wrong loss, a model can train smoothly and still get good at the wrong thing.

What a loss function does

Say a model predicts house prices in hundreds of thousands of dollars. For four houses it predicts 3.1, 7.4, 1.9, and 5.2, and the real prices are 3.0, 8.0, 2.0, and 5.0. A loss function compares each prediction with its true value and combines all four differences into a single number.

The usual notation: $\hat y$ (read "y-hat") is a prediction, $y$ is the true value, and $\mathcal{L}(\hat y, y)$ is the loss computed from them. When the loss is averaged over a dataset of $n$ examples, $\hat y_i$ and $y_i$ are the prediction and true value for example $i$.

A loss function takes four predictions and four true labels and returns one number, 0.32, and training looks for the weights that make that number as small as possible

The picture shows the loss as 0.32 for these four houses. Which loss produced that number is covered in the regression section below, because different losses give different numbers for the same predictions.

Why a model needs a loss function

A model's weights are the numbers inside it that training adjusts. To improve, the model needs to know, for each weight, whether nudging it up or down makes the predictions better. The loss answers that. For every weight, training computes how much the loss changes when that weight changes slightly. That collection of rates is the gradient, and computing it for every weight at once is what backpropagation does. Then every weight takes a small step in the direction that lowers the loss, which is gradient descent. Repeat that over many batches of data and the loss goes down.

This puts two requirements on a loss:

  1. It is one number. Gradient descent lowers one quantity at a time. If you care about two things, such as getting the class right and getting the box position right in object detection, you add the two losses into one, usually with a weight on each.
  2. It changes smoothly with the weights. A tiny change to one weight has to produce a tiny, measurable change in the loss, or the gradient is zero and training has nothing to follow. Accuracy fails this test. Nudge one weight slightly and almost every prediction stays on the same side of the decision, so accuracy usually stays the same. That is why classifiers train on cross-entropy and report accuracy.

Losses for regression: predicting a number

Regression means predicting a continuous number: a price, a temperature, a delivery time. The four common losses all start from the error, $\hat y_i - y_i$, and differ in how they charge for it.

Mean squared error (MSE)

MSE squares each error and averages the squares:

$$\text{MSE} = \frac{1}{n}\sum_{i=1}^{n}(\hat y_i - y_i)^2$$

For the four houses, the errors are 0.1, −0.6, −0.1, and 0.2. The squares are 0.01, 0.36, 0.01, and 0.04, which sum to 0.42, so the MSE is 0.105.

Squaring makes large errors cost far more than small ones. An error of 1 costs 1, and an error of 3 costs 9. MSE is the loss you get if you assume the errors follow a bell-shaped normal distribution around the true value, and it is the usual default for regression. It is a poor choice when the data has outliers, because a single bad example can outweigh everything else. In PyTorch it is nn.MSELoss().

Root mean squared error (RMSE)

RMSE is the square root of MSE:

$$\text{RMSE} = \sqrt{\text{MSE}}$$

For the four houses that is $\sqrt{0.105} \approx 0.32$, the number in the picture above. RMSE is in the same units as the target (hundreds of thousands of dollars here), so it is easier to read than MSE. Minimizing RMSE and minimizing MSE lead to the same weights, so people usually train on MSE and report RMSE. PyTorch has no separate RMSE loss, so compute it as torch.sqrt(nn.functional.mse_loss(pred, y)).

Mean absolute error (MAE)

MAE averages the absolute errors instead of squaring them:

$$\text{MAE} = \frac{1}{n}\sum_{i=1}^{n}\lvert \hat y_i - y_i \rvert$$

For the four houses, that is (0.1 + 0.6 + 0.1 + 0.2) / 4 = 0.25. An error three times larger costs three times more, not nine times more, so outliers pull on the model much less.

MAE grows in a straight line with the error while MSE grows as the square, so an error of 3 costs 3 under MAE and 9 under MSE

The difference shows up fast with one outlier. Add a fifth house predicted at 4.0 that actually sold for 14.0, an error of 10. MSE jumps from 0.105 to (0.42 + 100) / 5 ≈ 20.1, almost all of it from that one house. MAE goes from 0.25 to (1.0 + 10) / 5 = 2.2.

MAE has a corner at an error of 0, where it is not differentiable. PyTorch handles that point fine in practice, but the gradient has the same size whether the error is 0.01 or 10, so updates don't shrink as predictions get close to right. There is also a statistical difference: a model trained with MSE is pulled toward predicting the mean of the possible values, and one trained with MAE toward the median. In PyTorch MAE is nn.L1Loss().

Huber loss

Huber loss is MSE for small errors and MAE for large ones. A setting $\delta$ (delta) marks where it switches. For an error $e = \hat y - y$:

$$L_\delta(e) = \tfrac{1}{2}e^2 \quad \text{if } |e| \le \delta$$

$$L_\delta(e) = \delta\left(|e| - \tfrac{1}{2}\delta\right) \quad \text{otherwise}$$

Near zero it behaves like MSE, so updates shrink as the prediction gets close, and far out it grows in a straight line like MAE, so outliers can't take over. It is a good choice for data that is mostly clean with some bad values. In PyTorch it is nn.HuberLoss(delta=1.0). nn.SmoothL1Loss(beta=1.0) is the same shape divided by beta, and is often used for bounding boxes in object detection.

Huber loss compared with MSE and MAE for errors from -3 to 3, with delta set to 1: curved like MSE between -1 and 1, and a straight line like MAE beyond that

1import torch
2import torch.nn as nn
3 
4pred = torch.tensor([3.1, 7.4, 1.9, 5.2])
5target = torch.tensor([3.0, 8.0, 2.0, 5.0])
6 
7print(nn.MSELoss()(pred, target))               # 0.105
8print(torch.sqrt(nn.MSELoss()(pred, target)))   # RMSE, 0.324
9print(nn.L1Loss()(pred, target))                # MAE, 0.25
10print(nn.HuberLoss(delta=1.0)(pred, target))    # 0.0525, all errors are under delta

Losses for binary classification: yes or no

Binary classification means choosing between two classes: spam or not spam, fraud or not fraud. The model outputs one number per example, called a logit, which can be any real number. The sigmoid function turns the logit $z$ into a probability $p$ between 0 and 1:

$$\sigma(z) = \frac{1}{1 + e^{-z}}$$

Here $p$ is the predicted probability that the example is in class 1, and the label $y$ is 1 or 0. Binary cross-entropy (BCE) is:

$$\text{BCE} = -\frac{1}{n}\sum_{i=1}^{n}\Big[y_i \log p_i + (1 - y_i)\log(1 - p_i)\Big]$$

For each example only one of the two terms is active. If the label is 1, the loss is $-\log p$, and if the label is 0, it is $-\log(1-p)$. With a true label of 1, a prediction of $p = 0.9$ costs 0.105, a coin-flip $p = 0.5$ costs 0.693, $p = 0.1$ costs 2.303, and $p = 0.01$ costs 4.605.

The log is what makes a confident wrong answer expensive. Going from 0.1 to 0.01 on a true example doubles the loss, even though both predictions count as equally wrong on accuracy.

Binary cross-entropy against the predicted probability for the true class: nearly 0 when confident and right, 0.69 for a coin flip, and rising steeply when confident and wrong

MSE on sigmoid outputs trains poorly. When the model is confidently wrong, the sigmoid is nearly flat, so MSE's gradient is tiny exactly when the model most needs to change. With BCE the gradient with respect to the logit works out to $p - y$, which stays large when the prediction is far off.

In PyTorch, use nn.BCEWithLogitsLoss() and pass it raw logits. It applies the sigmoid and the log together in a numerically stable way, which avoids the log(0) that nn.BCELoss on already-squashed probabilities can hit. For imbalanced data, pos_weight makes errors on the rare positive class cost more, and a common starting value is the number of negatives divided by the number of positives.

1logits = model(x).squeeze(-1)           # shape (batch,)
2y = y.float()                           # 0.0 or 1.0, same shape as logits
3loss_fn = nn.BCEWithLogitsLoss(pos_weight=torch.tensor(20.0))  # 1 positive per 20 negatives
4loss = loss_fn(logits, y)

Losses for multi-class classification: one of K

Multi-class classification means picking exactly one of $K$ classes: which digit, which species, which intent. The model outputs $K$ logits $z_1, \dots, z_K$, and softmax turns them into $K$ probabilities that are each between 0 and 1 and sum to 1:

$$\text{softmax}(z)k = \frac{e^{z_k}}{\sum{j=1}^{K} e^{z_j}}$$

Cross-entropy is the negative log of the probability the model gave to the correct class, averaged over examples. Writing $p_{i,y_i}$ for the probability that example $i$ got on its correct class $y_i$:

$$\text{CE} = -\frac{1}{n}\sum_{i=1}^{n}\log p_{i,y_i}$$

If the correct class got probability 0.46, the loss for that example is $-\log 0.46 \approx 0.78$. The probabilities on the wrong classes don't appear in the formula directly, but softmax ties them together, so raising the correct class's probability lowers the others.

Softmax turns K logits into K probabilities between 0 and 1 that sum to 1, and nn.CrossEntropyLoss takes the raw logits and class-index targets

The log has a reason beyond making confident mistakes expensive. The probability the model assigns to all the correct labels in a dataset is the product of the per-example probabilities, and the log turns that product into a sum. Minimizing cross-entropy is therefore the same as making the observed labels as probable as possible under the model, which is called maximum likelihood.

nn.CrossEntropyLoss() takes raw logits of shape (batch, K) and targets as class indices of dtype long with shape (batch,). It applies log-softmax internally, so the model's last layer should be a plain linear layer with no softmax. Two options are worth knowing:

Losses for multi-label classification: several yes/no answers

In multi-label classification one example can have several labels at once, such as a photo that contains both a dog and a car. That is $K$ independent yes/no questions, so the model outputs $K$ logits and each one gets its own sigmoid and its own binary cross-entropy.

Softmax is the wrong choice here, because it forces the $K$ probabilities to sum to 1. A higher probability for "dog" would then push "car" down, even though both are in the photo. In PyTorch, use nn.BCEWithLogitsLoss() with a float target of 0s and 1s shaped (batch, K), the same shape as the logits.

Losses for other tasks

Most other losses are built from the ones above, applied to a different kind of output.

Which loss to use

task output layer loss PyTorch
regression one linear output MSE, MAE, or Huber nn.MSELoss, nn.L1Loss, nn.HuberLoss
binary one logit binary cross-entropy nn.BCEWithLogitsLoss
multi-class K logits cross-entropy nn.CrossEntropyLoss
multi-label K logits binary cross-entropy per label nn.BCEWithLogitsLoss

A decision tree for choosing a loss: predicting a number leads to MSE, MAE or Huber, or RMSE, and predicting a class leads to binary cross-entropy or cross-entropy

For regression, the choice comes down to what a big error should cost. Use MSE when a big miss really is much worse than a small one, MAE or Huber when big misses are mostly bad data, and report RMSE or MAE in the target's units either way.

Using the loss to evaluate a model

The loss on the training data only says how well the model fits examples it has already seen. A model can drive training loss close to 0 by memorizing, which is overfitting, and still do badly on new data. To see how it does on new data, you need a split.

The three-way split: 70% training set used in every gradient update, 15% validation set used for tuning decisions, 15% test set scored once

After each epoch, compute the same loss on the validation set with the model in evaluation mode and gradients turned off:

1model.eval()
2total, count = 0.0, 0
3with torch.no_grad():
4    for x, y in val_loader:
5        logits = model(x)
6        total += loss_fn(logits, y).item() * len(y)
7        count += len(y)
8val_loss = total / count
9model.train()

Then compare the two curves. Both falling means the model is learning. Training loss still falling while validation loss rises means overfitting. Both staying high means underfitting, a model too simple or trained too little to fit even the training data. The overfitting vs underfitting post shows what each curve looks like, the bias-variance tradeoff post explains why they happen, and regularization covers the fixes for overfitting.

Validation loss and a metric like accuracy can disagree. Validation loss can rise while accuracy holds steady, because the model grows more confident on the examples it already gets wrong, and cross-entropy charges heavily for confident mistakes while accuracy only counts right and wrong. Track both: the loss tells you how training is going, and the task metric (accuracy, F1, RMSE in real units) tells you what people using the model will see.

A validation score is only as good as the split behind it:

Losses are only comparable on the same data, with the same loss function and the same reduction. A validation loss of 0.4 on one split says nothing about a 0.5 on a different split.

Common mistakes

If the loss isn't going down at all, the loss not decreasing checklist walks through the usual causes.

QuiddityML teaches loss functions in the first unit of its ML Foundation track, and the exercises include picking a loss for house prices with a few extreme mansions, writing Huber loss and binary cross-entropy from scratch with tensor operations, and spotting what goes wrong when sigmoid outputs are passed to BCEWithLogitsLoss.