MXNet官方文档中文版教程(2):GPUCPU张量计算(NDArray)

文档英文原版参见NDArray - Imperative tensor operations on CPU/GPU

在MXNet中,NDArray 是所有数学计算的核心数据结构。每个NDArray 代表了一个多维的,固定大小的齐次数组。如果你对python的科学计算包Numpy熟悉的话,你会发现mxnet.ndarraynumpy.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()
分解

summean 这样的函数可以将一个数组分解为标量。

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线程仅仅就是将操作符送入后端引擎然后返回。这种优化有两点好处:

  1. 一旦前一个操作符已经送出,python主线程可以继续执行其他计算。这对于有着巨大开销的前端语言十分有用。
  2. 使得后端引擎更加容易地探索深度优化,例如我们马上要讨论的自动并行化。

后端引擎能分解数据的相关性并保持计算的正确性。这些都对前端使用者透明。我们可以明确地使用结果数组的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))
  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值