Activation Functions and Loss Functions
ReLU, GELU, Swish, sigmoid, softmax — and cross-entropy, MSE, Huber, focal loss. When to use each and why numerical stability matters more than you think.
The activation function decides what a neuron can express. The loss function decides what the network is trying to achieve. Both choices are made before training — and both can silently make a network fail.
You have a network architecture — layers, widths, connections. Two remaining decisions determine whether it trains successfully: what non-linearity to apply after each layer (activation function) and what quantity to minimise during training (loss function). Both are often treated as trivial defaults, but both have failure modes that are genuinely hard to debug.
The wrong activation function causes vanishing gradients (sigmoid in deep networks), dead neurons (ReLU with bad initialisation), or slow convergence (tanh). The wrong loss function causes the network to optimise for the wrong thing entirely — a model trained with MSE on a classification problem will learn to output the class mean, not the class probability. Numerical instability in either can silently corrupt training with NaN losses.
A football player's training regime (the loss function) determines what they get better at. Train them to minimise goals conceded — they become a defender. Train them to maximise goals scored — they become a striker. The same player, the same training intensity, but the objective determines the skill.
The activation function is the player's physical capability — how much they can bend, how fast they can turn. A player with no flexibility (linear activation) cannot do anything a simple regression cannot. A player with full agility (ReLU, GELU) can learn arbitrarily complex patterns.
Six activation functions — what each one does and when to use it
An activation function is applied element-wise after the linear transformation of each layer. Without it, a 10-layer network would collapse to a single linear transformation — no more expressive than one layer. The activation function is what gives neural networks their ability to learn non-linear patterns.
Six loss functions — match the loss to the task
The loss function is the quantity the network minimises during training. Choosing the wrong loss does not crash training — it often trains fine but optimises for the wrong thing. A network trained with MSE on a classification task learns to output class frequencies, not class probabilities. The outputs look reasonable but are fundamentally wrong.
The right loss function is determined entirely by the output type and what "correct" means for your task. Regression → MSE or MAE or Huber. Binary classification → BCE. Multi-class → Cross-entropy. Imbalanced classes → Focal loss. These are not interchangeable.
Predicting continuous values where large errors are costly. Delivery time, stock price, temperature.
Regression with outliers. Treats all errors proportionally.
Best of both: MSE for small errors (smooth gradient), MAE for large errors (outlier robust).
Two-class problems. Output layer must produce probabilities (0–1).
Three or more classes. Output layer produces one logit per class.
Severe class imbalance — fraud (1%), disease (0.1%). Downweights easy examples.
Numerical stability — why BCEWithLogitsLoss beats BCELoss every time
The most common source of NaN losses in production deep learning is not wrong architecture or bad data — it is numerical instability in loss functions. Understanding why BCEWithLogitsLoss exists and why CrossEntropyLoss takes raw logits (not softmax outputs) prevents hours of debugging.
Computing log(sigmoid(z)) directly overflows for large |z|. The numerically stable version uses the log-sum-exp trick:
PyTorch's BCEWithLogitsLoss and CrossEntropyLoss implement this trick internally. Using nn.Sigmoid() + nn.BCELoss() skips it — leading to NaN at training time when logits are large.
A complete decision guide — activation and loss for every task
Every common activation and loss mistake — explained and fixed
What production teams actually pick by default — and how dead neurons get caught before launch
In practice almost nobody picks an activation function from first principles for every new model. Teams inherit a default from whatever architecture family they are building on, and that default differs sharply depending on whether the model is a CNN, a transformer, or a small tabular MLP.
Three weeks into training a fraud-scoring MLP, validation AUC plateaus well below where a similar model performed on a previous dataset. Loss is still decreasing, slowly, and nothing in the training logs looks obviously broken. An engineer adds forward hooks that log the fraction of each ReLU layer's outputs that are exactly zero, on every batch, and plots it over training.
Layer 2's dead-unit fraction starts around 8 percent at initialisation and climbs steadily to 46 percent by epoch 20 — and it is still rising. Nearly half the neurons in that layer produce zero output, and therefore zero gradient, for every single input. The root cause: a learning rate of 0.01 combined with a large negative bias initialisation pushed many pre-activations permanently negative in the first few hundred steps, and ReLU's zero gradient for negative inputs meant those neurons could never recover on their own.
The fix was two changes, not one: switch that layer to LeakyReLU so a small gradient always flows even for negative pre-activations, and lower the learning rate from 0.01 to 0.003. Dead-unit tracking stayed in the training pipeline permanently afterward — logged as a metric alongside loss and accuracy, because by the time AUC visibly plateaus, the dead units have usually been accumulating for many epochs already.
Five things people get wrong about activation and loss functions
ReLU is a strong default, not a universal winner. Transformers — BERT, GPT, and effectively every modern large language model — use GELU by default because its smoothness (it is differentiable everywhere, unlike ReLU's hard kink at zero) measurably helps optimisation at that scale. LeakyReLU exists specifically because plain ReLU can permanently zero out neurons. RNNs and LSTMs still lean on tanh internally for zero-centred activations. "Default choice for hidden layers" is exactly right, but defaults get overridden the moment the architecture or task gives a reason to.
Softmax guarantees the outputs are non-negative and sum to 1 — that is a mathematical property of the formula, not a guarantee that a 0.97 output means the model is right 97% of the time. Modern, overparameterised networks are frequently overconfident: cross-entropy training pushes logits toward extreme values even after the network has already learned to classify correctly, inflating softmax outputs toward 0 or 1 well beyond what the true error rate justifies. Getting genuinely calibrated probabilities out of a classifier usually needs an explicit extra step, such as temperature scaling or Platt scaling, applied after training.
MSE measures squared distance from a target value, which is the right question for regression and the wrong question for classification. As this module's errors section shows directly: MSE on a binary problem is minimised by outputting the class mean (say 0.3 for a 30% positive rate) for every single input, because that constant genuinely minimises average squared error — the loss trains successfully by every metric except the one that actually matters. Cross-entropy is not an arbitrary alternative; it is the loss whose gradient actually pushes the network to separate classes.
In exact real-number arithmetic they are mathematically identical — but neural networks run in 32-bit floating point, where sigmoid(large logit) rounds to exactly 1.0 or 0.0 before log() ever sees it, producing log(0), which is negative infinity, which becomes NaN the instant it is combined with anything else. BCEWithLogitsLoss avoids this entirely by using the log-sum-exp identity to compute the same mathematical quantity without ever taking log(0) along the way. This is a correctness fix for a real failure mode, not a performance tweak — the naive version can silently produce wrong or NaN losses in production.
The two choices are coupled, not independent — this module's entire decision-guide table exists because getting one right without the other still breaks training. Using GELU in every hidden layer does nothing to fix a classifier trained with MSELoss; using the correct CrossEntropyLoss does nothing to fix a network that saturates because its output layer applies softmax before the loss instead of passing raw logits. The output layer's activation (or lack of one) and the loss function have to be chosen as a matched pair for the specific task — regression, binary, multi-class, or multi-label — not picked separately.
Activation and loss functions — 5 questions interviewers actually ask
Sigmoid squashes a single value independently to (0, 1) — appropriate for binary classification (one logit, one probability) or multi-label problems where each of several outputs is an independent yes/no decision. Softmax takes a whole vector of scores and converts them jointly into a probability distribution that sums to exactly 1 across all classes — appropriate when exactly one class is correct out of several. A binary problem can technically be modelled as 2-class softmax instead of 1-unit sigmoid, and the two are mathematically equivalent in that special case, but sigmoid with a single output is simpler and is what BCEWithLogitsLoss expects.
If a logit is large (say 100), sigmoid(100) rounds to exactly 1.0 in float32 due to limited precision. BCELoss then needs log(1 − 1.0) = log(0), which is negative infinity, and that propagates as NaN through the rest of training. BCEWithLogitsLoss avoids ever computing sigmoid and log as separate operations — it uses the log-sum-exp trick to compute the mathematically equivalent quantity in a form that never requires taking the log of exactly zero, regardless of how large or small the logit is. That is why PyTorch's own documentation recommends it over the two-step version unconditionally.
MSE is being used for a classification task. MSE is minimised, for a fixed input distribution, by outputting something close to the mean of the targets — for a class- imbalanced binary problem that constant is close to the positive rate itself, and the model can get quite low average loss without ever learning to separate the classes. The fix is to switch to BCEWithLogitsLoss for binary or CrossEntropyLoss for multi-class — losses whose gradients specifically reward pushing predicted probabilities toward the correct class, rather than just minimising average squared distance.
When the data has real outliers you do not want to ignore but also do not want to dominate training. MSE squares the error, so one wildly off prediction contributes disproportionately to the total loss and can drag the whole model toward accommodating that single point. MAE treats every error proportionally regardless of size, which is robust to outliers but has a constant-magnitude gradient that can cause oscillation right at convergence. Huber loss behaves like MSE for small errors — smooth gradients near the optimum — and like MAE for large errors beyond the delta threshold, giving outlier robustness without sacrificing smooth convergence.
Start with weighted BCEWithLogitsLoss using pos_weight set to roughly the negative-to- positive ratio (about 99 here) — this makes a missed positive example cost proportionally more than a missed negative, counteracting the model's natural tendency to default toward the majority class. If that is not enough, focal loss goes further by explicitly downweighting easy, already-well-classified examples so the gradient signal concentrates on the hard, informative ones. Either way, accuracy stops being a meaningful metric at this imbalance — a model predicting "negative" for everything scores 99% accuracy while being useless, so evaluation should shift to precision, recall, and PR-AUC instead.
Activations and losses are chosen. Next: how to make the gradient descent step itself smarter.
You now know what a neuron computes (activation functions) and what the network minimises (loss functions). Module 44 covers the final missing piece of the training loop: optimisers. SGD takes the same step size for every weight. Adam adapts the step size per weight based on gradient history. AdamW adds proper weight decay. Momentum accumulates direction. The right optimiser makes training 5–10× faster and more stable.
Momentum, adaptive learning rates, weight decay done right. Why AdamW replaced Adam as the default and when SGD still wins.
🎯 Key Takeaways
- ✓Use ReLU as the default hidden layer activation — fast, sparse, no vanishing gradient for positive inputs. Switch to LeakyReLU if dying neurons are a problem. Use GELU for transformers and modern architectures — it is smooth everywhere and increasingly the default.
- ✓Sigmoid and tanh belong only in specific places: sigmoid at the output layer for binary classification, tanh inside RNNs and LSTMs. Never use sigmoid in hidden layers of deep networks — its maximum derivative of 0.25 causes vanishing gradients.
- ✓Match the loss function to the task exactly: BCEWithLogitsLoss for binary classification, CrossEntropyLoss for multi-class, MSELoss or L1Loss for regression, HuberLoss for regression with outliers. Using MSELoss for classification causes the network to predict class frequencies, not probabilities.
- ✓Never apply sigmoid before BCEWithLogitsLoss or softmax before CrossEntropyLoss. Both losses apply the stable version internally using the log-sum-exp trick. Adding the activation first causes numerical overflow for large logits and produces NaN losses.
- ✓At inference time after CrossEntropyLoss training: apply torch.softmax(logits, dim=1) to get probabilities. After BCEWithLogitsLoss training: apply torch.sigmoid(logits) to get probabilities. During training, pass raw logits to the loss function — never activated outputs.
- ✓For imbalanced classification, use the weight parameter in CrossEntropyLoss (minority class weight = n_majority/n_minority) or pos_weight in BCEWithLogitsLoss. Focal loss is stronger but requires an external library — start with weighted cross-entropy first.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.