【d2l】2.5.自动微分

【d2l】2.5.自动微分

这一节看似只讲了torch的反向传播接口,但真正想理解还是得有一个计算图的概念

简单的例子

现在有一个向量\(x\),我需要计算\(y = 2x^\top x\)关于\(x\)求导,实现如下

import torch

x = torch.arange(4., requires_grad = True)
x, x.grad

y = 2 * torch.dot(x, x)
y

y.backward()
x.grad

结果如下

(tensor([0., 1., 2., 3.], requires_grad=True), None)

tensor(28., grad_fn=<MulBackward0>)

tensor([ 0.,  4.,  8., 12.])

结果的梯度应当是\(4x\),验证一下

x.grad == 4 * x
tensor([True, True, True, True])

结果看着简单,但其实要有一点意识,中间的计算图发生了什么

首先\(y\)是这样子算出来的:

x ──► dot(x, x) ──► *2 ──► y

反向传播是通过链式法则对于这些原子操作求梯度

由于整个计算图的起点是\(x\),并且梯度是作用在\(x\)上面的,因而最终是x.grad

下面给一个稍微复杂的例子

z = torch.dot(x, torch.exp(x))
z

结果如下

tensor(77.7530, grad_fn=<DotBackward0>)

计算梯度

x.grad.zero_() # 重新计算梯度需要把原先的计算图清零
z.backward()
x.grad
tensor([ 1.0000,  5.4366, 22.1672, 80.3421])

这个结果是

\[\nabla_x z = (x + 1) \odot e^x \]

这是个哈达玛积而非点乘,我一开始写成了点乘没有意识到问题在哪

  • 首先这个结果是个向量而非标量,因而从代数对象来看肯定是有问题的
  • 另外偏导的过程中,只有对应项有贡献,其他都是常数,因此不会参与求导,即

\[\frac{\partial z}{\partial x_k} = \frac{\partial}{\partial x_k}(x_k e^{x_k}) \]

所以验证的逻辑表达式应当为

# 错误写法 x.grad == torch.dot(x + 1, torch.exp(x))
x.grad == (x + 1) * torch.exp(x)
tensor([True, True, True, True])

接下来是一个衔接的函数,.sum()

x.grad.zero_()
y = x.sum()
y.backward()
x.grad
tensor([1., 1., 1., 1.])

非标量变量的反向传播

前面得到的结果都是标量,反向传播的过程很自然

如果结果是一个向量,想要反向传播需要先压扁

x.grad.zero_()
y = x * x
y.sum().backward()
x.grad

这里没有直接y.backward()而是先求和,就是因为标量无法直接反向传播,需要先另外处理

分离计算

接下来的问题是,在某种情况下,我们需要把某个过程量常数化,而不是沿着这个过程量进行反向传播,因而要采用一个detach()接口

x.grad.zero_()
y = x * x
u = y.detach()
z = u * x

z.sum().backward()
x.grad == u
tensor([True, True, True, True])

本来\(z = x^3\),但是由于中间变量\(y = x^2\)被常数化,让\(z = u x\),进而梯度为\(u\)

在计算图中发生了这样的事情:

x ──► y = x*x   (图在这里断了)
        │
        └──► u = y.detach() ──► z = u*x
                         ↑
                     视为常量

由于\(y\)的计算结果还是存在的,因此还是可以从\(y\)开始反向传播

x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
tensor([True, True, True, True])

Python控制流的梯度计算

这个部分展示了自动微分的高度可能性,对于Python的条件、循环、任意函数调用,自动微分都可以进行

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

接下来计算梯度

a = torch.randn(size = (), requires_grad = True)
d = f(a)
d.backward()

这个函数可以知道对于任何\(a\),最终的\(f(a)\)都是关于\(a\)的线性函数

因此验证可以用

a.grad == d / a
tensor(True)

可以得知,自动微分可以从计算上让任何对象都得到对应的偏导,哪怕可能是不可导的,这在计算上提供了巨大的可能性

小练习

题目是对于\(f(x) = \sin x\)求导并画出来,需要采用自动微分

\(f(x)\)之后的结果显然是一个向量,求和即可

import sys
sys.path.append("..")

import torch
import math
from d2l_local import torch as d2l

x = torch.linspace(-2 * math.pi, 2 * math.pi, 400, requires_grad = True)

y = torch.sin(x)

# 由于y此时是个向量,而且每个x视作等效,直接求和即可,本处采用另一种写法
y.backward(torch.ones_like(y))

dy_dx = x.grad

# 此处的detach()仅仅是为了表示不需要考虑梯度的问题,事实上是可以去掉的
d2l.plot(
    x.detach(),
    [y.detach(), dy_dx.detach()],
    legend = ['f(x) = sin(x)', 'df(x)/dx (autograd)'],
    xlabel = 'x',
    ylabel = 'value'
)

posted @ 2025-12-19 16:08  R4y  阅读(3)  评论(0)    收藏  举报