使用Sybmol模組來構建神經網路
符號程式設計
在之前的文章,我們介紹了NDArray模組,它是MXNet中處理資料的核心模組,我們可以使用NDArray完成非常豐富的數學運算。實際上,我們完全可以使用NDArray來定義神經網路,這種方式我們稱它為命令式的程式設計風格,它的優點是編寫簡單直接,方便除錯。像下面我們就定義了一個兩層的神經網路,它包含了一個全連線層,和一個relu的啟用層。
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的定義。
MXNet提供了Sybmol API,主要用於符號程式設計。符號程式設計不像是指令式程式設計語句一條一條的執行,我們會首先定義一個計算圖來描述整個計算過程,整個計算的輸入、輸出以及中間結果都是先通過佔位符來表示,我們可以編譯計算圖來生成一個實際的函式,生成的函式可以直接對NDArray進行計算。這樣看來,MXNet的Sybmol API有點像Caffe中使用的protobuf格式的網路配置檔案,所以我們也很容易將使用Symbol API定義的網路模型儲存到磁碟,再通過其他語言的api來讀取,方便部署。
符號程式設計另外一個優勢是,我們可以對整個計算圖描述的計算過程進行優化,因為在編譯計算圖的時候,整個計算過程都已經定義完成,我們更加了解每個計算步驟之間的依賴關係,以及一些中間變數的生命週期,這方便我們對操作進行並行化,對一些中間變數使用原地儲存來節省記憶體。
使用NDArray的好處:
- 簡單直觀
- 方便程式設計,可以整合在一些控制流中(for loop, if-else condition,...)以及一些庫相互呼叫(numpy)。
- 方便按步除錯
使用Symbol的好處:
- 提供了豐富的運算
- 方便對計算圖進行儲存和視覺化
- 通過編譯模組可以優化,平行計算與節約儲存空間
Sybmol中的基本組成
在MXNet的Sybmol API中,我們可以通過operators
把Symobls和Symbols組成在一起,形成計算圖。這些計算圖可以是很簡單的算術運算,也可以形成一個神經網路。每一種operator
都接收若干的輸入的變數,然後輸出一些變數,這些變數我們都用符號來表示。
下面我們程式碼演示瞭如果定義一個最簡單的加法的計算圖:
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
上面我們使用了+
操作符,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)
更復雜的operator
除了上面介紹的那些基本的操作運算(*
,+
,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)
通過用字首管理模組來管理Symbol模組引數的名稱
當我們要構建一個更大的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組合在一起
上面示例中所有構建的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輸出shape與type的推斷
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()
自定義Symbol
在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})