Search Tech Journey

Find topics, journeys and posts

6-month learning plan96 / 130
back to blog
mladvanced 55m read

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.

🤖Machine LearningM12 · Deep Learning· Session 096 of 130 90 min

🎯 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.

You will be able to
  • 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

A voting committee with a chairman
🌍 Real world

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.

💻 Code world

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

Memorise this — it is 90% of deep learning
  • 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

  1. 1943
    McCulloch & Pitts
    First mathematical model of a neuron: binary threshold with weighted inputs. No learning yet.
  2. 1958
    Rosenblatt · Perceptron
    Adds 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.’
  3. 1969
    Minsky & Papert · Perceptrons book
    Proves single-layer perceptron cannot learn XOR. Funding collapses. First AI winter.
  4. 1986
    Rumelhart, Hinton, Williams · Backprop
    Multi-layer networks trainable via chain rule. Sigmoid is the activation of choice.
  5. 2010
    ReLU rediscovered · Glorot & Bengio
    Fixes the vanishing gradient problem. Makes very deep networks trainable for the first time.
  6. 2016
    GELU · Hendrycks & Gimpel
    Smoother 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

11958-2010
Sigmoid · φ(z) = 1/(1+e^-z)

S-curve from 0 to 1. Historical default. Vanishing gradient for |z| > ~5. Still used for binary output layer only.

21990s
Tanh · φ(z) = tanh(z)

S-curve from -1 to 1. Zero-centred (nicer than sigmoid). Still suffers vanishing gradient. Used in RNN gates.

32010
ReLU · φ(z) = max(0, z)

Linear for z > 0, zero otherwise. Non-saturating gradient. THE default for hidden layers 2010-2020.

42013
Leaky ReLU · φ(z) = max(αz, z), α~0.01

Fixes ‘dead ReLU’ — small slope on negative side keeps the gradient alive.

52016 → BERT/GPT
GELU · φ(z) = z · Φ(z)

Smooth approximation to ReLU using the Gaussian CDF. Slightly better in practice for transformers.

62017
SiLU/Swish · φ(z) = z · σ(z)

Similar to GELU. Used in EfficientNet, LLaMA. Non-monotonic — has a small negative dip.

When to use which

Sigmoid

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
Tanh

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
ReLU

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
GELU / SiLU

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


Common misconception
✗ What most people think

"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."

✓ What is actually true

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.

Why the myth is so sticky

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.

Prove it to yourself

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.
From first principles
Start with the question

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?

  1. 1
    The 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
  2. 2
    On 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
  3. 3
    Even 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
  4. 4
    A 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
  5. 5
    Therefore 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

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.

Mental modelA weighted vote with an adjustable prejudice

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.
🔔 Fires when you see

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.

The tradeoff

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?

Engineer the features
+ you gain keeps the model linear, so training is convex with a unique global optimum and no initialisation, learning rate, or architecture to tune; coefficients stay interpretable; inference is a dot product; and it works with far less data because you supplied the structure instead of paying to learn it
− you pay requires knowing which transformation exposes the structure, which is domain expertise you may not have; the feature set becomes something a human must maintain as the data evolves; and it does not scale when the useful interactions are numerous or unknown
pick when the structure is known or guessable (a product term, a ratio, a polynomial, a periodicity) and data is limited — for XOR specifically, adding the single feature x₁·x₂ makes it linearly separable immediately
Add a hidden layer
+ you gain the network discovers useful intermediate representations without you naming them, which is the only tractable option when the number of relevant interactions is large or unknown; and the approach generalises to problems where no human could enumerate the right features
− you pay the objective becomes non-convex, so you inherit initialisation sensitivity, learning-rate tuning, and no guarantee of the global optimum; you need substantially more data to learn what you could have specified; and interpretability drops sharply
pick when the interactions are many or unknown, and you have data volume proportional to the capacity you are adding
A fixed non-linear basis (kernel, splines, random features)
+ you gain an expressive non-linear boundary while the optimisation stays convex — you get global optima and stable training with genuine non-linearity, which is precisely why SVMs dominated the pre-deep-learning era
− you pay kernel methods scale poorly with sample count, since the kernel matrix grows quadratically in n; and the basis is fixed rather than learned, so it cannot adapt to the specific structure of your problem the way a trained layer does
pick when moderate dataset size where you want non-linearity without the tuning burden of a network
What a senior engineer actually does

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

Line 17 · RNG.normal(0, 0.5, size=n_features)
Small random weights. Initialising to zero for a perceptron would still work — but for multi-layer networks (S097+) zero init kills training due to symmetry.
init
Line 27 · fit(...) — the perceptron learning rule
err = y - y_hat is either -1, 0, or +1. w += lr * err * xi nudges w toward classifying that xi correctly next time. Guaranteed to converge if the data is linearly separable.
core
Line 55 · xor_data()
The famous impossibility. This dataset is not linearly separable, and no amount of training will get single-layer perceptron above ~75% accuracy.
history
Line 66 · gelu(z) exact formula
This is the tanh-approximation used in most implementations. The ‘exact’ GELU uses erf() which is slightly slower.
activation
Line 80 · demo_activations
Prints each activation's output on the same input z. You'll see saturation (sigmoid clipping to 0/1), ReLU killing negatives, and GELU/SiLU letting a small negative gradient survive.
compare
Line 100 · dead ReLU illustration
A neuron with pre-activation stuck below zero has gradient = 0 for every example, so the weights never update. Fix: leaky ReLU, GELU, or better initialisation.
gotcha
Try itGet XOR to work — by stacking a layer

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.

💡 Hint · This is a preview of session 097 (MLP). One extra layer of two neurons + tanh will solve XOR. The math is the same neuron equation, applied twice.

(d) Production reality · 15 min

War story Deep learning · common failure mode across every frameworkuniversal
🔥 What broke

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.

🧯 The fix
Swap every sigmoid to ReLU. Loss immediately drops. Same architecture, same data — activation function was the only change. This is exactly the finding of Glorot & Bengio (2010) and He et al. (2015) that unlocked very deep networks.
🎓 Lesson to steal
Vanishing gradient is not a bug — it is the mathematical consequence of stacking saturating activations. ReLU/GELU sidestep it by not saturating on the positive side. If your deep net won't train, check activations first, learning rate second, initialisation third.
Post-mortem
War story Google Brain — early Inception experiments· 2014ImageNet-scale training
🔥 What broke
Inception-v1 with ReLU and default weight initialisation had many ‘dead’ neurons — Andrej Karpathy later reported up to 40% of ReLUs never firing on a validation batch. Effective network capacity was half of what the architecture claimed.
🧯 The fix

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.

🎓 Lesson to steal
Choosing ReLU is not free — pair it with He init and monitor the fraction of dead neurons per layer. If more than ~20% are dead, switch to leaky ReLU or GELU.
Post-mortem
War story OpenAI / Google — transformer architecture standardisationevery LLM shipped since 2018
🔥 What broke
Original ‘Attention is all you need’ (2017) used ReLU in the feed-forward blocks. Google's BERT and OpenAI's GPT-2 experimented and both settled on GELU. Later work (PaLM, LLaMA) moved to SwiGLU — a gated linear unit variant of Swish/SiLU.
🧯 The fix

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.

🎓 Lesson to steal
Activation function is a hyperparameter, and the best choice has migrated from sigmoid → tanh → ReLU → GELU → SwiGLU over 40 years. Follow the state of the art — the delta is small per neuron but huge at scale.

Where this shows up in the rest of the plan

The neuron equation is the atom of every deep model to come
S097 · MLP forward pass
Stack the neuron equation twice — solve XOR, then everything.
S098 · Backpropagation
Chain rule through the activation derivative — vanishing gradient explained algebraically.
S099 · Optimisers
SGD, Adam — the update rule for w and b beyond ‘lr × err × xi’.
S103 · RNN & LSTM
Sigmoid + tanh come back inside LSTM gates — one of their few remaining uses.
S105 · Transformers
Feed-forward block = 2 linear layers + GELU. It's literally two of these neurons.
S117 · Fine-tuning LLMs
LoRA freezes the linear part; the activation stays fixed — you'll live in this equation.

(e) Recall + stretch · 10 min

Quick recall · click to reveal
★ = stretch question

Explain-out-loud test

If you can't teach these three to a friend without notes, redo the session:

  1. What does one neuron compute? (write the equation)
  2. Why do neural networks need non-linear activations?
  3. 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.