使用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的好處:

  1. 簡單直觀
  2. 方便編程,可以整合在一些控制流中(for loop, if-else condition,...)以及一些庫相互調用(numpy)。
  3. 方便按步調試

使用Symbol的好處:

  1. 提供了豐富的運算
  2. 方便對計算圖進行保存和可視化
  3. 通過編譯模塊可以優化,並行計算與節約存儲空間

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.Convolutionmx.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})

參考資料

  1. Symbol - Neural network graphs and auto-differentiation
  2. Deep Learning Programming Style
  3. Symbol in Picture


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM