- Automatic Differentiation
- @tab tensorflow
- @tab pytorch
- Place our code inside an
autograd.recordscope to build the computational - graph
- @tab tensorflow
- Record all computations onto a tape
- @tab pytorch
- @tab tensorflow
- @tab pytorch
- PyTorch accumulates the gradient in default, we need to clear the previous
- values
- When we invoke
backwardon a vector-valued variabley(function ofx), - a new scalar variable is created by summing the elements in
y. Then the - gradient of that scalar variable with respect to
xis computed - @tab tensorflow
- @tab pytorch
- @tab tensorflow
- @tab pytorch
- @tab tensorflow
- @tab pytorch
Automatic Differentiation
:label:sec_autograd
As we have explained in :numref:sec_calculus,
differentiation is a crucial step in nearly all deep learning optimization algorithms.
While the calculations for taking these derivatives are straightforward,
requiring only some basic calculus,
for complex models, working out the updates by hand
can be a pain (and often error-prone).
Deep learning frameworks expedite this work by automatically calculating derivatives, i.e., automatic differentiation. In practice, based on our designed model the system builds a computational graph, tracking which data combined through which operations to produce the output. Automatic differentiation enables the system to subsequently backpropagate gradients. Here, backpropagate simply means to trace through the computational graph, filling in the partial derivatives with respect to each parameter.
A Simple Example
As a toy example, say that we are interested
in (differentiating the function
$y = 2\mathbf{x}^{\top}\mathbf{x}$
with respect to the column vector $\mathbf{x}$.)
To start, let us create the variable x and assign it an initial value.
```{.python .input} from mxnet import autograd, np, npx npx.set_np()
x = np.arange(4.0) x
```{.python .input}#@tab pytorchimport torchx = torch.arange(4.0)x
```{.python .input}
@tab tensorflow
import tensorflow as tf
x = tf.range(4, dtype=tf.float32) x
[**Before we even calculate the gradientof $y$ with respect to $\mathbf{x}$,we will need a place to store it.**]It is important that we do not allocate new memoryevery time we take a derivative with respect to a parameterbecause we will often update the same parametersthousands or millions of timesand could quickly run out of memory.Note that a gradient of a scalar-valued functionwith respect to a vector $\mathbf{x}$is itself vector-valued and has the same shape as $\mathbf{x}$.```{.python .input}# We allocate memory for a tensor's gradient by invoking `attach_grad`x.attach_grad()# After we calculate a gradient taken with respect to `x`, we will be able to# access it via the `grad` attribute, whose values are initialized with 0sx.grad
```{.python .input}
@tab pytorch
x.requiresgrad(True) # Same as x = torch.arange(4.0, requires_grad=True)
x.grad # The default value is None
```{.python .input}#@tab tensorflowx = tf.Variable(x)
(Now let us calculate $y$.)
```{.python .input}
Place our code inside an autograd.record scope to build the computational
graph
with autograd.record(): y = 2 * np.dot(x, x) y
```{.python .input}#@tab pytorchy = 2 * torch.dot(x, x)y
```{.python .input}
@tab tensorflow
Record all computations onto a tape
with tf.GradientTape() as t: y = 2 * tf.tensordot(x, x, axes=1) y
Since `x` is a vector of length 4,an inner product of `x` and `x` is performed,yielding the scalar output that we assign to `y`.Next, [**we can automatically calculate the gradient of `y`with respect to each component of `x`**]by calling the function for backpropagation and printing the gradient.```{.python .input}y.backward()x.grad
```{.python .input}
@tab pytorch
y.backward() x.grad
```{.python .input}#@tab tensorflowx_grad = t.gradient(y, x)x_grad
(The gradient of the function $y = 2\mathbf{x}^{\top}\mathbf{x}$ with respect to $\mathbf{x}$ should be $4\mathbf{x}$.) Let us quickly verify that our desired gradient was calculated correctly.
```{.python .input} x.grad == 4 * x
```{.python .input}#@tab pytorchx.grad == 4 * x
```{.python .input}
@tab tensorflow
x_grad == 4 * x
[**Now let us calculate another function of `x`.**]```{.python .input}with autograd.record():y = x.sum()y.backward()x.grad # Overwritten by the newly calculated gradient
```{.python .input}
@tab pytorch
PyTorch accumulates the gradient in default, we need to clear the previous
values
x.grad.zero_() y = x.sum() y.backward() x.grad
```{.python .input}#@tab tensorflowwith tf.GradientTape() as t:y = tf.reduce_sum(x)t.gradient(y, x) # Overwritten by the newly calculated gradient
Backward for Non-Scalar Variables
Technically, when y is not a scalar,
the most natural interpretation of the differentiation of a vector y
with respect to a vector x is a matrix.
For higher-order and higher-dimensional y and x,
the differentiation result could be a high-order tensor.
However, while these more exotic objects do show up in advanced machine learning (including [in deep learning]), more often (when we are calling backward on a vector,) we are trying to calculate the derivatives of the loss functions for each constituent of a batch of training examples. Here, (our intent is) not to calculate the differentiation matrix but rather (the sum of the partial derivatives computed individually for each example) in the batch.
```{.python .input}
When we invoke backward on a vector-valued variable y (function of x),
a new scalar variable is created by summing the elements in y. Then the
gradient of that scalar variable with respect to x is computed
with autograd.record():
y = x x # y is a vector
y.backward()
x.grad # Equals to y = sum(x x)
```{.python .input}#@tab pytorch# Invoking `backward` on a non-scalar requires passing in a `gradient` argument# which specifies the gradient of the differentiated function w.r.t `self`.# In our case, we simply want to sum the partial derivatives, so passing# in a gradient of ones is appropriatex.grad.zero_()y = x * x# y.backward(torch.ones(len(x))) equivalent to the belowy.sum().backward()x.grad
```{.python .input}
@tab tensorflow
with tf.GradientTape() as t: y = x x t.gradient(y, x) # Same as `y = tf.reduce_sum(x x)`
## Detaching ComputationSometimes, we wish to [**move some calculationsoutside of the recorded computational graph.**]For example, say that `y` was calculated as a function of `x`,and that subsequently `z` was calculated as a function of both `y` and `x`.Now, imagine that we wanted to calculatethe gradient of `z` with respect to `x`,but wanted for some reason to treat `y` as a constant,and only take into account the rolethat `x` played after `y` was calculated.Here, we can detach `y` to return a new variable `u`that has the same value as `y` but discards any informationabout how `y` was computed in the computational graph.In other words, the gradient will not flow backwards through `u` to `x`.Thus, the following backpropagation function computesthe partial derivative of `z = u * x` with respect to `x` while treating `u` as a constant,instead of the partial derivative of `z = x * x * x` with respect to `x`.```{.python .input}with autograd.record():y = x * xu = y.detach()z = u * xz.backward()x.grad == u
```{.python .input}
@tab pytorch
x.grad.zero_() y = x x u = y.detach() z = u x
z.sum().backward() x.grad == u
```{.python .input}#@tab tensorflow# Set `persistent=True` to run `t.gradient` more than oncewith tf.GradientTape(persistent=True) as t:y = x * xu = tf.stop_gradient(y)z = u * xx_grad = t.gradient(z, x)x_grad == u
Since the computation of y was recorded,
we can subsequently invoke backpropagation on y to get the derivative of y = x * x with respect to x, which is 2 * x.
```{.python .input} y.backward() x.grad == 2 * x
```{.python .input}#@tab pytorchx.grad.zero_()y.sum().backward()x.grad == 2 * x
```{.python .input}
@tab tensorflow
t.gradient(y, x) == 2 * x
## Computing the Gradient of Python Control FlowOne benefit of using automatic differentiationis that [**even if**] building the computational graph of (**a functionrequired passing through a maze of Python control flow**)(e.g., conditionals, loops, and arbitrary function calls),(**we can still calculate the gradient of the resulting variable.**)In the following snippet, note thatthe number of iterations of the `while` loopand the evaluation of the `if` statementboth depend on the value of the input `a`.```{.python .input}def f(a):b = a * 2while np.linalg.norm(b) < 1000:b = b * 2if b.sum() > 0:c = belse:c = 100 * breturn c
```{.python .input}
@tab pytorch
def f(a): b = a 2 while b.norm() < 1000: b = b 2 if b.sum() > 0: c = b else: c = 100 * b return c
```{.python .input}#@tab tensorflowdef f(a):b = a * 2while tf.norm(b) < 1000:b = b * 2if tf.reduce_sum(b) > 0:c = belse:c = 100 * breturn c
Let us compute the gradient.
```{.python .input} a = np.random.normal() a.attach_grad() with autograd.record(): d = f(a) d.backward()
```{.python .input}#@tab pytorcha = torch.randn(size=(), requires_grad=True)d = f(a)d.backward()
```{.python .input}
@tab tensorflow
a = tf.Variable(tf.random.normal(shape=())) with tf.GradientTape() as t: d = f(a) d_grad = t.gradient(d, a) d_grad
We can now analyze the `f` function defined above.Note that it is piecewise linear in its input `a`.In other words, for any `a` there exists some constant scalar `k`such that `f(a) = k * a`, where the value of `k` depends on the input `a`.Consequently `d / a` allows us to verify that the gradient is correct.```{.python .input}a.grad == d / a
```{.python .input}
@tab pytorch
a.grad == d / a
```{.python .input}#@tab tensorflowd_grad == d / a
Summary
- Deep learning frameworks can automate the calculation of derivatives. To use it, we first attach gradients to those variables with respect to which we desire partial derivatives. We then record the computation of our target value, execute its function for backpropagation, and access the resulting gradient.
Exercises
- Why is the second derivative much more expensive to compute than the first derivative?
- After running the function for backpropagation, immediately run it again and see what happens.
- In the control flow example where we calculate the derivative of
dwith respect toa, what would happen if we changed the variableato a random vector or matrix. At this point, the result of the calculationf(a)is no longer a scalar. What happens to the result? How do we analyze this? - Redesign an example of finding the gradient of the control flow. Run and analyze the result.
- Let $f(x) = \sin(x)$. Plot $f(x)$ and $\frac{df(x)}{dx}$, where the latter is computed without exploiting that $f’(x) = \cos(x)$.
:begin_tab:mxnet
Discussions
:end_tab:
:begin_tab:pytorch
Discussions
:end_tab:
:begin_tab:tensorflow
Discussions
:end_tab:
