基本概念

MegEngine 是基于计算图的深度神经网络学习框架。本节内容会简要介绍计算图及其相关基本概念,以及它们在 MegEngine 中的实现。

计算图(Computation Graph)

我们通过一个简单的数学表达式 基本概念 - 图1 来介绍计算图的基本概念,如下图所示:

../_images/computer_graph.png 图1

从中我们可以看到,计算图中存在:

  • 数据节点(图中的实心圈):如输入数据 基本概念 - 图3基本概念 - 图4基本概念 - 图5 ,运算得到的中间数据 基本概念 - 图6 ,以及最终的运算输出 基本概念 - 图7
  • 计算节点(图中的空心圈):图中 和 + 分别表示计算节点 乘法*加法,是施加在数据节点上的运算;

  • 边(图中的箭头):表示数据的流向,体现了数据节点和计算节点之间的依赖关系;

如上,便是一个简单的计算图示例。计算图是一个包含数据节点和计算节点的有向图(可以是有环的,也可以是无环的),是数学表达式的形象化表示。在深度学习领域,任何复杂的深度神经网络本质上都可以用一个计算图表示出来。

前向传播 是计算由计算图表示的数学表达式的值的过程。在图1中,变量 基本概念 - 图8基本概念 - 图9 ,从左侧输入,首先经过乘法运算得到中间结果 基本概念 - 图10 ,接着,基本概念 - 图11 和输入变量 基本概念 - 图12 经过加法运算,得到右侧最终的输出 基本概念 - 图13 ,这就是一个完整的前向传播过程。

在 MegEngine 中,我们用张量(Tensor)表示计算图中的数据节点,以及用算子(Operator)实现数据节点之间的运算。

张量(Tensor)

与 PyTorch,TensorFlow 等深度学习框架类似,MegEngine 使用张量(Tensor)来表示计算图中的数据。张量(Tensor)可以看做 NumPy 中的数组,它可以是标量、向量、矩阵或者多维数组。我们可以通过 NumPy 或者 Python List 来创建一个 Tensor 。

  1. import numpy as np
  2. import megengine as mge
  3.  
  4. # 初始化一个维度为 (2, 5) 的 ndarray,并转化成 MegEngine 的 Tensor
  5. # 注:目前 MegEngine Tensor 不支持 float64 数值类型,所以这里我们显式指定了 ndarray 的数值类型
  6. a = mge.tensor(np.random.random((2,5)).astype('float32'))
  7. print(a)
  8.  
  9. # 初始化一个长度为3的列表,并转化成 Tensor
  10. b = mge.tensor([1., 2., 3.])
  11. print(b)

输出:

  1. Tensor([[0.2976 0.4078 0.5957 0.3945 0.9413]
  2. [0.7519 0.3313 0.0913 0.3345 0.3256]])
  3.  
  4. Tensor([1. 2. 3.])

我们可以通过 set_value() 来更改 Tensor 的值。

  1. c = mge.tensor()
  2. # 此时 Tensor 尚未被初始化,值为 None
  3. print(c)
  4. c.set_value(np.random.random((2,5)).astype("float32"))
  5. # 此时我们将 Tensor c 进行了赋值
  6. print(c)

输出:

  1. Tensor(None)
  2. Tensor([[0.68 0.9126 0.7312 0.3037 0.8082]
  3. [0.1965 0.0413 0.395 0.6975 0.9103]])

通过 dtype 属性我们可以获取 Tensor 的数据类型;通过 astype() 方法我们可以拷贝创建一个指定数据类型的新 Tensor ,原 Tensor 不变。

  1. print(c.dtype)
  2. d = c.astype("float16")
  3. print(d.dtype)

输出:

  1. <class 'numpy.float32'>
  2. <class 'numpy.float16'>

通过 shape 属性,我们可以获取 Tensor 的形状:

  1. print(c.shape)

输出为一个Tuple:

  1. (2, 5)

通过 numpy() 方法,我们可以将 Tensor 转换为 numpy.ndarray:

  1. a = mge.tensor(np.random.random((2,5)).astype('float32'))
  2. print(a)
  3.  
  4. b = a.numpy()
  5. print(b)

输出:

  1. Tensor([[0.2477 0.9139 0.8685 0.5265 0.341 ]
  2. [0.6463 0.0599 0.555 0.1881 0.4283]])
  3.  
  4. [[0.2477342 0.9139376 0.8685143 0.526512 0.34099308]
  5. [0.64625365 0.05993681 0.5549845 0.18809062 0.42833906]]

算子(Operator)

MegEngine 中通过算子 (Operator) 来表示运算。类似于 NumPy,MegEngine 中的算子支持基于 Tensor 的常见数学运算和操作。下面介绍几个简单示例:

Tensor 的加法:

  1. a = mge.tensor(np.random.random((2,5)).astype('float32'))
  2. print(a)
  3. b = mge.tensor(np.random.random((2,5)).astype('float32'))
  4. print(b)
  5. print(a + b)

输出:

  1. Tensor([[0.119 0.5816 0.5693 0.3495 0.4687]
  2. [0.4559 0.524 0.3877 0.0287 0.9086]])
  3.  
  4. Tensor([[0.2488 0.5017 0.0975 0.2759 0.3443]
  5. [0.8404 0.7221 0.5179 0.5839 0.1876]])
  6.  
  7. Tensor([[0.3678 1.0833 0.6667 0.6254 0.813 ]
  8. [1.2963 1.2461 0.9056 0.6126 1.0962]])

Tensor 的切片:

  1. print(a[1, :])

输出:

  1. Tensor([0.4559 0.524 0.3877 0.0287 0.9086])

Tensor 形状的更改:

  1. a.reshape(5, 2)

输出:

  1. Tensor([[0.4228 0.2097]
  2. [0.9081 0.5133]
  3. [0.2152 0.7341]
  4. [0.0468 0.5756]
  5. [0.3852 0.2363]])

reshape() 的参数允许存在单个维度的缺省值,用 -1 表示。此时,reshape 会自动推理该维度的值:

  1. # 原始维度是 (2, 5),当给出 -1的缺省维度值时,可以推理出另一维度为10
  2. a = a.reshape(1, -1)
  3. print(a.shape)

输出:

  1. (1, 10)

MegEngine 的 functional 提供了更多的算子,比如深度学习中常用的矩阵乘操作、卷积操作等。

Tensor 的矩阵乘:

  1. import megengine.functional as F
  2.  
  3. a = mge.tensor(np.random.random((2,3)).astype('float32'))
  4. print(a)
  5. b = mge.tensor(np.random.random((3,2)).astype('float32'))
  6. print(b)
  7. c = F.matrix_mul(a, b)
  8. print(c)

输出:

  1. Tensor([[0.8021 0.5511 0.7935]
  2. [0.6992 0.9318 0.8736]])
  3.  
  4. Tensor([[0.6989 0.3184]
  5. [0.5645 0.0286]
  6. [0.2932 0.2545]])
  7.  
  8. Tensor([[1.1044 0.4731]
  9. [1.2708 0.4716]])

更多算子可以参见 functional 部分的文档。

不同设备上的 Tensor

创建的Tensor可以位于不同device,这根据当前的环境决定。通过 device 属性查询当前 Tensor 所在的设备。

  1. print(a.device)

输出:

  1. # 如果你是在一个GPU环境下
  2. gpu0:0

通过 to() 可以在另一个 device 上生成当前 Tensor 的拷贝,比如我们将刚刚在 GPU 上创建的 Tensor a 迁移到 CPU 上:

  1. # 下面代码是否能正确执行取决于你当前所在的环境
  2. b = a.to("cpu0")
  3. print(b.device)

输出:

  1. cpu0:0

反向传播和自动求导

反向传播 神经网络的优化通常通过随机梯度下降来进行。我们需要根据计算图的输出,通过链式求导法则,对所有的中间数据节点求梯度,这一过程被称之为 “反向传播”。例如,我们希望得到图1中输出

基本概念 - 图14 关于输入 基本概念 - 图15 的梯度,那么反向传播的过程如下图所示:

../_images/back_prop.png 图2

首先

基本概念 - 图17 ,因此 基本概念 - 图18 ;接着,反向追溯, 基本概念 - 图19 ,因此, 基本概念 - 图20 。根据链式求导法则, 基本概念 - 图21 ,因此最终 基本概念 - 图22 关于输入 基本概念 - 图23 的梯度为 基本概念 - 图24

自动求导 MegEngine 为计算图中的张量提供了自动求导功能,以上图的例子说明:我们假设图中的

基本概念 - 图25 是 shape 为 (1, 3) 的张量, 基本概念 - 图26 是 shape 为 (3, 1) 的张量, 基本概念 - 图27 是一个标量。利用MegEngine 计算 基本概念 - 图28 的过程如下:

  1. import megengine.functional as F
  2.  
  3. x = mge.tensor(np.random.normal(size=(1, 3)).astype('float32'))
  4. w = mge.tensor(np.random.normal(size=(3, 1)).astype('float32'))
  5. b = mge.tensor(np.random.normal(size=(1, )).astype('float32'))
  6. p = F.matrix_mul(x, w)
  7. y = p + b

我们可以直接调用 grad() 方法来计算输出

基本概念 - 图29 关于 基本概念 - 图30 的偏导数: 基本概念 - 图31

  1. import megengine.functional as F
  2. # 在调用 F.grad() 进行梯度计算时,第一个参数(target)须为标量,y 是 (1, 1) 的向量,通过索引操作 y[0] 将其变成维度为 (1, ) 的标量
  3. # use_virtual_grad 是一个涉及动静态图机制的参数,这里可以先不做了解
  4. grad_w = F.grad(y[0], w, use_virtual_grad=False)
  5. print(grad_w)

输出:

  1. Tensor([[-1.5197]
  2. [-1.1563]
  3. [ 1.0447]])

可以看到,求出的梯度本身也是 Tensor。