MXNet官方教程3Symbol -神经网络图和自动区分
Posted xiang_freedom
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MXNet官方教程3Symbol -神经网络图和自动区分相关的知识,希望对你有一定的参考价值。
在上一篇教程里,我们介绍了NDArray
,MXNet的基本数据操作结构。仅通过NDArray,我们可以计算大部分数学运算。实际上,仅通过NDArray,我们可以定义且训练一个完整的神经网络。NDArray可以在任何前端语言上执行高效的指令式科学计算,所以你可能会疑惑:为什么我们不直接使用NDArray呢?
MXNet提供了Symbol API,一个用于符号编程的接口。所谓符号编程,相比于传统的一步步计算,我们首先会定义一个计算图。计算图中包含规定的输入和输出的占位符。然后编译它,并与NDArray绑定起来进行计算。MXNet的Symbol API类似于Caffe的网络配置和Theano的符号编程。
符号编程的另一个优点是我们可以在使用它之前就优化。例如,当以指令式编程计算一个表达式时,我们不知道下一步需要什么数据。但是在符号编程里,我们已经预先定义好了需要的输出。这意味着在一边计算的时候,可以一边将中间步骤的内存回收掉。并且相同的网络下,Symbol API占用更少的内存。参见How To以及Architecture 部分。
在我们的设计笔记里,对指令式编程和符号式编程进行了更深入的比较。但在这篇教程里,我们只关注MXNet Symbol API的使用。在MXNet里,我们可以通过简单的矩阵运算符或者复杂的神经网络层,将一个Symbol转为另一个Symbol。运算符支持多维输入以及多维输出,还能保存中间状态的symbol。
为了对这些概念有一个可视化的理解,参看 Symbolic Configuration and Execution in Pictures。
更具体一点,我们来手把手操作Symbol API。Symbol的构成有不同的方式。
先决条件
我们需要:
- MXNet
- Jupyter
pip install jupyter
- GPUs:如果没有GPU,把gpu_device设置为mx.cpu()。
Symbol基本组成
基础运算符
这个例子创建了一个简单的表达式:a+b
。首先,使用mx.sym.Variable
创建两个占位符,并命名为a
和b
。然后,用+
运算符得到期望的Symbol。我们并不一定需要为变量命名,MXNet会自动给它生成一个独一无二的名字。比如例子中的c
。
import mxnet as mx
a = mx.sym.Variable('a')
b = mx.sym.Variable('b')
c = a + b
(a, b, c)
(<Symbol a>, <Symbol b>, <Symbol _plus0>)
大部分支持NDArray的运算符也支持Symbol,比如
# elemental wise multiplication
d = a * b
# matrix multiplication
e = mx.sym.dot(a, b)
# reshape
f = mx.sym.reshape(d+e, shape=(1,4))
# broadcast
g = mx.sym.broadcast_to(f, shape=(2,4))
# plot
mx.viz.plot_network(symbol=g)
上面的计算可以使用bind
方法绑定输入数据来执行运算。我们会在Symbol操作的部分详细介绍。
基础神经网络
Symbol支持大量的神经网络层,这个例子构造了一个2层全连接网络,在给定输入数据格式后将其可视化。
net = mx.sym.Variable('data')
net = mx.sym.FullyConnected(data=net, name='fc1', num_hidden=128)
net = mx.sym.Activation(data=net, name='relu1', act_type="relu")
net = mx.sym.FullyConnected(data=net, name='fc2', num_hidden=10)
net = mx.sym.SoftmaxOutput(data=net, name='out')
mx.viz.plot_network(net, shape='data':(100,200))
每一个symbol有一个唯一的名字。NDArray和Symbol都表示一个单独的张量。运算符表示张量之间的计算。运算符把Symbol(或NDArray)作为输入,并且接受其他超参数比如隐藏神经元个数或者激活函数,最后产生输出。
我们可以把Symbol视为带有参数的函数,通过下面的方法获得这些参数:
net.list_arguments()
['data', 'fc1_weight', 'fc1_bias', 'fc2_weight', 'fc2_bias', 'out_label']
这些参数有:
- data:变量data表示的输入数据
- fc1_weight和fc1_bias:第1个全连接网络层的权重和偏置
- fc2_weight和fc2_bias:第2个全连接网络层的权重和偏置
- out_label:变量loss表示的标签
我们也可以单独给每个变量命名:
net = mx.symbol.Variable('data')
w = mx.symbol.Variable('myweight')
net = mx.symbol.FullyConnected(data=net, weight=w, name='fc1', num_hidden=128)
net.list_arguments()
['data', 'myweight', 'fc1_bias']
上面的例子中,FullyConnected
层包括3个输入:data,weight,bias。当任何一个输入没有被指定时,都会得到一个自动生成的变量。
更复杂的组成
MXNet提供优化好的深度学习中常用的网络Symbol。我们还可以用python自定义运算符。下面的例子先把两个symbol按元素相加,然后放入全连接网络运算符。
lhs = mx.symbol.Variable('data1')
rhs = mx.symbol.Variable('data2')
net = mx.symbol.FullyConnected(data=lhs + rhs, name='fc1', num_hidden=128)
net.list_arguments()
['data1', 'data2', 'fc1_weight', 'fc1_bias']
对比上面提到的单向组合,我们可以使用更灵活的方式生成Symbol。
data = mx.symbol.Variable('data')
net1 = mx.symbol.FullyConnected(data=data, name='fc1', num_hidden=10)
net1.list_arguments()
net2 = mx.symbol.Variable('data2')
net2 = mx.symbol.FullyConnected(data=net2, name='fc2', num_hidden=10)
composed = net2(data2=net1, name='composed')
composed.list_arguments()
['data', 'fc1_weight', 'fc1_bias', 'fc2_weight', 'fc2_bias']
这个例子中,net2
作为一个函数,接收net1
作为输入,得到一个组合 Symbol,这个Symbol包含net1和net2的所有属性。
当你准备构建比较大的网络时,你可能需要用一个统一的前缀命名symbols,来更好的描述网络结构。你可以使用前缀命名管理器:
data = mx.sym.Variable("data")
net = data
n_layer = 2
for i in range(n_layer):
with mx.name.Prefix("layer%d_" % (i + 1)):
net = mx.sym.FullyConnected(data=net, name="fc", num_hidden=100)
net.list_arguments()
['data',
'layer1_fc_weight',
'layer1_fc_bias',
'layer2_fc_weight',
'layer2_fc_bias']
深度神经网络的模块化构造
一层一层地构建一个深度神经网络(比如google inception)是很乏味的,因为网络层次太多了。所以,我们通常模块化这些构造方法。
比如,在Google Inception网络里,我们先定义一个工厂方法,把卷积核,批标准化和线性整流函数(Relu)连在一起。
def ConvFactory(data, num_filter, kernel, stride=(1,1), pad=(0, 0),name=None, suffix=''):
conv = mx.sym.Convolution(data=data, num_filter=num_filter, kernel=kernel,
stride=stride, pad=pad, name='conv_%s%s' %(name, suffix))
bn = mx.sym.BatchNorm(data=conv, name='bn_%s%s' %(name, suffix))
act = mx.sym.Activation(data=bn, act_type='relu', name='relu_%s%s'
%(name, suffix))
return act
prev = mx.sym.Variable(name="Previous Output")
conv_comp = ConvFactory(data=prev, num_filter=64, kernel=(7,7), stride=(2, 2))
shape = "Previous Output" : (128, 3, 28, 28)
mx.viz.plot_network(symbol=conv_comp, shape=shape)
然后我们再利用刚才的工厂方法定义一个inception模块:
def InceptionFactoryA(data, num_1x1, num_3x3red, num_3x3, num_d3x3red, num_d3x3,
pool, proj, name):
# 1x1
c1x1 = ConvFactory(data=data, num_filter=num_1x1, kernel=(1, 1), name=('%s_1x1' % name))
# 3x3 reduce + 3x3
c3x3r = ConvFactory(data=data, num_filter=num_3x3red, kernel=(1, 1), name=('%s_3x3' % name), suffix='_reduce')
c3x3 = ConvFactory(data=c3x3r, num_filter=num_3x3, kernel=(3, 3), pad=(1, 1), name=('%s_3x3' % name))
# double 3x3 reduce + double 3x3
cd3x3r = ConvFactory(data=data, num_filter=num_d3x3red, kernel=(1, 1), name=('%s_double_3x3' % name), suffix='_reduce')
cd3x3 = ConvFactory(data=cd3x3r, num_filter=num_d3x3, kernel=(3, 3), pad=(1, 1), name=('%s_double_3x3_0' % name))
cd3x3 = ConvFactory(data=cd3x3, num_filter=num_d3x3, kernel=(3, 3), pad=(1, 1), name=('%s_double_3x3_1' % name))
# pool + proj
pooling = mx.sym.Pooling(data=data, kernel=(3, 3), stride=(1, 1), pad=(1, 1), pool_type=pool, name=('%s_pool_%s_pool' % (pool, name)))
cproj = ConvFactory(data=pooling, num_filter=proj, kernel=(1, 1), name=('%s_proj' % name))
# concat
concat = mx.sym.Concat(*[c1x1, c3x3, cd3x3, cproj], name='ch_concat_%s_chconcat' % name)
return concat
prev = mx.sym.Variable(name="Previous Output")
in3a = InceptionFactoryA(prev, 64, 64, 64, 64, 96, "avg", 32, name="in3a")
mx.viz.plot_network(symbol=in3a, shape=shape)
最后,我们组合不同的inception模块构成整个网络。完整代码在这里。
聚合多个Symbol
为了构造含有多个损失层的神经网络,我们可以使用mxnet.sym.Group
把多个Symbol聚合在一起,下面的例子聚合两个输出:
net = mx.sym.Variable('data')
fc1 = mx.sym.FullyConnected(data=net, name='fc1', num_hidden=128)
net = mx.sym.Activation(data=fc1, name='relu1', act_type="relu")
out1 = mx.sym.SoftmaxOutput(data=net, name='softmax')
out2 = mx.sym.LinearRegressionOutput(data=net, name='regression')
group = mx.sym.Group([out1, out2])
group.list_outputs()
['softmax_output', 'regression_output']
和NDArray的关系
就如你所见,Symbol和NDArray都提供了多维数组运算,比如MXNet里c = a + b
。我们在这里简单陈述一下区别。
NDArray提供的是类似于指令式编程的接口,计算是一步一步执行的。而Symbol更接近宣告式编程,是先定义计算,然后再提供数据执行。宣告式编程的例子有正则表达式和SQL。
NDArray的优点:
- 直观
- 在原生语言特性(循环,条件选择等)和第三方库(numpy等)上容易执行
- 容易debug
Symbol的优点:
- 提供几乎所有NDArray支持的操作,比如
+
,*
,sin
,reshape
等。 - 容易保存、加载和可视化
- 后端可以很容易的优化计算和内存使用
Symbol操作
Symbol和NDArray一个重要的区别是是要先定义计算图然后绑定数据最后运算。
这部分,我们介绍直接操作Symbol的方法。注意的是,大部分方法都位于module包。
shape和type推断
我们可以查询每一个symbol的参数、辅助状态和输出。在给定输入的shape或type时,我们还可以推断输出的shape或type,有利于内存分配。
arg_name = c.list_arguments() # get the names of the inputs
out_name = c.list_outputs() # get the names of the outputs
# infers output shape given the shape of input arguments
arg_shape, out_shape, _ = c.infer_shape(a=(2,3), b=(2,3))
# infers output type given the type of input arguments
arg_type, out_type, _ = c.infer_type(a='float32', b='float32')
'input' : dict(zip(arg_name, arg_shape)),
'output' : dict(zip(out_name, out_shape))
'input' : dict(zip(arg_name, arg_type)),
'output' : dict(zip(out_name, out_type))
'input': 'a': (2, 3), 'b': (2, 3), 'output': '_plus0_output': (2, 3)
'input': 'a': numpy.float32, 'b': numpy.float32,
'output': '_plus0_output': numpy.float32
绑定数据并执行
上面的Symbol c
定义好了计算图,为了执行计算,我们首先需要绑定数据。
我们可以使用bind
方法,接收一个硬件环境和一个由变量名和NDArray数据组成的字典,返回一个执行器。这个执行器有一个forward
方法执行计算,通过outputs
属性得到结果。
ex = c.bind(ctx=mx.cpu(), args='a' : mx.nd.ones([2,3]),
'b' : mx.nd.ones([2,3]))
ex.forward()
print('number of outputs = %d\\nthe first output = \\n%s' % (
len(ex.outputs), ex.outputs[0].asnumpy()))
number of outputs = 1
the first output =
[[ 2. 2. 2.]
[ 2. 2. 2.]]
当然也可以在gpu上执行。
注意:如果没有gpu,把gpu_device设为mx.cpu()。
gpu_device=mx.gpu() # Change this to mx.cpu() in absence of GPUs.
ex_gpu = c.bind(ctx=gpu_device, args='a' : mx.nd.ones([3,4], gpu_device)*2,
'b' : mx.nd.ones([3,4], gpu_device)*3)
ex_gpu.forward()
ex_gpu.outputs[0].asnumpy()
array([[ 5., 5., 5., 5.],
[ 5., 5., 5., 5.],
[ 5., 5., 5., 5.]], dtype=float32)
我们也可以用eval
方法来计算symbol,相当于同时调用bind和forward方法。
ex = c.eval(ctx = mx.cpu(), a = mx.nd.ones([2,3]), b = mx.nd.ones([2,3]))
print('number of outputs = %d\\nthe first output = \\n%s' % (
len(ex), ex[0].asnumpy()))
number of outputs = 1
the first output =
[[ 2. 2. 2.]
[ 2. 2. 2.]]
对于神经网络,更常见的方式是simple_bind
,它会创建所有的参数列表。然后可以调用forward
和backward
(如果需要计算梯度的话)。
加载和保存
本质上symbol和ndarray是一样的,它们都是张量,也都是运算符的输入/输出。我们可以用pickle
序列化一个Symbol,还可以像之前的NDArray教程里说的直接调用save
和load
。
当序列化NDArray时,我们序列化里面的张量数据并且直接以二进制格式存储在硬盘上。但是symbol用的是图的概念,图由一系列运算符组成,且隐式地由输出symbol表示。所以,序列化symbol的时候,我们序列化输出Symbol。symbol还需要可读的json格式来序列化。可以用tojson
方法转换symbol为json字符串。
print(c.tojson())
c.save('symbol-c.json')
c2 = mx.sym.load('symbol-c.json')
c.tojson() == c2.tojson()
"nodes": [
"op": "null",
"name": "a",
"inputs": []
,
"op": "null",
"name": "b",
"inputs": []
,
"op": "elemwise_add",
"name": "_plus0",
"inputs": [[0, 0, 0], [1, 0, 0]]
],
"arg_nodes": [0, 1],
"node_row_ptr": [0, 1, 2, 3],
"heads": [[2, 0, 0]],
"attrs": "mxnet_version": ["int", 1100]
True
自定义Symbol
大多数运算符比如mx.sym.Convolution
和mx.sym.Reshape
都是用C++实现的,但MXNet支持用任何前端语言比如python来实现自定义运算符。使用自定义symbol通常可以更容易的开发和debug。详见【MXNet常见问题1】怎么创建新运算符(网络层)。
高级用法
类型转换
MXNet默认使用32位浮点数,为了更高准确率,我们可以使用更低精度的数据类型。比如,Nvidia Tesla Pascal GPUs (e.g. P100)在16位浮点数上有更好的表现,GTX Pascal GPUs (e.g. GTX 1080)在8位整数上更快。
我们用mx.sym.cast
来转换数据类型。
a = mx.sym.Variable('data')
b = mx.sym.cast(data=a, dtype='float16')
arg, out, _ = b.infer_type(data='float32')
print('input':arg, 'output':out)
c = mx.sym.cast(data=a, dtype='uint8')
arg, out, _ = c.infer_type(data='int32')
print('input':arg, 'output':out)
'output': [<class 'numpy.float16'>], 'input': [<class 'numpy.float32'>]
'output': [<class 'numpy.uint8'>], 'input': [<class 'numpy.int32'>]
变量共享
为了在不同symbol间共享数据,我们可以把这些symbol绑定同样的数据。
a = mx.sym.Variable('a')
b = mx.sym.Variable('b')
b = a + a * a
data = mx.nd.ones((2,3))*2
ex = b.bind(ctx=mx.cpu(), args='a':data, 'b':data)
ex.forward()
ex.outputs[0].asnumpy()
array([[ 6., 6., 6.],
[ 6., 6., 6.]], dtype=float32)
原文地址:Symbol - Neural network graphs and auto-differentiation
以上是关于MXNet官方教程3Symbol -神经网络图和自动区分的主要内容,如果未能解决你的问题,请参考以下文章