目录
说明
- 本文不会介绍所有的数组函数,并且也不会讲解所有参数,详情可参阅官方文档。
- 带有方括号[ ]的参数可以省略。
- 每一小段代码的输出都会写在下方的注释中。
一、数组的创建
1.1 利用现有的数据创建数组
函数 | 作用 |
---|---|
np.array(object, dtype=None, copy=True) | object可以是标量、列表、元组等对象,dtype是数据类型,copy控制是否复制object;将object转换为n维数组 |
np.asarray(object, dtype=None) | 与np.array()的作用类似,不同之处请见1.1.2节 |
np.fromfunction(f, shape, dtype=float) | shape是一个大小为N的整型元组,f是具有N个参数的标量函数;返回shape形状的数组,其中的每个元素是按照其索引传入f后得到的值 |
np.fromstring(s, dtype=float, sep) | s是字符串,sep是分隔符;将字符串按分隔符分割后转换为数组 |
1.1.1 np.array()
np.array()是最常用的一种方法。
""" example 1 """
A = np.array([1, 2])
print(A)
print(type(A))
# [1 2]
# <class 'numpy.ndarray'>
""" example 2 """
A = np.array([(1, 2), (3, 4)])
print(A)
print(type(A))
# [[1 2]
# [3 4]]
# <class 'numpy.ndarray'>
""" example 3 """
A = np.array([1, 2, 3], dtype=np.complex128)
print(A)
# [1.+0.j 2.+0.j 3.+0.j]
""" example 4 """
A = np.array([1, 2, 3])
B = np.array(A)
print(B)
print(type(B))
# [1 2 3]
# <class 'numpy.ndarray'>
""" example 5 """
A = np.array([1, 2, 3])
B = np.array(A)
C = np.array(A, copy=False)
A[0] = 2
print(B, '\n', C)
# [1 2 3]
# [2 2 3]
从example 4中不难发现,我们的object也可以是ndarray。
从example 5中可以看出,如果关掉copy,我们修改A的值是会影响到C的(因为此时C指向A)。事实上,如果A不是ndarray(比如列表),那么无论copy设置成什么,我们的np.array()都会复制一份A。
1.1.2 np.asarray()
np.array()和np.asarray()都可以将源数据object转化为ndarray,但主要区别就在于:当源数据是ndarray时,array仍然会copy出一个副本,占用新的内存,但asarray不会。
从下面的两个例子便能看出区别:
""" example 1 """
A = [1, 2, 3]
B = np.array(A)
C = np.asarray(B)
A[0] = 2
print(A, '\n', B, '\n', C, sep='')
# [2, 2, 3]
# [1 2 3]
# [1 2 3]
""" example 2 """
A = np.array([1, 2, 3])
B = np.array(A)
C = np.asarray(A)
A[0] = 2
print(A, '\n', B, '\n', C, sep='')
# [2 2 3]
# [1 2 3]
# [2 2 3]
为了方便记忆,我们可以这样理解:
# 不严谨的来讲,以下两种语句等价:
np.array(object, copy=False)
np.asarray(object)
如无特殊需要,尽量使用np.array()而不是np.asarray()。
1.1.3 np.fromfunction()
我们首先定义一个函数
def f(x, y):
return x + y
因为 f 有两个参数,所以shape的大小也应为2。我们不妨设shape=(3, 2),那么最终返回的就是大小为 3 × 2 3\times 2 3×2 的数组. 记该数组为 A A A,那么自然有
A [ i ] [ j ] = f ( i , j ) = i + j A[i][j]=f(i,j)=i+j A[i][j]=f(i,j)=i+j
我们来看一下效果:
A = np.fromfunction(f, (3, 2), dtype=int)
print(A)
# [[0 1]
# [1 2]
# [2 3]]
若使用python的匿名函数,我们仅需一行代码:
A = np.fromfunction(lambda x, y: x + y, (3, 2), dtype=int)
使用 np.fromfunction(),我们可以很轻松的创建元素与索引相关的数组,例如,对于下面这种数组:
[ T r u e F a l s e F a l s e F a l s e T r u e F a l s e F a l s e F a l s e T r u e ] \begin{bmatrix} \mathrm{True} & \mathrm{False} & \mathrm{False} \\ \mathrm{False} & \mathrm{True} & \mathrm{False} \\ \mathrm{False} & \mathrm{False} & \mathrm{True} \\ \end{bmatrix} ⎣⎡TrueFalseFalseFalseTrueFalseFalseFalseTrue⎦⎤
我们可以这样来创建:
A = np.fromfunction(lambda i, j: i == j, (3, 3))
1.1.4 np.fromstring()
对于下面这样的字符串:
s = '1 2 3 4 5'
若要将其转换为整型数组,我们的第一反应是:
A = np.array(list(map(int, s.split())))
print(A)
# [1 2 3 4 5]
但是使用 np.fromstring(),我们可以更方便地完成创建。注意到字符串中的每个数字是由空格分割的,因此只需要设置sep=’ '即可:
A = np.fromstring(s, dtype=int, sep=' ')
print(A)
# [1 2 3 4 5]
1.2 根据数值范围创建数组
函数 | 作用 |
---|---|
np.arange([start], stop, [step], dtype=None) | 创建一个在 [ start, stop ) 区间内的,步长为step的数组;start省略时默认为0,step省略时默认为1 |
np.linspace(start, stop, num=50, endpoint=True, dtype=None) | 创建一个在 [ start, stop ] 区间内的,大小为num的数组;当endpoint关闭时,区间变为 [ start, stop ) |
np.logspace(start, stop, num=50, endpoint=True, base=10.0, dtype=None) | 相当于在np.linspace()结果的基础上,对其中的每个元素 a a a 作变换 a : = b a s e a a:=base^a a:=basea |
np.geomspace(start, stop, num=50, endpoint=True, dtype=None) | 与np.logspace()类似,只不过np.geomspace()会自动选择合适的基底 |
1.2.1 np.arange()
""" example 1 """
A = np.arange(6)
B = np.arange(2, 6)
C = np.arange(2, 10, 2)
print(A, '\n', B, '\n', C, sep='')
# [0 1 2 3 4 5]
# [2 3 4 5]
# [2 4 6 8]
""" example 2 """
A = np.arange(6, -1, -1, dtype=float)
print(A)
# [6. 5. 4. 3. 2. 1. 0.]
""" example 3 """
A = np.arange(6, -1, -1.0)
print(A)
# [6. 5. 4. 3. 2. 1. 0.]
1.2.2 np.linspace()
""" example 1 """
A = np.linspace(1, 10, 10)
B = np.linspace(1, 10, 10, endpoint=False)
print(A, '\n', B, sep='')
# [ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
# [1. 1.9 2.8 3.7 4.6 5.5 6.4 7.3 8.2 9.1]
1.2.3 np.logspace()
该函数与np.linspace()函数密切相关,我们先来看np.linspace()的一个例子:
A = np.linspace(1, 5, 5)
print(A)
# [1. 2. 3. 4. 5.]
将linspace换为logspace,并设置基底为2:
A = np.logspace(1, 5, 5, base=2)
根据前面表格中的描述,结果应该为:
[ 2 1 , 2 2 , 2 3 , 2 4 , 2 5 ] [2^1,2^2,2^3,2^4,2^5] [21,22,23,24,25]
输出也的确如此:
# [ 2. 4. 8. 16. 32.]
1.2.4 np.geomspace()
np.logspace() 和 np.geomspace() 都会生成指数增长的数组,区别在于,logspace生成的数组是:
[ b a s e s t a r t , ⋯ , b a s e s t o p ] [base^{start},\cdots,base^{stop}] [basestart,⋯,basestop]
而geomspace生成的数组是:
[ s t a r t , ⋯ , s t o p ] [start,\cdots,stop] [start,⋯,stop]
例如,设置start=1,stop=1000,num=4,则geomspace会自动选择基底10:
A = np.geomspace(1, 1000, num=4, dtype=int)
print(A)
# [ 1 10 100 1000]
1.3 创建特定类型的数组
函数 | 作用 |
---|---|
np.empty(shape) | shape为整数或整型列表/元组;返回shape形状的空数组(未初始化的数组) |
np.empty_like(prototype) | prototype是一个给定的数组;返回一个与给定数组形状相同的空数组 |
np.ones(shape) | 返回shape形状的全1数组 |
np.ones_like(prototype) | 返回与给定数组形状相同的全1数组 |
np.zeros(shape) | 返回shape形状的全0数组 |
np.zeros_like(prototype) | 返回与给定数组形状相同的全0数组 |
np.full(shape, value) | value为标量或数组;当value是标量时,返回一个与shape形状相同的全value数组;value是数组的情形见1.3.4节 |
np.full_like(prototype, value) | 返回与给定数组形状相同的全value数组 |
np.eye(m, n=m, k=0) | m是矩阵的行数,n是矩阵的列数,k控制对角线;返回主对角线全为1,其余位置全为0的矩阵(数组) |
np.identity(n) | 返回n阶单位矩阵 |
1.3.1 np.empty()
empty与zeros不同,它不会将元素初始化为0(但也并不代表数组中没有数字),所以速度可能会比zeros快一些。
""" example 1 """
A = np.empty(2)
print(A)
# [4.64929150e+143 2.37662553e-045]
""" example 2 """
A = np.empty([2, 2])
print(A)
# [[ 1.05328699e-311 -6.77097176e-233]
# [ 1.05328694e-311 1.05328694e-311]]
""" example 3 """
A = np.array([1, 2, 3])
B = np.empty_like(A)
print(B)
# [5111887 5701703 5111877]
1.3.2 np.ones()
""" example 1 """
A = np.ones([2, 2])
print(A)
# [[1. 1.]
# [1. 1.]]
""" example 2 """
A = np.array([1, 2, 3])
B = np.ones_like(A)
print(B)
# [1 1 1]
1.3.3 np.zeros()
""" example 1 """
A = np.zeros(5)
print(A)
# [0. 0. 0. 0. 0.]
""" example 2 """
A = np.array([1, 2, 3])
B = np.zeros_like(A)
print(B)
# [0 0 0]
1.3.4 np.full()
""" example 1 """
A = np.full((2, 3), 3)
print(A)
# [[3 3 3]
# [3 3 3]]
""" example 2 """
A = np.full((3, 3), [1, 2, 3])
print(A)
# [[1 2 3]
# [1 2 3]
# [1 2 3]]
可以看出,当value是数组时,返回的数组中每一行都是用value进行填充的。需要注意的是,在这种情形下,shape[1] 需要与 len(value) 保持一致,否则会报错:
A = np.full((3, 4), [1, 2, 3])
print(A)
# ValueError: could not broadcast input array from shape (3,) into shape (3,4)
我们再来看 np.full_like():
A = np.arange(6, dtype=int)
B = np.full_like(A, 1)
C = np.full_like(A, 0.1)
D = np.full_like(A, 0.1, dtype=float)
print(A, '\n', B, '\n', C, '\n', D, sep='')
# [0 1 2 3 4 5]
# [1 1 1 1 1 1]
# [0 0 0 0 0 0]
# [0.1 0.1 0.1 0.1 0.1 0.1]
1.3.5 np.eye()
先介绍一下对角线。
主对角线:
( 0 , 0 ) → ( 1 , 1 ) → ⋯ → ( s , s ) (0,0)\to(1,1)\to\cdots\to(s,s) (0,0)→(1,1)→⋯→(s,s)
例如,下面两个矩阵中,1所在的路径均为主对角线:
[ 1 0 0 1 ] , [ 1 0 0 0 1 0 ] \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ \end{bmatrix},\quad \begin{bmatrix} 1 & 0 & 0\\ 0 & 1 & 0\\ \end{bmatrix} [1001],[100100]
副对角线:
T y p e I : ( 0 , 0 + k ) → ( 1 , 1 + k ) → ⋯ → ( s , s + k ) , k ≥ 1 T y p e I I : ( 0 − k , 0 ) → ( 1 − k , 1 ) → ⋯ → ( s − k , s ) , k ≤ − 1 \mathrm{Type \;I}:\; (0,0+k)\to(1,1+k)\to\cdots\to(s,s+k),\quad k\geq 1 \\ \mathrm{Type \;II}:\; (0-k,0)\to(1-k,1)\to\cdots\to(s-k,s),\quad k\leq -1 \\ TypeI:(0,0+k)→(1,1+k)→⋯→(s,s+k),k≥1TypeII:(0−k,0)→(1−k,1)→⋯→(s−k,s),k≤−1
其中 k k k 是整数.
我们先看第一型副对角线,不妨就设 k = 1 k=1 k=1,则路径为
( 0 , 1 ) → ( 1 , 2 ) → ( 2 , 3 ) → ⋯ → ( s , s + 1 ) (0,1)\to(1,2)\to(2,3)\to\cdots\to(s,s+1) (0,1)→(1,2)→(2,3)→⋯→(s,s+1)
例如,下面两个矩阵中,1所在的路径均为第一型副对角线:
[ 0 1 0 0 0 1 0 0 0 ] , [ 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 ] \begin{bmatrix} 0 & 1 & 0\\ 0 & 0 & 1\\ 0 & 0 & 0\\ \end{bmatrix},\quad \begin{bmatrix} 0 & 1 & 0&0&0\\ 0 & 0 & 1&0&0\\ 0 & 0 & 0&1&0\\ \end{bmatrix} ⎣⎡000100010⎦⎤,⎣⎡000100010001000⎦⎤
相当于主对角线往上平移 k k k 个单位得到第一型副对角线。
再看第二型副对角线,不妨设 k = − 1 k=-1 k=−1,则路径为:
( 1 , 0 ) → ( 2 , 1 ) → ( 3 , 2 ) → ⋯ → ( s + 1 , s ) (1,0)\to(2,1)\to(3,2)\to\cdots\to(s+1,s) (1,0)→(2,1)→(3,2)→⋯→(s+1,s)
例如,下面两个矩阵中,1所在的路径均为第二型副对角线:
[ 0 0 0 1 0 0 0 1 0 ] , [ 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 ] \begin{bmatrix} 0 & 0 & 0\\ 1 & 0 & 0\\ 0 & 1 & 0\\ \end{bmatrix},\quad \begin{bmatrix} 0 & 0 & 0\\ 1 & 0 & 0\\ 0 & 1 & 0\\ 0 & 0 & 1 \\ 0 & 0 & 0 \\ \end{bmatrix} ⎣⎡010001000⎦⎤,⎣⎢⎢⎢⎢⎡010000010000010⎦⎥⎥⎥⎥⎤
相当于主对角线往下平移 − k -k −k 个单位得到第二型副对角线。
于是我们可以作一个总结:
- k = 0 k=0 k=0 时为主对角线。
- k > 0 k>0 k>0 时为第一型副对角线,相当于主对角线往上平移 k k k 个单位得到。
- k < 0 k<0 k<0 时为第二型副对角线,相当于主对角线往下平移 − k -k −k 个单位得到。
在知道了什么是对角线后,下面的例子就不难理解了:
""" example 1 """
A = np.eye(3)
print(A)
# [[1. 0. 0.]
# [0. 1. 0.]
# [0. 0. 1.]]
""" example 2 """
A = np.eye(3, 5)
print(A)
# [[1. 0. 0. 0. 0.]
# [0. 1. 0. 0. 0.]
# [0. 0. 1. 0. 0.]]
""" example 3 """
A = np.eye(3, 4, k=2)
print(A)
# [[0. 0. 1. 0.]
# [0. 0. 0. 1.]
# [0. 0. 0. 0.]]
""" example 4 """
A = np.eye(5, 3, k=-3)
print(A)
# [[0. 0. 0.]
# [0. 0. 0.]
# [0. 0. 0.]
# [1. 0. 0.]
# [0. 1. 0.]]
1.3.6 np.identity()
以下三种语句等价:
np.eye(n)
np.eye(n, n)
np.identity(n)
1.4 创建对角矩阵、上/下三角矩阵
函数 | 作用 |
---|---|
np.diag(v, k=0) | k控制对角线;当v是二维数组时,第k条对角线作为一维数组返回;当v是一维数组时, 会将该数组作为第k条对角线并构建对角矩阵 |
np.tri(m, n=m, k=0) | 创建一个 m×n 的矩阵,其中第k条对角线及其以下的部分全为1,其余位置全为0 |
np.tril(object, k=0) | object是二维数组;返回一个object的副本,其中该副本第k条对角线以上的部分置为0 |
np.triu(object, k=0) | 返回一个object的副本,其中该副本第k条对角线以下的部分置为0 |
1.4.1 np.diag()
""" example 1 """
A = np.array([[1, 2], [3, 4]])
print(np.diag(A))
print(np.diag(A, k=1))
# [1 4]
# [2]
""" example 2 """
A = np.array([1, 2, 3])
print(np.diag(A))
print(np.diag(A, k=1))
# [[1 0 0]
# [0 2 0]
# [0 0 3]]
# [[0 1 0 0]
# [0 0 2 0]
# [0 0 0 3]
# [0 0 0 0]]
1.4.2 np.tri()
""" example 1 """
A = np.tri(3, 4, k=1)
print(A)
# [[1. 1. 0. 0.]
# [1. 1. 1. 0.]
# [1. 1. 1. 1.]]
""" example 2 """
A = np.tri(3, 4, k=-1)
print(A)
# [[0. 0. 0. 0.]
# [1. 0. 0. 0.]
# [1. 1. 0. 0.]]
""" example 3 """
A = np.tri(2, k=-1)
print(A)
# [[0. 0.]
# [1. 0.]]
1.4.3 np.tril()
""" example 1 """
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(np.tril(A))
# [[1 0 0]
# [4 5 0]
# [7 8 9]]
""" example 2 """
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(np.tril(A, k=-1))
# [[0 0 0]
# [4 0 0]
# [7 8 0]]
1.4.4 np.triu()
""" example 1 """
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(np.triu(A))
# [[1 2 3]
# [0 5 6]
# [0 0 9]]
""" example 2 """
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(np.triu(A, k=1))
# [[0 2 3]
# [0 0 6]
# [0 0 0]]
二、数组的基本操作
2.1 查看数组信息
我们已经知道,在Numpy中,一维数组表示向量,二维数组表示矩阵,而对于三维及以上的数组,我们就要用张量来称呼了。
在Numpy中,数组的维度还有另外一个名称——轴(axes),几维数组就有几个轴。例如,对于下面这样的二维数组:
[[0., 0., 0.],
[1., 1., 1.]]
它有两个轴,且该数组沿第一个轴的长度为2,沿第二个轴的长度为3,即:
看到这里你可能会疑惑,为什么第一个轴是沿行方向的而第二个轴是沿列方向的呢? 为什么不能让第一个轴沿列方向而第二个轴沿行方向呢?
这是因为,我们在进行索引时,索引形式形如 A [ i ] [ j ] A[i][j] A[i][j]。行索引在第一个位置,列索引在第二个位置,所以我们的第一个轴就规定为沿行方向,第二个轴就规定为沿列方向。
我们有以下方法查看数组的信息:
方法 | 作用 |
---|---|
ndarray.ndim | 返回数组的维数(秩) |
ndarray.shape | 以元组形式返回数组的形状;例如对于二维数组,返回的形式为(m, n) |
ndarray.size | 返回数组中元素的总数;这等于shape中所有元素的乘积 |
ndarray.dtype | 返回数组的数据类型(因为数组要求其中的所有元素的数据类型必须相同) |
我们通过一个例子进一步学习这些方法:
A = np.array([[1, 2, 3], [4, 5, 6]])
print(A.ndim)
print(A.shape)
print(A.size)
print(A.dtype)
# 2
# (2, 3)
# 6
# int32
2.2 算术运算符
对数组应用算术运算符则会 按元素(又称逐个元素) 进行处理。
A = np.array([1, 2, 3])
B = np.array([4, 5, 6])
print(A + B)
# [5 7 9]
print(B - A)
# [3 3 3]
print(A + 2)
# [3 4 5]
print(A * 2)
# [2 4 6]
print(A / 2)
# [0.5 1. 1.5]
print(A ** 2)
# [1 4 9]
print(A * B)
# [ 4 10 18]
print(A ** B)
# [ 1 32 729]
print(A >= 2)
# [False True True]
当对不同精度(数据类型)的数组进行运算时,结果的精度(数据类型)为参与运算的数组的最高精度(向上转换)。
例如,将数据类型分别为int32和float64的两个数组相加,结果将是精度为float64的数组:
A = np.array([1, 2, 3], dtype=int)
B = np.array([4, 5, 6], dtype=float)
C = A + B
print(C)
print(C.dtype)
# [5. 7. 9.]
# float64
将数据类型分别为float64和complex128的两个数组相加,结果将是精度为complex128的数组:
A = np.array([1, 2, 3], dtype=complex)
B = np.array([4, 5, 6], dtype=float)
C = A + B
print(C)
print(C.dtype)
# [5.+0.j 7.+0.j 9.+0.j]
# complex128
2.3 索引与切片
Numpy的n维数组可以像Python的列表那样进行索引、赋值、切片、迭代等操作。
2.3.1 索引(indexing)
n维数组索引和赋值的方法和列表一样:
A = np.arange(12)
print(A[3])
# 3
A[5] = 9
print(A)
# [ 0 1 2 3 4 9 6 7 8 9 10 11]
当然我们还可以同时索引多个元素:
A = np.arange(12)
indexes_1 = np.array([1, 4, 7, 9])
indexes_2 = [1, 4, 7, 9]
print(A[indexes_1])
print(A[indexes_2])
# [1 4 7 9]
# [1 4 7 9]
可以看出,indexes为n维数组或列表时都可以完成对多个元素的索引,但indexes为元组时会索引失败。
对于二维列表,索引某个元素的方法为 A [ i ] [ j ] A[i][j] A[i][j]。 但在二维数组中,除了该方法以外,还有另外一种方法: A [ i , j ] A[i, j] A[i,j]。
A = np.array([[1, 2, 3], [4, 5, 6]])
print(A[0, 1])
print(A[1, -1])
# 2
# 6
如果索引数小于数组的维度,则会获得一个子数组:
A = np.array([[1, 2, 3], [4, 5, 6]])
print(A[0]) # 相当于取A的第0行
# [1 2 3]
回到一维数组,如果数组容量非常大,并且我们需要索引的元素非常多时,有没有比较简便的方法呢?
答案就是切片。
2.3.2 切片(slicing)
- 不止n维数组和列表,python中的任何有序序列都支持切片,如字符串,元组等。
- 切片返回的结果类型与被切片对象的类型一致。对数组切片得到数组,对列表切片得到列表,对字符串切片得到字符串。
我们接下来只讨论数组切片,其标准格式为:
A [ s t a r t : s t o p : s t e p ] , s t e p ≠ 0 A[start:stop:step],\quad step\neq0 A[start:stop:step],step=0
start是切片起点索引,end是切片终点索引,注意这里是左开右闭区间,即 [ s t a r t , s t o p ) [start, stop) [start,stop)。
例如:
A = np.arange(10)
print(A[1:6:1])
print(A[5:0:-1])
# [1 2 3 4 5]
# [5 4 3 2 1]
假设step给定:
- 若只省略stop,则会从start开始沿切片方向一直切片到数组的某一端点(包含端点)。
- 若只省略start,则会从数组的某一端点沿切片方向一直切片到stop(不包含stop)。
例如:
A = np.arange(10)
print(A[1::1])
print(A[:0:-1])
# [1 2 3 4 5 6 7 8 9]
# [9 8 7 6 5 4 3 2 1]
- 若start和stop都省略,则会从数组的其中一个端点开始沿切片方向一直切片到另一个端点(包含端点)
例如:
A = np.arange(10)
print(A[::1])
print(A[::-1])
# [0 1 2 3 4 5 6 7 8 9]
# [9 8 7 6 5 4 3 2 1 0]
于是我们得到了一个快速反转数组的方法:
A = A[::-1]
因为step为1时可以省略,并且可以进一步省略一个冒号,即下面三种语句等价:
A[start:stop:1]
A[start:stop:]
A[start:stop]
所以我们还能得到一个复制数组的方法:
B = A[:]
在熟悉了切片之后,我们可以完成更复杂的操作,例如对于下面这样的矩阵:
A = [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ] A= \begin{bmatrix} 1 & 2 & 3 & 4 \\ 5 & 6 & 7 & 8 \\ 9 & 10 & 11 & 12 \\ 13 & 14 & 15 & 16 \\ \end{bmatrix} A=⎣⎢⎢⎡15913261014371115481216⎦⎥⎥⎤
如果我们要想取出 A A A 的最后三行,可以这样做:
# 以下两种语句等价,试思考为什么等价
print(A[1:, :])
print(A[1:])
如果我们要想取出 A A A 的最后三列,则应该这样做:
# 试思考为什么只有这一种方案
print(A[:, 1:])
若要想获得 2 , 4 , 10 , 12 2, 4, 10, 12 2,4,10,12 这四个元素,我们只需进行如下索引(切片):
# 相当于取第0行、第3行与第1列、第3列交叉位置的元素
print(A[::2, 1::2])
# [[ 2 4]
# [10 12]]
2.3.3 高级索引
这一小节我们将讨论更高级的索引(切片),它能帮你方便快捷地选取某些元素。
2.3.3.1 省略号(…)的使用
回到2.3.1节,我们知道,对于一个二维数组,取出它沿第一个轴方向上的第一个子数组(即取第0行),应该使用 A[0]
,事实上,它等价于 A[0, :]
。对于三维数组而言,取它在第三个轴方向上的第k个子数组,我们应该用 A[:, :, k]
,这个时候我们就没有更简洁的表示形式了。
那么问题来了,对于N维数组,当N非常大时,如果我们要想取它在某个轴上的第k个子数组,我们的索引会涉及到大量的冒号和逗号,而手动输入是不现实的,那该如何解决这一问题呢?
好在Numpy提供了一种更为简洁的表示方式,当有两个及以上的冒号时,我们可以用三个点 ...
来代替。例如,对于上面的第二个例子,我们可以简洁地写成 A[..., k]
。
假如 A 是一个五维数组,则:
A[1,2,...]
等价于A[1,2,:,:,:]
A[...,3]
等价于A[:,:,:,:,3]
A[4,...,5,:]
等价于A[4,:,:,5,:]
我们来看一个三维数组的例子:
A = np.array([[[0, 1, 2],
[10, 12, 13]],
[[100, 101, 102],
[110, 112, 113]]])
print(A[1, :, :])
print(A[1, ...])
# [[100 101 102]
# [110 112 113]]
# [[100 101 102]
# [110 112 113]]
print(A[..., 2])
# [[ 2 13]
# [102 113]]
2.3.3.2 使用索引数组进行索引
在2.3.1节中我们提到了,可以使用n维数组或列表完成对多个元素的索引。为了更贴近Numpy,接下来我们摒弃列表,只使用n维数组去索引。该方法又称为使用索引数组进行索引。
先来看一个简单的例子:
A = np.arange(12)
i = np.array([0, 1, 4, 7])
print(A[i])
# [0 1 4 7]
当我们的索引数组是二维数组时,返回的结果也是二维数组:
A = np.arange(12)
i = np.array([[0, 1], [4, 7]])
print(A[i])
# [[0 1]
# [4 7]]
我们可以从数学上将其推广到更一般的形式,假设 indexes
是
k
k
k 维数组,即:
i n d e x e s [ i 1 , i 2 , ⋯ , i k ] = a i 1 i 2 ⋯ i k indexes[i_1,i_2,\cdots,i_k]=a_{i_1i_2\cdots i_k} indexes[i1,i2,⋯,ik]=ai1i2⋯ik
从而索引后的结果 A[indexes]
满足:
A [ i n d e x e s ] [ i 1 , i 2 , ⋯ , i k ] = A [ a i 1 i 2 ⋯ i k ] A[indexes][i_1,i_2,\cdots,i_k]=A[a_{i_1i_2\cdots i_k}] A[indexes][i1,i2,⋯,ik]=A[ai1i2⋯ik]
我们还可以为多个维度提供索引,但要注意每个维度的索引数组必须具有相同的形状:
A = np.array([[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11]])
i = np.array([[0, 1],
[1, 2]])
j = np.array([[2, 1],
[3, 3]])
print(A[i, j])
# [[ 2 5]
# [ 7 11]]
print(A[i, 2])
# [[ 2 6]
# [ 6 10]]
print(A[i, :])
# [[[ 0 1 2 3]
# [ 4 5 6 7]]
# [[ 4 5 6 7]
# [ 8 9 10 11]]]
我们既然能够完成对多个元素的索引,那我们也应该可以完成对多个元素的赋值:
""" example 1 """
A = np.array([0, 1, 2, 3, 4])
A[[0, 1, 3]] = 5
print(A)
# [5 5 2 5 4]
""" example 2 """
A = np.array([0, 1, 2, 3, 4])
A[[0, 1, 3]] = [7, 8, 9]
print(A)
# [7 8 2 9 4]
""" example 3 """
A = np.array([0, 1, 2, 3, 4])
A[[0, 1, 3]] = A[[0, 1, 3]] + 1
print(A)
# [1 2 2 4 4]
""" example 4 """
A = np.array([0, 1, 2, 3, 4])
A[[0, 0, 3]] += 1
print(A)
# [1 1 2 4 4]
可以看到,example 4中, 0 0 0 的值只增加了一次,为什么呢?
这是因为,A[[0, 0, 3]]
本身就是 [0, 0, 3]
,执行 A[[0, 0, 3]] += 1
相当于执行 A[[0, 0, 3]] = A[[0, 0, 3]] + 1
,也就相当于执行 A[[0, 0, 3]] = [1,1,4]
,该语句等价于
A[0] = 1
A[0] = 1
A[3] = 4
可以看出 A[0]
被重复赋值了,因此其值自然就不会增加两次。
那如果我们想让 A[0]
的值增加两次(在保持 A[3]
只增加一次的基础上),该怎么做呢?
我们只需要修改赋值的地方:
A[[0, 0, 3]] += [x, 2, 1]
其中 x 可以为任意的数字,因为随后 A[0]
的值就会被 2 覆盖。上述语句等价于:
A[0] = x
A[0] = 2
A[3] = 1
2.3.3.3 使用布尔数组进行索引
首先看一个例子:
A = np.array([[1, 2, 3],
[4, 5, 6]])
i = np.array([[True, False, False],
[False, False, True]])
print(A[i])
# [1 6]
可以看出,我们只索引了 True
的地方,而为 False
的地方没有进行索引。
利用这一特点,我们可以完成一些有趣的操作。例如,对于上述的数组 A,如果想让 A 中大于等于3的元素都置为0,我们可以先创建一个布尔数组,随后赋值:
A = np.array([[1, 2, 3], [4, 5, 6]])
i = A >= 3
print(i)
# [[False False True]
# [ True True True]]
A[i] = 0
print(A)
# [[1 2 0]
# [0 0 0]]
我们再来看几个例子进一步熟悉布尔数组索引:
A = np.array([[1, 2, 3],
[5, 6, 7]])
i = np.array([False, True])
j = np.array([True, False, True])
print(A[i, :])
# [[5 6 7]]
print(A[:, j])
# [[1 3]
# [5 7]]
print(A[i, j])
# [5 7]
2.4 广播机制(broadcasting)
Numpy在处理两个或多个不同形状的数组时会采取广播机制。简单来讲,就是扩展其中的一个数组或同时扩展两个数组使得它们的形状相同,这样才能继续进行运算。
2.4.1 标量与一维数组
例如,标量 * 数组
这一运算中,就采取了广播机制:
a = np.array([1, 2, 3])
b = 2
print(a * b)
# [2 4 6]
在计算 A * b
时,Numpy会将 b
扩展成和 A
一样大小的数组再执行按元素运算(算术运算符是按元素运算的,而按元素运算需要两个数组的形状相同):
对于上面的例子,我们也可以这样做:
a = np.array([1, 2, 3])
b = np.array([2, 2, 2])
print(a * b)
它们的结果都一样,但使用广播机制无疑是更好的,因为广播机制占用更少的内存空间,并且使用起来更方便。
2.4.2 一维数组与二维数组
考虑 一维数组 + 二维数组
的情形,因为 +
是算术运算符,则需要按元素运算,但它们的形状并不相同,因此Numpy会采取广播机制:一维数组通过复制自身扩展成与二维数组形状相同的数组。
a = np.array([[0, 0, 0],
[10, 10, 10],
[20, 20, 20],
[30, 30, 30]])
b = np.array([1, 2, 3])
print(a + b)
# [[ 1 2 3]
# [11 12 13]
# [21 22 23]
# [31 32 33]]
具体广播过程如下:
这里的 b
是行向量,因此会沿行方向进行广播(即向下);如果 b
是列向量,会沿列方向进行广播(即向右)。
a = np.array([[0, 0, 0],
[10, 10, 10],
[20, 20, 20],
[30, 30, 30]])
b = np.array([[1],
[2],
[3],
[4]])
print(a + b)
# [[ 1 1 1]
# [12 12 12]
# [23 23 23]
# [34 34 34]]
具体示意图这里不再给出,请读者自行想象。
2.4.3 一维数组(列向量)与一维数组(行向量)
考虑 列向量 + 行向量
的情形,此时Numpy会同时广播两个数组,将其都扩展成二维数组后,再执行按元素相加。
a = np.array([[0],
[10],
[20],
[30]])
b = np.array([1, 2, 3])
print(a + b)
# [[ 1 2 3]
# [11 12 13]
# [21 22 23]
# [31 32 33]]
具体广播过程如下:
2.4.4 广播规则
看到这里,可能会有读者疑惑,到底什么时候可以广播什么时候不能广播呢?
我们先作如下定义:
对于一个 N 维数组而言,它有 N 个轴。我们对该数组使用 shape 方法可以得到它的形状,且以元组形式呈现: ( a 1 , a 2 , ⋯ , a N ) (a_1,a_2,\cdots,a_N) (a1,a2,⋯,aN),为叙述方便起见,我们将其表示为
a 1 × a 2 × ⋯ × a N (A) a_1\times a_2\times\cdots\times a_N\tag{A} a1×a2×⋯×aN(A)
例如对于一个 5 5 5 行 3 3 3 列的矩阵(二维数组),其形状就是 5 × 3 5\times 3 5×3。
现在考虑 M 维的数组(其中 M < N M<N M<N),其形状为
b 1 × b 2 × ⋯ × b M b_1\times b_2\times\cdots\times b_M b1×b2×⋯×bM
为保证右对齐后下标一致,我们假设 M 维数组的形状是
b k × ⋯ × b N (B) b_k\times\cdots\times b_{N}\tag{B} bk×⋯×bN(B)
且应满足 N − k + 1 = M N-k+1=M N−k+1=M,即 k = N − M + 1 ≥ 2 k=N-M+1\geq2 k=N−M+1≥2.
现在我们将 ( B ) (B) (B) 式排列在 ( A ) (A) (A) 式的下方并执行右对齐,则有:
a 1 × a 2 × ⋯ × a k × a k + 1 × ⋯ × a N b k × b k + 1 × ⋯ × b N \begin{aligned} a_1\times a_2\times\cdots\times\,\, &a_k \times a_{k+1}\times\cdots\times a_N \\ &b_k\;\!\times b_{k+1}\;\!\times\cdots\times b_N \\ \end{aligned} a1×a2×⋯×ak×ak+1×⋯×aNbk×bk+1×⋯×bN
我们这里再给出一个定义: ( a i , b i ) (a_i,b_i) (ai,bi) 称为可兼容的,当且仅当 a i = b i a_i=b_i ai=bi 或 a i a_i ai 和 b i b_i bi 中有一个为 1 1 1.
于是我们就得到了广播规则:若 ( a i , b i ) , k ≤ i ≤ N (a_i,b_i),\; k\leq i\leq N (ai,bi),k≤i≤N 均是可兼容的,则 M 维数组可以广播。
我们来通过几个例子巩固一下这个概念:
""" 可广播的 """
A (4d array): 8 x 1 x 6 x 1
B (3d array): 7 x 1 x 5
Result (4d array): 8 x 7 x 6 x 5
A (2d array): 5 x 4
B (1d array): 1
Result (2d array): 5 x 4
A (2d array): 5 x 4
B (1d array): 4
Result (2d array): 5 x 4
A (3d array): 15 x 3 x 5
B (3d array): 15 x 1 x 5
Result (3d array): 15 x 3 x 5
A (3d array): 15 x 3 x 5
B (2d array): 3 x 5
Result (3d array): 15 x 3 x 5
A (3d array): 15 x 3 x 5
B (2d array): 3 x 1
Result (3d array): 15 x 3 x 5
""" 不可广播的 """
A (1d array): 3
B (1d array): 4
A (2d array): 2 x 1
B (3d array): 8 x 4 x 3
当出现不可广播的情形,程序就会报错:ValueError: operands could not be broadcast together with shapes ...