Deep Learning Essentials

Course Content

Deep Learning Essentials

13 sections · 61 lessons

What is a computational graph? How is it useful for Deep Learning?


loss = (x·w + b − 10) squared, with x = 2, w = 3, b = 1loss = 9z = 7, grad −6target 10x·w = 6, grad −6b = 1, grad −6x = 2w = 3, grad −12
Backward walks the tree from the root: the add node passes −6 to b unchanged, and the multiply node scales it by x = 2 to give w its −12.

What you need to know

A tiny graph, by hand

Take one weight w, one bias b, an input x = 2, and a target of 10:

Text
z    = x * w + b         with w = 3, b = 1  ->  z = 7loss = (z - 10)^2                            ->  loss = 9

The graph is: x and w feed a multiply node, its output and b feed an add node giving z, then subtract and square give the loss.

To go backwards, apply the chain rule node by node:

Text
dloss/dz = 2 * (z - 10)   = -6dz/dw    = x              =  2   ->  dloss/dw = -6 * 2 = -12dz/db    = 1                     ->  dloss/db = -6 * 1 = -6

PyTorch does exactly this for you:

Python
import torchx = torch.tensor(2.0)w = torch.tensor(3.0, requires_grad=True)b = torch.tensor(1.0, requires_grad=True)loss = (x * w + b - 10) ** 2print(loss.grad_fn)          # <PowBackward0 ...>  the last node in the graphloss.backward()              # walk the graph in reverseprint(w.grad, b.grad)        # tensor(-12.) tensor(-6.)

Every tensor produced by an operation carries a grad_fn, a link to the node that made it. backward() follows those links from the loss back to the leaves. The negative gradients say: increase w and b to reduce the loss, which makes sense because z = 7 is below the target 10.

Why the graph is useful

  • Automatic gradients — for a model with millions of weights, the same mechanism gives every gradient in roughly the cost of one extra forward pass.
  • Memory planning — the framework knows which intermediate values the backward pass needs, and can free the rest. torch.no_grad() and torch.inference_mode() skip recording the graph entirely, saving memory at inference.
  • Optimisation and export — a captured graph can be compiled (operations fused, memory reused), exported to ONNX, or split across devices.

Dynamic vs static graphs

Dynamic (define-by-run)Static (define-then-run)
BuiltFresh on every forward passOnce, before running
ExamplesPyTorch eager mode, TF2 eagerTF1, tf.function, torch.compile, JAX jit
StrengthEasy debugging, Python if and loops just workEasier to optimise and deploy

Modern frameworks offer both: write eagerly, then compile for speed.

A real-life example

A team fine-tunes a speech-command model and notices GPU memory growing every batch until it crashes. The cause: they logged the loss with total += loss instead of total += loss.item(). Because loss is attached to its graph, adding it to total kept every batch's graph alive. .item() returns a plain Python number with no graph, and memory stays flat. Understanding the graph turned a mystery crash into a one-word fix.

Follow-up questions to expect

  • "Why do we call optimizer.zero_grad()?" — PyTorch adds new gradients to .grad instead of replacing them, so you clear them before each backward pass. Accumulation is useful on purpose for simulating larger batches.
  • "What does .detach() do?" — Returns a tensor with the same data but cut off from the graph, so no gradients flow through it.
  • "Is backpropagation the same as autograd?" — Backpropagation is reverse-mode automatic differentiation applied to a neural network's loss.