The Story That Started Everything
Can a brain cell be modelled as a mathematical function?
Their answer was yes — and it planted the seed for every neural network, every LLM, every self-driving car that exists today. It all starts with one humble biological cell.
Before building artificial neurons, we need to understand the real one. The biological neuron is the fundamental unit of the nervous system. The human brain contains roughly 86 billion of them, each connected to thousands of others — forming the most complex information-processing system we know of.
Anatomy of a Biological Neuron
A neuron has three functional zones, each with a precise mathematical counterpart in the McCulloch-Pitts model. Understanding this mapping is the entire point of this tutorial.
Each dendrite carries a weighted input → soma sums them → axon fires if the sum exceeds threshold θ.
The McCulloch-Pitts Neuron — The Maths
That's the entire McCulloch-Pitts model. No backprop, no learning — just logic hardwired into biology.
The McCulloch-Pitts neuron computes two things in sequence:
McCulloch and Pitts showed that a network of such neurons could compute any logical function — AND, OR, NOT, XOR. They proved a biological brain cell could, in principle, perform digital logic. This was the first mathematical proof that thought could be computed.
Biological Parts → Model Parts
| Biological Part | Function in Nature | MP Model Equivalent | Modern DL Term |
|---|---|---|---|
| 🌿 Dendrites | Receive signals from other neurons | x₁, x₂, … xₙ (inputs) | Input features |
| 🔗 Synapse | Controls signal strength between cells | w₁, w₂, … wₙ (weights) | Trainable weights |
| 🔵 Soma | Accumulates all incoming signals | z = Σ(wᵢxᵢ) + b | Linear transformation |
| ⚖️ Threshold | Decides if the cell fires | y = step(z − θ) | Activation function |
| ⚡ Axon | Transmits the decision (fire/no fire) | y ∈ {0, 1} | Neuron output |
A Worked Example — AND Gate
Let's wire a McCulloch-Pitts neuron to behave like a logical AND gate. It should fire only when both inputs are 1.
| x₁ | x₂ | z = 1·x₁ + 1·x₂ | z ≥ 2 ? | Output y | AND Truth |
|---|---|---|---|---|---|
| 0 | 0 | 0 | No | 0 | 0 ✓ |
| 0 | 1 | 1 | No | 0 | 0 ✓ |
| 1 | 0 | 1 | No | 0 | 0 ✓ |
| 1 | 1 | 2 | Yes | 1 | 1 ✓ |
By choosing weights = 1 and threshold = 2, we've hardwired logic into biology. Change θ to 1 and you get an OR gate. Flip a weight to −2 and add threshold −1 and you get NOT. McCulloch and Pitts proved all of propositional logic can be expressed this way.
Inhibitory Inputs — The Biological Veto
| Input | Weight | Contribution |
|---|---|---|
| x₁ = 1 | +1 | +1 |
| x₂ = 1 | +1 | +1 |
| z = 2 | Fires (θ=1.5) |
| Input | Weight | Contribution |
|---|---|---|
| x₁ = 1 | +1 | +1 |
| x₂ = 1 | +1 | +1 |
| x₃ = 1 | −10 | −10 |
| z = −8 | Silent |
Python Implementation
Basic McCulloch-Pitts Neuron
import numpy as np
def mcculloch_pitts(inputs, weights, threshold):
"""
A single McCulloch-Pitts neuron.
inputs : list/array of binary inputs [x1, x2, ...]
weights : list/array of synaptic weights [w1, w2, ...]
threshold : firing threshold θ
returns : 1 (fire) or 0 (silent)
"""
inputs = np.array(inputs, dtype=float)
weights = np.array(weights, dtype=float)
z = np.dot(weights, inputs) # soma: weighted sum
y = 1 if z >= threshold else 0 # axon: step activation
return y, z # output + raw sum for inspection
# ── AND gate: w=[1,1], θ=2 ──────────────────────────────────
print("AND Gate Truth Table")
print("-" * 35)
for x1, x2 in [(0,0), (0,1), (1,0), (1,1)]:
y, z = mcculloch_pitts([x1, x2], weights=[1,1], threshold=2)
print(f" x1={x1}, x2={x2} → z={z:.1f} → output={y}")
# ── OR gate: w=[1,1], θ=1 ───────────────────────────────────
print("\nOR Gate Truth Table")
print("-" * 35)
for x1, x2 in [(0,0), (0,1), (1,0), (1,1)]:
y, z = mcculloch_pitts([x1, x2], weights=[1,1], threshold=1)
print(f" x1={x1}, x2={x2} → z={z:.1f} → output={y}")
Object-Oriented Neuron — With Inhibitory Support
import numpy as np
class MPNeuron:
"""McCulloch-Pitts neuron with excitatory and inhibitory inputs."""
def __init__(self, weights, threshold):
self.weights = np.array(weights, dtype=float)
self.threshold = threshold
def fire(self, inputs):
x = np.array(inputs, dtype=float)
z = np.dot(self.weights, x)
return 1 if z >= self.threshold else 0, z
# ── Demo: NAND gate ─────────────────────────────────────────
# NAND = NOT AND → fires on every input combo EXCEPT (1,1)
# weights = [-1,-1], threshold = -1
nand = MPNeuron(weights=[-1, -1], threshold=-1)
print("NAND Gate")
for x1, x2 in [(0,0), (0,1), (1,0), (1,1)]:
y, z = nand.fire([x1, x2])
print(f" ({x1},{x2}) z={z:+.0f} → {y}")
# ── Demo: Inhibitory veto ────────────────────────────────────
# x3 is an inhibitory signal: weight = -10
# Even if x1=x2=1, x3=1 will silence the neuron
veto_neuron = MPNeuron(weights=[1, 1, -10], threshold=1.5)
print("\nInhibitory Veto Test")
tests = [(1,1,0), (1,1,1)] # last: inhibitory fires
for x1, x2, x3 in tests:
y, z = veto_neuron.fire([x1, x2, x3])
label = "🔥 FIRES" if y else "🔕 SILENT"
print(f" x=({x1},{x2},{x3}) z={z:+.0f} → {label}")
Limitations — Why MP Neurons Led to the Perceptron
Frank Rosenblatt (1958) added learnable weights → the Perceptron. Rumelhart, Hinton & Williams (1986) added hidden layers and backpropagation → MLP. LeCun (1989) added convolutions → CNN. Vaswani et al. (2017) added attention → Transformer. Every step is just a more powerful version of what McCulloch and Pitts started with a pencil in 1943.
The Family Tree — From Neuron to GPT
Golden Rules
y = f(Σ wᵢxᵢ + b).
The biology never left the equation.
z ≥ θ, modern networks absorb θ into a learnable bias:
z = Σ(wᵢxᵢ) + b and fire if z ≥ 0. Same maths, cleaner notation.