Numpy基础笔记

简介

Numpy是一个功能强大的Python第三方pacakge,它为我们提供了一个非常强大的数据结构,n维数组,即numpy.ndarray。它是几乎所有Python数据科学工具包的基础。

Numpy底层使用C语言编写,因此相比与原生Python,它具有非常快的速度。同时,Numpy使用向量化Vectorization技术来减少代码中显式出现循环的次数,使得代码具有更好的可读性,更适合用于描述复杂的数学方程。可以说,Numpy就是Python Data science中多维数组的事实标准。

numpy的安装非常简单,直接使用pip或者conda进行即可。

1
2
pip install numpy
conda install numpy

在代码中进行引入的时候,通常会将numpy引入为np,后续我们也都遵循这个约定。同时后面在进行描述的时候,可能会涉及到数组、矩阵等表述,在大多数语境下,这两者指的是同一个东西,都是ndarray对象。

1
import numpy as np

如果只是为了学习相关编程语言,那么可以使用一个在线Code网站replit,它允许我们直接通过浏览器来实时运行代码。

核心概念

n维数组np.ndarray是Numpy中的核心和基础,学习Numpy最重要的就是学习这个类以及相关方法的使用。不过在此之前,有必要先了解Numpy中的几个核心概念,包括shape、axis、vectorization和broadcasting,理解这些概念有利于更好地把握numpy的运行机制。

Shape

对于n维数组来说,它的形状shape是非常重要的一个属性,我们可以通过.shape来查看数组的形状,也可以使用.reshape()方法来改变数组的形状。在使用ndarray的过程中,shape是一个非常基本的,也是我们应该非常关注的一个属性。

1
2
3
4
5
6
7
8
9
10
11
table = np.arange(24).reshape((2,3,4))
# table.shape
(2, 3, 4)
# table输出如下:
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]]])

对于二维数组来说,shape可以告诉我们这个数组有几行几列,但是对于更高维度的情况,就比较难直观地理解shape的含义。我们可以从数组嵌套的角度来理解shape,计算shape的过程就是一个逐层去除嵌套的过程。例如上面的table是一个三维的数组,它的shape为(2, 3, 4)。结合它的输出,第一个2表示去掉第一层[],我们可以得到两个元素;第二个3表示对于上面的每个元素,再次解除嵌套去掉[],可以得到三个元素;以此类推。

reshape方法则是上面这个过程的逆过程。对于一个需要进行reshape的矩阵,我们首先可以将其进行flatten,即拍平成一个一维矩阵,之后按照shape进行逐层的分配,最终得到期望的形状。

Axis

与shape紧密相关的概念是axis轴,或者可以理解为维度。对于一个矩阵来说,我们希望知道它的shape,同时也希望了解哪些数据位于哪些轴上。在numpy的数组中,轴从0开始进行索引,依次递增。从最外层[]开始是axis=0轴,逐步进入内部轴的索引也依次增加。例如对于二维数组来说,垂直轴为0轴,水平轴为1轴。

axis与shape也有对应关系。如果一个数组维度为n,那么它的axis最高就是n-1,同时它的shape也就是一个含有n个数字的tuple。实际上我们可以将shape理解为在描述每个axis上具有多少个元素。例如上面例子中的三维数组table,shape为(2, 3, 4),则代表在axis=0上有2个元素,在axis=1上有3个元素,在axis=2上有4个元素。这里的元素指的不是最小单位数字,而是可能由多个数字构成的,具有类似结构的组合。

在Numpy中,有许多方法会根据是否指定axis来改变对应的行为。以.max()方法为例,有如下测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
table = np.arange(24).reshape((2,3,4))
# shape: (2, 3, 4)
# axis: (0, 1, 2)
# table output:
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]]])

# table.max()
23

# table.max(axis=0)
# shape: (3, 4)
array([[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]])

# table.max(axis=1)
# shape: (2, 4)
array([[ 8, 9, 10, 11],
[20, 21, 22, 23]])

# table.max(axis=2)
# shape: (2, 3)
array([[ 3, 7, 11],
[15, 19, 23]])

可以看到,如果没有指定axis,那么numpy默认将所有的最小单元纳入考虑,相当于将整个数组拍平之后进行函数计算。而如果指定了axis,那么在计算过程中会沿着对应轴走,每次传入在该轴对应维度下的一个元素。对于聚合函数来说,沿着哪个轴走,最终得到的array shape就会消除掉哪一个维度(轴对应的维度),这点也可以在最终结果的shape中发现。此时可以将单个数字看作特殊的零维矩阵。

事实上,Numpy 的许多函数都是这样运行的:如果没有指定轴,那么它们会对整个数据集执行操作。否则,它们以轴方式执行操作。

Vectorization

Vectorization,又称矢量化,它指的是对数组中的每个元素以相同的方式执行相同的操作。该操作与for循环执行得到的结果相同,但是在代码中不需要出现for关键字,以此提高可读性。矢量化计算是Numpy能够做到干净,可读性的关键。虽然向量化并不会提高甚至有可能降低执行性能,但是相比于它带来的巨大可读性,这些性能损耗是可以接受的。

Numpy自身提供的方法大都能够进行矢量化,但是有些时候我们会使用到其他Package的方法,如果也想矢量化,那么需要使用 np.vectorize() 方法来获得该方法的矢量化版本。例如我们需要使用阶乘函数,即math.factorial

1
2
3
4
from math import factorial

a = np.arange(10).reshape((2, -1))
factorial(a)

直接使用会报错:

1
TypeError: only integer scalar arrays can be converted to a scalar index

此时需要先创建阶乘函数的向量化版本,之后才能正常使用:

1
2
3
4
5
from math import factorial

factorial_vectorization = np.vectorize(factorial)
a = np.arange(10).reshape((2, -1))
factorial_vectorization(a)

Broadcasting

Broadcasting,也称为广播,它是扩展两个不同形状的数组并弄清楚如何在它们之间执行矢量化计算的过程。

通常两个数组之间能够进行计算,是因为它们的维度能够相互匹配。例如对于数组加法,如果要进行数组之间的加法,这两个数组必须具有相同的shape。但是在numpy中,有时候两个数组的shape并不相同,但是它们仍然能够完成计算,这就是Broadcasting在发挥作用。

Broadcasting有一个核心规则:如果数组的维度匹配或者其中一个数组的大小为 1,则数组可以相互广播。如果数组沿轴的大小匹配,则将逐个元素对元素进行操作,类似于内置 Python 函数 zip() 的工作方式。如果其中一个数组在轴上的大小为 1,则该值将沿该轴进行广播,即重复多次,以匹配另一个数组中沿该轴的元素数量。

以下面的例子来进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
a = np.arange(3).reshape((1,3))
# a output:
array([[0, 1, 2]])

b = np.arange(3).reshape((3,1))
# b output:
array([[0],
[1],
[2]])

c = a + b
# c.shape: (3, 3)
# c = a+b: output
array([[0, 1, 2],
[1, 2, 3],
[2, 3, 4]])
# progress:
# broadcasting of a
[
[0, 1, 2],
[0, 1, 2],
[0, 1, 2]
]

# broadcasting of b
[
[0, 0, 0],
[1, 1, 1],
[2, 2, 2]
]

在上面的例子中,数组a的shape为(1, 3),数组b的shape为(3, 1),这两个数组进行加法运算,需要维度的匹配。经过观察可以发现,a和b可以进行广播,并且最终匹配的shape是(3, 3)。而广播实际上就是一个沿着某一个轴重复的过程。例如这里的数组a需要广播到(3, 3),因此它需要沿着axis=0轴进行元素重复;数组b需要广播到(3, 3),它需要沿着axis=1轴进行元素重复。两个数组都广播到相同shape之后,就可以按照位置对应进行加法计算了。

不过需要注意只有在某个轴上大小为1时才能进行Broadcasting,否则会报错:

1
ValueError: operands could not be broadcast together with shapes ...

基础操作

ndarray创建

np.array

在之前的操作中我们使用np.arange来创建过数组,得到的是一个从0到指定值的递增序列。同样我们可以通过np.array()来从python序列类型中创建ndarray,注意其中提供的list嵌套形状就是最终矩阵的shape,而元素类型会由numpy自动推断,当然也可以通过dtype属性进行手动指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = np.array([
[8, 7, 6, 5],
[4, 3, 2, 1],
])

a.ndim # 2

a.shape # (2, 4)

a.size # 8

a.dtype # dtype('int64')

a.dtype.name # 'int64'

ndarray的常用属性如下:

  • ndim:数组的维度,也是shape tuple的元素个数,也是轴的个数
  • shape:数组的形状
  • size:数组所有元素的个数,是shape tuple中所有值的乘积
  • dtype:数组中元素的类型

在ndarray中,所有的元素都需要具有相同的类型,常用的类型有下面这些,完成的支持类型列表可以参考Data Types|Numpy Documentation

C type numpy type
bool np.bool_
int np.intc
long np.int_
float np.single
double np.double

在Numpy中使用字符串多少有点奇怪,但是Numpy确实也能够支持。不过因为这种用法确实非常少见,这里不进行更加细致的描述

other method

下面列出了一些其他常用的特殊数组创建方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# zeros: 全零矩阵
np.zeros((2, 4))
array([[0., 0., 0., 0.],
[0., 0., 0., 0.]])

# ones: 全一矩阵
np.ones((3, 2))
array([[1., 1.],
[1., 1.],
[1., 1.]])
# arange: 通过range构造序列再生成矩阵
# 生成的矩阵都是1维的,可以通过reshape进行变换
np.arange(10, 30, 5)
# reshape最后一个值为-1表示自动计算
np.arange(10).reshape((2, -1))

# empyt: 随机矩阵
np.empty((3, 4))

# linspace: 在范围内均匀生成指定元素个数
# 解决在arrage中无法通过step精确控制矩阵元素个数的问题
np.linspace(10, 30, 7)

# random: 返回[0, 1)之间的随机值
# 生成一维矩阵
np.random.random(10)

# randn: 按照标准正态分布随机
# 生成一维矩阵
np.random.randn(5)

数学计算

由于Numpy支持向量化,因此利用Numpy来进行数学计算变得非常直观:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a = np.array([20, 30, 40, 50])
b = np.array([0, 1, 2, 3])

# 以下方法均返回一个新的对象,而非在原对象上修改

# 矩阵加法
a + b
# 矩阵减法
a - b
# 矩阵点乘(对应位置相乘)
a * b
# 矩阵乘法: @ 或者.dot()
a.reshape((4, 1)) @ b.reshape((1, 4))
a.reshape((4, 1)).dot(b.reshape((1, 4)))
# 逐元素操作
a ** 2
# 矩阵转置: 转置后的矩阵shape是原来的逆序
a.T
a.transpose()

Numpy中还提供了许多通用的方法,这些方法同样支持向量化,允许直接接受一个ndarray作为输入,然后对其中的元素依次进行对应操作。更多方法可以参考官方文档Functions and Methods Overview|Numpy Documentation

1
2
3
4
5
6
7
a = np.array([20, 30, 40, 50])

np.exp(a) # e的乘方
np.sqrt(a) # 开根号
np.ceil(a) # 向上取整
np.floor(a) # 向下取整
# ...

Numpy中还有许多聚合函数,例如.sum(), .max(), .mean().std()等等,这些聚合函数基本都有两种运行模式,在不指定axis的情况下考虑整个矩阵,在指定axis的情况下沿着对应axis进行聚合计算。

Index与Mark

基础索引

在ndarray中进行取值,方法与python原生的list类似,利用[]完成,并且支持通过单个索引位置或者切片进行取值。不过ndarray更加灵活,它在任意一个维度上都支持索引或者切片操作。并且利用索引进行取值之后,我们也可以通过直接赋值来修改矩阵中对应的value。

1
2
3
a = np.arange(120).reshape((6, 5, 4))
a[1, 2, 3]
a[:,3, 1:-1]

在Numpy中还提供了...表达式,它可以用来简化连续的多个:,例如有:

1
2
3
4
5
6
7
# assume x has 5 axes

x[1, 2, ...] is equivalent to x[1, 2, :, :, :]

x[..., 3] is equivalent to x[:, :, :, :, 3]

x[4, ..., 5, :] is equivalent to x[4, :, :, 5, :]

Numpy中提供一个np.nonzero()方法来查找数组中不为零的元素的下标,利用这个方法可以进行快速的过滤和查找。对于一个n维的数组,该方法会返回n个一维数组。这些一维数组具有相同的长度,利用类似zip的方法可以获取到每个不为零元素的下标。

1
2
3
4
5
a = np.arange(6).reshape((1, 2, 3))
np.nonzero(a)

# np.nonzero(a) output:
(array([0, 0, 0, 0, 0]), array([0, 0, 1, 1, 1]), array([1, 2, 0, 1, 2]))

Mark矩阵

此外,ndarray还允许我们通过Mark矩阵来进行取值。Mark矩阵是一个和原矩阵具有相同shape的矩阵,元素类型为布尔类型。利用Mark矩阵取值的过程就是判断Mark矩阵对应位置上是否为True,为True就加入最终的结果。利用Mark矩阵进行取值,最终得到的结果是一个一维矩阵。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 可以通过条件表达式来得到一个由布尔值构成的矩阵
a = np.arange(20).reshape((4, 5))

mark = a % 3 == 0
# mark output:
array([[ True, False, False, True, False],
[False, True, False, False, True],
[False, False, True, False, False],
[ True, False, False, True, False]])

a[mark]
# a[mark] output:
array([ 0, 3, 6, 9, 12, 15, 18])

我们可以通过条件表达式来得到一个由布尔值构成的矩阵,多个条件可以使用&|来进行多条件连接,注意不能使用andor,原因是这些内置的操作符没有实现向量化。

Index矩阵

更进一步,Numpy允许使用index矩阵来进行索引,区别于Mark矩阵中的值都是布尔值,这个index矩阵由下标索引组成。

1
2
3
4
5
6
7
8
9
10
11
a = np.arange(6)**2

index = np.array([
[1, 3],
[2, 4],
])

# index矩阵
a[index]
array([[ 1, 9],
[ 4, 16]])

index矩阵完成的操作非常容易理解,实际上就是讲index中各个位置上的元素替换为被查询矩阵对应下标的元素,被查询的维度是矩阵的第一维度。以上面的举例来看,a[index]的计算实际上就是下面的过程:

1
2
3
4
5
6
7
8
9
# index
array([[1, 3],
[2, 4]])
# a[index]
array([[a[1], a[3]],
[a[2], a[4]]])
# 即为
array([[ 1, 9],
[ 4, 16]])

我们还可以在矩阵的不同维度上使用index矩阵,不过要求多个index矩阵之间的shape相同。在不同维度上应用index矩阵与上面的单个index矩阵具有相同的原理,不同维度上index矩阵的值结合起来就是最终定位对应元素的依据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
a = np.arange(12).reshape(3, 4)
# a output
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])

# two index matrix
i = np.array([[0, 1], # indices for the first dim of `a`
[1, 2]])
j = np.array([[2, 1], # indices for the second dim
[3, 3]])

# a[i, j]
# array([[a[0, 2], a[1, 1]],
# [a[1, 3], a[2, 3]]])
a[i, j]
array([[ 2, 5],
[ 7, 11]])

# a[i, 2] support vectorization
# array([[a[0, 2], a[1, 2]],
# [a[1, 2], a[2, 2]]])
a[i, 2]
array([[ 2, 6],
[ 6, 10]])

# a[:, j] support silce
a[:, j]
array([[[ 2, 1], [ 3, 3]],

[[ 6, 5],
[ 7, 7]],

[[10, 9],
[11, 11]]])

Shape变换

reshape

我们可以利用Numpy中提供的方法来操作单个矩阵的shape。

1
2
3
4
5
6
7
8
9
a = np.arange(60).reshape((6, 2, 5))

# 矩阵展平,返回一维矩阵
a.flatten() # 返回一个新矩阵
a.ravel() # 返回原矩阵的一个视图view

# 修改矩阵的shape
a.reshape((10, 6)) # 返回新矩阵
a.resize((10, 6)) # 直接修改原矩阵

新增维度

有时候我们可能需要为矩阵新增一个维度,表现在shape上就是在某个轴上多出一个1,此时我们可以使用numpy中提供的关键字np.newaxis。实际上这个关键字就是None的别名,不过具有更好的可读性。

1
2
3
4
a = np.arange(6)
a[np.newaxis, :].shape # (1, 6)

np.newaxis is None # True

或者可以使用numpy提供的expand_dims方法,通过axis指定需要在哪个轴上新增维度。

1
2
3
4
a = np.arange(6)  # a shape: (6,)

b = np.expand_dims(a, axis=0)
b.shape # (1, 6)

矩阵堆叠

numpy中还提供多个矩阵拼接的方法,或者称为矩阵堆叠。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
a = np.array([
[1, 2],
[3, 4]
])
b = np.array([
[5, 6],
[7, 8]
])

# vstack
np.vstack((a, b))
array([[1, 2],
[3, 4],
[5, 6],
[7, 8]])

# hstack
np.hstack((a, b))
array([[1, 2, 5, 6],
[3, 4, 7, 8]])

对于二维矩阵来说,vstack表示垂直(vertical)堆叠;hstack表示水平(horizontal)堆叠。对于维度更高的矩阵来说,vstack表示沿axis=0堆叠;hstack表示沿axis=1堆叠。沿着哪个轴进行堆叠,对应维度的shape会增加,其他维度的shape不发生改变。因此在堆叠的时候,其他维度的shape需要相同才能够堆叠。

除了vstackhstack之外,numpy中还有一个更加灵活的方法concatenate,它允许接受axis参数指定沿着哪个轴进行堆叠。如果提供axis为None,则直接拍平再进行操作。

1
2
3
np.concatenate((a, b), axis=0)
np.concatenate((a, b), axis=1)
np.concatenate((a, b), axis=None)

矩阵分割

除了矩阵堆叠,还可以进行矩阵分割split,有类似的方法hsplitvsplitarray_splithsplit表示水平切割,或者说沿axis=1切割;vsplit表示垂直切割,或者说沿axis=0切割;array_split允许接受axis参数,指定沿着哪个轴进行切割;如果提供axis为Node,则直接拍平再进行操作。以上方法都接收一个目标切割数indices_or_sections,注意该目标数需要与对应的维度shape具有整除关系,这样才能正常切割。不过array_split并不受这个限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
a = np.arange(15).reshape((3, 5))

# hsplit
np.hsplit(a, 5)
[array([[ 0],
[ 5],
[10]]),
array([[ 1],
[ 6],
[11]]),
array([[ 2],
[ 7],
[12]]),
array([[ 3],
[ 8],
[13]]),
array([[ 4],
[ 9],
[14]])]

# vsplit
np.vsplit(a, 3)
[array([[0, 1, 2, 3, 4]]),
array([[5, 6, 7, 8, 9]]),
array([[10, 11, 12, 13, 14]])]

# array_split
# 下面两个表达式分别对应上面的两个方法
np.array_split(a, 5, axis=1)
np.array_split(a, 3, axis=0)

# array_split indices_or_sections 不受整除限制

np.array_split(a, 2, axis=0)
[array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]]),
array([[10, 11, 12, 13, 14]])]

视图View与复制Copy

在使用numpy的过程中,通常会涉及到矩阵的复制等操作。对于ndarray来说,主要分为三种情况,分别是直接赋值=,使用视图view()以及使用复制copy()

如果直接通过等号进行赋值,实际上并没有产生复制,新旧矩阵完全是相同的。

1
2
3
a = np.arange(15).reshape((3, 5))
b = a
print(b is a) # True

ndarray提供视图.view()方法,通过view方法产生的矩阵是原矩阵的一个视图,它们共享相同的底层数据。可以修改视图矩阵的shape而不影响原矩阵,但是修改视图的数据,原矩阵的数据也会受到影响。

1
2
3
4
5
6
7
8
9
10
a = np.arange(15).reshape((3, 5))
b = a.view()
print(b is a) # False
print(b.base is a) # True

b = b.reshape((2,2)) # b shape变成(2, 2)
print(a.shape) # a shape 保持不变

b[0, 0] = 10 # 修改b中的元素
print(a[0]) # ouptut 10,a中的元素也相应改变

ndarray同样提供复制方法.copy()。通过copy方法产生的矩阵是完完全全的复制,属于deep copy,新旧矩阵之间不会有任何的相互影响。

1
b = a.copy()

数据保存和读取

numpy中也提供数据的读取和保存方法,可以将一个ndarray对象保存在文件当中,并在需要的时候可以从文件中进行读取。对应的存储文件有两种,后缀名分别是.npy.npz。单个ndarray的保存对应npy文件,同时存储多个ndarray对应使用npz文件。

1
2
3
4
5
6
7
8
9
10
11
12
a = np.arange(6)
b = np.arange(10)

# npy file
np.save("test-save-a", a)
c = np.load("test-save-a.npy")

# npz file
np.savez("test-save-a", a, b)
d = np.load("test-save-a.npz")
d['arr_0'] # means a
d['arr_1'] # means b

参考文章

  1. Numpy Tutorial|RealPython
  2. Numpy User Guide|Numpy Documentation
  3. Numpy API Reference|Numpy Documentation

Numpy基础笔记
http://example.com/2023/08/04/Numpy基础笔记/
作者
EverNorif
发布于
2023年8月4日
许可协议