【pyTorch学习笔记⑤】PyTorch基础·下篇

四、Tensor与Autograd

1.自动求导

在神经网络中一个重要的内容就是参数学习,而参数学习离不开求导。
PyTorch有自动求导的功能:Autograd包
torch.Tensor和torch.Function是Autograd的两个核心类,他们相互连接可以生成一个有向非循环图(称为“计算图”,是一张动态图)

2.计算图

计算图是用图形方式来表示算子与变量之间的关系,直观高效。
如下图:表达的是
y = x w ; z = y + b y=xw ; z=y+b y=xw;z=y+b

z = w x + b z=wx+b z=wx+b

mul
w
add
b
z
y
x

可以根据复合函数的链式求导法则来更新各叶子节点的梯度。
∂ z ∂ x = ∂ z a y ∂ y ∂ x = w \frac{\partial z}{\partial x}=\frac{\partial z}{ay}\frac{\partial y}{\partial x}=w xz=ayzxy=w ∂ z ∂ w = ∂ z ∂ y ∂ y ∂ w = x \frac{\partial z}{\partial w}=\frac{\partial z}{\partial y}\frac{\partial y}{\partial w}=x wz=yzwy=x ∂ z ∂ b = 1 \frac{\partial z}{\partial b}=1 bz=1
如图:(这是一个反向传播过程)

db
addBackward
dw
mulBackward
dx
dy
dz

3.标量反向传播

假设x、w、b都是标量,z=wx+b,对标量z调用backward()方法。

1)定义叶子节点及算子节点

import torch

# 定义输入张量x,初始化权重参数w,偏移量b,并设置require_grad属性为True(为了自动求导)
x = torch.tensor([2])
w = torch.randn(1,requires_grad=True)
b = torch.randn(1,requires_grad=True)
# 实现向前传播
y = x.mul(w)
z = y.add(b)
# 查看x/w/b叶子节点的requite_grad属性
print("x,w,b的requite_grad属性为:{},{},{}".format(x.requires_grad,w.requires_grad,b.requires_grad))
>>>x,w,b的requite_grad属性为:FalseTrueTrue

2)查看叶子节点、非叶子节点的其他属性

#查看非叶子节点的requres grad属性,
print("y,z的requires grad属性分别为:{},{}".format(y.requires_grad,z.requires_grad))
#因与w,b有依赖关系,故y,z的requires grad属性也是: True,True
>>>y,z的requires grad属性分别为:True,True

#查看各节点是否为叶子节点
print("x,w,b,y,z的是否为叶子节点:{},{},{},{},{}".format(x.is_leaf,w.is_leaf,b.is_leaf,y.is_leaf,z.is_leaf))
>>>x,w,b,y,z的是否为叶子节点:True,True,True,False,False

# #查看叶子节点的qrad_fn属性
print("x,w, b的grad fn属性:{},{},{}".format(x.grad_fn,w.grad_fn,b.grad_fn))
#因x,w,b为用户创建的,为通过其他张量计算得到,故x,w,b的grad_fn属性: None,None,None
>>>x,w, b的grad fn属性:None,None,None

#查看非叶子节点的arad_fn属性
print("y,z的是否为叶子节点: {},{}".format(y.grad_fn,z.grad_fn))
>>>y,z的是否为叶子节点: <MulBackward0 object at 0x000001F316579128>,<AddBackward0 object at 0x000001F316579160>

3)自动求导,实现梯度方向传播,即梯度的反向传播

# 基于z张量进行梯度反向传播,执行backward之后计算图会自动清空
z.backward()
#如果需要多次使用backward,需要修改参数retain_graph为True,此时梯度是累加的
# #z.backward(retain_graph=True)

#查看叶子节点的梯度,x是叶子节点但它无须求导,故其梯度为None
print("参数w,b,x的梯度分别为:{},{},{})".format(w.grad,b.grad,x.grad))
>>>参数w,b,x的梯度分别为:tensor([2.]),tensor([1.]),None)

#非叶子节点的梯度,执行backward之后,会自动清空
print("非叶子节点y,z的梯度分别为:{},{}".format(y.grad,z.grad))
>>>非叶子节点y,z的梯度分别为:None,None

4.非标量反向传播

当目标张量为标量时,可以调用 backward()方法且无须传人参数目标张量一般都是标量,如我们经常使用的损失值 Loss,一般都是一个标量。

但也有非标量的情况,Deep Dream 的目标值就是一个含多个元素的张量。那如何对非标量进行反向传播呢? PyTorch 有个简单的规定,不让张量( Tensor) 对张量求导,只允许标量对张量求导,因此,如果目标张量对一个非标量调用 backward(),则需要传人一个gradient 参数,该参数也是张量,而且需要与调用 backward0的张量形状相同。

那么为什么要传人一个张量 gradient 呢?
传入这个参数就是为了把张量对张量的求导转换为标量对张量的求导。
举例子来说,假设目标值为 loss=(y1,y2,…,ym),传入的参数为 v=(v1,v2,…,vm),那么就可把对 loss 的求导,转换为对 loss* v T v^{T} vT标量的求导。即把原来一0(Jacobian)乘以张量T,便可得到我们需要的梯度矩阵。
backward 函数的格式为:
backward(gradient=None, retain_graph=None, create_graph=False)

1)定义叶子节点及算子节点

import torch
#定义叶子节点张量x,形状为1x2
x= torch.tensor([[2,3]], dtype=torch.float, requires_grad=True)
#初始化Jacobian矩阵
J= torch.zeros(2 ,2)
#初始化目标张量,形状为1x2
y = torch.zeros(1, 2)
#定义y与x之间的映射关系:
#y1=x1**2+3*x2,y2=x2**2+2*x1
y[0,0] = x[0,0] ** 2 + 3 * x[0,1]
y[0,1] = x[0,1] ** 2 + 2 * x[0,0]

2)手工计算y对x的梯度

先手工计算一下y对x的梯度,验证 PyTorch 的 backward 的结果是否正确。
y对 x 的梯度是一个雅可比矩阵,我们可通过以下方法进行计算各项的值。假设x= ( x 1 = 2 , x 2 = 3 ) , y = ( y 1 = x 1 2 + 3 x 2 , y 2 = x 2 2 + 2 x 1 ) x_{1}= 2,x_{2} = 3),y = (y_{1}=x_{1}^{2} +3x_2, y_2=x_2^2+ 2x_1) x1=2,x2=3),y=(y1=x12+3x2,y2=x22+2x1) ,不难得到:
J = ( ∂ y 1 ∂ x 1 ∂ y 1 ∂ x 2 ∂ y 2 ∂ x 1 ∂ x 2 ∂ y 2 ) = ( 2 x 1 3 2 2 x 2 ) J=\left(\begin{matrix} \frac{\partial y_1}{\partial x_1}&\frac{\partial y_1 } {\partial x_2} \\ \\ \frac{\partial y_2}{\partial x_1}&\frac{\partial x_2} {\partial_{y_2}} \end{matrix}\right)= \left(\begin{matrix}2x_1&3\\\\2&2x_2\\\end{matrix}\right) J= x1y1x1y2x2y1y2x2 = 2x1232x2
x 1 = 2 , x 2 = 3 x_1=2,x_2=3 x1=2,x2=3
J = ( 4 3 2 6 ) J = \left(\begin{matrix} 4&3\\2&6 \end{matrix}\right) J=(4236)
所以:
J T = ( 4 2 3 6 ) J^T=\left(\begin{matrix} 4&2\\3&6\end{matrix}\right) JT=(4326)

3)调用backward来获取y对x的梯度

y.backward(torch.Tensor([[1,1]]))
print(x.grad)
>>>tensor([[6., 9.]])

这个结果与我们手工运算的不符,显然这个结果是错误的,那错在哪里呢?这个结果的计算过程是:
J T ⋅ v T = ( 4 2 3 6 ) ( 1 1 ) = ( 6 9 ) J^T·v^T=\left(\begin{matrix} 4&2 \\3&6\end{matrix}\right)\left(\begin{matrix} 1 \\1\end{matrix}\right)=\left(\begin{matrix} 6 \\9\end{matrix}\right) JTvT=(4326)(11)=(69)
由此可见,错在v的取值,通过这种方式得到的并不是y对 x 的梯度。
这里我们可以分成两步计算。
首先让 v=(1,0)得到 y 1 y_1 y1对x的梯度,然后使 v=(0,1),得到 y 2 y_2 y2对x的梯度。这里因需要重复使用 backward(),需要使参数 retain_graph=True,具体代码如下:

# 生成y1对x的梯度
y.backward(torch.Tensor([[1, 0]]),retain_graph=True)
J[0]=x.grad
#梯度是累加的,故需要对x的梯度清零
x.grad = torch.zeros_like(x.grad)
#生成y2对x的梯度
y.backward(torch.Tensor([[0,1]]))
J[1]=x.grad
#显示jacobian矩阵的值
print(J)

>>>tensor([[4., 3.],
        [2., 6.]])

与手工计算一致。

相关推荐

【pyTorch学习笔记①】Numpy基础·上篇
【pyTorch学习笔记②】Numpy基础·下篇
【pyTorch学习笔记③】PyTorch基础·上篇
【pyTorch学习笔记④】PyTorch基础·中篇

猜你喜欢

转载自blog.csdn.net/qq_46319397/article/details/130452238