Discord

5Deep Learning 5.2Deep Learning Fundamentals

5.2.5 Activation Functions

The small non-linear functions between the layers of a network: what the common ones look like, how their gradients behave, and which one to use where.

Why do we need activation functions?

In Tensors, Matrices, and Related Mathematical Functions, we saw that a layer of a network is mostly a matrix multiplication plus a bias: z=Wx+bz = Wx + b. That is a linear operation. If we stack two such layers, we get W2(W1x+b1)+b2W_2(W_1x + b_1) + b_2, which is just another linear function of xx. However many layers we stack, the whole network could still only draw straight lines and flat planes. Multi-Layer Perceptrons shows this collapse happening in code.

An activation function fixes this. It is a small non-linear function applied to every value a layer produces, before the next layer sees it:

a=ϕ(z)=ϕ(Wx+b).a = \phi(z) = \phi(Wx + b).

With a non-linear ϕ\phi between the layers, stacking layers really does add power: the network can bend its decision boundaries and fit curved relationships. This page walks through the activation functions you will actually meet, how their gradients behave (which matters for training), and which one to pick in each part of a network.

You do not need to memorise every formula here. What you should take away is the shape of each function, what goes wrong with the older ones, and the simple rule for choosing.

What you need to know

By the end of this module, you should be comfortable with these ideas:

Idea In one line
Activation function A non-linear function applied to each value after a layer, so stacked layers can learn curved patterns.
Sigmoid Squashes any number into (0, 1); smooth, but its gradient is at most 0.25.
Tanh Squashes into (−1, 1) and is centred on zero; its gradient is at most 1.
ReLU Keeps positive values and turns negatives into 0; cheap, with gradient 1 for positive inputs. The usual default.
Leaky ReLU Like ReLU, but negatives are multiplied by a small slope instead of being set to 0.
GELU A smooth version of ReLU, used in transformer models.
Vanishing gradients Gradients that shrink layer after layer until the early layers barely learn.
Dead ReLU A ReLU unit whose input is always negative, so it outputs 0 and never updates.
Output layer Its activation depends on the task, and in PyTorch it is usually left to the loss function.

That is enough to read and design the networks in this guide.

Learn it properly

This page gives you a guided tour with small experiments. A proper reading adds the details and practice that make the ideas stick, so pair the two.

Pick one main course

The topic is small, so one good chapter is enough. Read it, run its code, and then come back here for the experiments.

Our pick: Dive into Deep Learning, section 5.1

Read the Activation Functions part of section 5.1 in Dive into Deep Learning. It gives the formula for ReLU, sigmoid and tanh, plots each function and its derivative with PyTorch, and is written for exactly this stage. The rest of section 5.1, on why hidden layers help, pairs well with Multi-Layer Perceptrons.

The exercises at the end of the section are worth trying, but the ones that ask for proofs are optional at this level.

If you prefer something else

A quick tour

The rest of this page shows each activation function in a line or two of PyTorch, and then two small experiments that explain why the modern choices won. Run the snippets yourself; they are short.

Where the activation goes

Let us start with one layer and one activation. The layer turns 3 input numbers into 4 values, and the activation is applied to each of those 4 values separately:

INPUTPython
import torch
from torch import nn

torch.manual_seed(0)
layer = nn.Linear(3, 4)
x = torch.tensor([1.0, -2.0, 0.5])

z = layer(x)            # the linear part: Wx + b
a = torch.relu(z)       # the activation, value by value
print(z)
print(a)
[OUTPUT]
tensor([-1.4129, -0.2851, -1.1906,  0.4664], grad_fn=<ViewBackward0>)
tensor([0.0000, 0.0000, 0.0000, 0.4664], grad_fn=<ReluBackward0>)

ReLU kept the one positive value and replaced the three negative ones with 0. The grad_fn part only tells us that PyTorch is tracking these values for backpropagation; you can ignore it for now.

In a whole network, we put an activation after every hidden layer. This is the pattern you will see in almost every small PyTorch model:

INPUTPython
model = nn.Sequential(
    nn.Linear(3, 16),
    nn.ReLU(),          # activation after the first layer
    nn.Linear(16, 16),
    nn.ReLU(),          # activation after the second layer
    nn.Linear(16, 1),   # no activation after the last layer
)

Notice that the last layer has no activation. We will see why in Which activation where.

The five functions

Now we can meet the functions themselves. We will feed the same five inputs to each one:

INPUTPython
z = torch.tensor([-2.0, -0.5, 0.0, 0.5, 2.0])

Sigmoid squashes any number into the range (0, 1):

σ(z)=11+e−z.\sigma(z) = \frac{1}{1 + e^{-z}}.

INPUTPython
torch.sigmoid(z)
[OUTPUT]
tensor([0.1192, 0.3775, 0.5000, 0.6225, 0.8808])

You met it already in logistic regression, where it turns a score into a probability. That is still its main job today.

Tanh (the hyperbolic tangent) has the same S-shape, but squashes into (−1, 1) and is centred on 0:

tanh⁡(z)=ez−e−zez+e−z.\tanh(z) = \frac{e^{z} - e^{-z}}{e^{z} + e^{-z}}.

INPUTPython
torch.tanh(z)
[OUTPUT]
tensor([-0.9640, -0.4621,  0.0000,  0.4621,  0.9640])

ReLU (rectified linear unit) keeps positive values and sets negative ones to zero:

ReLU⁡(z)=max⁡(0,z).\operatorname{ReLU}(z) = \max(0, z).

INPUTPython
torch.relu(z)
[OUTPUT]
tensor([0.0000, 0.0000, 0.0000, 0.5000, 2.0000])

It is almost free to compute, which matters when a network has millions of units.

Leaky ReLU multiplies negative values by a small slope α\alpha instead of setting them to 0. PyTorch's default slope is 0.01:

LeakyReLU⁡(z)={zz>0αzz≤0\operatorname{LeakyReLU}(z) = \begin{cases} z & z > 0 \\ \alpha z & z \le 0 \end{cases}

INPUTPython
import torch.nn.functional as F

F.leaky_relu(z, negative_slope=0.01)
[OUTPUT]
tensor([-0.0200, -0.0050,  0.0000,  0.5000,  2.0000])

GELU (Gaussian error linear unit) is a smooth version of ReLU. It multiplies zz by the probability that a standard normal variable is below zz, written Φ(z)\Phi(z):

GELU⁡(z)=z Φ(z).\operatorname{GELU}(z) = z\,\Phi(z).

INPUTPython
F.gelu(z)
[OUTPUT]
tensor([-0.0455, -0.1543,  0.0000,  0.3457,  1.9545])

For large positive inputs it behaves like ReLU, and for large negative ones it goes to 0, but the corner at 0 is rounded off. Transformer models, such as BERT and the GPT family, use it. You do not need the exact formula of Φ\Phi at this level.

Here are all five together:

Line chart of five activation functions for inputs from minus 4 to 4. Sigmoid rises smoothly from near 0 to near 1, crossing 0.5 at zero. Tanh rises from minus 1 to 1 through the origin. ReLU is flat at 0 for negative inputs and a straight line with slope 1 for positive inputs. Leaky ReLU, drawn with slope 0.1, dips slightly below zero on the left. GELU follows ReLU closely but is smooth near zero and dips a little below zero for small negative inputs.
The five activation functions. Leaky ReLU is drawn with slope 0.1 so the difference from ReLU is visible; PyTorch's default is 0.01.

Their gradients

Training works by following gradients backwards through every layer (Backpropagation explains how). Each time the gradient passes back through an activation, it gets multiplied by that activation's derivative. So the derivative decides whether a useful signal survives the trip:

Function Output range Derivative Largest derivative
Sigmoid (0, 1) σ(z) (1−σ(z))\sigma(z)\,(1-\sigma(z)) 0.25, at z=0z = 0
Tanh (−1, 1) 1−tanh⁡2(z)1 - \tanh^2(z) 1, at z=0z = 0
ReLU [0, ∞) 1 if z>0z > 0, else 0 1
Leaky ReLU (−∞, ∞) 1 if z>0z > 0, else α\alpha 1
GELU about [−0.17, ∞) smooth, close to ReLU's about 1.13
Line chart of the derivatives of the five activation functions for inputs from minus 4 to 4. The sigmoid derivative is a low bump with a peak of 0.25 at zero. The tanh derivative is a taller bump with a peak of 1 at zero, falling towards 0 on both sides. ReLU's derivative is 0 for negative inputs and 1 for positive inputs. Leaky ReLU's derivative is 0.1 on the left and 1 on the right. GELU's derivative dips slightly below zero around minus 1.5, then rises smoothly past 1 before settling at 1.
The derivatives. Sigmoid's is never above 0.25, and both sigmoid and tanh are close to 0 once the input is far from zero. ReLU's is exactly 1 for every positive input.

Two facts in this picture explain almost everything about choosing activations. Sigmoid's derivative is small everywhere, and sigmoid and tanh both go flat () for large inputs. ReLU's derivative, on the other hand, is exactly 1 whenever the unit is active. The next two experiments show what that means in practice.

OptionalWhere the 0.25 comes fromA two-line derivation of the sigmoid's derivative and why its peak is 0.25.1 min readShowHide

Write σ=σ(z)=(1+e−z)−1\sigma = \sigma(z) = (1+e^{-z})^{-1}. By the chain rule,

σ′(z)=e−z(1+e−z)2=11+e−z⋅e−z1+e−z=σ (1−σ).\sigma'(z) = \frac{e^{-z}}{(1+e^{-z})^2} = \frac{1}{1+e^{-z}}\cdot\frac{e^{-z}}{1+e^{-z}} = \sigma\,(1-\sigma).

The product σ(1−σ)\sigma(1-\sigma) of two numbers that add up to 1 is largest when both are 12\tfrac12, which happens at z=0z = 0. So the largest possible value is 12⋅12=0.25\tfrac12\cdot\tfrac12 = 0.25.

Vanishing gradients

Let us see what a small derivative does over many layers. We pass one number through ten sigmoids in a row, then ask PyTorch how much the final output changes when the starting number changes:

INPUTPython
start = torch.tensor(0.5, requires_grad=True)
value = start
for layer in range(10):
    value = torch.sigmoid(value)

value.backward()   # work out the gradient
print(start.grad)
[OUTPUT]
tensor(3.4831e-07)

The gradient is about 0.00000035. Each sigmoid multiplied it by at most 0.25, and 0.25100.25^{10} is already less than one in a million. Now the same with ReLU:

INPUTPython
start = torch.tensor(0.5, requires_grad=True)
value = start
for layer in range(10):
    value = torch.relu(value)

value.backward()
print(start.grad)
[OUTPUT]
tensor(1.)

With ReLU the gradient arrives untouched. In a real network the weights also scale the gradient, but the effect of the activation is the same: with sigmoid in every hidden layer, the early layers of a deep network receive almost no signal and barely learn. This is the vanishing gradient problem, and it is the main reason ReLU replaced sigmoid and tanh in hidden layers. (Good weight initialisation also matters here; it is a later, Silver-level topic.)

Dead ReLUs

ReLU has its own weakness. If a unit's input is negative for every example, it always outputs 0, its derivative is always 0, and its weights never change again. The unit is "dead". Let us build one on purpose, with a large negative bias:

INPUTPython
weight = torch.tensor(1.0, requires_grad=True)
bias = torch.tensor(-5.0, requires_grad=True)
inputs = torch.tensor([-1.0, 0.0, 1.0, 2.0])

outputs = torch.relu(inputs * weight + bias)
outputs.sum().backward()
print(outputs)
print(weight.grad, bias.grad)
[OUTPUT]
tensor([0., 0., 0., 0.], grad_fn=<ReluBackward0>)
tensor(0.) tensor(0.)

Every output is 0 and both gradients are 0, so gradient descent has nothing to work with. Leaky ReLU keeps a small slope for negative inputs, so the same unit still gets a gradient and can recover:

INPUTPython
weight = torch.tensor(1.0, requires_grad=True)
bias = torch.tensor(-5.0, requires_grad=True)

outputs = F.leaky_relu(inputs * weight + bias, negative_slope=0.01)
outputs.sum().backward()
print(weight.grad, bias.grad)
[OUTPUT]
tensor(0.0200) tensor(0.0400)

In practice, a few dead units are normal and harmless. It becomes a problem when many die at once, usually because the learning rate is too high. That is when Leaky ReLU or GELU are worth trying.

Which activation where

Now we can put it together. A network has two kinds of layers, and they follow different rules.

Hidden layers (every layer except the last) need a non-linearity so the network can learn curved patterns:

  • Use ReLU by default. It is fast, and its gradient does not shrink.
  • Try GELU or Leaky ReLU if training is unstable or many units seem to die. GELU is also the standard choice inside transformers.
  • Avoid sigmoid and tanh in the hidden layers of ordinary networks, because of vanishing gradients. You will still see them inside some specialised layers, such as the gates of recurrent networks; those come much later.

The output layer is different. Its job is to produce the kind of answer the task needs, and in PyTorch that job is shared with the loss function:

Task What the last layer outputs Activation on the last layer
Regression (predict a number) One number per target, any size None
Binary classification One raw score per example None in the model; the loss applies sigmoid internally
Multi-label (several yes/no labels) One raw score per label None in the model; the loss applies sigmoid internally
Multiclass (one of KK classes) KK raw scores per example None in the model; the loss applies softmax internally

These raw scores are called logits. Leaving the output activation to the loss is not laziness: combining the two in one function is more numerically stable, as Loss Functions shows. When you want actual probabilities after training, apply torch.sigmoid or torch.softmax yourself.

Softmax deserves a mention here because it is the multiclass version of sigmoid: it turns KK scores into KK probabilities that add up to 1,

softmax⁡(z)k=ezk∑jezj.\operatorname{softmax}(z)_k = \frac{e^{z_k}}{\sum_{j} e^{z_j}}.

It is not used between layers. We look at it as a function in Tensors, Matrices, and Related Mathematical Functions, and at how the loss uses it in Loss Functions.

Activations in PyTorch

PyTorch gives you each activation in two forms, and they compute exactly the same thing:

  • as a module such as nn.ReLU(), which you put inside nn.Sequential or store as an attribute of your model;
  • as a function such as torch.relu(z) or F.relu(z) (with import torch.nn.functional as F), which you call inside your model's forward method.
INPUTPython
relu_module = nn.ReLU()
values = torch.tensor([-1.0, 3.0])

print(relu_module(values))
print(torch.relu(values))
[OUTPUT]
tensor([0., 3.])
tensor([0., 3.])

Use whichever reads better in your code. The module names are the ones you would guess: nn.Sigmoid, nn.Tanh, nn.ReLU, nn.LeakyReLU, nn.GELU. The torch.nn documentation lists the rest; most of them are variations you do not need yet.

Common mistakes

Mistake What happens Fix
Writing nn.ReLU without brackets inside nn.Sequential TypeError: torch.nn.modules.activation.ReLU is not a Module subclass Write nn.ReLU(): the model needs an object, not the class.
No activation between two nn.Linear layers No error, but the two layers act as one linear layer Put an activation after every hidden layer.
ReLU or sigmoid after the last layer of a regression model No error, but ReLU can never predict a negative number and sigmoid never goes above 1 Leave the last layer of a regression model without an activation.
Softmax or sigmoid at the end of a classifier, then nn.CrossEntropyLoss or nn.BCEWithLogitsLoss No error, but the function is applied twice and training becomes slower and less accurate Output raw logits and let the loss apply it; see Loss Functions.
Sigmoid in every hidden layer of a deep network Training stalls, because the early layers barely receive a gradient Use ReLU or GELU in hidden layers.

The last three give no error message at all, which is why they are worth remembering: the code runs, the model just learns badly.

How to practise

  • Work through the Dive into Deep Learning section with its code open, and plot each function and its derivative yourself, as we did above.
  • Change the experiments on this page. How small does the gradient get after 20 sigmoids? What happens to the dead ReLU if you change the bias to −0.5?
  • Once you reach A Simple MLP in PyTorch, train the same network twice, once with nn.Sigmoid() and once with nn.ReLU() in the hidden layers, and compare how fast the training loss falls.

Check yourself

What comes next

  • Loss Functions: how the network's raw scores are turned into a single number that training can reduce, and why the output activation lives inside the loss.

Resources

SourceTitleWhy read it
Dive into Deep Learning5.1 Multilayer Perceptrons: Activation FunctionsOur recommended main reading. Short, with the maths, plots of each function and its derivative, and PyTorch code.
Stanford CS231nNeural Networks Part 1: Commonly used activation functionsClassic course notes on sigmoid, tanh, ReLU and Leaky ReLU, with practical advice.
GoogleMachine Learning Crash Course: Activation functionsA gentle, visual introduction.
StatQuestNeural Networks Pt. 3: ReLU In Action!!!A short video showing how ReLU lets a network bend a straight line.
Dive into Deep Learning5.4 Numerical Stability and Initialization: Vanishing GradientsWhy sigmoid makes deep networks hard to train, in more depth.
PyTorchNon-linear activations in torch.nnThe full list of activation modules, such as nn.ReLU and nn.GELU.

Finished this module? Mark it done.

Comments

Stuck on something, spotted a mistake, or found a resource that helped you? Tell us below.

Loading comments