Learning systems from first principles
The artificial neuron, backpropagation and credit assignment
A network learns when each parameter receives not a share of visible activity, but a sensitivity signal estimating how a tiny local change would alter the final error.
TLDR
- An artificial neuron transforms inputs, but its activation alone says nothing about how its weights should change.
- Backpropagation assigns path-conditioned credit: the loss sensitivity at a parameter is composed through every downstream transformation.
- Reverse-mode differentiation reuses one forward computation to obtain gradients for many parameters, provided the matching intermediate state is available.
- Depth, saturation, poor scale, severed paths and stale state can erase or distort credit even when the forward output looks plausible.
- Before changing data or architecture, audit gradient reach, directional agreement, update scale and forward-backward freshness.
The neuron that fired and did not deserve the blame
Two hidden neurons receive the same input. Both emit the value 0.8. The first is connected strongly to the wrong output. The second is disconnected from it. The prediction fails. Which neuron should learn?
The tempting answer is that both should change because both were equally active. That answer treats activity as responsibility. It overlooks the route from each activity to the measured error. If the second neuron has no downstream influence, changing it infinitesimally cannot change this loss. Its activation may be large, interesting or interpretable, yet its gradient is zero.
Now reconnect it with a negative weight. Its activation remains 0.8, but the sign of the appropriate update can reverse. Nothing about the neuron’s local output tells us this. The answer lives in the whole path from that neuron to the objective.
The central claim of this paper is that neural credit is not assigned to units by how much they fire. It is assigned to parameters by their path-conditioned sensitivity to a chosen loss. Backpropagation is the efficient procedure that computes this field of sensitivities. Understanding it changes how we debug training, interpret gradients and compare artificial learning with biological learning.
From a pulse to a trainable transformation
Begin with a small machine. It receives measurements x1, x2, …, multiplies them by adjustable weights, adds a bias and passes the result through a response curve. For one unit, the computation is:
This form descends from several different historical projects. McCulloch and Pitts used idealised threshold elements to analyse logical relations in nervous activity.[1] Rosenblatt’s perceptron joined a model of adaptive connections to classification.[2] Widrow and Hoff’s adaptive circuits used an error-driven least-mean-squares rule.[3] These are not interchangeable biological claims. Their common engineering move is to make a transformation depend on adjustable coefficients.
A neuron, however, does not learn merely because its coefficients are adjustable. It also needs a criterion. Suppose the unit predicts a probability p and the desired label is y. A loss function turns their mismatch into one scalar. An update rule must then answer a sharper question: which small parameter change would reduce that scalar?
For a single output unit the answer can be local. Under logistic output and binary cross-entropy, the sensitivity of loss to the pre-activation simplifies to p − y. The input-weight sensitivity is that error signal multiplied by the input. A feature with value zero receives no update on that example. A feature with the opposite sign receives the opposite update. The rule already depends on more than activity.
A complete update you can calculate by hand
Give the unit two inputs, x = [2, −1], weights w = [0.3, −0.2] and bias b = −0.1. The pre-activation is 0.7 and the logistic probability is about 0.668. The label is one, so binary cross-entropy is about 0.403. The output sensitivity is p − y = −0.332.
Multiplying by the inputs gives weight gradients [−0.664, +0.332]. Notice the second sign. Its input is negative, so increasing that weight would reduce the pre-activation; the loss instead asks the weight to become more negative. With learning rate 0.1, the updated weights are approximately [0.3664, −0.2332] and the bias becomes −0.0668.
On the same example the new pre-activation is 0.8992, the probability rises to 0.711 and loss falls to 0.341. The update is intelligible only when error, input sign and response derivative are read together. The neuron’s activation did not contain the update. The loss supplied direction, while the input supplied local leverage.
Why hidden units create the real puzzle
A single-layer classifier has a visible target at its output. A hidden unit does not. No label says that hidden unit 7 should emit 0.43. Its usefulness depends on what later layers do with it. This is the multilayer credit-assignment problem.
Rumelhart, Hinton and Williams made the practical neural-network procedure widely visible: repeatedly adjust connection weights to reduce the difference between actual and desired outputs.[5] The deeper mathematical device is reverse accumulation. Linnainmaa had described an algorithm that stores local derivative information during a computation and processes it in reverse to determine how local errors influence a final result.[4] In modern terms, backpropagation is reverse-mode automatic differentiation specialised to a network and its loss.
flowchart LR A["The neuron that fired and did<br/>not deserve the blame"] --> B["From a pulse to a trainable<br/>transformation"] B --> C["Responsibility travels through<br/>the path"] C --> D["When the backward field collapses"] D -. "re-test" .-> A classDef first fill:#0a1424,stroke:#087A98,stroke-width:2px,color:#e6f3ff classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff class A first class B,C,D step
Responsibility travels through the path
Imagine three adjustable dials in a row. The first changes a signal that enters the second. The second changes what reaches the third. The third determines the measured output. Turning the first dial matters only through the transformations after it. The chain rule is the bookkeeping law for this dependence.
For a two-layer scalar chain, let h = f(x, w1), let ŷ = g(h, w2), and let L(ŷ, y) measure error. Then:
Each factor is local. Their product is not. It expresses the influence of an early parameter through the whole downstream route. With many branches, contributions from all routes add. With vectors, each local factor becomes a Jacobian or a vector-Jacobian product. The principle stays the same.
For a layer with many outputs, writing the full Jacobian is usually wasteful. The backward sweep already possesses a row vector describing how the loss changes with the layer’s outputs. Multiplying that vector by the local Jacobian produces the sensitivity of the layer’s inputs. This vector-Jacobian product computes exactly the slice needed by the loss. It avoids materialising a matrix whose unused rows would describe other hypothetical output perturbations.
This also explains why branches add rather than compete for a fixed budget. If one hidden state affects the loss through three routes, a tiny change perturbs all three. The total first-order effect is the sum of the route effects. “Credit” is therefore not a conserved quantity divided among neurons. Two parameters may both have large gradients, or several routes may cancel to nearly zero.
When two correct routes partly cancel
Let one hidden value feed two outputs. The downstream loss sensitivities are 0.4 and −0.3. The connecting weights from the hidden value are 2 and 1. The hidden sensitivity is therefore 2 × 0.4 + 1 × (−0.3) = 0.5.
If the second branch were removed, the sensitivity would be 0.8. Adding a useful route reduced the net gradient because the current objectives disagree about which direction the hidden value should move. A small gradient here need not mean that the state is irrelevant or that transport failed. It may mark local cancellation among genuine downstream demands.
This distinction matters in multi-task learning. Per-layer gradient norms can reveal weak reach, but diagnosing cancellation requires examining route-specific gradients or their cosine agreement. The credit integrity profile therefore begins an investigation; it does not replace the graph and objective that generated the signal.
A two-weight chain with every factor exposed
Take input x = 2. The first weight is w1 = 3, so the hidden value is h = 6. The second weight is w2 = 0.5, so the prediction is ŷ = 3. The target is 4, and the squared loss is L = ½(ŷ − y)2 = 0.5.
The second weight receives gradient (3 − 4) × 6 = −6. The first receives (3 − 4) × 0.5 × 2 = −1. Both gradients point towards increasing their weights because an update subtracts the negative gradient. Yet the magnitudes differ because the two parameters sit in different positions and see different path factors.
Reverse mode is an economical reuse of the graph
A network may contain millions or billions of parameters but usually produces one scalar loss per batch. Finite differences would perturb each parameter, rerun the network and estimate the loss change. That costs roughly one extra forward evaluation per parameter and introduces a step-size approximation. Reverse mode instead computes the loss once, sets its derivative to one, and sweeps backwards. At each operation it combines the incoming sensitivity with the operation’s local derivative.
The sweep is efficient because a downstream sensitivity is reused by every parameter feeding that downstream state. This is why automatic-differentiation systems record a computational graph or retain the values needed to reconstruct local derivatives. The idea is visible in Linnainmaa’s reverse processing of saved operations and partial derivatives.[4]
What “responsibility” means here
The derivative has a counterfactual reading: hold the batch, objective and other parameters fixed, then ask how an infinitesimal change in this parameter would alter the loss. That is useful, but narrower than causal responsibility in a scientific explanation. A zero derivative can occur at a saturated unit or saddle even when a large intervention would help. A large gradient can arise because the chosen loss weights an example heavily, not because the feature caused the real-world outcome.
One parameter can collect credit from many uses
So far, each arrow has looked as though it owns a separate parameter. Real networks reuse parameters. A convolutional kernel is applied at many image locations. A recurrent weight is applied at many time steps. A mini-batch applies the same model to many examples. The computation graph therefore contains several uses of one stored value.
Suppose one scalar weight appears twice in a toy prediction, ŷ = wx₁ + wx₂. With squared error, the backward pass sends one contribution through each use and adds them at the shared parameter:
Set x₁ = 3 and x₂ = −2. The first use argues three units in one direction; the second argues two units in the other. The stored weight receives their sum. If the inputs were 3 and −3, the net gradient would be zero even though both paths carried large, opposing contributions. Cancellation is not the absence of credit. It is disagreement hidden by aggregation.
The same issue appears across examples. For a batch of size B, training usually averages B per-example gradients. A near-zero mean may indicate that every example is locally satisfied. It may instead conceal two subgroups demanding opposite changes. In a convolution, one edge may urge a kernel coefficient upward while another urges it downward. In a recurrent network, an early time step and a late time step may oppose each other through the same transition matrix.
This suggests a practical diagnostic before changing the optimiser: decompose the gradient by example, subgroup, spatial use or time step, then inspect both the sum and its dispersion. Large opposing components point to objective conflict, representational entanglement or a badly mixed batch. Small components everywhere point to weak reach or local saturation. The decomposition remains objective-relative, but it reveals a fact the final gradient norm erases: whether a quiet parameter is irrelevant, already adequate, or caught between incompatible demands.
When the backward field collapses
The chain rule is exact for the differentiable computation it describes. Exactness does not guarantee usefulness. The returned signal may be too small, too large, wrongly directed by a coding error, or correct for an old forward pass. Credit can fail in transport even when the final loss is perfectly well defined.
Four common ways credit transport fails
Saturation flattens the local derivative. A logistic unit driven far from zero emits a value near zero or one, where its derivative is tiny. Glorot and Bengio showed how activation choice and initialisation can push deep networks into regimes with poor gradient transport and long plateaux.[7] Rectifier-aware initialisation was later derived to preserve signal scale more effectively for rectified networks.[8]
Hard gates can sever a path. A ReLU below zero has derivative zero. An argmax, discrete lookup or external tool call may have no ordinary derivative at all. The forward system can still produce a value, but exact backpropagation cannot pass sensitivity through that operation without a surrogate, estimator or redesigned boundary.
Repeated Jacobians can explode. A direction expanded slightly at each layer can become enormous. The gradient may be mathematically correct but numerically unusable. Clipping limits update magnitude, while architectural routes such as residual connections create shorter derivative paths. Residual networks were introduced partly because substantially deeper plain networks were difficult to optimise.[10]
The correct derivative can be paired with the wrong state. If a backward pass consumes activations from another batch, parameter version or stochastic draw, it computes a derivative for a computation that did not produce the declared loss. Frameworks usually manage this tape. Custom asynchronous or distributed systems must make the invariant explicit.
Two neurons that can never become different
Create two hidden units with identical incoming weights, identical biases and identical outgoing weights. On every deterministic example they produce the same activation. Their downstream sensitivities are also identical, so backpropagation gives them identical gradients. After the update they remain identical. Repeating the process preserves the symmetry indefinitely.
The network may contain two parameter sets on paper, yet they implement one feature in practice. Adding more such units does not add effective representational directions. Random initial differences normally break this symmetry. The example exposes an important limit: a perfectly correct gradient can preserve an unhelpful invariant built into the initial state.
A gradient check passes because the derivatives are right. Reach is non-zero and freshness is intact. The failure appears in diversity across parameter trajectories. This is why the credit audit must be interpreted alongside the configured system. Correct credit answers how to move from the current point; it does not guarantee that the point makes all useful directions accessible.
The loss meter decides what counts as error
Credit is also objective-conditioned. Consider a synthetic inspection classifier whose missed defects are assigned eight times the loss weight of normal examples. For a positive case predicted at probability 0.2, the logit sensitivity is 8 × (0.2 − 1) = −6.4. For a negative case at the same probability it is +0.2. The positive example launches a signal 32 times larger in magnitude.
The network did not discover that the hidden units are morally more responsible for defects. The designer declared a cost asymmetry. Backpropagation faithfully transports it. Change the class weight, add an auxiliary loss or optimise calibration instead of accuracy, and the entire responsibility field changes without changing the initial forward activations.
The biological boundary
The artificial neuron is a computational primitive, not a compressed biological neuron. Exact backpropagation normally uses a global objective, stored forward state and backward transformations related to forward weights. That does not show that cortex implements the same algorithm.
Experiments with feedback alignment demonstrate that exact symmetric backward weights are not required for useful learning on every task: fixed random feedback can, under some conditions, become aligned with the forward system.[11] Direct feedback alignment sends random projections of output error directly to hidden layers and also learns on several benchmark settings.[12] More recent work proposes local spatio-temporal approximations in dynamical neuronal networks.[13]
These results weaken one claim, that exact weight transport is the only route to multilayer credit. They do not establish which mechanism a brain uses. The correct comparison is experimental: identify the teaching signal, locality constraints, timing and predicted perturbation response, then test them. Engineering success and biological implementation remain separate evidential ledgers.
stateDiagram-v2 [*] --> Claim state "A complete update you can<br/>calculate by hand" as Claim state "Why hidden units create the<br/>real puzzle" as Test state "When two correct routes partly<br/>cancel" as Revision Claim --> Test: expose mechanism Test --> Decision: evidence survives Test --> Revision: rival survives Revision --> Claim: narrow or rebuild Decision --> [*]
Audit the signal before changing the system
When a network fails to learn, teams often jump to more data, more width or a different optimiser. Those changes can hide a broken backward path. A smaller diagnostic question comes first: did the intended credit reach each trainable block, in the right direction, at a usable scale, from the correct forward pass?
The following practitioner synthesis turns that question into a four-part credit integrity profile. It is not a new learning algorithm. It is a decision instrument for deciding whether training evidence is interpretable.
Reach
Record norms of ∂L/∂hℓ and ∂L/∂θℓ by layer. A sudden zero can reveal a detached tensor, dead gate or unused branch. Smooth exponential decay suggests transport through derivatives below one. Norms alone do not prove correctness, but they show whether any first-order signal arrived.
Directional agreement
Choose a random unit direction v in parameter space. Compare the analytical directional derivative g · v with a centred finite difference:
Run the check on a small deterministic network in double precision. Large disagreement points to a wrong derivative, indexing error, mutation or unsuitable ε. A gradient check is slower than backpropagation, which is why it is a diagnostic, not the training method.
Scale
Compare the proposed step with the parameter scale, for example η‖gℓ‖/(‖θℓ‖ + ε). There is no universal correct band, but orders-of-magnitude differences across layers are actionable. A correct direction with a negligible step produces apparent stasis. An excessive step can increase the loss even though the gradient is right.
Freshness
Bind the loss, tape, batch identity, stochastic seed and parameter version to one pass identifier. In synchronous code this can be implicit. In pipeline parallelism, gradient accumulation, rematerialisation or custom distributed training it should be testable. The invariant is simple: the backward pass must differentiate the computation that produced the declared loss.
Build a 2 → 8 → 8 → 1 network without a machine-learning framework
The included NumPy program constructs every forward value and gradient explicitly. It trains on a synthetic quadrant-XOR task, checks one analytical derivative against a centred finite difference, rejects a stale activation tape and compares four credit conditions.
Run python paper34_credit_assignment_lab.py with Python 3.10 or later and NumPy installed. The full script is embedded below and is also available as a .
| Condition | Final loss | Training accuracy | ‖∇W₁‖ | Interpretation |
|---|---|---|---|---|
| Exact | 0.020271 | 0.998 | 0.00184 | Path-conditioned credit reaches both hidden layers. |
| Severed | 0.691194 | 0.529 | 0 | Only the output layer updates; the representation stays almost linear. |
| Reversed | 0.691646 | 0.529 | 0.00263 | A non-zero signal is present, but its hidden-layer direction is wrong. |
| Saturated | 0.255021 | 0.891 | 0.00576 | Exact gradients exist, but large sigmoid pre-activations create poor transport. |
The finite-difference check reports a relative error of 2.291 × 10−9 for the tested parameter. That supports the local analytical derivative. The freshness test deliberately pairs pass 1 with an expected pass 2 and correctly raises an error.
Read the four runs as interventions, not rankings. The severed condition isolates reach by setting hidden gradients to zero. The reversed condition preserves non-zero magnitudes but changes direction. Both remain near the binary-cross-entropy value expected from an uninformative balanced classifier. This contrast shows why “there is a gradient” is weaker than “there is useful credit”.
The saturated run changes activation and initial scale, so it is not a single-variable comparison with the exact run. Its purpose is diagnostic: exact backpropagation can coexist with weak local derivatives and slow improvement. The exact curve also rises briefly around step 700 under the deliberately simple fixed learning rate. A gradient is a local slope. A finite step can cross into a worse region, especially when curvature changes along the move.
A negative lab result has several meanings. If finite differences disagree, fix the implementation before tuning. If they agree but reach collapses, inspect gates and paths. If reach and alignment pass but steps are badly scaled, test initialisation or optimisation. If all four checks pass and loss still resists, the objective, representation or data may be the limiting factor. The audit orders the investigation without pretending to finish it.
Open the full runnable Python implementation
"""Credit-assignment lab for Paper 34.
Builds a small multilayer network with NumPy only, implements the forward
pass and backpropagation by hand, checks the analytical gradient against a
finite difference, and compares four credit conditions on synthetic XOR data.
Run:
python paper34_credit_assignment_lab.py
Requires:
Python 3.10+
numpy
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, Iterable, Literal, Tuple
import numpy as np
Mode = Literal["exact", "severed", "reversed", "saturated"]
def stable_sigmoid(x: np.ndarray) -> np.ndarray:
"""Numerically stable logistic function."""
positive = x >= 0
out = np.empty_like(x, dtype=np.float64)
out[positive] = 1.0 / (1.0 + np.exp(-x[positive]))
exp_x = np.exp(x[~positive])
out[~positive] = exp_x / (1.0 + exp_x)
return out
def binary_cross_entropy_with_logits(logits: np.ndarray, y: np.ndarray) -> float:
"""Mean binary cross-entropy, computed without unstable log(sigmoid(.)) calls."""
terms = np.maximum(logits, 0.0) - logits * y + np.log1p(np.exp(-np.abs(logits)))
return float(np.mean(terms))
def make_xor(n: int = 512, seed: int = 7) -> Tuple[np.ndarray, np.ndarray]:
"""Synthetic quadrant-XOR dataset. No external or client data is used."""
rng = np.random.default_rng(seed)
x = rng.uniform(-1.0, 1.0, size=(n, 2))
y = ((x[:, 0] * x[:, 1]) > 0.0).astype(np.float64).reshape(-1, 1)
return x, y
@dataclass
class Tape:
"""Values saved from one forward pass and consumed by its matching backward pass."""
pass_id: int
x: np.ndarray
z1: np.ndarray
h1: np.ndarray
z2: np.ndarray
h2: np.ndarray
logits: np.ndarray
class TinyMLP:
"""A 2 -> 8 -> 8 -> 1 network with manually implemented backpropagation."""
parameter_names = ("W1", "b1", "W2", "b2", "W3", "b3")
def __init__(self, mode: Mode = "exact", seed: int = 3) -> None:
self.mode = mode
self.pass_counter = 0
rng = np.random.default_rng(seed)
if mode == "saturated":
# Deliberately poor choice: large sigmoid pre-activations create flat derivatives.
self.activation = "sigmoid"
self.W1 = rng.normal(0.0, 20.0, size=(2, 8))
self.W2 = rng.normal(0.0, 20.0, size=(8, 8))
self.W3 = rng.normal(0.0, 1.0, size=(8, 1))
else:
self.activation = "tanh"
self.W1 = rng.normal(0.0, 0.18, size=(2, 8))
self.W2 = rng.normal(0.0, 0.35, size=(8, 8))
self.W3 = rng.normal(0.0, 0.35, size=(8, 1))
self.b1 = np.zeros((1, 8))
self.b2 = np.zeros((1, 8))
self.b3 = np.zeros((1, 1))
def _activate(self, z: np.ndarray) -> np.ndarray:
return np.tanh(z) if self.activation == "tanh" else stable_sigmoid(z)
def _activation_derivative_from_output(self, h: np.ndarray) -> np.ndarray:
return 1.0 - h * h if self.activation == "tanh" else h * (1.0 - h)
def forward(self, x: np.ndarray) -> Tuple[np.ndarray, Tape]:
self.pass_counter += 1
z1 = x @ self.W1 + self.b1
h1 = self._activate(z1)
z2 = h1 @ self.W2 + self.b2
h2 = self._activate(z2)
logits = h2 @ self.W3 + self.b3
return logits, Tape(self.pass_counter, x, z1, h1, z2, h2, logits)
def backward(self, tape: Tape, y: np.ndarray, expected_pass_id: int) -> Dict[str, np.ndarray]:
if tape.pass_id != expected_pass_id:
raise RuntimeError(
f"stale activation tape: loss came from pass {expected_pass_id}, "
f"but backward received pass {tape.pass_id}"
)
n = tape.x.shape[0]
probability = stable_sigmoid(tape.logits)
d_logits = (probability - y) / n
gW3 = tape.h2.T @ d_logits
gb3 = np.sum(d_logits, axis=0, keepdims=True)
d_h2 = d_logits @ self.W3.T
d_z2 = d_h2 * self._activation_derivative_from_output(tape.h2)
gW2 = tape.h1.T @ d_z2
gb2 = np.sum(d_z2, axis=0, keepdims=True)
d_h1 = d_z2 @ self.W2.T
d_z1 = d_h1 * self._activation_derivative_from_output(tape.h1)
gW1 = tape.x.T @ d_z1
gb1 = np.sum(d_z1, axis=0, keepdims=True)
if self.mode == "severed":
# The output layer still learns, but no credit reaches hidden transformations.
gW1 *= 0.0
gb1 *= 0.0
gW2 *= 0.0
gb2 *= 0.0
elif self.mode == "reversed":
# The hidden layers receive a signal with the wrong direction.
gW1 *= -1.0
gb1 *= -1.0
gW2 *= -1.0
gb2 *= -1.0
return {"W1": gW1, "b1": gb1, "W2": gW2, "b2": gb2, "W3": gW3, "b3": gb3}
def step(self, gradients: Dict[str, np.ndarray], learning_rate: float) -> None:
for name in self.parameter_names:
setattr(self, name, getattr(self, name) - learning_rate * gradients[name])
@staticmethod
def accuracy(logits: np.ndarray, y: np.ndarray) -> float:
return float(np.mean((logits >= 0.0) == (y >= 0.5)))
def parameter_items(self) -> Iterable[Tuple[str, np.ndarray]]:
for name in self.parameter_names:
yield name, getattr(self, name)
def gradient_check(model: TinyMLP, x: np.ndarray, y: np.ndarray, epsilon: float = 1e-5) -> float:
"""Check one W1 element against a centred finite difference."""
logits, tape = model.forward(x)
analytical = model.backward(tape, y, expected_pass_id=tape.pass_id)["W1"][0, 0]
original = float(model.W1[0, 0])
model.W1[0, 0] = original + epsilon
plus, _ = model.forward(x)
loss_plus = binary_cross_entropy_with_logits(plus, y)
model.W1[0, 0] = original - epsilon
minus, _ = model.forward(x)
loss_minus = binary_cross_entropy_with_logits(minus, y)
model.W1[0, 0] = original
numerical = (loss_plus - loss_minus) / (2.0 * epsilon)
denominator = max(1e-12, abs(analytical) + abs(numerical))
return float(abs(analytical - numerical) / denominator)
def train(mode: Mode, steps: int = 1000) -> Tuple[TinyMLP, list[dict[str, float]]]:
x, y = make_xor()
model = TinyMLP(mode=mode)
learning_rate = 0.4 if mode == "reversed" else 0.8
history: list[dict[str, float]] = []
for step in range(steps + 1):
logits, tape = model.forward(x)
loss = binary_cross_entropy_with_logits(logits, y)
gradients = model.backward(tape, y, expected_pass_id=tape.pass_id)
if step % 50 == 0:
history.append(
{
"step": float(step),
"loss": loss,
"accuracy": model.accuracy(logits, y),
"gW1": float(np.linalg.norm(gradients["W1"])),
"gW2": float(np.linalg.norm(gradients["W2"])),
"gW3": float(np.linalg.norm(gradients["W3"])),
}
)
if step < steps:
model.step(gradients, learning_rate)
return model, history
def main() -> None:
x, y = make_xor(n=32)
check_model = TinyMLP(mode="exact")
relative_error = gradient_check(check_model, x, y)
print(f"finite-difference relative error: {relative_error:.3e}")
print()
print("condition final_loss accuracy ||gW1|| ||gW2|| ||gW3||")
for mode in ("exact", "severed", "reversed", "saturated"):
_, history = train(mode) # type: ignore[arg-type]
last = history[-1]
print(
f"{mode:<10} {last['loss']:>9.6f} {last['accuracy']:>7.3f} "
f"{last['gW1']:>8.5f} {last['gW2']:>8.5f} {last['gW3']:>8.5f}"
)
# Demonstrate the freshness invariant without corrupting a training run.
model = TinyMLP(mode="exact")
logits_a, tape_a = model.forward(x)
_, tape_b = model.forward(x)
try:
model.backward(tape_a, y, expected_pass_id=tape_b.pass_id)
except RuntimeError as exc:
print()
print(f"freshness check: PASS ({exc})")
if __name__ == "__main__":
main()
Expected console output for the fixed seeds
finite-difference relative error: 2.291e-09
condition final_loss accuracy ||gW1|| ||gW2|| ||gW3||
exact 0.020271 0.998 0.00184 0.00429 0.00390
severed 0.691194 0.529 0.00000 0.00000 0.00015
reversed 0.691646 0.529 0.00263 0.00356 0.00470
saturated 0.255021 0.891 0.00576 0.00465 0.00538
freshness check: PASS (stale activation tape: loss came from pass 2, but backward received pass 1)
A realistic inspection-training incident
Suppose an inspection model has 24 sensor inputs, two hidden layers and one defect logit. The model is not deployed; this is a configured scenario. The training contract names the batch, parameter version, activation function, weighted loss and pass identifier. Each update produces a receipt containing the loss, gradient norms, directional-check sample and new parameter version.
During a failed run, the output-layer gradient is healthy while the first hidden-layer norm is exactly zero. Training accuracy stays near the majority rate. The team could add capacity, but the credit profile already narrows the fault: either the first layer is detached, every local derivative is zero, or its output is unused. A one-line graph inspection finds an accidental stop_gradient around a preprocessing adapter.
In a second run, every layer receives a gradient and finite differences agree, but the relative step in the first layer is 10,000 times smaller than in the last. Here the backward code is not broken. Scale is. Adjusting initialisation or per-layer optimisation is a justified next experiment. The profile prevents two different failures from receiving the same vague diagnosis, “the model does not learn”.
The receipt also records that false negatives carry eight times the loss weight. A reviewer can therefore explain why positive defect examples launch much larger gradients. Without that objective record, a large update might be misread as unstable data or an unusually influential sensor. Configuration is part of the evidence because it defines the very quantity being differentiated.
| Method | Signal fidelity | Cost and state | Useful question | Break condition |
|---|---|---|---|---|
| Exact reverse mode | Exact first derivative of the recorded differentiable graph, up to numerical error | One reverse sweep; retains or recomputes intermediates | How should many parameters change for this scalar loss? | Non-differentiable boundaries, stale graph or unhelpful local geometry |
| Finite difference | Approximate directional or coordinate derivative | Extra forward evaluations; sensitive to step size | Does the analytical gradient agree locally? | High dimension, noise or poor epsilon choice |
| Feedback alignment | Surrogate teaching direction through fixed random feedback | Relaxes exact weight transport | Can useful credit emerge without symmetric backward weights? | Task and architecture regimes where alignment does not form |
| Local auxiliary loss | Exact gradient of a different, partly local objective | Shorter paths; additional heads and labels or proxies | Can an intermediate representation learn from local targets? | Proxy pressure conflicts with the end objective |
sequenceDiagram participant Q as Question participant M as Mechanism participant E as Evidence participant D as Decision Q->>M: The neuron that fired and did not deserve the blame M->>E: A complete update you can calculate by hand E-->>M: compare rival explanations E->>D: Verify the learning mechanism before reading the loss D-->>Q: revise scope or act
Verify the learning mechanism before reading the loss
When a network produces the wrong output, do not ask which neuron “caused” it by looking at the largest activation. When training stalls, do not begin by increasing scale. Ask what objective launched the error signal, which paths connected each parameter to it, and whether the returned sensitivity was reachable, directionally correct, appropriately scaled and fresh.
This changes model interpretation too. A large gradient means that a small parameter change would alter the current loss strongly under the current batch and configuration. It does not mean the parameter is globally important, that its represented feature caused the real-world label, or that the same intervention remains important after retraining. Use ablation, counterfactual data, causal design and replication for those stronger questions.
It also changes comparisons with biological learning. Backpropagation is a precise computational solution to a declared optimisation problem. The research question for brains is not whether neurons “do backprop” in a loose metaphor. It is which locally available signals, dynamics and plasticity rules can produce the observed credit under biological constraints, and which perturbations distinguish those mechanisms.
Once that gate passes, decisions about architecture, data and optimisation become meaningful. Before it passes, aggregate loss is evidence that something happened, not evidence that the intended learning mechanism ran.
Glossary
- Activation
- The value emitted by a unit after its weighted input passes through a response function.
- Backpropagation
- Reverse-mode differentiation applied to a network and loss, followed by an update rule such as gradient descent.
- Credit assignment
- The problem of determining how adjustable parts should change in response to a distal outcome or error.
- Gradient
- The vector of first derivatives of a scalar loss with respect to parameters.
- Jacobian
- The matrix of local derivatives for a vector-valued transformation.
- Path-conditioned sensitivity
- The loss sensitivity at a parameter after all downstream derivative routes are composed.
- Reverse mode
- A differentiation strategy that propagates one scalar output’s sensitivity backwards to many inputs.
- Activation tape
- The intermediate state saved or recomputed so the backward pass can evaluate local derivatives for the matching forward computation.
References
- McCulloch, W. S. & Pitts, W. (1943). “A logical calculus of the ideas immanent in nervous activity.” Bulletin of Mathematical Biophysics, 5, 115–133. https://doi.org/10.1007/BF02478259
- Rosenblatt, F. (1958). “The perceptron: a probabilistic model for information storage and organization in the brain.” Psychological Review, 65(6), 386–408. https://doi.org/10.1037/h0042519
- Widrow, B. & Hoff, M. E. (1960). “Adaptive switching circuits.” IRE WESCON Convention Record, 4, 96–104. Stanford-hosted paper
- Linnainmaa, S. (1976). “Taylor expansion of the accumulated rounding error.” BIT Numerical Mathematics, 16, 146–160. https://doi.org/10.1007/BF01931367
- Rumelhart, D. E., Hinton, G. E. & Williams, R. J. (1986). “Learning representations by back-propagating errors.” Nature, 323, 533–536. https://doi.org/10.1038/323533a0
- Bengio, Y., Simard, P. & Frasconi, P. (1994). “Learning long-term dependencies with gradient descent is difficult.” IEEE Transactions on Neural Networks, 5(2), 157–166. https://doi.org/10.1109/72.279181
- Glorot, X. & Bengio, Y. (2010). “Understanding the difficulty of training deep feedforward neural networks.” Proceedings of Machine Learning Research, 9, 249–256. PMLR article
- He, K., Zhang, X., Ren, S. & Sun, J. (2015). “Delving deep into rectifiers: surpassing human-level performance on ImageNet classification.” ICCV. arXiv record
- Pascanu, R., Mikolov, T. & Bengio, Y. (2013). “On the difficulty of training recurrent neural networks.” Proceedings of Machine Learning Research, 28, 1310–1318. PMLR article
- He, K., Zhang, X., Ren, S. & Sun, J. (2016). “Deep residual learning for image recognition.” CVPR, 770–778. CVF open access
- Lillicrap, T. P., Cownden, D., Tweed, D. B. & Akerman, C. J. (2016). “Random synaptic feedback weights support error backpropagation for deep learning.” Nature Communications, 7, 13276. https://doi.org/10.1038/ncomms13276
- Nøkland, A. (2016). “Direct feedback alignment provides learning in deep neural networks.” Advances in Neural Information Processing Systems, 29. arXiv record
- Ellenberger, B. et al. (2026). “Backpropagation through space, time and the brain.” Nature Communications, 17, 66. https://doi.org/10.1038/s41467-025-66666-z