Pytorch:Autograd
PyTorch在autograd模块中实现了计算图的相关功能,autograd中的核心数据结构是Variable。从v0.4版本起,Variable和Tensor合并。我们可以认为需要求导(requires_grad)的tensor即Variable. autograd记录对tensor的操作记录用来构建计算图。
Variable提供了大部分tensor支持的函数,但其不支持部分inplace
函数,因这些函数会修改tensor自身,而在反向传播中,variable需要缓存原来的tensor来计算反向传播梯度。如果想要计算各个Variable的梯度,只需调用根节点variable的backward
方法,autograd会自动沿着计算图反向传播,计算每一个叶子节点的梯度。
variable.backward(gradient=None, retain_graph=None, create_graph=None)
主要有如下参数:
- grad_variables:形状与variable一致,对于
y.backward()
,grad_variables相当于链式法则dzdx=dzdy×dydxdzdx=dzdy×dydx中的dzdydzdy。grad_variables也可以是tensor或序列。 - retain_graph:反向传播需要缓存一些中间结果,反向传播之后,这些缓存就被清空,可通过指定这个参数不清空缓存,用来多次反向传播。
- create_graph:对反向传播过程再次构建计算图,可通过
backward of backward
实现求高阶导数。
上述描述可能比较抽象,如果没有看懂,不用着急,会在本节后半部分详细介绍,下面先看几个例子。
Function
,每一个变量在图中的位置可通过其grad_fn
属性在图中的位置推测得到。在反向传播过程中,autograd沿着这个图从当前变量(根节点zz)溯源,可以利用链式求导法则计算所有叶子节点的梯度。每一个前向传播操作的函数都有与之对应的反向传播函数用来计算输入的各个variable的梯度,这些函数的函数名通常以Backward
结尾。下面结合代码学习autograd的实现细节。变量的requires_grad
属性默认为False,如果某一个节点requires_grad被设置为True,那么所有依赖它的节点requires_grad
都是True。这其实很好理解,对于x→y→zx→y→z,x.requires_grad = True,当需要计算∂z∂x∂z∂x时,根据链式法则,∂z/∂x=∂z∂y∂y∂x∂z∂x=∂z∂y∂y∂x,自然也需要求∂z∂y∂z∂y,所以y.requires_grad会被自动标为True.
有些时候我们可能不希望autograd对tensor求导。认为求导需要缓存许多中间结构,增加额外的内存/显存开销,那么我们可以关闭自动求导。对于不需要反向传播的情景(如inference,即测试推理时),关闭自动求导可实现一定程度的速度提升,并节省约一半显存,因其不需要分配空间计算梯度。
with torch.no_grad(): # 运行的代码不会自动求导
如果我们想要修改tensor的数值,但是又不希望被autograd记录,那么我么可以对tensor.data进行操作
a = t.ones(3,4,requires_grad=True) b = t.ones(3,4,requires_grad=True) c = a * b a.data # 还是一个tensor a.data.requires_grad # 但是已经是独立于计算图之外 d = a.data.sigmoid_() # sigmoid_ 是个inplace操作,会修改a自身的值 d.requires_grad # 如果我们希望对tensor操作,但是又不希望被记录, 可以使用tensor.data 或者tensor.detach() # 近似于 tensor=a.data, 但是如果tensor被修改,backward可能会报错 tensor = a.detach() tensor.requires_grad # 统计tensor的一些指标,不希望被记录 mean = tensor.mean() std = tensor.std() maximum = tensor.max() tensor[0]=1 # 下面会报错: RuntimeError: one of the variables needed for gradient # computation has been modified by an inplace operation # 因为 c=a*b, b的梯度取决于a,现在修改了tensor,其实也就是修改了a,梯度不再准确 # c.sum().backward()
在PyTorch中计算图的特点可总结如下:
- autograd根据用户对variable的操作构建其计算图。对变量的操作抽象为
Function
。 - 对于那些不是任何函数(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采用动态图设计,可以很方便地查看中间层的输出,动态的设计计算图结构。
这些知识不懂大多数情况下也不会影响对pytorch的使用,但是掌握这些知识有助于更好的理解pytorch,并有效的避开很多陷阱
扩展autograd
目前绝大多数函数都可以使用autograd
实现反向求导,但如果需要自己写一个复杂的函数,不支持自动反向求导怎么办? 写一个Function
,实现它的前向传播和反向传播代码,Function
对应于计算图中的矩形, 它接收参数,计算并返回结果。下面给出一个例子。
class Mul(Function): @staticmethod def forward(ctx, w, x, b, x_reuqires_grad = True): ctx.x_requires_grad = x_requires_grad ctx.save_for_backward(w, x) output = w * x + b return output @staticmethod def backword(ctx, grad_output): w, x = ctx.save_tensors 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
- 自定义的Function需要继承autograd.Function,没有构造函数
__init__
,forward和backward函数都是静态方法 - backward函数的输出和forward函数的输入一一对应,backward函数的输入和forward函数的输出一一对应
- backward函数的grad_output参数即t.autograd.backward中的
grad_variables
- 如果某一个输入不需要求导,直接返回None,如forward中的输入参数x_requires_grad显然无法对它求导,直接返回None即可
- 反向传播可能需要利用前向传播的某些中间结果,需要进行保存,否则前向传播结束后这些对象即被释放
from torch.autograd import Function class MultiplyAdd(Function): @staticmethod def forward(ctx, w, x, b): ctx.save_for_backward(w,x) output = w * x + b return output @staticmethod def backward(ctx, grad_output): w,x = ctx.saved_tensors grad_w = grad_output * x grad_x = grad_output * w grad_b = grad_output * 1 return grad_w, grad_x, grad_b
之所以forward函数的输入是tensor,而backward函数的输入是variable,是为了实现高阶求导。backward函数的输入输出虽然是variable,但在实际使用时autograd.Function会将输入variable提取为tensor,并将计算结果的tensor封装成variable返回。在backward函数中,之所以也要对variable进行操作,是为了能够计算梯度的梯度(backward of backward)。下面举例说明,有关torch.autograd.grad的更详细使用请参照文档。