深度学习之自动求导

发布于:2025-03-28 ⋅ 阅读:(21) ⋅ 点赞:(0)

自动求导

在这里插入图片描述
下面的例子涉及到向量:
在这里插入图片描述
下面的例子涉及到矩阵:
在这里插入图片描述
实际上,给定一个复杂的函数,我们是可以通过链式法则加上一些基本函数的求导公式是可以对该函数进行求导的。

但是最大的问题是神经网络动不动就是几百层,几乎很难手写。因此就希望能够自动求导来解决这个问题。
在这里插入图片描述
自动求导如何做出来呢?这个涉及到一个叫计算图的知识点,下面介绍何为计算图:

虽然PyTorch框架已经给我们封装好了对应的知识,但是我们还是有必要了解一下计算图内部的工作原理。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

实现

求导是几乎所有深度学习优化算法的关键步骤,对于简单的模型来说,你当然可以通过手工进行,但是对于复杂的模型,手工进行更新是一件很痛苦的事情,且经常容易出错。

深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。实际中,根据设计好的模型,系统会构建一个计算图(computational graph),来跟踪计算是哪些数据通过哪些操作组合起来产生输出。自动微分使系统能够随后反向传播梯度。这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。

下面还是举一个简单的例子进行说明:
假设我们想对函数 y = 2 x ⊤ x y=2\mathbf{x}^{\top}\mathbf{x} y=2xx关于列向量 x \mathbf{x} x求导

首先,我们创建变量x并为其分配一个初始值。

import torch

x = torch.arange(4.0)
x

在我们计算 y y y关于 x \mathbf{x} x的梯度之前,需要一个地方来存储梯度

重要的是,我们不会在每次对一个参数求导时都分配新的内存。因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。

需要注意的是,一个标量函数关于向量 x \mathbf{x} x的梯度是向量,并且与 x \mathbf{x} x具有相同的形状。

x.requires_grad_(True)  # 等价于x=torch.arange(4.0,requires_grad=True)
# 使张量x可以记录梯度计算的历史,便于进行自动求导
x.grad  # 默认值是None
# 计算出来的梯度将会存在张量的.grad属性中

在这里插入图片描述
下面通过调用反向传播函数来自动计算y关于x每个分量的梯度,打印出来看看:

y.backward()  # 求导
x.grad   # 之前的求导结果都是放在这里面的,因此可以通过该条语句访问导数

上述我们可以通过数学的方式进行验证,但是我们也可以通过代码的方式验证是否正确:

函数 y = 2 x ⊤ x y=2\mathbf{x}^{\top}\mathbf{x} y=2xx关于 x \mathbf{x} x的梯度应为 4 x 4\mathbf{x} 4x。下面快速验证这个梯度是否计算正确:

x.grad == 4 * x # 可以使用该条语句进行求导验证

在这里插入图片描述

下面来计算x的另一个函数。
默认情况下,PyTorch会累积梯度,因此在每次求解之前需要将梯度清零

# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()
# 将张量x的梯度清零,要是不清零的话,会导致梯度累积,结果是不正确的
y = x.sum() # 计算x的所有元素的和,并将结果存储在一个新的张量y中
y.backward() # 触发反向传播,计算y相对于所有需要梯度的张量的梯度
x.grad
# 求和之后,对于x[i]只有x[i]对其偏导数有贡献,其余的都没贡献

在这里插入图片描述
上述 y = ∑ k = 1 n x k = x m + x m + 1 + … + x n y=\sum_{k=1}^{n} x_k = x_m + x_{m+1} + \ldots + x_n y=k=1nxk=xm+xm+1++xn对每一个 x i x_i xi求导的话,结果都是1,因此自动求导正确。

非标量变量的反向传播

上述定义的 y y y 都是标量,当 y y y 不是标量的时候,向量y关于向量x的导数的最自然解释是一个矩阵。对于高阶和高维的 yx ,求导的结果可以是一个高阶张量。

然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括[深度学习中]),但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。这里(,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。)

# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad

在这里插入图片描述

分离计算

某些情况下,我们希望将某些计算移动到记录的计算图之外。例如,假设y是作为x的函数计算的,而z则是作为yx的函数计算的。想象一下,我们想计算z关于x的梯度,但由于某种原因,希望将y视为一个常数,并且只考虑到xy被计算后发挥的作用。

这里可以分离y来返回一个新变量u,该变量与y具有相同的值,但丢弃计算图中如何计算y的任何信息。换句话说,梯度不会向后流经ux

因此,下面的反向传播函数计算z=u*x关于x的偏导数,同时将u作为常数处理,而不是z=x*x*x关于x的偏导数。

x.grad.zero_()  # 梯度清零
y = x * x
u = y.detach()
# 切断计算图,从计算图中分离出来一个张量,这样就可以将y视为一个常数了
z = u * x

z.sum().backward()
x.grad == u # 因为切断了联系,因此求导后就只剩下了u这样一个常数了

在这里插入图片描述
由于记录了y的计算结果,随后在y上调用反向传播,得到y=x*x关于的x的导数,即2*x
在这里插入图片描述

Python 控制流的梯度计算

使用自动微分的一个好处是:即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),仍然可以计算得到的变量的梯度

在下面的代码中,while循环的迭代次数和if语句的结果都取决于输入a的值。

def f(a):
    b = a * 2
    while b.norm() < 1000:  # 要是b的二范数小于1000
        b = b * 2
    if b.sum() > 0:
        c = b
    else:
        c = 100 * b
    return c

下面计算梯度:

a = torch.randn(size=(), requires_grad=True) # 创建输入张量a并启用梯度计算
d = f(a)
d.backward() # 执行反向传播

我们现在可以分析上面定义的f函数。请注意,它在其输入a中是分段线性的。换言之,对于任何a,存在某个常量标量k,使得f(a)=k*a,其中k的值取决于输入a,因此可以用d/a验证梯度是否正确。

a.grad == d / a
# 看完函数f(a)    得d=f(a)=c,    c为b   或者  b*100
# 且 b 为a*2 或者a*(n个2    2n=实数)
# 综上得  d  最终结果都是   此处用A代表实数   a*A  
# 则   d=f(a)=Aa
# 对 d  求导得 A
# d=Aa  故d/a = 实数A     a的导数   a.grad = 实数A

在这里插入图片描述
关于上述 s i z e ( ) size() size() 的用法,大致简单介绍如下:
在这里插入图片描述

思考QA

当在神经网络中求梯度的时候,是需要正向算一遍,反向算一遍的,这是必要的
反向指的是通过计算图,求出目标函数对每个中间以及输入的导
正向指的是要把他的“公式”写出来