Pytorch : 自动求导
Posted Shilong Wang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Pytorch : 自动求导相关的知识,希望对你有一定的参考价值。
在训练神经网络时,最常用的算法是反向传播算法。在该算法中,参数(模型权重)根据损失函数相对于给定参数的梯度进行调整。损失函数计算神经网络产生的期望输出和实际输出之间的差值。目标是使损失函数的结果尽可能接近于零。该算法通过网络反向遍历来调整权重和偏差,以重新训练模型。这就是为什么它被称为反向传播。
为了计算这些梯度,PyTorch有一个内置的微分引擎torch.autograd
。它支持任何计算图的梯度自动计算。
import torch
x = torch.ones(5) # input tensor
y = torch.zeros(3) # expected output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)
Tensors, Functions and Computational graph
上面的代码定义的计算图如下:
在这个网络中,w和b是我们需要优化的参数。因此,我们需要能够计算损失函数关于这些变量的的梯度。为了做到这一点,我们设置了这些张量的requires_grad
性质。
Note: 你可以在创建一个张量时设置
require_grad
的值,或者稍后使用x.requires_grad_(True)
方法
构造计算图的作用于tensors上的函数实际上是Function
类的一个对象。该对象知道如何正向计算函数值,以及如何在反向传播步骤中计算其导数。对反向传播函数的引用存储在tensor的grad _fn
属性中
为了优化神经网络中参数的权重,我们需要计算损失函数对参数的导数,在x和y的固定值下,我们需要计算
∂
l
o
s
s
∂
w
,
∂
l
o
s
s
∂
b
\\rm \\dfrac\\partial loss\\partial w,\\dfrac\\partial loss\\partial b
∂w∂loss,∂b∂loss。为了计算这些导数,我们调用loss.backward()
,然后从w.grad
和b.grad
中获取。
我们只能获得计算图的叶节点的grad属性,这些叶节点需要设置
requires_grad
属性为true。对于我们图中的其他节点,梯度将不可获得。此外,出于性能原因,我们只能在给定图上使用一次backward()
进行梯度计算。如果我们需要在同一图上进行几次反向传播,则需要在调用backward
时设置retain_graph = True
。
Disabling gradient tracking
当我们不需要反向传播计算梯度时,可以使用torch.no_grad()
函数停止跟踪计算梯度。
# python代码
z = torch.matmul(x, w)+b
print(z.requires_grad)
with torch.no_grad():
z = torch.matmul(x, w)+b
print(z.requires_grad)
//等效的C++代码
z = torch::matmul(x, w) + b;
std::cout << z.requires_grad() << std::endl;
torch::NoGradGuard no_grad;
z = torch::matmul(x, w) + b;
std::cout << z.requires_grad() << std::endl;
对于PyTorch的C++库LibTorch,torch.no_grad在PyTorch的Python API中的等效物是
torch::NoGradGuard no_grad
。这是一个RAII线程本地守卫,用于在C++中禁用梯度计算。与Python的实现类似,通过实例化torch::NoGradGuard类的对象可以禁用LibTorch中的梯度计算。在此模式下,每个计算的结果都将具有requires_grad=false。
实现相同效果的另一种方法是在张量上使用detach()
方法
z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)
从概念上讲,autograd用由Function对象组成的有向无环图(DAG)记录数据(张量)和所有执行的操作(以及由此产生的新张量)。在这个DAG中,叶是输入张量,根是输出张量。通过从根到叶跟踪这个图,您可以使用链式法则自动计算梯度。
使用backward()
函数反向传播计算tensor的梯度时,并不计算所有tensor的梯度,而是只计算满足这几个条件的tensor的梯度:
- 类型为叶子节点
- requires_grad=True
- 依赖该tensor的所有tensor的requires_grad=True
所有满足条件的变量梯度会自动保存到对应的grad属性里。
Tensor gradients and Jacobian products
Python API
torch.autograd.grad(outputs, inputs,
grad_outputs=None,
retain_graph=None,
create_graph=False,
allow_unused=False,
is_grads_batched=False)
C++API
using namespace torch::autograd;
using torch::autograd::variable_list = std::vector<Variable>;
using torch::autograd::Variable = at::Tensor;
//variable_list就是std::vector<at::Tensor>
variable_list torch::autograd::grad
(const variable_list &outputs,
const variable_list &inputs,
const variable_list &grad_outputs = ,
c10::optional<bool> retain_graph = c10::nullopt,
bool create_graph = false, bool allow_unused = false)
计算并返回输出对输入的梯度。
outputs
: 可微函数的输出inputs
: 输入,计算的输出相对于输入的梯度(不会积累到at::Tensor::grad).grad_outputs
: Jacobian-vector乘积中的向量。通常是相对于每个输出的预先计算的梯度。标量Tensors 或者不需要计算梯度的张量可以设置为torch::Tensor().retain_graph
: 如果为false,则用于计算grad的图形将被释放。请注意,在几乎所有情况下,将此选项设置为true是不必要的,而且通常可以以更高效的方式解决。默认值为create_graph的值。create_graph
: 如果为真,则将构建导数图,从而允许计算更高阶导数乘积。默认值为:false。allow_unused
: 如果设置为false,则在计算输出与输入无关(因此它们的梯度始终为零),则报错。默认为false。is_grads_batched
: 目前为python特有参数。如果为True,grad_output
的第一个维度将被解释为批处理维度。我们不再计算单个的向量-雅可比矩阵积,而是为批中的每个向量计算一批向量-雅可比矩阵积。我们使用vmap
原型特性作为后端来向量化对autograd
引擎的调用,这样计算就可以在单个调用中执行。与手动循环和多次向后执行相比,这应该会导致性能改进。
函数行为
输入
m
m
m 个输入向量
x
i
x_i
xi ,
n
n
n 个输出向量
y
j
y_j
yj以及 以及
n
n
n 个权重向量
v
j
v_j
vj(形状与对应
y
j
y_j
yj一致),函数会输出
m
m
m 个向量的元组
(
g
1
,
g
2
,
⋯
,
g
m
)
(g_1,g_2,\\cdots,g_m)
(g1,g2,⋯,gm),细节为 :
输入:
x
⃗
i
,
y
⃗
j
,
v
⃗
j
;
i
=
1
,
⋯
,
m
,
j
=
1
,
⋯
,
n
;
y
⃗
j
,
v
⃗
j
\\vecx_i,\\vecy_j,\\vecv_j;i=1,\\cdots,m,j=1,\\cdots,n;\\vecy_j,\\vecv_j
xi,yj,vj;i=1,⋯,m,j=1,⋯,n;yj,vj形状一致
令
y
=
∑
j
=
1
n
v
⃗
j
⋅
y
⃗
j
y=\\sum_j=1^n\\vecv_j\\cdot\\vecy_j
y=j=1∑nvj⋅yj
其中
(
⋅
)
(\\cdot)
(⋅) 为向量内积运算
对任意
i
∈
1
,
2
,
…
,
m
i\\in\\1,2,\\ldots,m\\
i∈1,2,…,m,记
x
⃗
i
=
(
x
i
1
,
x
i
2
,
…
,
x
i
k
i
)
\\vecx_i=(x_i_1,x_i_2,\\ldots,x_i_k_i)
xi=(xi1,xi2,…,xiki),则
g
⃗
i
=
(
∂
y
∂
x
i
1
,
…
,
∂
y
∂
x
i
k
i
)
\\vecg_i=\\left(\\dfrac\\partial y\\partial x_i_1,\\ldots,\\dfrac\\partial y\\partial x_i_k_i\\right)
gi=(∂xi1∂y,…,∂xiki∂y)
输出元组
g
=
(
g
⃗
1
,
g
⃗
2
,
…
,
g
⃗
m
)
\\mathbfg=(\\vecg_1,\\vecg_2,\\ldots,\\vecg_m)
g=(Pytorch Note6 自动求导Autograd
全部笔记的汇总贴:Pytorch Note 快乐星球
自动求导
用Tensor训练网络很方便,但有时候反向传播过程需要手动实现。这对于像线性回归等较为简单的模型来说,还可以应付,但实际使用中经常出现非常复杂的网络结构,此时如果手动实现反向传播,不仅费时费力,而且容易出错,难以检查。torch.autograd就是为方便用户使用,而专门开发的一套自动求导引擎,它能够根据输入和前向传播过程自动构建计算图,并执行反向传播。
计算图(Computation Graph)是现代深度学习框架如PyTorch和TensorFlow等的核心,其为高效自动求导算法——反向传播(Back Propogation)提供了理论支持,了解计算图在实际写程序过程中会有极大的帮助。
import torch
from torch.autograd import Variable
简单情况的自动求导
下面我们显示一些简单情况的自动求导,"简单"体现在计算的结果都是标量,也就是一个数,我们对这个标量进行自动求导。
x = Variable(torch.Tensor([2]), requires_grad=True)
y = x + 2
z = y ** 2 + 3
print(z)
tensor([19.], grad_fn=)
通过上面的一些列操作,我们从 x 得到了最后的结果out,我们可以将其表示为数学公式
z = ( x + 2 ) 2 + 3 z = (x + 2)^2 + 3 z=(x+2)2+3
那么我们从 z 对 x 求导的结果就是
∂
z
∂
x
=
2
(
x
+
2
)
=
2
(
2
+
2
)
=
8
\\frac{\\partial z}{\\partial x} = 2 (x + 2) = 2 (2 + 2) = 8
∂x∂z=2(x+2)=2(2+2)=8
如果你对求导不熟悉,可以查看以下网址进行复习
# 使用自动求导
z.backward()
print(x.grad)
tensor([8.])
对于上面这样一个简单的例子,我们验证了自动求导,同时可以发现发现使用自动求导非常方便。如果是一个更加复杂的例子,那么手动求导就会显得非常的麻烦,所以自动求导的机制能够帮助我们省去麻烦的数学计算,下面我们可以看一个更加复杂的例子。
x = Variable(torch.randn(10, 20), requires_grad=True)
y = Variable(torch.randn(10, 5), requires_grad=True)
w = Variable(torch.randn(20, 5), requires_grad=True)
out = torch.mean(y - torch.matmul(x, w)) # torch.matmul 是做矩阵乘法
out.backward()
如果你对矩阵乘法不熟悉,可以查看下面的网址进行复习
# 得到 x 的梯度
print(x.grad)
# 得到 y 的的梯度
print(y.grad)
# 得到 w 的梯度
print(w.grad)
上面数学公式就更加复杂,矩阵乘法之后对两个矩阵对应元素相乘,然后所有元素求平均,有兴趣的同学可以手动去计算一下梯度,使用 PyTorch 的自动求导,我们能够非常容易得到 x, y 和 w 的导数,因为深度学习中充满大量的矩阵运算,所以我们没有办法手动去求这些导数,有了自动求导能够非常方便地解决网络更新的问题。
复杂情况的自动求导
上面我们展示了简单情况下的自动求导,都是对标量进行自动求导,可能你会有一个疑问,如何对一个向量或者矩阵自动求导了呢?感兴趣的同学可以自己先去尝试一下,下面我们会介绍对多维数组的自动求导机制。
m = Variable(torch.FloatTensor([[2, 3]]), requires_grad=True) # 构建一个 1 x 2 的矩阵
n = Variable(torch.zeros(1, 2)) # 构建一个相同大小的 0 矩阵
print(m)
print(n)
Variable containing:
2 3
[torch.FloatTensor of size 1x2]
Variable containing:
0 0
[torch.FloatTensor of size 1x2]
# 通过 m 中的值计算新的 n 中的值
n[0, 0] = m[0, 0] ** 2
n[0, 1] = m[0, 1] ** 3
print(n)
Variable containing:
4 27
[torch.FloatTensor of size 1x2]
将上面的式子写成数学公式,可以得到
n
=
(
n
0
,
n
1
)
=
(
m
0
2
,
m
1
3
)
=
(
2
2
,
3
3
)
n = (n_0,\\ n_1) = (m_0^2,\\ m_1^3) = (2^2,\\ 3^3)
n=(n0, n1)=(m02, m13)=(22, 33)
下面我们直接对 n 进行反向传播,也就是求 n 对 m 的导数。
这时我们需要明确这个导数的定义,即如何定义
∂
n
∂
m
=
∂
(
n
0
,
n
1
)
∂
(
m
0
,
m
1
)
\\frac{\\partial n}{\\partial m} = \\frac{\\partial (n_0,\\ n_1)}{\\partial (m_0,\\ m_1)}
∂m∂n=∂(m0, m1)∂(n0, n1)
在 PyTorch 中,如果要调用自动求导,需要往backward()
中传入一个参数,这个参数的形状和 n 一样大,比如是
(
w
0
,
w
1
)
(w_0,\\ w_1)
(w0, w1),那么自动求导的结果就是:
∂
n
∂
m
0
=
w
0
∂
n
0
∂
m
0
+
w
1
∂
n
1
∂
m
0
\\frac{\\partial n}{\\partial m_0} = w_0 \\frac{\\partial n_0}{\\partial m_0} + w_1 \\frac{\\partial n_1}{\\partial m_0}
∂m0∂n=w0∂m0∂n0+w1∂m0∂n1
∂
n
∂
m
1
=
w
0
∂
n
0
∂
m
1
+
w
1
∂
n
1
∂
m
1
\\frac{\\partial n}{\\partial m_1} = w_0 \\frac{\\partial n_0}{\\partial m_1} + w_1 \\frac{\\partial n_1}{\\partial m_1}
∂m1∂n=w0∂m1∂n0+w1∂m1∂n1
n.backward(torch.ones_like(n)) # 将 (w0, w1) 取成 (1, 1)
print(m.grad)
Variable containing:
4 27
[torch.FloatTensor of size 1x2]
通过自动求导我们得到了梯度是 4 和 27,我们可以验算一下
∂
n
∂
m
0
=
w
0
∂
n
0
∂
m
0
+
w
1
∂
n
1
∂
m
0
=
2
m
0
+
0
=
2
×
2
=
4
\\frac{\\partial n}{\\partial m_0} = w_0 \\frac{\\partial n_0}{\\partial m_0} + w_1 \\frac{\\partial n_1}{\\partial m_0} = 2 m_0 + 0 = 2 \\times 2 = 4
∂m0∂n=w0∂m0∂n0+w1∂m0∂n1=2m0+0=2×2=4
∂
n
∂
m
1
=
w
0
∂
n
0
∂
m
1
+
w
1
∂
n
1
∂
m
1
=
0
+
3
m
1
2
=
3
×
3
2
=
27
\\frac{\\partial n}{\\partial m_1} = w_0 \\frac{\\partial n_0}{\\partial m_1} + w_1 \\frac{\\partial n_1}{\\partial m_1} = 0 + 3 m_1^2 = 3 \\times 3^2 = 27
∂m1以上是关于Pytorch : 自动求导的主要内容,如果未能解决你的问题,请参考以下文章
(d2l-ai/d2l-zh)《动手学深度学习》pytorch 笔记前言(介绍各种机器学习问题)以及数据操作预备知识Ⅱ(线性代数微分自动求导)