在MXNet中,NDArray 是所有数学计算的核心数据结构。每个NDArray 代表了一个多维的,固定大小的齐次数组。如果你对python的科学计算包Numpy熟悉的话,你会发现mxnet.ndarray与numpy.ndarray在诸多方面十分相似。就像对应的NumPy数据结构,MXNet的NDArray也能够进行命令式计算。
所以你可能会想,为什么不用NumPy呢?MXNet提供了两种引人注目的优势。第一,MXNet的NDArray支持在各种硬件上的快速计算,包括CPU,GPU和多GPU集群;也可以扩展到云中的分布式系统上。第二,MXNet的NDArray惰性执行代码,允许在硬件设备上自动并行执行多种操作。
NDArray是一组同类型数据的集合,例如一个3D空间中的点的坐标值[2, 1, 6]就是一个shape=(3)的一维数组。而一个二维数组如下所示,其第一维度的长度为2,第二维度的长度为3:
[[0, 1, 2]
[3, 4, 5]]
注意:这里的“维度”的使用时经过重载的。当我们说一个2维数组时,我们意思是一个有着两个维度的数组,而不是由两部分组成的数组。
NDArray对象常用的属性有:
- ndarray.shape:数组的维度。是一个表明每个维度的长度的整数元组。对于一个n行m列的矩阵,其shape 值为(n, m)
- ndarry.dtype:一个用于描述元素类型的numpy 对象
- ndarry.size:数组中的元素总个数,与shape 中元素的乘积相等
- ndarray.context:用来表明数组储存的设备,比如cpu() 或者gpu(1)
前提条件
为了完成以下教程,我们需要:
pip install jupyter
- GPUs:教程的部分实现需要用到GPU。如果没有GPU,只用把变量gpu_device 设置为mx.cpu()即可
创建数组
可以通过多种途径创建NDArray
- 我们可以通过array 函数由一般的Python列表或者元组建立数组:
import mxnet as mx
# create a 1-dimensional array with a python list
a = mx.nd.array([1,2,3])
# create a 2-dimensional array with a nested python list
b = mx.nd.array([[1,2,3], [2,3,4]])
{'a.shape':a.shape, 'b.shape':b.shape}
- 或者由一个numpy.ndarray对象建立:
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 选项来指定元素的类型(支持numpy类型),元素类型默认为float32 。
# float32 is used in default
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)
如果我们仅仅知道数组的大小而还不确定元素的具体值。MXNet还提供了多种通过初始化占位符来创建数组的函数。
# create a 2-dimensional array full of zeros with shape (2,3)
a = mx.nd.zeros((2,3))
# create a same shape array full of ones
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))
打印数组
当要访问NDArray的内容时,一般先用asnumpy 函数将NDArray 转换为numpy.ndarray ,Numpy使用如下布局:
- 最后一个坐标轴从左至右打印
- 第二至最后一个坐标轴从上至下打印;
- 其余的也从上至下打印,每一个切片和下一个用空行隔开
b = mx.nd.arange(18).reshape((3,2,3))
b.asnumpy()
基本操作
数组的算术运算符作用于两个数组依次对应的元素。结果被写入到一个新创建的数组。
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)
f.asnumpy()
与Numpy 相似,*用于矩阵对应元素乘法,而矩阵乘法则用dot。
a = mx.nd.arange(4).reshape((2,2))
b = a * a
c = mx.nd.dot(a,a)
print("b: %s, \n c: %s" % (b.asnumpy(), c.asnumpy()))
赋值运算符,例如+= 和*= ,用于将结果写入一个已存在数组而不是新建一个数组。
a = mx.nd.ones((2,2))
b = mx.nd.ones(a.shape)
b += a
b.asnumpy()
索引和切片
切片操作符 [ ] 从0维开始
a = mx.nd.array(np.arange(6).reshape(3,2))
a[1:2] = 1
a[:].asnumpy()
我们也可以通过slice_axis 方法来获得特定维的切片。
d = mx.nd.slice_axis(a, axis=1, begin=1, end=2)
d.asnumpy()
形状修改
使用reshape 可以改变数组的形状,但要保证数组的大小保持不变。
a = mx.nd.array(np.arange(24))
b = a.reshape((2,3,4))
b.asnumpy()
concat 方法将多个数组堆叠在第一维(数组的形状必须一致)。
a = mx.nd.ones((2,3))
b = mx.nd.ones((2,3))*2
c = mx.nd.concat(a,b)
c.asnumpy()
分解
像sum 和mean 这样的函数可以将一个数组分解为标量。
a = mx.nd.ones((2,3))
b = mx.nd.sum(a)
b.asnumpy()
也可以对某一维分解
c = mx.nd.sum_axis(a, axis=1)
c.asnumpy()
广播
广播操作,沿长度为1的轴复制数组的值。以下代码在第1维广播数组:
a = mx.nd.array(np.arange(6).reshape(6,1))
b = a.broadcast_to((6,4)) #
b.asnumpy()
也可以沿多个轴同时广播,以下的例子是沿第1和2维广播:
c = a.reshape((2,1,1,3))
d = c.broadcast_to((2,2,2,3))
d.asnumpy()
广播在执行某些操作时将自动执行,比如用于不同形状数组的* 和 +
a = mx.nd.ones((3,2))
b = mx.nd.ones((1,2))
c = a + b
c.asnumpy()
复制
当将NDArray分配给另一个Python变量时,我们将这个NDArray的引用复制过去了。然而,我们经常需要复制数据,以便我们可以在不覆盖原始值的情况下操作新数组。
a = mx.nd.ones((2,2))
b = a
b is a # will be True
copy 方法对数组和数据进行深拷贝。
b = a.copy()
b is a # will be False
上面的代码申请一个新的NDArray,然后分配给b。当我们不想分配额外的内存时,我们可以使用copyto 方法或用切片操作[]来代替。
b = mx.nd.ones(a.shape)
c = b
c[:] = a
d = b
a.copyto(d)
(c is b, d is b) # Both will be True
进阶
MXNet的NDArray还有一些高级的特性,让mxnet与其他库有所区别。
GPU支持
默认情况下,操作符在CPU上执行。MXNet可以很方便地切换到另一个计算资源,例如GPU(如果可行的话)。资源设备信息存储在ndarray.context。当MXNet以标志位USE_CUDA = 1 编译且存在至少一个英伟达GPU显卡时,我们可以通过使用上下文mx.gpu(0) 或者仅仅使用mx.gpu() 来将所有的的计算运行于GPU 0。如果有两个以上GPU,第二个GPU由mx.gpu(1) 表示, 以此类推。
gpu_device=mx.gpu() # Change this to mx.cpu() in absence of GPUs.
def f():
a = mx.nd.ones((100,100))
b = mx.nd.ones((100,100))
c = a + b
print(c)
# in default mx.cpu() is used
f()
# change the default context to the first GPU
with mx.Context(gpu_device):
f()
我们也可以在创建数组时明确地指定上下文:
a = mx.nd.ones((100, 100), gpu_device)
a
通常MXNet的计算需要两个数组位于同一个设备,有多种方法可以在不同设备之间拷贝数据:
a = mx.nd.ones((100,100), mx.cpu())
b = mx.nd.ones((100,100), gpu_device)
c = mx.nd.ones((100,100), gpu_device)
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,正如你所用其他任何Python对象。NDArray是与pickle模块兼容的。
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()
第二种方法是通过save和load方法直接以二进制的格式存入磁盘。除了NDArray,我们也可以load/save单个NDArray或者列表(list):
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
或者词典(dict):
d = {'a':a, 'b':b}
mx.nd.save("temp.ndarray", d)
c = mx.nd.load("temp.ndarray")
c
load/save在两方面优于pickle模块:
1.使用Python接口保存的数据可以被用于其他的语言联编(Lanuage Binding)。例如我们可以在Python中保存数据:
a = mx.nd.ones((2, 3))
mx.nd.save("temp.ndarray", [a,])
接着我们可以在R语言中读取它:
a <- mx.nd.load("temp.ndarray")
as.array(a[[1]])
## [,1] [,2] [,3]
## [1,] 1 1 1
## [2,] 1 1 1
2.如果建立了分布式文件系统例如Amazon S3或Hadoop HDFS,我们可以直接在其load和save。
mx.nd.save('s3://mybucket/mydata.ndarray', [a,]) # if compiled with USE_S3=1
mx.nd.save('hdfs///users/myname/mydata.bin', [a,]) # if compiled with USE_HDFS=1
惰性计算与自动并行化
MXNet使用惰性计算以达到更好的性能。当我们在Python中运行a=b+1时,Python线程仅仅就是将操作符送入后端引擎然后返回。这种优化有两点好处:
- 一旦前一个操作符已经送出,python主线程可以继续执行其他计算。这对于有着巨大开销的前端语言十分有用。
- 使得后端引擎更加容易地探索深度优化,例如我们马上要讨论的自动并行化。
后端引擎能分解数据的相关性并保持计算的正确性。这些都对前端使用者透明。我们可以明确地使用结果数组的wait_to_read 方法来等待计算完成。从数组复制数据到其他包(例如Numpy)的操作会隐式调用wait_to_read 。
import time
def do(x, n):
"""push computation into the backend engine"""
return [mx.nd.dot(x,x) for i in range(n)]
def wait(x):
"""wait until all results are available"""
for y in x:
y.wait_to_read()
tic = time.time()
a = mx.nd.ones((1000,1000))
b = do(a, 50)
print('time for all computations are pushed into the backend engine:\n %f sec' % (time.time() - tic))
wait(b)
print('time for all computations are finished:\n %f sec' % (time.time() - tic))
除了分析数据读写的相关性,后端引擎能将非相关的计算并行。例如如下代码:
a = mx.nd.ones((2,3))
b = a + 1
c = a + 2
d = b * c
第二和第三条语句可以并行执行。以下示例首先运行于CPU然后运行与GPU:
n = 10
a = mx.nd.ones((1000,1000))
b = mx.nd.ones((6000,6000), gpu_device)
tic = time.time()
c = do(a, n)
wait(c)
print('Time to finish the CPU workload: %f sec' % (time.time() - tic))
d = do(b, n)
wait(d)
print('Time to finish both CPU/GPU workloads: %f sec' % (time.time() - tic))
现在我们同时下放所有工作量。后端引擎会尽量并行CPU和GPU计算。
tic = time.time()
c = do(a, n)
d = do(b, n)
wait(c)
wait(d)
print('Both as finished in: %f sec' % (time.time() - tic))