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')
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))
# 输出结果如下
接下来,调用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))
# 输出结果如下:
这个执行器就是一个完整的计算图了,因此可以调用执行器的forward()方法进行计算以得到结果:
output=e.forward()
print(output[0])
#输出结果如下:
[ 10. 21. 36.]
相比之下,通过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.]
虽然使用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.]]]]
然后,通过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.]]]]
总体来看,Symbol和NDArray有很多相似的地方,同时,二者在MXNet中都扮演着重要的角色。采用命令式编程的NDArray其特点是直观,常用来实现底层的计算;采用符号式编程的Symbol其特点是高效,主要用来定义计算图。
Module
在MXNet框架中,Module是一个高级的封装模块,可用来执行通过Symbol模块定义的网络模型的训练。Module接口提供了许多非常方便的方法用于模型训练,只需要将准备好的数据、超参数等传给对应的方法就能启动训练。
之前我们用Symbol接口定义了一个网络结构sym,接下来我们将基于这个网络结构介绍Module模块,首先来看看如何通过Module模块执行模型的预测操作。
至此,一个可用的网络结构执行器就初始化完成了。初始化网络结构执行器的代码具体如下:
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]]
接下来介绍如何通过Module模块执行模型的训练操作:
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)
参考链接: