几乎所有的深度学习框架背后的设计核心都是张量和计算图, PyTorch 也不例外。
Tensor
使用Tensor函数新建tensor是最复杂多变的方式,它既可以接收一个list,并根据list的数据新建tensor ,也能根据指定的形状新建tensor,还能传入其他的tensor。除了tensor.size(),还可以利用tensor.shape直接查看tensor的形状,tensor.shape等价于tensor.size()。
需要注意的是,t.Tensor(*sizes)创建tensor时,系统不会马上分配空间,只会计算剩余的内存是否足够使用,使用到tensor时才会分配,而其他操作都是在创建完tensor后马上进行空间分配。
# 指定tensor的形状,a的数值取决于内存空间的状态
a = torch.Tensor(2, 3)
# 把tensor转为list
b = a.tolist()
# 创建一个元素为2和3的tensor
c = torch.Tensor((2, 3))
通过tensor.view方法可以调整tensor的形状,但必须保证调整前后元素总数一致。view不会修改自身的数据,返回的新tensor与源tensor共享内存,即更改其中一个,另外一个也会跟着改变。在实际应用中可能经常需要添加或减少某一维度,这时squeeze和unsqueeze两个函数就派上了用场。
resize是另一种可用来调整size的方法,但与view不同,它可以修改tensor的尺寸。如果新尺寸超过了原尺寸,会自动分配新的内存空间,而如果新尺寸小于原尺寸,则之前的数据依旧会被保存。
import torch
b = torch.Tensor([1,2,3],[4,5,6])
c = b.resize(1,3) # 返回[1,2,3]
d = b.resize(3,3) # 返回[[1,2,3],[4,5,6],[0,0,0]]
autograd
用Tensor 训练网络很方便,但实际使用中经常出现非常复杂的网络结构,此时如果手动实现反向传播,不仅费时费力,而且容易出错,难以检查。torch.autograd 就是为方便用户使用,专门开发的一套自动求导引擎,它能够根据输入和前向传播过程自动构建计算图,并执行反向传播。
Variable的数据结构如下图:
Variable主要包含三个属性:
- data:保存Variable所包含的Tensor;
- grad:保存data所对应的梯度,grad也是个Variable,而不是Tensor,它和data的形状一样;
- grad_fn:指向一个Function对象,这个Function用来反向传播计算输入的梯度。
Variable 的构造函数需要传人tensor ,同时有两个可选参数:
- requires_grad (bool) : 是否需要对该variable进行求导。
- volatile (bool): 意为"挥发",设置为True,构建在该variable之上的图都不会求导,专为推理阶段设计。
Pytorch中的自动求导函数backward()所需参数含义
如果out.backward()中的out是一个标量的话(相当于一个神经网络有一个样本,这个样本有两个属性,神经网络有一个输出)那么此时我的backward函数是不需要输入任何参数的 [ 1 ] ^{[1]} [1]。
如果out.backward()中的out是一个向量(或者理解成1xN的矩阵)的话,我们对向量进行自动求导,那么此时我的backward函数是需要输入参数的。
计算图
正向传播:
反向传播:
在反向传播过程中,autograd沿着这个图从当前变量(根节点z) 溯源,可以利用链式求导法则计算所有叶子节点的梯度。每一个前向传播操作的函数都有与之对应的反向传播函数用来计算输入的各个variable的梯度,这些函数的函数名通常以Backward结尾。下面结合代码学习autograd的实现细节。
import torch
from torch.autograd import Variable
x = Variable(torch.ones(1))
b = Variable(torch.rand(1),requires_grad=True)
w = Variable(torch.rand(1),requires_grad=True)
y = w * x
z = y + b
# grad_fn可以查看这个variable的反向传播函数,
# z是add 函数的输出,所以它的反向传播函数是Add.Backward
z.grad_fn
# 第一个是y,它是乘法(mul)的输出,所以对应的反向传播函数y.grad_fn是MulBackward
# 第二个是b,它是叶子节点,由用户创建,grad_fn为None,但是有((<torch.autograd.function.MulBackward at Ox7f6ca3032ed8>, 0),(<AccwnulateGrad at Ox7f6ca3007510>, 0))
z.grad_fn.next_functions
# ((<AcculnulateGrad at Ox7f6ca30076dO>, 0), (None, 0))
y.grad_fn.next_functions
volatile = True是另外一个很重要的标识,它能够将所有依赖于它的节点全部设为volatile =True,其优先级比requires_grad = True高。volatile= True的节点不会求导,即使requires_grad=True ,也无法进行反向传播。对于不需要反向传播的情景,该参数可实现一定程度的速度提升,并节省约一半显存,因为其不需要分配空间保存梯度。
import torch
from torch.autograd import Variable
X = Variable(t.ones(1), volatile=True)
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, False)
值得注意的是,只有对variable的操作才能使用autograd,如果对variable 的data直接进行操作,将无法使用反向传播。除了参数初始化,一般我们不会直接修改variable.data的值。
在PyTorch中计算图的特点可总结如下。
- autograd根据用户对variable的操作构建计算图。对variable的操作抽象为Function。由用户创建的节点称为叶子节点,叶子节点的grad_fn为None 。叶子节点中需要求导的variable ,具有AccumulateGrad 标识,因其梯度是累加的。
- variable默认是不需要求导的,即requires_grad属性默认为False。如果某一个节点requires_grad被设置为True,那么所有依赖它的节点requires_grad都为True。
- variable的volatile属性默认为False,如果某一个variable的volatile 属性被设为True,那么所有依赖它的节点volatile属性都为True。volatile属性为True的节点不会求导, volatile的优先级requires_grad 高。
- 多次反向传播时,梯度是累加的。反向传播的中间缓存会被清空,为进行多次反向传播需指定retain_graph =True来保存这些缓存。
- 非叶子节点的梯度计算完之后即被清空,可以使用autograd.grad或hook技术获取非叶子节点梯度的值。
- variable的grad与data 形状一致,应避免直接修改variable.data, 因为对data 的直接操作元法利用autograd进行反向传播。
- 反向传播函数backward 的参数grad_variables可以看成链式求导的中间结果,如果是标量,可以省略,默认为1。
- PyTorch采用动态图设计,可以很方便地查看中间层的输出,动态地设计计算图结构。
扩展autograd(即实现自定义复杂函数的反向传播)
目前,绝大多数函数都可以使用autograd实现反向求导,但如果需要自己写一个复杂的函数,不支持自动反向求导怎么办?答案是写一个Function,实现它的前向传播和反向传播代码,Function对应于计算图中的矩形,它接收参数,计算并返回结果。下面给出一个例子 [ 2 ] ^{[2]} [2]。
from torch.autograd import Function
from torch.autograd import Variable as V
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_variables
grad_x = output * (1 - output) * grad_output
return grad_x
class MultiplyAdd(Function): # <----- 类需要继承Function类
@staticmethod # <-----forward和backward都是静态方法
def forward(ctx, w, x, b): # <-----ctx作为内部参数在前向反向传播中协调
print('type in forward',type(x))
ctx.save_for_backward(w,x) # <-----ctx保存参数
output = w * x + b
return output # <-----forward输入参数和backward输出参数必须一一对应
@staticmethod # <-----forward和backward都是静态方法
def backward(ctx, grad_output): # <-----ctx作为内部参数在前向反向传播中协调
w,x = ctx.saved_variables # <-----ctx读取参数
print('type in backward',type(x))
grad_w = grad_output * x # <-----对w求导得到x
grad_x = grad_output * w
grad_b = grad_output * 1
return grad_w, grad_x, grad_b
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) # <-----forward
print('开始反向传播')
z.backward() # 等效 # <-----backward
# x不需要求导,中间过程还是会计算它的导数,但随后被清空
print(x.grad, w.grad, b.grad)
对以上代码的分析如下。
- 自定义的Function需要继承autograd.Function,没有构造函数__init__ , forward和backward 函数都是静态方法。
- forward 函数的输入和输出都是tensor,backward 函数的输入和输出都是variable。
- backward函数的输出和forward函数的输入一一对应,backward 函数的输入和forward函数的输出一一对应。
- backward函数的grad_output参数即t.autograd.backward中的grad_variables。
- 如果某一个输入不需要求导, 直接返回None, 例如forward 中的输入参数x_requires_grad显然无法对它求导,直接返回None即可。
- 反向传播可能需要利用前向传播的某些中间结果,在前向传播过程中, 需要保存中间结果,否则前向传播结束后这些对象即被释放。
- 使用Function.apply(variable) 即可调用实现的Function。
参考文献
1:Pytorch中的自动求导函数backward()所需参数含义
2:深入理解autograd_下:函数扩展&高阶导数