不同大小的数组不能相加、相减或一般用于算术运算。
克服这个问题的一种方法是复制较小的数组,使其与较大数组具有相同的维度和大小。这称为数组广播,在 NumPy 中执行数组算术时可用,可以大大减少和简化您的代码。
在本教程中,您将了解数组广播的概念以及如何在 NumPy 中实现它。
完成本教程后,您将了解:
- 不同大小数组的算术运算问题。
- 广播的解决方案以及一维和二维中的常见示例。
- 数组广播的规则以及何时广播失败。
通过我的新书《机器学习线性代数》启动您的项目,包括分步教程和所有示例的 Python 源代码文件。
让我们开始吧。

NumPy 数组广播简介
图片来源:pbkwee,保留部分权利。
教程概述
本教程分为4个部分,它们是:
- 数组算术的局限性
- 数组广播
- NumPy 中的广播
- 广播的局限性
在机器学习线性代数方面需要帮助吗?
立即参加我为期7天的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
数组算术的局限性
您可以直接对 NumPy 数组执行算术运算,例如加法和减法。
例如,可以将两个数组相加,以创建一个新数组,其中每个索引处的值相加。
例如,数组 a 可以定义为 [1, 2, 3],数组 b 可以定义为 [1, 2, 3],将它们相加将得到一个新数组,其值为 [2, 4, 6]。
1 2 3 4 |
a = [1, 2, 3] b = [1, 2, 3] c = a + b c = [1 + 1, 2 + 2, 3 + 3] |
严格来说,算术运算只能在具有相同维度和相同大小维度的数组上执行。
这意味着长度为 10 的一维数组只能与另一个长度为 10 的一维数组进行算术运算。
数组算术的这种限制确实非常有限。值得庆幸的是,NumPy 提供了一个内置的变通方法,允许不同大小的数组之间进行算术运算。
数组广播
广播是 NumPy 用于允许不同形状或大小的数组之间进行数组算术运算的方法的名称。
尽管该技术是为 NumPy 开发的,但它也已在其他数值计算库中得到更广泛的应用,例如 Theano、TensorFlow 和 Octave。
广播通过沿最后一个不匹配的维度复制较小的数组来解决不同形状数组之间的算术问题。
术语“广播”描述了 NumPy 在算术运算期间如何处理不同形状的数组。在某些限制下,较小的数组会被“广播”到较大的数组上,以便它们具有兼容的形状。
—— 广播,SciPy.org
NumPy 实际上并没有复制较小的数组;相反,它有效地利用内存中现有结构来达到相同的效果,从而实现内存和计算效率。
这个概念也渗透到线性代数符号中,以简化简单操作的解释。
在深度学习的背景下,我们也使用一些不太传统的符号。我们允许矩阵和向量相加,得到另一个矩阵:C = A + b,其中 Ci,j = Ai,j + bj。换句话说,向量 b 被添加到矩阵的每一行。这种简写消除了在执行加法之前定义一个将 b 复制到每一行中的矩阵的需要。这种将 b 隐式复制到许多位置的操作称为广播。
—— 第 34 页,《深度学习》,2016 年。
NumPy 中的广播
我们可以通过查看 NumPy 中的三个示例来具体说明广播。
本节中的示例并非详尽无遗,而是您可能看到或实现的常见广播类型。
标量与一维数组
单个值或标量可以与一维数组进行算术运算。
例如,我们可以想象一个具有三个值 [a1, a2, a3] 的一维数组“a”加上一个标量“b”。
1 2 |
a = [a1, a2, a3] b |
标量需要通过复制其值两次以上来广播到一维数组。
1 |
b = [b1, b2, b3] |
然后可以将两个一维数组直接相加。
1 2 |
c = a + b c = [a1 + b1, a2 + b2, a3 + b3] |
下面的示例演示了 NumPy 中的这一点。
1 2 3 4 5 6 7 8 |
# 标量和一维 from numpy import array a = 数组([1, 2, 3]) print(a) b = 2 print(b) c = a + b print(c) |
运行示例首先打印定义的一维数组,然后是标量,最后是结果,其中标量被添加到数组中的每个值。
1 2 3 4 5 |
[1 2 3] 2 [3 4 5] |
标量与二维数组
标量值可以与二维数组进行算术运算。
例如,我们可以想象一个具有 2 行 3 列的二维数组“A”加上标量“b”。
1 2 3 4 |
a11, a12, a13 A = (a21, a22, a23) b |
标量需要通过复制 5 次以上来广播到二维数组的每一行。
1 2 |
b11, b12, b13 B = (b21, b22, b23) |
然后可以直接添加这两个二维数组。
1 2 3 4 |
C = A + B a11 + b11, a12 + b12, a13 + b13 C = (a21 + b21, a22 + b22, a23 + b23) |
下面的示例演示了 NumPy 中的这一点。
1 2 3 4 5 6 7 8 |
# 标量和二维 from numpy import array A = 数组([[1, 2, 3], [1, 2, 3]]) print(A) b = 2 print(b) C = A + b print(C) |
运行示例首先打印定义的二维数组,然后是标量,然后是加法的结果,其中值“2”被添加到数组中的每个值。
1 2 3 4 5 6 7 |
[[1 2 3] [1 2 3]] 2 [[3 4 5] [3 4 5]] |
一维数组与二维数组
一维数组可以与二维数组进行算术运算。
例如,我们可以想象一个具有 2 行 3 列的二维数组“A”加上一个具有 3 个值的一维数组“b”。
1 2 3 4 |
a11, a12, a13 A = (a21, a22, a23) b = (b1, b2, b3) |
一维数组通过创建第二个副本,广播到二维数组的每一行,从而得到一个新的二维数组“B”。
1 2 |
b11, b12, b13 B = (b21, b22, b23) |
然后可以直接添加这两个二维数组。
1 2 3 4 |
C = A + B a11 + b11, a12 + b12, a13 + b13 C = (a21 + b21, a22 + b22, a23 + b23) |
以下是 NumPy 中的一个实际示例。
1 2 3 4 5 6 7 8 |
# 一维和二维 from numpy import array A = 数组([[1, 2, 3], [1, 2, 3]]) print(A) b = 数组([1, 2, 3]) print(b) C = A + b print(C) |
运行示例首先打印定义的二维数组,然后是定义的一维数组,最后是结果 C,其中二维数组中的每个值实际上都加倍了。
1 2 3 4 5 6 7 |
[[1 2 3] [1 2 3]] [1 2 3] [[2 4 6] [2 4 6]] |
广播的局限性
广播是一个方便的快捷方式,在处理 NumPy 数组时非常有用。
也就是说,它并不适用于所有情况,事实上它施加了一个严格的规则,必须满足该规则才能执行广播。
算术运算(包括广播)只能在数组中每个维度的形状相等或其中一个维度的尺寸为 1 时执行。维度按倒序考虑,从末尾维度开始;例如,在二维情况下,先看列而不是行。
当我们考虑到 NumPy 在比较数组时实际上会用“1”的大小填充缺失的维度时,这会更有意义。
因此,二维数组“A”(2 行 3 列)和包含 3 个元素的向量“b”之间的比较
1 2 |
A.shape = (2 x 3) b.shape = (3) |
实际上,这变成了以下两者之间的比较:
1 2 |
A.shape = (2 x 3) b.shape = (1 x 3) |
同样的概念也适用于将标量视为具有所需维度数的数组进行比较。
1 2 |
A.shape = (2 x 3) b.shape = (1) |
这变成了一个比较
1 2 |
A.shape = (2 x 3) b.shape = (1 x 1) |
当比较失败时,无法执行广播,并会引发错误。
下面的示例尝试将一个两元素数组广播到 2 x 3 数组。此比较实际上是
1 2 |
A.shape = (2 x 3) b.shape = (1 x 2) |
我们可以看到最后一个维度(列)不匹配,我们预计广播会失败。
下面的示例演示了 NumPy 中的这一点。
1 2 3 4 5 6 7 8 |
# 广播错误 from numpy import array A = 数组([[1, 2, 3], [1, 2, 3]]) 打印(A.形状) b = 数组([1, 2]) 打印(b.形状) C = A + b print(C) |
运行示例首先打印数组的形状,然后在尝试广播时引发错误,正如我们所预料的。
1 2 3 |
(2, 3) (2,) ValueError: 操作数不能以形状 (2,3) (2,) 一起广播 |
扩展
本节列出了一些您可能希望探索的扩展本教程的想法。
- 创建三个新的不同 NumPy 数组广播示例。
- 实现您自己的广播函数,用于一维和二维情况下的手动广播。
- 使用包含非常大数组的一维和二维情况,对 NumPy 广播和您自己的自定义广播函数进行基准测试。
如果您探索了这些扩展中的任何一个,我很想知道。
进一步阅读
如果您想深入了解,本节提供了更多关于该主题的资源。
书籍
- 第 2 章,《深度学习》,2016 年。
文章
- 广播,NumPy API,SciPy.org
- TensorFlow 中的广播语义
- numpy 中的数组广播,EricsBroadcastingDoc
- 广播,Theano
- Numpy 中的广播数组, 2015.
- Octave 中的广播
总结
在本教程中,您学习了数组广播的概念以及如何在 NumPy 中实现它。
具体来说,你学到了:
- 不同大小数组的算术运算问题。
- 广播的解决方案以及一维和二维中的常见示例。
- 数组广播的规则以及何时广播失败。
你有什么问题吗?
在下面的评论中提出你的问题,我会尽力回答。
谢谢,我明白了
不客气。
谢谢你,贾森,你的文章很有帮助。
我很高兴听到西蒙这么说。
感谢您提供的精彩教程!
我被导致错误的案例弄糊涂了
A.shape = (2 x 3)
b.shape = (1)
因为你之前展示了一个 2 x 3 矩阵和一个标量的加法是有效的,在我看来,(1)-数组可以被视为标量用于广播目的。
为什么不允许这种情况有明确的理由吗?
只是为了好玩,这里有一种与您描述的 NumPy 方式不同的广播方式,它对标量和数组都能更统一地工作
给定两个数组 A 和 B,它们的形状分别为 (r_1,...,r_n) 和 (s_1, ..., s_m),其中 n 是 A 的维度数,m 是 B 的维度数。我们可以假设 m < n,也就是说,B 的维度少于 A,因为如果不是这样,那么我们可以简单地交换它们,将 A 和 B 视为 B 和 A。
请注意,上述内容甚至适用于标量,因为标量具有 0 个维度。因此,如果 A 是标量,则 n 为 0,(r_1,…r_n) 只是空元组 ();如果 B 是标量,则 m 为 0,(s_1,…,s_m) 是空元组 ()。
首先,我们想象(因为它实际上并未在内存中创建)一个具有 n 维度的新数组 B' (1, ..., 1, s_1, ..., s_m),也就是说,B 的维度前面加上足够多的 1 来达到 n 维度。
其次,我们检查广播是否有效。要使其有效,我们比较 A 和 B' 的每对对应维度。这就是与您描述的不同之处,因为现在我们只要求每对维度要么相同,要么至少其中一个为 1。
让我们看看这是否适用于我上面提到的情况。对于 (2 x 3) + (1) 的情况,B' 的维度是 (1 x 1)(前面加了一个“1”,以填充到像 (2 x 3) 这样的两个维度)。那么第一个维度(A 为 2,B' 为 1)满足条件,第二个维度(A 为 3,B' 为 1)也满足条件。
让我们检查 (2 x 3) 和标量。因为标量是 0 维的,所以 B' 的维度将是 (1 x 1)。这同样满足条件,因为每对对应维度中至少有一个 1。
现在,我们实际计算结果。我们逐元素执行操作。结果 C 将具有 n 个维度,并且给定索引 i_1,…, i_n,元素 C_(i_1,…,i_n) 定义为
C_(i_1,…,i_n) = A_(i_1,…,i_n) + B_(i_(n – m +1),…,i_n)
也就是说,我们对所有 i_1, …, i_(n – m) 的值都使用 B 的相同元素(它们是广播的)。
这种方法将对 (2 x 3) + (1) 和 (2 x 3) + 标量产生相同的结果,这在我看来更有意义。
不确定他们为什么没有以这种更通用(在我看来,更简单)的方式定义它。我很想知道原因。而且,即使没有特别的原因,我认为如果我理解了原则上如何更普遍地完成它,并提醒自己他们只是以不同的方式处理标量情况,这将有助于我理解 NumPy 的做法。
我想你可能混淆了这些情况。
帖子中的最终错误是针对两个维度不同的向量,而不是向量和标量。
我指的不是帖子中的最终错误,我指的是以下错误
同样的概念也适用于将标量视为具有所需维度数的数组进行比较。
A.shape = (2 x 3)
b.shape = (1)
1
2
A.shape = (2 x 3)
b.shape = (1)
这变成了一个比较
A.shape = (2 x 3)
b.shape = (1 x 1)
1
2
A.shape = (2 x 3)
b.shape = (1 x 1)
当比较失败时,无法执行广播,并会引发错误。
我明白了,谢谢。
原因可能只是与 API 的简单一致性,例如数组必须具有匹配的维度。
另请参阅 numpy 的外积,以了解相关概念。
谢谢 Paul,您能详细说明一下吗?
这句话“标量需要通过复制其值两次以上来广播到一维数组”是什么意思?
嗨,亚伦……以下资源可能会引起您的兴趣
https://numpy.com.cn/doc/stable/user/basics.broadcasting.html