一、符号分类
符号对我们想要进行的计算进行了描述, 下图展示了符号如何对计算进行描述.
我们定义了符号变量A, 符号变量B, 生成了符号变量C, 其中, A, B为参数节点, C为内部节点! mxnet.symbol.Variable可以生成参数节点, 用于表示计算时的输入.
二、常用符号方法
一个Symbol具有的属性和方法如下图所示:
关联节点查看
list_argument()用来检查计算图的输入参数;
list_output()返回此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()可以得到相应的梯度值.
神经网络的简单绑定接口
反向传播时,我们需要定义很多新的grad节点并绑定给Executor,过程较为繁琐,Symbol.simple_bind()函数可以帮助我们简化这个过程,指定输入数据的大小(shape),这个函数可以定位梯度参数并将其绑定为Executor.
辅助变量
四、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,这样训练就可以照常进行了。