NDArray教程
MXNet中一个主要的对象就是mxnet.ndarray(缩写mxnet.nd)中的多维数组。如果你对python的科学计算包Numpy熟悉的话,你会发现mxnet.ndarray与numpy.ndarray在诸多方面十分相似。源网址:CPU/GPU Array Manipulation
基础
多维数组是一组同类型数据的集合,例如一个3D空间中的点的坐标值[1, 2, 3]就是一个长度为3的一维数组。而一个二维数组如下所示,其第一纬度的长度为2,第二纬度的长度为3:
[ [0, 1, 2]
[ 3, 4, 5] ]
数组类的名称为NDArray。NDArray对象常用的属性有:
l ndarray.shape:数组的纬度。是一个表明每个纬度的长度的整数元组。对于一个n行m列的矩阵,其shape值为(n, m)。
l ndarry.dtype:一个用于描述元素类型的numpy对象。
l ndarry.size:数组中的元素总个数,与shape中元素的乘积相等。
l ndarray.context:用来表明数组储存的设备。设备可以是CPU或第i个GPU。
建立数组
可以通过多种途径建立数组。例如,我们可以通过array函数由一般的Python列表或者元组建立数组:
import mxnet as mx
# create a1-dimensional array with a python list
a = mx.nd.array([1,2,3])
# create a2-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}
输出:
{'a.shape': (3L,), 'b.shape': (2L, 3L)}
或者由一个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}
输出:
{'a.shape': (3L, 5L)}
我们可以通过dtype选项来指定元素的类型(支持numpy类型),元素类型默认为float32。
# 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)
输出:
(numpy.float32, numpy.int32, numpy.float16)
如果我们仅仅知道数组的大小而还不确定元素的具体值。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))
打印数组
为了打印数组,我们通常首先用asnumpy函数将NDArray转换为numpy.ndarray。Numpy使用如下布局:
l 最后一个坐标轴从左至右打印;
l 第二至最后一个坐标轴从上至下打印;
l 其余的也从上至下打印,每一个切片和下一个用空行隔开
a= mx.nd.ones((2,3))
a.asnumpy()
b= mx.nd.ones((3,3,3))
b.asnumpy()
输出
array([[ 1., 1., 1.],
[ 1., 1., 1.]], dtype=float32)
array([[[ 1., 1., 1.],
[ 1., 1., 1.],
[ 1., 1., 1.]],
[[ 1., 1., 1.],
[ 1., 1., 1.],
[ 1., 1., 1.]],
[[ 1., 1., 1.],
[ 1., 1., 1.],
[ 1., 1., 1.]]], dtype=float32)
基础操作
数组的算术运算符作用于两个数组依次对应的元素。结果被写入到一个新创建的数组。
a = mx.nd.ones((2,3))
b = mx.nd.ones((2,3))
# elementwise plus
c = a + b
# elementwise minus
d =- c
# elementwise powand sin, and then transpose
e = mx.nd.sin(c**2).T
# elementwise max
f = mx.nd.maximum(a,c)
f.asnumpy()
输出
array([[ 2., 2., 2.],
[ 2., 2., 2.]], dtype=float32)
与Numpy相似,*用于矩阵对应元素乘法,而矩阵乘法则用dot。
a = mx.nd.ones((2,2))
b = a * a
c = mx.nd.dot(a,a)
{'b':b.asnumpy(), 'c':c.asnumpy()}
输出
{'b': array([[1., 1.],
[ 1., 1.]], dtype=float32), 'c': array([[ 2., 2.],
[ 2., 2.]], dtype=float32)}
赋值运算符,例如+=和*=,用于将结果写入一个已存在数组而不是新建一个数组。
a = mx.nd.ones((2,2))
b = mx.nd.ones(a.shape)
b += a
b.asnumpy()
输出
array([[ 2., 2.],
[ 2., 2.]], dtype=float32)
索引与切片
切片操作符 [ ] 从0维开始
a = mx.nd.array(np.arange(6).reshape(3,2))
a[1:2] =1
a[:].asnumpy()
输出
array([[ 0., 1.],
[ 1., 1.],
[ 4., 5.]], dtype=float32)
我们也可以通过slice_axis方法来获得特定维的切片。
d = mx.nd.slice_axis(a,axis=1, begin=1, end=2)
d.asnumpy()
输出
array([[ 1.],
[ 1.],
[ 5.]], dtype=float32)
形状修改
数组的形状可以改变,但要保证数组的大小保持不变。
a = mx.nd.array(np.arange(24))
b = a.reshape((2,3,4))
b.asnumpy()
输出
array([[[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]],
[[ 12., 13., 14., 15.],
[ 16., 17., 18., 19.],
[ 20., 21., 22., 23.]]], dtype=float32)
concatenate方法将多个数组堆叠在第一维(数组的形状必须一致)。
a = mx.nd.ones((2,3))
b = mx.nd.ones((2,3))*2
c = mx.nd.concatenate([a,b])
c.asnumpy()
输出
array([[ 1., 1., 1.],
[ 1., 1., 1.],
[ 2., 2., 2.],
[ 2., 2., 2.]], dtype=float32)
分解
我们可以将一个数组分解为标量。
a = mx.nd.ones((2,3))
b = mx.nd.sum(a)
b.asnumpy()
输出
array([ 6.],dtype=float32)
或者对某一维分解
c = mx.nd.sum_axis(a, axis=1)
c.asnumpy()
输出:
array([ 3., 3.], dtype=float32)
传播
我们也可以通过赋值传播数组。以下代码在第1维传播数组:
a = mx.nd.array(np.arange(6).reshape(6,1))
b = a.broadcast_to((6,2)) #
b.asnumpy()
输出
array([[ 0., 0.],
[ 1., 1.],
[ 2., 2.],
[ 3., 3.],
[ 4., 4.],
[ 5., 5.]], dtype=float32)
或者沿第1和2维传播:
c = a.reshape((2,1,1,3))
d = c.broadcast_to((2,2,2,3))
d.asnumpy()
输出
array([[[[ 0., 1., 2.],
[ 0., 1., 2.]],
[[ 0., 1., 2.],
[ 0., 1., 2.]]],
[[[ 3., 4., 5.],
[ 3., 4., 5.]],
[[ 3., 4., 5.],
[ 3., 4., 5.]]]], dtype=float32)
传播应用于操作符,例如*和+。
a = mx.nd.ones((3,2))
b = mx.nd.ones((1,2))
c = a + b
c.asnumpy()
输出
array([[ 2., 2.],
[ 2., 2.],
[ 2., 2.]], dtype=float32)
赋值
在一般赋值中,数据没有被复制。
a = mx.nd.ones((2,2))
b = a
b is a
输出
True
函数的参数传递也是一样
deff(x):
return x
a is f(a)
输出
True
copy方法完成数组及其数据的深度拷贝
b = a.copy()
b is a
输出
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)
输出
(True, 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)表示。
deff():
a = mx.nd.ones((100,100))
b = mx.nd.ones((100,100))
c = a + b
print(c)
# in defaultmx.cpu() is used
f()
# change thedefault context to the first GPU
with mx.Context(mx.gpu()):
f()
输出
我们也可以在创建数组时明确地指定上下文:
a = mx.nd.ones((100, 100), mx.gpu(0))
a
输出
通常MXNet的计算需要两个数组位于同一个设备。而有多种方法可以在不同设备之间拷贝数据:
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}
输出
{'d':
序列化从/至(分布式)文件系统
有两种简便的方法可以保存数据到(从读取数据)磁盘。第一种方法使用piickle.NDArray,与pickle模块兼容。
importpickleaspkl
a = mx.nd.ones((2, 3))
# pack and thendump into disk
data = pkl.dumps(a)
pkl.dump(data, open('tmp.pickle', 'wb'))
# load from diskand then unpack
data = pkl.load(open('tmp.pickle', 'rb'))
b = pkl.loads(data)
b.asnumpy()
输出
array([[ 1., 1., 1.],
[ 1., 1., 1.]], dtype=float32)
第二种方法是通过save和load方法直接以二进制的格式存入磁盘。除了NDArray,我们也可以load/save列表(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
输出
{'a':
load/save在两方面优于pickle模块:
1.使用Python接口保存的数据可以被用于其他的语言联编(Lanuage Binding)。例如我们可以在Python中保存数据:
a = mx.nd.ones((2, 3))
mx.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线程仅仅就是将操作符送入后端引擎然后返回。这种优化有两点好处:
1.一旦前一个操作符已经送出,python主线程可以继续执行其他计算。这对于有着巨大开销的前端语言十分有用。
2.使得后端引擎更加容易地探索深度优化,例如我们马上要讨论的自动并行化。
后端引擎能分解数据的相关性并保持计算的正确性。这些都对前端使用者透明。我们可以明确地使用结果数组的wait_to_read方法来等待计算完成。从数组复制数据到其他包(例如Numpy)的操作会隐式调用wait_to_read。
# @@@AUTOTEST_OUTPUT_IGNORED_CELL
importtime
defdo(x, n):
"""pushcomputation into the backend engine"""
return [mx.nd.dot(x,x) for i inrange(n)]
defwait(x):
"""waituntil 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 allcomputations are pushed into the backend engine:\n %f sec'% (time.time() - tic))
wait(b)
print('time for allcomputations are finished:\n %f sec'% (time.time() - tic))
输出
time for allcomputations are pushed into the backend engine:
0.001089 sec
time for allcomputations are finished:
5.398588 sec
除了分析数据读写的相关性,后端引擎能将非相关的计算并行。例如如下代码:
a = mx.nd.ones((2,3))
b = a +1
c = a +2
d = b * c
第二和第三条语句可以并行执行。以下示例首先运行于CPU然后运行与GPU:
# @@@AUTOTEST_OUTPUT_IGNORED_CELL
n =10
a = mx.nd.ones((1000,1000))
b = mx.nd.ones((6000,6000), mx.gpu())
tic = time.time()
c = do(a, n)
wait(c)
print('Time to finishthe CPU workload: %f sec'% (time.time() - tic))
d = do(b, n)
wait(d)
print('Time to finishboth CPU/CPU workloads: %f sec'% (time.time() - tic))
输出:
Time to finish theCPU workload: 1.089354 sec
Time to finish bothCPU/CPU workloads: 2.663608 sec
现在我们同时下放所有工作量。后端引擎会尽量并行CPU和GPU计算。
# @@@ AUTOTEST_OUTPUT_IGNORED_CELL
tic = time.time()
c = do(a, n)
d = do(b, n)
wait(c)
wait(d)
print('Both asfinished in: %f sec'% (time.time() - tic))
输出
Both as finished in: 1.543902 sec
目前情况
我们尽量保持NDArray API与numpy一致。但是还未完全与numpy兼容。在此,我们总结了一些我们期望在近期修改的主要区别。我们同样也欢迎任何贡献。
l 切片与索引
Ø NDArray每次只能在一个维度上切片,也就是说我们不能使用x[:,1]来切片两个维度。
Ø 只支持连续的索引值,我们不能执行x[1:2:3]操作。
Ø 不支持布尔索引,例如x[y==1]。
l 缺少分解函数例如max,min…