[STAT-157] Data processing(mxnet: nd autograd)

课程Youtube:Deep Learning UC Berkeley STAT-157

课本:动手学深度学习 

代码:d2l-ai


2.2. 数据操作

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

在MXNet中,NDArray是一个类,也是存储和变换数据的主要工具。为了简洁,本书常将NDArray实例直接称作NDArray。如果你之前用过NumPy,你会发现NDArray和NumPy的多维数组非常类似。然而,NDArray提供GPU计算和自动求梯度等更多功能,这些使NDArray更加适合深度学习。

2.2.1. 创建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)”里的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。除了形状改变之外,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.2122064   0.7740038   1.0434405   1.1839255 ]
 [ 1.8917114  -1.2347414  -1.771029   -0.45138445]
 [ 0.57938355 -1.856082   -1.9768796  -0.20801921]]
<NDArray 3x4 @cpu(0)>

2.2.2. 运算

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.389056   2.7182817 54.59815   20.085537 ]
 [ 2.7182817  7.389056  20.085537  54.59815  ]
 [54.59815   20.085537   7.389056   2.7182817]]
<NDArray 3x4 @cpu(0)>

除了按元素计算外,我们还可以使用dot函数做矩阵乘法。下面将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连结(concatenate)。下面分别在行上(维度0,即形状中的最左边元素)和列上(维度1,即形状中左起第二个元素)连结两个矩阵。可以看到,输出的第一个NDArray在维度0的长度(66)为两个输入矩阵在维度0的长度之和(3+33+3),而输出的第二个NDArray在维度1的长度(88)为两个输入矩阵在维度1的长度之和(4+44+4)。

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的L2L2范数结果同上例一样是单元素NDArray,但最后结果变换成了Python中的标量。

In [18]:
X.norm().asscalar()
Out[18]:
22.494442

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

2.2.3. 广播机制

前面我们看到如何对两个形状相同的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中第一列的3个元素被广播(复制)到了第二列,而B中第一行的2个元素被广播(复制)到了第二行和第三行。如此,就可以对2个3行2列的矩阵按元素相加。

In [20]:
A + B
Out[20]:
[[0. 1.]
 [1. 2.]
 [2. 3.]]
<NDArray 3x2 @cpu(0)>

2.2.4. 索引

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)>

当然,我们也可以截取一部分元素,并为它们重新赋值。在下面的例子中,我们为行索引为1的每一列元素重新赋值。

In [23]:
X[1:2, :] = 12
X
Out[23]:
[[ 0.  1.  2.  3.]
 [12. 12. 12. 12.]
 [ 8.  9. 10. 11.]]
<NDArray 3x4 @cpu(0)>

2.2.5. 运算的内存开销

在前面的例子里我们对每个操作新开内存来存储运算结果。举个例子,即使像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

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

In [27]:
before = id(X)
X += Y
id(X) == before
Out[27]:
True

2.2.6. NDArray和NumPy相互变换

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

In [28]:
import numpy as np

P = np.ones((2, 3))
D = nd.array(P)
D
Out[28]:
[[1. 1. 1.]
 [1. 1. 1.]]
<NDArray 2x3 @cpu(0)>

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

In [29]:
D.asnumpy()
Out[29]:
array([[1., 1., 1.],
       [1., 1., 1.]], dtype=float32)

2.2.7. 小结

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

2.2.8. 练习

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

 

2.3. 自动求梯度

在深度学习中,我们经常需要对函数求梯度(gradient)。本节将介绍如何使用MXNet提供的autograd模块来自动求梯度。如果对本节中的数学概念(如梯度)不是很熟悉,可以参阅附录中“数学基础”一节。

In [1]:
from mxnet import autograd, nd

2.3.1. 简单例子

我们先看一个简单例子:对函数 y=2x⊤xy=2x⊤x 求关于列向量 xx 的梯度。我们先创建变量x,并赋初值。

In [2]:
x = nd.arange(4).reshape((4, 1))
x
Out[2]:
[[0.]
 [1.]
 [2.]
 [3.]]
<NDArray 4x1 @cpu(0)>

为了求有关变量x的梯度,我们需要先调用attach_grad函数来申请存储梯度所需要的内存。

In [3]:
x.attach_grad()

下面定义有关变量x的函数。为了减少计算和内存开销,默认条件下MXNet不会记录用于求梯度的计算。我们需要调用record函数来要求MXNet记录与求梯度有关的计算。

In [4]:
with autograd.record():
    y = 2 * nd.dot(x.T, x)

由于x的形状为(4, 1),y是一个标量。接下来我们可以通过调用backward函数自动求梯度。需要注意的是,如果y不是一个标量,MXNet将默认先对y中元素求和得到新的变量,再求该变量有关x的梯度。

In [5]:
y.backward()

函数 y=2x⊤xy=2x⊤x 关于xx 的梯度应为4x4x。现在我们来验证一下求出来的梯度是正确的。

In [6]:
assert (x.grad - 4 * x).norm().asscalar() == 0
x.grad
Out[6]:
[[ 0.]
 [ 4.]
 [ 8.]
 [12.]]
<NDArray 4x1 @cpu(0)>

2.3.2. 训练模式和预测模式

从上面可以看出,在调用record函数后,MXNet会记录并计算梯度。此外,默认情况下autograd还会将运行模式从预测模式转为训练模式。这可以通过调用is_training函数来查看。

In [7]:
print(autograd.is_training())
with autograd.record():
    print(autograd.is_training())
False
True

在有些情况下,同一个模型在训练模式和预测模式下的行为并不相同。我们会在后面的章节详细介绍这些区别。

2.3.3. 对Python控制流求梯度

使用MXNet的一个便利之处是,即使函数的计算图包含了Python的控制流(如条件和循环控制),我们也有可能对变量求梯度。

考虑下面程序,其中包含Python的条件和循环控制。需要强调的是,这里循环(while循环)迭代的次数和条件判断(if语句)的执行都取决于输入a的值。

In [8]:
def f(a):
    b = a * 2
    while b.norm().asscalar() < 1000:
        b = b * 2
    if b.sum().asscalar() > 0:
        c = b
    else:
        c = 100 * b
    return c

我们像之前一样使用record函数记录计算,并调用backward函数求梯度。

In [9]:
a = nd.random.normal(shape=1)
a.attach_grad()
with autograd.record():
    c = f(a)
c.backward()

我们来分析一下上面定义的f函数。事实上,给定任意输入a,其输出必然是 f(a) = x * a的形式,其中标量系数x的值取决于输入a。由于c = f(a)有关a的梯度为x,且值为c / a,我们可以像下面这样验证对本例中控制流求梯度的结果的正确性。

In [10]:
a.grad == c / a
Out[10]:
[1.]
<NDArray 1 @cpu(0)>

2.3.4. 小结

  • MXNet提供autograd模块来自动化求导过程。
  • MXNet的autograd模块可以对一般的命令式程序进行求导。
  • MXNet的运行模式包括训练模式和预测模式。我们可以通过autograd.is_training()来判断运行模式。

2.3.5. 练习

  • 在本节对控制流求梯度的例子中,把变量a改成一个随机向量或矩阵。此时计算结果c不再是标量,运行结果将有何变化?该如何分析该结果?
  • 重新设计一个对控制流求梯度的例子。运行并分析结果。

 

 

2.4. 查阅文档

受篇幅所限,本书无法对所有用到的MXNet函数和类一一详细介绍。读者可以查阅相关文档来做更深入的了解。

2.4.1. 查找模块里的所有函数和类

当我们想知道一个模块里面提供了哪些可以调用的函数和类的时候,可以使用dir函数。下面我们打印nd.random模块中所有的成员或属性。

In [1]:
from mxnet import nd

print(dir(nd.random))
['NDArray', '_Null', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_internal', '_random_helper', 'current_context', 'exponential', 'exponential_like', 'gamma', 'gamma_like', 'generalized_negative_binomial', 'generalized_negative_binomial_like', 'multinomial', 'negative_binomial', 'negative_binomial_like', 'normal', 'normal_like', 'numeric_types', 'poisson', 'poisson_like', 'randint', 'randn', 'shuffle', 'uniform', 'uniform_like']

通常我们可以忽略掉由__开头和结尾的函数(Python的特别对象)或者由_开头的函数(一般为内部函数)。通过其余成员的名字我们大致猜测出这个模块提供了各种随机数的生成方法,包括从均匀分布采样(uniform)、从正态分布采样(normal)、从泊松分布采样(poisson)等。

2.4.2. 查找特定函数和类的使用

想了解某个函数或者类的具体用法时,可以使用help函数。让我们以NDArray中的ones_like函数为例,查阅它的用法。

In [2]:
help(nd.ones_like)
Help on function ones_like:

ones_like(data=None, out=None, name=None, **kwargs)
    Return an array of ones with the same shape and type
    as the input array.

    Examples::

      x = [[ 0.,  0.,  0.],
           [ 0.,  0.,  0.]]

      ones_like(x) = [[ 1.,  1.,  1.],
                      [ 1.,  1.,  1.]]



    Parameters
    ----------
    data : NDArray
        The input

    out : NDArray, optional
        The output NDArray to hold the result.

    Returns
    -------
    out : NDArray or list of NDArrays
        The output of this function.

从文档信息我们了解到,ones_like函数会创建和输入NDArray形状相同且元素为1的新NDArray。我们可以验证一下:

In [3]:
x = nd.array([[0, 0, 0], [2, 2, 2]])
y = x.ones_like()
y
Out[3]:
[[1. 1. 1.]
 [1. 1. 1.]]
<NDArray 2x3 @cpu(0)>

在Jupyter记事本里,我们可以使用?来将文档显示在另外一个窗口中。例如,使用nd.random.uniform?将得到与help(nd.random.uniform)几乎一样的内容,但会显示在额外窗口里。此外,如果使用nd.random.uniform??,那么会额外显示该函数实现的代码。

2.4.3. 在MXNet网站上查阅

读者也可以在MXNet的网站上查阅相关文档。访问MXNet网站 http://mxnet.apache.org/ (如图2.1所示),点击网页顶部的下拉菜单“API”可查阅各个前端语言的接口。此外,也可以在网页右上方含“Search”字样的搜索框中直接搜索函数或类名称。

MXNet官方网站

图 2.1 MXNet官方网站

图2.2展示了MXNet网站上有关ones_like函数的文档。

MXNet网站上有关\ ``ones_like``\ 函数的文档

图 2.2 MXNet网站上有关ones_like函数的文档

2.4.4. 小结

  • 遇到不熟悉的MXNet API时,可以主动查阅它的相关文档。
  • 查阅MXNet文档可以使用dirhelp函数,或访问MXNet官方网站。

2.4.5. 练习

  • 查阅NDArray支持的其他操作。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值