0%

Eat-pytorch-2-conception

1. Introduction

1.1 Preface

本系列博文是和鲸社区的活动《20天吃掉那只PyTorch》学习的笔记,本篇为系列笔记的第二篇—— Pytorch 的核心概念。该专栏是 Github2.8K 星的项目,在学习该书的过程中可以参考阅读《Python深度学习》一书的第一部分"深度学习基础"内容。

《Python深度学习》这本书是 Keras 之父 Francois Chollet 所著,该书假定读者无任何机器学习知识,以Keras 为工具,使用丰富的范例示范深度学习的最佳实践,该书通俗易懂,全书没有一个数学公式,注重培养读者的深度学习直觉。

《Python深度学习》一书的第一部分的 4 个章节内容如下,预计读者可以在 20 小时之内学完。

  1. 什么是深度学习
  2. 神经网络的数学基础
  3. 神经网络入门
  4. 机器学习基础

本系列博文的大纲如下:

  • 一、PyTorch的建模流程
  • 二、PyTorch的核心概念
  • 三、PyTorch的层次结构
  • 四、PyTorch的低阶API
  • 五、PyTorch的中阶API
  • 六、PyTorch的高阶API

最后,本博文提供所使用的全部数据,读者可以从下述连接中下载数据:

Download Now

1.2 Pytorch 的核心概念

Pytorch 是一个基于Python的机器学习库。它广泛应用于计算机视觉,自然语言处理等深度学习领域。是目前和TensorFlow 分庭抗礼的深度学习框架,在学术圈颇受欢迎。

它主要提供了以下两种核心功能:

  1. 支持 GPU 加速的张量计算。
  2. 方便优化模型的自动微分机制。

Pytorch的主要优点:

  • 简洁易懂:PytorchAPI 设计的相当简洁一致。基本上就是 tensor, autograd, nn 三级封装。学习起来非常容易。有一个这样的段子,说三大框架的设计哲学如下:

    • TensorFlow: Make it complicated

    • Keras: Make it complicated and hide it

    • Pytorch: Keep it simple and stupid.

  • 便于调试:Pytorch 采用动态图,可以像普通 Python 代码一样进行调试。不同于 TensorFlow, Pytorch 的报错说明通常很容易看懂。

    有一个这样的段子,说你永远不可能从 TensorFlow 的报错说明中找到它出错的原因。

  • 强大高效:Pytorch 提供了非常丰富的模型组件,可以快速实现想法。并且运行速度很快。目前大部分深度学习相关的 Paper 都是用 Pytorch 实现的。

    有些研究人员表示,从使用 TensorFlow 转换为使用 Pytorch 之后,他们的睡眠好多了,头发比以前浓密了,皮肤也比以前光滑了。

俗话说,万丈高楼平地起,Pytorch 这座大厦也有它的地基。

Pytorch 底层最核心的概念是张量,动态计算图以及自动微分。

2. Tensor data structure

2.1 Tensor

张量的数据类型和 numpy.array 基本一一对应,但是不支持 str 类型。

包括:

  • torch.float64(torch.double);
  • torch.float32(torch.float);
  • torch.float16;
  • torch.int64(torch.long);
  • torch.int32(torch.int);
  • torch.int16;
  • torch.int8;
  • torch.uint8;
  • torch.bool.

一般神经网络建模使用的都是 torch.float32 类型。

2.1.1 Data type

  • 自动推断数据类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import numpy as np
    import torch

    # 自动推断数据类型
    i = torch.tensor(1)
    print(i,i.dtype)
    x = torch.tensor(2.0)
    print(x,x.dtype)
    b = torch.tensor(True)
    print(b,b.dtype)

    Results:

    1
    2
    3
    tensor(1) torch.int64
    tensor(2.) torch.float32
    tensor(True) torch.bool

  • 指定数据类型

    1
    2
    3
    4
    # 指定数据类型

    i = torch.tensor(1,dtype = torch.int32);print(i,i.dtype)
    x = torch.tensor(2.0,dtype = torch.double);print(x,x.dtype)

    Results:

      tensor(1, dtype=torch.int32) torch.int32
      tensor(2., dtype=torch.float64) torch.float64
  • 使用特定类型构造函数

    1
    2
    3
    i = torch.IntTensor(1);print(i,i.dtype)
    x = torch.Tensor(np.array(2.0));print(x,x.dtype) # 等价于torch.FloatTensor
    b = torch.BoolTensor(np.array([1,0,2,0])); print(b,b.dtype)

    Results:

      tensor([1], dtype=torch.int32) torch.int32
      tensor(2.) torch.float32
      tensor([ True, False,  True, False]) torch.bool
  • 不同类型进行转换

    1
    2
    3
    4
    i = torch.tensor(1); print(i,i.dtype)
    x = i.float(); print(x,x.dtype) #调用 float方法转换成浮点类型
    y = i.type(torch.float); print(y,y.dtype) #使用type函数转换成浮点类型
    z = i.type_as(x);print(z,z.dtype) #使用type_as方法转换成某个Tensor相同类型

    Results:

      tensor(1) torch.int64
      tensor(1.) torch.float32
      tensor(1.) torch.float32
      tensor(1.) torch.float32

2.2 Dimension of tensor

不同类型的数据可以用不同维度(dimension)的张量来表示。

  • 标量: 0 维张量;
  • 向量:1 维张量;
  • 矩阵:2 维张量。

彩色图像有 rgb 三个通道,可以表示为 3 维张量。视频还有时间维,可以表示为 4 维张量。

Note:可以简单地总结为:有几层中括号,就是多少维的张量。

  • Scalar

    1
    2
    3
    scalar = torch.tensor(True)
    print(scalar)
    print(scalar.dim()) # 标量,0维张量

    Results:

      tensor(True)
      0
  • Vector

    1
    2
    3
    vector = torch.tensor([1.0,2.0,3.0,4.0]) #向量,1维张量
    print(vector)
    print(vector.dim())

    Results:

      tensor([1., 2., 3., 4.])
      1
  • Matrix

    1
    2
    3
    tensor3 = torch.tensor([[[1.0,2.0],[3.0,4.0]],[[5.0,6.0],[7.0,8.0]]])  # 3维张量
    print(tensor3)
    print(tensor3.dim())

    Results:

    1
    2
    3
    4
    5
    6
    tensor([[[1., 2.],
    [3., 4.]],

    [[5., 6.],
    [7., 8.]]])
    3

  • 4-D tensor

    1
    2
    3
    4
    tensor4 = torch.tensor([[[[1.0,1.0],[2.0,2.0]],[[3.0,3.0],[4.0,4.0]]],
    [[[5.0,5.0],[6.0,6.0]],[[7.0,7.0],[8.0,8.0]]]]) # 4维张量
    print(tensor4)
    print(tensor4.dim())

    Results:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    tensor([[[[1., 1.],
    [2., 2.]],

    [[3., 3.],
    [4., 4.]]],

    [[[5., 5.],
    [6., 6.]],

    [[7., 7.],
    [8., 8.]]]])

    4

2.3 Size of tensor

几个常用的跟大小相关的方法: - shape

可以使用 `shape` 属性或者 `size()` 方法查看张量在每一维的长度,
  • view

    可以使用 view 方法改变张量的尺寸。

  • reshape

    如果 view 方法改变尺寸失败,可以使用 reshape 方法.

    1
    2
    3
    vector = torch.tensor([1.0,2.0,3.0,4.0])
    print(vector.size())
    print(vector.shape)

    Results:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
        torch.Size([4])
    torch.Size([4])

    - view 改变张量尺寸

    ```python
    # 使用view可以改变张量尺寸

    vector = torch.arange(0,12)
    print(vector)
    print(vector.shape)

    matrix34 = vector.view(3,4)
    print(matrix34)
    print(matrix34.shape)

    matrix43 = vector.view(4,-1) #-1表示该位置长度由程序自动推断
    print(matrix43)
    print(matrix43.shape)

    Results:

      tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
      torch.Size([12])
      tensor([[ 0,  1,  2,  3],
              [ 4,  5,  6,  7],
              [ 8,  9, 10, 11]])
      torch.Size([3, 4])
      tensor([[ 0,  1,  2],
              [ 3,  4,  5],
              [ 6,  7,  8],
              [ 9, 10, 11]])
      torch.Size([4, 3])
  • reshape 方法

    有些操作会让张量存储结构扭曲,直接使用 view 会失败,可以用 reshape 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 有些操作会让张量存储结构扭曲,直接使用view会失败,可以用reshape方法

    matrix26 = torch.arange(0,12).view(2,6)
    print(matrix26)
    print(matrix26.shape)

    # 转置操作让张量存储结构扭曲
    matrix62 = matrix26.t()
    print(matrix62.is_contiguous())

    # 直接使用view方法会失败,可以使用reshape方法
    #matrix34 = matrix62.view(3,4) #error!
    matrix34 = matrix62.reshape(3,4) #等价于matrix34 = matrix62.contiguous().view(3,4)
    print(matrix34)

    Results:

    1
    2
    3
    4
    5
    6
    7
    tensor([[ 0,  1,  2,  3,  4,  5],
    [ 6, 7, 8, 9, 10, 11]])
    torch.Size([2, 6])
    False
    tensor([[ 0, 6, 1, 7],
    [ 2, 8, 3, 9],
    [ 4, 10, 5, 11]])

2.4 Tensor and numpy array

可以用 numpy 方法从 Tensor 得到 numpy 数组,也可以用 torch.from_numpynumpy 数组得到 Tensor

Note:这两种方法关联的 Tensornumpy 数组是共享数据内存的。如果改变其中一个,另外一个的值也会发生改变。

如果有需要,可以: 1. 用张量的 clone 方法拷贝张量,中断这种关联。 2. 使用 item 方法从标量张量得到对应的 Python 数值。 3. 使用tolist方法从张量得到对应的Python数值列表。

  • from_numpy()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import numpy as np
    import torch

    #torch.from_numpy函数从numpy数组得到Tensor

    arr = np.zeros(3)
    tensor = torch.from_numpy(arr)
    print("before add 1:")
    print(arr)
    print(tensor)

    print("\nafter add 1:")
    np.add(arr,1, out = arr) #给 arr增加1,tensor也随之改变
    print(arr)
    print(tensor)

    Results:

      before add 1:
      [0. 0. 0.]
      tensor([0., 0., 0.], dtype=torch.float64)
    
      after add 1:
      [1. 1. 1.]
      tensor([1., 1., 1.], dtype=torch.float64)
  • numpy()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # numpy方法从Tensor得到numpy数组

    tensor = torch.zeros(3)
    arr = tensor.numpy()
    print("before add 1:")
    print(tensor)
    print(arr)

    print("\nafter add 1:")

    #使用带下划线的方法表示计算结果会返回给调用 张量
    tensor.add_(1) #给 tensor增加1,arr也随之改变
    #或: torch.add(tensor,1,out = tensor)
    print(tensor)
    print(arr)

    Results:

      before add 1:
      tensor([0., 0., 0.])
      [0. 0. 0.]
    
      after add 1:
      tensor([1., 1., 1.])
      [1. 1. 1.]
  • clone()

    可以用clone() 方法拷贝张量,中断这种关联

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    tensor = torch.zeros(3)

    #使用clone方法拷贝张量, 拷贝后的张量和原始张量内存独立
    arr = tensor.clone().numpy() # 也可以使用tensor.data.numpy()
    print("before add 1:")
    print(tensor)
    print(arr)

    print("\nafter add 1:")

    #使用 带下划线的方法表示计算结果会返回给调用 张量
    tensor.add_(1) #给 tensor增加1,arr不再随之改变
    print(tensor)
    print(arr)

    Results:

      before add 1:
      tensor([0., 0., 0.])
      [0. 0. 0.]
    
      after add 1:
      tensor([1., 1., 1.])
      [0. 0. 0.]
  • tolist()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # item方法和tolist方法可以将张量转换成Python数值和数值列表
    scalar = torch.tensor(1.0)
    s = scalar.item()
    print(s)
    print(type(s))

    tensor = torch.rand(2,2)
    t = tensor.tolist()
    print(t)
    print(type(t))

    Results:

      1.0
      <class 'float'>
      [[0.6784629225730896, 0.811877429485321], [0.28022295236587524, 0.9736372828483582]]
      <class 'list'>

3. Auto differentiation mechanism

神经网络通常依赖反向传播求梯度来更新网络参数,求梯度过程通常是一件非常复杂而容易出错的事情。而深度学习框架可以帮助我们自动地完成这种求梯度运算。

Pytorch 一般通过反向传播 backward 方法 实现这种求梯度计算。该方法求得的梯度将存在对应自变量张量的 grad 属性下。除此之外,也能够调用 torch.autograd.grad 函数来实现求梯度计算。这就是 Pytorch 的自动微分机制。

3.1 利用 backward 方法求导数

backward 方法通常在一个标量张量上调用,该方法求得的梯度将存在对应自变量张量的 grad 属性下。如果调用的张量非标量,则要传入一个和它同形状的 gradient 参数张量。相当于用该 gradient 参数张量与调用张量作向量点乘,得到的标量结果再反向传播。

3.1.1 标量的反向传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np 
import torch

# f(x) = a*x**2 + b*x + c的导数

x = torch.tensor(0.0,requires_grad = True) # x需要被求导
a = torch.tensor(1.0)
b = torch.tensor(-2.0)
c = torch.tensor(1.0)
y = a*torch.pow(x,2) + b*x + c

y.backward()
dy_dx = x.grad
print(dy_dx)

Results:

tensor(-2.)

3.1.2 非标量的反向传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import numpy as np 
import torch

# f(x) = a*x**2 + b*x + c

x = torch.tensor([[0.0,0.0],[1.0,2.0]],requires_grad = True) # x需要被求导
a = torch.tensor(1.0)
b = torch.tensor(-2.0)
c = torch.tensor(1.0)
y = a*torch.pow(x,2) + b*x + c

gradient = torch.tensor([[1.0,1.0],[1.0,1.0]])

print("x:\n",x)
print("y:\n",y)
y.backward(gradient = gradient)
x_grad = x.grad
print("x_grad:\n",x_grad)

Results:

x:
 tensor([[0., 0.],
        [1., 2.]], requires_grad=True)
y:
 tensor([[1., 1.],
        [0., 1.]], grad_fn=<AddBackward0>)
x_grad:
 tensor([[-2., -2.],
        [ 0.,  2.]])

3.1.3 非标量的反向传播可以用标量的反向传播实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np 
import torch

# f(x) = a*x**2 + b*x + c

x = torch.tensor([[0.0,0.0],[1.0,2.0]],requires_grad = True) # x需要被求导
a = torch.tensor(1.0)
b = torch.tensor(-2.0)
c = torch.tensor(1.0)
y = a*torch.pow(x,2) + b*x + c

gradient = torch.tensor([[1.0,1.0],[1.0,1.0]])
z = torch.sum(y*gradient)

print("x:",x)
print("y:",y)
z.backward()
x_grad = x.grad
print("x_grad:\n",x_grad)

Results:

x: tensor([[0., 0.],
        [1., 2.]], requires_grad=True)
y: tensor([[1., 1.],
        [0., 1.]], grad_fn=<AddBackward0>)
x_grad:
 tensor([[-2., -2.],
        [ 0.,  2.]])

3.2 利用autograd.grad方法求导数

3.2.1 单变量求导

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import numpy as np 
import torch

# f(x) = a*x**2 + b*x + c的导数

x = torch.tensor(0.0,requires_grad = True) # x需要被求导
a = torch.tensor(1.0)
b = torch.tensor(-2.0)
c = torch.tensor(1.0)
y = a*torch.pow(x,2) + b*x + c


# create_graph 设置为 True 将允许创建更高阶的导数
dy_dx = torch.autograd.grad(y,x,create_graph=True)[0]
print(dy_dx.data)

# 求二阶导数
dy2_dx2 = torch.autograd.grad(dy_dx,x)[0]

print(dy2_dx2.data)

Results:

tensor(-2.)
tensor(2.)

3.2.2 多变量求导

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np 
import torch

x1 = torch.tensor(1.0,requires_grad = True) # x需要被求导
x2 = torch.tensor(2.0,requires_grad = True)

y1 = x1*x2
y2 = x1+x2


# 允许同时对多个自变量求导数
(dy1_dx1,dy1_dx2) = torch.autograd.grad(outputs=y1,inputs = [x1,x2],retain_graph = True)
print(dy1_dx1,dy1_dx2)

# 如果有多个因变量,相当于把多个因变量的梯度结果求和
(dy12_dx1,dy12_dx2) = torch.autograd.grad(outputs=[y1,y2],inputs = [x1,x2])
print(dy12_dx1,dy12_dx2)

Results:

tensor(2.) tensor(1.)
tensor(3.) tensor(2.)

3.3 利用自动微分和优化器求最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import numpy as np 
import torch

# f(x) = a*x**2 + b*x + c的最小值

x = torch.tensor(0.0,requires_grad = True) # x需要被求导
a = torch.tensor(1.0)
b = torch.tensor(-2.0)
c = torch.tensor(1.0)

optimizer = torch.optim.SGD(params=[x],lr = 0.01)


def f(x):
result = a*torch.pow(x,2) + b*x + c
return(result)

for i in range(500):
optimizer.zero_grad()
y = f(x)
y.backward()
optimizer.step()


print("y=",f(x).data,";","x=",x.data)

Results:

y= tensor(0.) ; x= tensor(1.0000)

4. 动态计算图

本节学习 Pytorch 的动态计算图。

包括:

  • 动态计算图简介
  • 计算图中的Function
  • 计算图和反向传播
  • 叶子节点和非叶子节点
  • 计算图在TensorBoard中的可视化

4.1 动态计算图简介

Pytorch 的计算图由节点和边组成,节点表示张量或者 Function,边表示张量和 Function 之间的依赖关系。

Pytorch 中的计算图是动态图。这里的动态主要有两重含义:

  1. 计算图的正向传播是立即执行的。无需等待完整的计算图创建完毕,每条语句都会在计算图中动态添加节点和边,并立即执行正向传播得到计算结果。

  2. 计算图在反向传播后立即销毁。下次调用需要重新构建计算图。如果在程序中使用了 backward 方法执行了反向传播,或者利用 torch.autograd.grad 方法计算了梯度,那么创建的计算图会被立即销毁,释放存储空间,下次调用需要重新创建。

  • 计算图的正向传播是立即执行的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import torch 
    w = torch.tensor([[3.0,1.0]],requires_grad=True)
    b = torch.tensor([[3.0]],requires_grad=True)
    X = torch.randn(10,2)
    Y = torch.randn(10,1)
    Y_hat = X@w.t() + b # Y_hat定义后其正向传播被立即执行,与其后面的loss创建语句无关
    loss = torch.mean(torch.pow(Y_hat-Y,2))

    print(loss.data)
    print(Y_hat.data)

    Results:

      tensor(13.0005)
      tensor([[ 2.9207],
              [ 3.6278],
              [ 3.5438],
              [-3.7237],
              [ 1.8447],
              [ 1.5206],
              [-1.8222],
              [-0.3198],
              [ 5.2020],
              [ 6.8607]])
  • 计算图在反向传播后立即销毁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import torch 
    w = torch.tensor([[3.0,1.0]],requires_grad=True)
    b = torch.tensor([[3.0]],requires_grad=True)
    X = torch.randn(10,2)
    Y = torch.randn(10,1)
    Y_hat = X@w.t() + b # Y_hat定义后其正向传播被立即执行,与其后面的loss创建语句无关
    loss = torch.mean(torch.pow(Y_hat-Y,2))

    #计算图在反向传播后立即销毁,如果需要保留计算图, 需要设置retain_graph = True
    loss.backward() #loss.backward(retain_graph = True)

    #loss.backward() #如果再次执行反向传播将报错

4.2 计算图中的Function

计算图中的 张量我们已经比较熟悉了, 计算图中的另外一种节点是 Function, 实际上就是 Pytorch 中各种对张量操作的函数。

这些 Function 和我们 Python 中的函数有一个较大的区别,那就是它同时包括正向计算逻辑和反向传播的逻辑。我们可以通过继承 torch.autograd.Function 来创建这种支持反向传播的 Function

  • 自定义 Relu

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class MyReLU(torch.autograd.Function):

    #正向传播逻辑,可以用ctx存储一些值,供反向传播使用。
    @staticmethod
    def forward(ctx, input):
    ctx.save_for_backward(input)
    return input.clamp(min=0)

    #反向传播逻辑
    @staticmethod
    def backward(ctx, grad_output):
    input, = ctx.saved_tensors
    grad_input = grad_output.clone()
    grad_input[input < 0] = 0
    return grad_input

  • 反向传播

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import torch 
    w = torch.tensor([[3.0,1.0]],requires_grad=True)
    b = torch.tensor([[3.0]],requires_grad=True)
    X = torch.tensor([[-1.0,-1.0],[1.0,1.0]])
    Y = torch.tensor([[2.0,3.0]])

    relu = MyReLU.apply # relu现在也可以具有正向传播和反向传播功能
    Y_hat = relu(X@w.t() + b)
    loss = torch.mean(torch.pow(Y_hat-Y,2))

    loss.backward()

    print(w.grad)
    print(b.grad)

    # Y_hat的梯度函数即是我们自己所定义的 MyReLU.backward
    print(Y_hat.grad_fn)

    Results:

      tensor([[4.5000, 4.5000]])
      tensor([[4.5000]])
      <torch.autograd.function.MyReLUBackward object at 0x000001BB4DEECF20>

4.3 计算图与反向传播

了解了 Function 的功能,我们可以简单地理解一下反向传播的原理和过程。理解该部分原理需要一些高等数学中求导链式法则的基础知识。

1
2
3
4
5
6
7
8
import torch 

x = torch.tensor(3.0,requires_grad=True)
y1 = x + 1
y2 = 2*x
loss = (y1-y2)**2

loss.backward()

loss.backward()语句调用后,依次发生以下计算过程。

  1. loss 自己的 grad 梯度赋值为 1,即对自身的梯度为 1
  2. loss 根据其自身梯度以及关联的 backward 方法,计算出其对应的自变量即 y1y2 的梯度,将该值赋值到 y1.grady2.grad
  3. y2y1 根据其自身梯度以及关联的 backward 方法, 分别计算出其对应的自变量 x 的梯度,x.grad 将其收到的多个梯度值累加。

Note:1,2,3 步骤的求梯度顺序和对多个梯度值的累加规则恰好是求导链式法则的程序表述。正因为求导链式法则衍生的梯度累加规则,张量的 grad 梯度不会自动清零,在需要的时候需要手动置零。

4.4 叶子节点和非叶子节点

执行下面代码,我们会发现 loss.grad 并不是我们期望的 1 ,而是 None。类似地, y1.grad 以及 y2.grad也是 None

这是为什么呢?

这是由于 它们不是叶子节点张量。在反向传播过程中,只有 is_leaf=True 的叶子节点,需要求导的张量的导数结果才会被最后保留下来。

那么什么是叶子节点张量呢?

叶子节点张量需要满足两个条件

  1. 叶子节点张量是由用户直接创建的张量,而非由某个 Function 通过计算得到的张量;
  2. 叶子节点张量的 requires_grad 属性必须为 True

Pytorch 设计这样的规则主要是为了节约内存或者显存空间,因为几乎所有的时候,用户只会关心他自己直接创建的张量的梯度。所有依赖于叶子节点张量的张量, 其 requires_grad 属性必定是 True 的,但其梯度值只在计算过程中被用到,不会最终存储到 grad 属性中。

如果需要保留中间计算结果的梯度到 grad 属性中,可以使用 retain_grad 方法。如果仅仅是为了调试代码查看梯度值,可以利用 register_hook 打印日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch 

x = torch.tensor(3.0,requires_grad=True)
y1 = x + 1
y2 = 2*x
loss = (y1-y2)**2

loss.backward()
print("loss.grad:", loss.grad)
print("y1.grad:", y1.grad)
print("y2.grad:", y2.grad)
print(x.grad)

print(x.is_leaf)
print(y1.is_leaf)
print(y2.is_leaf)
print(loss.is_leaf)

Results:

loss.grad: None
y1.grad: None
y2.grad: None
tensor(4.)   

True
False
False
False

利用 retain_grad 可以保留非叶子节点的梯度值,利用 register_hook 可以查看非叶子节点的梯度值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch 

#正向传播
x = torch.tensor(3.0,requires_grad=True)
y1 = x + 1
y2 = 2*x
loss = (y1-y2)**2

#非叶子节点梯度显示控制
y1.register_hook(lambda grad: print('y1 grad: ', grad))
y2.register_hook(lambda grad: print('y2 grad: ', grad))
loss.retain_grad()

#反向传播
loss.backward()
print("loss.grad:", loss.grad)
print("x.grad:", x.grad)

Results:

y2 grad:  tensor(4.)
y1 grad:  tensor(-4.)
loss.grad: tensor(1.)
x.grad: tensor(4.)

4.5 计算图在 TensorBoard 中的可视化

可以利用 torch.utils.tensorboard 将计算图导出到 TensorBoard 进行可视化。

1
2
3
4
5
6
7
8
9
10
11
12
from torch import nn 
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.w = nn.Parameter(torch.randn(2,1))
self.b = nn.Parameter(torch.zeros(1,1))

def forward(self, x):
y = x@self.w + self.b
return y

net = Net()
1
2
3
4
5
6
from torch.utils.tensorboard import SummaryWriter

data_dir = '../data/'
writer = SummaryWriter(data_dir + 'tensorboard')
writer.add_graph(net,input_to_model = torch.rand(10,2))
writer.close()
1
2
%load_ext tensorboard
#%tensorboard --logdir ../data/tensorboard
1
2
from tensorboard import notebook
notebook.list()
1
2
#在tensorboard中查看模型
notebook.start("--logdir ../data/tensorboard")
-------------This blog is over! Thanks for your reading-------------