PyTorch入门(2)—— 自动求梯度

  • 参考:《动手学深度学习(PyTorch版)》—— 李沐
  • 注意:由于本文是jupyter文档转换来的,代码不一定可以直接运行,有些注释是jupyter给出的交互结果,而非运行结果!!

文章目录

  • 1. 基础概念
  • 2. Tensor
  • 3. 梯度
    • 3.1 示例
    • 3.2 向量求导
    • 3.3 中断梯度追踪
    • 3.4 在不影响反向传播的情况下修改Tensor的值

  • 深度学习中经常需要对函数求梯度(gradient)。PyTorch提供的 autograd 包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。下面介绍如何使用 autograd 包来进行自动求梯度的有关操作,autograd 包的核心类是 Tensor

1. 基础概念

  1. 开启和停止追踪
    1. 开启追踪:设置 Tensor.requires_grad = True,开始追踪此 Tensor上的所有操作,这样就可以利用链式法则进行梯度传播了
    2. 停止追踪:调用 Tensor.detach()Tensor 从追踪记录中分离出来,防止将来的计算被追踪,这样梯度就传不过去了;还可以用 with torch.no_grad() 将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时不需要计算可训练参数(.requires_grad = True)的梯度
  2. 进行梯度计算
    1. 使用 Tensor.backward() 来完成所有梯度计算,梯度将累计Tensor.grad 属性中(重复求梯度的话,通常要先用 .grad.data.zero_() 把累积的梯度清零)
    2. y.backward() 时,如果 y 是标量,则不需要传入参数;否则,需要传入一个与 y 同形的 Tensor(解释见 3.2 节)
  3. Function
    1. Function 对象用于反向计算输入的梯度,FunctionTensor 结合在一起就能构建一个记录整个计算过程的有向无环图(DAG),即 “计算图”
    2. 使用 Tensor.grad_fn 属性创建 Tensor 对应的 Function 对象实例并指向它,如果此 Tensor 是通过某些运算得到的,则 grad_fn 返回一个与这些运算相关的对象;否则返回 None
    3. 重复访问 Tensor.grad_fn 属性,将重复生成不同的 Function 对象实例

2. Tensor

  • PyTorch中,张量 torch.Tensor 是存储和变换数据的主要工具,请参考:PyTorch入门(1)—— Tensor基本数据操作

  • 如果把函数计算过程看作一颗树

    1. 叶子节点:直接创建的 Tensor
      1. 叶子节点有属性 .is_leaf = True
      2. 叶子节点对应的 .grad_fn = None
      3. 叶子节点默认 .requires_grad = False,除非在创建时使用 requires_grad=True 进行设置
    2. 非叶子节点:由其他 Tensor 计算出来的 Tensor
      1. 非叶子节点有属性 .is_leaf = False
      2. 非叶子节点调用 .grad_fn 生成的 Function 实例反应了上一步计算过程;除非计算它的 Tensor 都是.requires_grad = False,这时其 .requires_grad = False.grad_fn = None
    3. 通过 Tensor.requires_grad_(True/False),可以用in-place的方式改变 .requires_grad 属性
  • 示例

    import torch
    
    # 直接创建的 Tensor,生成的 Function 对象为 None
    x = torch.ones(2,2,requires_grad=True)
    print(x)
    print(x.grad_fn)
    
    '''
    tensor([[1., 1.],
            [1., 1.]], requires_grad=True)
    None
    '''
    
    # 计算得到的Tensor,生成对应计算类型的Function对象'
    print((x+2).grad_fn)	# 
    print((x-2).grad_fn)	# 
    print(x.sum().grad_fn)	# 
    
    # 重复访问 .grad_fn 属性,将生成并指向不同的 Function 对象实例
    y = x+2
    print(y.grad_fn)	# 
    print(y.grad_fn)	# 
    
    a = torch.randn(2, 2)  # 如果不显式设置,默认 .requires_grad = False
    print(a.requires_grad) # False
    
    b = (a * a).sum()      # 使用 .requires_grad = False 的Tensor计算的Tensor,仍然.requires_grad = False,.grad_fn = None
    print(b.requires_grad) # False
    print(b.grad_fn)       # None
    
    a.requires_grad_(True) # in-place方式直接设置 requires_grad = True
    print(a.requires_grad) # True
    
    b = (a * a).sum()
    print(b.requires_grad) # True
    print(b.grad_fn)       # 
    
    print(a.is_leaf,b.is_leaf)  # True  False
    

3. 梯度

  1. 使用 Tensor.backward() 来完成所有梯度计算,梯度将累计到 Tensor.grad 属性中
  2. y.backward()
    1. 如果 y 是标量,则不需要传入参数
    2. 如果 y 是张量,需要传入一个与 y 同形的 Tensor。若不传入,报错 grad can be implicitly created only for scalar outputs

3.1 示例

  • x = { x 1 , x 2 , x 3 , x 4 } \pmb{x} = \{x_1,x_2,x_3,x_4\} xxx={x1,x2,x3,x4} 是一个 1 × 4 1\times 4 1×4 的向量,求下式梯度 f ( x ) = 1 4 ∑ i = 1 4 3 ( x i + 2 ) 2 f(\pmb{x})=\frac{1}{4}\sum_{i=1}^43(x_i+2)^2 f(xxx)=41i=143(xi+2)2
    这个梯度 ∂ f ∂ x \frac{\partial f}{\partial \mathbf{x}} xf 也将是一个同尺寸( 1 × 4 1\times 4 1×4)的向量

    x = torch.tensor([[1,1,2,2],],dtype = torch.float,requires_grad=True)
    a = x+2
    b = a*a
    c = 3*b
    d = c.mean()
    
    print(c)			# tensor([[27., 27., 48., 48.]], grad_fn=)
    # c.backward()  	# 报错 grad can be implicitly created only for scalar outputs
    	
    d.backward()        # 求梯度
    print(x.grad)		# tensor([[4.5000, 4.5000, 6.0000, 6.0000]])
    
    
  • 可以把计算图如下画出,可见,有 ∂ f ∂ x i = ∂ d ∂ x i = ∂ d ∂ c i ∂ c i ∂ b i ∂ b i ∂ a i ∂ a i ∂ x i = 1 4 ⋅ 3 ⋅ ( 2 x i + 4 ) ⋅ 1 = 1.5 x i + 3 \frac{\partial f}{\partial x_i} =\frac{\partial d}{\partial x_i}= \frac{\partial d}{\partial c_i}\frac{\partial c_i}{\partial b_i}\frac{\partial b_i}{\partial a_i}\frac{\partial a_i}{\partial x_i}=\frac{1}{4}·3·(2x_i+4)·1=1.5x_i+3 xif=xid=cidbiciaibixiai=413(2xi+4)1=1.5xi+3
    PyTorch入门(2)—— 自动求梯度_第1张图片

  • 注意:grad在反向传播过程中是累加的,每一次运行 .backward(),梯度都会累加之前的梯度,所以一般在反向传播前把梯度清零

    x = torch.tensor([[1,1,2,2],],dtype = torch.float,requires_grad=True)
    
    # 第一遍反向传播计算梯度
    d = ((x + 2) * (x + 2) * 3).mean()
    d.backward()        
    print(x.grad)		# tensor([[4.5000, 4.5000, 6.0000, 6.0000]])
    
    # 重复反向传播,梯度会累加
    d = ((x + 2) * (x + 2) * 3).mean()
    d.backward()        
    print(x.grad)		# tensor([[ 9.,  9., 12., 12.]])
    
    # 在反向传播之前先把梯度清零
    d = ((x + 2) * (x + 2) * 3).mean()
    x.grad.data.zero_() # inplace 操作清零累积的梯度
    d.backward()
    print(x.grad)		#  tensor([[4.5000, 4.5000, 6.0000, 6.0000]])
    

3.2 向量求导

  • 设一个函数值和自变量都是向量的函数 y = f ( x ) \pmb{y}=f(\pmb{x}) yyy=f(xxx),那么 ∂ y ∂ x \frac{\partial \mathbf{y}}{\partial \mathbf{x}} xy 是一个雅可比矩阵
    J = [ ∂ y 1 ∂ x 1 … ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 … ∂ y m ∂ x n ] (3) J = \left[ \begin{matrix} \frac{\partial y_1}{\partial x_1} & \dots & \frac{\partial y_1}{\partial x_n}\\ \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \dots & \frac{\partial y_m}{\partial x_n} \end{matrix} \right] \tag{3} J=x1y1x1ymxny1xnym(3)

  • Pytorch 中的自动求导其实就是在计算一些雅可比矩阵的乘积。设有标量函数 l = g ( y ) l=g(\pmb{y}) l=g(yyy),标量 l l l 对向量 y \pmb{y} yyy 的梯度为 ∂ l ∂ y = [ ∂ l ∂ y 1 … ∂ l ∂ y m ] \frac{\partial l}{\partial \pmb{y}} = \left[\frac{\partial l}{\partial y_1} \dots \frac{\partial l}{\partial y_m} \right] yyyl=[y1lyml] 根据链式法则,我们有标量 l l l 关于向量 x \pmb{x} xxx 的梯度为
    ∂ l ∂ x = ∂ l ∂ y J = [ ∂ l ∂ y 1 … ∂ l ∂ y m ] [ ∂ y 1 ∂ x 1 … ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 … ∂ y m ∂ x n ] = [ ∂ l ∂ x 1 … ∂ l ∂ x n ] \frac{\partial l}{\partial \pmb{x}} = \frac{\partial l}{\partial \pmb{y}} J = \left[\frac{\partial l}{\partial y_1} \dots \frac{\partial l}{\partial y_m} \right] \left[ \begin{matrix} \frac{\partial y_1}{\partial x_1} & \dots & \frac{\partial y_1}{\partial x_n}\\ \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \dots & \frac{\partial y_m}{\partial x_n} \end{matrix}\right] = \left[\frac{\partial l}{\partial x_1} \dots \frac{\partial l}{\partial x_n} \right] xxxl=yyylJ=[y1lyml]x1y1x1ymxny1xnym=[x1lxnl]

  • 前文说到,若对向量/张量执行 .backward() 操作,必须传入一个与其同形的 Tensor。这其实是为了避免向量/张量对张量求导,这种计算太复杂了。

    举个例子,假设形状为 m x n 的矩阵 X 经过运算得到了 p x q 的矩阵 YY 又经过运算得到了 s x t 的矩阵 Z。那么按照前面讲的规则,dZ/dY 应该是一个 s x t x p x q 四维张量,dY/dX 是一个 p x q x m x n 的四维张量。问题来了,怎样反向传播?怎样将两个四维张量相乘???这要怎么乘???就算能解决两个四维张量怎么乘的问题,四维和三维的张量又怎么乘?导数的导数又怎么求,这一连串的问题,感觉要疯掉……

    Pytorch中只允许标量对张量求导,得到和自变量同形的张量。所以,必要的时候,我们要通过对元素加权求和的方式把张量转换为标量。假设张量 y \pmb{y} yyy 是张量 x \pmb{x} xxx 的函数, w \pmb{w} www y \pmb{y} yyy 同形,则 y.backward(w) 代表:先计算标量 l=torch.sum(y*w),然后求 ∂ l ∂ x \frac{\partial l}{\partial \mathbf{x}} xl。对于3.1节的示例,有
    ∂ c ∂ x ∣ w = [ w 1 ∂ c 1 ∂ x 1 … w n ∂ c 4 ∂ x 4 ] , 其 中 w i ∂ c i ∂ x i = 6 w i x i + 12 w i \frac{\partial \pmb{c}}{\partial \pmb{x}} {\bigg|}_{\mathbf{w}} =\left[w_1 \frac{\partial c_1}{\partial x_1} \dots w_n \frac{\partial c_4}{\partial x_4}\right], 其中 w_i \frac{\partial c_i}{\partial x_i} = 6w_ix_i+12w_i xxxcccw=[w1x1c1wnx4c4],wixici=6wixi+12wi

    x = torch.tensor([[1,1,2,2],],dtype = torch.float,requires_grad=True)
    a = x+2
    b = a*a
    c = 3*b
    
    c.backward(torch.tensor([[1,0.5,1,0.5],],dtype = torch.float))
    print(x.grad)	# tensor([[18.,  9., 24., 12.]])
    

3.3 中断梯度追踪

  • 考虑这个标量函数: y 3 = y 1 + y 2 = x 2 + x 3 y_3 = y_1+y_2=x^2+x^3 y3=y1+y2=x2+x3,这时有
    ∂ y 3 ∂ x = ∂ y 1 ∂ x + ∂ y 2 ∂ x = 2 x + 3 x = 5 x \frac{\partial y_3}{\partial x} = \frac{\partial y_1}{\partial x} + \frac{\partial y_2}{\partial x} = 2x+3x = 5x xy3=xy1+xy2=2x+3x=5x 如果将其中的 y 2 y_2 y2 设置为禁止追踪 requires_grad=False,梯度就无法从 y 2 y_2 y2 往回传播,这时有
    ∂ y 3 ∂ x = ∂ y 1 ∂ x = 2 x \frac{\partial y_3}{\partial x} = \frac{\partial y_1}{\partial x}= 2x xy3=xy1=2x 计算图如下
    PyTorch入门(2)—— 自动求梯度_第2张图片
    x = torch.tensor(1.0, requires_grad=True)
    y1 = x ** 2 
    with torch.no_grad():
        y2 = x ** 3
    y3 = y1 + y2
    
    print(x.requires_grad)		# True
    print(y1, y1.requires_grad) # tensor(1., grad_fn=) True
    print(y2, y2.requires_grad) # tensor(1.) False
    print(y3, y3.requires_grad) # tensor(2., grad_fn=) True
    
    y3.backward()
    print(x.grad)               # tensor(2.)
    
    #y2.backward() # 报错: element 0 of tensors does not require grad and does not have a grad_fn
    

3.4 在不影响反向传播的情况下修改Tensor的值

  • 要想修改 Tensor tensor 的值,且不影响梯度计算,可以对 tensor.data 进行操作

  • tensor.data 也是一个 Tensor,且 tensor.data.requires_grad = False也就是说 tensor.data 独立于计算图之外,修改它不会被 autograd 记录,也就不会影响反向传播算出的梯度

    x = torch.ones(1,requires_grad=True)
    y = 3*x
    
    y.backward()        # 求梯度
    print(x.grad)		# tensor([3.])
    
    x = torch.ones(1,requires_grad=True)
    y = 3*x
    # 通过.data修改,不影响计算图,不影响梯度结果
    y.data = 4*x
    
    y.backward()        # 求梯度
    print(x.grad)		# tensor([3.])
    
    x = torch.ones(1,requires_grad=True)
    y = 3*x
    # 直接修改,影响计算图,影响梯度结果
    y = 4*x
    
    y.backward()        # 求梯度
    print(x.grad)		# tensor([4.])
    

你可能感兴趣的:(#,PyTorch,pytorch,自动梯度)