Autograd:自动求导

Autograd:自动求导

PyTorch 中,所有神经网络的核心就是 autograd 包,autograd 包为张量上的操作提供了自动求导机制。它是一个在运行时定义(define-by-run)的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。

torch.Tensor 是 autograd 包的核心类,如果将它的属性 requires_grad 设置为 True,那么它将会追踪对于该张量的所有操作。
当完成前向计算后,可以通过调用 backward() 方法来自动计算所有的梯度。
这个张量的所有梯度将会自动累加到 grad 属性。

要阻止一个张量被跟踪历史,可以通过调用 detach() 方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。

为了防止跟踪历史记录(和使用内存),可以将代码块包装在 with torch.no_grad(): 中,在评估模型时特别有用,因为模型可能具有 requires_grad = True 的可训练参数,但是我们不需要在此过程中对它们进行梯度的计算。

还有一个类对于 autograd 的实现非常重要,那就是 Function 类。Tensor 和 Function 相互连接生成了一个无圈图(acyclic graph),它编码了完整的计算历史。
每个张量都有一个 grad_fn 属性,该属性引用了创建 Tensor 自身的 Function(除非这个张量是用户手动创建的,即这个张量的 grad_fn 是 None)。

如果需要计算导数,可以在 Tensor 上调用 backward() 方法。如果 Tensor 是一个标量(即只包含一个元素),则不需要为 backward() 指定任何参数;但如果它有更多的元素,则需要指定一个 gradient 参数,该参数是形状匹配的张量。

x = torch.ones(2, 2, requires_grad=True)  # 创建一个张量并设置参数 requires_grad=True 以追踪其计算历史;因为 x 是手工创建的,所以 grad_fn 属性值为 None
y = x + 2  # 对张量 x 做一次运算
z = y * y * 3  # 对 y 做运算
out = z.mean()
print(x)
print(y)
print(y.grad_fn)  # y 是 x 计算的结果,同时 x 设置了梯度,所以 y 有 grad_fn 属性
print(z, out)
---------
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)
tensor([[3., 3.],
        [3., 3.]], grad_fn=<AddBackward0>)
<AddBackward0 object at 0x000001B707490BB0>
tensor([[27., 27.],
        [27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)

通过 requires_grad_() 方法可以原地改变现有张量的 requires_grad 标志;如果没有指定的话,默认输入的标志是 False。

a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
b = (a * a).sum()
print(a, a.requires_grad)  # requires_grad 默认为 False
print(b, b.grad_fn)  # 因为 a 没有设置梯度,而 b 由 a 计算得到,所以 b 的 grad_fn 属性值为 None
a.requires_grad_(True)  # 原地改变张量 a 的 requires_grad 标志
print(a, a.requires_grad)  # 有了梯度
c = (a * a).sum()  # 因为 a 设置了梯度,c 又由 a 计算得到,所以 c 有实际的 grad_fn 属性值
print(c, c.grad_fn)
---------
tensor([[-0.9174,  0.7608],
        [-1.6861,  0.8395]]) False
tensor(4.9682) None
tensor([[-0.9174,  0.7608],
        [-1.6861,  0.8395]], requires_grad=True) True
tensor(4.9682, grad_fn=<SumBackward0>) <SumBackward0 object at 0x000002E1B92A0C10>

现在进行反向传播,因为 out 是一个标量,因此 out.backward() 与 out.backward(torch.tensor(1.)) 等价。

x = torch.ones(2, 2, requires_grad=True)  
y = x + 2  
z = y * y * 3  
out = z.mean()
out.backward()  # 从 out 开始反向传播计算梯度
print(out)
print(x.grad)  # 对 x 计算梯度,输出导数 d(out)/dx
---------
tensor(27., grad_fn=<MeanBackward0>)
tensor([[4.5000, 4.5000],
        [4.5000, 4.5000]])

o u t = 1 / 4 ∗ 3 ∗ ( x + 2 ) ( x + 2 ) = 1 / 4 ( 3 x 2 + 12 x + 12 ) out=1/4*3*(x+2)(x+2)=1/4(3x^2+12x+12) out=1/43(x+2)(x+2)=1/4(3x2+12x+12),对 x 求导,得 d ( o u t ) / d x = 1 / 4 ( 6 x + 12 ) = 1.5 x + 3 d(out)/dx=1/4(6x+12)=1.5x+3 d(out)/dx=1/4(6x+12)=1.5x+3,将 x 的值代入其中,得到 [[4.5, 4.5], [4.5, 4.5]],这就是反向传播并对 x 求导的过程。

当想打印 y.grad 时会出现 UserWarning,该警告告诉我们:正在访问非叶张量的 grad 属性,在 autograd.backward() 期间不会填充其 grad 属性,如确实需要非叶张量的梯度,请在非叶张量上使用 retain_grad()。
由用户初始创建的张量(而不是根据初始张量于程序中间产生的其他张量),则为叶张量。在上面的程序中,张量 x 为叶张量,张量 y 为非叶张量。
可以通过 tensor.is_leaf 来判断该张量是否为叶张量,结果为 True 时即为叶张量,结果为 False 则为非叶张量。

x1 = torch.ones(2, 2)  # 不设置梯度
x2 = torch.rand(2, 2, requires_grad=True)  # 设置梯度
x3 = x2 * 10
x4 = x3.mean()
print(x1, x1.is_leaf)  # 叶张量(用户创建的张量)
print(x2, x2.is_leaf)  # 叶张量(用户创建的张量)
print(x3, x3.is_leaf)  # 非叶张量(中间产生的张量)
print(x4, x4.is_leaf)  # 非叶张量(中间产生的张量)
----------
tensor([[1., 1.],
        [1., 1.]]) True
tensor([[0.6571, 0.8360],
        [0.6584, 0.1944]], requires_grad=True) True
tensor([[6.5715, 8.3601],
        [6.5842, 1.9437]], grad_fn=<MulBackward0>) False
tensor(5.8649, grad_fn=<MeanBackward0>) False

为什么要区分叶张量和非叶张量?

  1. 从链式求导法则来讲

    对于一个可导函数,无论其多么复杂,一定可由若干个可导函数的四则运算组成。也就是说其中任何一个运算都是可导的,而这些运算都是由初始变量以及根据初始变量得到的中间结果变量组成。很多时候,初始变量和中间结果变量是需要区分的。

  2. 从求导目的来讲

    在深度学习或机器学习中,求导或求梯度的根本原因,是通过反向传播获取梯度,进而更新相应的参数以达到学习的目的。在上面例子中,x3 与 x4 是前向传播中计算得到的中间值,它们虽然需要求导,但是不需要更新参数。

  3. 从实际需要来讲

    将那些需要更新参数的张量设置为叶张量,在计算梯度的时候会保留它们的梯度信息,供反向传播更新参数时使用;那些不需要更新参数的中间张量,仍然会计算与之相关联的梯度,但是程序不会保留该张量对应的梯度信息,这样有利于节约计算资源。

下图是一个反向传播的示意图,在 PyTorch 中一般只保存叶节点的梯度,也就是下图中的 d、e 节点,而非叶节点(也就是中间张量,如 b、c 节点)在反向计算完成后会自动释放梯度以节省空间。
Autograd:自动求导_第1张图片

但在调试过程中,有时我们需要对中间张量的梯度进行监控,以确保网络的有效性,这个时候我们可以通过 tensor.retain_grad() 方法来获取其梯度。
tensor.retain_grad() 可以显式地保存非叶节点的梯度,代价是会增加显存的消耗。

x = torch.ones(2, 2, requires_grad=True) 
y = x + 2  
y.retain_grad()  # 保存中间张量 y 的梯度
z = y * y * 3 
z.retain_grad()  # 保存中间张量 z 的梯度
out = z.mean()
out.backward()
print(y.grad)
print(z.grad)
---------
tensor([[4.5000, 4.5000],
        [4.5000, 4.5000]])
tensor([[0.2500, 0.2500],
        [0.2500, 0.2500]])

可以通过将代码块包装在 with torch.no_grad(): 中,来阻止 autograd 跟踪设置了 requires_grad=True 的张量的历史记录。
with torch.no_grad(): 的具体行为就是停止梯度计算,从而节省 GPU 算力和显存。

x = torch.ones(2, 2, requires_grad=True)
with torch.no_grad():  # 可以将不需要计算梯度的中间张量放进这里
    y = x + 2
    z = y * y * 3
w = x * 10
print(y, y.requires_grad)
print(z, z.requires_grad)
print(w, w.requires_grad)
---------
tensor([[3., 3.],
        [3., 3.]]) False
tensor([[27., 27.],
        [27., 27.]]) False
tensor([[10., 10.],
        [10., 10.]], grad_fn=<MulBackward0>) True

你可能感兴趣的:(Pytorch,深度学习,pytorch,python,计算机视觉,神经网络)