NumPy学习笔记
NumPy(Numerical Python)是一个开源的Python科学计算库。ndarray是NumPy定义的最重要的概念,它是一个n维数组,是一组相同元素的集合。NumPy基于ndarray提供了很多实用的数学函数,包括线性代数运算、傅里叶变换、随机数生成等功能。对于同样的数值计算任务,使用NumPy要比直接使用Python编写代码要便捷得多,这是因为NumPy能够直接对数组和矩阵进行计算,而不用编写复杂的循环代码。NumPy的大部分代码使用C语言编写,这也使得NumPy要比纯Python代码的运行效率要高得多。
ndarray
ndarray是NumPy定义的最重要的概念,它是一个n维数组,是一组相同元素的集合。ndarray由两个部分组成:
- 实际存储的数组数据
- 描述这些数据的元数据
大多数对ndarray的操作都是在修改元数据,而不会改变实际存储的数组数据。ndarray的重要属性有:
- ndim: 维度,轴的数量
- shape: 数组形状
- size: 数组元素数量
- dtype: 数组元素类型
- itemsize: 每个元素的大小,以bytes为单位
- data: 数据存储区,存储实际的数组数据
下面,我们先创建一个 2 * 3 * 4 维的ndarray对象:
>>> a = np.arange(24, dtype=np.int32).reshape(2,3,4)
>>> a
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]], dtype=int32)
我们通过创建一个包含24个int32元素的一维数组,并修改它的shape为(2, 3, 4)来创建一个 2 * 3 * 4 的三维数组。先看一个它的各个属性值是什么:
>>> a.ndim
3
>>> a.shape
(2, 3, 4)
>>> a.size
24
>>> a.dtype
dtype('int32')
>>> a.itemsize
4
>>> a.data
<read-write buffer for 0x7f8a59fc3240, size 96, offset 0 at 0x106f89df0>
创建数组
np.arange
上面已经演示了如何使用 np.arange 方法来创建数组,除了指定结束值,还可以指定起始值和间隔值,如
>>> np.arange(5, 10, 2)
[5 7 9]
np.linspace
返回指定区间上均匀分布的一组元素
>>> np.linspace(0, 10, num=10, endpoint=False, retstep=True)
(array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]), 1.0)
>>> np.linspace(0, 10, num=10, endpoint=True, retstep=True)
(array([ 0. , 1.11111111, 2.22222222, 3.33333333, 4.44444444,
5.55555556, 6.66666667, 7.77777778, 8.88888889, 10. ]), 1.1111111111111112)
注意 endpoint 为 True 时,结束值会包括在返回数组内,否则不包括,相应地间隔值也会有差异。
np.logspace
与 np.linspace 类似, 不同的是返回指数空间均匀分布的一组元素
>>> np.logspace(1, 4, num=4, endpoint=True)
[ 10. 100. 1000. 10000.]
np.geomspace
与 np.logspace 类似, 不同的是返回对数空间均匀分布的一组元素
>>> np.geomspace(1, 512, num=4, endpoint=True)
[ 1. 8. 64. 512.]
np.array
np.array 通过一个 array-like 的对象来创建数组,比如
>>> np.array([1,2,3,4])
array([1, 2, 3, 4])
也可以指定数组的最小维度,如果参数数组的维度小于最小维度,会自动在shape前面补1以满足要求
>>> np.array([[1,2], [3,4]], ndmin=3)
array([[[1, 2],
[3, 4]]])
np.zeros & np.zeros_like
np.zeros 根据指定形状创建一个数组并用0填充,而 np.zeros_like 根据一个 array-like 的对象创建一个全部填充为0的数组
>>> np.zeros(5)
array([ 0., 0., 0., 0., 0.])
>>> x = np.zeros((2,4))
>>> x
array([[ 0., 0., 0., 0.],
[ 0., 0., 0., 0.]])
>>> np.zeros_like(x)
array([[ 0., 0., 0., 0.],
[ 0., 0., 0., 0.]])
np.ones & np.ones_like
与 np.zeros 和 np.zeros_like 类似,不同的是用1填充数组。
np.empty & np.empty_like
与 np.zeros 和 np.zeros_like 类似,不过数组元素都未初始化
>>> np.empty(5)
array([ 2.00000000e+000, 2.00000000e+000, 1.48219694e-323,
3.95252517e-323, 2.00000000e+000])
np.full & np.full_like
与 np.zeros 和 np.zeros_like 类似,并用指定值初始化数组元素
>>> np.full(5, 2)
array([ 2., 2., 2., 2., 2.])
np.random.rand
>>> np.random.rand(2,3)
array([[ 0.87597292, 0.10275693, 0.04778205],
[ 0.29727814, 0.57628216, 0.07926471]])
索引和切片
一维数组的索引和切片和Python列表的操作相似,我们直接讲多维数组的索引和切片操作。假设我们以 层 * 行 * 列 的方式来描述一个三维数组,即一个 2 * 3 * 4 的三维数组表示它有2层,每层有3行,每行有4列。要获取第0层第1行第2列的元素可以这样写
>>> a[0, 1, 2]
6
如果我们要查看第1层的所有元素,可以这样写
>>> a[1, :, :]
array([[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]], dtype=int32)
也可以这样写
>>> a[1, ...]
array([[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]], dtype=int32)
这两种方法是等价的,:表示遍历该维度上所有的元素,... 表示遍历剩下所有维度上的所有元素,相当于在剩下的所有维度上都使用 : 来索引。
如果要查看每一层第2列的元素,可以这样写
>>> a[:, :, 2]
array([[ 2, 6, 10],
[14, 18, 22]], dtype=int32)
还有更高级的写法,比如说我们想要查看每一层间隔一行最后一列的元素,可以这样写
>>> a[:, ::2, -1]
array([[ 3, 11],
[15, 23]], dtype=int32)
还可以这样,查看每一层间隔一行最后一列的元素,且按行倒排
>>> a[:, ::-2, -1]
array([[11, 3],
[23, 15]], dtype=int32)
改变维度
上面我们已经演示了如何使用reshape方法把一个一维数组重定义为一个三维的数组,我们也可以使用一个元组来设置数组的shape属性,和reshape是一个效果
>>> a = np.arange(24, dtype=np.int32)
>>> a.shape = (2, 3, 4)
>>> a
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]], dtype=int32)
我们也可以使用ravel或flatten方法把一个n维数组展平成一个一维数组
>>> a.ravel()
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23], dtype=int32)
>>> a.flatten()
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23], dtype=int32)
不同的是,ravel只是返回数组的一个View,而flatten会返回数组的一份Copy。那么View和Copy有什么区别呢?这个我们稍后解释。 求一个矩阵的转置矩阵是线性代数中一个很常见的操作,在NumPy中可以使用transpose方法来求转置矩阵
>>> a.transpose()
array([[[100, 12],
[ 4, 16],
[ 8, 20]],
[[ 1, 13],
[ 5, 17],
[ 9, 21]],
[[ 2, 14],
[ 6, 18],
[ 10, 22]],
[[ 3, 15],
[ 7, 19],
[ 11, 23]]], dtype=int32)
或者,更简单的,我们可以写做 a.T,和 a.transpose() 是一个效果。
我们也可以使用resize方法来改变数组的形状,和reshape不同的是,resize会直接修改数组本身,而reshape不会
>>> a.reshape(4, 3, 2)
array([[[100, 1],
[ 2, 3],
[ 4, 5]],
[[ 6, 7],
[ 8, 9],
[ 10, 11]],
[[ 12, 13],
[ 14, 15],
[ 16, 17]],
[[ 18, 19],
[ 20, 21],
[ 22, 23]]], dtype=int32)
>>> a
array([[[100, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[ 12, 13, 14, 15],
[ 16, 17, 18, 19],
[ 20, 21, 22, 23]]], dtype=int32)
>>> a.resize(4, 3, 2)
>>> a
array([[[100, 1],
[ 2, 3],
[ 4, 5]],
[[ 6, 7],
[ 8, 9],
[ 10, 11]],
[[ 12, 13],
[ 14, 15],
[ 16, 17]],
[[ 18, 19],
[ 20, 21],
[ 22, 23]]], dtype=int32)
View & Copy
接着上面的例子,稍做一下修改
>>> b = a.ravel()
>>> b[0] = 100
>>> a
array([[[100, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[ 12, 13, 14, 15],
[ 16, 17, 18, 19],
[ 20, 21, 22, 23]]], dtype=int32)
>>> b = a.flatten()
>>> b[0] = 200
>>> a
array([[[100, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[ 12, 13, 14, 15],
[ 16, 17, 18, 19],
[ 20, 21, 22, 23]]], dtype=int32)
我们发现,改变ravel得到的数组元素值,原数组元素跟着一起改变;而改变flatten得到的数组元素值,原数组元素不变,这就是Copy和View的区别。Copy会把原数组的数据区拷贝到一个新的内存区,而View会和原数组共享一个数据内存区。那么哪些操作会产生Copy,哪些会产生View呢?
1. No Copy at All
简单的赋值操作不会产生任何Copy
>>> b = a
>>> b is a
True
2. View or Shadow Copy
view方法会产生一个新的array对象,但是会共享data数据
>>> b = a.view()
>>> b is a
False
>>> b.base is a
True
3. Deep Copy
copy方法会产生一个array对象,data数据也会新复制一份
>>> b = a.copy()
>>> b is a
False
>>> b.base is a
False
组合与分割
我们可以把多个数组组合成一个数组,也可以把一个数组分割成多个数组。下面介绍一些常用的组合与分割的方法:
concatenate: 将多个数组沿指定轴的方向进行组合,所有数组需要拥有相同的形状,且除了指定轴以外,其它所有轴维度相同。
vstack: 将多个数组沿第1个轴的方向(垂直方向)组合成一个新的数组,这些数组需要拥有相同的形状,且除了第1维外,其它所有维度大小相同。eg:
>>> b = np.arange(12, dtype=np.int32).reshape(1, 3, 4)
>>> np.vstack((a, b))
array([[[100, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[ 12, 13, 14, 15],
[ 16, 17, 18, 19],
[ 20, 21, 22, 23]],
[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]]], dtype=int32)
我们也可以使用 concatenate 来实现 vstack 的功能,下面的代码和上面具有相同的效果:
>>> np.concatenate((a, b), axis = 0)
hstack: 将多个数组沿第2个轴的方向(水平方向)组合成一个新的数组,这些数组需要拥有相同的形状,且除了第2维外,其它所有维度大小相同。eg:
>>> b = np.arange(8, dtype=np.int32).reshape(2,1,4)
>>> np.hstack((a, b))
array([[[100, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[ 0, 1, 2, 3]],
[[ 12, 13, 14, 15],
[ 16, 17, 18, 19],
[ 20, 21, 22, 23],
[ 4, 5, 6, 7]]], dtype=int32)
同样,可以使用 concatenate 实现上面的功能:
>>> np.concatenate((a, b), axis=1)
dstack: 将多个数组沿第3个轴的方向(深度方向)组合成一个新的数组,这些数组需要拥有相同的形状,且除了第3维外,其它所有维度大小相同。eg:
>>> b = np.arange(6, dtype=np.int32).reshape(2,3,1)
>>> np.dstack((a,b))
array([[[100, 1, 2, 3, 0],
[ 4, 5, 6, 7, 1],
[ 8, 9, 10, 11, 2]],
[[ 12, 13, 14, 15, 3],
[ 16, 17, 18, 19, 4],
[ 20, 21, 22, 23, 5]]], dtype=int32)
同样,可以使用 concatenate 实现上面的功能:
>>> np.concatenate((a, b), axis=2)
column_stack: 将多个一维数组作为列组合成一个二维数组。eg:
>>> a = np.arange(3)
>>> b = np.arange(3) * 2
>>> np.column_stack((a, b))
array([[0, 0],
[1, 2],
[2, 4]])
row_stack: 将多个一维数组作为行组合成一个二维数组。eg:
>>> np.row_stack((a, b))
array([[0, 1, 2],
[0, 2, 4]])
上面介绍了如何把多个数组组合成一个数组,同样地,我们也可以把一个数组分割成多个数组。与 concatenate 对应的是 split,可以沿着指定轴对数组进行分割。看个例子:
>>> a = np.arange(24).reshape(6,2,2)
>>> np.split(a,[2,4], axis=0)
[array([[[0, 1],
[2, 3]],
[[4, 5],
[6, 7]]]), array([[[ 8, 9],
[10, 11]],
[[12, 13],
[14, 15]]]), array([[[16, 17],
[18, 19]],
[[20, 21],
[22, 23]]])]
类似地,还有 vsplit, hsplit, dsplit,这里不再赘述。
