Learning systems from first principles

The artificial neuron, backpropagation and credit assignment

Rajesh Ranjan Mahapatra

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.

© 2026 Rajesh Ranjan Mahapatra. 33 min read

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

  1. An artificial neuron transforms inputs, but its activation alone says nothing about how its weights should change.
  2. Backpropagation assigns path-conditioned credit: the loss sensitivity at a parameter is composed through every downstream transformation.
  3. Reverse-mode differentiation reuses one forward computation to obtain gradients for many parameters, provided the matching intermediate state is available.
  4. Depth, saturation, poor scale, severed paths and stale state can erase or distort credit even when the forward output looks plausible.
  5. 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:

z = Σᵢ wᵢxᵢ + b     and     a = φ(z) The inputs x are current signals, w and b are adjustable parameters, z is the pre-activation, φ is the response function, and a is the emitted activation.

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.

Worked example · One neuron

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.

A forward wave and a backward responsibility field
A network landscape with forward activations moving right and loss sensitivities moving left The forward pass creates a prediction from inputs through three transformations. The backward pass carries sensitivity contours from the loss towards parameters. A disconnected lower route carries activation but receives no backward signal. x h₁ h₂ L h* Forward: what value did the system compute? Backward: which local changes alter the loss? Activation can travel along a route whose gradient is severed. dotted: no effective return path
The forward pass and backward pass answer different questions. The first computes values. The second composes sensitivities through the routes that connect those values to the loss. Illustrative conceptual figure.

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.

The argument in one pass
Chart · flow
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
A structural route through The artificial neuron, backpropagation and credit assignment: from The neuron that fired and did not… to Verify the learning mechanism before…. Reading use: follow the relation, then return to the surrounding argument for evidence and limits.

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:

∂L/∂w₁ = (∂L/∂ŷ)(∂ŷ/∂h)(∂h/∂w₁) The first factor says how the loss reacts to the prediction. The second says how the prediction reacts to the hidden value. The third says how that hidden value reacts to the early weight.

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.

Equal activity, unequal credit
Two panels compare hidden units with equal activation but different downstream paths In the left panel, a hidden unit with activation 0.8 connects to the loss through weight 2 and receives non-zero gradient. In the right panel, the same activation connects through weight zero and receives zero gradient. A · Connected path B · Severed path x h = 0.8same activity L w = 2 ∂L/∂h ≠ 0 x h = 0.8same activity L w = 0 ∂L/∂h = 0
The activation is held constant while the downstream connection changes. The gradient follows influence, not magnitude of firing. Illustrative causal intervention.
Worked example · A branching path

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.

Worked example · Mental arithmetic

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.

The numerical chain and its returning sensitivities
Annotated two-weight computation showing forward values and backward gradients The forward route is x equals 2, times w1 equals 3, giving h equals 6, times w2 equals 0.5, giving prediction 3 and loss 0.5 against target 4. The backward route gives gradients minus 6 for w2 and minus 1 for w1. x = 2 × w₁w₁ = 3 h = 6 × w₂w₂ = 0.5 ŷ = 3y = 4, L = 0.5 Forward values Backward sensitivities ∂L/∂w₂ = (3−4)×6 = −6 ∂L/∂w₁ = (3−4)×0.5×2 = −1 The same final error is transformed by each parameter’s position in the chain.
Every gradient is a product of an upstream error term and local sensitivity factors. Setting w2 to zero would leave h at six but make the gradient for w1 zero. Illustrative worked example.

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]

One forward pass, one matching reverse sweep
Timeline of a forward pass saving intermediate values and a backward pass consuming them in reverse The top timeline computes x, z1, h1, z2, h2 and loss while saving a tape. The lower timeline starts from the loss and computes sensitivities in reverse. Pass ID 417 links the two timelines. Forward computation x z₁ h₁ z₂ h₂ L Activation tape · pass_id 417 saved inputs, pre-activations and outputs needed by local derivatives Reverse sensitivity sweep 1 δh₂ δz₂ δh₁ δz₁ δx A different pass ID means a different derivative problem.
Reverse mode depends on the exact intermediate state that produced the loss. The tape may be stored or recomputed, but it must correspond to the same forward computation. Illustrative implementation timeline, informed by reverse accumulation.[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:

∂L/∂w = (ŷ − y)(x₁ + x₂) = (ŷ − y)x₁ + (ŷ − y)x₂

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.

Depth amplifies small departures from neutral transport
Chart of log gradient magnitude across depth for repeated local factors Curves for repeated local factors 0.5 and 0.9 decrease with depth, factor 1 stays level, and factors 1.1 and 1.5 increase. The vertical axis is log base ten of relative gradient magnitude. 10³10²10¹10⁰10⁻¹10⁻²10⁻³10⁻⁴ 024681012 number of repeated derivative factors relative gradient magnitude, log scale factor 0.5factor 0.9factor 1.0factor 1.1factor 1.5
A gradient is a product of local Jacobian effects. Values below one tend to attenuate; values above one tend to amplify. Real networks mix directions and matrices, but the scalar corridor exposes the core arithmetic. Illustrative calculation. The broader vanishing and exploding gradient problem is analysed by Bengio, Simard and Frasconi, and by Pascanu, Mikolov and Bengio.[6][9]

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.

Worked example · Failure edge case

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.

Four break patterns in the backward field
A diagnostic landscape for saturation, severed paths, exploding gradients and stale state Four quadrants show a flattening channel for saturation, a cut route for zero derivative, a widening unstable funnel for exploding gradients, and crossed timelines for stale activation state. Saturation output changes little, derivative flattens Severed operation argmax, stop-gradient or zero local derivative Exploding product small directional expansion compounds Stale tape 41 42 backward pass 42 consumes state from pass 41
These failures have different signatures and remedies. Adding data will not reconnect a stopped gradient, while clipping will not repair a stale activation tape. Illustrative failure taxonomy.

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.

How the claim changes under pressure
Chart · state
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 --> [*]
The revision loop begins with A complete update you can calculate by… and keeps counterexamples inside the reasoning path. Reading use: follow the relation, then return to the surrounding argument for evidence and limits.

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.

The credit integrity profile
Four-lane audit for gradient reach, alignment, scale and freshness A central loss sends four diagnostic beams to reach, alignment, scale and freshness. Each lane names a test, a positive reading and a failure interpretation. Declaredloss Reachlayerwise norms Signal arrives at every intended block Alignmentfinite difference Analytic direction matches loss change Scalerelative step size Update is neither inert nor destructive Freshnesspass and version IDs Loss, tape, batch and parameters agree
A positive profile permits interpretation of the current backward computation. It does not show that the objective is good, the data are representative or the model will generalise. Practitioner synthesis.

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:

DᵥL ≈ [L(θ + εv) − L(θ − εv)] / (2ε) Agreement checks the backward implementation for the current batch and parameter state. It does not certify every point in parameter space.

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.

A pass-consistent training boundary
System boundary showing batch, parameter version, activation tape, loss, backward pass and optimiser receipt Inside a training-step boundary, batch B73 and parameter version 42 create activation tape and loss with pass ID 417. Backward consumes those matching artefacts, and the optimiser emits parameter version 43 plus a receipt. A red dotted path shows rejected stale tape from pass 416. Training step boundary · pass_id 417 Batch B73inputs + labels Parametersversion 42 Forwarddeterministic seed S9configured graph G12 Tape 417saved local state Loss 417objective O6 Backwardgradient + profilepass IDs asserted Step receiptversion 43 Tape 416 · rejected as stale The receipt proves process consistency, not task validity or generalisation.
Versioned state turns an implicit autograd assumption into a testable invariant. This matters most when forward and backward work can be reordered, retried or distributed. Practitioner architecture.
Executable lab

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.

What it testsWhether path-conditioned gradients are numerically consistent and whether hidden layers receive usable credit.
AssumptionsDifferentiable dense layers, binary cross-entropy, full-batch updates and deterministic synthetic data.
Positive result permitsTrust in this implementation’s local gradient calculation for the checked state.
It cannot establishGeneralisation, global optimality, biological plausibility, fair objectives or causal truth in a real domain.

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 .

Measured learning under four credit conditions
Training loss curves for exact, severed, reversed and saturated credit conditions Exact backpropagation falls to a loss near 0.02. Severed and reversed hidden credit stay near 0.69. Saturated sigmoid training improves more slowly to about 0.255. Results are from the bundled deterministic synthetic experiment. 1.10.90.70.50.30.10 02004006008001000 training steps binary cross-entropy loss exact severed reversed saturated
Exact backpropagation reaches loss 0.0203 and 99.8 per cent training accuracy. Severed and reversed hidden credit remain near chance at 52.9 per cent. Deliberately saturated sigmoid layers learn more slowly and finish at 89.1 per cent. The small non-monotonic bumps in the exact curve also show that a correct gradient does not guarantee every finite step lowers loss. Measured synthetic results from the bundled script, fixed seeds, 512 examples and 1,000 full-batch steps. These are mechanism demonstrations, not benchmark claims.
ConditionFinal lossTraining accuracy‖∇W₁‖Interpretation
Exact0.0202710.9980.00184Path-conditioned credit reaches both hidden layers.
Severed0.6911940.5290Only the output layer updates; the representation stays almost linear.
Reversed0.6916460.5290.00263A non-zero signal is present, but its hidden-layer direction is wrong.
Saturated0.2550210.8910.00576Exact 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
Python · NumPy only
"""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
Console
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)
Worked example · Configured system

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.

What each credit method can legitimately tell you
MethodSignal fidelityCost and stateUseful questionBreak condition
Exact reverse modeExact first derivative of the recorded differentiable graph, up to numerical errorOne reverse sweep; retains or recomputes intermediatesHow should many parameters change for this scalar loss?Non-differentiable boundaries, stale graph or unhelpful local geometry
Finite differenceApproximate directional or coordinate derivativeExtra forward evaluations; sensitive to step sizeDoes the analytical gradient agree locally?High dimension, noise or poor epsilon choice
Feedback alignmentSurrogate teaching direction through fixed random feedbackRelaxes exact weight transportCan useful credit emerge without symmetric backward weights?Task and architecture regimes where alignment does not form
Local auxiliary lossExact gradient of a different, partly local objectiveShorter paths; additional heads and labels or proxiesCan an intermediate representation learn from local targets?Proxy pressure conflicts with the end objective
Methods differ in the question they answer. A surrogate can be useful without being the gradient of the original loss, while an exact gradient can still be unhelpful for a badly chosen objective. Synthesis based on reverse accumulation, gradient checking, feedback alignment and auxiliary-loss practice.[11][12]
The evidence conversation
Chart · sequence
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
Question, mechanism, evidence and decision exchange the burdens developed in The artificial neuron, backpropagation and credit assignment. Reading use: follow the relation, then return to the surrounding argument for evidence and limits.

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.

Operational rule: treat the forward output as behaviour, the backward gradient as path-conditioned sensitivity, and the credit integrity profile as the gate that determines whether a training result is interpretable.

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

  1. 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
  2. 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
  3. Widrow, B. & Hoff, M. E. (1960). “Adaptive switching circuits.” IRE WESCON Convention Record, 4, 96–104. Stanford-hosted paper
  4. Linnainmaa, S. (1976). “Taylor expansion of the accumulated rounding error.” BIT Numerical Mathematics, 16, 146–160. https://doi.org/10.1007/BF01931367
  5. 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
  6. 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
  7. Glorot, X. & Bengio, Y. (2010). “Understanding the difficulty of training deep feedforward neural networks.” Proceedings of Machine Learning Research, 9, 249–256. PMLR article
  8. He, K., Zhang, X., Ren, S. & Sun, J. (2015). “Delving deep into rectifiers: surpassing human-level performance on ImageNet classification.” ICCV. arXiv record
  9. 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
  10. He, K., Zhang, X., Ren, S. & Sun, J. (2016). “Deep residual learning for image recognition.” CVPR, 770–778. CVF open access
  11. 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
  12. Nøkland, A. (2016). “Direct feedback alignment provides learning in deep neural networks.” Advances in Neural Information Processing Systems, 29. arXiv record
  13. 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