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实现求高阶导数。

上述描述可能比较抽象,如果没有看懂,不用着急,会在本节后半部分详细介绍,下面先看几个例子。

 在PyTorch实现中,autograd会随着用户的操作,记录生成当前variable的所有操作,并由此建立一个有向无环图。用户每进行一个操作,相应的计算图就会发生改变。更底层的实现中,图中记录了操作Function,每一个变量在图中的位置可通过其grad_fn属性在图中的位置推测得到。在反向传播过程中,autograd沿着这个图从当前变量(根节点zz)溯源,可以利用链式求导法则计算所有叶子节点的梯度。每一个前向传播操作的函数都有与之对应的反向传播函数用来计算输入的各个variable的梯度,这些函数的函数名通常以Backward结尾。下面结合代码学习autograd的实现细节。
 
PyTorch使用的是动态图,它的计算图在每次前向传播时都是从头开始构建,所以它能够使用Python控制语句(如for、if等)根据需求创建计算图。这点在自然语言处理领域中很有用,它意味着你不需要事先构建所有可能用到的图的路径,图在运行时才构建。
 

变量的requires_grad属性默认为False,如果某一个节点requires_grad被设置为True,那么所有依赖它的节点requires_grad都是True。这其实很好理解,对于xyzx→y→z,x.requires_grad = True,当需要计算zx∂z∂x时,根据链式法则,z/x=zyyx∂z∂x=∂z∂y∂y∂x,自然也需要求zy∂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.gradhook技术获取非叶子节点的值。
  • 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的更详细使用请参照文档。

原文地址:https://www.cnblogs.com/ziwh666/p/12355723.html