机器学习库初探之MXnet

什么是MXnet?

这是自 xgboost, cxxnet, minerva 以来集合DMLC几乎所有开发者 (李沐和陈天奇等各路英雄豪杰) 力量的一个机器学习项目。MXNet名字源于”Mix and Maximize”。MXnet 的目标就是希望把 cxxnet 这样强调性能静态优化的 C++ 库和灵活的 NDArray 有机结合在一起。做包含cxxnet 的静态优化,却又可以像 minerva, theano, torch 那样进行灵活扩展的深度学习库。

与其他工具相比,MXnet 结合了符号语言和过程语言的编程模型,并试图最大化各自优势,利用统一的执行引擎进行自动多 GPU 并行调度优化。不同的编程模型有各自的优势,以往的深度学习库往往着重于灵活性,或者性能。MXNet 通过融合的方式把各种编程模型整合在一起,并且通过统一的轻量级运行引擎进行执行调度。使得用户可以直接复用稳定高效的神经网络模块,并且可以通过 Python 等高级语言进行快速扩展。

MXNet 由 dmlc/cxxnet, dmlc/minerva 和 Purine2 的作者发起,融合了Minerva 的动态执行,cxxnet 的静态优化和 Purine2 的符号计算等思想,直接支持基于Python 的 parameter server 接口,使得代码可以很快向分布式进行迁移。每个模块都进行清晰设计,使得每一部分本身都具有被直接利用的价值。C 接口和静态/动态 Library 使得对于新语言的扩展更加容易,目前支持C++和python 2/3 ,接下来相信会有更多语言支持,并方便其他工具增加深度学习功能。

MXnet的关键特性

轻量级调度引擎

在数据流调度的基础上引入了读写操作调度,并且使得调度和调度对象无关,用以直接有机支持动态计算和静态计算的统一多 GPU 多线程调度,使得上层实现更加简洁灵活。

支持符号计算

MXNet 支持基于静态计算流图符号计算。计算流图不仅使设计复杂网络更加简单快捷,而且基于计算流图,MXNet 可以更加高效得利用内存。同时进一步优化了静态执行的规划,内存需求比原本已经省的 cxxnet 还要少。

混合执行引擎

相比 cxxnet 的全静态执行,minerva 的全动态执行。MXNet采用动态静态混合执行引擎,可以把 cxxnet 静态优化的效率带和 ndarray 动态运行的灵活性结合起来。把高效的 c++ 库更加灵活地和 Python 等高级语言结合在一起。

弹性灵活

在 MShadow C++ 表达式模板的基础上,符号计算和 ndarray 使在 Python 等高级语言内编写优化算法,损失函数和其他深度学习组件并高效无缝支持 CPU/GPU 成为可能。用户无需关心底层实现,在符号和 NDArray 层面完成逻辑即可进行高效的模型训练和预测。

云计算友好

所有数据模型可以从 S3/HDFS/Azure 上直接加载训练。

代码简洁高效

大量使用 C++11 特性,使 MXNet 利用最少的代码实现尽可能最大的功能。用约 11k 行 C++ 代码 (加上注释 4k 行) 实现了以上核心功能。

开源用户和设计文档

MXNet 提供了非常详细的用户文档和设计文档以及样例。所有的代码都有详细的文档注释。并且会持续更新代码和系统设计细节,希望对于广大深度学习系统开发和爱好者有所帮助。

MXnet在公司内的实践

社区活跃度

DMLC (Distributed (Deep) Machine Learning Community) 是国内最大的开源分布式机器学习项目2。DMLC 的相关代码直接托管在 GitHub 中,并采用 Apache2.0 协议进行维护。

MXnet的安装

推荐已编译的方式安装 MXnet,安装的过程分为2步3:

  1. 从 C++ 源码编译共享库(libmxnet.so for linux,libmxnet.dylib for osx,libmxnet.dll for windows);
  2. 安装语言包;

构建共享库依赖

目标是构建共享库文件。

最小构建需求:

  • 最新的支持C++ 11的C++编译器,比如g++ >= 4.8,clang
  • 一份BLAS库,比如libblas,atlas,openblas,或者 intel mkl

可选库:

  • CUDA Toolkit >= v7.0 以运行 nvidia GPUs (需要 GPU 支持 Compute Capability >= 2.0)
  • CUDNN 加速 GPU computation (only CUDNN 3 is supported)
  • opencv 进行图像的分段 (如果编译安装,需要cmake)

Ubuntu/Debian上构建

安装依赖:

sudo apt-get update
sudo apt-get install -y build-essential git libatlas-base-dev libopencv-dev

构建MXnet:

git clone --recursive https://github.com/dmlc/mxnet
cd mxnet; make -j$(nproc)

OSX上的构建

安装依赖:

brew update
brew tap homebrew/science
brew info opencv
brew install opencv

构建MXnet:

git clone --recursive https://github.com/dmlc/mxnet
cd mxnet; cp make/osx.mk ./config.mk; make -j$(sysctl -n hw.ncpu)

安装python语言包

进入源码目录编译好的python子目录安装python语言包即可

cd ./python/
sudo python setup.py install

更详细的安装文档参见官网手册

MXnet基本概念和操作

NDArray

多维的数据结构,提供在 cpu 或者 gpu 上进行矩阵运算和张量计算,能够自动并行计算

NDArray 是 MXnet 中最底层的计算单元,与 numpy.ndarray 非常相似,但是也有 2 点不同的特性:

  • 支持多设备
  • 所有的操作可以在不同的设备上运行,包括 cpu 和 gpu。

Python

>>> import mxnet as mx
>>> a = mx.nd.empty((2, 3)) # 在cpu0上创建一个2X3的矩阵
>>> b = mx.nd.empty((2, 3), mx.gpu()) # 在gpu0上创建一个2X3的矩阵
>>> c = mx.nd.empty((2, 3), mx.gpu(2)) # 在gpu2上创建一个2X3的矩阵
>>> c.shape # 维度
(2L, 3L)
>>> c.context # 设备信息
gpu(2)

# 其他的初始化方式
>>> a = mx.nd.zeros((2, 3)) # 创建2X3的全0矩阵
>>> b = mx.nd.ones((2, 3))  # 创建2X3的全1矩阵
>>> b[:] = 2 # 所有元素赋值为2

# 不同的设备之间进行数据拷贝
>>> a = mx.nd.ones((2, 3))
>>> b = mx.nd.zeros((2, 3), mx.gpu())
>>> a.copyto(b) # 从cpu拷贝数据到gpu

# NDArray转换为numpy.ndarray
>>> a = mx.nd.ones((2, 3))
>>> b = a.asnumpy()
>>> type(b)
<type 'numpy.ndarray'>
>>> print b
[[ 1.  1.  1.]
 [ 1.  1.  1.]]

# numpy.ndarray转换为NDArray
>>> import numpy as np
>>> a = mx.nd.empty((2, 3))
>>> a[:] = np.random.uniform(-0.1, 0.1, a.shape)
>>> print a.asnumpy()
[[-0.06821112 -0.03704893  0.06688045]
 [ 0.09947646 -0.07700162  0.07681718]]

# NDArray基本运算
>>> a = mx.nd.ones((2, 3)) * 2
>>> b = mx.nd.ones((2, 3)) * 4
>>> print b.asnumpy()
[[ 4.  4.  4.]
 [ 4.  4.  4.]]
>>> c = a + b   # 对应元素求和
>>> print c.asnumpy()
[[ 6.  6.  6.]
 [ 6.  6.  6.]]
>>> d = a * b   # 对应元素求积
>>> print d.asnumpy()
[[ 8.  8.  8.]
 [ 8.  8.  8.]]

# 不同设备上的NDArray需要移动到一起才能计算
>>> a = mx.nd.ones((2, 3)) * 2
>>> b = mx.nd.ones((2, 3), mx.gpu()) * 3
>>> c = a.copyto(mx.gpu()) * b
>>> print c.asnumpy()
[[ 6.  6.  6.]
 [ 6.  6.  6.]]

# 数据的导出与载入
# 1. 通过pickle导出与载入数据
>>> import mxnet as mx
>>> import pickle as pkl

>>> a = mx.nd.ones((2, 3)) * 2
>>> data = pkl.dumps(a)
>>> b = pkl.loads(data)
>>> print b.asnumpy()
[[ 2.  2.  2.]
 [ 2.  2.  2.]]

# 2. 直接保存为二进制文件 
>>> a = mx.nd.ones((2,3))*2
>>> b = mx.nd.ones((2,3))*3
>>> mx.nd.save('mydata.bin', [a, b])
>>> c = mx.nd.load('mydata.bin')
>>> print c[0].asnumpy()
[[ 2.  2.  2.]
 [ 2.  2.  2.]]
>>> print c[1].asnumpy()
[[ 3.  3.  3.]
 [ 3.  3.  3.]]

# 直接保存到分布式文件系统上(s3或hdfs) 
>>> mx.nd.save('s3://mybucket/mydata.bin', [a,b])
>>> mx.nd.save('hdfs///users/myname/mydata.bin', [a,b])
  • 自动并行计算

不同的操作自动进行并行计算。

Python

a = mx.nd.ones((2,3))
b = a
c = a.copyto(mx.cpu())
a += 1
b *= 3
c *= 3

a += 1可与c *= 3并行计算,因为在不同的设备上,但是a += 1b *= 3只能相继执行。

Symbol

Symbol使得非常容易定义神经网络,并且能自动求导 以下的范例创建了一个 2 层的感知器网络:

Python

>>> import mxnet as mx
>>> net = mx.symbol.Variable('data')
>>> net = mx.symbol.FullyConnected(data=net, name='fc1', num_hidden=128)
>>> net = mx.symbol.Activation(data=net, name='relu1', act_type="relu")
>>> net = mx.symbol.FullyConnected(data=net, name='fc2', num_hidden=64)
>>> net = mx.symbol.SoftmaxOutput(data=net, name='out')
>>> type(net)
<class 'mxnet.symbol.Symbol'>

每一个 Symbol 可以绑定一个名字,Variable 通常用来定义输入,其他的 Symbol 有一个参数data以一个Symbol 类型作为输入数据,另外还有其他的超参数num_hidden(隐藏层的神经元数目),act_type(激活函数的类型)。

Symbol 的作用可以被简单的看成是实现了一个函数,函数的参数名称自动生成,可以通过以下的方式查看:

Python

>>> net.list_arguments()
['data', 'fc1_weight', 'fc1_bias', 'fc2_weight', 'fc2_bias', 'out_label']

我们也可以明确指定这些自动生成的参数的名字:

Python

>>> net = mx.symbol.Variable('data')
>>> w = mx.symbol.Variable('myweight')
>>> net = mx.symbol.FullyConnected(data=net, weight=w, name='fc1', num_hidden=128)
>>> net.list_arguments()
['data', 'myweight', 'fc1_bias']

Symbol 可以组合之后,在传入全连接中:

Python

>>> lhs = mx.symbol.Variable('data1')
>>> rhs = mx.symbol.Variable('data2')
>>> net = mx.symbol.FullyConnected(data=lhs + rhs, name='fc1', num_hidden=128)
>>> net.list_arguments()
['data1', 'data2', 'fc1_weight', 'fc1_bias']

Symbol 也可以被随后的操作替换:

Python

>>> net = mx.symbol.Variable('data')
>>> net = mx.symbol.FullyConnected(data=net, name='fc1', num_hidden=128)
>>> net2 = mx.symbol.Variable('data2')
>>> net2 = mx.symbol.FullyConnected(data=net2, name='net2', num_hidden=128)
>>> composed_net = net(data=net2, name='compose')
>>> composed_net.list_arguments()
['data2', 'net2_weight', 'net2_bias', 'compose_fc1_weight', 'compose_fc1_bias']

一旦定义好了 Symbol,只需要指定输入数据的维度,就可以推算出各级中间参数的维度:

Python

>>> net = mx.symbol.Variable('data')
>>> net = mx.symbol.FullyConnected(data=net, name='fc1', num_hidden=10)
>>> arg_shape, out_shape, aux_shape = net.infer_shape(data=(100, 100))
>>> dict(zip(net.list_arguments(), arg_shape))
{'data': (100, 100), 'fc1_weight': (10, 100), 'fc1_bias': (10,)}
>>> out_shape
[(100, 10)]

接下来通过绑定变量,就可以执行实际的运算了:

Python

>>> # 定义计算图
>>> A = mx.symbol.Variable('A')
>>> B = mx.symbol.Variable('B')
>>> C = A * B
>>> a = mx.nd.ones(3) * 4
>>> b = mx.nd.ones(3) * 2
>>> # 绑定变量到Symbol
>>> c_exec = C.bind(ctx=mx.cpu(), args={'A' : a, 'B': b})
>>> # 进行前向计算
>>> c_exec.forward()
>>> c_exec.outputs[0].asnumpy()
[ 8.  8.  8.]

KVStore

KVStore 实现了在多个运算器之间,或者在多台计算机之间的数据同步 通过create可以对 kvstore 进行简单的初始化:

Python

>>> kv = mx.kv.create('local') # 创建一个本地的kvstore
>>> shape = (2,3)
>>> kv.init(3, mx.nd.ones(shape)*2)
>>> a = mx.nd.zeros(shape)
>>> kv.pull(3, out = a)
>>> print a.asnumpy()
[[ 2.  2.  2.]
 [ 2.  2.  2.]]

初始化后,可以通过相同的 key 进行数值更新:

Python

>>> kv.push(3, mx.nd.ones(shape)*8)
>>> kv.pull(3, out = a) # 取出值
>>> print a.asnumpy()
[[ 8.  8.  8.]
 [ 8.  8.  8.]]

push的数据可以在任何设备上,此外,可以在同一个 key 上传递多个值,KVStore 会对多个值求和,push聚合后的值:

Python

>>> gpus = [mx.gpu(i) for i in range(4)]
>>> b = [mx.nd.ones(shape, gpu) for gpu in gpus]
>>> kv.push(3, b)
>>> kv.pull(3, out = a)
>>> print a.asnumpy()
[[ 4.  4.  4.]
 [ 4.  4.  4.]]

KVStore 对每次push进来的值做的默认行为是ASSIGN,这个行为可以被自定义的行为替换:

Python

>>> def update(key, input, stored):
>>>     print "update on key: %d" % key
>>>     stored += input * 2
>>> kv._set_updater(update)
>>> kv.pull(3, out=a)
>>> print a.asnumpy()
[[ 4.  4.  4.]
 [ 4.  4.  4.]]
>>> kv.push(3, mx.nd.ones(shape))
update on key: 3
>>> kv.pull(3, out=a)
>>> print a.asnumpy()
[[ 6.  6.  6.]
 [ 6.  6.  6.]]

同push类似,通过一次调用,我们也可以将值同时pull到多个设备上:

Python

>>> b = [mx.nd.ones(shape, gpu) for gpu in gpus]
>>> kv.pull(3, out = b)
>>> print b[1].asnumpy()
[[ 6.  6.  6.]
 [ 6.  6.  6.]]

除了单个 key-value 的存储,KVStrore 还提供了批量的接口:

Python

# 针对单个设备
>>> keys = [5, 7, 9]
>>> kv.init(keys, [mx.nd.ones(shape)]*len(keys))
>>> kv.push(keys, [mx.nd.ones(shape)]*len(keys))
update on key: 5
update on key: 7
update on key: 9
>>> b = [mx.nd.zeros(shape)]*len(keys)
>>> kv.pull(keys, out = b)
>>> print b[1].asnumpy()
[[ 3.  3.  3.]
 [ 3.  3.  3.]]

# 针对多个设备 
>>> b = [[mx.nd.ones(shape, gpu) for gpu in gpus]] * len(keys)
>>> kv.push(keys, b)
update on key: 5
update on key: 7
update on key: 9
>>> kv.pull(keys, out = b)
>>> print b[1][1].asnumpy()
[[ 11.  11.  11.]
 [ 11.  11.  11.]]

MXnet体验

以下借助 MXnet 实现了一个简单的单变量线性回归程序:

Python

import mxnet as mx
import numpy as np
import matplotlib.pyplot as plt

# 定义输入数据
X_data = np.linspace(-1, 1, 100)
noise = np.random.normal(0, 0.5, 100)
y_data = 5 * X_data + noise

# Plot 输入数据
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.scatter(X_data, y_data)

# 定义mxnet变量
X = mx.symbol.Variable('data')
Y = mx.symbol.Variable('softmax_label')

# 定义网络
Y_ = mx.symbol.FullyConnected(data=X, num_hidden=1, name='pre')
loss = mx.symbol.LinearRegressionOutput(data=Y_, label=Y, name='loss')

# 定义模型
model = mx.model.FeedForward(
            ctx=mx.cpu(),
            symbol=loss,
            num_epoch=100,
            learning_rate=0.001,
            numpy_batch_size=1
        )

# 训练模型
model.fit(X=X_data, y=y_data)

# 预测
prediction = model.predict(X_data)
lines = ax.plot(X_data, prediction, 'r-', lw=5)
plt.show()

总结

MXnet 是个深度学习的框架,支持从单机到多GPU、多集群的计算能力。MXnet 真正厉害之处在于可以结合命令式编程和符号式编程两种风格。此外,MXnet 安装相对简单,社区活跃,文档虽然还不完善,好在易读易懂,代码也相对丰富,还是很容易上手的。

参考资料

  1. MXNet技术特性
  2. DMLC对于机器学习和系统开发者意味着什么
  3. Installation Guide Page

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

2 条评论
登录 后参与评论

相关文章

来自专栏全球人工智能的专栏

TensorFlow 的 c ++ 实践及各种坑!

本文重点介绍 tensorflow C++ 服务化过程中实现方式及遇到的各种问题。

2.2K1
来自专栏C语言及其他语言

【每日一题】尼科彻斯定理

题目描述 验证尼科彻斯定理,即:任何一个正整数的立方都可以写成一串连续奇数的和。 输入 任一正整数 输出 该数的立方分解为一串连续奇数的和 样例输入 13 样例...

2979
来自专栏企鹅号快讯

人生苦短,我用python

基础书籍推荐 父与子的编程之旅 这本书说的都是一些基本的概念,告诉我们编程到底是怎么一回事,初步了解Python的世界。 笨方法学Python 前面很适合新手学...

1847
来自专栏转载gongluck的CSDN博客

[C++]:A*——A Star算法简介

A*算法 求最优解 算法一直维护两个表: Open和Close 将起点S加入Open中 将所有S可到达的点(障碍物以及位于Close表中的点均看成不可达...

3066
来自专栏青玉伏案

算法与数据结构(八) AOV网的关键路径(Swift版)

上篇博客我们介绍了AOV网的拓扑序列,请参考《数据结构(七) AOV网的拓扑排序(Swift面向对象版)》。拓扑序列中包括项目的每个结点,沿着拓扑序列将项目进行...

1908
来自专栏安恒信息

基于大数据分析的异常检测方法及其思路实例

1 概述 随着人类社会信息化程度的不断深入,信息系统产生的数据也在呈几何级数增长。对这些数据的深入分析可以得到很多有价值的信息。由于数据量太大以及数据属性的多样...

3466
来自专栏曾子骄的专栏

Tensorflow c++ 实践及各种坑

众所周知,python 在开发效率、易用性上有着巨大的优势,但作为一个解释性语言,在性能方面还是存在比较大的缺陷,在各类AI服务化过程中,采用 python 作...

7.3K4
来自专栏ATYUN订阅号

【业界】Booking.com如何使用Kubernetes进行机器学习

1483
来自专栏我是极客人

图片去霾算法实践】NDK下二维数组的传递

最近看到了一篇关于图片“去霾算法”的文章,一下子就有了兴趣,所以想着能不能实现。由于数学能力捉急,无法理解文章的思想和相关论文。于是在Github上找到了相关的...

653
来自专栏程序员互动联盟

【编程基础第七讲】如何编写有界面的程序?

存在问题: 好多小伙伴都有一个疑问,我我们学编程貌似都是看输出,怎么才能搞个像window上程序带个又界面的,眼见为实嘛 解决方案: 很多群友都在问学习了C语言...

3027

扫码关注云+社区