《MXNet深度学习实战》—3.2 Symbol

举报
华章计算机 发表于 2019/06/16 17:01:28 2019/06/16
【摘要】 本节书摘来自华章计算机《MXNet深度学习实战》一书中的第3章,第3.2节,作者是魏凯峰。

3.2 Symbol

Symbol是MXNet框架中用于构建网络层的模块,Symbol的官方文档地址是:https://mxnet.apache.org/api/python/symbol/symbol.html,与Symbol相关的接口都可以在该文档中查询。与NDArray不同的是,Symbol采用的是符号式编程(symbolic programming),其是MXNet框架实现快速训练和节省显存的关键模块。之前我们介绍过符号式编程的含义,简单来说就是,符号式编程需要先用Symbol接口定义好计算图,这个计算图同时包含定义好的输入和输出格式,然后将准备好的数据输入该计算图完成计算。而3.1节介绍的NDArray采用的是命令式编程(imperative programming),计算过程可以逐步来步实现。其实在你了解了NDArray之后,你完全可以仅仅通过NDArray来定义和使用网络,那么为什么还要提供Symbol呢?主要是为了提高效率。在定义好计算图之后,就可以对整个计算图的显存占用做优化处理,这样就能大大降低训练模型时候的显存占用。

在MXNet中,Symbol接口主要用来构建网络结构层,其次是用来定义输入数据。接下来我们再来列举一个例子,首先定义一个网络结构,具体如下。

1)用mxnet.symbol.Variable()接口定义输入数据,用该接口定义的输入数据类似于一个占位符。

2)用mxnet.symbol.Convolution()接口定义一个卷积核尺寸为3*3,卷积核数量为128的卷积层,卷积层是深度学习算法提取特征的主要网络层,该层将是你在深度学习算法(尤其是图像领域)中使用最为频繁的网络层。

3)用 mxnet.symbol.BatchNorm()接口定义一个批标准化(batch normalization,常用缩写BN表示)层,该层有助于训练算法收敛。

4)用mxnet.symbol.Activation()接口定义一个ReLU激活层,激活层主要用来增加网络层之间的非线性,激活层包含多种类型,其中以ReLU激活层最为常用。

5)用mxnet.symbol.Pooling()接口定义一个最大池化层(pooling),池化层的主要作用在于通过缩减维度去除特征图噪声和减少后续计算量,池化层包含多种形式,常用形式有均值池化和最大池化。

6)用mxnet.symbol.FullyConnected()接口定义一个全连接层,全连接层是深度学习算法中经常用到的层,一般是位于网络的最后几层。需要注意的是,该接口的num_hidden参数表示分类的类别数。

7)用mxnet.symbol.SoftmaxOutput()接口定义一个损失函数层,该接口定义的损失函数是图像分类算法中常用的交叉熵损失函数(cross entropy loss),该损失函数的输入是通过softmax函数得到的,softmax函数是一个变换函数,表示将一个向量变换成另一个维度相同,但是每个元素范围在[0,1]之间的向量,因此该层用mxnet.symbol.SoftmaxOutput()来命名。这样就得到了一个完整的网络结构了。

网络结构定义代码如下:

import mxnet as mx

data = mx.sym.Variable('data')

conv = mx.sym.Convolution(data=data, num_filter=128, kernel=(3,3), pad=(1,1), name='conv1')

bn = mx.sym.BatchNorm(data=conv, name='bn1')

relu = mx.sym.Activation(data=bn, act_type='relu', name='relu1')

pool = mx.sym.Pooling(data=relu, kernel=(2,2), stride=(2,2), pool_type='max', name='pool1')

fc = mx.sym.FullyConnected (data=pool, num_hidden=2, name='fc1')

sym = mx.sym.SoftmaxOutput (data=fc, name='softmax')

mx.sym是mxnet.symbol常用的缩写形式,后续篇章默认采用这种缩写形式。另外在定义每一个网络层的时候最好都能指定名称(name)参数,这样代码看起来会更加清晰。

定义好网络结构之后,你肯定还想看看这个网络结构到底包含哪些参数,毕竟训练模型的过程就是模型参数更新的过程,在MXNet中,list_arguments()方法可用于查看一个Symbol对象的参数,命令如下:

print(sym.list_arguments())

由下面的输出结果可以看出,第一个和最后一个分别是'data'和'softmax_label',这二者分别代表输入数据和标签;'conv1_weight'和'conv1_bias'是卷积层的参数,具体而言前者是卷积核的权重参数,后者是偏置参数;'bn1_gamma'和'bn1_beta'是BN层的参数;'fc1_weight'和'fc1_bias'是全连接层的参数。

['data', 'conv1_weight', 'conv1_bias', 'bn1_gamma', 'bn1_beta', 'fc1_weight', 'fc1_bias', 'softmax_label']

除了查看网络的参数层名称之外,有时候我们还需要查看网络层参数的维度、网络输出维度等信息,这一点对于代码调试而言尤其有帮助。在MXNet中,可以用infer_shape()方法查看一个Symbol对象的层参数维度、输出维度、辅助层参数维度信息,在调用该方法时需要指定输入数据的维度,这样网络结构就会基于指定的输入维度计算层参数、网络输出等维度信息:

arg_shape,out_shape,aux_shape = sym.infer_shape(data=(1,3,10,10))

print(arg_shape)

print(out_shape)

print(aux_shape)

由下面的输出结果可知,第一行表示网络层参数的维度,与前面list_arguments()方法列出来的层参数名一一对应,例如输入数据'data'的维度是(1, 3, 10, 10);卷积层的权重参数'conv1_weight'的维度是(128, 3, 3, 3);卷积层的偏置参数'conv1_bias'的维度是(128,),因为每个卷积核对应于一个偏置参数;全连接层的权重参数'fc1_weight'的维度是(2, 3200),这里的3000是通过计算5*5*128得到的,其中5*5表示全连接层的输入特征图的宽和高。第二行表示网络输出的维度,因为网络的最后一层是输出节点为2的全连接层,且输入数据的批次维度是1,所以输出维度是[(1, 2)]。第三行是辅助参数的维度,目前常见的主要是BN层的参数维度。

[(1, 3, 10, 10), (128, 3, 3, 3), (128,), (128,), (128,), (2, 3200), (2,), (1,)]

[(1, 2)]

[(128,), (128,)]

如果要截取通过Symbol模块定义的网络结构中的某一部分也非常方便,在MXNet中可以通过get_internals()方法得到Symbol对象的所有层信息,然后选择要截取的层即可,比如将sym截取成从输入到池化层为止:

sym_mini = sym.get_internals()['pool1_output']

print(sym_mini.list_arguments())

输出结果如下,可以看到层参数中没有sym原有的全连接层和标签层信息了:

['data', 'conv1_weight', 'conv1_bias', 'bn1_gamma', 'bn1_beta']

截取之后还可以在截取得到的Symbol对象后继续添加网络层,比如增加一个输出节点为5的全连接层和一个softmax层:

fc_new = mx.sym.FullyConnected (data=sym_mini, num_hidden=5, name='fc_new')

sym_new = mx.sym.SoftmaxOutput (data=fc_new, name='softmax')

print(sym_new.list_arguments())

输出结果如下,可以看到全连接层已经被替换了:

['data', 'conv1_weight', 'conv1_bias', 'bn1_gamma', 'bn1_beta', 'fc_new_weight', 'fc_new_bias', 'softmax_label']

除了定义神经网络层之外,Symbol模块还可以实现NDArray的大部分操作,接下来以数组相加和相乘为例介绍通过Symbol模块实现上述操作的方法。首先通过 mxnet.symbol.Variable()接口定义两个输入data_a和data_b;然后定义data_a和data_b相加并与data_c相乘的操作以得到结果s,通过打印s的类型可以看出s的类型是Symbol,代码如下:

import mxnet as mx

data_a = mx.sym.Variable ('data_a')  

data_b = mx.sym.Variable ('data_b')

data_c = mx.sym.Variable ('data_c')

s = data_c*(data_a+data_b)

print(type(s))

输出结果如下:

<class 'mxnet.symbol.symbol.Symbol'>

接下来,调用s的bind()方法将具体输入和定义的操作绑定到执行器,同时还需要为bind()方法指定计算是在CPU还是GPU上进行,执行bind操作后就得到了执行器e,最后打印e的类型进行查看,代码如下:

e = s.bind(mx.cpu(), {'data_a':mx.nd.array([1,2,3]), 'data_b':mx.nd.array([4,5,6]),

    'data_c':mx.nd.array([2,3,4])})

print(type(e))

输出结果如下:

<class 'mxnet.executor.Executor'>

这个执行器就是一个完整的计算图了,因此可以调用执行器的forward()方法进行计算以得到结果:

output=e.forward()

print(output[0])

输出结果如下:

[ 10. 21. 36.]

<NDArray 3 @cpu(0)>

相比之下,通过NDArray模块实现这些操作则要简洁和直观得多,代码如下:

import mxnet as mx

data_a = mx.nd.array([1,2,3])

data_b = mx.nd.array([4,5,6])

data_c = mx.nd.array([2,3,4])

result = data_c*(data_a+data_b)

print(result)

输出结果如下:

[ 10. 21. 36.]

<NDArray 3 @cpu(0)>

虽然使用Symbol接口的实现看起来有些复杂,但是当你定义好计算图之后,很多显存是可以重复利用或共享的,比如在Symbol模块实现版本中,底层计算得到的data_a+data_b的结果会存储在data_a或data_b所在的空间,因为在该计算图中,data_a和data_b在执行完相加计算后就不会再用到了。

前面介绍的是Symbol模块中Variable接口定义的操作和NDArray模块中对应实现的相似性,除此之外,Symbol模块中关于网络层的操作在NDArray模块中基本上也有对应的操作,这对于静态图的调试来说非常有帮助。之前提到过,Symbol模块采用的是符号式编程(或者称为静态图),即首先需要定义一个计算图,定义好计算图之后再执行计算,这种方式虽然高效,但是对代码调试其实是不大友好的,因为你很难获取中间变量的值。现在因为采用命令式编程的NDArray模块中基本上包含了Symbol模块中同名的操作,因此可以在一定程度上帮助调试代码。接下来以卷积层为例看看如何用NDArray模块实现一个卷积层操作,首先用mxnet.ndarray.arange()接口初始化输入数据,这里定义了一个4维数据data,之所以定义为4维是因为模型中的数据流基本上都是4维的。具体代码如下:

data = mx.nd.arange(0,28).reshape((1,1,4,7))

print(data)

输出结果如下:

[[[[ 0.  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. 26. 27.]]]]

<NDArray 1x1x4x7 @cpu(0)>

然后,通过mxnet.ndarray.Convolution()接口定义卷积层操作,该接口的输入除了与mxnet.symbol.Convolution()接口相同的data、num_filter、kernel和name之外,还需要直接指定weight和bias。weight和bias就是卷积层的参数值,为了简单起见,这里将weight初始化成值全为1的4维变量,bias初始化成值全为0的1维变量,这样就能得到最后的卷积结果。具体代码如下:

conv1 = mx.nd.Convolution(data=data, weight=mx.nd.ones((10,1,3,3)),

                          bias=mx.nd.zeros((10)), num_filter=10, kernel=(3,3),

                          name='conv1')

print(conv1)

输出结果如下:

[[[[ 72.  81.  90.  99. 108.]

   [135. 144. 153. 162. 171.]]

  [[ 72.  81.  90.  99. 108.]

   [135. 144. 153. 162. 171.]

  [[ 72.  81.  90.  99. 108.]

   [135. 144. 153. 162. 171.]]

  [[ 72.  81.  90.  99. 108.]

   [135. 144. 153. 162. 171.]]

  [[ 72.  81.  90.  99. 108.]

   [135. 144. 153. 162. 171.]]

  [[ 72.  81.  90.  99. 108.]

   [135. 144. 153. 162. 171.]]

  [[ 72.  81.  90.  99. 108.]

   [135. 144. 153. 162. 171.]]

  [[ 72.  81.  90.  99. 108.]

   [135. 144. 153. 162. 171.]]

  [[ 72.  81.  90.  99. 108.]

   [135. 144. 153. 162. 171.]]

  [[ 72.  81.  90.  99. 108.]

   [135. 144. 153. 162. 171.]]]]

<NDArray 1x10x2x5 @cpu(0)>

总体来看,Symbol和NDArray有很多相似的地方,同时,二者在MXNet中都扮演着重要的角色。采用命令式编程的NDArray其特点是直观,常用来实现底层的计算;采用符号式编程的Symbol其特点是高效,主要用来定义计算图。


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。