TAI BUI
Back to curriculum
BuildPython

Activation Functions

Without nonlinearity, a hundred-layer network is a fancy matrix multiply and nothing more. Activation functions are the gates that let it bend into curves and actually learn.

TIME: ~75 minutesPREREQUISITES: Lesson 03.03 (Backpropagation)

Learning Objectives

  • Implement sigmoid, tanh, ReLU, Leaky ReLU, GELU, Swish, and softmax with their derivatives from scratch
  • Diagnose the vanishing gradient problem by measuring activation magnitudes through 10+ layers with different activations
  • Detect dead neurons in a ReLU network and explain why GELU avoids this failure mode
  • Select the correct activation function for a given architecture (transformer, CNN, RNN, output layer)

The Problem

Stack two linear transformations: y = W2(W1x + b1) + b2. Expand it and you get y = W2W1x + W2b1 + b2, which is just y = Ax + c: a single linear transformation. No matter how many linear layers you stack, the result collapses to one matrix multiply. Your 100-layer network has the exact same representational power as a single layer.

That's not a theoretical curiosity. A deep linear network literally cannot learn XOR, cannot classify a spiral dataset, cannot recognize a face. Without activation functions, depth is an illusion.

Activation functions break the linearity. They warp the output of each layer through a nonlinear function, giving the network room to bend decision boundaries, approximate arbitrary functions, and actually learn. Pick the wrong one, though, and something breaks: gradients vanish to zero (sigmoid in deep networks), explode to infinity (unbounded activations without careful initialization), or neurons die permanently (ReLU with large negative biases). The activation function you choose determines whether your network learns at all.

Warm-up0/3
1. During gradient descent, a weight's gradient comes out as exactly 0 on every step. What happens to that weight?Warm-up
A loss curve plotted against one weight that is flat on its right side, with a red dot on the flat part labelled gradient = 0, and a box listing gradient descent steps 1 to 4 and every later step, each with gradient 0.
Illustrative curve; no numbers are claimed.
2. In backpropagation, how does the gradient reaching an early layer depend on the layers after it?Warm-up
A chain of four layers joined by arrows with local derivatives d2, d3 and d4 marked on the links and an arrow to the loss; below it a red arrow runs backward from the loss to layer 1, and the gradient reaching layer 1 is left as a question mark.
3. You multiply a number by 0.9 a hundred times in a row, and separately multiply another number by 1.1 a hundred times. What happens to the two results?Warm-up
Two rows. Each starts from a number and multiplies by a fixed factor three times, then dots, then a final box after 100 multiplications. The first row uses the factor 0.9 and the second row uses 1.1.
Check: The Problem0/1
1. Why can't you build a useful deep network using only linear layers (no activation functions)?Explain
A chain x, layer 1 (W1, b1), layer 2 (W2, b2), layer 3 (W3, b3), y with no function between the layers, and the equation y = W3 (W2 (W1 x + b1) + b2) + b3 underneath.

The Concept

Why Nonlinearity Is Necessary

Matrix multiplication is composable. Multiplying a vector by matrix A then matrix B is identical to multiplying by AB. Stack ten linear layers and the result is mathematically equivalent to one linear layer with one big matrix. All those parameters, all that depth. Wasted. You need something to break the chain. That's what activation functions do.

Here's the proof. A linear layer computes f(x) = Wx + b. Stack two:

Layer 1: h = W1 * x + b1
Layer 2: y = W2 * h + b2

Substitute:

y = W2 * (W1 * x + b1) + b2
y = (W2 * W1) * x + (W2 * b1 + b2)
y = A * x + c

One layer. Insert a nonlinear activation g() between layers:

h = g(W1 * x + b1)
y = W2 * h + b2

Now the substitution breaks. W2 * g(W1 * x + b1) + b2 cannot be reduced to a single linear transformation. The network can represent nonlinear functions. Each additional layer with an activation adds representational capacity.

Sigmoid

Sigmoid squeezes any real number into a smooth, differentiable S-curve: a probability-like output, and the original activation function for neural networks.

sigmoid(x) = 1 / (1 + e^(-x))

Output range: (0, 1). The derivative:

sigmoid'(x) = sigmoid(x) * (1 - sigmoid(x))

The maximum value of this derivative is 0.25, occurring at x = 0. In backpropagation, gradients multiply through layers. Ten layers of sigmoid means the gradient gets multiplied by at most 0.25 ten times:

0.25^10 = 0.000000953674

Less than one millionth of the original signal. This is the vanishing gradient problem. Gradients in early layers become so small that weights barely update. The network appears to learn (loss decreases in later layers), but the first layers stay frozen. Deep sigmoid networks simply do not train.

One more problem: sigmoid outputs are always positive (0 to 1), which means gradients on weights are always the same sign. This causes zig-zagging during gradient descent.

Tanh

Tanh is sigmoid recentered: the same S-curve, but zero-centered, which eliminates the zig-zag problem.

tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x))

Output range: (-1, 1). The derivative:

tanh'(x) = 1 - tanh(x)^2

Maximum derivative is 1.0 at x = 0: four times better than sigmoid. But the vanishing gradient problem still exists. For large positive or negative inputs, the derivative approaches zero. Ten layers still crush the gradient, just less aggressively.

ReLU: The Breakthrough

ReLU (Rectified Linear Unit) zeroes out every negative input and passes positive inputs straight through, unchanged:

relu(x) = max(0, x)

Nair and Hinton popularized it for deep learning in 2010, though the function itself dates to Fukushima's 1969 work.

Output range: [0, infinity). The derivative is trivially simple:

relu'(x) = 1  if x > 0
            0  if x <= 0

No vanishing gradient for positive inputs: the gradient is exactly 1, passed straight through. This is why deep networks became trainable: ReLU preserves gradient magnitude across layers.

But there is a failure mode: the dead neuron problem. If a neuron's weighted input is always negative (due to a large negative bias or unfortunate weight initialization), its output is always zero, its gradient is always zero, and it never updates. It is permanently dead. In practice, 10-40% of neurons in a ReLU network can die during training.

ReLU's output is also unbounded on the positive side, which cuts the other way from vanishing gradients. With careless weight initialization, activations and their gradients can grow layer over layer instead of shrinking, exploding toward NaN. Vanishing and exploding are the same compounding-multiplication mechanism, just pointed in opposite directions: a per-layer multiplier under 1 shrinks a signal to nothing over enough layers, and a multiplier over 1 blows it up.

Leaky ReLU

Leaky ReLU gives the negative side a small slope instead of zero: the simplest fix for dead neurons.

leaky_relu(x) = x        if x > 0
                alpha * x if x <= 0

Alpha is a small constant, typically 0.01. That small slope means dead neurons still get a gradient signal and can recover.

GELU: The Modern Default

GELU (Gaussian Error Linear Unit) weights each input by how likely it is to be positive under a Gaussian distribution: a probabilistic gate instead of a hard cutoff. Hendrycks and Gimpel introduced it in 2016, and it's now the default activation in BERT, GPT, and most modern transformers.

gelu(x) = x * Phi(x)

Where Phi(x) is the cumulative distribution function of the standard normal distribution. The approximation used in practice:

gelu(x) ~= 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))

GELU is smooth everywhere, allows small negative values (unlike ReLU, which hard-clips to zero), and this smooth gating outperforms ReLU in transformer architectures because it provides better gradient flow and avoids the dead neuron problem entirely.

Swish / SiLU

Swish gates each input by its own sigmoid: a self-gated activation that Ramachandran et al. found in 2017 through automated search.

swish(x) = x * sigmoid(x)

Google ran that search over activation function space: a neural network designing parts of neural networks.

Like GELU, it is smooth, non-monotonic, and allows small negative values. The difference is subtle: Swish uses sigmoid for gating while GELU uses the Gaussian CDF. In practice, performance is nearly identical. Swish is used in EfficientNet and some vision models. GELU dominates in language models.

Softmax: The Output Activation

Softmax converts a vector of raw scores (logits) into a probability distribution: every output lands between 0 and 1, and they all sum to 1. It's never used in hidden layers, only as the standard final activation for multi-class classification.

softmax(x_i) = e^(x_i) / sum(e^(x_j) for all j)

The largest logit gets the highest probability, but unlike argmax, softmax is differentiable and preserves information about relative confidence.

Comparison of Shapes

Sigmoid, tanh, ReLU, and GELU plotted on the same axes: sigmoid and tanh flatten at both ends while ReLU and GELU keep climbing, and GELU dips slightly negative where ReLU cannot

Gradient Flow Comparison

Mermaid diagram

Which Activation When

Mermaid diagram

Check: The Concept0/23
1. What is an activation function in a neural network?Define
A single neuron: inputs x1 = 1.0 and x2 = 2.0 with weights 0.5 and -1.0 and a bias of 0.2 feed a sum box giving z = -1.3, which goes through a box labelled g, here the sigmoid, to give an output of 0.214.
Worked with the lesson's sigmoid; the numbers are an example.
2. What is the vanishing gradient problem?Define
A bar chart on a log scale of the norm of the gradient arriving at each of the 10 layers of a sigmoid network, layer 1 to layer 10, measured in one run.
Real run: 8 units per layer, N(0,1) weights, squared error with a linear output unit, first step on the lesson's circle data.
3. What is an exploding gradient?Define
A bar chart on a log scale of the norm of the gradient arriving at each of the 10 layers of a ReLU network with large initial weights, layer 1 to layer 10, measured in one run.
Real run: 16 units per layer, weights drawn with standard deviation 1.5, squared error with a linear output unit, first step on the circle data.
4. What does the sigmoid function 1/(1+e^-x) do to its input?Define
The lesson's sigmoid function in a code window, and a table of five inputs x = -6, -2, 0, 2 and 6 whose sigmoid(x) outputs are left as question marks.
5. What is GELU?Define
A plot of the GELU curve for x from -4 to 4, which is flat near zero on the left, dips slightly below zero and then climbs smoothly, next to a table of gelu(x) at x = -3, -1, 0, 1 and 3 from the lesson's code.
Values from the lesson's gelu() (the tanh approximation).
6. What does softmax compute?Define
Three input scores 2.0, 1.0 and 0.1 for classes A, B and C drawn as bars on the left, an arrow labelled softmax, and the three outputs on the right left as question marks.
7. What is saturation in an activation function?Define
A plot of the sigmoid curve for x from -8 to 8, with no annotations.
Curve from the lesson's sigmoid().
8. What is a logit?Define
A pipeline for a 3-class classifier: one input goes through the network, which produces numbers X = 2.0, 1.0, 0.1; a softmax box follows, which produces numbers Y = 0.659, 0.242, 0.099.
Class scores are an example; the softmax values come from the lesson's softmax().
9. What is the output range of the ReLU activation function?Define
The lesson's relu function in a code window, and a table of five inputs x = -3, -1, 0, 2 and 4 whose relu(x) outputs are left as question marks.
10. What is the 'dead neuron' problem in ReLU networks?Define
A table for one ReLU neuron with weights 0.8 and -0.5 and bias -6.0 on five example inputs, with the weighted sum z, relu(z) and the derivative relu'(z) left as question marks.
Example neuron and inputs chosen for illustration.
11. What is the formula for Leaky ReLU, and what does the alpha parameter typically equal?Define
A plot of ReLU for x from -4 to 4 with the left half shaded and labelled x <= 0, output 0, slope 0, and a note saying Leaky ReLU is a variant of this function.
12. How is Swish/SiLU defined, and where is it commonly used?Define
A plot of the Swish curve for x from -6 to 6, close to zero on the left with a small dip below zero, then rising along a diagonal, next to a table of swish(x) at x = -4, -1, 0, 1 and 4 from the lesson's code.
Values from the lesson's swish().
13. Why did ReLU make deep networks trainable when sigmoid had not?Explain
Sigmoid in blue and ReLU in orange plotted for x from -6 to 6: sigmoid is an S-curve between 0 and 1 and ReLU is zero on the left and a rising diagonal on the right.
14. What is the key advantage of tanh over sigmoid?Explain
Sigmoid in blue and tanh in orange plotted for x from -6 to 6; both are S-curves, sigmoid running from 0 to 1 and tanh from -1 to 1.
15. Sigmoid's derivative never exceeds 0.25. Why does that make deep sigmoid networks hard to train?Explain
A plot of the derivative of the sigmoid function for x from -8 to 8: a bell-shaped bump with its peak of 0.25 marked at x = 0 and values near zero at both ends.
Curve from the lesson's sigmoid_derivative().
16. Vanishing and exploding gradients are described as the same mechanism pointed in opposite directions. What is that mechanism?Explain
A log-scale line chart of the norm of the gradient arriving at each layer against layer number for two runs: a 10-layer sigmoid network in blue, whose gradient is tiny at layer 1 and larger at layer 10, and a 10-layer ReLU network with large weights in orange, whose gradient is largest at layer 1.
Two real runs, first step on the circle data, squared error with a linear output unit, 10 layers; sigmoid with 8 units per layer and N(0,1) weights, ReLU with 16 units and weights of standard deviation 1.5.
17. Why does GELU avoid the dead neuron failure mode that ReLU suffers from?Explain
ReLU as a solid blue line and GELU as a dashed orange line plotted for x from -4 to 4. They are close on the right; on the left ReLU lies on zero and GELU dips slightly below zero before returning toward it.
18. Why is softmax preferred over argmax for turning a final layer's scores into predictions during training?Explain
Three final-layer scores 2.0, 1.0 and 0.1 for classes 0, 1 and 2 drawn as bars, with two arrows from them to an argmax box and a softmax box, each leading to an output left as a question mark.
19. A 10-layer network uses sigmoid in every layer, and each layer's derivative sits at its maximum of 0.25. Using the lesson's arithmetic, how much of the original gradient reaches the first layer?Apply
Ten boxes L1 to L10 each labelled with sigmoid derivative 0.25 beneath them, a red arrow running from layer 10 back to layer 1 labelled backward, and the fraction of the original gradient reaching layer 1 left as a question mark.
20. A 10-layer sigmoid network shows falling loss, yet the first layers' weights barely change. What is the most likely cause?Analyze
Two panels from one real run of a 10-layer sigmoid network: the training loss falling over 1000 epochs, and a log-scale bar chart of how far each of the 10 layers' weights moved from their initial values, layer 1 to layer 10.
Real run: loss 0.355 to 0.199. 8 units per layer, N(0,1) weights, learning rate 0.5, the lesson's circle data, squared error. Bars show the norm of the weight change divided by the norm of the initial weights.
21. In a ReLU network, one hidden unit outputs 0 for every input and its weights stay identical across hundreds of epochs. Why can more training not fix it?Analyze
A table for hidden unit 0 of a ReLU network at epochs 0, 100, 200 and 300: its two input weights and its bias are the same in every row, and its maximum output over the 200 training points is 0.0 in every row.
Real run of the lesson's ActivationNetwork with ReLU, 8 hidden units, bias of unit 0 set to -10 before training.
22. A ReLU network with careless weight initialization produces activations that grow layer over layer until the loss turns NaN. Which diagnosis fits?Analyze
A table of the mean absolute activation at selected layers of a 60-layer ReLU network with N(0,1) weights, rising from 3.4 at layer 1 through very large values to inf at layer 49 and nan from layer 52, plus a small plot of its log10 for layers 1 to 40.
Real numpy run in float32; the width and the weight scale are the setup, not the lesson's values.
23. A 10-layer tanh network trains better than the sigmoid one but still trains slowly. What explains this?Analyze
Training loss over 1500 epochs for two 10-layer networks on the circle data: the sigmoid network in blue stays nearly flat, and the tanh network in orange falls gradually and is still well above zero at the end.
Real run: 8 units per layer, N(0,1) weights, learning rate 0.02, squared error. A toy setup, not the lesson's.

Build It

Step 1: Implement All Activation Functions with Derivatives

Each function takes a single float and returns a float. Each derivative function takes the same input and returns the gradient.

python
import math
 
def sigmoid(x):
    x = max(-500, min(500, x))
    return 1.0 / (1.0 + math.exp(-x))
 
def sigmoid_derivative(x):
    s = sigmoid(x)
    return s * (1 - s)
 
def tanh_act(x):
    return math.tanh(x)
 
def tanh_derivative(x):
    t = math.tanh(x)
    return 1 - t * t
 
def relu(x):
    return max(0.0, x)
 
def relu_derivative(x):
    return 1.0 if x > 0 else 0.0
 
def leaky_relu(x, alpha=0.01):
    return x if x > 0 else alpha * x
 
def leaky_relu_derivative(x, alpha=0.01):
    return 1.0 if x > 0 else alpha
 
def gelu(x):
    return 0.5 * x * (1 + math.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * x ** 3)))
 
def gelu_derivative(x):
    phi = 0.5 * (1 + math.erf(x / math.sqrt(2)))
    pdf = math.exp(-0.5 * x * x) / math.sqrt(2 * math.pi)
    return phi + x * pdf
 
def swish(x):
    return x * sigmoid(x)
 
def swish_derivative(x):
    s = sigmoid(x)
    return s + x * s * (1 - s)
 
def softmax(xs):
    max_x = max(xs)
    exps = [math.exp(x - max_x) for x in xs]
    total = sum(exps)
    return [e / total for e in exps]

Step 2: Visualize Where Gradients Die

Compute the gradient at 100 evenly-spaced points from -5 to 5. Print a text histogram showing where each activation's gradient is near-zero.

python
def gradient_scan(name, derivative_fn, start=-5, end=5, n=100):
    step = (end - start) / n
    near_zero = 0
    healthy = 0
    for i in range(n):
        x = start + i * step
        g = derivative_fn(x)
        if abs(g) < 0.01:
            near_zero += 1
        else:
            healthy += 1
    pct_dead = near_zero / n * 100
    print(f"{name:15s}: {healthy:3d} healthy, {near_zero:3d} near-zero ({pct_dead:.0f}% dead zone)")
 
gradient_scan("Sigmoid", sigmoid_derivative)
gradient_scan("Tanh", tanh_derivative)
gradient_scan("ReLU", relu_derivative)
gradient_scan("Leaky ReLU", leaky_relu_derivative)
gradient_scan("GELU", gelu_derivative)
gradient_scan("Swish", swish_derivative)

Step 3: Vanishing Gradient Experiment

Forward-pass a signal through N layers using sigmoid, ReLU, and GELU. Measure how the activation magnitude changes.

python
import random
 
def vanishing_gradient_experiment(activation_fn, name, n_layers=10, n_inputs=5):
    random.seed(42)
    values = [random.gauss(0, 1) for _ in range(n_inputs)]
 
    print(f"\n{name} through {n_layers} layers:")
    for layer in range(n_layers):
        weights = [random.gauss(0, 1) for _ in range(n_inputs)]
        z = sum(w * v for w, v in zip(weights, values))
        activated = activation_fn(z)
        magnitude = abs(activated)
        bar = "#" * int(magnitude * 20)
        print(f"  Layer {layer+1:2d}: magnitude = {magnitude:.6f} {bar}")
        values = [activated] * n_inputs
 
vanishing_gradient_experiment(sigmoid, "Sigmoid")
vanishing_gradient_experiment(relu, "ReLU")
vanishing_gradient_experiment(gelu, "GELU")

Step 4: Dead Neuron Detector

Create a ReLU network, pass random inputs through it, count how many neurons never fire.

python
def dead_neuron_detector(n_inputs=5, hidden_size=20, n_samples=1000):
    random.seed(0)
    weights = [[random.gauss(0, 1) for _ in range(n_inputs)] for _ in range(hidden_size)]
    biases = [random.gauss(0, 1) for _ in range(hidden_size)]
 
    fire_counts = [0] * hidden_size
 
    for _ in range(n_samples):
        inputs = [random.gauss(0, 1) for _ in range(n_inputs)]
        for neuron_idx in range(hidden_size):
            z = sum(w * x for w, x in zip(weights[neuron_idx], inputs)) + biases[neuron_idx]
            if relu(z) > 0:
                fire_counts[neuron_idx] += 1
 
    dead = sum(1 for c in fire_counts if c == 0)
    rarely_fire = sum(1 for c in fire_counts if 0 < c < n_samples * 0.05)
    healthy = hidden_size - dead - rarely_fire
 
    print(f"\nDead Neuron Report ({hidden_size} neurons, {n_samples} samples):")
    print(f"  Dead (never fired):     {dead}")
    print(f"  Barely alive (<5%):     {rarely_fire}")
    print(f"  Healthy:                {healthy}")
    print(f"  Dead neuron rate:       {dead/hidden_size*100:.1f}%")
 
    for i, c in enumerate(fire_counts):
        status = "DEAD" if c == 0 else "WEAK" if c < n_samples * 0.05 else "OK"
        bar = "#" * (c * 40 // n_samples)
        print(f"  Neuron {i:2d}: {c:4d}/{n_samples} fires [{status:4s}] {bar}")
 
dead_neuron_detector()

Step 5: Comparing Sigmoid, ReLU, and GELU

Train the same two-layer network on the circle dataset (points inside a circle = class 1, outside = class 0) with three different activations. Compare convergence speed.

python
def make_circle_data(n=200, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        x = random.uniform(-2, 2)
        y = random.uniform(-2, 2)
        label = 1.0 if x * x + y * y < 1.5 else 0.0
        data.append(([x, y], label))
    return data
 
 
class ActivationNetwork:
    def __init__(self, activation_fn, activation_deriv, hidden_size=8, lr=0.1):
        random.seed(0)
        self.act = activation_fn
        self.act_d = activation_deriv
        self.lr = lr
        self.hidden_size = hidden_size
 
        self.w1 = [[random.gauss(0, 0.5) for _ in range(2)] for _ in range(hidden_size)]
        self.b1 = [0.0] * hidden_size
        self.w2 = [random.gauss(0, 0.5) for _ in range(hidden_size)]
        self.b2 = 0.0
 
    def forward(self, x):
        self.x = x
        self.z1 = []
        self.h = []
        for i in range(self.hidden_size):
            z = self.w1[i][0] * x[0] + self.w1[i][1] * x[1] + self.b1[i]
            self.z1.append(z)
            self.h.append(self.act(z))
 
        self.z2 = sum(self.w2[i] * self.h[i] for i in range(self.hidden_size)) + self.b2
        self.out = sigmoid(self.z2)
        return self.out
 
    def backward(self, target):
        error = self.out - target
        d_out = error * self.out * (1 - self.out)
 
        for i in range(self.hidden_size):
            d_h = d_out * self.w2[i] * self.act_d(self.z1[i])
            self.w2[i] -= self.lr * d_out * self.h[i]
            for j in range(2):
                self.w1[i][j] -= self.lr * d_h * self.x[j]
            self.b1[i] -= self.lr * d_h
        self.b2 -= self.lr * d_out
 
    def train(self, data, epochs=200):
        losses = []
        for epoch in range(epochs):
            total_loss = 0
            correct = 0
            for x, y in data:
                pred = self.forward(x)
                self.backward(y)
                total_loss += (pred - y) ** 2
                if (pred >= 0.5) == (y >= 0.5):
                    correct += 1
            avg_loss = total_loss / len(data)
            accuracy = correct / len(data) * 100
            losses.append(avg_loss)
            if epoch % 50 == 0 or epoch == epochs - 1:
                print(f"    Epoch {epoch:3d}: loss={avg_loss:.4f}, accuracy={accuracy:.1f}%")
        return losses
 
 
data = make_circle_data()
 
configs = [
    ("Sigmoid", sigmoid, sigmoid_derivative),
    ("ReLU", relu, relu_derivative),
    ("GELU", gelu, gelu_derivative),
]
 
results = {}
for name, act_fn, act_d_fn in configs:
    print(f"\n=== Training with {name} ===")
    net = ActivationNetwork(act_fn, act_d_fn, hidden_size=8, lr=0.1)
    losses = net.train(data, epochs=200)
    results[name] = losses
 
print("\n=== Final Loss Comparison ===")
for name, losses in results.items():
    print(f"  {name:10s}: start={losses[0]:.4f} -> end={losses[-1]:.4f} (improvement: {(1 - losses[-1]/losses[0])*100:.1f}%)")
Check: Build It0/7
1. What does sigmoid_derivative(0) return in the lesson's code?Apply
The lesson's sigmoid and sigmoid_derivative functions in a code window, followed by the call sigmoid_derivative(0).
2. What does relu_derivative(0.0) return in the lesson's code?Apply
The lesson's relu_derivative function, return 1.0 if x > 0 else 0.0, in a code window, followed by the call relu_derivative(0.0).
3. What does leaky_relu(-2.0) return with the default alpha?Apply
The lesson's leaky_relu function with default alpha 0.01 in a code window, followed by the call leaky_relu(-2.0).
4. gradient_scan labels a point near-zero when abs(g) < 0.01 and scans 100 points. What does it report for Leaky ReLU with alpha 0.01?Apply
The lesson's gradient_scan function, with its test abs(g) < 0.01, and leaky_relu_derivative with default alpha 0.01 in a code window, followed by the call gradient_scan with Leaky ReLU.
5. A dead_neuron_detector run with hidden_size=20 finds 3 neurons that never fired. What does the line 'Dead neuron rate' print?Apply
A grid of the 20 hidden neurons numbered 0 to 19 with three outlined in red as never having fired, and the lesson's print line for the dead neuron rate with dead = 3 and hidden_size = 20.
Which three neurons are dead is illustrative; the code line is the lesson's.
6. In dead_neuron_detector with 1000 samples, a neuron fired 40 times. What status does the printout give it?Apply
A panel stating n_samples = 1000 and that one neuron fired c = 40 times, above a code window with the lesson's status line (DEAD if c == 0, WEAK if c < n_samples * 0.05, else OK) and its print line.
7. A gradient scan shows ReLU's derivative is zero on about half of the input range. A colleague concludes that half of the neurons are dead. What is wrong with that conclusion?Analyze
A plot of the ReLU derivative for x from -5 to 5, 0 on the left half and 1 on the right half, the lesson's real gradient scan line reporting 49 healthy and 51 near-zero, and a box quoting a colleague who concludes half the neurons are dead.
Scan output is real, from the lesson's gradient_scan.

Use It

PyTorch provides all of these as both functional and module forms:

python
import torch
import torch.nn as nn
import torch.nn.functional as F
 
x = torch.randn(4, 10)
 
relu_out = F.relu(x)
gelu_out = F.gelu(x)
sigmoid_out = torch.sigmoid(x)
swish_out = F.silu(x)
 
logits = torch.randn(4, 5)
probs = F.softmax(logits, dim=1)
 
model = nn.Sequential(
    nn.Linear(10, 64),
    nn.GELU(),
    nn.Linear(64, 32),
    nn.GELU(),
    nn.Linear(32, 5),
)

Hidden layers in a transformer: GELU. Hidden layers in a CNN: ReLU. Output layer for classification: softmax. Output layer for regression: none (linear). Output layer for probabilities: sigmoid. That's it. Start with these defaults. Change them only when you have evidence.

RNNs and LSTMs use tanh for hidden state and sigmoid for gates, but if you're building from scratch today, you're probably not using RNNs. If neurons are dying in your ReLU network, switch to GELU. Don't reach for Leaky ReLU unless you have a specific reason: GELU solves the dead neuron problem and gives better gradient flow.

Check: Use It0/8
1. Which pairing of default hidden-layer activations is correct?Explain
Two block diagrams with one open slot each: a CNN block of Conv, act ?, Pool, Conv, act ?, Pool, and a transformer block of Attention, Linear, act ?, Linear, where each act ? slot is highlighted.
2. If neurons are dying in your ReLU network, what does the Use It section recommend?Explain
A terminal printout of the dead neuron report for a 20-neuron ReLU layer: the dead, barely alive and healthy counts, and the fire counts of neurons 2, 3, 4 and 11, where neurons 3 and 11 show 0 fires out of 1000 and the status DEAD.
The lesson's detector logic rerun with the biases of neurons 3 and 11 set to -12 to force dead neurons; the counts are real for that setup.
3. logits = torch.randn(4, 5) and probs = F.softmax(logits, dim=1). What is the shape of probs and what do its entries sum to?Apply
A 4 by 5 grid of logits from torch.randn with seed 0, rows labelled sample 0 to 3 and columns labelled as dim 1, 5 class scores, next to a label dim 0, 4 samples, under the code line probs = F.softmax(logits, dim=1).
Values from a real torch run with torch.manual_seed(0).
4. For x = torch.randn(4, 10), what is the shape of the output of the nn.Sequential model (Linear 10 to 64, GELU, Linear 64 to 32, GELU, Linear 32 to 5)?Apply
The nn.Sequential model drawn as boxes in two rows: input x of shape (4, 10), Linear 10 to 64, GELU, Linear 64 to 32, GELU, Linear 32 to 5, and an output box whose shape is a question mark; the intermediate shapes are shown as (4, ?).
5. You are building a regression model that predicts a house price. Which activation should the output layer use?Design
A model diagram: house features go into hidden layers, then an output layer highlighted with its activation left as a question mark, then a single predicted price.
6. You are designing a 10-class image classifier as a CNN. Which choice of hidden and output activations fits the lesson's defaults?Design
A pipeline for a 10-class image classifier: image, Conv with act ?, Conv with act ?, Flatten, Dense with 10 outputs, and an arrow from the Dense layer to a highlighted box reading output activation ?.
7. Your ReLU network trains well and shows no dead neurons. A teammate proposes benchmarking six other activations before going further. What does the lesson's guidance suggest?Design
A loss curve falling over 200 epochs for the lesson's ReLU network on the circle dataset, next to a table giving final loss 0.0102, accuracy 99.5 percent and 0 dead units of 8, and a line saying a teammate proposes benchmarking six other activations.
Real run of the lesson's ActivationNetwork with ReLU, 8 hidden units, learning rate 0.1.
8. You must pick activations for a recurrent cell that has both a hidden state and gates. Which assignment matches the lesson?Design
A diagram of a recurrent cell at one time step: inputs x_t and h_t-1 feed two highlighted boxes, gates and hidden state update, each with its activation left as a question mark, which join in a combine box that outputs h_t.

Ship It

This lesson produces:

  • outputs/prompt-activation-selector.md, a reusable prompt that helps you pick the right activation function for any architecture

Exercises

  1. Implement Parametric ReLU (PReLU) where the negative slope alpha is a learnable parameter. Train it on the circle dataset and compare to fixed Leaky ReLU.

  2. Run the vanishing gradient experiment with 50 layers instead of 10. Plot the magnitude at each layer for sigmoid, tanh, ReLU, and GELU. At which layer does each activation's signal effectively reach zero?

  3. Implement the ELU (Exponential Linear Unit): elu(x) = x if x > 0, alpha * (e^x - 1) if x <= 0. Compare its dead neuron rate to ReLU on the same network.

  4. Build a "gradient health monitor" that runs during training: at each epoch, compute the average gradient magnitude at each layer. Print a warning when any layer's gradient drops below 0.001 or exceeds 100.

  5. Modify the training comparison to use the XOR dataset from Lesson 01 instead of circles. Which activation converges fastest on XOR? Why does this differ from the circle results?

Key Terms

TermWhat people sayWhat it actually means
Activation function"The nonlinear part"A function applied to each neuron's output that breaks linearity, enabling the network to learn nonlinear mappings
Vanishing gradient"Gradients disappear in deep networks"Gradients shrink exponentially through layers when the activation's derivative is less than 1, making early layers untrainable
Exploding gradient"Gradients blow up"Gradients grow exponentially through layers when the effective multiplier exceeds 1, causing unstable training
Dead neuron"A neuron that stopped learning"A ReLU neuron whose input is permanently negative, producing zero output and zero gradient
Sigmoid"Squishes values to 0-1"The logistic function 1/(1+e^-x), historically important but causes vanishing gradients in deep networks
ReLU"Clips negatives to zero"max(0, x): the activation that made deep learning practical by preserving gradient magnitude
GELU"The transformer activation"Gaussian Error Linear Unit, a smooth activation that weights inputs by their probability of being positive
Swish/SiLU"Self-gated ReLU"x * sigmoid(x), discovered through automated search, used in EfficientNet
Softmax"Turns scores into probabilities"Normalizes a vector of logits into a probability distribution where all values are in (0,1) and sum to 1
Leaky ReLU"ReLU that doesn't die"max(alpha*x, x) where alpha is small (0.01), preventing dead neurons by allowing small negative gradients
Saturation"The flat part of sigmoid"Regions where an activation's derivative approaches zero, blocking gradient flow
Logit"The raw score before softmax"The unnormalized output of the final layer before applying softmax or sigmoid

Further Reading

  • Nair & Hinton, "Rectified Linear Units Improve Restricted Boltzmann Machines" (2010) — the paper that introduced ReLU and enabled training of deep networks
  • Hendrycks & Gimpel, "Gaussian Error Linear Units (GELUs)" (2016) — introduced the activation function that became the default for transformers
  • Ramachandran et al., "Searching for Activation Functions" (2017) — used automated search to discover Swish, showing that activation design can be automated
  • Glorot & Bengio, "Understanding the difficulty of training deep feedforward neural networks" (2010) — the paper that diagnosed vanishing/exploding gradients and proposed Xavier initialization
  • Goodfellow, Bengio, Courville, "Deep Learning" Chapter 6.3 (https://www.deeplearningbook.org/) — rigorous treatment of hidden units and activation functions

Code

Ships With