Pytorch问题:autograd与backward()及相关参数的理解

关于backward标量的问题

问题提出

在pytorch的官方教程中,有这么一段(我稍微修改了一些)

import torch
#x = torch.randn(3, requires_grad=True)
x = torch.tensor([-1.0, -0.1, -0.05],requires_grad=True)
print(x)
y = x * 2
index = 0
while y.data.norm() < 1000:
    y = y * 2
    index = index + 1
print(y)     #tensor([-1024.0000,  -102.4000,   -51.2000], grad_fn=<MulBackward0>)
print(index) #9
 
# Now in this case y is no longer a scalar. torch.autograd could not compute the full 
# Jacobian directly, but if we just want the Jacobian-vector product, simply pass the
# vector to backward as argument:
v = torch.tensor([1, 1, 1], dtype=torch.float)
v = torch.tensor([0.5,0.5,0.5], dtype=torch.float)
#v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)
print(x.grad)

#===>>>output
#tensor([-1.0000, -0.1000, -0.0500], requires_grad=True)
#tensor([-1024.0000,  -102.4000,   -51.2000], grad_fn=<MulBackward0>) 9
#tensor([1024., 1024., 1024.])

#===>>>output
#tensor([-1.0000, -0.1000, -0.0500], requires_grad=True)
#tensor([-1024.0000,  -102.4000,   -51.2000], grad_fn=<MulBackward0>) 9
#tensor([512., 512., 512.])

其中大意是,如果不使用v这个向量,直接使用y.backward()就会报错,即:如果在这里运行y.backward(),会得到这样一个报错:RuntimeError: grad can be implicitly created only for scalar outputs。另外,我想说的是,这里y不是一个固定的运算,因为while y.data.norm() < 1000可能需要3次运算,也可能需要5次运算。
这里v相当于各个标量(共有3个标量,构成一个向量)的系数,如果要直接对所有标量进行backward,全部改成1.0即可,用数学表达式就是

v[0]\times\frac{\partial v}{\partial x_0}|x_0, \quad v[1]\times\frac{\partial v}{\partial x_1}|x_1, \quad v[2]\times\frac{\partial v}{\partial x_2}|x_2

原因解释: Tensor 没法对 Tensor 求导

如果我们把函数写成 y=f(\textbf{x})  这样的形式,就会发现,y只有是标量时,才能对\textbf{x}求偏导;如果你有一个向量\textbf{y},我们努力点分解开来写成下面的形式,
y1=f(\textbf{x}), \quad y2=f(\textbf{x}), \quad \textbf{y}=[y1,y2]
这样两个函数,那么当你写成 \textbf{y}.backward(),那么对不起,autograd是不干活的。你只能一个一个来,分别用下面的形式才能各自独立地求导,
y1.backward(), \quad y2.backward()
此时的y1, y2必须为标量。从图的解度来说,输入输出都必须是叶结点才能反向求导,也就是输入和输出都必须是向量,比如输出的y1, y2, 输入本质上则是指x1,x2等。

但是,如果你的意思是求向量在\textbf{x}=[x_0, \dots, x_n] 处的导数,导数再弄个系数,那是可以的,上面例子中的向量v就是一个活生生的系数的例子,但必须是以pytorch能读懂的形式,

v = torch.tensor([x1, x2, x3], dtype=torch.float)
y.backward(v)

 Tensor 不能 Tensor 求导最根本的原因,是因为这会导致程序异常复杂(我想pytorch的设计者们一定尝试过)。

 

复习一下,加深理解

再看一个标量的问题,当结果out是标量时,backward()是不报错的,

x = torch.randn(3, requires_grad=True)
y = x * 2
z = y * y * 3
out = z.mean()
print(z, out)
out.backward()

否则,像如下代码,z不是标量,会报错,

x = torch.randn(3, requires_grad=True)
y = x * 2
z = y * y * 3
z.backward() #RuntimeError: grad can be implicitly created only for scalar outputs

 

那个no_grad()

有时你可能要手动计算某一步的梯度,那这一步可以通过no_grad()来避免自动求导,测试如下,

>>> import torch as t                                                       

>>> x = t.ones(2,2, requires_grad=True)                                     

>>> with t.no_grad(): 
...:     y = x + 2 
...:                                                                         

>>> y.grad_fn
>>> (这里啥都没有,因为此时y没有自动求导函数)                                             

>>> z = x + 2
>>> z.grad_fn                                                               
Out[6]: <AddBackward0 at 0x7fe89276be80>

 

requires_grad 的含义及标志位说明

在tensor.py中backward是这样定义的:
def backward(self, gradient=None, retain_graph=None, create_graph=False)
注:这里面调用的也还是autograd中的backward。
在autograd.__init__.py中是这样定义的:
def backward(tensors, grad_tensors=None, retain_graph=None, create_graph=False, grad_variables=None)

detach

如果 x 为中间输出,y = x.detach 表示创建一个与 x 相同,但requires_grad==False 的tensor, 实际上就是把y 以前的计算图 grad_fn 都消除,y自然也就成了叶节点。原先反向传播时,回传到x时还会继续,而现在回到y处后,就结束了,不继续回传求到了。另外值得注意, x和y指向同一个Tensor ,即 x.data 。而detach_() 表示不创建新变量,而是直接修改 x 本身。

retain_graph

如果retain_graph=true,就会每次运行时重新生成图。也就是说,每次 backward() 时,默认会把整个计算图free掉。一般情况下是每次迭代,只需一次 forward() 和一次 backward() , 前向运算forward() 和反向传播backward()是成对存在的,一般一次backward()也是够用的。但是不排除,由于自定义loss等的复杂性,需要一次forward(),多个不同loss的backward()来累积同一个网络的grad来更新参数。于是,若在当前backward()后,不执行forward() 而可以执行另一个backward(),需要在当前backward()时,指定保留计算图,即backward(retain_graph)。

create_graph

为True时,会构造一个导数的图,用来计算出更高阶导数结果。

这里面,我没找到一个create_graph的详细数学上能明确的解释,只好把大概的意思描述一下,比如对前面第一个例子,

y.backward(v)
print(x.grad)

y.backward(v) # --- error ----
print(x.grad)

你会在第二个backward()产生严重报错:

Exception has occurred: RuntimeError

Trying to backward through the graph a second time, but the buffers have already been freed. Specify retain_graph=True when calling backward the first time...

如果要避免报错,你只能这样用,

y.backward(v, create_graph=True)
print(x.grad)

y.backward(v)
print(x.grad)

一般情况下应该是这样:既然你可以create_graph了,那么前面那个retain_graph基本就没啥必要了(实际内置了retain_graph = create_graph)。计算的结果请大家自行难证,我觉得就是一个不断叠加的过程,没看到真正意义上的高阶导数。如果你要实现真正意义上的高阶层数呢,我给个简单的例子,请自行难证:

import torch as t
from torch.autograd import Variable as V

# ----key function----
#grad_x =t.autograd.grad(y, x, create_graph=True)
#grad_grad_x = t.autograd.grad(grad_x[0],x)
 
x = V(t.Tensor([4]), requires_grad=True)
y = x ** 2 
 
grad_x = t.autograd.grad(y, x, create_graph=True)
print(grad_x)  # dy/dx = 2*x = 8
 
grad_2x = t.autograd.grad(grad_x[0],x)
print(grad_2x) # d(2*x)/dx = 2

 

参考链接:

https://www.jianshu.com/p/cbce2dd60120
https://pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html#sphx-glr-beginner-blitz-autograd-tutorial-py
https://linlinzhao.com/tech/2017/10/24/understanding-backward()-in-PyTorch.html
https://blog.csdn.net/douhaoexia/article/details/78821428
https://zhuanlan.zhihu.com/p/29923090

 

posted @ 2019-01-09 12:49  SpaceVision  阅读(96)  评论(0编辑  收藏  举报