零、写在前面

NumPy数组库是python中的一个库,能够帮助创建一个支持多种操作的数组。作为一名ai学生,这个库必然是要牢牢掌握的。据我所知,学习经济金融这一块的朋友们在做数据分析时,也需要用到NumPy,因此也可以参考本教程。

虽然我在程序设计实习这门课已经学过了,但是我上课几乎一个字没听(我也不知道我在干什么),再加上在ai引论这门课曾经多次使用这个库,每次写lab的时候我都对numpy的各种函数感到迷茫。所以我决定重新从头学习一遍。

本教程参考B站UP主“爆肝杰哥”的视频《Python深度学习:NumPy数组库》,谢谢杰哥(×)。虽然UP主说不准抄袭讲义,但是他应该找不到我()

一、数组基础

第一个知识点:导入numpy时要先写:import numpy as np。(真有人不知道吗)

1. 数据类型

为了节省内存,一个numpy数组只容纳一种数据类型,所以可以把numpy数组简单分为整数型数组浮点型数组。但凡数组中有一个元素是浮点数,那么这个数组就是浮点型数组。比如,[1,2,3] 是整数型数组,但 [1.0,2,3] 是浮点型数组,输出为[1. 2. 3.]。注意,输出numpy数组时,元素之间没有逗号

往整数型数组中插入浮点数,浮点数会变成整数(向下取整)。同理,整数插入到浮点型数组时,也会变成浮点数

要转换一个numpy数组的类型,需要使用.astype()方法。例如,要把整数型数组arr1转化成浮点型数组arr2,代码如下:

arr2 = arr1.astype(float)

当然也不只有这种方法。如果整数型数组与浮点数做运算、整数型数组遇到除法、整数型数组与浮点型数组做运算,都会变成浮点数。这一点和整数是一样的。浮点型数组一般不会自动变成整数型数组。

2. 数组维度

在深度学习中,基本用到的是一维和二维数组。不同维度的数组,从外形上的本质区别就是有几层中括号

有的函数需要传入数组的形状参数。所谓形状参数,就是标明维数和各维长度的元组。元组中有几个数字表示有几维,每个数字的大小表明长度。比如,一维数组的形状参数为(x,),二维(x,y),三维(x,y,z),以此类推。对于三维数组,x表示最大的中括号里有多少个二级中括号;y表示二级中括号里有多少个三级中括号;z表示最小的中括号里有几个数字。二维的同理。

想知道一个数组的形状,使用.shape属性。返回的就是形状参数。注意,属性是不需要参数的,后面不接括号;方法本质上是一个函数,需要接括号,有时括号里还有参数。

转换数组的形状,需要使用.reshape()方法。这个方法可以实现一维数组与二维数组的互相转换,也可以实现二维数组的长度变化。例如,对数组arr1 = [1 2 3]使用arr2 = arr1.reshape((3,1)),就会变成[[1][2][3]]的三行一列数组。

值得一提的是,可以留一个维数设置成-1,让系统自己算。比如上文可以改成reshape((1,-1)),仍然可以正常运行。

下文中,一维数组称为向量,二维数组称为矩阵

二、数组的创建

1. 创建指定数组

使用np.array()函数,将一个已知的列表转化为numpy数组。

import numpy as np
arr1 = np.array([1,2,3])  # arr1 = [1 2 3]
arr2 = np.array([[1,2,3]])  # arr2 = [[1 2 3]]
arr3 = np.array([[1],[2],[3]])  # arr3 = [[1] [2] [3]]
arr4 = np.array([[1,2],[3,4]])  # arr4 = [[1 2][3 4]]

对于同样数据的数组,向量所需要的中括号是最少的,消耗的内存也是最少的。同理,列矩阵所消耗的内存是最大的。

2. 创建递增数组

使用np.arange()函数创建递增数组(arange全称为array-range)。括号里的参数和range()一样,分别是(首项,尾项,步长)。比如:

arr1 = np.array(10,20,2)  # arr1 = [10 12 14 16 18]。注意是左闭右开区间

3. 创建同值数组

使用np.zeros()np.ones()函数,可以创建出全部为0和全部为1的数组,括号内的参数是形状参数。如果要变成全部为x的数组,只需给全为1的数组乘以x即可。比如:

arr1 = np.zeros(3)  # arr1 = [0. 0. 0.]
arr2 = np.ones((3,1))  # arr2 = [[1.] [1.] [1.]]
arr3 = 3.14 * np.ones(3)  # arr3 = [3.14 3.14 3.14]

注意,np.zeros()np.ones()生成的都是浮点型数组。这个设定可能是为了防止插进去的浮点数被截断。

4. 创建随机数组

使用np.random系列函数,可以创建出各式各样的随机数组。比如:创建0到1之间均匀分布的浮点型随机数组,可以使用np.random.random()函数,参数是形状参数。注意,均匀分布是指每个数字的产生是等概率的。

arr1 = np.random.random(5)

如果想要改变范围,只需要给这个函数配凑常数即可。例如,要创建a到b之间的随机数组,只需把基底设为a,并给每个数放大为(b-a)倍即可。用代码实现如下:

arr2 = a + (b - a) * np.random.random(5)

创建整数型随机数组,可以使用np.random.randint()函数,参数有三个,分别是(最小值,最大值,形状参数),范围仍然是左闭右开区间

arr3 = np.random.randint(10, 100, (1,15))

其实创建整数型随机数组也可以借助浮点型随机数组,搭配.astype()实现。比如,上文的arr3可以这样实现:

arr4 = 10 + ((100 - 10) * np.random.random((1,15))).astype(int)

创建服从正态分布的随机数组,可以使用np.random.normal()函数,参数有三个,分别是(平均值,标准差,形状参数)。如果大家忘记了正态分布是什么,只需要记得随机数是平均值的概率最大,离平均值越远概率越小,在平均值两侧对称的数字概率相等;标准差越小,远离平均值时的概率降低速度就越快。实在无法理解的可以自行搜索。

arr5 = np.random.normal(0, 1, (2,3))

一种特殊的正态分布是标准正态分布,其平均值为0,方差为1。这种正态分布有专属的函数np.random.randn(),参数只有形状参数。

三、数组的索引

1. 访问与修改数组元素

与列表一样,numpy数组可以实现随机访问,只需要用下标索引即可。同时,也可以作为左值进行修改。

arr1 = np.arange(1,10)
print(arr1[3])  # 正序访问,输出为4
print(arr1[-2])  # 倒序访问,输出为8
arr1[3] = 666  # 修改数组元素

矩阵也同样可以访问与修改,下标可以使用传统的[x][y]索引,也可以使用[x,y]索引,其中x是行数,y是列数,都是从0开始的。我个人还是倾向于用前者,但是注意,传统的这种索引实际上是进行了两次索引(先找到第x行,再找到第y个元素),所以效率会降低。

arr2 = np.array([[1,2,3],[4,5,6]])
print(arr2[1,2])
print(arr2[1][2])

2. 花式索引

花式索引(Fancy indexing)可以同时返回数组中的多个元素。对于向量,只需要把原先的下标改成下标列表,返回的就是一个局部向量,比如:

arr1 = np.arange(0,90,10)
print(arr1[ [0,2,5] ])  # [0 20 50]

对于矩阵,只需要把原来的下标对改成下标列表对,返回的是一个向量。比如:

arr2 = np.arange(1,17).reshape(4,4)
print(arr2[ [0,1,2],[2,1,0] ])  # [3 6 9],是arr2中[0,2],[1,1],[2,0]组成的向量

和普通索引一样,可以修改元素。如果想把多个元素同时赋值成同一个值,只用= 100;如果赋值成不同值,也可以用列表赋值,比如= [100,200,300]。如果赋值成不同值,需要注意列表的元素数量要和被赋值的元素数量一样,否则会报错。

普通索引使用一层中括号,花式索引使用两层中括号

3. 数组切片

i. 向量的切片

向量的切片与列表切片操作完全一致,都是用[x:y:z]表示切出向量的[x,y)部分,每z个元素采样一次。比如:

arr1 = np.arange(10)
print(arr1[1:4])  # [1 2 3]
print(arr1[1:-7]) # [1 2 3],倒数第7个数也是第4个数
print(arr1[1:])  # 切到结尾
print(arr1[:4])  # 从头开始切
print(arr1[1:4:2])  # [1 3],每两个元素取一个

ii. 矩阵的切片

类似于索引,矩阵的切片其实就是对两个下标分别切片。但是注意,不能用两个中括号来索引了。比如:

arr2 = np.arange(1,21).reshape(4,5)
print(arr2[1:3,1:4])  # [[7 8 9][12 13 14]]

特别地,可以切出矩阵的一整行和一整列。值得一提的是,当切出矩阵的一整列时,输出的会是一个向量(系统为了节省空间的操作)。值得二提的是,当切出矩阵的一整行时,可以简写,不需要写第二个下标

iii. 切片的本质是引用

这个观点十分重要,因此单独拿一段出来强调。当修改切片时,原来的数组也会随之改变,即浅拷贝。比如:

arr = np.arange(10)  # [0 1 ··· 9]
cut = arr[0:3]  # [0 1 2]
cut[0] = 100  # 修改cut,arr也会随之修改
print(arr)  # [100 1 2 ··· 9]

这样有个好处,就是能够节省内存,切片实际上与原数组占用的是同一片内存区域。因此,我们一般会使用切片,比如arr[:] = <表达式>,来替代arr = <表达式>。(其实我没看懂为啥能节省内存,有知道的朋友请在评论区为我解答)

同理,直接把一个数组赋值给另一个数组也是浅拷贝,比如cut = arr,如果修改cut,arr也会随之变化。

如果真的要创建新变量,即深拷贝,使用.copy()方法。比如:

arr = np.arange(10)  # [0 1 ··· 9]
copy = arr[0:3].copy()  # [0 1 2]
copy[0] = 100  # 修改copy,arr不会随之修改
print(arr)  # [0 1 2 ··· 9]
copy2 = arr.copy()  # 修改copy2,arr不会随之修改

四、数组的变形

1. 数组的转置

使用.T转置矩阵。注意,只能转置矩阵,不能转置向量。因此,对于向量,要先变成矩阵。比如:

arr1 = np.arange(1,4)  # [1 2 3]
arr2 = arr1.reshape((1,-1))  # [[1 2 3]]
arr3 = arr2.T  # [[1] [2] [3]]

2. 数组的翻转

数组有两种翻转方式,一种是左右翻转,也就是把最后一列挪到第一列、倒数第二列挪到第二列、···、以此类推。另一种是上下翻转,就是把行倒着排。对应了两个函数,一个是左右翻转np.fliplr(),即flip-left-right,另一个是上下翻转np.flipud(),即flip-up-down。参数是被翻转的数组。

注意:向量只可以上下翻转。是不是很反常识?其实在NumPy的世界观里,向量都是列向量。

arr1 = np.arange(10)
arr_ud = np.flipud(arr1)  # [9 8 7 ··· 0]

仍然值得一提的是,经过我的尝试,如果修改了arr_ud,那么原数组arr1也会被修改,其实和切片是一样的。你也可以理解成翻转是一种特殊的切片。

3. 矩阵的重塑

用的是我们之前已经提及的.reshape()方法,参数是形状参数。既然讲过了,并且之前用过很多次了,我就不举例了,但是还是强调一下:可以留一个维度写-1,让系统自己算。这个真的很实用。

4. 数组的拼接

两个向量拼接,得到的是加长版向量。使用np.concatenate()函数实现,参数是一个列表,包含了需要拼接的向量。比如:

arr1 = np.array([1,2,3])
arr2 = np.array([4,5,6])
arr3 = np.concatenate([arr1,arr2])
print(arr3)  # [1 2 3 4 5 6]

两个矩阵仍然是用上述函数拼接,但是多了一个参数axis,表示沿着哪个方向拼接。默认值是0,即沿着行的方向拼接;也可以加上np.concatenate([], axis=1),改成按列拼接。注意,沿着行拼接时,行数要一样;沿着列拼接时,列数要一样,否则会报错。

拓展:axis = -1表示按照最后一个维度进行拼接。

向量和矩阵不能直接拼接!需要先把向量转换成行矩阵。

5. 数组的分裂

一个向量分裂,使用np.split()函数实现,参数为(向量名,[断点列表])。其中,断点列表是由整数构成的列表,表示在这个位置之前断开。返回的值也是一个列表,包括了被断开的向量。比如:

arr = np.arange(1,10)
arr1,arr2,arr3 = np.split(arr, [2,8])
print(arr1)  # [1 2]
print(arr2)  # [3 4 5 6 7 8]
print(arr3)  # [9]

矩阵的分裂也是用上述函数,但是还是需要注明分裂的方向,即参数为(向量名,[断点列表],axis=0)。默认值为0,即沿着行切;设为1,则沿着列切。

五、数组的运算

数组可以与常数、与其他数组做运算。

1. 数组与常数做运算

数组与常数计算,就是数组中的每个数字都与常数做相应的运算。因此,加、减、乘、除、幂、括号、整除、求余等运算符都与Python完全一样。以防大家忘记Python有哪些运算符,这里还是再列举一下,举例就懒得写了。

运算符 含义
+、-、*、/ 加、减、乘、除
**
() 修正运算次序
// 整除
% 求余

2. 数组与数组之间的运算

如果用上文的各种运算符作用在数组之间的运算,那么会进行逐元素计算(element-wise),这要求两个数组的形状完全相同。比如:

arr1 = [[1 2 3][4 5 6]]  # 大家创建np数组的时候一定不要这么写啊
arr2 = [[7 8 9][10 11 12]]  # 我在偷懒
print(arr1 * arr2)  # [[7 16 27][40 55 72]]

在这里我发现一个比较好玩的事情:如果赋值arr2 = -arr1,此时修改arr2是不会影响arr1的。也就是说,创建arr2 = -(-arr1)的话,可以直接实现深拷贝!真是太有意思了()

3. 广播

广播是为了处理不同形状的数组之间的运算。不同形状的数组有如下运算规则:

  • 向量与矩阵做运算,向量自动被升级为行矩阵
  • 如果某矩阵是行矩阵或列矩阵,则其被广播,以适配另一个矩阵的形状。

需要提前说的是,一般只推荐对向量进行广播,后两种基本上不会使用,因为实在是太反直觉了。

i. 向量被广播

当一个形状为(x,y)的矩阵与一个向量做运算时,要求该向量的形状必须为y。运算时,这个向量会自动升级成(1,y)的行矩阵,再被自动广播为(x,y)的矩阵,每一个元素都会向下复制x次。比如:

arr1 = [-100 0 100]
arr2 = [[1 2 3][4 5 6]]
print(arr1 * arr2)   # [[-100 0 300] [-400 0 600]]

ii. 列矩阵被广播

行矩阵被广播的情况就和向量被广播的情况是一样的,此处不再赘述。这里讲解列矩阵如何被广播。

当一个形状为(x,y)的矩阵与一个列矩阵做运算时,要求该列矩阵的形状必须为(x,1),它会被自动广播为(x,y)的矩阵,每一个元素都会向右复制y次。比如:

arr1 = [[0] [1] [2]]
arr2 = [[1 2] [3 4] [5 6]]
print(arr1 * arr2)  # [[0 0] [3 4] [10 12]]

iii. 行矩阵与列矩阵同时被广播

一个(1,y)的行矩阵与一个(x,1)的列矩阵相乘,两个矩阵会同时广播成(x,y)的矩阵,然后再相乘。通过简单的计算,可以知道,这种乘法得到的矩阵,第i行第j列的值是由行矩阵的第i个元素和列矩阵的第j个元素相乘得到的。懒得举例了,大家可以自行尝试。

六、数组的函数

本节讲一些常用的特殊函数。

1. 矩阵乘积

终于回归了线性代数中最熟悉的矩阵乘法,通过np.dot()函数实现。值得一提的是,当乘积中有向量时,它会根据需要,随意变成行矩阵或列矩阵,但是输出的结果必须是向量!这一点非常重要,接着看下去你就明白了。

i. 向量与向量的乘积

两个向量相乘,一定是前者为行向量,后者为列向量,输出为一个数字。向量长度必须相同。比如:

arr1 = np.arange(5)
arr2 = np.arange(5)
print(np.dot(arr1,arr2))  # 30

提问:为什么不是前者变成列向量,后者变成行向量?如果这样,输出的将是一个 5×5 的矩阵,不符合规定。

ii. 向量与矩阵的乘积

向量在前面就会变成行向量,在后面就会变成列向量。这是为了保证输出结果为向量。比如:

arr1 = [1 2]
arr2 = [[300 300] [600 600]]
print(np.dot(arr1,arr2))  # [1500 1500]
print(np.dot(arr2,arr1))  # [900 1800]

iii. 矩阵与矩阵的乘积

这个就不必多说了,和线性代数里的规则一样的。

2. 数学函数

这里介绍一部分最重要的数学函数。我只会列举函数、参数与它的效果,不会举例说明,大家可以自行尝试,如果发现了什么彩蛋可以在评论区告诉我。

函数名 参数 作用
np.abs() (数组) 对每个元素取绝对值
np.sin() (数组) 对每个元素取正弦,输出为科学计数法
np.cos() (数组) 对每个元素取余弦,输出为科学计数法
np.tan() (数组) 对每个元素取正切,输出为科学计数法
np.pi π
np.exp() (数组) 每个元素x变成e^x
np.log() (数组) 每个元素x变成lnx

这里提两个我发现的小彩蛋:第一、tan(pi/2)不会直接输出无穷大,而是一个非常非常大的数字;第二、如果要把每个数字x变成log_a(x),只需用换底公式计算即可。由于我的网站暂时疑似不支持latex公式,我只好让你们自己去搜换底公式了。

3. 聚合函数

我不知道为啥叫这个名,反正就是介绍几个常见的函数。

i. 最大值、最小值

使用np.max()np.min()函数。参数是(数组,axis)。如果数组是向量,则不需要axis值;是矩阵的话,如果不写,返回的是整个矩阵中的最大(或最小)元素;如果axis = 0,则把矩阵压缩成一个向量,每一个元素是原本每列中最大(或最小)的数字;axis = 1时则相反。比如:

arr = [[1 2 3] [4 5 6]]
print(np.max(arr))  # 6
print(np.max(arr, axis = 0))  # [4 5 6]
print(np.max(arr, axis = 1))  # [3 6]

ii. 求和、求积

使用np.sum()np.prod()函数。参数和上述完全一样。如果axis = 0,则把矩阵压缩成一个向量,每一个元素是原本每列的和或积。比如:

arr = [[1 2 3] [4 5 6]]
print(np.sum(arr))  # 21
print(np.sum(arr, axis = 0))  # [5 7 9]
print(np.sum(arr, axis = 1))  # [6 15]

iii. 均值、标准差

使用np.mean()np.std()函数。参数和上述完全一样。比如:

arr = [[1 2 3] [4 5 6]]
print(np.mean(arr))  # 3.5
print(np.mean(arr, axis = 0))  # [2.5 3.5 4.5]
print(np.mean(arr, axis = 1))  # [2 5]

iv. 聚合函数的注意事项

  • 关于axis的理解:axis = 0 时,向量的长度保持行长度;axis = 1 时,向量的长度保持列长度
  • 由于大型数组可能出现数据缺失情况,所有聚合函数有安全版本:在函数前面加一个nan,比如np.nanmin()等等。如果数组里有数字缺失,会直接忽略这个数字。

七、布尔型数组

除了整数型数组和浮点型数组,还有一种常用的数组叫布尔型数组

1. 创建布尔型数组

布尔型数组的创建需要借助各种比较符号:>,<,==,!=,≥,≤。

我们可以把数组与单个数字比较,也可以将两个相同形状的数组比较。比如:

arr1 = [[1 2 3] [4 5 6]]
print(arr1 > 4)  # [[False False False] [False True True]]
arr2 = [[6 5 4] [3 2 1]]
print(arr1 > arr2)  # 懒得写了

连接不同的布尔型数组,依靠与&、或|、非~。注意,这里和Python可不一样了!

2. True的数量

统计布尔型数组中True的数量,有三种函数:

  • np.sum()函数,统计True的个数。
  • np.any()函数,只要有一个True,就返回True。
  • np.all()函数,全为True才返回True。

比如:

arr1 = [1 2 3 4 5]
arr2 = [5 4 3 2 1]
arr_bool = (arr1 == arr2)
print(np.sum(arr_bool))  # 1
print(np.any(arr_bool))  # True
print(np.all(arr_bool))  # False

3. 布尔型数组作为掩码

布尔型数组的第一个作用,就是以布尔型数组为索引,找出数组中所有满足条件的元素。比如:

arr1 = [[1 2 3] [4 5 6]]
arr2 = arr1[arr1 > 4]
print(arr2)  # [5 6]

注意,在掩码之后,数组会退化成向量。

4. 布尔型数组寻找满足条件的元素所在位置

布尔型数组的第二个作用,是寻找满足条件的元素所在的位置。这需要使用np.where()函数,参数是一个布尔型数组,返回的值是一个元组,元组只有一个元素,是所有满足条件的元素下标组成的列表。如果加上一个下标[0],就返回一个数组。比如:

arr = [1 2 3 4 5]
print(np.where(arr > 2))  # (array([2,3,4]),)
print(np.where(arr > 2)[0])  # [2 3 4]

那就有一个小连招,能返回数组的最大值下标:np.where(arr == np.max(arr))[0]

八、从数组到张量

PyTorch作为当前首屈一指的深度学习库,吸收了NumPy的语法,又将使用CPU的数组进步到使用GPU的张量,运算速度也提高了。要调用这个库,需要安装torch库。注意PyCharm中,pytorch库并非官方库,不要下载错误。

1. 数组与张量

NumPy与PyTorch的基础语法基本一致,区别在于:

  • np换成torch;
  • array换成张量tensor;
  • n维数组换成n维张量。

数组与张量之间也能互相转化:把数组arr换成张量ts,只需ts = torch.tensor(arr);反过来,就是arr = np.array(ts)

2. 语法不同点

PyTorch只是少量修改了上文中NumPy的部分函数或方法。在原视频UP主的努力下,发现了这些改变,感谢UP主。下面是两者的不同点比较:

函数作用 NumPy的函数 PyTorch的函数 PyTorch中的特殊点
数组类型 .astype() .type()
01随机数组 np.random.dandom() torch.rand()
随机整数数组 np.random.randint() torch.randint() 不接纳向量
随机正态数组 np.random.normal() torch.normal() 不接纳向量
随机标准正态数组 np.random.randn() torch.randn()
复制数组 .copy() .clone()
数组拼接 np.concatenate() torch.cat()
数组分裂 np.split() torch.split() 第二个参数改为分裂后长度的列表
矩阵乘积 np.dot() torch.matmul()
矩阵乘积 np.dot(v,v) torch.dot() 向量乘积
矩阵乘积 np.dot(v,m) torch.mv() 向量乘以矩阵
矩阵乘积 np.dot(m,m) torch.mm() 矩阵乘积
指数 np.exp() torch.exp() 不接纳矩阵
对数 np.log() torch.log() 不接纳矩阵
平均数 np.mean() torch.mean() 只接纳浮点型数组
标准差 np.std() torch.std() 只接纳浮点型数组

3. 预告片

PyTorch作为深度学习的库,肯定是适配了很多深度学习的函数,不过这并非本节主题,我就不介绍了,大家可以期待我更新PyTorch教程!

九、总结

内容实在太多了,大家不一定要记住,只要对有哪些函数有大概的印象,再来这个教程里寻找相应函数即可。希望对大家有帮助!

Leave a comment