掌握 NumPy:高效数组处理综合指南(第 1/2 部分)
文章目录
- 一、介绍
- 二、什么是Numpy,我们为什么要使用它?
- 三. 数组初始化
- 四. 计算速度和内存使用量
- 五、内存使用情况
- 六、数据类型
- 七、索引和切片
一、介绍
你们以前听说过 NumPy 吗?好吧,你可能有,也许你甚至在你的许多项目中使用过它。就我而言,我第一次使用它是在我开始学习机器学习的时候。然而,当时我只使用了基本函数,即数组初始化、数学运算等。直到我意识到深入研究 Numpy 本身的好处,我才开始探索有关该主题的各种 YouTube 教程和电子书。令我惊讶的是,实际上有许多我希望我早点知道的 Numpy 功能。
二、什么是Numpy,我们为什么要使用它?
在我们进入代码之前,我想先谈谈这个模块。Numpy 代表 Numerical Python,基本上是一个开源 Python 模块,允许我们处理数字和多维数组。与标准 Python 列表相比,使用此模块至少可以获得 3 个优势。首先,在数值计算和数组操作方面,它提供了更大的灵活性。第二,Numpy 速度更快,第三,它消耗的内存更少。最后提到的两个优点基本上是因为 Numpy 的大多数后端都使用 C 编程语言 [1]。
在这篇文章中,我将为想要从一开始就学习Numpy的你写一个分步指南。本文分为几个章节,列出如下:
Numpy 安装
数组初始化
Numpy 数组限制
计算速度和内存使用率
数据类型
索引和切片
数组生成函数
随机数
查看和复制
数学函数
逻辑运算符和按位运算符
搜索和排序
塑造和重塑
串联和拆分
事不宜迟,让我们现在亲自动手使用代码吧!
注意:本文中使用的所有资源都可以在我的 GitHub 存储库中找到。 这是它的链接。
- Numpy 安装
安装 Numpy 非常简单,您只需在笔记本电脑或命令提示符下运行即可。就我而言,图 1 中的文本出现,因为该模块已经正确安装。
pip install numpy
# Codeblock 1
! pip install numpy
图 1.如果我们尝试在已经安装了模块的环境中安装 Numpy,输出会是什么样子。
安装完成后,我们可以通过运行 来导入它。当这段代码运行时,现在我们可以说这是 Nupy 的别名。好吧,从技术上讲,实际上可以为 Nupy 创建任何别名。但我建议你不要这样做,因为它可能会让你将来感到困惑。接下来,要检查当前的 Numpy 版本,我们可以直接打印出 的属性。如下图所示,我在本教程中使用的 Numpy 版本是 1.25.0。如果您的版本不同,请不要担心,因为我认为一个版本和另一个版本之间没有太大区别。但是,如果您不确定,可以使用 .import numpy as npnp__version__nppip install numpy==1.25.0
# Codeblock 2
import numpy as np
np.__version__
图2.我在本教程中使用的 Numpy 版本。
三. 数组初始化
我想谈的第一个 Numpy 函数是用于初始化数组的函数,即 。要使用此函数,我们可以简单地传递输入列表。或者,您也可以用来做同样的事情。嗯,和 之间实际上有细微的区别。但是,您现在可以忽略它。np.array()np.asarray()np.array()np.asarray()
# Codeblock 3
np.array([7,6,5,4,3,2])
### Alternative
# np.asarray([7,6,5,4,3,2])
图3.代码块 3.
运行上述代码时,它将返回一个输出,如图 3 所示。如果将该数组放在函数中,则输出看起来会有所不同。在下图中,您可以看到文本“数组”以及逗号消失了。然而,请记住,这实际上只是代表的问题。print()
# Codeblock 4
print(np.asarray([7,6,5,4,3,2]))
图4.如果我们使用 print() 显示数组,它会是什么样子。
上面的行为实际上与Python列表不同。如果我们尝试将其打印出来,无论使用函数如何,结果都将如图 5 所示。我们可以在图中看到,所有元素都用逗号分隔,同时没有打印“数组”文本。print()
# Codeblock 5
[7,6,5,4,3,2]
### Returns the exact same result.
# print([7,6,5,4,3,2])
图5.标准 Python 列表是什么样子的。
当我们尝试打印出 2D 数组时,也可以看到 Numpy 数组和 Python 列表之间的另一个区别。您可以在下面看到 Numpy 数组自动打印为行和列,而 Python 列表则不是。
# Codeblock 6
print(np.asarray([[7,6,5,4,3,2],
[9,8,7,6,5,4]]), end='\n\n')
print([[7,6,5,4,3,2],
[9,8,7,6,5,4]])
图6.以 Numpy 数组(上图)和 Python 列表(下图)的形式显示 2D 数组。
两者都用于将列表转换为 Numpy 数组。要反之亦然,我们可以使用该方法。请参阅下面的代码块 7。np.array()np.asarray()tolist()
# Codeblock 7
A = [2,4,6,8]
B = np.array(A) # Convert to Numpy array.
C = B.tolist() # Convert to Python list.
print(B)
print(C)
图7.可以使用 np.array() 和 tolist() 完成两种数组类型之间的转换。
如果您不确定变量是否包含列表或 Numpy 数组,我们可以使用该函数进行检查,而无需显示该变量的全部内容。在 Codeblock 8 中,我在 和 上使用该函数。顺便说一句,结果输出中的术语“ndarray”基本上代表 N 维数组。type()ABC
# Codeblock 8
print('type(A):', type(A))
print('type(B):', type(B))
print('type(C):', type(C))
图8.A、B 和 C 的对象类型。
在本章中,我想向您展示的最后一件事是,我们可以根据 txt 文件中的数据自动初始化 Numpy 数组。我们可以为此使用的函数是 。举个例子,下面是文件内容的样子,我们将使用 Codeblock 9 中的代码加载它。在这里,我将参数设置为(逗号)。您可能需要根据 txt 文件中一个数字与另一个数字的分离方式进行更改。np.genfromtxt()data.txtdelimiter,
图 9.数据将加载为 Numpy 数组的 txt 文件。
# Codeblock 9
np.genfromtxt('data.txt', delimiter=',')
图 10.data.txt已使用 np.genfromtxt() 成功加载为 Numpy 数组。
3. Numpy 数组限制
在我们讨论使用 Nupy 可以完成的所有事情之前,我将告诉您该模块不能做什么。在下面的 Codeblock 10 中,我将多个不同数据类型的值放在 Python 列表中,该列表似乎能够正确处理这些值。
# Codeblock 10
D = [2, 'Hello', True, 9.886]
D
图 11.列表 D 同时包含整数、字符串、布尔值和浮点数数据类型。
如果我们将 list 转换为 Numpy 数组,即使我们再次将其转换回 list,里面的所有值也会自动变成字符串。D
# Codeblock 11
print(np.array(D))
print(np.array(D).tolist())
图 12.Numpy 将列表 D 的所有元素设置为字符串。
上面的演示基本上表明了使用 Numpy 数组的缺点:它无法存储多个数据类型的元素。然而,这种行为背后实际上是有原因的,与 Python 列表相比,它允许数组计算更快且内存效率更高。我将在下一章中证明这个概念。
四. 计算速度和内存使用量
为了查看 Numpy 数组和 Python 列表执行完全相同的操作所需的时间,我们需要导入模块并初始化要使用的数组。time
# Codeblock 12
import time
E = [123] * 9999999
F = np.array(E, dtype='int8')
这个实验的想法非常简单,我想做的就是使用 和 对列表和数组中的所有值求和。之后,我将打印出计算时间。我在下面写的两个函数,即使用 Python 函数的函数,而使用 .EFsum()np.sum()summation_python_sum()sum()summation_numpy_sum()np.sum()
# Codeblock 13
def summation_python_sum(arr):
start_time = time.time()
sum(arr)
end_time = time.time()
total_time = end_time - start_time
return total_time
def summation_numpy_sum(arr):
start_time = time.time()
np.sum(arr)
end_time = time.time()
total_time = end_time - start_time
return total_time
由于上面的两个函数已经声明,现在我们可以通过运行下面的代码块 14 来开始我们的实验。
# Codeblock 14
print('Python list with sum()\t\t: ', summation_python_sum(E), 'sec')
print('Python list with np.sum()\t: ', summation_numpy_sum(E), 'sec')
print('Numpy array with sum()\t\t: ', summation_python_sum(F), 'sec')
print('Numpy array with np.sum()\t: ', summation_numpy_sum(F), 'sec')
图 13.Python list 和 Numpy 数组以及 sum() 和 np.sum() 函数之间的运行时间比较。
根据上面的结果,我们可以看到 Numpy 数组的工作速度比其他数组快得多,尤其是当它与 Numpy 函数配对时(0.01 秒)。当使用 Python 函数执行 Python 列表时,获得第二快的结果(0.09 秒)。但是,在这种情况下,它仍然比 Numpy 慢 9 倍左右。我们需要注意的一件事是,我认为我们应该避免将 Python 函数与 Numpy 数组混合使用,或者将 Python 列表与 Numpy 函数混合使用,因为这会导致处理时间变得更慢。
五、内存使用情况
这就是计算速度,现在让我们看看列表和数组占用多少内存。我们可以通过执行下面的代码块来做到这一点。EF
# Codeblock 15
import sys
print('E (Python list):', sys.getsizeof(E), 'bytes')
print('F (Numpy array):', sys.getsizeof(F), 'bytes')
图 14.E 和 F 使用的内存量。
如上所示,与数组相比,list 占用的内存是数组的 8 倍。您可能已经注意到,在代码块 12 中,我用于参数,它本质上代表 8 位整数。在这种情况下,我决定只使用 8 位,因为我们数组中的数字非常小。Numpy 确实允许我们在数据类型中指定此类细节,使其在内存使用方面更有效率。我们将在第 5 章中更深入地了解 Numpy 数据类型。EFint8dtype
六、数据类型
我们可以通过打印出 Numpy 数组的属性来检查它的数据类型。在下面的输出中,我们可以看到数组的数据类型是 ,这与我们之前设置的完全相同。同时,请记住,以前我们没有指定数组的数据类型。在这种情况下,Numpy 通常会将其设置为 。dtypeFint8Bint32
# Codeblock 16
print(B.dtype)
print(F.dtype)
图 15.数组 B 和 F 的数据类型。
我经常使用的其他数组数据类型是 float 和 boolean。在 float 的情况下,您只需将非整数数组传入其中,它就会自动将数据类型设置为 。np.array()float64
# Codeblock 17
print(np.array([5, 5, 5, 5.3, 5]).dtype)
print(np.array([True, True, False]).dtype)
图 16.代码块 17 中两个数组的数据类型。
Integer 和 Float 数据类型
现在我想更深入地谈谈整数和浮点数。正如我之前提到的,写在后面的数字基本上表示用于表示整数值的位数。让我们看一下这个例子:假设我们有数字 26。在二进制中,26 等价于 11010,其中它使用 5 个二进制数字表示,即 5 位。如果我们将这个数字存储到具有数据类型的 Numpy 数组中,则该数字将存储为 00011010。intint8
00011010本身的二进制序列基本上仍然等价于 26,只是它现在以 8 位表示。如果改用,则该数字将写为 00000000 00011010。根据这一事实,它现在感觉到,即使存储的数字保持不变,更多的位也需要更多的内存。int16
此外,您还需要知道,较小的位数会导致我们的数字范围更有限。可以存储的最大和最小数量分别为 127 和 -128。第一个数字用于表示符号(即正数或负数),而其余数字则负责存储幅度。同时,在浮点数的情况下,较小的位使得要存储的数字具有较低的精度。int8
我们可以使用以下代码查看每个整数数据类型的详细信息。
# Codeblock 18
print(np.iinfo(np.int8))
print(np.iinfo(np.int16))
print(np.iinfo(np.int32))
print(np.iinfo(np.int64))
图 17.可以通过 np.iinfo() 函数查看每个整数数据类型的最大值和最小值。
对于浮点数据类型,也可以使用 获得类似的信息。np.finfo()
# Codeblock 19
print(np.finfo(np.float16))
print(np.finfo(np.float32))
print(np.finfo(np.float64))
图 18.Numpy 中每个浮点数据类型的详细信息。
不仅是布尔值、整数和浮点数,实际上还有很多其他数据类型我没有解释。如果你有兴趣了解更多信息,你可以通过这个链接[2]访问Numpy关于数据类型的文档。
七、索引和切片
在许多情况下,对 Numpy 数组进行索引和切片类似于 Python 列表。为了演示这一点,让我们考虑以下数组。
# Codeblock 20
G = np.array([9, 7, 5, 6, 4, 8, 2, 6, 9, 5, 2, 4, 1, 4])
我将在 Codeblock 21 中对数组执行各种索引和切片技术。G
# Codeblock 21
print('G[7]\t\t:', G[7]) #(1)
print('G[-2]\t\t:', G[-2]) #(2)
print('G[:3]\t\t:', G[:3]) #(3)
print('G[3:]\t\t:', G[3:]) #(4)
print('G[2:6]\t\t:', G[2:6]) #(5)
print('G[1:7:2]\t:', G[1:7:2]) #(6)
print('G[::3]\t\t:', G[::3]) #(7)
以下是上述代码的实际作用:
G[7]→ 取数组 G 的第 7 个元素。
G[-2]→ 从后面拿第二个元素。
G[:3]→ 取前 3 个元素。
G[3:]→ 取除前 3 个元素以外的所有元素。我们将从一路开始到结束。请记住,数组索引从 0 开始。G[3]
G[2:6]→ 从索引 2 到 5 中取元素(不包括索引 6)。
G[1:7:2]→ 以 2 为步长从索引 1 到 6 取元素。
G[::3]→ 在第 3 步中获取所有元素。
以下是 Codeblock 21 的输出如下所示:
图 19.代码块 21 的输出。
上面关于索引和切片的演示也可以在 Python 列表上实现。实际上,有一种技术专门适用于 Numpy 数组:使用另一个数组进行索引。在下面的示例中,我们从数组中获取索引 6、4、5、7 和 3 的值。G
# Codeblock 22
H = np.array([6,4,5,7,4,3])
print('G[H]\t:', G[H])
图 20.将一个数组与另一个数组建立索引。
多维数组
在索引和切片方面,Numpy 数组优于 Python 列表的优势可以清楚地看到,尤其是当我们使用多维数组时。本章中的以下示例不适用于列表。
现在让我们先初始化一个 2D 数组。
# Codeblock 23
I = np.array([[3, 1, 5, 7],
[2, 5, 3, 2],
[3, 8, 5, 9],
[4, 8, 2, 6]])
二维数组实际上可以被看作是一个矩阵。在本例中,矩阵的大小为 4×4(4 行和 4 列)。该矩阵如下面的图 21 所示。I
图 21.以矩阵形式可视化的 I 数组。
为了获取第 0 行和第 2 列(数字 5),我们可以使用 Codeblock 24 中的代码。这个想法很简单,我们需要做的就是写下要选择的行号和列号。
# Codeblock 24
I[0,2]
图 22.代码块 24.
接下来,我们可以使用 Codeblock 25 获取前 2 行和前 3 列。
# Codeblock 25
I[:2,:3]
图 23.使用 Codeblock 25 选择的元素。
如果要获取整行或整列,只需使用 .在下面的示例中,我想做的是将第 1 列的整行取到 3 列(不包括第三个索引)。:
# Codeblock 26
I[:,1:3]
图 24.取第 1 列和第 2 列的整行。
以下示例与上一个示例类似,只是此处我们排除最后一行。
# Codeblock 27
I[:-1,1:3]
图 25.与图 24 类似,但此处不采用最后一行。
使用另一个数组或列表进行索引也适用于 2D 数组。在下面的示例中,我从第 0 行和第 2 行中选取前 3 列。
# Codeblock 28
I[[0,2], :3]
图 26.仅从第 0 行和第 2 行中获取前 3 列的值。
最后,下面是当我们同时从“坐标”中获取元素时的结果。[0,0][1,1][2,2]
# Codeblock 29
I[[0,1,2], [0,1,2]]
图 27.与图 26 类似,不同之处在于这里我们指定了要手动获取的两个轴的索引。
7. 数组生成函数
Numpy 允许我们轻松生成特定数字的数组。我将从 和 开始。这些函数的使用方式基本相同,只是前者生成一个包含 1 的数组,而后者生成一个填充 0 的数组。我们需要做的就是为参数指定所需的形状。在 Codeblock 30 中,我演示了如何使用 np.ones() 创建由 10 个元素组成的一维数组。np.ones()np.zeros()
# Codeblock 30
np.ones(10)
图 28.使用 np.ones() 创建的一维数组。
若要创建二维数组,参数需要采用元组形式。在这里,我创建了具有 4 行和 3 列的全零数组。
# Codeblock 31
np.zeros((4,3))
图 29.使用 np.zeros() 创建的形状为 4×3 的 2D 数组。
不仅是一维和二维的,而且两者兼而有之,还允许我们创建更大维度的数组。在下面的例子中,我们可以将其视为四个 2D 数组,其中每个数组由 5 行和 6 列组成。np.ones()np.zeros()
# Codeblock 32
np.zeros((4,5,6))
图 30.四个 2D 阵列,每个阵列的大小为 5×6。
如果您熟悉矩阵,您可能经常听到“单位矩阵”一词。Numpy 提供了一个函数来通过 创建这种矩阵。在代码块 33 中,我创建了一个大小为 6×6 的单位矩阵。np.identity()
# Codeblock 33
np.identity(6)
图 31.使用 np.identity() 创建恒等矩阵。
请记住,这只能形成一个方阵。如果您需要创建一个具有更多灵活性的类似版本,则可以使用。 允许我们控制矩阵的高度 () 和宽度 () 以及对角线 () 的偏移量。在随后的示例中,我将矩阵大小设置为 6×4,对角线偏移量为 1(这意味着矩阵向右移动一次)。np.identity()np.eye()np.eye()NMk
# Codeblock 34
np.eye(N=6, M=4, k=1)
图 32.使用 np.eye() 构造的矩阵。
np.diag()另一方面,创建一个数组,可以手动定义对角线中的元素。也可以将该参数用于此函数。k
# Codeblock 35
np.diag([1,4,3,4,4])
图 33.使用 .np.diag()
接下来我要给大家看的是。实际上,我发现这个函数很有趣,因为生成的输出看起来像一个随机值。事实上,它并不是为了生成随机数,而是基本上采用当前内存状态的值。如果您想分配一个空数组以供稍后填充,但您非常关心计算时间,则此方法非常有用。根据我的实验,比两者都慢得多。 本身确实比 .np.empty()np.ones()np.zeros()np.empty()np.empty()np.zeros()
# Codeblock 36
np.empty((3,3))
图 34.由 np.empty() 生成的 3×3 数组。
如果您需要一个具有与 相似行为的函数,但具有您可以自行指定的数字,则可以使用 .随后的代码块显示了我如何使用该函数创建一个大小为 3×5 的数组,其中的元素都是 999。np.zeros()np.ones()np.full()
# Codeblock 37
J = np.full(shape=(3,5), fill_value=999)
J
图 35.创建一个所有元素均为 999 的数组。
如果您需要创建一个由特定数字组成的数组,同时您还希望它具有与另一个现有数组相同的形状,则可以使用 、 或 。好吧,一开始听起来可能有点令人困惑,但一旦你看到下面的例子,你就会明白。np.full_like()np.empty_like()np.ones_like()np.zeros_like()
# Codeblock 38
np.full_like(J, fill_value=111)
图 36.创建形状与数组 J 完全相同的 all-111 数组。
在上面的例子中,采用数组的形状,即 。然后,它将所有元素的值设置为 111。类似的事情也适用于其他三个函数,只是我们不需要使用参数。如图 37 中的输出所示,所有生成的数组都具有与 相同的形状。np.full_like()J(3,5)fill_valueJ
# Codeblock 39
print(np.empty_like(J), end='\n\n')
print(np.ones_like(J), end='\n\n')
print(np.zeros_like(J))
图 37.由 np.ones_like()、np.zeros_like() 和 np.empty_like() 生成的数组。
序号
现在让我们讨论如何使用 Numpy 创建序列号数组。用于此目的的第一个函数是 。这个函数其实类似于 Python 的 .两者的不同之处在于自动生成数组,而 while 是迭代器。下面的代码块 40 显示了如何使用创建包含数字 0 到 19 的数组。np.arange()range()np.arange()range()np.arange()
# Codeblock 40
np.arange(20)
图 38.使用 np.arange() 创建序列号。
为了使用 创建相同的东西,我们需要这样写下来:range()
# Codeblock 41
[i for i in range(20)]
图 39.使用 range() 生成相同的数字序列。
尽管行为存在这种差异,但可设置的参数完全相同,分别是 、 和 。请记住,我们为停靠点指定的编号不会包含在序列中。以下代码块显示了如何使用 .startstopstepnp.arange()
# Codeblock 42
print(np.arange(3, 20, 2), end='\n\n')
print(np.arange(100, 0, -3), end='\n\n')
print(np.arange(20, 30, 0.5))
图 40.np.arange() 生成的数字序列。
事实上,该函数有一个优点:它允许我们将浮点数传递给参数,就像我在 Codeblock 42 的最后一个示例中所做的那样。np.arange()range()
另一个生成序列号的 Numpy 函数基本上代表线性空间。此函数的前两个参数是 和 ,就像 一样。但是,第三个是不同的,它用于控制我们想要生成的元素数量。在下面的示例中,我将函数设置为写入 50 个数字,从 1 到 15 均匀延伸。请注意,参数 的行为与 不同,从某种意义上说,传入它的数字包含在数组中。np.linspace()startstopnp.arange()np.linspace()numstopnp.linspace()np.arange()
# Codeblock 43
np.linspace(1, 15, 50)
图 41.由 np.linspace() 生成的数组。所有这些数字都是均匀延伸的。
有一个类似的函数,我实际上从未使用过。数字的分布不是遵循线性比例,而是遵循几何比例。以下是如何使用该函数的示例。np.geomspace()
# Codeblock 44
np.geomspace(1, 15, 50)
图 42.请注意,np.geomspace() 生成的数字与 np.linspace() 不同。
未完待续
在这一点上,我才意识到这篇文章已经达到了 17 分钟的阅读时间,我想这对于一篇文章来说太长了。所以,我决定继续另一个。下期见!
引用
[1] 从源代码构建。NumPy的。https://numpy.org/doc/stable/user/building.html [2024年1月4日访问]。