MXNet官方文件教程(6):神經網路圖
符號教程
除了張量計算介面NDArray,MXNet中另一個主要的物件就是Symbol,位於mxnet.symbol(縮寫mxnet.sym)中。一個符號代表一個多輸出的符號表達式。他們由運算子複合而成,例如簡單的矩陣運算(如“+”),或者一個神經網路層(如卷積層)。一個操作符可以獲得多個輸入變數,並提供至少一個輸出變數,並擁有內部變數。一個變數可以是空的(我們可以在之後為其賦值),也可以是一個其他符號的輸出。
符號組成
基本操作符
以下的例子複合了一個簡單的表示式“a+b”。我們首先使用mx.sym.Variable建立佔位符a和b及其名稱,然後用操作符“+”構造期望的符號。在新建時如果名字字串沒有給定,
import mxnet as mx
a = mx.sym.Variable('a')
b = mx.sym.Variable('b')
c = a+ b
(a, b, c)
輸出
(<Symbol a>, <Symbol b>, <Symbol _plus0>)
大多數NDArray操作符可以被應用於Symbol,例如:
# elemental wise times
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))
mx.viz.plot_network(symbol=g)
基本神經網路
除了基本的操作符,Symbol擁有豐富的神經網路層集。以下程式碼構造了一個兩層的全連線層,然後通過給定輸入資料大小例項化該結構。
# Output may vary
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)})
深度網路的模組化構建
對於深度網路,例如Google Inception,當有大量的層時,一個一個地構建層會十分痛苦。對於這些網路,我們通常模組化其構建。以Google Inception為例,我們首先定義一個製造函式來將卷積層,批標準化層和Relu啟用層捆綁在一起:
# Output may vary
def ConvFactory(data, num_filter, kernel, stride=(1,1), pad=(0,0), name=None, suffix=''):
conv = mx.symbol.Convolution(data=data, num_filter=num_filter, kernel=kernel, stride=stride, pad=pad, name='conv_%s%s'%(name, suffix))
bn = mx.symbol.BatchNorm(data=conv, name='bn_%s%s'%(name, suffix))
act = mx.symbol.Activation(data=bn, act_type='relu', name='relu_%s%s'%(name, suffix))
return act
prev= mx.symbol.Variable(name="Previos Output")
conv_comp= ConvFactory(data=prev, num_filter=64, kernel=(7,7), stride=(2,2))
shape= {"Previos Output" : (128,3,28,28)}
mx.viz.plot_network(symbol=conv_comp, shape=shape)
然後我們定義一個構建基於ConvFactory的Inception模型的函式:
# @@@ AUTOTEST_OUTPUT_IGNORED_CELL
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.symbol.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.symbol.Concat(*[c1x1, c3x3, cd3x3, cproj], name='ch_concat_%s_chconcat'% name)
return concat
prev= mx.symbol.Variable(name="Previos Output")
in3a= InceptionFactoryA(prev, 64,64,64,64,96,"avg",32, name="in3a")
mx.viz.plot_network(symbol=in3a, shape=shape)
最終我們可以通過改變多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()
輸出
['softmax_output','regression_output']
與NDArray關係
如我們目前所見,在MXNet中,Symbol與NDArray都提供多維陣列操作符,例如c=a+b
NDArray提供了類指令式程式設計的介面:其中,計算被一條語句一條語句地定值。而相比之下,Symbol更加接近於宣告式程式設計:其中,我們首先宣告計算,然後再用資料定值。
同樣的例子包括正則表示式和SQL。
NDArray優點:
- 簡便。
- 方便與其他語言特性(迴圈、分支)和庫(numpy,…)配合。
- 易於逐步debug。
Symbol優點:
- 提供幾乎NDArray所有的功能,例如+,*和reshape。
- 提供了大量的神經網路相關操作符,例如卷積、啟用和BatchNorm。
- 提供了自動微分。
- 易於構建和操作複雜的計算,例如深度神經網路。
- 易於儲存、讀取和視覺化。
- 後端易於優化計算和儲存的使用。
符號操作
Symbol與NDArray的一大不同就是,我們首先宣告計算,然後繫結資料來執行。
在此部分,我們介紹直接操作符號的函式。但注意,它們大部分都被mx.module完美地包裝了起來。所以,即便跳過本節也無傷大雅。
形狀介面
對於每個符號,我們可以詢問其輸入(或者引數)和輸出。我們也可以通過給定輸入大小來獲得輸出大小,這有易於儲存空間申請。
arg_name = c.list_arguments() # get the names of the inputs
out_name = c.list_outputs() # get the names ofthe outputs
arg_shape, out_shape,_ = c.infer_shape(a=(2,3), b=(2,3))
{'input' :dict(zip(arg_name, arg_shape)),
'output' :dict(zip(out_name, out_shape))}
輸出
{'input': {'a':(2L, 3L), 'b': (2L, 3L)},
'output': {'_plus0_output': (2L, 3L)}}
繫結資料與定值
我們構建的符號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 ofoutputs =%d\nthe first output =\n%s'% (
len(ex.outputs), ex.outputs[0].asnumpy())
輸出
number of outputs =1
the first output =
[[ 2. 2. 2.]
[ 2. 2. 2.]]
我們在GPU上用不同的的資料計算同一個符號:
ex_gpu = c.bind(ctx=mx.gpu(), args={'a' : mx.nd.ones([3,4], mx.gpu())*2,
'b' : mx.nd.ones([3,4], mx.gpu())*3})
ex_gpu.forward()
ex_gpu.outputs[0].asnumpy()
輸出
array([[ 5., 5., 5., 5.],
[ 5., 5., 5., 5.],
[ 5., 5., 5., 5.]], dtype=float32)
儲存和載入
類似於NDArray,我們可以使用pickle模組序列號Symbol或者直接使用save和load。與NDArray選擇的二進位制格式不同,Symbol使用可讀性更強的json格式來序列化。tojson方法返回json字串。
print(c.tojson())
c.save('symbol-c.json')
c2= mx.symbol.load('symbol-c.json')
c.tojson()== c2.tojson()
輸出
{
"nodes": [
{
"op": "null",
"name": "a",
"inputs": []
},
{
"op": "null",
"name": "b",
"inputs": []
},
{
"op": "elemwise_add",
"name": "_plus0",
"inputs": [[0, 0, 0], [1, 0, 0]]
}
],
"arg_nodes": [0, 1],
"node_row_ptr": [0, 1, 2, 3],
"heads": [[2, 0, 0]],
"attrs": {"mxnet_version": ["int", 901]}
}
True
自定義符號
大部分操作符例如mx.sym.Convolution和mx.symReshape為了更好的效能使用C++實現。MXNet也支援使用者用任何前端語言例如Python撰寫新的操作符。這經常使開發和除錯更加簡便。
為了用Python實現一個操作符,我們只需要定義兩個計算方法forward和backward和一些查詢屬性的方法,例如list_arguments和infer_shape。
NDArray是forward和backward方法預設的引數型別。因此我們通常用NDArray操作符來實現計算。當然為例展示MXNet的靈活性,我們將演示使用NumPy來實現一個softmax層的過程。即便基於NumPy的操作符只能運行於CPU且失去一些可應用於NDArray的優化,但是可以享受NumPy提供的豐富的方法功能。
我們首先建立一個mx.operator.CustomOp的子類,然後定義forward和backward。
class Softmax(mx.operator.CustomOp):
def forward(self, is_train, req, in_data, out_data, aux):
x = in_data[0].asnumpy()
y = np.exp(x- x.max(axis=1).reshape((x.shape[0],1)))
y /= y.sum(axis=1).reshape((x.shape[0],1))
self.assign(out_data[0], req[0], mx.nd.array(y))
def backward(self, req, out_grad, in_data, out_data, in_grad, aux):
l = in_data[1].asnumpy().ravel().astype(np.int)
y = out_data[0].asnumpy()
y[np.arange(l.shape[0]), l] -=1.0
self.assign(in_grad[0], req[0], mx.nd.array(y))
此處我們使用asnumpy將NDArray輸入轉換為numpy.ndarray。然後根據req的取值(“重寫”或“加上”)使用CustomOp.assign來將結果賦回mxnet.NDArray。
之後我們建立一個mx.operator.CustomOpProp的子類來查詢屬性。
# register this operator into MXNet by name "softmax"
@mx.operator.register("softmax")
class SoftmaxProp(mx.operator.CustomOpProp):
def __init__(self):
# softmax is a loss layer so we don’t need gradient input
# from layers above.
super(SoftmaxProp,