网络搭建

基本概念 中我们介绍了计算图、张量和算子,神经网络可以看成一个计算图。在 MegEngine 中,我们按照计算图的拓扑结构,将张量和算子连接起来,即可完成对网络的搭建。MegEngine 提供了基于 functional 和基于 Module 的两种方式搭建网络。 functional 仅提供最基本的算子功能,数据连接的工作完全由用户完成; Module 对网络模块(包含若干算子及其参数的基本单元)进行了进一步的封装,代码更易复用和维护。

基于 functional 搭建网络

functional 包提供了常用的算子函数(如 conv2d()linear() 等)。这些函数接受参与计算的张量并返回计算结果。参与计算的张量通常包括两类:输入数据和该算子自身的参数,其中后者是网路中需要学习的变量。比如,二维卷积( conv2d() )接受多通道的二维图像作为输入数据,把卷积核作为参数,输出经卷积操作后的多通道二维图像。

算子的输入和输出数据都是 Tensor 类型。算子的参数通常由 Parameter 类表示。 ParameterTensor 的子类,其对象(即网络参数)可以被优化器更新。更多内容参见 网络的训练和测试

下面的例子实现了一个两层卷积网络(使用 ReLU) 作为激活函数):

  1. import megengine as mge
  2. import megengine.functional as F
  3. import numpy as np
  4.  
  5. def two_layer_conv(x):
  6. # (8, 3, 3, 3) 代表(输出信道数,输入信道数,卷积核高度,卷积核宽度)
  7. conv_weight = mge.Parameter(np.random.randn(8, 3, 3, 3).astype(np.float32))
  8. # 对于 8 个卷积核,提供 8 个 bias
  9. conv_bias = mge.Parameter(np.zeros((1, 8, 1, 1), dtype=np.float32))
  10. x = F.conv2d(x, conv_weight, conv_bias)
  11. x = F.relu(x)
  12. conv_weight = mge.Parameter(np.random.randn(16, 8, 3, 3).astype(np.float32))
  13. conv_bias = mge.Parameter(np.zeros((1, 16, 1, 1), dtype=np.float32))
  14. x = F.conv2d(x, conv_weight, conv_bias)
  15. x = F.relu(x)
  16. return x
  17.  
  18. # 输入形状为 (2, 3, 32, 32) 的张量
  19. x = mge.tensor(np.random.randn(2, 3, 32, 32).astype(np.float32))
  20. out = two_layer_conv(x)
  21. print(out.shape) # 输出: (2, 16, 28, 28)

基于 Module 搭建网络

在上面的代码中,对于每一个需要参数的算子,都需要单独定义其网络参数。由于“ conv + relu ”这样的组合出现了两次,代码显得臃肿。对于更加复杂的网络,这样的写法可读性、可复用性和可维护性会比较差。

为了更好的封装和复用算子, MegEngine 在 functional 基础上提供了 module 包。

megengine.module 包定义了抽象的网络模块基类 Module 。它是构造网络的基本单元,可以被组合和叠加。它定义了网络模块的基本接口和属性,如“前向传播”等。所有 Module 子类都需要实现 Module 定义的两个抽象方法,介绍如下:

  • init() :在构造方法中创建这个模块,包括定义网络参数、构造和连接其子模块等工作。

  • forward() : 该方法定义前向传播计算流程。它接受输入数据并返回前向传播的计算结果。注意, Module 对象是可被调用的 ( callable ),其实现就是 forward()

megengine.module 包提供了常用的网络基本模块,如 Conv2dLinear 等。以 Conv2d 为例,该类的 init() 方法定义并初始化卷积核参数,其 forward() 方法执行卷积操作。

基于各种常用的网络模块,我们可以方便地搭建非常复杂的网络。例如,上一个例子的网络定义可以简化成如下写法:

  1. import megengine.module as M
  2.  
  3. # 为了演示,我们在这里定义了一个简单的卷积模块。注意: MegEngine 已经提供了更为通用的 Conv2d 模块。
  4. class ConvReLU(M.Module):
  5. def __init__(self, in_channels, out_channels):
  6. # 先调用父类的初始化
  7. super().__init__()
  8.  
  9. # 定义卷积权重和 bias ,作为模块参数
  10. self.conv_weight = mge.Parameter(np.random.randn(out_channels, in_channels, 3, 3).astype(np.float32))
  11. self.conv_bias = mge.Parameter(np.zeros((1, out_channels, 1, 1), dtype=np.float32))
  12. # 将激活函数 ReLU 作为子模块
  13. self.relu = M.ReLU()
  14.  
  15. def forward(self, x):
  16. x = F.conv2d(x, self.conv_weight, self.conv_bias)
  17. x = self.relu(x)
  18. return x
  19.  
  20.  
  21. # 基于 ConvReLU ,定义一个两层卷积网络
  22. class TwoLayerConv(M.Module):
  23. def __init__(self):
  24. super().__init__()
  25. self.conv_relu1 = ConvReLU(3, 8)
  26. self.conv_relu2 = ConvReLU(8, 16)
  27.  
  28. def forward(self, x):
  29. x = self.conv_relu1(x)
  30. x = self.conv_relu2(x)
  31. return x
  32.  
  33. # 输入形状为 (2, 3, 32, 32) 的张量
  34. x = mge.tensor(np.random.randn(2, 3, 32, 32).astype(np.float32))
  35. two_layer_conv_module = TwoLayerConv()
  36. out = two_layer_conv_module(x)
  37. print(out.shape) # 输出: (2, 16, 28, 28)

使用 Module 定义的网络比使用 functional 进一步封装了内部实现,更易复用,统一的接口使得代码更易维护。 我们推荐使用 Module 搭建网络。

此外, Module 其它常用的方法如下:

  • parameters() : 该方法返回包含网络参数的迭代器。

  • named_parameters() : 该方法返回包含参数名称及对应网络参数的迭代器。

  • state_dict():返回以参数名称和网络参数为键值对的有序字典,可用于保存训练好的模型。比如,对于上面定义的 ConvReLU 模块,打印它的一个实例的 state_dict

  1. conv_relu = ConvReLU(2, 3)
  2. print(conv_relu.state_dict())

输出的参数信息有卷积的权重项 'conv_weight' 和偏置项 'conv_bias'

  1. OrderedDict([('conv_bias', array([[[[0.]],
  2.  
  3. [[0.]],
  4.  
  5. [[0.]]]], dtype=float32)), ('conv_weight', array([[[[-0.53457755, 0.2799128 , -0.6624546 ],
  6. [-0.9222688 , 1.2226251 , -0.5591961 ],
  7. [-0.45538583, -0.95166504, 1.1570141 ]],
  8.  
  9. [[-0.89926094, 0.09956062, -0.7329557 ],
  10. [-0.67284465, 0.34817234, 0.6731445 ],
  11. [ 0.61970276, 1.8007269 , 1.6130987 ]]],
  12.  
  13.  
  14. [[[ 1.7108068 , -1.7188625 , -0.52539474],
  15. [-0.04049037, 0.03099988, -1.4271212 ],
  16. [-0.9138133 , 0.3976046 , -1.1582668 ]],
  17.  
  18. [[-1.2193677 , 0.24107741, -0.50833786],
  19. [ 0.9088649 , -0.2747458 , -0.1261102 ],
  20. [ 0.00594431, 0.65737075, 1.5280651 ]]],
  21.  
  22.  
  23. [[[ 0.24874896, -1.3824748 , 2.2161844 ],
  24. [-0.6629168 , 1.0220655 , -0.53007567],
  25. [ 0.37829646, 1.1993718 , 1.0667052 ]],
  26.  
  27. [[-0.66264534, -0.6392335 , -0.41280702],
  28. [ 1.7417566 , 0.75295806, -0.4228349 ],
  29. [-0.94973356, 2.4136777 , -0.06665667]]]], dtype=float32))])

最后,我们来搭建更加复杂的、经典的 LeNet 网络,其结构如下图:

../_images/lenet.jpg 图1 LeNet ( http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf )

使用 Module 搭建 LeNet 的代码如下:

  1. class LeNet(M.Module):
  2. def __init__(self):
  3. super(LeNet, self).__init__()
  4. # 单信道图片, 两层 5x5 卷积 + ReLU + 池化
  5. self.conv1 = M.Conv2d(1, 6, 5)
  6. self.relu1 = M.ReLU()
  7. self.pool1 = M.MaxPool2d(2, 2)
  8. self.conv2 = M.Conv2d(6, 16, 5)
  9. self.relu2 = M.ReLU()
  10. self.pool2 = M.MaxPool2d(2, 2)
  11. # 两层全连接 + ReLU
  12. self.fc1 = M.Linear(16 * 5 * 5, 120)
  13. self.relu3 = M.ReLU()
  14. self.fc2 = M.Linear(120, 84)
  15. self.relu4 = M.ReLU()
  16. # 分类器
  17. self.classifer = M.Linear(84, 10)
  18.  
  19. def forward(self, x):
  20. x = self.pool1(self.relu1(self.conv1(x)))
  21. x = self.pool2(self.relu2(self.conv2(x)))
  22. # F.flatten 将原本形状为 (N, C, H, W) 的张量x从第一个维度(即C)开始拉平成一个维度,
  23. # 得到的新张量形状为 (N, C*H*W) 。 等价于 reshape 操作: x = x.reshape(x.shape[0], -1)
  24. x = F.flatten(x, 1)
  25. x = self.relu3(self.fc1(x))
  26. x = self.relu4(self.fc2(x))
  27. x = self.classifer(x)
  28. return x
  29.  
  30.  
  31. # 输入形状为 (2, 1, 32, 32) 的张量
  32. x = mge.tensor(np.random.randn(2, 1, 32, 32).astype(np.float32))
  33. le_net = LeNet()
  34. # 调用网络,即执行 le_net 的 forward 成员方法,返回网络处理结果
  35. out = le_net(x)
  36. print(out.shape) # 输出: (2, 10)