参考https://github.com/chenyuntc/pytorch-book/tree/v1.0html
但愿你们直接到上面的网址去查看代码,下面是本人的笔记python
torch.autograd就是为了方便用户使用,专门开发的一套自动求导引擎,她可以根据输入和前向传播过程自动构建计算图,并执行反向传播c++
1.Variablegit
深度学习算法的本质是经过反向函数求导数,pytorch的Autograd模块实现了此功能。在Tensor上的全部操做,Autograd都可以为他们自动提供微分,避免手动计算的复杂过程github
其中Autograd.Variable是Autograd的核心类。Tensor被封装成Variable后,能够调用它的.backward实现反向传播,自动计算全部梯度算法
Variable的构造函数须要传入tensor,同时有两个可选参数:api
Variable支持大多数tensor支持的函数,但其不支持部分inplace函数,由于这些函数会修改tensor自身,而在反向传播中,variable须要缓存原来的tensor来计算梯度缓存
若是想要计算各个Variable的梯度,只须要调用根节点variable的backward方法,autograd会自动沿着计算图反向传播,计算每个叶子节点的梯度app
variable.backward(grad_variables=None, retain_graph=None, create_graph=None)的参数:函数
举例:
from __future__ import print_function import torch as t from torch.autograd import Variable as V
生成a:
#从tensor中建立variable,指定须要求导 a = V(t.ones(3,4), requires_grad=True) a
返回:
tensor([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]], requires_grad=True)
生成b:
b = V(t.zeros(3,4)) b
返回:
tensor([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]])
生成c,实现对a,b的操做:
#函数的使用与tensor一致 #也能够写成c = a+b c = a.add(b) c
返回:
tensor([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]], grad_fn=<AddBackward0>)
反向:
d = c.sum() d.backward()#反向传播
⚠️
#注意tensor和variable二者操做的区别 #前者在取data后变成tensor,从tensor计算sum获得float #后者计算sum后仍然是Variable c.data.sum(), c.sum()
返回:
(tensor(12.), tensor(12., grad_fn=<SumBackward0>))
查看梯度:
#反向传播后就可以查看变量的梯度了 a.grad
返回:
tensor([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]])
说明:
#此处虽然没有指定c须要求导,但c依赖于a,而a须要求导 #所以c的requires_grad属性会自动设置为True a.requires_grad, b.requires_grad, c.requires_grad
返回:
(True, False, True)
#由用户建立的variable是叶子节点,其对应的grad_fn是None a.is_leaf, b.is_leaf, c.is_leaf
返回:
(True, True, False)
#c.grad是None,c不是叶子节点,它的梯度是用来计算a的梯度的 #虽然c.requires_grad为True,可是其梯度在计算完后会被释放 c.grad is None #返回True
接下来咱们看看autograd计算的导数和咱们手动推导的区别
def f(x) : '''计算y''' y = x**2 * t.exp(x) return y def gradf(x): '''手动自动求导''' dx = 2*x*t.exp(x) + x**2*t.exp(x) return dx
生成x开始计算y:
x = V(t.randn(3,4), requires_grad=True) y = f(x) y
返回:
tensor([[0.0090, 0.3115, 1.2975, 0.4315], [0.5321, 0.3219, 0.1713, 0.3801], [0.1355, 0.0842, 0.1229, 0.4506]], grad_fn=<MulBackward0>)
反向传播:
y.backward(t.ones(y.size())) #grad_variables形状与y一致 x.grad
返回:
tensor([[-0.1716, 1.7068, 4.6518, -0.2922], [-0.0763, 1.7446, 1.1561, -0.3552], [ 0.9972, -0.4043, -0.4409, 2.1901]])
grad_variables为何这样设置,可见:pytorch的backward
#autograd的计算结果与利用公式的计算结果一致 gradf(x)
返回:
tensor([[-0.1716, 1.7068, 4.6518, -0.2922], [-0.0763, 1.7446, 1.1561, -0.3552], [ 0.9972, -0.4043, -0.4409, 2.1901]], grad_fn=<AddBackward0>)
2.计算图
pytorch中autograd的底层采用了计算图,其是一种有向无环图(DAG),用于计算算子和变量之间的关系
如表达式z = wx +b可分解成y = wx, z = y+b,其计算图以下所示:
通常用矩形表示算子,椭圆表示变量,即图中的MUL和ADD都是算子,w,x,b为变量
如这个有向无环图,X和b是叶子节点,这些节点一般由用户本身建立,不依赖于其余变量。z称为根节点,是计算图的最终目标。
利用链式法则很容易求得各个叶子节点的梯度:
有了计算图,上述链式求导便可利用计算图的反向传播自动完成,其过程以下:
在pytorch实现中,autograd会随着用户的操做,记录生成当前variable的全部操做,由此创建一个有向无环图,每个变量在图中的位置能够经过其grad_fn属性在图中的位置推测获得
用户每进行一次 操做,相应的计算图就会发生变化
每个前向传播操做的函数都有与之对应的反向传播函数用来计算输入的各个variable的梯度,这些函数的函数名一般以Backward结尾,代码实现:
x = V(t.ones(1)) b = V(t.rand(1),requires_grad=True) w = V(t.rand(1),requires_grad=True) y = w * x #等价于 y = w.mul(x) z = y + b #等价于 z = y.add(b)
查看requires_grad:
x.requires_grad,b.requires_grad,w.requires_grad
返回:
(False, True, True)
#虽然未指定y.requires_grad为True,但因为y依赖于须要求导的w #故而y.requires_grad自动设置为True y.requires_grad #返回True
查看叶子节点:
x.is_leaf,b.is_leaf,w.is_leaf
返回:
(True, True, True)
y.is_leaf, z.is_leaf
返回:
(False, False)
查看grad_fn:
#grad_fn能够查看这个variable的反向传播函数 #z是add函数的输出,因此它的反向传播函数是AddBackward z.grad_fn
返回:
<AddBackward0 at 0x116776240>
next_functions:
#next_functions保存grad_fn的输入,grad_fn的输入是一个tuple #第一个是y,它是乘法mul的输出,因此对应的反向传播函数y.grad_fn是MulBackward #第二个是b,它是叶子节点,由用户建立,grad_fn为None,可是有 z.grad_fn.next_functions
返回:
((<MulBackward0 at 0x116776c50>, 0), (<AccumulateGrad at 0x116776828>, 0))
#Variable的grad_fn对应着图中的function z.grad_fn.next_functions[0][0] == y.grad_fn #返回True
#第一个是w,叶子节点,须要求导,梯度是累加的 #第二个是x,叶子节点,不须要求导,因此为None y.grad_fn.next_functions
返回:
((<AccumulateGrad at 0x116776ba8>, 0), (None, 0))
#叶子节点的grad_fn是None w.grad_fn, x.grad_fn
返回:
(None, None)
计算w的梯度时须要用到x的数值(由于),这些数值在前向过程当中会保存下来成buffer,在计算完梯度后会自动清空。
为了可以屡次反向传播,须要指定retain_graph来保留这些buffer
y.grad_fn.saved_variables
返回错误:
AttributeError: 'MulBackward0' object has no attribute 'saved_variables'
缘由确实是版本问题,PyTorch0.3 中把许多python的操做转移到了C++中,saved_variables 如今是一个c++的对象,没法经过python访问。(https://github.com/chenyuntc/pytorch-book/issues/7)
能够查看这里进行学习https://github.com/chenyuntc/pytorch-book/blob/0.3/chapter3-Tensor和autograd/Autograd.ipynb,省掉上面的操做:
#使用retain_graph来保存buffer z.backward(retain_graph=True) w.grad
返回:
tensor([1.])
#屡次反向传播,梯度累加,这就是w中AccumulateGrad标识的含义 z.backward() w.grad
返回:
tensor([2.])
PyTorch使用的是动态图,它的计算图在每次前向传播时都是从头开始构建,因此它可以使用Python控制语句(如for、if等)根据需求建立计算图。这点在天然语言处理领域中颇有用,它意味着你不须要事先构建全部可能用到的图的路径,图在运行时才构建。
def abs(x): if x.data[0] > 0:return x else: return -x x = V(t.ones(1),requires_grad=True) y = abs(x) y.backward() x.grad
返回:
tensor([1.])
x = V(-1*t.ones(1),requires_grad=True) y = abs(x) y.backward() x.grad
返回:
tensor([-1.])
若是不使用.float(),报错:
RuntimeError: Only Tensors of floating point dtype can require gradients
若是使用data[0],报错:
IndexError: invalid index of a 0-dim tensor. Use tensor.item() to convert a 0-dim tensor to a Python number
最终正确版:
def f(x): result = 1 for ii in x: if ii.data > 0:result = ii*result #这里若是使用data[0]会报错 return result x = V(t.arange(-2, 4).float(),requires_grad=True) #这里若是不使用.float()会报错 y = f(x) # y = x[3]*x[4]*x[5] y.backward() x.grad
返回:
tensor([0., 0., 0., 6., 3., 2.])
变量的requires_grad
属性默认为False,若是某一个节点requires_grad被设置为True,那么全部依赖它的节点requires_grad
都是True。这其实很好理解,对于,x.requires_grad = True,当须要计算
时,根据链式法则,
,天然也须要求
,因此y.requires_grad会被自动标为True.
volatile=True
是另一个很重要的标识,它可以将全部依赖于它的节点所有都设为volatile=True(默认为True)
,其优先级比requires_grad=True
高。volatile=True
的节点不会求导,即便requires_grad=True
,也没法进行反向传播。对于不须要反向传播的情景(如inference,即测试推理时),该参数可实现必定程度的速度提高,并节省约一半显存,因其不须要分配空间计算梯度。
x = V(t.ones(1)) w = V(t.rand(1),requires_grad=True) y = x*w #y依赖于x,w,但x.volatile = True,w.requires_grad = True x.requires_grad, w.requires_grad, y.requires_grad
返回:
(False, True, True)
x.volatile, w.volatile, y.volatile
报错:
/anaconda2/envs/deeplearning3/lib/python3.6/site-packages/ipykernel_launcher.py:1: UserWarning: volatile was removed (Variable.volatile is always False) """Entry point for launching an IPython kernel.
返回与预期不符:
(False, False, False)
该属性已经在0.4版本中被移除了(我使用的是pytorch-1.0.1),可使用with torch.no_grad()
代替该功能,函数可使用装饰器@torch.no_grad(),即:
>>> x = torch.tensor([1], requires_grad=True) >>> with torch.no_grad(): ... y = x * 2 >>> y.requires_grad False >>> @torch.no_grad() ... def doubler(x): ... return x * 2 >>> z = doubler(x) >>> z.requires_grad False
详细内容可见https://pytorch.org/docs/master/autograd.html#locally-disable-grad
因此如今通常只使用requires_grad来查看,不使用volatile了
在反向传播过程当中非叶子节点的导数计算完以后即被清空。若想查看这些变量的梯度,有两种方法:
autograd.grad
和hook
方法都是很强大的工具,更详细的用法参考官方api文档,这里举例说明基础的使用。推荐使用hook
方法,可是在实际使用中应尽可能避免修改grad的值。
x = V(t.ones(3),requires_grad=True) y = V(t.rand(3),requires_grad=True) y = x*w # y依赖于w,而w.requires_grad = True z = y.sum() x.requires_grad, w.requires_grad, y.requires_grad
返回:
(True, True, True)
#非叶子节点grad计算完后自动清空,因此y.grad为None z.backward() x.grad, w.grad, y.grad
返回:
(tensor([0.8205, 0.8205, 0.8205]), tensor([3.]), None)
第一种方法:
#第一种方法:使用grad得到中间变量的梯度 x = V(t.ones(3), requires_grad=True) w = V(t.rand(3), requires_grad=True) y = x * w z = y.sum() #Z对y的梯度,隐式调用backward() t.autograd.grad(z, y)
返回:
(tensor([1., 1., 1.]),)
第二种方法:
#第二种方法:使用hook #hook是一个函数,输入是梯度,不该该有返回值 def varaible_hook(grad): print('y的梯度:\r\n',grad) x = V(t.ones(3), requires_grad=True) w = V(t.rand(3), requires_grad=True) y = x * w #注册hook hook_handle = y.register_hook(varaible_hook) #存储的是那个值的梯度就对那个值进行注册hook z = y.sum() z.backward() #除非你每次都要用hook,不然用完后记得移除hook hook_handle.remove()
返回:
y的梯度: tensor([1., 1., 1.])
最后再来看看variable中grad属性和backward函数grad_variables
参数的含义,这里直接下结论:
z.backward()
在必定程度上等价于y.backward(grad_y)。z.backward()
省略了grad_variables参数,是由于x = V(t.arange(0,3).float(),requires_grad=True) y = x**2 + x*2 z = y.sum() z.backward() #z是标量,即一个数,因此能够省略参数 x.grad
返回:
tensor([2., 4., 6.])
x = V(t.arange(0,3).float(), requires_grad=True) y = x**2 + x*2 z = y.sum() y_grad_variables = V(t.Tensor([1,1,1])) # dz/dy,由于y不是标量,即一个数 y.backward(y_grad_variables) #从y开始反向传播 x.grad
返回:
tensor([2., 4., 6.])
另外值得注意的是,只有对variable的操做才能使用autograd,若是对variable的data直接进行操做,将没法使用反向传播。除了对参数初始化,通常咱们不会修改variable.data的值。
在PyTorch中计算图的特色可总结以下:
Function
。grad_fn
为None。叶子节点中须要求导的variable,具备AccumulateGrad
标识,因其梯度是累加的。requires_grad
属性默认为False,若是某一个节点requires_grad被设置为True,那么全部依赖它的节点requires_grad
都为True。volatile
属性默认为False,若是某一个variable的volatile
属性被设为True,那么全部依赖它的节点volatile
属性都为True。volatile属性为True的节点不会求导,volatile的优先级比requires_grad
高。retain_graph
=True来保存这些缓存。autograd.grad
或hook
技术获取非叶子节点的值。backward
的参数grad_variables
能够当作链式求导的中间结果,若是是标量,能够省略,默认为13.扩展autograd
目前绝大多数函数均可以使用autograd
实现反向求导,但若是须要本身写一个复杂的函数,不支持自动反向求导怎么办? 写一个Function
,实现它的前向传播和反向传播代码,Function
对应于计算图中的矩形, 它接收参数,计算并返回结果。下面给出一个例子。
class Mul(Function): @staticmethod def forward(ctx, w, x, b, x_requires_grad=True): ctx.x_requires_grad = x_requires_grad ctx.save_for_backward(w, x)#存储用来反向传播的参数 output = w*x +b return output @staticmethod def backward(ctx, grad_output): w, x = ctx.save_variables grad_w = grad_output * x if ctx.x_requires_grad: grad_x = grad_output * w else: grad_x = None grad_b = grad_output * 1 return grad_w, grad_x, grad_b, None
上面这种写法是有问题的:
NameError: name 'Function' is not defined
分析以下:
__init__
,forward和backward函数都是静态方法grad_variables
Function的使用利用Function.apply(variable)
from torch.autograd import Function class MultiplyAdd(Function): @staticmethod def forward(ctx, w, x, b): print('type in forward', type(x)) ctx.save_for_backward(w, x)#存储用来反向传播的参数 output = w*x +b return output @staticmethod def backward(ctx, grad_output): w, x = ctx.saved_variables #deprecated,如今使用saved_tensors print('type in backward',type(x)) grad_w = grad_output * x grad_x = grad_output * w grad_b = grad_output * 1 return grad_w, grad_x, grad_b
调用方法一:
类名.apply(参数)
输出变量.backward()
x = V(t.ones(1)) w = V(t.rand(1),requires_grad=True) b = V(t.rand(1),requires_grad=True) print('开始前向传播') z = MultiplyAdd.apply(w, x, b) print('开始反向传播') z.backward() # x不须要求导,中间过程仍是会计算它的导数,但随后被清空 x.grad, w.grad, b.grad
警告:
/anaconda2/envs/deeplearning3/lib/python3.6/site-packages/ipykernel_launcher.py:13: DeprecationWarning: 'saved_variables' is deprecated; use 'saved_tensors' del sys.path[0]
返回:
开始前向传播 type in forward <class 'torch.Tensor'> 开始反向传播 type in backward <class 'torch.Tensor'> (None, tensor([1.]), tensor([1.]))
类名.apply(参数)
输出变量.grad_fn.apply()
x = V(t.ones(1)) w = V(t.rand(1),requires_grad=True) b = V(t.rand(1),requires_grad=True) print('开始前向传播') z = MultiplyAdd.apply(w, x, b) print('开始反向传播') # 调用MultiplyAdd.backward # 会自动输出grad_w, grad_x, grad_b z.grad_fn.apply(V(t.ones(1)))
返回:
开始前向传播 type in forward <class 'torch.Tensor'> 开始反向传播 type in backward <class 'torch.Tensor'> Out[68]: (tensor([1.]), tensor([0.8597], grad_fn=<MulBackward0>), tensor([1.]))
之因此forward函数的输入是tensor,而backward函数的输入是variable,是为了实现高阶求导。backward函数的输入输出虽然是variable,但在实际使用时autograd.Function会将输入variable提取为tensor,并将计算结果的tensor封装成variable返回。在backward函数中,之因此也要对variable进行操做,是为了可以计算梯度的梯度(backward of backward)。下面举例说明,有关torch.autograd.grad的更详细使用请参照文档。
x = V(t.Tensor([5]),requires_grad=True) y = x**2 grad_x = t.autograd.grad(y,x,create_graph=True) grad_x # dy/dx = 2 * x
返回:
(tensor([10.], grad_fn=<MulBackward0>),)
grad_grad_x = t.autograd.grad(grad_x[0],x) grad_grad_x #二阶导数 d(2x)/dx = 2
返回:
(tensor([2.]),)
这种设计虽然能让autograd
具备高阶求导功能,但其也限制了Tensor的使用,因autograd中反向传播的函数只能利用当前已经有的Variable操做。这个设计是在0.2
版本新加入的,为了更好的灵活性,也为了兼容旧版本的代码,PyTorch还提供了另一种扩展autograd的方法。
PyTorch提供了一个装饰器@once_differentiable
,可以在backward函数中自动将输入的variable提取成tensor,把计算结果的tensor自动封装成variable。有了这个特性咱们就可以很方便的使用numpy/scipy中的函数,操做再也不局限于variable所支持的操做。可是这种作法正如名字中所暗示的那样只能求导一次,它打断了反向传播图,再也不支持高阶求导。
上面所描述的都是新式Function,还有个legacy Function,能够带有__init__
方法,forward
和backwad
函数也不须要声明为@staticmethod
,但随着版本更迭,此类Function将愈来愈少遇到,在此不作更多介绍。
上面这些都是比较老的写法了,如今都不使用了,如今的声明都相似:
class StyleLoss(nn.Module): def __init__(self, target_feature): super(StyleLoss, self).__init__() self.target = gram_matrix(target_feature).detach() def forward(self, input): G = gram_matrix(input) self.loss = F.mse_loss(G, self.target) return input
backward方法都不本身写了,都使用autograd
此外在实现了本身的Function以后,还可使用gradcheck
函数来检测实现是否正确。gradcheck
经过数值逼近来计算梯度,可能具备必定的偏差,经过控制eps
的大小能够控制容忍的偏差。
下面举例说明如何利用Function实现sigmoid Function
class Sigmoid(Function): @staticmethod def forward(ctx, x): output = 1 / (1 + t.exp(-x)) ctx.save_for_backward(output) return output @staticmethod def backward(ctx, grad_output): output, = ctx.saved_tensors grad_x = output * (1 - output) *grad_output return grad_x
#采用数值逼近方式检验计算梯度的公式对不对 test_input =V(t.randn(3,4), requires_grad=True) t.autograd.gradcheck(Sigmoid.apply, (test_input,), eps=1e-3) #返回True
def f_sigmoid(x): y = Sigmoid.apply(x) y.backward(t.ones(x.size())) def f_naive(x): y = 1/(1 + t.exp(-x)) y.backward(t.ones(x.size())) def f_th(x): y = t.sigmoid(x) y.backward(t.ones(x.size()))
测试:
import time x = V(t.randn(100, 100), requires_grad=True) start_sigmoid = time.time() for i in range(100): f_sigmoid(x) end_sigmoid = time.time() print(end_sigmoid-start_sigmoid) start_naive = time.time() for i in range(100): f_naive(x) end_naive = time.time() print(end_naive-start_naive) start_th = time.time() for i in range(100): f_th(x) end_th = time.time() print(end_th-start_th)
返回:
0.01591801643371582 0.02293086051940918 0.010257244110107422
显然f_sigmoid
要比单纯利用autograd
加减和乘方操做实现的函数快很多,由于f_sigmoid的backward优化了反向传播的过程。另外能够看出系统实现的buildin接口(t.sigmoid)更快。
4.小试牛刀——用Variable实现线性回归
import torch as t from torch.autograd import Variable as V from matplotlib import pyplot as plt from IPython import display
#设置随机数种子,为了在不一样人电脑上运行时下面的输入一致 t.manual_seed(1000) def get_fake_data(batch_size=8): ''' 产生随机数据:y = x*2 + 3,加上了一些噪声''' x = t.rand(batch_size,1) * 20 y = x * 2 + (1 + t.randn(batch_size, 1))*3 return x, y
# 来看看产生x-y分布是什么样的 x, y = get_fake_data() plt.scatter(x.squeeze().numpy(), y.squeeze().numpy())
图示:
#随机初始化参数 w = V(t.rand(1,1), requires_grad=True) b = V(t.zeros(1,1), requires_grad=True) lr = 0.001 #学习率 for ii in range(8000):#8000次迭代 x, y = get_fake_data() x, y = V(x), V(y) #forward:计算loss y_pred = x.mm(w) + b.expand_as(y) loss = 0.5 * (y_pred - y) ** 2 loss = loss.sum() # backward:手动计算梯度 loss.backward() # 更新参数 w.data.sub_(lr * w.grad.data) b.data.sub_(lr * b.grad.data) #梯度清零 w.grad.data.zero_() b.grad.data.zero_() if ii%1000 == 0: # 画图 display.clear_output(wait=True) x = t.arange(0, 20).float().view(-1, 1) y = x.mm(w.data) + b.data.expand_as(x) plt.plot(x.numpy(), y.numpy()) # predicted x2, y2 = get_fake_data(batch_size=20) plt.scatter(x2.numpy(), y2.numpy()) # true data plt.xlim(0,20) plt.ylim(0,41) plt.show() plt.pause(0.5) print(w.data.squeeze(), b.data.squeeze())
返回:
tensor(2.0516) tensor(2.9800)
图示: