针对 NumPy 数组的高效计算:通用函数

已完成

Python 的某些属性使其非常适用于数据科学(例如其动态的解释性性质),但也会使其速度变慢。 这对于循环尤其如此。 处理大型数据集时,这些小的性能问题可能会导致延长几分钟(或更长时间)。

当我们在 Python 简介中首次检查循环时,你可能不会注意到任何延迟。 循环很短,以至于 Python 相对缓慢的循环不是问题。 请考虑此函数,可计算一组数字的倒数:

import numpy as np
np.random.seed(0)

def compute_reciprocals(values):
    output = np.empty(len(values))
    for i in range(len(values)):
        output[i] = 1.0 / values[i]
    return output

values = np.random.randint(1, 10, size=5)
compute_reciprocals(values)

输出为:

array([0.16666667, 1.        , 0.25      , 0.25      , 0.125     ])

当你运行此循环时,可能很难注意到执行不是即时的。

但让我们在更大的数组上试试。 若要凭经验进行此测试,我们将使用 IPython 的 %timeit magic 命令计时。

big_array = np.random.randint(1, 100, size=1000000)
%timeit compute_reciprocals(big_array)

输出为:

2.96 s ± 130 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

你肯定发现了这种延迟。 当我们多次重复许多小运算时,此循环的缓慢会变得很明显。

性能瓶颈并非出自运算本身,而是 Python 在每个循环周期中执行的类型检查和函数调度造成的。 对于前面示例中的 compute_reciprocals 函数,Python 每次计算倒数时,会首先检查对象的类型,动态查找用于该类型的正确函数。 使用经解释的代码就是这样。 但是,如果我们使用的是经编译的代码(如用 C 语言编写的代码),则在代码运行之前就知道对象类型规范,并且可以更有效地计算结果。 这就是 NumPy 通用函数的作用。

Ufuncs

我们在处理和分析数据时,NumPy 中的通用函数(通常缩写为 ufuncs)为需要运行的许多运算提供静态类型的已编译函数

接下来看看这在实践中的表现如何。 让我们再次查找 big_array 的倒数,这次对数组使用内置的 NumPy 除法 ufunc:

%timeit (1.0 / big_array)

输出为:

2.97 ms ± 201 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

这次的量级更好。

可以在标量和数组之间以及任意维度的数组之间使用 ufuncs。

在执行相同计算时,ufuncs 向量化的计算几乎总是比 Python 循环更高效。 这种效率的提高在大型数组上尤其明显。 如果可能,请在运算 NumPy 数组时尝试使用 ufuncs,而不是使用普通的 Python 循环。

Ufuncs 分为两种风格:采用单个输入的一元 ufuncs、运算两个输入的二元 ufuncs。 我们将在此处介绍的常见 ufuncs 包含这两种风格。

数组算术

许多 NumPy ufuncs 使用 Python 的本机算术运算符。 因此,你可使用第 1 节所述的标准加法、减法、乘法和除法运算符:

a = np.arange(4)
print("a     =", a)
print("a + 5 =", a + 5)
print("a - 5 =", a - 5)
print("a * 2 =", a * 2)
print("a / 2 =", a / 2)
print("a // 2 =", a // 2)  # Floor division

输出为:

a     = [0 1 2 3]
a + 5 = [5 6 7 8]
a - 5 = [-5 -4 -3 -2]
a * 2 = [0 2 4 6]
a / 2 = [0.  0.5 1.  1.5]
a // 2 = [0 0 1 1]

还有用于求反、乘方和取模运算的 ufuncs:

print("-a     = ", -a)
print("a ** 2 = ", a ** 2)
print("a % 2  = ", a % 2)

输出为:

-a     =  [ 0 -1 -2 -3]
a ** 2 =  [0 1 4 9]
a % 2  =  [0 1 0 1]

还可使用标准运算顺序来组合这些 ufuncs:

-(0.5*a + 1) ** 2

输出为:

array([-1.  , -2.25, -4.  , -6.25])

Python 运算符实际上不是 ufuncs,而是 NumPy 内置函数的包装器。 因此,+ 运算符实际上是 add 函数的包装器:

np.add(a, 2)

输出为:

array([2, 3, 4, 5])

下面是 Python 运算符和 NumPy ufuncs 之间的等效内容的速查表:

运算符 等效 ufunc 说明
+ np.add 加法(例如 1 + 1 = 2
- np.subtract 减法(例如 3 - 2 = 1
- np.negative 一元求反(例如 -2
* np.multiply 乘法(例如 2 * 3 = 6
/ np.divide 除法(例如 3 / 2 = 1.5
// np.floor_divide Floor 除法(例如 3 // 2 = 1
** np.power 乘方(例如 2 ** 3 = 8
% np.mod 取模/余数(例如 9 % 4 = 1

Python 布尔运算符也起作用。 本节的后面部分将探讨这些运算符。

绝对值

NumPy 还了解 Python 的内置绝对值函数:

a = np.array([-2, -1, 0, 1, 2])
abs(a)

输出为:

array([2, 1, 0, 1, 2])

此函数对应于 NumPy ufunc np.absolute(也可在别名 np.abs 下使用):

np.absolute(a)

输出为:

array([2, 1, 0, 1, 2])

对于:

np.abs(a)

输出相同:

array([2, 1, 0, 1, 2])

指数和对数

数据科学中经常需要使用指数和对数。 这些数据转换在机器学习和统计工作中很常见。

a = [1, 2, 3]
print("a     =", a)
print("e^a   =", np.exp(a))
print("2^a   =", np.exp2(a))
print("3^a   =", np.power(3, a))

输出为:

a     = [1, 2, 3]
e^a   = [ 2.71828183  7.3890561  20.08553692]
2^a   = [2. 4. 8.]
3^a   = [ 3  9 27]

基本 np.log 计算自然对数。 如果需要计算以 2 为基数或以 10 为基数的对数,NumPy 还提供了以下各项:

a = [1, 2, 4, 10]
print("a        =", a)
print("ln(a)    =", np.log(a))
print("log2(a)  =", np.log2(a))
print("log10(a) =", np.log10(a))

输出为:

a        = [1, 2, 4, 10]
ln(a)    = [0.         0.69314718 1.38629436 2.30258509]
log2(a)  = [0.         1.         2.         3.32192809]
log10(a) = [0.         0.30103    0.60205999 1.        ]

这些 ufuncs 还有一些专用版本,可帮助你在处理非常小的输入时保持精准率:

a = [0, 0.001, 0.01, 0.1]
print("exp(a) - 1 =", np.expm1(a))
print("log(1 + a) =", np.log1p(a))

输出为:

exp(a) - 1 = [0.         0.0010005  0.01005017 0.10517092]
log(1 + a) = [0.         0.0009995  0.00995033 0.09531018]

当你对 np.log 的非常小的值使用这些函数时,这些函数比原始 np.expa 提供更精确的值。

专用 ufuncs

NumPy 有许多其他 ufuncs。 专用和不明确的 ufuncs 的另一来源是子模块 scipy.special。 如果需要对数据计算一些特殊的数学或统计函数,可在 scipy.special 中实现。

from scipy import special

下面是一些 gamma 函数(广义阶乘)和一个相关函数:

a = [1, 5, 10]
print("gamma(a)     =", special.gamma(a))
print("ln|gamma(a)| =", special.gammaln(a))
print("beta(a, 2)   =", special.beta(a, 2))

输出为:

gamma(a)     = [1.0000e+00 2.4000e+01 3.6288e+05]
ln|gamma(a)| = [ 0.          3.17805383 12.80182748]
beta(a, 2)   = [0.5        0.03333333 0.00909091]

要点

NumPy 中的通用函数提供比常规 Python 函数更快的计算函数,尤其是在处理数据科学中常见的大型数据集时。 此速度非常重要,因为它可以提高数据科学家的效率。 它还可更好地利用时间和计算资源,在数据中执行更广泛的查询。