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

T1d 2022-8-4 2,276 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 -

T1d

8月06日09:56

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

共有 2 条评论

您必须 后可评论

  1. T1d

    hututu博主

    😀

  2. WJY

    😍

Welcome~
欢迎访问糊涂涂的个人博客呀