MXNet Memo

MXNet 的官方不是很详尽, 在使用的过程中经常要深入源码, 这里记录一下在使用的过程中总结的一些方法, 方便查阅.

CentOS 安装 MXNet

pkg-config 找不到 OpenCV, pkg-config 需要配置环境变量来确定搜索路径,首先通过find命令找到 opencv.pc 所在的目录,然后把该目录增加到环境变量 PKG_CONFIG_PATH 中。

Symbol 概述

Composite Symbol

Symbol 主要是符号编程的思想

1
2
3
4
5
6
a = mx.sym.Variable('a') # a 称为 placeholder, 其 name 为 'a'
b = mx.sym.Variable('b') # 同上
c = a + b # 这里不是 + 这个计算, 而是一个操作, a 和 b 通过 + 这个操作 construct 了一个新的 symbol c
# 对于 symbol 如果没有指定名字, 那么, mxnet 会自动分配一个唯一的 name, 例如这里的 c
(type(c), c.name)
# output: (mxnet.symbol.Symbol, '_plus0')

Shape Inference

对于每一个 Symbol 都可以查询它的输入(或者 argument)和输出, 而且, 给定输入的 shape 还可以 inference 到输出的 shape, 这个功能的作用就是可以计算一下需要多少 memory

1
2
3
4
5
6
7
arg_name = c.list_arguments() # get the names of the inputs
out_name = c.list_outputs() # get the names of the outputs
arg_shape, out_shape, aux_shape = c.infer_shape(a=(2,3), b=(2,3)) # 给定输入的shape 计算输出的 shape
print('inputs: %s' % (dict(zip(arg_name, arg_shape))))
print('outputs: %s' % (dict(zip(out_name, out_shape))))
# inputs: {'a': (2L, 3L), 'b': (2L, 3L)}
# outputs: {'_plus0_output': (2L, 3L)}

Bind with Data and Evaluate

上面定义的 c 表明了需要进行什么样的计算, 但是, 只有架子没有数据填充怎么计算呢? 所以, 为了计算, 需要:
feed arguments with data, 这个过程可以通过 bind 方法来解决. 参数为 1. device content 2. dict mapping free Variable name to NDArray

1
2
3
4
5
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()))

Load and Save

Symbol 可以保存到文件中或者从文件中载入. 文件的格式为JSON. 当然也可以通过 cPickle 保存成二进制格式, 但是, JSON 格式易读性更好.

1
2
3
print(c.tojson())
c.save('symbol-c.json')
c2 = mx.symbol.load('symbol-c.json')

自动求导

Symbol 和 NDArray 的一个不同点就是 Symbol 可以自动求导. 调用的方法是 grad

1
2
3
4
5
6
7
8
x = c.grad(wrt=('a'))
print x.list_arguments()
print x.list_outputs()
ex2 = x.bind(ctx=mx.cpu(), args={c.name+'_0_grad': mx.nd.ones([3,4])*5,
'a': mx.nd.ones([3,4])*2,
'b' : mx.nd.ones([3,4])*3})
ex2.forward()
print ex2.outputs[0].asnumpy()

NDArray 概述

复制

常规的 assignment 并没有真正的做 copy 操作. 只是做 reference 操作

1
2
3
4
5
6
7
a = mx.nd.ones((2,2))
b = a # copy by reference
print(b is a)
def f(x): # also copy by reference
print(id(x))
f(a)
print(id(a))

真正的 copy 操作是

1
b=a.copy()

另外, copyto 方法和 [] 操作都不会分配新的内存

1
2
3
4
5
6
b = mx.nd.ones(a.shape)
print(id(b))
b[:] = a
print(id(b))
a.copyto(b)
print(id(b))

Basic Operations

对于 array 的所有的操作都是 elementwise 的. 而且会生成新的 array, 结果保存到这个新生程的 array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = mx.nd.ones((2,3))
b = mx.nd.ones((2,3))
c = a + b # elementwise plus
d = - c # elementwise minus
print(d.asnumpy())
e = mx.nd.sin(c**2).T # elementwise pow and sin, and then transpose
print(e.asnumpy())
f = mx.nd.maximum(a, c) # elementwise max
print(f.asnumpy())
a = mx.nd.ones((2,2))
b = a * a # elementwise multiply
c = mx.nd.dot(a,a) # matrix-matrix multiplication
print(b.asnumpy())
print(c.asnumpy())

获取中间层的输出

1
2
3
4
# net 是网络的该层以及之前的层组成的 symbol, input1, input2, input3 是该层的输入的 shape
arg_shape, output_shape, aux_shape = net.infer_shape(input1=(8,3,32,32),
input2=(8,3,32,32),
input3=(8,3,32,32))

使用已有的模型初始化网络 I

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 首先载入现有模型参数, 其中 "models/ir" 是模型的名字,
# 1 是对应的 保存模型的 epoch,
# 典型的自动保存的模型的命名方式为: ir-0001.params, ir-symbol.json
# 新的网络是根据 node 节点的名字去初始化参数的,
# 所以, 如果要重新初始化某一层, 就要把该层的名字重新命名
old_model = mx.model.FeedForward.load("models/ir", 1)
model= mx.model.FeedForward(
ctx=dev,
symbol=get_symbol(), # 新的网络
arg_params=old_model.arg_params, # 载入参数
aux_params=old_model.aux_params # 载入参数
allow_extra_params=True) # 允许其它额外的参数, 即允许在 old_model 中没有的 layer name,
# 这样, 才可以使用默认的初始化方法初始化这些 layer, 进而 finetune
# old_model 中没有的参数, 使用 estimator 来初始化
# MXNet 中相关的源码:
# model.FeedForward.fit()
if isinstance(self.optimizer, str):
batch_size = data.batch_size
if kvstore and kvstore.type == 'dist_sync':
batch_size *= kvstore.num_workers
optimizer = opt.create(self.optimizer,
rescale_grad=(1.0/batch_size),
**(self.kwargs))
elif isinstance(self.optimizer, opt.Optimizer):
optimizer = self.optimizer

自动保存模型

1
model.fit(epoch_end_callback=mx.callback.do_checkpoint("model_name")))

使用已有的模型初始化网络 II (推荐)

1
2
3
4
5
6
7
8
9
10
11
12
optimizer = mx.optimizer.SGD(momentum=0.99) # lr_scheduler=lr_scheduler)
model = mx.model.FeedForward(
allow_extra_params=True,
ctx=dev,
symbol=network,
num_epoch=10,
learning_rate=0.1*1e-2,
wd=0.0001,
# 这里是与方法 I 不同的地方, 使用 mx.init.Load 来载入参数值, 并且, 给予默认的初始化方法, 没有载入的参数使用
# 默认的方法初始化
initializer=mx.init.Load("resnet-18-0000.params", default_init=mx.init.Xavier(rnd_type="gaussian", factor_type="in", magnitude=2)),
optimizer=optimizer)

手动保存模型

1
model.save()

获取中间层的输出结果

1
sym.get_internals()['***_output']

列举出所有 symbol 的 attribute

1
2
# 目前主要是 lr_mult 这个 attribute
symbol.list_attr(recursive=True)

可视化网络结构

1
2
3
4
5
# 指定输入数据的维数, 否则不会可视化数据的 shape 信息, 而且 "data" 是输入 Symbol 的 name
shape = {"data" : (128, 3, 28, 28)}
g=mx.viz.plot_network(symbol=network, shape=shape)
# cleanup 是删除中间过程产生的数据和文件
g.render(filename="filename", cleanup=True)

其它

图片数据格式

如果使用 MXNet 提供的图片数据转换工具 im2rec.py, 需要注意的是, 该工具把数据转换成了 RGB 格式, 所以, 实际使用的时候, 需要把通过 OpenCV 读取的图片转换成 RGB 格式, 方法如下:

1
dst = cv2.cvtColor(src, cv2.COLOR_BGR2RGB)

读取 mean.bin

在训练的过程中常常会生成 mean.bin 文件, 每张图片在 feed 到网络中之前要先减去 mean, 因此, 在 predict 的时候, 也需要相应的减去 mean. 首先要载入该 mean.bin

1
mean_img = mx.nd.load("./mean.bin")['mean_img'].asnumpy()

获取参数的 shape

1
2
3
4
ex = net.simple_bind(ctx=mx.cpu(), data=(batch_size, num_features))
args = dict(zip(net.list_arguments(), ex.arg_arrays))
for name in args:
print(name, args[name].shape)