https://blog.csdn.net/qq_36165459/article/details/78394259
文檔英文原版參見Symbol - Neural network graphs and auto-differentiation
在前面的教程中,我們介紹了NDArray,MXNet中操作數據的基本數據結構。僅僅使用NDArray本身,我們就可以執行很多數學操作。實際上,我們也可以使用NDArray定義和更新一整個神經網絡。NDArray支持命令式編程(用於科學計算),充分利用任何前端語言的本機控制。但是我們為什么沒有就使用NDArray用於所有的計算呢?
MXNet提供了符號接口,用於符號編程的交互。它和一步步的解釋的命令式編程不同,我們首先要定義一個計算圖。這個圖包括了輸入的占位符和設計的輸出。之后編譯這個圖,產生一個可以綁定到NDArray s並運行的函數。Symbol API類似於caffe中的網絡配置或者Theano中的符號編程。
另一個優勢就是我們可以在使用之前優化我們的函數。例如,我們要執行命令式的數學計算,但是我們並不知道運行每個操作的時間,因為這個值是在之后才計算出來的。但是使用符號化編程,我們提前聲明所需的輸出。這意味着我們可以通過執行操作來回收中間步驟分配的內存。Symbol API 對於同一個網絡使用更少的內存。
在我們的設計文檔中,我們對命令式和符號式編程的優勢做了一個更深入的探討。但是在本篇文檔中,我們只是教你如何使用MXNet的Symbol API。我們可以從其他符合中組合新的符號,使用簡單的矩陣操作符(例如“+”)或者整個神經網絡層。操作符支持多個變量輸入,多個輸出,並且維持內部狀態符號。
前提條件
為了完成以下教程,我們需要:
pip install jupyter
- 1
- GPUs:教程的部分實現需要用到GPU。如果沒有GPU,只用把變量gpu_device 設置為mx.cpu()即可
基本符號組成
基本操作符
以下的例子復合了一個簡單的表達式“a+b”。我們首先使用mx.sym.Variable 創建占位符a和b及其名稱,然后用操作符“+”構造期望的符號。在新建時如果名字字符串沒有給定,MXNet會自動為符號生產一個獨一無二的名字,如c的例子所示。
import mxnet as mx a = mx.sym.Variable('a') b = mx.sym.Variable('b') c = a + b (a, b, c)
- 1
- 2
- 3
- 4
- 5
大多數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)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
上述示例中聲明的計算可以使用bind 方法綁定到輸入數據進行評估。我們將在符號布局部分討論。
基本神經網絡
除了基本的操作符,Symbol 擁有豐富的神經網絡層集。以下代碼構造了一個兩層的全連接層,然后通過給定輸入數據大小實例化該結構。
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)})
- 1
- 2
- 3
- 4
- 5
- 6
每一個符號具有唯一的字符串名稱。NDArray和Symbol都表示單個的張量。操作符表示張量之間的計算。操作符接受symbol(或者NDArray)作為輸入,也可以增加像隱藏神經元個數(num_hidden),激活類型(act_type)這樣的超參數,並產生輸出。
我們其實可以把一個symbol看做是具有多個參數的函數,可以用以下的方法遍歷這些參數:
net.list_arguments()
- 1
這些參數是每一個symbol所需要的參數和輸入:
- data:變量data需要的輸入數據
- fc1_weight & fc1_bias:第一個全連接層fc1的權重和偏置
- fc2_weight &fc2_bias:第二個全連接層fc2的權重和偏置
- out_label:損失函數需要的標簽
我們也可以明確指定名稱:
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()
- 1
- 2
- 3
- 4
在上述示例中,FullyConnected 層有三個輸入:數據,權值,偏置。任何,任何沒有指定輸入的將自動生成一個變量。
更加復雜的組成
MXNet對深度學習常用的層提供更優化的符號。我們也可以在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()
- 1
- 2
- 3
- 4
我們還可以以比上述示例中描述的單個前向組合更靈活的方式構建符號:
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()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
在此示例中,net2 作為一個函數應用於現有符號net1 上,所得到的組合符號將具有net1 和net2 的所有屬性。
一旦開始構建更大的網絡,就可能需要使用公共前綴命名一些符號來統籌網絡的結構。你可以像下面的示例一樣使用前綴名稱管理:
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()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
深度網絡的模塊化構建
對於深度網絡,例如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)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
然后我們定義一個構建基於ConvFactory 的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)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
最終我們可以通過改變多inception模型獲得整個網絡。
多符號組合
為了使用多損失層構建網絡,我們可以使用mxnet.sym.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]) group.list_outputs()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
與NDArray關系
如我們目前所見,在MXNet中,Symbol 與NDArray 都提供多維數組操作符,例如c = a + b。在此我們簡短地闡明兩者不同。NDArray 提供是類似交互的命令式編程,其中計算是逐條語句執行的。而Symbol 更接近聲明式編程,我們先聲明計算方式,之后用數據評估。此類中的示例包括正則表達式和SQL。
NDArray 的優點:
- 直截了當
- 易於使用本地語言功能(for循環,if-else條件,..)和庫(numpy,..)
- 易於分步代碼調試
Symbol 的優點:
- 提供NDArray 的幾乎所有功能,如+,*,sin,reshape等
- 易於保存,加載和可視化
- 方便后台優化計算和內存使用
符號操作
Symbol與NDArray的一大不同就是,我們首先聲明計算,然后綁定數據來運行。
在此部分,我們介紹直接操作符號的函數。但注意,它們大部分都被module 完美地包裝了起來。所以,即便跳過本節也無傷大雅。
形狀和類型接口
對於每個符號,我們可以詢問其輸入(或者參數)和輸出。我們也可以通過給定輸入大小來獲得輸出大小,這有易於存儲空間申請。
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))}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
綁定數據與評估
我們構建的符號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()))
- 1
- 2
- 3
- 4
- 5
我們在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()
- 1
- 2
- 3
- 4
- 5
- 6
也可以用eval 方法來評估符號,它結合了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()))
- 1
- 2
- 3
對於神經網絡,更常用的模式是simple_bind ,它為你提供了所有的參數數組。之后可以調用forward 和backward 來獲得梯度。
保存和載入
類似於NDArray,它們都表示一個張量,都是操作符的輸入/輸出。我們可以使用pickle 模塊序列號Symbol 或者直接使用save 和load。當序列化NDArray時,我們序列化其中的張量數據,並以二進制格式直接轉儲到磁盤。 但符號使用圖的概念。 圖通過鏈接操作符組合起來。 它們由輸出符號隱含地表示。所以,當序列化符號時,我們序列化輸出是符號的圖。雖然序列化,Symbol使用可讀性更強的json格式進行序列化。如果要將符號轉換為json 字符串,要使用tojson 方法。
print(c.tojson()) c.save('symbol-c.json') c2 = mx.sym.load('symbol-c.json') c.tojson() == c2.tojson()
- 1
- 2
- 3
- 4
自定義符號
大部分操作符例如mx.sym.Convolution 和mx.symReshape 使用C++實現可以得到更好的性能。MXNet也支持用戶用任何前端語言例如Python撰寫新的操作符。這使得開發和調試更加簡便。用Python實現請參考如何創建新的操作符
高級用法
類型轉換
MXNet默認使用32位float類型。有時我們為了更好的正確率—性能權衡,想要使用低精度的數據類型。例如,英偉達Tesla Pascal GPUs(如P100)使用16位浮點的性能提升,以及GTX Pascal GPUs(如GTX1080)使用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})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
變量共享
有時我們想要共享多個不同符號中的內容。這可以直接通過用同一個數組綁定這些符號來實現。
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()