PROMETHEUS OS
NATIVE AI OPERATING SYSTEM
[INIT] Prometheus OS loading...
[CORE] Neural interface initializing...
[AI] LLM router connecting...
[MEM] Memory graph loading...
[AGT] Agent chains on standby...
[SEC] Security protocols active...
[NET] Provider mesh established...
[UI] Workspace rendering...
[DONE] All systems nominal.
Welcome to PROMETHEUS OS.
READY · OFFLINE · SECURE · PRIVATE
🎨 THEME ENGINE
PROMETHEUS OS · 23 THEMES
Trial version: Dark + Light themes only.
Full theme access with PROMETHEUS OS license.
Dark theme active
Experience all 23 themes in PROMETHEUS OS

Calculus and Gradients: How Networks Learn

Intermediate 18 min· 4 exercises

Compute derivatives, gradients and Jacobians, run backpropagation through a small network by hand, and explain and verify how automatic differentiation produces the gradients every training loop depends on.

By the end of this lesson you will be able to
  • Compute derivatives and partial derivatives of the functions used in neural networks, and explain the gradient as the direction of steepest ascent.
  • Apply the multivariable chain rule on a computational graph to obtain gradients.
  • Derive the gradients of a batched linear layer and perform a full forward and backward pass through a two-layer network by hand.
  • Compare forward-mode and reverse-mode automatic differentiation and explain why reverse mode is used for training.
  • Implement a numerical gradient check with central differences and interpret the relative error.

Before this: Linear Algebra for AI

Training a neural network means nudging millions or billions of numbers so a loss goes down. To know which way to nudge each number, you need its derivative: how much the loss changes when that number changes a little. Backpropagation, the algorithm behind every deep-learning framework, is nothing more than the chain rule from first-year calculus applied systematically to a graph of operations. This lesson builds that machinery from the ground up, works a full backward pass by hand, and shows how to check gradients the way professionals do when they write custom layers.

Intuition: sensitivity and blame

Imagine a factory line where each station transforms a part and passes it on, and a quality inspector at the end gives a score. If the score is bad, each station wants to know: "if I had changed my setting slightly, how much would the final score have moved?" The last station can answer directly. The station before it answers by multiplying its own local sensitivity by the sensitivity of everything downstream. That relay of blame, from the end of the line back to the start, is backpropagation. The derivative is the sensitivity; the chain rule is how sensitivities multiply along the line.

Derivatives

For a function f:R→Rf: \mathbb{R} \to \mathbb{R}, the derivative at xx is

f′(x)=dfdx=lim⁡h→0f(x+h)−f(x)h.f'(x) = \frac{df}{dx} = \lim_{h \to 0} \frac{f(x + h) - f(x)}{h}.

It is the slope of the tangent line, and it gives the best linear approximation near xx: f(x+h)≈f(x)+f′(x) hf(x + h) \approx f(x) + f'(x)\,h for small hh. That approximation is the entire justification for gradient descent: if you step against the slope, the function decreases, at least for small steps.

Rules you will use constantly (with cc a constant):

FunctionDerivativeWhere in ML
c xc\,xcclinear layers
xnx^nnxn−1n x^{n-1}squared error
exe^xexe^xsoftmax
ln⁡x\ln x1/x1/xlog-likelihood, cross-entropy
σ(x)=11+e−x\sigma(x) = \frac{1}{1+e^{-x}}σ(x)(1−σ(x))\sigma(x)(1 - \sigma(x))logistic regression, gates
tanh⁡x\tanh x1−tanh⁡2x1 - \tanh^2 xRNNs
ReLU(x)=max⁡(0,x)\mathrm{ReLU}(x) = \max(0, x)11 if x>0x > 0, 00 if x<0x < 0most networks

ReLU is not differentiable at exactly 00; frameworks simply pick 00 there (a valid subgradient), and it causes no problems in practice. The sum rule (f+g)′=f′+g′(f + g)' = f' + g' and product rule (fg)′=f′g+fg′(fg)' = f'g + fg' round out the toolkit.

Example: f(x)=x2f(x) = x^2 at x=3x = 3 has f′(3)=6f'(3) = 6. Check with h=0.01h = 0.01: (3.012−9)/0.01=0.0601/0.01=6.01(3.01^2 - 9)/0.01 = 0.0601/0.01 = 6.01, close to 6.

Partial derivatives and the gradient

Models have many parameters, so losses are functions f:Rn→Rf: \mathbb{R}^n \to \mathbb{R}. The partial derivative ∂f/∂xi\partial f / \partial x_i is the derivative with respect to xix_i while every other input is held fixed. Collecting all of them gives the gradient:

∇f(x)=(∂f∂x1,…,∂f∂xn).\nabla f(\mathbf{x}) = \left( \frac{\partial f}{\partial x_1}, \dots, \frac{\partial f}{\partial x_n} \right).

Worked example. f(x,y)=x2y+3yf(x, y) = x^2 y + 3y.

  • ∂f/∂x=2xy\partial f/\partial x = 2xy (treat yy as a constant).
  • ∂f/∂y=x2+3\partial f/\partial y = x^2 + 3 (treat xx as a constant).
  • At (x,y)=(1,2)(x, y) = (1, 2): f=2+6=8f = 2 + 6 = 8 and ∇f=(4,4)\nabla f = (4, 4).

Three facts make the gradient the central object of optimization:

  1. Linear approximation: f(x+δ)≈f(x)+∇f(x)⊤δf(\mathbf{x} + \boldsymbol{\delta}) \approx f(\mathbf{x}) + \nabla f(\mathbf{x})^\top \boldsymbol{\delta}.
  2. Steepest ascent: among all directions of unit length, ∇f\nabla f points the way ff increases fastest, and −∇f-\nabla f is the steepest descent. (By Cauchy–Schwarz, ∇f⊤u\nabla f^\top \mathbf{u} over unit u\mathbf{u} is maximized when u\mathbf{u} is parallel to ∇f\nabla f.)
  3. Stationary points: at a minimum, maximum or saddle point of a smooth function, ∇f=0\nabla f = \mathbf{0}.

The gradient has the same shape as the parameters: the gradient of a loss with respect to a 768×3072768 \times 3072 weight matrix is itself a 768×3072768 \times 3072 matrix. That is what param.grad holds in PyTorch.

The chain rule

If y=f(u)y = f(u) and u=g(x)u = g(x), then

dydx=dydu⋅dudx.\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx}.

With several intermediate variables, you sum over every path: if LL depends on xx through u1,…,uku_1, \dots, u_k, then

∂L∂x=∑j=1k∂L∂uj∂uj∂x.\frac{\partial L}{\partial x} = \sum_{j=1}^{k} \frac{\partial L}{\partial u_j} \frac{\partial u_j}{\partial x}.

Worked example: one logistic neuron. y^=σ(z)\hat{y} = \sigma(z) with z=wx+bz = wx + b, and w=2w = 2, x=1x = 1, b=−1b = -1.

  1. Forward: z=2⋅1−1=1z = 2 \cdot 1 - 1 = 1, y^=σ(1)=1/(1+e−1)≈0.7311\hat{y} = \sigma(1) = 1/(1 + e^{-1}) \approx 0.7311.
  2. Local derivatives: dy^/dz=σ(1)(1−σ(1))≈0.7311×0.2689≈0.1966d\hat{y}/dz = \sigma(1)(1 - \sigma(1)) \approx 0.7311 \times 0.2689 \approx 0.1966; ∂z/∂w=x=1\partial z/\partial w = x = 1; ∂z/∂b=1\partial z / \partial b = 1.
  3. Chain: ∂y^/∂w=0.1966×1=0.1966\partial \hat{y}/\partial w = 0.1966 \times 1 = 0.1966 and ∂y^/∂b=0.1966\partial \hat y / \partial b = 0.1966.

Notice that σ′(z)≤0.25\sigma'(z) \le 0.25 everywhere. Chain twenty sigmoids and the product of local derivatives is at most 0.2520≈9×10−130.25^{20} \approx 9 \times 10^{-13}: this is the vanishing gradient problem, and one reason ReLU (local derivative 1 when active) and residual connections replaced deep sigmoid stacks.

Computational graphs

Any computation can be drawn as a directed acyclic graph (DAG): nodes are intermediate values, edges show which values feed which operations. The forward pass evaluates nodes in order from inputs to output. The backward pass visits nodes in reverse order and, at each node uu, computes the adjoint uˉ=∂L/∂u\bar{u} = \partial L / \partial u by summing over the node's consumers vv:

uˉ=∑v : u→vvˉ ∂v∂u.\bar{u} = \sum_{v \,:\, u \to v} \bar{v} \, \frac{\partial v}{\partial u}.

Each operation only needs to know its own local derivative; the graph takes care of the rest. That locality is why you can add a new layer type to a framework by writing just its forward function and its local backward rule.

Jacobians, briefly

For a vector-valued function f:Rn→Rm\mathbf{f}: \mathbb{R}^n \to \mathbb{R}^m, the derivative is the Jacobian matrix J∈Rm×nJ \in \mathbb{R}^{m \times n} with Jij=∂fi/∂xjJ_{ij} = \partial f_i / \partial x_j. The chain rule becomes matrix multiplication: if y=f(g(x))\mathbf{y} = \mathbf{f}(\mathbf{g}(\mathbf{x})), then Jy,x=Jf JgJ_{\mathbf{y}, \mathbf{x}} = J_{\mathbf{f}} \, J_{\mathbf{g}}.

Examples: for y=Wx\mathbf{y} = W\mathbf{x}, the Jacobian is WW itself. For an elementwise activation, it is diagonal with entries ϕ′(xi)\phi'(x_i), which is why frameworks implement it as an elementwise multiply rather than building a matrix. For softmax, ∂pi/∂zj=pi(δij−pj)\partial p_i / \partial z_j = p_i(\delta_{ij} - p_j) (with δij=1\delta_{ij} = 1 if i=ji = j, else 00).

Frameworks never build full Jacobians for large layers. Reverse mode computes vector–Jacobian products yˉ⊤J\bar{\mathbf{y}}^\top J directly. For a batched linear layer Y=XWY = XW with upstream gradient G=∂L/∂YG = \partial L / \partial Y (same shape as YY), the results are

∂L∂W=X⊤G,∂L∂X=G W⊤,∂L∂b=∑rowsG.\frac{\partial L}{\partial W} = X^\top G, \qquad \frac{\partial L}{\partial X} = G\, W^\top, \qquad \frac{\partial L}{\partial \mathbf{b}} = \textstyle\sum_{\text{rows}} G.

Check the shapes: XX is (B,din)(B, d_{\text{in}}), GG is (B,dout)(B, d_{\text{out}}), so X⊤GX^\top G is (din,dout)(d_{\text{in}}, d_{\text{out}}), matching WW. When in doubt, derive matrix gradients by matching shapes, then confirm numerically.

Worked backpropagation through a 2-layer network

Network: input x∈R2\mathbf{x} \in \mathbb{R}^2, hidden layer of 2 ReLU units, one linear output, squared-error loss.

z=W1x+b1,h=ReLU(z),y^=w2⊤h+b2,L=12(y^−y)2.\mathbf{z} = W_1\mathbf{x} + \mathbf{b}_1, \quad \mathbf{h} = \mathrm{ReLU}(\mathbf{z}), \quad \hat{y} = \mathbf{w}_2^\top \mathbf{h} + b_2, \quad L = \tfrac12 (\hat{y} - y)^2.

Values: x=(1,2)\mathbf{x} = (1, 2), target y=1y = 1,

W1=(0.10.20.3−0.4),b1=(0,0),w2=(0.5,−0.6),b2=0.1.W_1 = \begin{pmatrix} 0.1 & 0.2 \\ 0.3 & -0.4 \end{pmatrix}, \quad \mathbf{b}_1 = (0, 0), \quad \mathbf{w}_2 = (0.5, -0.6), \quad b_2 = 0.1.

Forward pass.

  1. z1=0.1⋅1+0.2⋅2=0.5z_1 = 0.1 \cdot 1 + 0.2 \cdot 2 = 0.5; z2=0.3⋅1−0.4⋅2=−0.5z_2 = 0.3 \cdot 1 - 0.4 \cdot 2 = -0.5.
  2. h=(max⁡(0,0.5),max⁡(0,−0.5))=(0.5,0)\mathbf{h} = (\max(0, 0.5), \max(0, -0.5)) = (0.5, 0).
  3. y^=0.5⋅0.5+(−0.6)⋅0+0.1=0.35\hat{y} = 0.5 \cdot 0.5 + (-0.6) \cdot 0 + 0.1 = 0.35.
  4. L=12(0.35−1)2=12(0.4225)=0.21125L = \tfrac12 (0.35 - 1)^2 = \tfrac12 (0.4225) = 0.21125.

Backward pass (each line uses only the line above and a local derivative).

  1. y^ˉ=∂L/∂y^=y^−y=−0.65\bar{\hat{y}} = \partial L / \partial \hat{y} = \hat{y} - y = -0.65.
  2. Output layer: ∂L/∂w2=y^ˉ h=−0.65⋅(0.5,0)=(−0.325,0)\partial L/\partial \mathbf{w}_2 = \bar{\hat y}\, \mathbf{h} = -0.65 \cdot (0.5, 0) = (-0.325, 0); ∂L/∂b2=−0.65\partial L / \partial b_2 = -0.65.
  3. Into the hidden layer: hˉ=y^ˉ w2=−0.65⋅(0.5,−0.6)=(−0.325,0.39)\bar{\mathbf{h}} = \bar{\hat y}\, \mathbf{w}_2 = -0.65 \cdot (0.5, -0.6) = (-0.325, 0.39).
  4. Through ReLU (local derivative 1 where z>0z > 0, 0 where z<0z < 0): zˉ=(−0.325⋅1,  0.39⋅0)=(−0.325,0)\bar{\mathbf{z}} = (-0.325 \cdot 1, \; 0.39 \cdot 0) = (-0.325, 0).
  5. First layer: ∂L/∂W1=zˉ x⊤\partial L / \partial W_1 = \bar{\mathbf{z}}\, \mathbf{x}^\top (an outer product):
∂L∂W1=(−0.3250)(12)=(−0.325−0.6500),∂L∂b1=(−0.325,0).\frac{\partial L}{\partial W_1} = \begin{pmatrix} -0.325 \\ 0 \end{pmatrix} \begin{pmatrix} 1 & 2 \end{pmatrix} = \begin{pmatrix} -0.325 & -0.65 \\ 0 & 0 \end{pmatrix}, \qquad \frac{\partial L}{\partial \mathbf{b}_1} = (-0.325, 0).

Interpretation: the second hidden unit is "dead" for this input (its pre-activation is negative), so neither its incoming weights nor w2,2w_{2,2} receive any gradient. All gradients are negative where non-zero, so a gradient-descent step will increase those weights, raising y^\hat{y} toward the target. With learning rate η=0.1\eta = 0.1, the new b2b_2 would be 0.1−0.1⋅(−0.65)=0.1650.1 - 0.1 \cdot (-0.65) = 0.165.

Automatic differentiation: forward vs reverse mode

There are three ways to get derivatives by computer:

  • Symbolic differentiation manipulates formulas; it is exact but expressions can blow up in size.
  • Numerical differentiation uses finite differences; it is easy but approximate and needs one or two function evaluations per input.
  • Automatic differentiation (AD) applies the chain rule to the actual sequence of elementary operations your program executes, giving derivatives exact to floating-point precision.

AD has two modes. For f:Rn→Rm\mathbf{f}: \mathbb{R}^n \to \mathbb{R}^m:

Forward modeReverse mode
Propagatestangents u˙=∂u/∂xj\dot{u} = \partial u / \partial x_j from inputs forwardadjoints uˉ=∂yi/∂u\bar{u} = \partial y_i / \partial u from outputs backward
One pass givesone column of JJ (Jacobian–vector product)one row of JJ (vector–Jacobian product)
Passes for full JJnnmm
Memorylow (no need to store the graph)must store intermediate values from the forward pass
Best whenfew inputs, many outputsmany inputs, few outputs

A training loss is a single scalar (m=1m = 1) depending on billions of parameters (nn huge), so reverse mode computes the entire gradient for roughly the cost of a small constant number of forward passes (the usual rule of thumb is that a backward pass costs about twice the forward pass). Backpropagation is reverse-mode AD. The price is memory: activations from the forward pass must be kept for the backward pass, which is why activation memory dominates training and why activation checkpointing (recomputing some activations during the backward pass) trades compute for memory. Forward mode is used for Jacobian–vector products, for example in some second-order methods.

A reverse-mode autodiff engine in 30 lines

import math

class Value:
    def __init__(self, data, parents=(), local_grads=()):
        self.data, self.grad = data, 0.0
        self.parents, self.local_grads = parents, local_grads

    def __add__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        return Value(self.data + other.data, (self, other), (1.0, 1.0))

    def __mul__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        return Value(self.data * other.data, (self, other), (other.data, self.data))

    def relu(self):
        return Value(max(0.0, self.data), (self,), (1.0 if self.data > 0 else 0.0,))

    def sigmoid(self):
        s = 1 / (1 + math.exp(-self.data))
        return Value(s, (self,), (s * (1 - s),))

    def backward(self):
        order, seen = [], set()
        def visit(v):                      # topological sort of the graph
            if id(v) not in seen:
                seen.add(id(v))
                for p in v.parents: visit(p)
                order.append(v)
        visit(self)
        self.grad = 1.0
        for v in reversed(order):          # adjoints flow output -> inputs
            for p, g in zip(v.parents, v.local_grads):
                p.grad += v.grad * g

w, x, b = Value(2.0), Value(1.0), Value(-1.0)
y = (w * x + b).sigmoid()
y.backward()
print(round(y.data, 4), round(w.grad, 4), round(b.grad, 4))  # 0.7311 0.1966 0.1966

Each operation records its parents and its local derivatives during the forward pass; backward sorts the graph topologically and accumulates adjoints with += (the sum over paths in the chain rule). This reproduces the logistic-neuron numbers from earlier. PyTorch, JAX and TensorFlow do the same thing on tensors, with hand-written vector–Jacobian products for each operation. In PyTorch the equivalent is a tensor created with requires_grad=True, a forward computation, then loss.backward(), after which each leaf tensor's .grad holds its gradient.

Checking gradients numerically

When you implement a custom layer or a hand-derived gradient, verify it against the central difference

∂f∂θi≈f(θ+hei)−f(θ−hei)2h,\frac{\partial f}{\partial \theta_i} \approx \frac{f(\boldsymbol{\theta} + h\mathbf{e}_i) - f(\boldsymbol{\theta} - h\mathbf{e}_i)}{2h},

where ei\mathbf{e}_i is the ii-th unit vector. Its error shrinks like O(h2)O(h^2), versus O(h)O(h) for the one-sided difference. Choose h≈10−5h \approx 10^{-5} to 10−610^{-6} in float64: too large and the approximation error dominates; too small and floating-point cancellation does. Compare with the relative error

rel=∣ganalytic−gnumeric∣max⁡(∣ganalytic∣,∣gnumeric∣,ϵ),\text{rel} = \frac{|g_{\text{analytic}} - g_{\text{numeric}}|}{\max(|g_{\text{analytic}}|, |g_{\text{numeric}}|, \epsilon)},

where ϵ\epsilon is a tiny constant that avoids division by zero. In float64, values around 10−710^{-7} or below indicate a correct gradient; 10−210^{-2} or above means a bug.

import numpy as np

x, y = np.array([1., 2.]), 1.0
params = {"W1": np.array([[.1, .2], [.3, -.4]]), "b1": np.zeros(2),
          "w2": np.array([.5, -.6]), "b2": np.array(.1)}

def loss_and_grads(p):
    z = p["W1"] @ x + p["b1"]; h = np.maximum(z, 0)
    yhat = p["w2"] @ h + p["b2"]; L = 0.5 * (yhat - y) ** 2
    d_yhat = yhat - y
    d_z = d_yhat * p["w2"] * (z > 0)
    grads = {"W1": np.outer(d_z, x), "b1": d_z,
             "w2": d_yhat * h, "b2": np.array(d_yhat)}
    return L, grads

L, g = loss_and_grads(params)
print(L, g["W1"])                              # 0.21125 [[-0.325 -0.65] [0 0]]

h = 1e-6
for name, P in params.items():
    num = np.zeros_like(P)
    for i in np.ndindex(P.shape):
        old = P[i]
        P[i] = old + h; Lp, _ = loss_and_grads(params)
        P[i] = old - h; Lm, _ = loss_and_grads(params)
        P[i] = old
        num[i] = (Lp - Lm) / (2 * h)
    rel = np.abs(num - g[name]) / np.maximum(1e-12, np.abs(num) + np.abs(g[name]))
    print(name, rel.max())                      # all tiny (~1e-11)

The loop perturbs each parameter in place, recomputes the loss, and compares. Gradient checking is slow (two forward passes per parameter) so you run it on tiny inputs, once, as a unit test. Avoid checking exactly at ReLU kinks (pre-activations equal to zero), where the numerical derivative is meaningless; and disable dropout or other randomness while checking.

Common pitfalls

  • Forgetting to zero gradients. Frameworks accumulate into .grad (the += above). In PyTorch, call optimizer.zero_grad() each step or gradients from previous batches leak in.
  • Shape-mismatched matrix gradients. If your derived ∂L/∂W\partial L/\partial W does not have WW's shape, a transpose is missing.
  • Breaking the graph. Converting to NumPy, using .item(), or in-place edits on tensors needed for backward can silently stop gradients or raise errors.
  • Vanishing and exploding gradients. Long chains of small (or large) local derivatives shrink (or blow up) the product; monitor per-layer gradient norms.
  • Dead ReLUs. Units whose pre-activation is negative for all inputs get zero gradient forever, as unit 2 did in the worked example.
  • Gradient checks in float32 or at kinks. Use float64 and avoid non-differentiable points, or you will chase false alarms.

In practice

  • Let autodiff compute gradients; derive by hand only to understand, to write fused kernels, or to debug. Always back hand-derived gradients with a numerical check (PyTorch provides torch.autograd.gradcheck for this).
  • Log the global gradient norm every step. Sudden spikes precede loss spikes; clipping the global norm (often to 1.0 in LLM training recipes) is a common safeguard.
  • Memory, not compute, is often the limit in training: activations stored for reverse mode scale with batch size, sequence length and depth. Activation checkpointing, smaller micro-batches and mixed precision are the standard levers.
  • Use torch.no_grad() (or the equivalent) during evaluation and inference so no graph is built.

Key takeaways

  • A derivative is a local linear approximation; gradient descent works because stepping against the gradient decreases the function for small enough steps.
  • The gradient has the same shape as the parameters and points in the direction of steepest increase.
  • Backpropagation is the chain rule applied in reverse topological order on a computational graph, summing over all paths.
  • Reverse-mode autodiff gets the full gradient of a scalar loss in a few forward-pass equivalents, at the cost of storing activations.
  • For Y = XW with upstream gradient G, dL/dW = XᵀG and dL/dX = GWᵀ; matching shapes is a fast sanity check.
  • Always verify hand-written gradients with a float64 central-difference check and relative error.

Terms in this lesson

Derivative
The instantaneous rate of change of a function with respect to its input, equal to the slope of its best local linear approximation.
Partial derivative
The derivative of a multivariable function with respect to one input while all other inputs are held fixed.
Gradient
The vector of all partial derivatives of a scalar function, pointing in the direction of steepest increase.
Chain rule
The rule that the derivative of a composition is the product of the derivatives of its parts, summed over all paths of dependence.
Computational graph
A directed acyclic graph whose nodes are intermediate values and whose edges record which operations produced them.
Jacobian
The m-by-n matrix of first partial derivatives of a function from n inputs to m outputs.
Reverse-mode automatic differentiation
An algorithm that computes vector-Jacobian products by propagating adjoints from outputs back to inputs; backpropagation is its use for neural networks.
Gradient check
A test that compares analytic gradients with finite-difference estimates to detect implementation bugs.

Check your understanding

  1. 1.

    For f(x, y) = 3x²y − y³, what is ∇f at (1, 2)?

  2. 2.

    A function maps 10 inputs to 1,000,000 outputs and you need its full Jacobian. Which mode of autodiff is cheaper?

  3. 3.

    For Y = XW with X of shape (64, 300) and W of shape (300, 100), what is the shape and formula of ∂L/∂W given G = ∂L/∂Y?

  4. 4.

    Your gradient check reports a relative error of 0.03 for one layer in float64. What is the most likely conclusion?

  5. 5.

    Why do frameworks accumulate gradients with += rather than overwrite them?

Exercises

Work each problem before opening the solution.

  1. Exercise 1Core

    Let L=(σ(wx+b)−y)2L = (\sigma(wx + b) - y)^2 with w=0.5w = 0.5, x=2x = 2, b=0b = 0, y=1y = 1. Compute LL, ∂L/∂w\partial L/\partial w and ∂L/∂b\partial L/\partial b to four decimal places.

    Show solution

    Forward: z=0.5⋅2+0=1z = 0.5 \cdot 2 + 0 = 1; y^=σ(1)≈0.7311\hat y = \sigma(1) \approx 0.7311; L=(0.7311−1)2=(−0.2689)2≈0.0723L = (0.7311 - 1)^2 = (-0.2689)^2 \approx 0.0723.

    Backward: ∂L/∂y^=2(y^−y)=−0.5379\partial L/\partial \hat y = 2(\hat y - y) = -0.5379. ∂y^/∂z=σ(1)(1−σ(1))≈0.1966\partial \hat y / \partial z = \sigma(1)(1 - \sigma(1)) \approx 0.1966. So ∂L/∂z≈−0.5379×0.1966≈−0.1058\partial L / \partial z \approx -0.5379 \times 0.1966 \approx -0.1058.

    ∂L/∂w=∂L/∂z⋅x≈−0.10575×2≈−0.2115\partial L / \partial w = \partial L/\partial z \cdot x \approx -0.10575 \times 2 \approx -0.2115; ∂L/∂b=∂L/∂z≈−0.1058\partial L / \partial b = \partial L/\partial z \approx -0.1058. (Carry full precision through the chain: ∂L/∂z=−0.105754…\partial L/\partial z = -0.105754\ldots, so the gradients are −0.21151-0.21151 and −0.10575-0.10575; doubling the already-rounded −0.1058-0.1058 would give a misleading −0.2116-0.2116.)

  2. Exercise 2Core

    In the worked 2-layer example, change the input to x=(2,1)\mathbf{x} = (2, 1) (all weights unchanged, target y=1y = 1). Run the forward and backward passes and give ∂L/∂W1\partial L/\partial W_1.

    Show solution

    Forward: z1=0.1⋅2+0.2⋅1=0.4z_1 = 0.1\cdot2 + 0.2\cdot1 = 0.4; z2=0.3⋅2−0.4⋅1=0.2z_2 = 0.3\cdot2 - 0.4\cdot1 = 0.2. Both positive, so h=(0.4,0.2)\mathbf{h} = (0.4, 0.2). y^=0.5⋅0.4−0.6⋅0.2+0.1=0.2−0.12+0.1=0.18\hat y = 0.5\cdot0.4 - 0.6\cdot0.2 + 0.1 = 0.2 - 0.12 + 0.1 = 0.18. L=12(0.18−1)2=12⋅0.6724=0.3362L = \tfrac12(0.18 - 1)^2 = \tfrac12 \cdot 0.6724 = 0.3362.

    Backward: y^ˉ=−0.82\bar{\hat y} = -0.82. hˉ=−0.82⋅(0.5,−0.6)=(−0.41,0.492)\bar{\mathbf h} = -0.82 \cdot (0.5, -0.6) = (-0.41, 0.492). Both ReLUs are active, so zˉ=(−0.41,0.492)\bar{\mathbf z} = (-0.41, 0.492).

    ∂L∂W1=zˉ x⊤=(−0.82−0.410.9840.492).\frac{\partial L}{\partial W_1} = \bar{\mathbf z}\,\mathbf x^\top = \begin{pmatrix} -0.82 & -0.41 \\ 0.984 & 0.492 \end{pmatrix}.

    Also ∂L/∂w2=−0.82⋅(0.4,0.2)=(−0.328,−0.164)\partial L/\partial \mathbf w_2 = -0.82 \cdot (0.4, 0.2) = (-0.328, -0.164) and ∂L/∂b2=−0.82\partial L/\partial b_2 = -0.82. The positive gradients in row 2 mean gradient descent will decrease unit 2's weights, reducing a unit whose output currently pushes y^\hat y down (since w2,2<0w_{2,2} < 0).

  3. Exercise 3Core

    Explain why the one-sided difference (f(x+h)−f(x))/h(f(x+h) - f(x))/h has error O(h)O(h) while the central difference has error O(h2)O(h^2). Use a Taylor expansion.

    Show solution

    Taylor: f(x±h)=f(x)±hf′(x)+h22f′′(x)±h36f′′′(x)+…f(x \pm h) = f(x) \pm h f'(x) + \tfrac{h^2}{2} f''(x) \pm \tfrac{h^3}{6} f'''(x) + \dots

    One-sided: (f(x+h)−f(x))/h=f′(x)+h2f′′(x)+O(h2)(f(x+h) - f(x))/h = f'(x) + \tfrac{h}{2} f''(x) + O(h^2), so the error is O(h)O(h).

    Central: subtracting the two expansions cancels the even terms, f(x+h)−f(x−h)=2hf′(x)+h33f′′′(x)+…f(x+h) - f(x-h) = 2hf'(x) + \tfrac{h^3}{3}f'''(x) + \dots, so (f(x+h)−f(x−h))/(2h)=f′(x)+h26f′′′(x)+…(f(x+h) - f(x-h))/(2h) = f'(x) + \tfrac{h^2}{6}f'''(x) + \dots, an O(h2)O(h^2) error.

  4. Exercise 4Challenge

    Extend the Value class from the lesson with __sub__, __pow__ (for a constant integer exponent) and exp. Use it to compute the gradient of L=(σ(w1x1+w2x2+b)−y)2L = (\sigma(w_1 x_1 + w_2 x_2 + b) - y)^2 at w1=0.3w_1 = 0.3, w2=−0.2w_2 = -0.2, b=0.1b = 0.1, x=(1,2)x = (1, 2), y=1y = 1, and confirm one entry with a central difference.

    Show solution
    import math
    
    class Value:
        def __init__(self, data, parents=(), local_grads=()):
            self.data, self.grad = data, 0.0
            self.parents, self.local_grads = parents, local_grads
        def _wrap(self, o):
            return o if isinstance(o, Value) else Value(o)
        def __add__(self, o):
            o = self._wrap(o); return Value(self.data + o.data, (self, o), (1.0, 1.0))
        def __mul__(self, o):
            o = self._wrap(o); return Value(self.data * o.data, (self, o), (o.data, self.data))
        def __sub__(self, o):
            o = self._wrap(o); return Value(self.data - o.data, (self, o), (1.0, -1.0))
        def __pow__(self, k):
            return Value(self.data ** k, (self,), (k * self.data ** (k - 1),))
        def exp(self):
            e = math.exp(self.data); return Value(e, (self,), (e,))
        def sigmoid(self):
            s = 1 / (1 + math.exp(-self.data)); return Value(s, (self,), (s * (1 - s),))
        def backward(self):
            order, seen = [], set()
            def visit(v):
                if id(v) not in seen:
                    seen.add(id(v)); [visit(p) for p in v.parents]; order.append(v)
            visit(self); self.grad = 1.0
            for v in reversed(order):
                for p, g in zip(v.parents, v.local_grads):
                    p.grad += v.grad * g
    
    def loss(w1, w2, b):
        w1, w2, b = Value(w1), Value(w2), Value(b)
        L = ((w1 * 1.0 + w2 * 2.0 + b).sigmoid() - 1.0) ** 2
        return L, (w1, w2, b)
    
    L, (w1, w2, b) = loss(0.3, -0.2, 0.1)
    L.backward()
    print(round(L.data, 6), round(w1.grad, 6), round(w2.grad, 6), round(b.grad, 6))
    h = 1e-6
    num = (loss(0.3 + h, -0.2, 0.1)[0].data - loss(0.3 - h, -0.2, 0.1)[0].data) / (2 * h)
    print(round(num, 6))
    

    By hand: z=0.3−0.4+0.1=0z = 0.3 - 0.4 + 0.1 = 0, σ(0)=0.5\sigma(0) = 0.5, L=0.25L = 0.25. ∂L/∂z=2(0.5−1)⋅0.25=−0.25\partial L/\partial z = 2(0.5 - 1)\cdot 0.25 = -0.25. So ∂L/∂w1=−0.25⋅1=−0.25\partial L/\partial w_1 = -0.25 \cdot 1 = -0.25, ∂L/∂w2=−0.25⋅2=−0.5\partial L/\partial w_2 = -0.25 \cdot 2 = -0.5, ∂L/∂b=−0.25\partial L / \partial b = -0.25. The program prints 0.25 -0.25 -0.5 -0.25 and the central difference -0.25. (exp is included as requested; the lesson's sigmoid uses its own local derivative.)

Further reading

Go further

Sources

Fact-checked and last reviewed 2026-11-05

Stay in the loop
Prometheus OS™ · Native AI OS · Built for the future © 2025 - 2026