【MXNet】MXNet学习笔记

MXNet中经常会用到三个接口:NDArray、Symbol和Module
NDarray采用的是命令式编程的方式,可以简单地认为是运行在GPU上的Numpy Array。
Symbol是MXNet中用于构建网络层的模块,采用的是符号式编程的方式(是MXNet实现快速训练和节省显存的关键模块)。

符号式编程需要先用Symbol接口定义好计算图,这个计算图同时包含定义好的输入和输出格式,然后将准备好的数据输入该计算图完成计算。
命令式编程中,计算过程可以逐步来步实现。
完全可以仅仅通过NDArray来定义和使用网络,那么为什么还要提供Symbol呢?主要是为了提高效率。在定义好计算图之后,就可以对整个计算图的显存占用做优化处理,这样就能大大降低训练模型时候的显存占用。

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

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')
  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()来命名。这样就得到了一个完整的网络结构了。

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

在MXNet中,list_arguments()方法可用于查看一个Symbol对象的参数,命令如下:

print(sym.list_arguments())

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

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

在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)

[(1, 3, 10, 10), (128, 3, 3, 3), (128,), (128,), (128,), (2, 3200), (2,), (1,)] #表示网络层参数的维度,与前面list_arguments()方法列出来的层参数名一一对应
[(1, 2)] 
#表示网络输出的维度,因为网络的最后一层是输出节点为2的全连接层,且输入数据的批次维度是1,所以输出维度是[(1, 2)]
[(128,), (128,)]
#表示辅助参数的维度,目前常见的主要是BN层的参数维度。

如果要截取通过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模块实现这些操作则要简洁和直观得多,通过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)1,3,3))

# 输出结果如下:
[[[[ 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其特点是高效,主要用来定义计算图。

Module
在MXNet框架中,Module是一个高级的封装模块,可用来执行通过Symbol模块定义的网络模型的训练。Module接口提供了许多非常方便的方法用于模型训练,只需要将准备好的数据、超参数等传给对应的方法就能启动训练。

之前我们用Symbol接口定义了一个网络结构sym,接下来我们将基于这个网络结构介绍Module模块,首先来看看如何通过Module模块执行模型的预测操作。

  1. 通过mxnet.module.Module()接口初始化一个Module对象,在初始化时需要传入定义好的网络结构sym并指定运行环境,这里设置为GPU环境。
  2. 然后执行Module对象的bind操作,这个bind操作与Symbol模块中的bind操作类似,目的也是将网络结构添加到执行器,使得定义的静态图能够真正运行起来,因为这个过程涉及显存分配,因此需要提供输入数据和标签的维度信息才能执行bind操作,读者可以在命令行通过“$ watch nvidia-smi”命令查看执行bind前后,显存的变化情况。bind操作中还存在一个重要的参数是for_training,这个参数默认是True,表示接下来要进行的是训练过程,因为我们这里只需要进行网络的前向计算操作,因此将该参数设置为False。
  3. 最后调用Module对象的init_params()方法初始化网络结构的参数,初始化的方式是可以选择的,这里采用默认方式。

至此,一个可用的网络结构执行器就初始化完成了。初始化网络结构执行器的代码具体如下:

mod = mx.mod.Module(symbol=sym, context=mx.gpu(0))
mod.bind(data_shapes=[('data',(8,3,28,28))],
         label_shapes=[('softmax_label',(8,))],
         for_training=False)
mod.init_params()

接下来随机初始化一个4维的输入数据,该数据的维度需要与初始化Module对象时设定的数据维度相同,然后通过mxnet.io.DataBatch()接口封装成一个批次数据,之后就可以作为Module对象的forward()方法的输入了,执行完前向计算后,调用Module对象的get_outputs()方法就能得到模型的输出结果,具体代码如下:

data = mx.nd.random.uniform(0,1,shape=(8,3,28,28))
mod.forward(mx.io.DataBatch([data]))
print(mod.get_outputs()[0])

# 输出结果如下,因为输入数据的批次大小是8,网络的全连接层输出节点数是2,因此输出的维度是8*2:
[[ 0.50080067  0.4991993 ] 
 [ 0.50148612  0.49851385]
 [ 0.50148612  0.49851385]
 [ 0.50103837  0.4989616 ]
 [ 0.50171131  0.49828872]
 [ 0.50254387  0.4974561 ]
 [ 0.50104254  0.49895743]
 [ 0.50223148  0.49776852]
 [ 0.49780959  0.50219035]]
<NDArray 8x2 @gpu(0)>

接下来介绍如何通过Module模块执行模型的训练操作:

  1. 用mxnet.module.Module()接口初始化得到一个Module对象,这一步至少要输入一个Symbol对象,另外这一步还可以指定训练环境是CPU还是GPU,这里采用GPU。
  2. 调用Module对象的bind()方法将准备好的数据和网络结构连接到执行器构成一个完整的计算图。
  3. 调用Module对象的init_params()方法初始化网络的参数,因为前面定义的网络结构只是一个架子,里面没有参数,因此需要执行参数初始化。
  4. 调用Module对象的init_optimizer()方法初始化优化器,默认采用随机梯度下降法(stochastic gradient descent,SGD)进行优化。
  5. 调用mxnet.metric.create()接口创建评价函数,这里采用的是准确率(accuracy)。
  6. 执行5次循环训练,每次循环都会将所有数据过一遍模型,因此在循环开始处需要执行评价函数的重置操作、数据的初始读取等操作。
  7. 此处的while循环只有在读取完训练数据之后才会退出,该循环首先会调用Module对象的forward()方法执行模型的前向计算,这一步就是输入数据通过每一个网络层的参数进行计算并得到最后结果。
  8. 调用Module对象的backward()方法执行模型的反向传播计算,这一步将涉及损失函数的计算和梯度的回传。
  9. 调用Module对象的update()方法执行参数更新操作,参数更新的依据就是第9步计算得到的梯度,这样就完成了一个批次(batch)数据对网络参数的更新。
  10. 调用Module对象的update_metric()方法更新评价函数的计算结果。
  11. 读取下一个批次的数据,这里采用了Python中的try和except语句,表示如果try包含的语句执行出错,则执行except包含的语句,这里用来标识是否读取到了数据集的最后一个批次。
  12. 调用评价对象的get_name_value()方法并打印此次计算的结果。
  13. 调用Module对象的get_params()方法读取网络参数,并利用这些参数初始化Module对象。
  14. 调用数据对象的reset()方法进行重置,这样在下一次循环中就可以从数据的最初始位置开始读取了。
import mxnet as mx
import logging

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')

data = mx.nd.random.uniform(0,1,shape=(1000,3,224,224))
label = mx.nd.round(mx.nd.random.uniform(0,1,shape=(1000)))
train_data = mx.io.NDArrayIter(data={'data':data},
                               label={'softmax_label':label},
                               batch_size=8,
                               shuffle=True)
print(train_data.provide_data)
print(train_data.provide_label)

mod = mx.mod.Module(symbol=sym,context=mx.gpu(0))
mod.bind(data_shapes=train_data.provide_data,
         label_shapes=train_data.provide_label)
mod.init_params()
mod.init_optimizer()

eval_metric = mx.metric.create('acc')

for epoch in range(5):
    end_of_batch = False
    eval_metric.reset()
    data_iter = iter(train_data)
    next_data_batch = next(data_iter)

    while not end_of_batch:
        data_batch = next_data_batch
        mod.forward(data_batch)
        mod.backward()
        mod.update()
        mod.update_metric(eval_metric, labels=data_batch.label)

        try:
            next_data_batch = next(data_iter)
            mod.prepare(next_data_batch)
        except StopIteration:
            end_of_batch = True

    eval_name_vals = eval_metric.get_name_value()
    print("Epoch:{} Train_Acc:{:.4f}".format(epoch, eval_name_vals[0][1]))
    arg_params, aux_params = mod.get_params()
    mod.set_params(arg_params, aux_params)
    train_data.reset()

上面的代码其实从mod.bind()方法这一行到最后都可以用Module模块中的fit()方法来实现。fit()方法不仅封装了上述的bind操作、参数初始化、优化器初始化、模型的前向计算、反向传播、参数更新和计算评价指标等操作,还提供了保存训练结果等其他操作,因此fit()方法将是今后使用MXNet训练模型时经常调用的方法。

下面这段代码就演示了fit()方法的调用,前面两行设置命令行打印训练信息,这三行代码可以直接替换上述代码中从mod.bind()那一行到最后的所有代码:

logger = logging.getLogger()
logger.setLevel(logging.INFO)
mod.fit(train_data=train_data, num_epoch=5)

最终简化版的代码为:

import mxnet as mx
import logging

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')

data = mx.nd.random.uniform(0,1,shape=(1000,3,224,224))
label = mx.nd.round(mx.nd.random.uniform(0,1,shape=(1000)))
train_data = mx.io.NDArrayIter(data={'data':data},
                               label={'softmax_label':label},
                               batch_size=8,
                               shuffle=True)

print(train_data.provide_data)
print(train_data.provide_label)
mod = mx.mod.Module(symbol=sym,context=mx.gpu(0))

logger = logging.getLogger()
logger.setLevel(logging.INFO)
mod.fit(train_data=train_data, num_epoch=5)

得益于MXNet的静态图设计和对计算过程的优化,MXNet的训练速度相较于大部分深度学习框架要快,而且显存占用非常少。这使得能够在单卡或单机多卡上使用更大的batch size训练相同的模型,这对于复杂模型的训练非常有利,有时候甚至还会影响训练结果。

总结
Symbol是MXNet框架中定义网络结构层的接口,采取符号式编程的Symbol通过构建静态计算图可以大大提高模型训练的效率。Symbol中提供了多种方法用于查看Symbol对象的信息,包括参数层、参数维度等,同时也便于用户在设计网络结构的过程中查漏补缺。

此外,Symbol中的大部分网络层接口在NDArray中都有对应的实现,因此可以通过NDArray中对应名称的网络层查看具体的计算过程。

Module是MXNet框架中封装了训练模型所需的大部分操作的高级接口,用户可以通过Module模块执行bind操作、参数初始化、优化器初始化、模型的前向计算、损失函数的反向传播、网络参数更新、评价指标计算等,同时,Module模块还将常用的训练操作封装在了fit()方法中,通过该方法,用户可以更加方便地训练模型,可以说是既灵活又简便。

参数:
prefix(str)-模型名的前缀(可以是个文件夹)
epoch(int)-模型的epoch的数量(epoch在机器学习里面指的是把所有的样本进行一次全部操作(前向传播,反向传播等等),和普通的迭代相比,epoch的尺度比较大)
symbol(Symbol)-输入的symbol(符号)。
arg_params(一个NDArray的字符字典)-模型参数,以及网络权重字典。
aux_params(一个NDArray的字符字典)-模型参数,以及一些附加状态(auxiliary states)的字典。

# elemental wise multiplication
d = a * b 
# matrix multiplication
e = mx.sym.dot(a, b)

参考链接:

  1. 深度学习高能干货:手把手教你搭建MXNet框架
  2. MXNet的Model API
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值