0
点赞
收藏
分享

微信扫一扫

Numpy快速入门(二)——数组入门(数组的创建+数组的基本操作)

梅梅的时光 2022-02-05 阅读 101

目录

说明

  • 本文不会介绍所有的数组函数,并且也不会讲解所有参数,详情可参阅官方文档。
  • 带有方括号[ ]的参数可以省略。
  • 每一小段代码的输出都会写在下方的注释中。

一、数组的创建

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),k1TypeII:(0k,0)(1k,1)(sk,s),k1

其中 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]=ai1i2ik

从而索引后的结果 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[ai1i2ik]

我们还可以为多个维度提供索引,但要注意每个维度的索引数组必须具有相同的形状

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 Nk+1=M,即 k = N − M + 1 ≥ 2 k=N-M+1\geq2 k=NM+12.

现在我们将 ( 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),kiN 均是可兼容的,则 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 ...

举报

相关推荐

0 条评论