NumPy - 使用数组进行性能优化
使用数组进行性能优化
使用数组进行性能优化涉及提高数组操作的效率,例如减少计算时间和内存使用量。
我们应该出于以下原因优化性能:
- 速度:更快的计算速度可带来更快的结果和更灵敏的应用程序。
- 可扩展性:优化的代码可以高效地处理更大的数据集和更复杂的操作。
- 资源效率:减少内存使用量和计算开销。
使用向量化操作
向量化操作是指无需使用显式循环,即可在单个步骤中对整个数组或矩阵执行操作的能力。
这是通过以下方式实现的广播和内部优化,使这些操作更快、更高效。
示例
在下面的示例中,我们使用 NumPy 的数组操作对两个大数组"a"和"b"执行矢量化加法。此操作计算数组的元素和,并将结果存储在新数组"c"中 -
import numpy as np # 创建两个大数组 a = np.random.rand(1000000) b = np.random.rand(1000000) # 向量化加法 c = a + b print (c)
以下是得到的输出 -
[0.91662816 0.65486861 1.60409272 ... 0.95122935 1.12795861 0.15812103]
利用高效数据类型
为数组选择合适的数据类型对于优化 NumPy 的性能和内存使用至关重要。
例如,使用 np.float32 而不是 np.float64 会显著影响内存使用和性能,尤其是在处理大型数据集时。
在 NumPy 中,数据类型 (dtype) 定义了数组包含的元素类型以及存储每个元素所需的空间。
示例
在此示例中,我们通过创建一个双精度(64 位)浮点数数组,然后使用 astype() 方法将其转换为单精度(32 位)浮点数,来演示精度更改的用法 -
import numpy as np # 创建一个双精度数组(64 位) arr_double = np.array([1.0, 2.0, 3.0], dtype=np.float64) # 打印原始双精度数组 print("原始双精度数组:") print(arr_double) print("数据类型:", arr_double.dtype) # 转换为单精度数组(32 位) arr_single = arr_double.astype(np.float32) # 打印转换后的单精度数组 print(" 转换后的单精度数组:") print(arr_single) print("数据类型:", arr_single.dtype)
这将产生以下结果 -
原始双精度数组: [1. 2. 3.] 数据类型:float64 转换后的单精度数组: [1. 2. 3.] 数据类型:float32
使用 NumPy 函数避免循环
NumPy 的主要优势之一是能够通过使用内置函数和数组操作来避免显式循环。这种方法通常被称为向量化。
使用 NumPy 函数,您可以一次性对整个数组执行操作,这比使用循环更简洁。
示例
在下面的示例中,我们使用 np.mean() 函数计算数组元素的平均值,而无需使用任何显式循环 -
import numpy as np # 创建数组 arr = np.array([1, 2, 3, 4, 5]) # 计算数组元素的平均值 mean = np.mean(arr) print("mean:",mean)
以下是上述代码的输出 -
mean: 3.0
使用广播进行矢量化
广播是指对不同形状的数组执行逐元素操作的能力。它遵循一组规则来确定不同形状的数组如何对齐以进行操作 -
- 相同维度:如果数组的维度不同,则较小数组的形状会在左侧填充 1,直到两个形状的长度相同。
- 维度兼容性:当两个维度相等或其中一个为 1 时,它们是兼容的。对于每个维度,如果大小不同,并且两个维度都不为 1,则广播失败。
- 拉伸:维度为 1 的数组会沿该维度拉伸,以匹配其他数组维度的大小。
示例
在下面的示例中,我们广播"array_1d"以匹配"array_2d"的形状,从而允许逐元素加法 -
import numpy as np # 创建一个二维数组和一个一维数组 array_2d = np.array([[1, 2, 3], [4, 5, 6]]) array_1d = np.array([10, 20, 30]) # 将一维数组添加到二维数组的每一行 result = array_2d + array_1d print(result)
输出结果如下 -
[[11 22 33] [14 25 36]]
向量化的原地操作
NumPy 中的原地操作是指直接修改数组的数据,而无需创建新的数组来存储结果。节省内存并提高性能。
这是通过使用改变原始数组内容的运算符和函数来实现的。这些操作通常使用带有就地后缀的运算符(例如 +=、-=、*=、/=)或支持就地修改的函数。
示例:使用就地运算符
在此示例中,我们直接对数组应用算术运算"+=",而无需创建新数组 -
import numpy as np # 创建数组 arr = np.array([1, 2, 3, 4, 5]) # 将每个元素就地加 10 arr += 10 print(arr)
执行上述代码后,我们得到以下输出 -
[11 12 13 14 15]
示例:使用就地函数
这里,我们使用 NumPy exp() 函数就地计算数组中每个元素的指数值 -
import numpy as np # 创建一个浮点型数组 arr = np.array([1, 2, 3, 4, 5], dtype=np.float64) # 就地计算每个元素的指数 np.exp(arr, out=arr) print(arr)
执行上述代码后,我们得到以下输出 -
[ 2.71828183 7.3890561 20.08553692 54.59815003 148.4131591 ]
使用内存视图进行矢量化
内存视图是指在不复制数据的情况下访问或查看数组中相同底层数据的不同方式。这一概念允许您创建数组的不同"视图"或"切片",这些视图可以以各种方式对同一数据进行操作 -
- 切片: 对数组进行切片时,NumPy 会创建原始数组的视图,而不是副本。此视图共享相同的数据缓冲区,因此对视图的更改会影响原始数组,反之亦然。
- 重塑: 重塑数组会创建相同数据但形状不同的新视图。这不会改变底层数据,但会改变其解释方式。
示例:切片
在下面的示例中,我们创建了一个二维 NumPy 数组和原始数组的视图(切片)。修改视图也会影响原始数组 -
import numpy as np # 创建二维数组 arr = np.array([[1, 2, 3], [4, 5, 6]]) # 创建原始数组的视图(切片) view = arr[:, 1:] # 修改视图 view[0, 0] = 99 print(arr)
我们得到如下所示的输出 -
[[ 1 99 3] [ 4 5 6]]
示例:重塑
在这里,我们使用 arange() 函数创建一个一维 NumPy 数组,然后使用 将其重塑为二维数组3 行 4 列,在保留原始数据的同时改变其结构 -
import numpy as np # 创建一维数组 arr = np.arange(12) # 重塑为二维数组 reshaped = arr.reshape((3, 4)) print(reshaped)
我们得到如下所示的输出 -
[[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]]
使用步长进行矢量化
步长是一个元组,表示遍历数组时在每个维度上要步长的字节数。它们决定了数组元素在内存中的访问方式,从而深入了解数据的布局和访问方式。
Strides 为您提供每个维度的内存偏移量。例如,在二维数组中,第二维的步幅表示访问该行中的下一个元素需要在内存中移动多少字节。
示例
在以下示例中,我们创建一个二维 NumPy 数组,并使用 strides 属性获取遍历数组时在每个维度上需要移动的字节数 -
import numpy as np # 创建二维数组 arr = np.array([[1, 2, 3], [4, 5, 6]]) # 打印数组的步幅 print(arr.strides)
我们得到如下所示的输出 -
(24, 8)