使用广播对数组进行计算

已完成

向量化运算的另一种方法是使用 NumPy 的广播功能:为对不同大小的数组应用二元 ufuncs(如加法、减法或乘法)创建规则。

以前,当我们对相同大小的数组执行二元运算时,将按元素逐个执行这些操作。

first_array = np.array([3, 6, 8, 1])
second_array = np.array([4, 5, 7, 2])
first_array + second_array

输出为:

array([ 7, 11, 15,  3])

通过广播,可以对不同大小的数组执行此类二元运算。 因此,你可轻松地将标量(这只是一个零维数组)添加到数组中:

first_array + 5

输出为:

array([ 8, 11, 13,  6])

同样,可以将一维数组添加到二维数组中:

one_dim_array = np.ones((1))
one_dim_array

输出为:

array([1.])

对于此运算:

two_dim_array = np.ones((2, 2))
two_dim_array

输出为:

array([[1., 1.],
       [1., 1.]])

接下来,请尝试:

one_dim_array + two_dim_array

输出为:

array([[2., 2.],
       [2., 2.]])

到目前为止,一切顺利。 但你可通过更复杂的方法在数组上使用广播。 请考虑此示例:

horizontal_array = np.arange(3)
vertical_array = np.arange(3)[:, np.newaxis]

print(horizontal_array)
print(vertical_array)

输出为:

[0 1 2]
[[0]
 [1]
 [2]]

现在运行:

horizontal_array + vertical_array

输出为:

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

广播规则

广播遵循一组规则,以确定两个数组之间的交互:

  • 规则 1:如果这两个数组的维数不同,则维度较少的数组的形状将在其前(左)端以 1 填充。
  • 规则 2:如果两个数组的形状在任何维度中都不匹配,该维度中形状等于 1 的数组将拉伸以匹配另一个形状。
  • 规则 3:如果在任何维度中,大小不一致且两者都不等于 1,则 NumPy 将引发错误。

接下来看一下这些规则的实际应用,以便更好地了解它们。

广播示例 1

了解如何将一维数组添加到二维数组中:

two_dim_array = np.ones((2, 3))
one_dim_array = np.arange(3)

请考虑对这两个数组进行运算。 数组的形状是:

  • two_dim_array.shape = (2, 3)
  • one_dim_array.shape = (3,)

数组 one_dim_array 的维度较少。 根据规则 1,我们在其左侧以 1 填充:

  • two_dim_array.shape -> (2, 3)
  • one_dim_array.shape -> (1, 3)

我们现在看到第一个维度不一致。 根据规则 2,我们将延伸此维度以进行匹配:

  • two_dim_array.shape -> (2, 3)
  • one_dim_array.shape -> (2, 3)

形状匹配,显示最终形状将为 (2, 3)

two_dim_array + one_dim_array

输出为:

array([[1., 2., 3.],
       [1., 2., 3.]])

亲自试一试

翻转此运算。 试着将它们添加到 two_dim_array = np.ones((3, 2))one_dim_array = np.arange(3)[:, np.newaxis]

结果如何?


提示 (展开以显示)
two_dim_array = np.ones((3, 2))
two_dim_array

输出为:

array([[1., 1.],
     [1., 1.],
     [1., 1.]])

并使用:

one_dim_array = np.arange(3)[:, np.newaxis]
one_dim_array

输出为:

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




广播示例 2

接下来看看当这两个数组需要广播时,会发生什么情况:

vertical_array = np.arange(3).reshape((3, 1))
horizontal_array = np.arange(3)

同样,我们首先编写数组的形状:

  • vertical_array.shape = (3, 1)
  • horizontal_array.shape = (3,)

规则 1 指出,需要使用 1 填充 horizontal_array 的形状:

  • vertical_array.shape -> (3, 1)
  • horizontal_array.shape -> (1, 3)

而规则 2 指出,我们需要升级其中的每个 1,以匹配其他数组的相应大小:

  • vertical_array.shape -> (3, 3)
  • horizontal_array.shape -> (3, 3)

由于结果匹配,这些形状是兼容的。 可在此处查看:

vertical_array + horizontal_array

输出为:

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

广播示例 3

下面是不兼容数组的情况:

M = np.ones((3, 2))
i = np.arange(3)

这种情况与第一个示例中的情况略有不同:矩阵 M 已转置。 这一变化对计算有何影响? 数组的形状是:

  • M.shape = (3, 2)
  • i.shape = (3,)

同样,规则 1 指出,我们需要使用 1 填充 i 的形状:

  • M.shape -> (3, 2)
  • i.shape -> (1, 3)

根据规则 2,延伸 i 的第一个维度,使其与 M 的维度相匹配:

  • M.shape -> (3, 2)
  • i.shape -> (3, 3)

现在来到规则 3:最终形状不匹配,而且这两个数组不兼容:

M + i

输出为:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-140-90923315444d> in <module>
----> 1  M + i

ValueError: operands could not be broadcast together with shapes (3,2) (3,)

广播实践

Ufuncs 使你可以避免使用慢速 Python 循环。 广播就建立在这个基础上。

常见的数据实践是集中数据数组。 例如,假设我们有一个包含 10 个观察值的数组,其中每个观察值都由三个值(在此上下文中称为特征)组成。 我们可能需要集中该数据,以便发现与平均值(而非原始数据本身)的差。 这样做有助于更好地比较不同的值。

我们会将此数据存储在 10 x 3 数组中:

T = np.random.random((10, 3))
T

输出为:

array([[0.90137772, 0.75292208, 0.26398243],
       [0.46383154, 0.38378661, 0.93354504],
       [0.17893062, 0.2792731 , 0.24203594],
       [0.32155001, 0.11237084, 0.9133384 ],
       [0.59296043, 0.45756963, 0.25448096],
       [0.91645837, 0.04154293, 0.02954946],
       [0.23938136, 0.13087661, 0.37177127],
       [0.34654638, 0.0745468 , 0.78676817],
       [0.33536819, 0.49004234, 0.90644125],
       [0.03382156, 0.3552458 , 0.3588655 ]])

现在,在第一个维度中使用 mean 聚合来计算每个特征的平均值:

Tmean = T.mean(0)
Tmean

输出为:

array([0.43302262, 0.30781767, 0.50607784])

最后减去平均值来集中 T。 (此运算为广播运算。)

T_centered = T - Tmean
T_centered

输出为:

array([[ 0.4683551 ,  0.4451044 , -0.24209541],
       [ 0.03080892,  0.07596894,  0.4274672 ],
       [-0.254092  , -0.02854458, -0.2640419 ],
       [-0.11147261, -0.19544684,  0.40726056],
       [ 0.15993781,  0.14975196, -0.25159688],
       [ 0.48343576, -0.26627475, -0.47652838],
       [-0.19364126, -0.17694106, -0.13430658],
       [-0.08647624, -0.23327088,  0.28069033],
       [-0.09765443,  0.18222467,  0.40036341],
       [-0.39920105,  0.04742813, -0.14721234]])

这种方法不仅速度更快,而且比编写循环更简单。

要点

你将在数据科学中处理的数据总是有不同的形状和大小(至少在处理该数据的数组方面是如此)。 通过 NumPy 中的广播功能,能够以可预测的方式对不规则拟合数据使用二元函数。