数据操作

在深度学习中,我们通常会频繁地对数据进行操作。作为动手学深度学习的基础,本节将介绍如何对内存中的数据进行操作。

在 MXNet 中,NDArray 是存储和变换数据的主要工具。如果你之前用过 NumPy,你会发现 NDArray 和 NumPy 的多维数组非常类似。然而,NDArray 提供诸如 GPU 计算和自动求导在内的更多功能。这些都使得 NDArray 更加适合深度学习。

创建 NDArray

我们先介绍 NDArray 的最基本功能。如果你对我们用到的数学操作不是很熟悉,可以参阅附录中 “数学基础” 一节。

首先从 MXNet 导入 ndarray 模块。这里的 ndndarray 的缩写形式。

In [1]:
from mxnet import nd

然后我们用 arange 函数创建一个行向量。

In [2]:
x = nd.arange(12)
x
Out[2]:

[  0.   1.   2.   3.   4.   5.   6.   7.   8.   9.  10.  11.]
<NDArray 12 @cpu(0)>

其返回一个 NDArray 实例,里面一共包含从 0 开始的 12 个连续整数。从打印 x 时显示的属性 <NDArray 12 @cpu(0)> 可以看到,它是长度为 12 的一维数组,且被创建在 CPU 主内存上,CPU 里面的 0 没有特别的意义。

我们可以通过 shape 属性来获取 NDArray 实例形状。

In [3]:
x.shape
Out[3]:
(12,)

我们也能够通过 size 属性得到 NDArray 实例中元素(element)的总数。

In [4]:
x.size
Out[4]:
12

下面使用 reshape 函数把向量 x 的形状改为(3,4),也就是一个 3 行 4 列的矩阵。除了形状改变之外,x 中的元素保持不变。

In [5]:
x = x.reshape((3, 4))
x
Out[5]:

[[  0.   1.   2.   3.]
 [  4.   5.   6.   7.]
 [  8.   9.  10.  11.]]
<NDArray 3x4 @cpu(0)>

注意 x 属性中的形状发生了变化。上面 x.reshape((3, 4)) 也可写成 x.reshape((-1, 4))x.reshape((3, -1))。由于 x 的元素个数是已知的,这里的 -1 是能够通过元素个数和其他维度的大小推断出来的。

接下来,我们创建一个各元素为 0,形状为(2,3,4)的张量。实际上,之前创建的向量和矩阵都是特殊的张量。

In [6]:
nd.zeros((2, 3, 4))
Out[6]:

[[[ 0.  0.  0.  0.]
  [ 0.  0.  0.  0.]
  [ 0.  0.  0.  0.]]

 [[ 0.  0.  0.  0.]
  [ 0.  0.  0.  0.]
  [ 0.  0.  0.  0.]]]
<NDArray 2x3x4 @cpu(0)>

类似地,我们可以创建各元素为 1 的张量。

In [7]:
nd.ones((3, 4))
Out[7]:

[[ 1.  1.  1.  1.]
 [ 1.  1.  1.  1.]
 [ 1.  1.  1.  1.]]
<NDArray 3x4 @cpu(0)>

我们也可以通过 Python 的列表(list)指定需要创建的 NDArray 中每个元素的值。

In [8]:
y = nd.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
y
Out[8]:

[[ 2.  1.  4.  3.]
 [ 1.  2.  3.  4.]
 [ 4.  3.  2.  1.]]
<NDArray 3x4 @cpu(0)>

有些情况下,我们需要随机生成 NDArray 中每个元素的值。下面我们创建一个形状为(3,4)的 NDArray。它的每个元素都随机采样于均值为 0 标准差为 1 的正态分布。

In [9]:
nd.random.normal(0, 1, shape=(3, 4))
Out[9]:

[[ 2.21220636  0.7740038   1.04344046  1.18392551]
 [ 1.89171135 -1.23474145 -1.771029   -0.45138445]
 [ 0.57938355 -1.85608196 -1.9768796  -0.20801921]]
<NDArray 3x4 @cpu(0)>

运算

NDArray 支持大量的运算符(operator)。例如,我们可以对之前创建的两个形状为 (3, 4) 的 NDArray 做按元素加法。所得结果形状不变。

In [10]:
x + y
Out[10]:

[[  2.   2.   6.   6.]
 [  5.   7.   9.  11.]
 [ 12.  12.  12.  12.]]
<NDArray 3x4 @cpu(0)>

按元素乘法:

In [11]:
x * y
Out[11]:

[[  0.   1.   8.   9.]
 [  4.  10.  18.  28.]
 [ 32.  27.  20.  11.]]
<NDArray 3x4 @cpu(0)>

按元素除法:

In [12]:
x / y
Out[12]:

[[  0.     1.     0.5    1.  ]
 [  4.     2.5    2.     1.75]
 [  2.     3.     5.    11.  ]]
<NDArray 3x4 @cpu(0)>

按元素做指数运算:

In [13]:
y.exp()
Out[13]:

[[  7.38905621   2.71828175  54.59814835  20.08553696]
 [  2.71828175   7.38905621  20.08553696  54.59814835]
 [ 54.59814835  20.08553696   7.38905621   2.71828175]]
<NDArray 3x4 @cpu(0)>

除去按元素计算外,我们可以做矩阵运算。下面将 xy 的转置做矩阵乘法。由于 x 是 3 行 4 列的矩阵,y 转置为 4 行 3 列的矩阵,两个矩阵相乘得到 3 行 3 列的矩阵。

In [14]:
nd.dot(x, y.T)
Out[14]:

[[  18.   20.   10.]
 [  58.   60.   50.]
 [  98.  100.   90.]]
<NDArray 3x3 @cpu(0)>

我们也可以将多个 NDArray 合并。下面分别在行上(维度 0,即形状中的最左边元素)和列上(维度 1,即形状中左起第二个元素)连结两个矩阵。

In [15]:
nd.concat(x, y, dim=0), nd.concat(x, y, dim=1)
Out[15]:
(
 [[  0.   1.   2.   3.]
  [  4.   5.   6.   7.]
  [  8.   9.  10.  11.]
  [  2.   1.   4.   3.]
  [  1.   2.   3.   4.]
  [  4.   3.   2.   1.]]
 <NDArray 6x4 @cpu(0)>,
 [[  0.   1.   2.   3.   2.   1.   4.   3.]
  [  4.   5.   6.   7.   1.   2.   3.   4.]
  [  8.   9.  10.  11.   4.   3.   2.   1.]]
 <NDArray 3x8 @cpu(0)>)

使用条件判断式可以得到元素为 0 或 1 的新的 NDArray。以 x == y 为例,如果 xy 在相同位置的条件判断为真(值相等),那么新 NDArray 在相同位置的值为 1;反之为 0。

In [16]:
x == y
Out[16]:

[[ 0.  1.  0.  1.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]]
<NDArray 3x4 @cpu(0)>

对 NDArray 中的所有元素求和得到只有一个元素的 NDArray。

In [17]:
x.sum()
Out[17]:

[ 66.]
<NDArray 1 @cpu(0)>

我们可以通过 asscalar 函数将结果变换为 Python 中的标量。下面例子中 x\(L_2\) 范数结果同上一样是单元素 NDArray,但最后结果是 Python 中标量。

In [18]:
x.norm().asscalar()
Out[18]:
22.494444

我们也可以把 y.exp()x.sum()x.norm() 等分别改写为 nd.exp(y)nd.sum(x)nd.norm(x) 等。

广播机制

前面我们看到如何对两个形状相同的 NDArray 做按元素操作。当对两个形状不同的 NDArray 做同样操作时,可能会触发广播(broadcasting)机制:先适当复制元素使得这两个 NDArray 形状相同后再按元素操作。

定义两个 NDArray:

In [19]:
a = nd.arange(3).reshape((3, 1))
b = nd.arange(2).reshape((1, 2))
a, b
Out[19]:
(
 [[ 0.]
  [ 1.]
  [ 2.]]
 <NDArray 3x1 @cpu(0)>,
 [[ 0.  1.]]
 <NDArray 1x2 @cpu(0)>)

由于 ab 分别是 3 行 1 列和 1 行 2 列的矩阵,如果要计算 a+b,那么 a 中第一列的三个元素被广播(复制)到了第二列,而 b 中第一行的两个元素被广播(复制)到了第二行和第三行。如此,我们就可以对两个 3 行 2 列的矩阵按元素相加。

In [20]:
a + b
Out[20]:

[[ 0.  1.]
 [ 1.  2.]
 [ 2.  3.]]
<NDArray 3x2 @cpu(0)>

索引

在 NDArray 中,索引(index)代表了元素的位置。NDArray 的索引从 0 开始逐一递增。例如一个 3 行 2 列的矩阵的行索引分别为 0、1 和 2,列索引分别为 0 和 1。

在下面的例子中,我们指定了 NDArray 的行索引截取范围 [1:3]。依据左闭右开指定范围的惯例,它截取了矩阵 x 中行索引为 1 和 2 的两行。

In [21]:
x[1:3]
Out[21]:

[[  4.   5.   6.   7.]
 [  8.   9.  10.  11.]]
<NDArray 2x4 @cpu(0)>

我们可以指定 NDArray 中需要访问的单个元素的位置,例如矩阵中行和列的索引,并重设该元素的值。

In [22]:
x[1, 2] = 9
x
Out[22]:

[[  0.   1.   2.   3.]
 [  4.   5.   9.   7.]
 [  8.   9.  10.  11.]]
<NDArray 3x4 @cpu(0)>

当然,我们也可以截取一部分元素,并重设它们的值。

In [23]:
x[1:2, 1:3] = 12
x
Out[23]:

[[  0.   1.   2.   3.]
 [  4.  12.  12.   7.]
 [  8.   9.  10.  11.]]
<NDArray 3x4 @cpu(0)>

运算的内存开销

前面例子里我们对每个操作新开内存来储存它的结果。例如即使是 y = x + y 我们也会新创建内存,然后再将 y 指向新内存。为了展示这一点,我们可以使用 Python 自带的 id 函数:如果两个实例的 ID 一致,那么它们所对应的内存地址相同;反之则不同。

In [24]:
before = id(y)
y = y + x
id(y) == before
Out[24]:
False

如果我们想指定结果到特定内存,我们可以使用前面介绍的索引来进替换操作。在下面的例子中,我们先通过 zeros_like 创建和 y 形状相同且元素为 0 的 NDArray,记为 z。接下来,我们把 x + y 的结果通过 [:] 写进 z 所对应的内存中。

In [25]:
z = y.zeros_like()
before = id(z)
z[:] = x + y
id(z) == before
Out[25]:
True

注意到这里我们还是为 x + y 创建了临时内存来存储计算结果,再复制到 z 所对应的内存。如果想避免这个内存开销,我们可以使用运算符的全名函数中的 out 参数。

In [26]:
nd.elemwise_add(x, y, out=z)
id(z) == before
Out[26]:
True

如果现有的 NDArray 的值在之后的程序中不会复用,我们也可以用 x[:] = x + y 或者 x += y 来减少运算的内存开销。

In [27]:
before = id(x)
x += y
id(x) == before
Out[27]:
True

NDArray 和 NumPy 相互变换

我们可以通过 arrayasnumpy 函数令数据在 NDArray 和 Numpy 格式之间相互转换。下面将 NumPy 实例变换成 NDArray 实例。

In [28]:
import numpy as np

p = np.ones((2, 3))
d = nd.array(x)
d
Out[28]:

[[  2.   3.   8.   9.]
 [  9.  26.  27.  18.]
 [ 20.  21.  22.  23.]]
<NDArray 3x4 @cpu(0)>

再将 NDArray 实例变换成 NumPy 实例。

In [29]:
d.asnumpy()
Out[29]:
array([[  2.,   3.,   8.,   9.],
       [  9.,  26.,  27.,  18.],
       [ 20.,  21.,  22.,  23.]], dtype=float32)

小结

  • NDArray 是 MXNet 中存储和变换数据的主要工具。
  • 我们可以轻松地对 NDArray 进行创建、运算、指定索引和与 NumPy 之间的相互变换。

练习

  • 运行本节代码。将本节中条件判断式 x == y 改为 x < yx > y,看看能够得到什么样的 NDArray。
  • 将广播机制中按元素操作的两个 NDArray 替换成其他形状,结果是否和预期一样?

扫码直达 讨论区