深度学习笔记(一)—— 数据操作

糊涂涂 2022-8-4 421 8/4

写在前面:由于前段时间学习python时使用过pycharm,而且pycharm可以自动补全函数,相对方便,所以在学习深度学习时我同样选择了pycharm,因此笔记中的代码均为pycharm中的格式。

当然,使用pycharm也可以使用原书中的代码格式且具有自动补全功能,只需打开pycharm中的python 控制台在控制台输入即可。


创建NDArray

首先我们要从MXNet中导⼊ndarray模块。这⾥的nd是ndarray的缩写形式。

from mxnet import nd

这里需要注意:在pycharm中应该先安装相应软件包:mxnet(如下图)安装软件包

深度学习笔记(一)—— 数据操作

然后我们⽤arange函数创建⼀个⾏向量。

输入:

x = nd.arange(12)
print(x)

运行代码,输出:

[ 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,size属性获取NDArray实例的形状和元素(element)的总数:

print(x.shape)
print(x.size)

依次输出:

(12,)
12

接下来使⽤reshape函数把⾏向量x的形状改为(3,4),也就是⼀个3⾏4列的矩阵,并记作X。除了形状改变之外,X中的元素保持不变。

X = x.reshape((3,4))
print(X)

输出:

[[ 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或1,形状为(2, 3, 4)的张量。实际上,之前创建的向量和矩阵都是
特殊的张量。

y = nd.zeros((2, 3, 4)) # 元素全为0
print(y)
z = nd.ones((2, 3, 4))  # 元素全为1
print(z)

输出:

[[[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. 1. 1. 1.]
  [1. 1. 1. 1.]
  [1. 1. 1. 1.]]

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

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

Y = nd.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
print(Y)

输出:

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

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

Z = nd.random.normal(0, 1, shape=(3, 4))
print(Z)

输出:

[[ 1.1630785   0.4838046   0.29956347  0.15302546]
 [-1.1688148   1.558071   -0.5459446  -2.3556297 ]
 [ 0.54144025  2.6785064   1.2546344  -0.54877406]]
<NDArray 3x4 @cpu(0)>

运算

NDArray⽀持⼤量的运算符(operator)。例如,我们可以对之前创建的两个形状为(3,4)的NDArray做按元素做加减乘除和指数运算。所得结果形状不变。

print(X, Y, X + Y, X - Y, X * Y, X / Y, Y.exp())

输出:

[[ 0.  1.  2.  3.]
 [ 4.  5.  6.  7.]
 [ 8.  9. 10. 11.]]
<NDArray 3x4 @cpu(0)> 
[[2. 1. 4. 3.]
 [1. 2. 3. 4.]
 [4. 3. 2. 1.]]
<NDArray 3x4 @cpu(0)> 
[[ 2.  2.  6.  6.]
 [ 5.  7.  9. 11.]
 [12. 12. 12. 12.]]
<NDArray 3x4 @cpu(0)> 
[[-2.  0. -2.  0.]
 [ 3.  3.  3.  3.]
 [ 4.  6.  8. 10.]]
<NDArray 3x4 @cpu(0)> 
[[ 0.  1.  8.  9.]
 [ 4. 10. 18. 28.]
 [32. 27. 20. 11.]]
<NDArray 3x4 @cpu(0)> 
[[ 0.    1.    0.5   1.  ]
 [ 4.    2.5   2.    1.75]
 [ 2.    3.    5.   11.  ]]
<NDArray 3x4 @cpu(0)> 
[[ 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函数做矩阵乘法。下⾯将X与Y的转置做矩阵乘法。由于X是3⾏4列的矩阵,Y转置为4⾏3列的矩阵,因此两个矩阵相乘得到3⾏3列的矩阵。

Z = nd.dot(X, Y.T)
print(Z)

输出:

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

我们也可以将多个NDArray连结(concatenate)。下⾯分别在⾏上(维度0【dim=0】,即形状中的最左边元素)和列上(维度1【dim=1】,即形状中左起第⼆个元素)连结两个矩阵。可以看到,输出的第⼀个NDArray在维度0的⻓度(6)为两个输⼊矩阵在维度0的⻓度之和(3 + 3),而输出的第⼆个NDArray在维度1的⻓度(8)为两个输⼊矩阵在维度1的⻓度之和(4 + 4)。

Z1 = nd.concat(X, Y, dim=0)
Z2 = nd.concat(X, Y, dim=1)
print(Z1, Z2)

输出:

[[ 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为例,如果X和Y在相同位置的条件判断为真(值相等),那么新的NDArray在相同位置的值为1;反之为0。

print(X == Y)

输出:

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

同理对于"<"、">"、"!="也是一样,由于"<"">""==""!="在python中均为判断,其结果为布尔值,不是True就是False,所以我们类比其结果可以得到,若相同位置两元素判断结果为True,那么新的NDArray在相同位置的值为1;如果为False,那么新的NDArray在相同位置的值为0。

from mxnet import nd

X = nd.arange(9).reshape((3, 3))
Y = X * (-1) + 8

print(X, Y)

print(Y == X)
print(Y != X)
print(Y < X)
print(Y > X)

输出:

[[0. 1. 2.]
 [3. 4. 5.]
 [6. 7. 8.]]
<NDArray 3x3 @cpu(0)> 
[[8. 7. 6.]
 [5. 4. 3.]
 [2. 1. 0.]]
<NDArray 3x3 @cpu(0)>

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

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

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

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

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

print(X.sum())

输出:

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

我们可以通过asscalar函数将结果变换为Python中的标量。下⾯例⼦中X的L2范数 X.norm()(详见本分类文章数学基础)结果同上例⼀样是单元素NDArray,但最后结果变换成了Python中的标量。

print(X.norm().asscalar())

输出:

22.494442

注:我们也可以把Y.exp()、X.sum()、X.norm()等分别改写为nd.exp(Y)、nd.sum(X)、nd.norm(X)等,但不常用。

广播机制

当对两个形状不同的NDArray按元素运算时,可能会触发⼴播(broadcasting)机制:先适当复制元素使这两个NDArray形状相同后再按元素运算。

定义两个NDArray:

由于A和B分别是3⾏1列和1⾏2列的矩阵,如果要计算A + B,那么A中第⼀列的3个元素被⼴播(复制)到了第⼆列,而B中第⼀⾏的2个元素被⼴播(复制)到了第⼆⾏和第三⾏。如此,就可以对2个3⾏2列的矩阵按元素相加。

A = nd.arange(3).reshape((3, 1))
B = nd.arange(2).reshape((1, 2))

print(A, B)
print(A + B)

输出:

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

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

 


提醒!提醒!提醒!

这里需要注意到,广播机制不是万能的,如下:

我们重新定义两个NDArray:

A = nd.arange(6).reshape((3, 2))
B = nd.arange(3).reshape((1, 3))

print(A, B)

输出:

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

我们可以看到,此时对于B可以复制三次后满足A的格式,但是A若要和B格式匹配则需要变成三列,那么此时复制第一列还是第二列呢?我们尝试输出A + B来测试一下。

输出:

MXNetError: Check failed: l == 1 || r == 1: operands could not be broadcast together with shapes [3,2] [1,3]

这里我们发现程序报错了,这种格式 "could not be broadcast together",我们查看官方文档可以发现:

Two tensors are “broadcastable” if the following rules hold:

    · Each tensor has at least one dimension.
    · When iterating over the dimension sizes, starting at the trailing dimension,the dimension sizes must either be equal, one of them is 1, or one of them does not exist.

翻译之后通俗来说就是:

广播机制触发时,必须同时满足如下2个条件:

    · 两个张量都必须不为空;

    · 将两个张量的形状信息右对齐,从右向左观看同一维度下,张量的轴长必须相等,或者其中一个轴长为1,或者其中一个轴长为空。

所以以后在使用广播机制时我们需要注意这两个前提,避免程序出现异常。


 

索引

在NDArray中,索引(index)代表了元素的位置。类比于python的索引与切片来理解即可。索引是从0开始的,例如,一个3⾏2列的矩阵的⾏索引分别为0、1和2,列索引分别为0和1;同时要注意其左闭右开的规则,例如:我们指定了NDArray的⾏索引截取范围[1:3],它截取的便是矩阵X中⾏索引为1和2的两⾏。

我们先设定一个X:

X = nd.arange(12).reshape((3, 4))

截取行/列/元素:

print(X)
print(X[0:2, :])    # 截取1,2行
print(X[:, 0:2])    # 截取1,2列
print(X[1, 1])      # 截取第二行第二列元素

输出:

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

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

[[0. 1.]
 [4. 5.]
 [8. 9.]]
<NDArray 3x2 @cpu(0)>

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

这里在截取行、列时使用的X[:, :]可以和切片类比,逗号前面表示行,后面表示列,所以在截取行时我们可以简化为X[0:2],它会默认截取行内元素,但是在截取列时不可以省略。同样,在截取单一行单一列时,我们可以省略":",类似于截取单一元素。

print(X)
print(X[0, :])  # 截取第一行
print(X[0])     # 截取第一行
print(X[:, 0])  # 截取第一列

输出:

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

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

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

[0. 4. 8.]
<NDArray 3 @cpu(0)>

我们也可以给元素赋值:

print(X)
X[0] = 12       # 第一行元素全部赋值12
print(X)
X[:, 0] = 13    # 第一列元素全部赋值13
print(X)
X[2, 3] = 0     # 第三行第四列元素赋值0
print(X)

输出:

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

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

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

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

运算的内存开销

这里我们需要了解python的id()函数,该函数会返回对象的唯一标识符,标识符是一个整数,这个数便是这个对象的内存地址。如果两个实例的id一致,那么它们所对应的内存地址相同;反之则不同。在前⾯的例⼦⾥我们对每个操作新开内存来存储运算结果。举个例⼦,即使像Y = X + Y这样的运算,我们也会新开内存,然后将Y指向新内存。

Y = nd.ones(shape=X.shape)

before = id(Y)
Y = Y + X
print(id(Y) == before)

注:"=="在这里作为判断,其输出结果是一个布尔值,如果数值相同返回True,如果不同返回False。

输出:

False

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

Z = Y.zeros_like()
before = id(Z)
Z[:] = X + Y
print(id(Z) == before)

输出:

True

实际上,上例中我们还是为X + Y开了临时内存来存储计算结果,再复制到Z对应的内存。如果想避免这个临时内存开销,我们可以使⽤运算符全名函数中的out参数。

Z = Y.zeros_like()
before = id(Z)
nd.elemwise_add(X, Y, out=Z)
print(id(Z) == before)

输出:

True

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

before = id(X)
X[:] = X + Y
print(id(X) == before)

before = id(X)
X += Y
print(id(X) == before)

输出:

True
True

NDArray和NumPy相互变换

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

from mxnet import nd
import numpy as np

P = np.ones((2, 3))

D = nd.array(P)
print(D, '\n\n', type(D))

Q = D.asnumpy()
print(Q, '\n\n', type(Q))

输出:

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

 <class 'mxnet.ndarray.ndarray.NDArray'>
[[1. 1. 1.]
 [1. 1. 1.]] 

 <class 'numpy.ndarray'>

注:这里我们用到python自带的type()函数查看对象数据类型。

- THE END -

糊涂涂

8月06日09:56

最后修改:2022年8月6日
2

共有 1 条评论

  1. hututu博主

    😀