Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

PyTorch Autograd

Requirement already satisfied: torchviz in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (0.0.3)
Requirement already satisfied: torch==2.13.0 in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (2.13.0)
Requirement already satisfied: filelock in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (from torch==2.13.0) (4.0.1)
Requirement already satisfied: typing-extensions>=4.10.0 in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (from torch==2.13.0) (4.16.0)
Requirement already satisfied: setuptools>=77.0.3 in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (from torch==2.13.0) (84.0.0)
Requirement already satisfied: sympy>=1.13.3 in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (from torch==2.13.0) (1.14.0)
Requirement already satisfied: networkx>=2.5.1 in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (from torch==2.13.0) (3.4.2)
Requirement already satisfied: jinja2 in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (from torch==2.13.0) (3.1.6)
Requirement already satisfied: fsspec>=0.8.5 in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (from torch==2.13.0) (2026.9.0)
Requirement already satisfied: graphviz in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (from torchviz) (0.21)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (from sympy>=1.13.3->torch==2.13.0) (1.3.0)
Requirement already satisfied: MarkupSafe>=2.0 in /Users/jcolen/miniconda3/envs/pytorch/lib/python3.10/site-packages (from jinja2->torch==2.13.0) (3.0.3)
Apple Metal Performance Shaders framework is available, using mps

Concepts of PyTorch Autograd

  • requires_grad enables gradient tracking

  • grad_fn enables gradient backpropagation

  • is_leaf defines whether a tensor is a leaf tensor

x: tensor([2., 3.], device='mps:0', requires_grad=True)
y: tensor([1., 2.], device='mps:0', requires_grad=True)
c: tensor([3., 4.], device='mps:0', requires_grad=True)
z: tensor([ 5., 10.], device='mps:0', grad_fn=<AddBackward0>)
loss: 46.59133529663086
tensor(46.5913, device='mps:0', grad_fn=<PowBackward0>)

A leaf tensor is a tensor that does not have a grad_fn. It is not further differentiable. It is typically a network parameter that has been initialized by the user, and does not originate as the result of some computation.

A root tensor is the start of what we will call the computational graph. It is what we call backward() from to begin gradient backpropagation.

Is x a leaf tensor: True	tensor([2., 3.], device='mps:0', requires_grad=True)
Is y a leaf tensor: True	tensor([1., 2.], device='mps:0', requires_grad=True)
Is c a leaf tensor: True	tensor([3., 4.], device='mps:0', requires_grad=True)
Is z a leaf tensor: False	tensor([ 5., 10.], device='mps:0', grad_fn=<AddBackward0>)
Is loss a leaf tensor: False	46.59133529663086

Break the above down into elementary computations

z=mean(x∗y+c)\begin{align*} z = \text{mean}(x * y + c) \end{align*}
Is x a leaf tensor: True	tensor([2., 3.], device='mps:0', requires_grad=True)
Is y a leaf tensor: True	tensor([1., 2.], device='mps:0', requires_grad=True)
Is c a leaf tensor: True	tensor([3., 4.], device='mps:0', requires_grad=True)
Is r1 a leaf tensor: False	tensor([2., 6.], device='mps:0', grad_fn=<MulBackward0>)
Is w a leaf tensor: False	tensor([ 5., 10.], device='mps:0', grad_fn=<AddBackward0>)
Is z a leaf tensor: False	7.5
gradient of x: [0.5, 1.0]
gradient of y: [1.0, 1.5]
gradient of c: [0.5, 0.5]
Loading...

Another example

We can compute gradients with non-scalar root nodes

Q=3a3−b2\begin{align*} Q &= 3 a^3 - b^2 \\ \end{align*}

Here, the ground truth should be

∂Q∂a=9a2∂Q∂b=−2b\begin{align*} \frac{\partial Q}{\partial a} &= 9 a^2 \\ \frac{\partial Q}{\partial b} &= -2 b \\ \end{align*}
torch.Size([2])

We can call backward(gradient="something") which defines the running gradient with respect to each output tensor element.

gradient[0]=∂L∂Q0=1gradient[1]=∂L∂Q1=1\begin{align*} \texttt{gradient}[0] &= \frac{\partial L}{\partial Q_0} = 1 \\ \texttt{gradient}[1] &= \frac{\partial L}{\partial Q_1} = 1 \end{align*}
tensor([True, True], device='mps:0')
tensor([True, True], device='mps:0')
Loading...

Loss computation and gradient tracking

z=w x+bL=(y−z)2\begin{align*} z &= w \, x + b \\ L &= (y - z)^2 \end{align*}

Here, we expect the ground truth to be

∂L∂w=2x (z−y)∂L∂b=2(z−y)\begin{align*} \frac{\partial L}{\partial w} &= 2 x\,(z-y) \\ \frac{\partial L}{\partial b} &= 2 (z-y) \end{align*}
dL/dw = -6.300000190734863, should be tensor([-6.3000], grad_fn=<MulBackward0>)
dL/db = -12.600000381469727, should be tensor([-12.6000], grad_fn=<MulBackward0>)
Loading...

Using this in a torch neural network

Inputs:  tensor([0.0331])
Outputs: tensor([0.2693], grad_fn=<TanhBackward0>)
Loading...

Comparing different ways to compute gradients

Consider the sigmoid activation function below. Its computational graph will break it down into elementary computations

σ(x)=11+e−x\sigma(x) = \frac{1}{1 + e^{-x}}
Loading...

This is a lot of operations for a single activation. Backprop has to handle at least four compositions

a(z)=−zb(a)=eac(b)=1+bd(c)=1/c\begin{align} a(z) &= -z \\ b(a) &= e^a \\ c(b) &= 1 + b \\ d(c) &= 1 / c \\ \end{align}
Analytical derivative: 0.1966119259595871
PyTorch Autograd: 0.1966119408607483
Manual Reverse-Mode: 0.19661197066307068
Manual Forward-Mode: 0.19661197066307068

Manually coding a 1-hidden layer NN

With autograd:
w1_grad = tensor([[0.0026, 0.0051],
        [0.0069, 0.0135],
        [0.0044, 0.0086],
        [0.0034, 0.0068]])
w2_grad = tensor([[0.0079, 0.0081, 0.0210, 0.0165]])
-------------
Manually (reverse-mode):
w1_grad = tensor([[0.0026, 0.0051],
        [0.0069, 0.0135],
        [0.0044, 0.0086],
        [0.0034, 0.0068]], grad_fn=<MmBackward0>)
w2_grad = tensor([[0.0079, 0.0081, 0.0210, 0.0165]], grad_fn=<MmBackward0>)

Note that we’re doing this “reverse-mode automatic differentiation” by starting with derivatives from the loss and propagating backward.

∂L∂w1∼∂L∂z2∂z2∂z1∂z1∂w1\begin{align*} \frac{\partial L}{\partial w_1} &\sim \frac{\partial L}{\partial z_2} \frac{\partial z_2}{\partial z_1} \frac{\partial z_1}{\partial w_1} \end{align*}

That is, we’re performing the above operation from left to right. For a single example, the size of each tensor is

∼(∂L∂z2⏟1×O⋅∂z2∂z1⏟O×H)∂z1∂w1⏟H×I\begin{align*} &\sim \bigg( \underbrace{\frac{\partial L}{\partial z_2}}_{1 \times O} \cdot \underbrace{\frac{\partial z_2}{\partial z_1}}_{O \times H} \bigg) \underbrace{\frac{\partial z_1}{\partial w_1}}_{H \times I} \end{align*}

Thus, the total number of operations here is H O+H I=H(O+I)H \, O + H\, I = H (O + I)

Another way to do it would be to do “forward-mode automatic differentiation,” which performs the above operation from right to left.

∼∂L∂z2⏟1×O(∂z2∂z1⏟O×H⋅∂z1∂w1⏟H×I)\begin{align*} &\sim \underbrace{\frac{\partial L}{\partial z_2}}_{1 \times O} \bigg( \underbrace{\frac{\partial z_2}{\partial z_1}}_{O \times H} \cdot \underbrace{\frac{\partial z_1}{\partial w_1}}_{H \times I} \bigg) \end{align*}

Here, the total number of operations is O H I+O I=O I(1+H)O\,H\,I + O\,I = O\,I(1 + H)

Suppose we have a contracting computation, i.e. I≥H≥O≥1I \ge H \ge O \ge 1. Then IO≥I+OI O \ge I + O, and we expect reverse-mode automatic differentiation to be more efficient than forward-mode. Typically, if ∣L∣≪I|L| \ll I, reverse-mode is more efficient, while if ∣L∣≫I|L| \gg I, forward-mode is more efficient. The latter is a less common situation, and in fact PyTorch’s forward-mode autodiff is still in beta.

Autograd can compute derivatives of functions for us

<Figure size 960x720 with 1 Axes>

This framework extends pretty far, for example to 2D images

<Figure size 960x720 with 3 Axes>
<Figure size 960x720 with 4 Axes>
Loading...