在以前的文章,咱们介绍了NDArray模块,它是MXNet中处理数据的核心模块,咱们可使用NDArray完成很是丰富的数学运算。实际上,咱们彻底可使用NDArray来定义神经网络,这种方式咱们称它为命令式的编程风格,它的优势是编写简单直接,方便调试。像下面咱们就定义了一个两层的神经网络,它包含了一个全链接层,和一个relu的激活层。html
import mxnet as mx import mxnet.ndarray as nd def net(X, w, b): z = nd.FullyConnected(data=X, weight=w, bias=b, num_hidden=128) out = nd.Activation(data=z, act_type='relu') return out
既然如此,咱们为何不用NDArray来完成全部事情呢?咱们想像一下,若是咱们要将咱们上面定义的模型保存下来,使用C++ API来实际运行呢,没有很是直接的方法,咱们只能根据Python的代码结构来找到对应的c++ api的定义。python
MXNet提供了Sybmol API,主要用于符号编程。符号编程不像是命令式编程语句一条一条的执行,咱们会首先定义一个计算图来描述整个计算过程,整个计算的输入、输出以及中间结果都是先经过占位符来表示,咱们能够编译计算图来生成一个实际的函数,生成的函数能够直接对NDArray进行计算。这样看来,MXNet的Sybmol API有点像Caffe中使用的protobuf格式的网络配置文件,因此咱们也很容易将使用Symbol API定义的网络模型保存到磁盘,再经过其余语言的api来读取,方便部署。c++
符号编程另一个优点是,咱们能够对整个计算图描述的计算过程进行优化,由于在编译计算图的时候,整个计算过程都已经定义完成,咱们更加了解每一个计算步骤之间的依赖关系,以及一些中间变量的生命周期,这方便咱们对操做进行并行化,对一些中间变量使用原地存储来节省内存。apache
使用NDArray的好处:编程
使用Symbol的好处:json
在MXNet的Sybmol API中,咱们能够经过operators
把Symobls和Symbols组成在一块儿,造成计算图。这些计算图能够是很简单的算术运算,也能够造成一个神经网络。每一种operator
都接收若干的输入的变量,而后输出一些变量,这些变量咱们都用符号来表示。api
下面咱们代码演示了若是定义一个最简单的加法的计算图:网络
a = mx.sym.var('a') b = mx.sym.var('b') c = a + b (a, b, c)
mx.viz.plot_network(c)
从上面咱们能够看到,使用mx.sym.var
来定义一个符号,同时须要指定符号的名称。可是在第三条语句中,咱们使用了+
这个operator
来链接符号a
和符号b
,它的输出为符号c
,符号c并无显式的指定一个名称,它的名称是自动生成且唯一的。从输出中,咱们能够看出是_plus0
ide
上面咱们使用了+
操做符,Sybmol模块定义了丰富的操做符,NDArray支持的运算在Symbol中基本都支持:模块化
d = a * b e = mx.sym.dot(a, b) f = mx.sym.reshape(d + e, shape=(1,4)) g = mx.sym.broadcast_to(f, shape=(2,4)) mx.viz.plot_network(g)
除了上面介绍的那些基本的操做运算(*
,+
,reshape
)外,Symbol还提供了丰富的神经网络的层的操做,下面的例子显示了,使用Symbol模块的一些高级的operator来构建一个高层的神经网络。
net = mx.sym.var('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.Activation(data=net, name='relu2', act_type='relu') net = mx.sym.SoftmaxOutput(data=net,name='out') mx.viz.plot_network(net, shape={'data':(28,28)})
像mx.sym.FullyConnected
这样的operator接收符号变量做为输入,同时这个操做自己内部是带有参数的,咱们经过接口的一些参数来指定。最后的net咱们也能够当作是接入了一组参数的一个函数,这个函数须要参数咱们能够用下面的方法列出来:
net.list_arguments()
针对深度学习中一些常见的层,MXNet在Symbol模块中都直接作好了优化封装。同时针对于各类不一样的须要,咱们也能够用Python来定义咱们新的operator。
除了像上面那边一层一层向前的组装咱们的Sybmol外,咱们还能够对多个复杂的Symbol进行组合,造成结构更加复杂的Symbol。
data = mx.sym.var('data') net1 = mx.sym.FullyConnected(data=data, name='fc1', num_hidden=10) net2 = mx.sym.var('data') net2 = mx.sym.FullyConnected(data=net2, name='fc2', num_hidden=10) # net2就像一个函数同样,接收Symbol net1做为输入 composed = net2(data=net1, name='composed') mx.viz.plot_network(composed)
当咱们要构建一个更大的network时,一般一些Symbol咱们但愿有一个共同的命名前缀。那么咱们就可使用MXNet的Prefix NameManager来处理:
data = mx.sym.var('data') net = mx.sym.FullyConnected(data=data, name='fc1', num_hidden=10) net = mx.sym.FullyConnected(data=net, name='fc2', num_hidden=10) net.list_arguments()
data = mx.sym.var('data') with mx.name.Prefix('layer1'): net = mx.sym.FullyConnected(data=data, name='fc1', num_hidden=10) net = mx.sym.FullyConnected(data=net, name='fc2', num_hidden=10) net.list_arguments()
当咱们在构建大的神经网络结构的时候,好比Google Inception Network,它的层不少,若是咱们一层一层的构建那将是一个很是烦索的工做,可是实际上这些网络结构是由很是多结构相似的小网络组合而成的,咱们能够模块化的来构建。
在Google Inception network中,其中有一个很是基本的结构就是卷积
->BatchNorm
->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_{}{}'.format(name, suffix)) bn = mx.sym.BatchNorm(data=conv, name='bn_{}{}'.format(name, suffix)) act = mx.sym.Activation(data=bn, act_type='relu', name='relu_{}{}'.format(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)
接下来咱们就能够用ConvFactory来构建一个inception module了,它是Google 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)
上面示例中全部构建的Symbol都是串行向下,有一个输入,一个输出的。但在神经网络中,尤为是设计loss的时候,咱们须要将多个loss layer做为输出,这时咱们可使用Symbol模块提供的Group功能,将多个输出组合起来。
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]) print(group.list_outputs()) mx.viz.plot_network(symbol=group)
Symbol只是咱们定义好的一个计算图,它自己内部并无操做任何实际的数据。但咱们也能够从这个计算图获取至关多的信息,好比这个网络的输入输出,参数,状态,以及输出的形状和数据类型等。
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))}
若是要使得咱们以前定义的计算图可以完成计算的功能,咱们必须给计算图喂入对应的数据,也就是Symbol的全部自由变量。咱们可使用bind
方法,它接收一个contxt参数和一个dict参数,dict的元素都是变量名及对应的NDArry组成的一个pair。
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()))
咱们同时可使用GPU数据进行绑定:
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()
对于神经网络来讲,一个更加经常使用的模式就是使用simple_bind
在咱们序列化一个NDArray
对象时,咱们序列化的是面的的tensor数据,咱们直接把这些数据以二进制的格式保存到磁盘。可是Symbol是一个计算图,它包含了一连串的操做,咱们只是使用最终的输出来表示整个计算图。当咱们序列化一个计算图时,咱们也是对它的输入Sybmol进行序列化,咱们保存为json
格式,方向阅读与修改。
print(group.tojson()) group.save('symbol-group.json') group2 = mx.sym.load('symbol-group.json') group.tojson() == group2.tojson()
在MXNet中,为了更好的性能,大部分的operators
都是用C++实现的,好比mx.sym.Convolution
和mx.sym.Reshape
。MXNet同时容许用户用Python本身写了一些新的operator,这部分的内容能够参考:How to create new operator
MXNet在默认的状况下使用float32
做为全部operator的操做类型。可是为了最大化程序的运行性能,咱们可使用低精度的数据类型。好比:在Nvidia TeslaPascal(P100)上可使用FP16
,在GTX Pascal GPUS(GTX 1080)上可使用INT8
。
咱们可使用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})