在以前的文章,咱們介紹了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})