MXNet入门

The basic

创建矩阵

1
2
3
import mxnet as mx
a=mx.nd.array([1,2,3])
b=mx.nd.array([1,2,3],[2,3,4])

或者也可以通过numpy.ndarray来创建

1
2
3
4
5
6
import numpy as np
import math
c = np.arange(15).reshape(3,5)
# create a 2-dimensional array from a numpy.ndarray object
a = mx.nd.array(c)
{'a.shape':a.shape}

可以通过dtype来指定元素类型

1
2
3
4
5
6
7
# float32 is used in deafult
a = mx.nd.array([1,2,3])
# create an int32 array
b = mx.nd.array([1,2,3], dtype=np.int32)
# create a 16-bit float array
c = mx.nd.array([1.2, 2.3], dtype=np.float16)
(a.dtype, b.dtype, c.dtype)

同时还可以初始化矩阵而不需要给每一个元素指定数值,其中后面两种需要特别注意

1
2
3
4
5
6
7
a=mx.nd.zeros((2,3))
b=mx.nd.ones((2,3))
# create a same shape array with all elements set to 7
c = mx.nd.full((2,3), 7)
# create a same shape whose initial content is random and
# depends on the state of the memory
d = mx.nd.empty((2,3))

基础操作

算数运算都是元素和元素之间的(elementwise)的,同时一个新的array会被创建并用来存储这个结果。

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

矩阵的乘法运算和numpy一样,是使用dot来实现的。

1
2
3
a = mx.nd.ones((2,2))
b = a * a
c = mx.nd.dot(a,a)

+=*=操作会直接修改现有的array而不是去生成一个新的。

1
2
3
4
a = mx.nd.ones((2,2))
b = mx.nd.ones(a.shape)
b += a
b.asnumpy()

索引和切片

其中将a的第二行全部设置为了1

1
2
3
a = mx.nd.array(np.arange(6).reshape(3,2))
a[1:2] = 1
a[:].asnumpy()

也可以沿着特定的维度进行切片。沿着第二个维度进行切片,也就是将第二个维度中满足(begin,end]的维度切出来。

1
2
d = mx.nd.slice_axis(a, axis=1, begin=1, end=2)
d.asnumpy()

形状操作

array修改成另一个大小相同的array

1
2
3
a = mx.nd.array(np.arange(24))
b = a.reshape((2,3,4))
b.asnumpy()

也可将多个array进行拼接,可以使用concatenate方法,在不给额外参数的情况下是沿着第0维拼接的。可以通过增加axis=1来指定沿其他维度进行拼接,这时候要求这几个array除了被axis指定的维度的大小不一样以外,其他维度都要求是相同的。

1
2
3
4
a = mx.nd.ones((2,3))
b = mx.nd.ones((2,3))*2
c = mx.nd.concatenate([a,b])
c.asnumpy()

归约

沿着第一维求和,最后会让第一维消失

1
2
a=mx.nd.ones((2,3))
b=mx.nd.sum(a,axis=1)

Broadcast

这个操作会自动补齐维度,通过复制的方式

1
2
3
a = mx.nd.array(np.arange(6).reshape(6,1))
b = a.broadcast_to((6,2)) #
b.asnumpy()

1
2
3
c = a.reshape((2,1,1,3))
d = c.broadcast_to((2,2,2,3))
d.asnumpy()

*+会自动使用boardcast让两个矩阵的形状相同

数据拷贝

普通的赋值操作并不会产生新的数据,下面的b实际上还是a本身,类似指针。

1
2
3
a = mx.nd.ones((2,2))
b = a
b is a

1
True

下面的操作同样也没有产生新的数据

1
2
3
def f(x):
return x
a is f(a)

显示

1
True

可以使用copy操作来进行真正的拷贝

1
2
b = a.copy()
b is a

利用GPU进行运算

可以加上mx.gpu(0)或者mx.gpu()来切换到GPU来进行计算

1
2
a = mx.nd.ones((100, 100), mx.gpu(0))
a

则显示

1
<NDArray 100x100 @gpu(0)>

现在的MXNet要求两个矩阵要在同一个设备上才能进行计算,可以将一个矩阵拷贝到GPU

1
2
3
4
5
6
7
a = mx.nd.ones((100,100), mx.cpu())
b = mx.nd.ones((100,100), mx.gpu())
c = mx.nd.ones((100,100), mx.gpu())
a.copyto(c) # copy from CPU to GPU
d = b + c
e = b.as_in_context(c.context) + c # same to above
{'d':d, 'e':e}

将数据写到磁盘以及从磁盘中读取数据

使用pickle

1
2
3
4
5
6
7
8
9
import pickle as pkl
a = mx.nd.ones((2, 3))
# pack and then dump into disk
data = pkl.dumps(a)
pkl.dump(data, open('tmp.pickle', 'wb'))
# load from disk and then unpack
data = pkl.load(open('tmp.pickle', 'rb'))
b = pkl.loads(data)
b.asnumpy()

使用mxnet自带的功能

可以dump一个list

1
2
3
4
5
a = mx.nd.ones((2,3))
b = mx.nd.ones((5,6))
mx.nd.save("temp.ndarray", [a,b])
c = mx.nd.load("temp.ndarray")
c

NDArray和Numpy的区别

  1. NDArray每次只能对一个维度做切片,像x[:,1]这样同时对两个维度做切片是做不到的
  2. 切片只能是连续的,不能使用x[1:2:3]这样的
  3. 布尔索引是不被支持的
  4. 缺乏像maxmin这样的reduce函数

Symbol

创建三个symbol,并将其命名为a,b。可以使用mx.symmx.symbol,两者是一样的东西

1
2
3
4
5
import mxnet as mx
a = mx.sym.Variable('a')
b = mx.sym.Variable('b')
c = a + b
(a, b, c)

大部分的NDArray的操作可以直接作用于Symbol

1
2
3
4
5
6
7
8
9
# 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)

可以将网络可视化输出为pdf只需要调用,但是似乎会报错。尽管会报错,但是输出的也是正常的。

1
mx.viz.plot_network(symbol=g).view()

Symbol和NDArray的关系

  1. Symbol可以提供几乎所有的NDArray的功能。
  2. 提供大量的神经网络相关操作,比如卷积,激活和BatchNorm
  3. 提供自动求导的功能
  4. 便于构建和操作复杂的计算,例如深度神经网络
  5. 便于存储、加载和可视化
  6. 便于后端优化计算和内存使用

    Symbol Save和Load

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

建立自己的操作

1
2
3
4
5
6
7
8
9
10
11
12
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,最后再用CustomOp.assign来将结果写回mxnet.NDArray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 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, 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):
data_shape = in_shape[0]
label_shape = (in_shape[0][0],)
output_shape = in_shape[0]
return [data_shape, label_shape], [output_shape], []

def create_operator(self, ctx, shapes, dtypes):
return Softmax()

最后能够使用mx.sym.Customregister name来使用这个operator

1
net = mx.symbol.Custom(data=prev_input, op_type='softmax')

但是这个方法定义的operator不能放在GPU上运行。

Advanced Usages

MXNet默认使用32-bit float。有时候我们想要使用低精度的数据类型来获得更好的accuracy-performance trade-off。例如Nvidia Tesla Pascal GPUs已经改进了16-bit float的性能,并且GTX Pascal GPUs(e.g.GTX1080)在8-bit intergers上是很快的。

我们可以使用mx.sym.Cast操作来转换数据类型

1
2
3
4
5
6
7
8
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})

Variable Sharing

1
2
3
4
5
6
7
8
9
a = mx.sym.Variable('a')
b = mx.sym.Variable('b')
c = mx.sym.Variable('c')
d = a + b * c

data = mx.nd.ones((2,3))*2
ex = d.bind(ctx=mx.cpu(), args={'a':data, 'b':data, 'c':data})
ex.forward()
ex.outputs[0].asnumpy()

Training and Inference Module

在这个tutorial中,我们将使用一个简单的多层感知机和一个合成的数据集

1
2
3
4
5
6
7
8
9
10
11
12
import mxnet as mx
from data_iter import SyntheticData

# mlp
net = mx.sym.Variable('data')
net = mx.sym.FullyConnected(net, name='fc1', num_hidden=64)
net = mx.sym.Activation(net, name='relu1', act_type="relu")
net = mx.sym.FullyConnected(net, name='fc2', num_hidden=10)
net = mx.sym.SoftmaxOutput(net, name='softmax')
# synthetic 10 classes dataset with 128 dimension
data = SyntheticData(10, 128)
mx.viz.plot_network(net)

Create Module

最广泛使用的模块是Module,这个包含了Symbol和一个或多个Executor。我们构建一个module通过

  • symbol网络的符号
  • context执行的设备
  • data_names数据变量名字的list
  • label_names
    1
    2
    3
    4
    mod = mx.mod.Module(symbol=net, 
    context=mx.cpu(),
    data_names=['data'],
    label_names=['softmax_label'])

Train, Predict and Evaluate

前面我们获得了一个mod,然后这个模块提供了一个fit函数,可以看到其中需要一个数据迭代器来传送训练数据和标准数据。

1
2
3
4
5
6
7
8
9
10
11
# @@@ AUTOTEST_OUTPUT_IGNORED_CELL
import logging
logging.basicConfig(level=logging.INFO)

batch_size=32
mod.fit(data.get_iter(batch_size),
eval_data=data.get_iter(batch_size),
optimizer='sgd',
optimizer_params={'learning_rate':0.1},
eval_metric='acc',
num_epoch=5)

当整个模型训练好了之后,可以使用predict来预测新的数据,输入的是一个数据迭代器,虽然每次只是输入一个batch,但是它会收集并返回所有的预测结果。

1
y = mod.predict(data.get_iter(batch_size))

考虑到如果内存是不够的,那么也可以每次只预测一个batch

1
2
3
4
for preds, i_batch, batch in mod.iter_predict(data.get_iter(batch_size)):
pred_label = preds[0].asnumpy().argmax(axis=1)
label = batch.label[0].asnumpy().astype('int32')
print('batch %d, accuracy %f' % (i_batch, float(sum(pred_label==label))/len(label)))

如果是用来在测试集上做评价的话,可以使用score函数来得出结果

1
mod.score(data.get_iter(batch_size), ['mse', 'acc'])

这里给了MSEACC两种度量方式,那么返回的也会是两种

1
[('mse', 27.438781929016113), ('accuracy', 0.115625)]

Save and Load

Save

我们可以在每一个epoch结束之后保存模型的参数,这个里的方式就是使用一个回调函数。

1
2
3
4
5
6
7
# @@@ AUTOTEST_OUTPUT_IGNORED_CELL
# construct a callback function to save checkpoints
model_prefix = 'mx_mlp'
checkpoint = mx.callback.do_checkpoint(model_prefix)

mod = mx.mod.Module(symbol=net)
mod.fit(data.get_iter(batch_size), num_epoch=5, epoch_end_callback=checkpoint)

Load

从磁盘中读取上一次的网络状态,然后将它设置到模型中

1
2
3
4
5
sym, arg_params, aux_params = mx.model.load_checkpoint(model_prefix, 3)
print(sym.tojson() == net.tojson())

# assign the loaded parameters to the module
mod.set_params(arg_params, aux_params)

也可以直接直接开始训练

1
2
3
4
5
6
7
# @@@ AUTOTEST_OUTPUT_IGNORED_CELL
mod = mx.mod.Module(symbol=sym)
mod.fit(data.get_iter(batch_size),
num_epoch=5,
arg_params=arg_params,
aux_params=aux_params,
begin_epoch=3)

Module as a computation “machine”

一个模块有多个状态

  • Initial state内存还未分配,还没有准备好计算
  • Binded输入、输出和参数的形状都已经知道了,内存也已经分配给它了,已经准备好了计算
  • Parameter initialized一个模块如果没有对参数初始化就进行计算的话,会出现没定义好的输出
  • Optimizer installed指定了网络的优化方式,只有这样才能对权值进行更新。
    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
    # @@@ AUTOTEST_OUTPUT_IGNORED_CELL
    # initial state
    mod = mx.mod.Module(symbol=net)

    # bind, tell the module the data and label shapes, so
    # that memory could be allocated on the devices for computation
    train_iter = data.get_iter(batch_size)
    mod.bind(data_shapes=train_iter.provide_data, label_shapes=train_iter.provide_label)

    # init parameters
    mod.init_params(initializer=mx.init.Xavier(magnitude=2.))

    # init optimizer
    mod.init_optimizer(optimizer='sgd', optimizer_params=(('learning_rate', 0.1), ))

    # use accuracy as the metric
    metric = mx.metric.create('acc')

    # train one epoch, i.e. going over the data iter one pass
    for batch in train_iter:
    mod.forward(batch, is_train=True) # compute predictions
    mod.update_metric(metric, batch.label) # accumulate prediction accuracy
    mod.backward() # compute gradients
    mod.update() # update parameters using SGD

    # training accuracy
    print(metric.get())

Loading Data

Basic Data Iterator

这是一个数据迭代器,类似python中的迭代器。

1
2
3
4
5
class SimpleBatch(object):
def __init__(self, data, label, pad=0):
self.data = data
self.label = label
self.pad = pad

一个batch的形状应该是batch_size x num_chaneel x height x width

Symbol and Data Variables

新建一个数据迭代器

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
28
29
30
31
32
33
34
35
36
37
38
import numpy as np
class SimpleIter:
def __init__(self, data_names, data_shapes, data_gen,
label_names, label_shapes, label_gen, num_batches=10):
self._provide_data = zip(data_names, data_shapes)
self._provide_label = zip(label_names, label_shapes)
self.num_batches = num_batches
self.data_gen = data_gen
self.label_gen = label_gen
self.cur_batch = 0

def __iter__(self):
return self

def reset(self):
self.cur_batch = 0

def __next__(self):
return self.next()

@property
def provide_data(self):
return self._provide_data

@property
def provide_label(self):
return self._provide_label

def next(self):
if self.cur_batch < self.num_batches:
self.cur_batch += 1
data = [mx.nd.array(g(d[1])) for d,g in zip(self._provide_data, self.data_gen)]
assert len(data) > 0, "Empty batch data."
label = [mx.nd.array(g(d[1])) for d,g in zip(self._provide_label, self.label_gen)]
assert len(label) > 0, "Empty batch label."
return SimpleBatch(data, label)
else:
raise StopIteration

mxnet.symbol.SoftmaxOutput

在前向传播中,这个函数会返回softmax的输出,而在后向传播的时候,这个函数会加上logit loss

用numpy为mx.nd.array赋值

1
2
a=mx.nd.ones((2,3),ctx=mx.gpu())
a[:]=np.random.rand(*a.shape)

不能使用

1
a=np.random.rand(*a.shape)

分享到