PyTorch Fundamentals · lesson 08/9
Autograd: Automatic Differentiation
Autograd records every operation applied to a tensor with requires_grad=True, then replays that graph in reverse to compute exact gradients. It is the machinery that turns a loss value into a weight update.
The idea
Mark a leaf tensor with requires_grad=True, run the forward pass, call loss.backward(), and read .grad. Autograd builds the graph dynamically as your code runs — there is no separate graph definition and no compilation step.
loss.backward()fills.gradon every tensor that required grad and fed intoloss.θ.gradhas exactly the same shape asθ.- Gradients accumulate by default. Call
optimizer.zero_grad()(or set.grad = None) before the next backward, or updates compound. loss.backward()needs a scalar. For a non-scalar, pass agradient=argument of matching shape to seed it.- Wrap evaluation in
torch.no_grad()to skip graph construction and save memory. .detach()returns a tensor that shares data but is cut off from the graph.
Under the hood it is the chain rule applied edge by edge in reverse topological order, which is why backward costs roughly one extra forward pass.
Worked example
Fit y = 2x + 1 from three points with one linear layer:
- Inputs
x = [[0.], [1.], [2.]], targetsy = [[1.], [3.], [5.]] - Forward:
pred = x @ W + b, thenloss = ((pred - y) ** 2).mean(), a 0-dim tensor loss.backward()setsweight.gradto shape(1, 1)andbias.gradto shape(1,)optimizer.step()moves each parameter by-lr * grad, andoptimizer.zero_grad()clears the grads for the next round
A gradient check on f(x) = x³ at x = 2 returns 12.0 exactly, matching 3x² — not a finite-difference approximation.
In code
import torch
x = torch.tensor(2.0, requires_grad=True)
(x**3).backward()
print(x.grad) # tensor(12.) exact: 3x^2 at x = 2
x.grad = None # clear, then reuse the same leaf
(x * 4).backward()
print(x.grad) # tensor(4.)
w = torch.randn(1, 1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
pred = torch.tensor([[0.], [1.], [2.]]) @ w + b
loss = ((pred - torch.tensor([[1.], [3.], [5.]])) ** 2).mean()
loss.backward()
print(w.grad.shape, b.grad.shape) # torch.Size([1, 1]) torch.Size([1])Check yourself
- Why does
loss.backward()fail whenlossis a non-scalar tensor? - If you forget
optimizer.zero_grad(), what happens tow.gradafter two backward passes? - What does
x.detach()do, and when would you use it?
Key takeaways
- Set
requires_grad=True, callbackward()on a scalar loss, read.grad. - Gradients accumulate, so clear them between optimization steps.
no_grad()anddetach()both stop tracking; the first for evaluation, the second for data handoffs.