S096 · Perceptron & Activation Functions
The neuron from first principles — from Rosenblatt's 1958 perceptron to the ReLU/GELU/SiLU zoo of modern deep learning. Learn what a single artificial neuron actually computes, why the choice of activation function is one of the top-3 architecture decisions in a network, and code a perceptron from scratch that learns to classify.
🎯 Derive and code the perceptron from scratch, then explain why every modern network uses ReLU/GELU instead of sigmoid — with a mental model that survives contact with a real transformer.
Why this session exists
You cannot understand deep learning without understanding the neuron. Not the flashy "brain-inspired" analogy — the actual math: a weighted sum, a bias, a non-linear activation, and a gradient-based update rule. This is the LEGO brick from which every model in the rest of the plan is built (MLP, CNN, RNN, transformer). Get this session right and everything downstream slots into place. Skim it and you'll be lost by session 100.
- Write the perceptron equation and its gradient update from memory.
- Explain sigmoid, tanh, ReLU, leaky ReLU, GELU, SiLU/Swish — and pick the right one for a given layer.
- Diagnose ‘dead ReLU’ and ‘vanishing gradient’ in one sentence each.
- Code a working perceptron in ~30 lines of numpy that learns AND, OR, and fails XOR — and explain why.
- State what changed between 1958 and 2015 that made deep networks trainable at all.
Prerequisites
- S050 · NumPy fundamentals — you can do vector/matrix ops without looking things up.
- S092 · Evaluation metrics — you can score a classifier.
- Basic calculus intuition (derivatives, chain rule). If shaky, skim 3Blue1Brown's "Essence of Calculus" chapter 1-3 before this session.
(a) Intuition · 5 min
Imagine a small committee where each member is an expert in one factor. To decide "is this email spam?", the "contains-VIAGRA" expert votes strongly yes, the "sender-is-my-mom" expert votes strongly no, and so on. The chairman weights each vote by how much they trust that expert, adds them up, and applies a threshold: sum above 0 → yes, below 0 → no.
Over time, when the chairman is wrong, they adjust trust in each expert. Trusted more when their vote agreed with the correct answer; trusted less when it didn't.
A perceptron IS this chairman. Each input feature is an expert vote. The weight w_i is how much the perceptron trusts that feature. The bias b is the chairman's baseline (‘tend to say yes even when data is neutral’). The activation function is the threshold that turns the sum into a decision.
The learning rule is exactly the trust adjustment: when the perceptron is wrong, nudge every weight in the direction that would have made the answer right.
The neuron equation you will see everywhere
- z = w · x + b → a weighted sum of inputs plus a bias. Called the ‘pre-activation’.
- a = φ(z) → apply a non-linear activation function φ. This is the neuron's output.
- For binary perceptron: φ = sign(z), a ∈ {−1, +1}. That's Rosenblatt's 1958 version.
- For modern DL: φ = ReLU / GELU / tanh / sigmoid depending on layer role.
- A network is thousands of these neurons stacked in layers. That's it. No magic.
The seven decades that got us here
- 1943McCulloch & PittsFirst mathematical model of a neuron: binary threshold with weighted inputs. No learning yet.
- 1958Rosenblatt · PerceptronAdds the learning rule. Runs on custom hardware. New York Times: ‘the embryo of an electronic computer that will be able to walk, talk, see, write, reproduce itself.’
- 1969Minsky & Papert · Perceptrons bookProves single-layer perceptron cannot learn XOR. Funding collapses. First AI winter.
- 1986Rumelhart, Hinton, Williams · BackpropMulti-layer networks trainable via chain rule. Sigmoid is the activation of choice.
- 2010ReLU rediscovered · Glorot & BengioFixes the vanishing gradient problem. Makes very deep networks trainable for the first time.
- 2016GELU · Hendrycks & GimpelSmoother ReLU. Adopted by BERT, GPT, every major transformer since.
(b) Visual walkthrough · 15 min
One neuron, one picture
Every input has a weight; weights + inputs + bias are summed; sum goes through activation; result is the neuron's output. Now multiply this picture by a hundred million and you have GPT.
The activation function zoo — visualised
S-curve from 0 to 1. Historical default. Vanishing gradient for |z| > ~5. Still used for binary output layer only.
S-curve from -1 to 1. Zero-centred (nicer than sigmoid). Still suffers vanishing gradient. Used in RNN gates.
Linear for z > 0, zero otherwise. Non-saturating gradient. THE default for hidden layers 2010-2020.
Fixes ‘dead ReLU’ — small slope on negative side keeps the gradient alive.
Smooth approximation to ReLU using the Gaussian CDF. Slightly better in practice for transformers.
Similar to GELU. Used in EfficientNet, LLaMA. Non-monotonic — has a small negative dip.
When to use which
Historical only — one use left
- Range: (0, 1) — reads as probability
- Only sensible in the OUTPUT layer of binary classification
- Vanishing gradient for |z| large — kills training in deep hidden layers
- In multi-class output, use softmax instead
Better sigmoid, still saturating
- Range: (−1, 1) — zero-centred
- Used inside LSTM/GRU gates
- Still saturates for large |z|
- Not a good default for feed-forward hidden layers
The default hidden activation for a decade
- Range: [0, ∞)
- Fast to compute, easy to differentiate
- ‘Dead ReLU’ problem: if z stays < 0, gradient is 0, neuron never updates
- Fix: initialise carefully, use Leaky ReLU or GELU
The modern default for transformers
- Smooth, monotonic-ish, non-zero gradient for negative z
- Slightly slower than ReLU, negligibly so on GPU
- BERT, GPT, LLaMA, Gemma — all GELU or SwiGLU variants
- Use in any new architecture unless you have a specific reason
Why the perceptron cannot learn XOR — the picture that killed AI for 15 years
"The perceptron failed on XOR because it was too simple — one neuron isn't enough. Adding more neurons in the same layer would fix it, and modern networks work because they just have many more units."
Width does not help. A layer of perceptrons with no non-linearity between layers still computes a linear function — stack a hundred of them and you can compose the weight matrices into a single matrix, so the whole network collapses to one linear map. XOR is not linearly separable, so no arrangement of linear units, however many, can represent it. What broke the impasse was the non-linear activation, not the neuron count.
The myth is sticky because "more capacity solves it" is a correct instinct almost everywhere else, and because the historical account is usually told as "single-layer perceptrons couldn't do XOR, then multi-layer networks could" — which is true but hides the load-bearing detail. Layers alone are not the fix. A multi-layer network of purely linear units is exactly as powerless as one perceptron, and you can verify that in three lines of matrix algebra. The activation function is doing all the work, and it gets mentioned as an implementation detail.
Collapse a deep linear network into one matrix, then add one non-linearity and watch XOR become solvable:
import numpy as np
W1, W2, W3 = np.random.randn(4,2), np.random.randn(4,4), np.random.randn(1,4)
x = np.random.randn(2, 1)
deep = W3 @ (W2 @ (W1 @ x))
single = (W3 @ W2 @ W1) @ x
print(np.allclose(deep, single)) # True -- 3 layers ARE 1 layer
relu = lambda z: np.maximum(0, z)
nonlin = W3 @ relu(W2 @ relu(W1 @ x))
# now no single matrix reproduces this. that is the whole difference.Why is the perceptron convergence guarantee — it will find a separating boundary in finite steps if one exists — considered a weak result, when a convergence proof is normally the strongest thing you can have?
- 1The theorem is conditional: it guarantees convergence if the data is linearly separable. It says nothing at all about what happens otherwise.forced by · the proof bounds the number of updates using the margin, which only exists when a separating hyperplane exists
- 2On non-separable data the algorithm does not converge, does not degrade gracefully, and does not signal failure — it cycles forever, and where you stop determines which weights you get. So the same code on the same data gives different answers depending on your iteration cap.forced by · the update rule fires on every misclassification, and on non-separable data some point is always misclassified
- 3Even when it does converge, it stops at the first separating hyperplane it stumbles into, which may pass arbitrarily close to the training points. There is no preference for a boundary that leaves room on either side.forced by · the update rule's stopping condition is "zero misclassifications", and it has no term expressing margin
- 4A boundary that barely separates generalises poorly, because a small shift in a new point flips its label. So "converged" and "good" are different properties, and the theorem only promises the first.forced by · generalisation depends on the distance to the boundary, which the objective never mentions
- 5Therefore the algorithm optimises a criterion (zero training errors) that is neither achievable in general nor sufficient when achievable. The fix requires a different objective: maximise the margin (SVM) or minimise a smooth surrogate loss (logistic regression), both of which have a unique optimum whether or not the data separates.forced by · a well-posed learning problem needs an objective with a defined optimum on all inputs, not just the easy ones
Therefore the perceptron's guarantee is weak because it is conditional on the easy case and silent on quality. The lasting lesson is that convergence to something is not the same as convergence to something good.
And note what this predicts: because the perceptron uses a hard step function, its output has zero derivative everywhere it is defined and is undefined at the threshold — so gradient-based training is impossible and the update rule had to be hand-crafted. That is precisely why the step function had to be replaced by sigmoid, tanh, or ReLU before backpropagation could exist. The activation choice was not an accuracy tweak; it was the precondition for having gradients at all.
A perceptron is a voter. Each input casts a vote, each weight is how much that voter is trusted, and the bias is the voter's standing prejudice — how far the total must tip before they say yes. Sum the weighted votes, compare to a threshold, output a decision.
Geometrically that sum-and-threshold is exactly a hyperplane: the weight vector is the normal direction, and the bias slides the plane along it. Learning is rotating and sliding one flat surface until the classes fall on opposite sides. Everything a single perceptron can and cannot do follows from the fact that it only ever owns one flat surface.
- One perceptron = one hyperplane. If the classes cannot be split by a flat cut in that space, no weights exist that will work.
- The bias is not optional. Without it every boundary must pass through the origin, which is a severe and arbitrary restriction.
- The learning rule is error-driven: on a mistake, push the weights toward the input for a false negative and away for a false positive. Correct predictions produce no update at all.
- Without a non-linear activation, depth is an illusion — composed linear maps are one linear map. The activation is what makes layers mean anything.
Fire this the moment you see: a network built with no activation functions or with linear activations throughout · someone adding width to fix a representational failure · a claim that a model "can't learn" a pattern that is not linearly separable in the given features · a training loop that never converges on noisy labels · an argument about whether the bias term matters.
Your data is not linearly separable. Do you engineer features that make it separable, or add a hidden layer and let the network learn the representation?
On tabular data with a known or guessable structure, engineering the feature nearly always wins — one multiplicative term beats a hidden layer on XOR in every respect: fewer parameters, convex training, an interpretable model, and it works with a handful of examples. The instinct to reach for depth first is usually the expensive answer to a cheap problem.
Depth earns its cost when the representation genuinely cannot be written down — pixels, audio, text — where the useful features are hierarchical and no human can enumerate them. The general rule that survives from the perceptron era: a linear model in the right space beats a non-linear model in the wrong one. Deep learning did not repeal that; it automated the search for the right space, and only when you have the data to pay for the search.
(c) Hands-on · 25 min
Build a perceptron from scratch in numpy that learns AND, OR, and demonstrably fails on XOR. Save as perceptron_lab.py, uv run perceptron_lab.py.
"""perceptron_lab.py — the perceptron from first principles.
Trains on:
1. AND — should reach 100% accuracy quickly.
2. OR — same.
3. XOR — will bounce around near 50% forever. This is the whole point.
Then swaps in a variety of activation functions and shows what each output looks like.
"""
from __future__ import annotations
import numpy as np
RNG = np.random.default_rng(42)
class Perceptron:
"""Rosenblatt perceptron. sign activation, perceptron learning rule."""
def __init__(self, n_features: int, lr: float = 0.1) -> None:
self.w = RNG.normal(0, 0.5, size=n_features)
self.b = 0.0
self.lr = lr
def predict(self, X: np.ndarray) -> np.ndarray:
z = X @ self.w + self.b
return np.where(z >= 0, 1, 0)
def fit(self, X: np.ndarray, y: np.ndarray, epochs: int = 20) -> list[float]:
"""Returns accuracy per epoch."""
history = []
for _ in range(epochs):
for xi, yi in zip(X, y, strict=False):
y_hat = 1 if (xi @ self.w + self.b) >= 0 else 0
err = yi - y_hat
self.w += self.lr * err * xi
self.b += self.lr * err
acc = float((self.predict(X) == y).mean())
history.append(acc)
return history
def and_data():
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
y = np.array([0, 0, 0, 1])
return X, y
def or_data():
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
y = np.array([0, 1, 1, 1])
return X, y
def xor_data():
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
y = np.array([0, 1, 1, 0])
return X, y
# --- activation function zoo ---
def sigmoid(z): return 1.0 / (1.0 + np.exp(-z))
def tanh(z): return np.tanh(z)
def relu(z): return np.maximum(0.0, z)
def leaky_relu(z, a=0.01): return np.where(z > 0, z, a * z)
def gelu(z):
# Exact GELU: z * Φ(z), Φ = standard normal CDF
from math import sqrt
return 0.5 * z * (1.0 + np.tanh(sqrt(2.0 / np.pi) * (z + 0.044715 * z ** 3)))
def silu(z): return z * sigmoid(z)
def demo_activations() -> None:
z = np.linspace(-3, 3, 7)
print("\nActivation values on z =", z.round(2).tolist())
for name, fn in [("sigmoid", sigmoid), ("tanh", tanh), ("relu", relu),
("leaky_relu", leaky_relu), ("gelu", gelu), ("silu", silu)]:
vals = fn(z)
print(f" {name:<10s}: {np.round(vals, 3).tolist()}")
def train_and_report(name: str, data_fn) -> None:
X, y = data_fn()
p = Perceptron(n_features=2, lr=0.1)
hist = p.fit(X, y, epochs=30)
print(f"\n{name}: final accuracy = {hist[-1]:.2f} (final w={p.w.round(2)}, b={p.b:.2f})")
print(f" accuracy per epoch (first 10): {[round(h,2) for h in hist[:10]]}")
if __name__ == "__main__":
print("=== single-layer perceptron on 3 problems ===")
train_and_report("AND", and_data)
train_and_report("OR", or_data)
train_and_report("XOR", xor_data)
demo_activations()
print("\n=== dead ReLU illustration ===")
# A neuron whose pre-activation is very negative for all inputs → gradient = 0 forever
x = np.array([1.0, 2.0, 3.0])
w = np.array([-1.0, -1.0, -1.0])
b = -10.0
z = x @ w + b
print(f" z = {z:.2f}, relu(z) = {relu(z):.2f}, gradient of relu at z<0 = 0")
print(" If z stays below zero for every input the neuron will never learn — the dead ReLU problem.")Anatomy of the script
Anatomy of the script
Write a MLP_XOR class with:
- 2 input → 2 hidden neurons (tanh activation)
- 2 hidden → 1 output neuron (sigmoid activation)
- Fixed weights (no training yet):
W1 = [[1, 1], [1, 1]],b1 = [0, -1],W2 = [1, -2],b2 = 0
Compute the output on all 4 XOR inputs by hand. You should get {(0,0)→0, (0,1)→~1, (1,0)→~1, (1,1)→0}. Two layers + non-linearity = XOR solved. That's the entire deep-learning revolution in one exercise.
(d) Production reality · 15 min
Team trains a 50-layer ResNet with sigmoid activations. Loss plateaus after epoch 3. Investigation shows the gradients at layer 40+ are essentially zero — the sigmoid saturates and its derivative is at most 0.25. Multiply that by 40 layers of chain-rule and the gradient at the first layer is 4×10^-25. No learning is happening past layer 5.
Two fixes, stacked: (1) He initialisation (weights ~ N(0, √(2/n_in))) instead of Xavier — matches ReLU's zero-negative-half; (2) Leaky ReLU or GELU, which have non-zero gradient for negative inputs so a dead neuron can revive.
Standard practice today: GELU for standard transformer FFN blocks, SwiGLU for modern LLMs (LLaMA, Mistral, Gemma). The improvement is small (~0.3-1 perplexity point on language modelling) but consistent and compounds over billions of parameters.
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
If you can't teach these three to a friend without notes, redo the session:
- What does one neuron compute? (write the equation)
- Why do neural networks need non-linear activations?
- Why did ReLU beat sigmoid?
What comes next
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.