MXNet 中文文档
MXNet设计和实现简介
对于一个优秀的深度学习系统,或者更广来说优秀的科学计算系统,最重要的是编程接口的设计。他们都采用将一个领域特定语言(domain specific language)嵌入到一个主语言中。例如numpy将矩阵运算嵌入到python中。这类嵌入一般分为两种,其中一种嵌入的较浅,其中每个语句都按原来的意思执行,且通常采用命令式编程(imperative programming),其中numpy和Torch就是属于这种。而另一种则用一种深的嵌入方式,提供一整套针对具体应用的迷你语言。这一种通常使用声明式语言(declarative programing),既用户只需要声明要做什么,而具体执行则由系统完成。这类系统包括Caffe,theano和刚公布的TensorFlow。
浅嵌入,命令式编程 | 深嵌入,声明式编程 | |
---|---|---|
如何执行 a=b+1 | 需要b已经被赋值。立即执行加法,将结果保存在a中。 | 返回对应的计算图(computation graph),我们可以之后对b进行赋值,然后再执行加法运算 |
优点 | 语义上容易理解,灵活,可以精确控制行为。通常可以无缝地和主语言交互,方便地利用主语言的各类算法,工具包,debug和性能调试器。 | 在真正开始计算的时候已经拿到了整个计算图,所以我们可以做一系列优化来提升性能。实现辅助函数也容易,例如对任何计算图都提供forward和backward函数,对计算图进行可视化,将图保存到硬盘和从硬盘读取。 |
缺点 | 实现统一的辅助函数和提供整体优化都很困难。 | 很多主语言的特性都用不上。某些在主语言中实现简单,但在这里却经常麻烦,例如if-else语句 。debug也不容易,例如监视一个复杂的计算图中的某个节点的中间结果并不简单。 |
目前现有的系统大部分都采用上两种编程模式的一种。与它们不同的是,MXNet尝试将两种模式无缝的结合起来。在命令式编程上MXNet提供张量运算,而声明式编程中MXNet支持符号表达式。用户可以自由的混合它们来快速实现自己的想法。例如可以用声明式编程来描述神经网络,并利用系统提供的自动求导来训练模型。另一方便,模型的迭代训练和更新模型法则中可能涉及大量的控制逻辑,因此我们可以用命令式编程来实现。同时用它来进行方便地调式和与主语言交互数据。
下表比较MXNet和其他流行的深度学习系统
主语言 | 从语言 | 硬件 | 分布式 | 命令式 | 声明式 | |
---|---|---|---|---|---|---|
Caffe | C++ | Python/Matlab | CPU/GPU | x | x | v |
Torch | Lua | - | CPU/GPU/FPGA | x | v | x |
Theano | Python | - | CPU/GPU | x | v | x |
TensorFlow | C++ | Python | CPU/GPU/Mobile | v | x | v |
MXNet | C++ | Python/R/Julia/Go | CPU/GPU/Mobile | v | v | v |
MXNet的系统框架如下:
从上到下分别为各种主语言的嵌入,编程接口(矩阵运算,符号表达式,分布式通讯),两种编程模式的统一系统实现,以及各硬件的支持。
编程接口
Symbol: 声明式的符号表达式
MXNet使用多值输出的符号表达式来声明计算图。符号是由操作子(也就是TensorFlow 中的ops概念)构建而来。一个操作子可以是一个简单的矩阵运算“+”,也可以是一个复杂的神经网络里面的层,例如卷积层。一个操作子可以有多个输入变量和多个输出变量,还可以有内部状态变量。一个变量既可以是自由的,我们可以之后对其赋值;也可以是某个操作子的输出。
NDArray:命令式的张量计算
MXNet提供命令式的张量计算来桥接主语言的和符号表达式。下面代码中,我们在GPU上计算矩阵和常量的乘法,并使用numpy来打印结果
>>> import MXNet as mx
>>> a = mx.nd.ones((2, 3), mx.gpu())
>>> print (a * 2).asnumpy()
[[ 2. 2. 2.]
[ 2. 2. 2.]]
另一方面,NDArray可以无缝和符号表达式进行对接。假设我们使用Symbol定义了一个神经网络,那么我们可以如下实现一个梯度下降算法
for (int i = 0; i < n; ++i) {
net.forward();
net.backward();
net.weight -= eta * net.grad
}
这里梯度由Symbol计算而得。Symbol的输出结果均表示成NDArray,我们可以通过NDArray提供的张量计算来更新权重。此外,还利用了主语言的for循环来进行迭代,学习率eta也是在主语言中进行修改。
KVStore: 多设备间的数据交互
MXNet提供一个分布式的key-value存储来进行数据交换。它主要有两个函数,
push: 将key-value对从一个设备push进存储
pull:将某个key上的值从存储中pull出来此外,KVStore还接受自定义的更新函数来控制收到的值如何写入到存储中。最后KVStore提供数种包含最终一致性模型和顺序一致性模型在内的数据一致性模型。
分布式梯度下降算法:
KVStore kvstore("dist_async");
kvstore.set_updater([](NDArray weight, NDArray gradient) {
weight -= eta * gradient;
});
for (int i = 0; i < max_iter; ++i) {
kvstore.pull(network.weight);
network.forward();
network.backward();
kvstore.push(network.gradient);
}
在这里先使用最终一致性模型创建一个kvstore,然后将更新函数注册进去。在每轮迭代前,每个计算节点先将最新的权重pull回来,之后将计算的得到的梯度push出去。kvstore将会利用更新函数来使用收到的梯度更新其所存储的权重。
这里push和pull跟NDArray一样使用了延后计算的技术。它们只是将对应的操作提交给后台引擎,而引擎则调度实际的数据交互。所以上述的实现跟我们使用纯符号实现的性能相差无几。
读入数据模块
数据读取在整体系统性能上占重要地位。MXNet提供工具能将任意大小的样本压缩打包成单个或者数个文件来加速顺序和随机读取。
通常数据存在本地磁盘或者远端的分布式文件系统上(例如HDFS或者Amazon S3),每次我们只需要将当前需要的数据读进内存。MXNet提供迭代器可以按块读取不同格式的文件。迭代器使用多线程来解码数据,并使用多线程预读取来隐藏文件读取的开销。
训练模块
MXNet实现了常用的优化算法来训练模型。用户只需要提供数据数据迭代器和神经网络的Symbol便可。此外,用户可以提供额外的KVStore来进行分布式的训练。例如下面代码使用分布式异步SGD来训练一个模型,其中每个计算节点使用两快GPU。
import MXNet as mx
model = mx.model.FeedForward(
ctx = [mx.gpu(0), mx.gpu(1)],
symbol = network,
num_epoch = 100,
learning_rate = 0.01,
momentum = 0.9,
wd = 0.00001,
initializer = mx.init.Xavier(factor_type="in", magnitude=2.34))
model.fit(
X = train_iter,
eval_data = val_iter,
kvstore = mx.kvstore.create('dist_async'),
epoch_end_callback = mx.callback.do_checkpoint(