使用Tensor 及Antograd实现机器学习
节选自《python深度学习:基于pytorch》 因对自动求导不太了解,所以特查看此书,并做好笔记.
重点是非标量反向传播,对应的是多元函数的方向传播问题!!!
要点:
- 创建叶子节点(leaf node)的Tensor,使用
requires_grad 参数指定是否记录对其的操作,以便之后利用backward() 方法进行梯度求解.requires_grad 参数的缺省值为False,如果对其求导设置为True,然后与之有依赖关系的节点会自动变为True. - 可利用
requires_grad_() 方法修改tensor的requires_grad 属性.可以调用.detach() 或with torch.no_grad(): ,姜不再计算张量的梯度,跟踪张量的历史记录.这点在评估模型、测试模型阶段中常常用到. - 通过运算创建的Tensor(即非叶子节点会自动被赋予f
grad_fn 的书ing.该属性表示梯度函数.叶子节点的grad_fn 为None. - 最后得到的Tensor执行
backward() 函数,此时自动计算各变量的梯度,并将累加结果保存到grad 的属性中.计算完成后,非叶子节点的梯度自动释放. backward() 函数接受参数,该参数和调用函数的Tensor的维度相同,或者是可以broadcast的维度.如果求导的Tensor为标量(即一个数字),则backward中的参数可以省略.- 方向传播的中间缓存会被清空,如果需要进行多次方向传播,需要指点
backward 中的参数retarin_graph=True .多次反向传播时,梯度时累加的. - 非叶子节点的梯度
backward 调用后即被清空. - 可以通过调用
torch.no_grad() 包裹代码块的形式来阻止requesgrad=True 的张量的历史记录.这步在测试阶段经常使用.
在整个过程中,pytorch采用计算图的形式进行组织,该计算图为动态图,且在每次钱箱传播时,将重新构建.其他深度学习框架,如Tensorflow\keras一般为静态图.接下来我们介绍计算图,用图的形式描述更直观了,该计算图为有向无环图.
计算图
下面通过代码实现这个计算图.
3标量反向传播
假设x、w、b都是标量,z=wx+b, 对标量z调用backward() 方法,我们无须对backward() 传入参数。 以下是实现自动求导的主要步骤: 1)定义叶子节点及算子节点:
x=torch.Tensor([2])
w=torch.randn(1 ,requires_grad=True)
b=torch.randn(1,requires_grad=True)
y=torch.mul(w,x)
z=torch.add(y,b)
print(" x,w,b的require_ grad属性分 别为: {},{},{}".format(x.requires_grad,w.requires_grad, b.requires_grad))
out: x,w,b的require_ grad属性分 别为: False,True,True
输入变量 == 叶子节点,默认没有梯度的。 算子后的量 =》 默认有梯度 输出变量 == 根节点
2)查看叶子节点、非叶子节点的其他属性。
print("y, z的requires_grad属 性分别为: {},{}".format(y.requires_grad, z.requires_grad))
print("x,w, b, y, z的是否为叶子节点: {},{},{},{}".format(x.is_leaf, w.is_leaf, b.is_leaf , y.is_leaf, z.is_leaf))
print("x, w,b的grad_fn属性: {},{},{}".format(x.grad_fn, w.grad_fn, b.grad_fn))
print("y,z的是否为叶子节点: {},{}".format(y.grad_fn,z.grad_fn))
3)自动求导,实现梯度方向传播,即梯度的反向传播。
z.backward()
print("参数w,b的梯度分别为:{},{},{}".format(w.grad,b.grad,x.grad))
print("非叶子节点y,z的梯度分别为:{},{}".format(y.grad,z.grad))
4非标量 反向传播
在2.5.3节中介绍了当目标张量为标量时,可以调用backward()方法且无须传入参数。目标张量-般都是标量,如我们经常使用的损失值Loss,-般都是一个标量。但也有非标量的情况,后面将介绍的Deep Dream的目标值就是一个含多个元素的张量。那如何对非标量进行反向传播呢? PyTorch有个简单的规定,不让张量(Tensor)对张量求导,只允许标量对张量求导,因此,如果目标张量对一个非标量调用backward(),则需要传入一个gradient 参数, 该参数也是张量,而且需要与调用backward()的张量形状相同。那么为什么要传入一个张量gradient呢?
传入这个参数就是为了把张量对张量的求导转换为标量对张量的求导。这有点拗口,我们举一个例子来说, 假设目标值为Ioss=(y1,y2,...,ym) ,传入的参数为v=(v1,v2,...,vm) ,那么就可把对loss的求导,转换为对
l
o
s
s
?
v
T
loss*v^T
loss?vT标量的求导。 即把原来
?
l
o
s
s
?
x
\frac{\partial loss}{\partial x}
?x?loss?得到的雅可比矩阵(Jacobian) 乘以张量
v
T
v^T
vT,便可得到我们需要的梯度矩阵。 backward函数的格式为: backward(gradient=None, retain_graph=None, create_graph=False) 上面说的可能有点抽象,下面来通过一 个实例进行说明 。 1)定义叶子节点及计算节点。
import torch
x= torch.tensor([[2, 3]], dtype=torch.float, requires_grad=True)
J= torch.zeros(2 ,2)
y = torch.zeros(1,2)
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的梯度
J
T
=
[
4
2
3
6
]
(2)
J^T=\left[ \begin{matrix} 4 & 2 \\ 3 & 6 \\ \end{matrix} \right]\tag{2}
JT=[43?26?](2) 3)调用backward来获取y对x的梯度
y.backward(torch.Tensor([[1, 1]]))
print(x.grad)
这个结果与我们手工运算的不符,显然这个结果是错误的,那错在哪里呢?这个结果的计算过程是:
由此可见,错在v的取值, 通过这种方式得到的并不是y对x的梯度。这里我们可以分成两步计算。首先让v=(1,0)得到y1对x的梯度,然后使v=(0,1), 得到y2对x的梯度。这里因需要重复使用backward(),需要使参数retain_ graph=True, 具体代码如下:
y.backward(torch.Tensor([[1,0]]),retain_graph=True)
J[0]=x.grad
x.grad = torch.zeros_like(x.grad)
y.backward(torch. Tensor([[0, 1]]))
J[1]=x.grad
print(J)
out: tensor([[4., 3.], [2., 6.]]) 这个结果于手工运行的式(2-5)结果一致.
|