NumPy广播:规则与向量量化示例
“广播”(broadcasting)描述了NumPy在算术运算中如何处理形状不同的数组。在满足一定约束时,较小的数组会被“广播”到较大的数组上,使它们具有兼容的形状。广播提供了数组运算向量化的方法,让循环在C中执行,而非在Python中执行。它不会进行不必要的数据复制,通常能得到高效的算法实现。不过,有时广播并不合适,因为它会低效地使用内存,使计算变慢。
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会直接使用原始标量值,而不会真的创建副本,以便让广播运算在内存和计算方面尽可能高效。

第二个例子的代码比第一个更高效,因为广播在乘法运算中搬动的数据更少:b是标量,而非数组。
通用广播规则
对两个数组进行运算时,NumPy逐维比较它们的形状。比较从末尾,即最右侧维度开始,逐步向左进行。在以下任一条件下,两维兼容:
- 两者相等;
- 其中一个为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中则由于形状不兼容而抛出异常。


广播为计算两个数组的外积或其他外部运算提供了便捷方式。下例演示两个一维数组的外部加法:
>>> 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.]])

这里的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

通常会将大量观测值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;完整许可随来源文件保留。











暂无评论内容