一、符號分類
符號對我們想要進行的計算進行了描述, 下圖展示了符號如何對計算進行描述.
我們定義了符號變量A, 符號變量B, 生成了符號變量C, 其中, A, B為參數節點, C為內部節點! mxnet.symbol.Variable可以生成參數節點, 用於表示計算時的輸入.
二、常用符號方法
一個Symbol具有的屬性和方法如下圖所示:
關聯節點查看
list_arguments()用來檢查計算圖的輸入參數;
list_outputs()返回此Symbol的所有輸出,輸出的自動命名遵循一定的規則
input = mx.sym.Variable('data') # 生成一個符號變量,名字是可以隨便取的 fc1 = mx.sym.FullyConnected(data=input, num_hidden=128,name='fc1') # 全連接層 act1 = mx.sym.Activation(fc1, act_type='relu') # 激活 type(fc1) # mxnet.symbol.Symbol, act1的類型也是這個!!! fc1.list_outputs() # ['fc1_output'],自動在輸入name屬性名的后面加上"_output"作為本節點名稱 fc1.list_arguments() # ['data','fc1_weight','fc1_bias'],自動生成fc1_weight,fc1_bias兩個參數節點 act1.list_outputs() # ['actvation0_output'] 這個名字就不是隨便起的了!!! act1.list_arguments() # ['data','fc1_weight','fc1_bias']
返回邏輯如下圖,
數據維度推斷
mxnet.symbol.Symbol.infer_shape(self, *args, **kwargs): 推測輸入參數和輸出參數的shape, 返回一個list of tuple;
a = mx.sym.Variable('A') b = mx.sym.Variable('B') c = (a + b) / 10 d = c + 1 input_shapes = {'A':(10,2), 'B':(10,2)} # 定義輸入的shape d.infer_shape(**input_shapes) # ([(10L, 2L), (10L, 2L)], [(10L, 2L)], []) arg_shapes, out_shapes, aux_shapes = d.infer_shape(**input_shapes)
In [1]: arg_shapes
Out[1]: [(10L, 2L), (10L, 2L)]
In [2]: out_shapes
Out[2]: [(10L, 2L)]
In [3]: aux_shapes
Out[3]: []
附、可視化
mx.viz.plot_network(d).view()
三、綁定執行
A = mx.sym.Variable('A') B = mx.sym.Variable('B') C = A * B D = mx.sym.Variable('D') E = C + D a = mx.nd.empty(1) # 生成一個維度為1的隨機值 b = mx.nd.ones(1) # b等於1 d = mx.nd.ones(1) executor = E.bind(ctx=mx.cpu(), args={'A':a, 'B':b, 'D':d}) type(executor) # mxnet.executor.Executor executor.arg_dict # {'A': <NDArray 1 @cpu(0)>, 'B': <NDArray 1 @cpu(0)>, 'D': <NDArray 1 @cpu(0)>} executor.forward() # [<NDArray 1 @cpu(0)>] executor.outputs[0] # <NDArray 1 @cpu(0)>, 值呢? 還是看不到值啊??? executor.outputs[0].asnumpy() # array([ 1.], dtype=float32)
首先我們需要調用綁定函數(bind function:*.bind)來綁定NDArrays(下圖中的a/b/d)到參數節點(argument nodes: A/B/D,不是內部節點C/E),從而獲得一個執行器(Executor),其作用是獲取數組大小,以分配內存或顯存:
然后,調用Executor.Forward 便可以得到輸出結果.
執行器屬性方法如下:
綁定多個輸出
我們可以使用mx.symbol.Group([])來將symbols進行分組,然后將它們進行綁定,從而得到更多的中間變量輸出。
下圖中,A/B/D為參數節點,C/E為內部節點,將E/C綁定為G,這樣,E和C的計算結果都可以得到,但是出於優化計算圖的考慮,不建議過多綁定輸出節點。
梯度計算
在綁定函數中,可以指定NDArrays來保存梯度,在Executor.forward()的后面調用Executor.backward()可以得到相應的梯度值.
輔助變量
四、新建symbol節點
官方文檔例子,復現一個softmax節點,並進行一次反向傳播(沒有更新參數):
import mxnet as mx from mxnet.test_utils import get_mnist_iterator import numpy as np import logging import mxnet.ndarray as nd 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)) @mx.operator.register("softmax") class SoftmaxProp(mx.operator.CustomOpProp): def __init__(self): """使用need_top_grad = False調用基礎構造函數, 因為softmax是一個損失層,不需要前面層的梯度輸入""" super(SoftmaxProp, self).__init__(need_top_grad=False) def list_arguments(self): return ['data', 'label'] def list_outputs(self): return ['output'] def infer_shape(self, in_shape): """提供infer_shape來聲明輸出/權重的形狀並檢查輸入形狀的一致性""" data_shape = in_shape[0] label_shape = (in_shape[0][0],) output_shape = in_shape[0] return [data_shape, label_shape], [output_shape], [] def infer_type(self, in_type): return in_type, [in_type[0]], [] def create_operator(self, ctx, shapes, dtypes): """定義一個create_operator函數,該函數將由后端調用以創建softmax的實例""" return Softmax() # define mlp net = mx.sym.Variable('data') net = mx.sym.FullyConnected(net, name='fc', num_hidden=6) net = mx.sym.Activation(net, name='relu', act_type="relu") mlp = mx.symbol.Custom(data=net, name='softmax', op_type='softmax') # train # logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.INFO) # MXNET_CPU_WORKER_NTHREADS must be greater than 1 for custom op to work on CPU context=mx.cpu() # Uncomment this line to train on GPU # context=mx.gpu(0) print(mlp.list_arguments(), mlp.list_outputs()) input_shapes = {'data':(5, 28*28)} print(mlp.infer_shape(**input_shapes)) args = {'data': mx.nd.ones((1, 4)), 'fc_weight': mx.nd.ones((6, 4)), 'fc_bias': mx.nd.array((1, 4, 4, 4, 5, 6)), 'softmax_label': mx.nd.ones((1))} args_grad = {'fc_weight': mx.nd.zeros((6, 4)), 'fc_bias': mx.nd.zeros((6))} executor = mlp.bind(ctx=mx.cpu(0), args=args, args_grad=args_grad, grad_req='write') # 所有參數節點數組 print("executor.arg_dict 初始值\n", executor.arg_dict) # 所有參數節點對應梯度數組 print("executor.grad_dict 初始值\n", executor.grad_dict) executor.backward() # # data # train, val = get_mnist_iterator(batch_size=100, input_shape = (784,)) # mod = mx.mod.Module(mlp, context=context) # mod.fit(train_data=train, eval_data=val, optimizer='sgd', # optimizer_params={'learning_rate':0.1, 'momentum': 0.9, 'wd': 0.00001}, # num_epoch=10, batch_end_callback=mx.callback.Speedometer(100, 100))
['data', 'fc_weight', 'fc_bias', 'softmax_label'] ['softmax_output'] ([(5, 784), (6, 784), (6,), (5,)], [(5, 6)], []) executor.arg_dict 初始值 {'data': [[1. 1. 1. 1.]] <NDArray 1x4 @cpu(0)>, 'fc_weight': [[1. 1. 1. 1.] [1. 1. 1. 1.] [1. 1. 1. 1.] [1. 1. 1. 1.] [1. 1. 1. 1.] [1. 1. 1. 1.]] <NDArray 6x4 @cpu(0)>, 'fc_bias': [1. 4. 4. 4. 5. 6.] <NDArray 6 @cpu(0)>, 'softmax_label': [1.] <NDArray 1 @cpu(0)>} executor.grad_dict 初始值 {'data': None, 'fc_weight': [[0. 0. 0. 0.] [0. 0. 0. 0.] [0. 0. 0. 0.] [0. 0. 0. 0.] [0. 0. 0. 0.] [0. 0. 0. 0.]] <NDArray 6x4 @cpu(0)>, 'fc_bias': [0. 0. 0. 0. 0. 0.] <NDArray 6 @cpu(0)>, 'softmax_label': None}
可以看到,bind方法其實蠻麻煩的,需要將參數、梯度參數全部初始化,才能進行下一步的操作,這就引出了兩個其他方法:
- 僅僅指定輸入shape以申請內存的Symbol.simple_bind(),其參數僅僅是shape,這也意味這此方法僅僅能夠測試,由於沒有引進實際數據執行forward、backward兩個方法的返回值並無意義。
- mxnet.mod.Module類,集成了參數初始化、前傳反傳、參數更新等一系列方法,簡化了訓練的繁瑣,個人感覺是介於gluon和基礎symbol之間產物。
simple_bind
反向傳播時,我們需要定義很多新的grad節點並綁定給Executor,過程較為繁瑣,Symbol.simple_bind()函數可以幫助我們簡化這個過程,指定輸入數據的大小(shape),這個函數可以定位梯度參數並將其綁定為Executor.
v1 = mx.ndarray.array([[1, 1]])
v2 = mx.ndarray.array([[2, 2]])
v3 = mx.ndarray.array([[3, 3]])
a = mx.symbol.Variable('a')
b = mx.symbol.Variable('b')
c = mx.symbol.Variable('c')
d = b + c
b_stop_grad = mx.symbol.BlockGrad(3 * d)
loss = mx.sym.MakeLoss(b_stop_grad + a)
executor = loss.simple_bind(ctx=mx.cpu(), a=(1,2), b=(1,2), c=(1,2))
executor.forward(is_train=True, a=v1, b=v2, c=v3)
executor.outputs
Out[5]:
[
[[16. 16.]]
<NDArray 1x2 @cpu(0)>]
executor.backward()
executor.grad_dict
Out[6]:
{'b':
[[0. 0.]]
<NDArray 1x2 @cpu(0)>, 'c':
[[0. 0.]]
<NDArray 1x2 @cpu(0)>, 'a':
[[1. 1.]]
<NDArray 1x2 @cpu(0)>}
五、Modue對象
更為常用的方法是使用symbol生成計算圖后將之轉換為Module對象,再進行訓練,
import mxnet as mx # construct a simple MLP data = mx.symbol.Variable('data') fc1 = mx.symbol.FullyConnected(data, name='fc1', num_hidden=128) act1 = mx.symbol.Activation(fc1, name='relu1', act_type="relu") fc2 = mx.symbol.FullyConnected(act1, name = 'fc2', num_hidden = 64) act2 = mx.symbol.Activation(fc2, name='relu2', act_type="relu") fc3 = mx.symbol.FullyConnected(act2, name='fc3', num_hidden=10) out = mx.symbol.SoftmaxOutput(fc3, name = 'softmax') # construct the module mod = mx.mod.Module(out) mod.bind(data_shapes=train_dataiter.provide_data, label_shapes=train_dataiter.provide_label) mod.init_params() mod.fit(train_dataiter, eval_data=eval_dataiter, optimizer_params={'learning_rate':0.01, 'momentum': 0.9}, num_epoch=n_epoch
首先是定義了一個簡單的MLP,symbol的名字就叫做out,然后可以直接用mx.mod.Module來創建一個mod。之后mod.bind的操作是在顯卡上分配所需的顯存,所以我們需要把data_shapehe label_shape傳遞給他,然后初始化網絡的參數,再然后就是mod.fit開始訓練了。
fit方法核心代碼如下:
for epoch in range(begin_epoch, num_epoch): tic = time.time() eval_metric.reset() for nbatch, data_batch in enumerate(train_data): if monitor is not None: monitor.tic() self.forward_backward(data_batch) #網絡進行一次前向傳播和后向傳播 self.update() #更新參數 self.update_metric(eval_metric, data_batch.label) #更新metric if monitor is not None: monitor.toc_print() if batch_end_callback is not None: batch_end_params = BatchEndParam(epoch=epoch, nbatch=nbatch, eval_metric=eval_metric, locals=locals()) for callback in _as_list(batch_end_callback): callback(batch_end_params)
對於訓練過程我們可以做出很多改進,舉個最簡單的例子:如果我們的訓練網絡是大小可變怎么辦? 我們可以實現一個mutumodule,基本上就是,每次data的shape變了的時候,我們就重新bind一下symbol,這樣訓練就可以照常進行了。