Torch已深度学习框架被熟知,但它首先是作为Numpy的存在。我们首先比较一下Torch和Numpy有什么不同,为什么可以实现深度学习。
从数据结构看起。Numpy的强大之处就在于array的数据结构,它是多维数组,要求所有元素是相同类型的,这样就可以以矩阵运算代替for循环,提高效率。相比之下,python原生的list支持元素是不同的数据类型,而在实现上list使用了指针的方法从而增加了内存(不连续)和CPU的消耗。
numpy还支持数组与标量的运算,维度不完全一致的数组直接的运算(广播)
#ndarray可与标量直接运算,数组之间也可矢量化运算
a=np.arange(0.0,1,0.1)
print (a+10,type(a))
b=np.arange(10,20,1)
print (a+b)
#ndarray可对维数不同的矢量进行广播计算
a=np.arange(1,4)
b=np.array([[1,2,3],[4,5,6]])
print (a+b)
既然Numpy及其array已经做到了较高的效率,那Pytorch还可以从哪方面改进呢?我们对比一下Tensor和ndarray的区别,发现Tensor复杂多了:
ndarray只保存了最大最小值,shape,size等变量。T
ensor记录了很多和grad相关的变量:_grad,_grad_fn,grad,grad_fn,requires_grad;和GPU相关的变量:device,is_cuda;还有与数据结构有关的:is_leaf,is_sparse。这意味着Pytorch从一开始就是为了深度学习而准备的。
初始化方法
x = torch.tensor([5.5, 3])
x = torch.rand(5, 3)
x = torch.empty(5, 3)
x = torch.zeros(5, 3, dtype=torch.long)
梯度的计算
对于计算图(Computational Graphs),前向传导是很自然的:前一个的输出作为下一个的输入。难点在于如何对梯度反向传播。其实,在forward的过程中可以为backward做一些准备工作:用一个标记位记录每个新产生的输出是通过什么运算得到的,这个标记位就是上文提到的grad_fn。fn表示function,意味着它记录了计算函数,所以在反向求导时就可以对应求导。事实上,grad_fn的取值有:MeanBackward0,SumBackward0,MulBackward0,AddBackward0.
执行loss.backward(),开始反向传播,对于变量w,w.grad就是loss对于w的偏导。注意,在这之前我们需要把w记录梯度的开关打开:设置它的属性 .requires_grad
为 True
,那么autograd
将会追踪对于该张量的所有操作。但这个条件只是必要条件而非充分条件,即便是requires_grad=True时,该变量的grad仍然可能为None。这就涉及到tensor中的is_leaf概念,只有当同时满足requires和is_leaf都是True时才记录梯度。叶子节点与非叶子节点的区别在这里就是网络参数和中间变量的区别。网络参数是我们初始化的,也是最关心的需要训练得到的,而中间变量是训练过程中由其他变量计算得到的,只起到桥梁的作用,没必要保存下来而浪费内存。
注意loss.backward()这个函数,一般情况下不必传入任何参数,这是因为一般模型对应的loss都是标量;当loss是多维的,则需要在backward()中指定一个gradient参数,它是形状匹配的张量。
为了搞清楚backward()的参数为什么这么奇怪,我们需要知道它到底计算的是什么。
梯度其实是一阶导,这就需要计算出雅克比矩阵。对于:
但其实backward()计算的是雅克比向量积。因为element-wise运算机制,雅克比矩阵表征的是y在各个维度对x各个维度的偏导,为了得到dY,需要将关于的偏导数在反向上进行投影(也可以理解为加权的过程)。而投影的结果就是雅克比向量积,它其实利用了求导的链式法则。标量函数,为了得到l对x的偏导,可以使用雅克比矩阵与相对于的偏导的向量积:
现在我们可以解释backward()这个函数中的参数了,当loss是标量时,相当于v是一个全1数组,因此 out.backward()
和 out.backward(torch.tensor(1.))
等价,这也解释了反向传播到x时各个维度的结果一样;当loss是多维的向量,可以人为构造一个标量函数l,l对loss的偏导作为v传入backward()。刚才也提到,v可以理解为权重向量或者投影向量,官方叫法是easy feed external gradient(grad_variables),要求它的大小与向量函数的个数对应:
The graph is differentiated using the chain rule. If any of variables are non-scalar (i.e. their data has more than one element) and require gradient, the function additionally requires specifying grad_variables. It should be a sequence of matching length, that contains gradient of the differentiated function w.r.t. corresponding variables (None is an acceptable value for all variables that don’t need gradient tensors).
这样设计的原因是为了避免tensor对tensor求导的问题,只允许标量scalar对张量tensor求导,结果是和自身tensor维度一样的tensor。如果loss是多维的,那么就会根据用户提供的v构造=torch.sum(y,v),v同时就是l对y的导数。
当不想跟踪变量的梯度时,不必逐个更改requires_grad属性,可以将整个代码段放在with torch.no_grad()中:
with torch.no_grad():
print((x ** 2).requires_grad)
backward()在计算图中计算,而pytorch的计算图是动态的,在backward()之后被释放,所以没办法连续两次backward()。如果你需要多次backward可以在第一次中指定不释放:
loss.backward(retain_graph=True) # 添加retain_graph=True标识,让计算图不被立即释放
loss.backward()
backward()之前要对网络和学习器中记录的梯度清零,否则梯度会累加:
net.zero_grad() # 清零所有参数(parameter)的梯度缓存
# 创建优化器(optimizer)
optimizer = optim.SGD(net.parameters(), lr=0.01)
optimizer.zero_grad() # 清零梯度缓存,因为网络参数已经放入优化器,所以在优化器中清除梯度和对网络清除梯度是等效的
对于一个变量,可以使用detach()将其从图中分离开,返回的值不会再要求梯度的计算。
Returns a new Tensor, detached from the current graph.The result will never require gradient.
tensor相比于numpy已经针对网络做了更新,但为了更方便地构建网络,torch又把tensor封装成了variable。操作和Tensor是一样的,但是每个Variable都有三个属性,Varibale的Tensor本身的.data,对应Tensor的梯度.grad,以及这个Variable是通过什么方式得到的.grad_fn。
Reference:
1.PyTorch 的 Autograd - 知乎
2.https://pytorch.apachecn.org/docs/1.4/blitz/tensor_tutorial.html
3.backwardhttps://www.cnblogs.com/JeasonIsCoding/p/10164948.html
4.pytorch中backward()函数详解_Camlin_Z的博客-CSDN博客_backward()
5.githubGitHub - pytorch/pytorch: Tensors and Dynamic neural networks in Python with strong GPU acceleration