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)

注意 endpointTrue 时,结束值会包括在返回数组内,否则不包括,相应地间隔值也会有差异。

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.zerosnp.zeros_like 类似,不同的是用1填充数组。

np.empty & np.empty_like

np.zerosnp.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.zerosnp.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]]])]

类似地,还有 vsplithsplit, dsplit,这里不再赘述。

Written on June 13, 2018
View: