NumPy广播:规则与向量量化示例

NumPy广播:规则与向量量化示例

“广播”(broadcasting)描述了NumPy在算术运算中如何处理形状不同的数组。在满足一定约束时,较小的数组会被“广播”到较大的数组上,使它们具有兼容的形状。广播提供了数组运算向量化的方法,让循环在C中执行,而非在Python中执行。它不会进行不必要的数据复制,通常能得到高效的算法实现。不过,有时广播并不合适,因为它会低效地使用内存,使计算变慢。

另见numpy.broadcast。

NumPy通常对两个数组逐元素进行运算。在最简单的情况下,两数组必须具有完全相同的形状,例如:

>>> import numpy as np
>>> a = np.array([1.0, 2.0, 3.0])
>>> b = np.array([2.0, 2.0, 2.0])
>>> a * b
array([2.,  4.,  6.])

当数组形状满足特定条件时,NumPy的广播规则放宽了这一限制。最简单的广播例子,是数组和标量一起进行运算:

>>> import numpy as np
>>> a = np.array([1.0, 2.0, 3.0])
>>> b = 2.0
>>> a * b
array([2.,  4.,  6.])

结果与前一个例子相同,尽管前例中的b是数组。可以认为,在算术运算期间,标量b被拉伸成了一个形状与a相同的数组。如图1所示,b中的新元素只是原始标量的副本。不过,拉伸只是概念上的比喻。NumPy会直接使用原始标量值,而不会真的创建副本,以便让广播运算在内存和计算方面尽可能高效。

标量被广播,以匹配与它相乘的一维数组形状
图1:最简单的广播示例中,标量b被拉伸为与a形状相同的数组,使逐元素乘法的形状相容。

第二个例子的代码比第一个更高效,因为广播在乘法运算中搬动的数据更少:b是标量,而非数组。

通用广播规则

对两个数组进行运算时,NumPy逐维比较它们的形状。比较从末尾,即最右侧维度开始,逐步向左进行。在以下任一条件下,两维兼容:

  1. 两者相等;
  2. 其中一个为1。

若不满足这些条件,就会抛出ValueError: operands could not be broadcast together异常,表示数组形状不兼容。

输入数组不必具有相同的维数。结果数组的维数与输入数组中维数最多的数组相同;各维的大小取输入数组中对应维度的最大值。缺少的维度视为大小为1。

例如,有一个256x256x3的RGB值数组,希望按不同数值缩放图像中的各颜色,就可以把图像乘以包含3个值的一维数组。按广播规则将这些数组的末尾轴大小对齐,可以看到它们相容:

Image  (3d array): 256 x 256 x 3
Scale  (1d array):             3
Result (3d array): 256 x 256 x 3

被比较的维度中有一个为1时,就使用另一个维度的大小。换言之,大小为1的维度会被拉伸,或者说“复制”,以匹配另一个维度。

下例中,A和B数组都具有长度为1的轴,在广播运算期间,这些轴会被扩展到更大尺寸:

A      (4d array):  8 x 1 x 6 x 1
B      (3d array):      7 x 1 x 5
Result (4d array):  8 x 7 x 6 x 5

可广播数组

如果上述规则可以产生有效的结果,一组数组就称为能够广播到相同形状的可广播数组。

例如,若a.shape为(5,1),b.shape为(1,6),c.shape为(6,),而d.shape为(),即d是标量,那么a、b、c、d均可广播到形状(5,6):

  • a的行为如同(5,6)数组,a[:,0]广播到其他列。
  • b的行为如同(5,6)数组,b[0,:]广播到其他行。
  • c的行为如同(1,6)数组,进而如同(5,6)数组,其中c[:]广播到每一行。
  • d的行为如同(5,6)数组,唯一的值重复出现在各位置。

下面还有一些例子:

A      (2d array):  5 x 4
B      (1d array):      1
Result (2d array):  5 x 4

A      (2d array):  5 x 4
B      (1d array):      4
Result (2d array):  5 x 4

A      (3d array):  15 x 3 x 5
B      (3d array):  15 x 1 x 5
Result (3d array):  15 x 3 x 5

A      (3d array):  15 x 3 x 5
B      (2d array):       3 x 5
Result (3d array):  15 x 3 x 5

A      (3d array):  15 x 3 x 5
B      (2d array):       3 x 1
Result (3d array):  15 x 3 x 5

以下是无法广播的形状:

A      (1d array):  3
B      (1d array):  4 # trailing dimensions do not match

A      (2d array):      2 x 1
B      (3d array):  8 x 4 x 3 # second from last dimensions mismatched

将一维数组加到二维数组上的广播示例:

>>> import numpy as np
>>> a = np.array([[ 0.0,  0.0,  0.0],
...               [10.0, 10.0, 10.0],
...               [20.0, 20.0, 20.0],
...               [30.0, 30.0, 30.0]])
>>> b = np.array([1.0, 2.0, 3.0])
>>> a + b
array([[  1.,   2.,   3.],
        [11.,  12.,  13.],
        [21.,  22.,  23.],
        [31.,  32.,  33.]])
>>> b = np.array([1.0, 2.0, 3.0, 4.0])
>>> a + b
Traceback (most recent call last):
ValueError: operands could not be broadcast together with shapes (4,3) (4,)

如图2所示,b会加到a的每一行上。图3中则由于形状不兼容而抛出异常。

形状为3的一维数组被拉伸,以匹配形状为4乘3的二维数组,结果为4乘3数组
图2:一维数组与二维数组相加时,若一维数组的元素数量等于二维数组的列数,就会发生广播。
形状4乘3的二维数组和形状4的一维数组因不匹配而无法广播
图3:数组末尾维度不相等时广播失败,因为无法将第一个数组各行的值与第二个数组的元素对齐,以进行逐元素相加。

广播为计算两个数组的外积或其他外部运算提供了便捷方式。下例演示两个一维数组的外部加法:

>>> import numpy as np
>>> a = np.array([0.0, 10.0, 20.0, 30.0])
>>> b = np.array([1.0, 2.0, 3.0])
>>> a[:, np.newaxis] + b
array([[ 1.,   2.,   3.],
       [11.,  12.,  13.],
       [21.,  22.,  23.],
       [31.,  32.,  33.]])
4乘1的二维数组和形状3的一维数组被拉伸,得到4乘3数组
图4:在某些情况下,广播会同时拉伸两个数组,形成比任一初始数组都大的输出数组。

这里的newaxis索引操作符在a中插入一个新轴,使它成为二维的4x1数组。将这个4x1数组与形状为(3,)的b结合,就得到4x3数组。

实际示例:向量量化

现实问题中经常会用到广播。典型例子是信息论、分类及相关领域中的向量量化(VQ)算法。VQ的基本操作,是在一组点中找到与指定点最近的点;在VQ术语中,这组点称为codes,指定点称为observation。

在下面非常简单的二维示例中,observation中的值描述待分类运动员的体重和身高,codes代表不同的运动员类别。[1]要找出最近的点,需要计算观测值与每个码点之间的距离,最短距离提供最佳匹配。这里codes[0]是最近的类别,表明这名运动员很可能是篮球运动员。

>>> from numpy import array, argmin, sqrt, sum
>>> observation = array([111.0, 188.0])
>>> codes = array([[102.0, 203.0],
...                [132.0, 193.0],
...                [45.0, 155.0],
...                [57.0, 173.0]])
>>> diff = codes - observation    # the broadcast happens here
>>> dist = sqrt(sum(diff**2,axis=-1))
>>> argmin(dist)
0

本例中,observation数组被拉伸,以匹配codes数组的形状:

Observation      (1d array):      2
Codes            (2d array):  4 x 2
Diff             (2d array):  4 x 2
身高与体重图展示体操运动员、马拉松运动员、篮球运动员、橄榄球锋线球员及待分类运动员,待分类点离篮球类别最近
图5:向量量化的基本操作,是计算待分类对象(深色方块)与多个已知码点(灰色圆圈)之间的距离。在此简单示例中,码点代表各类别;更复杂的情形会为每个类别使用多个码点。

通常会将大量观测值observations,例如从数据库读取的观测数据,与一组codes比较。考虑以下情形:

Observation      (2d array):      10 x 3
Codes            (3d array):   5 x 1 x 3
Diff             (3d array):  5 x 10 x 3

三维数组diff是广播的结果,而非计算必需的形式。大型数据集会产生很大的中间数组,使计算效率低下。相反,如果在上述二维示例外加一个Python循环,对每个观测值单独计算,就会使用小得多的数组。

广播是强大的工具,能写出简短、通常也直观的代码,并在C中高效完成计算。不过,有些算法采用广播时,会消耗不必要的大量内存。这时最好用Python编写算法的外层循环。这样还可能提高代码可读性,因为广播中的维数增加时,算法往往更难理解。

脚注

[1] 本例中,体重因数值更大,对距离计算的影响比身高更大。实践中,对身高与体重进行归一化非常重要,通常可以使用它们在数据集中的标准差,使两者对距离计算产生相同影响。

来源与许可

来源:NumPy官方文档:Broadcasting,NumPy Developers。本版为中文翻译。项目版权、BSD再分发条件及免责声明见官方LICENSE.txt;完整许可随来源文件保留。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容