1.6 广播机制
第 4 章提到,标量与数组运算时标量会扩展到每个元素。这个"扩展"背后有一套通用规则,叫做广播(broadcasting)。广播允许形状不同但兼容的数组参与同一运算,NumPy 会自动把较小的数组扩展成较大的形状,而不需要显式复制数据。理解广播,才能判断哪些形状能一起运算、运算结果是什么形状,避免写出报错或结果错误的代码。本节将讲解广播规则、形状推断方法,以及控制广播的实用技巧。
1.6.1 广播的基本概念
形状相同直接运算
形状完全相同的数组逐元素运算,不存在广播:
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[10, 20, 30], [40, 50, 60]])
print(a + b)
# [[11 22 33]
# [44 55 66]]标量与数组
标量(形状 () )与任何数组都兼容,广播为与数组相同的形状:
a = np.array([[1, 2, 3], [4, 5, 6]])
print(a * 2)
# [[ 2 4 6]
# [ 8 10 12]]行向量与二维数组
形状 (1, 3) 的行向量与 (2, 3) 的数组运算,行向量沿行方向复制:
a = np.array([[1, 2, 3], [4, 5, 6]])
row = np.array([10, 20, 30])
print(a + row)
# [[11 22 33]
# [24 35 46]]这里 row 形状是 (3,),被广播成 (2, 3) 后逐行相加。这是广播最常见的用途:对整个二维数组的每一行做相同的变换。
列向量与二维数组
形状 (3, 1) 的列向量与 (3, 3) 数组运算,列向量沿列方向复制:
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
col = np.array([[10], [20], [30]])
print(a + col)
# [[11 12 13]
# [24 25 26]
# [37 38 39]]1.6.2 广播规则(从后向前比对维度)
NumPy 广播的完整规则:从最后一个维度开始,向前逐个比较两个数组的维度大小,满足以下任一条件即可广播:
- 两个维度大小相等
- 其中一个维度大小为 1
- 其中一个数组没有该维度(视为 1)
所有维度都满足条件时,结果形状取每个维度上的较大值;任一维度两个大小不相等且都不为 1,则广播失败,抛出 ValueError。
import numpy as np
# 规则示例 1: (2, 3) 与 (3,)
# 从后往前: 3 == 3, 2 与缺省(视为 1) -> 2 或 1 取大值
a = np.zeros((2, 3))
b = np.array([1, 2, 3])
print((a + b).shape) # (2, 3)
# 规则示例 2: (3, 1) 与 (1, 3)
# 从后往前: 1 与 3 -> 3; 3 与 1 -> 3
x = np.ones((3, 1))
y = np.arange(3) # (3,)
print((x + y).shape) # (3, 3)
print(x + y)
# [[0. 1. 2.]
# [1. 2. 3.]
# [2. 3. 4.]]
# 规则示例 3: (2, 1, 3) 与 (4, 3)
c = np.zeros((2, 1, 3))
d = np.ones((4, 3))
print((c + d).shape) # (2, 4, 3)常见广播组合速查
| 数组 A 形状 | 数组 B 形状 | 结果形状 |
|---|---|---|
| (2, 3) | (3,) | (2, 3) |
| (2, 3) | (2, 1) | (2, 3) |
| (3, 1) | (1, 3) | (3, 3) |
| (4, 1) | (3,) | (4, 3) |
| (2, 1, 3) | (4, 3) | (2, 4, 3) |
| (2, 3) | (4, 3) | 广播失败 |
广播失败的例子
import numpy as np
a = np.ones((2, 3))
b = np.ones((2, 4))
# a + b 会报错: 从后往前比对, 3 与 4 不等, 且都不为 1
# ValueError: operands could not be broadcast together with shapes (2,3) (2,4)1.6.3 广播后的形状推断
用 reshape/newaxis 显式广播
广播时常需要显式调整形状,常见手段是 reshape 和 np.newaxis:
import numpy as np
# 对每一列做标准化(减去列均值)
data = np.array([[1.0, 100.0], [3.0, 200.0], [5.0, 300.0]])
col_mean = data.mean(axis=0) # shape (2,)
print(col_mean) # [3. 200.]
centered = data - col_mean # (3,2) 与 (2,) 广播
print(centered)
# [[-2. -100.]
# [ 0. 0.]
# [ 2. 100.]]如果要对每行做操作,需要把向量先转成列向量:
# 对每一行做归一化(除以行和)
row_sum = data.sum(axis=1) # shape (3,)
print(row_sum) # [101. 203. 305.]
# 直接 data / row_sum 会广播失败吗? (3,2) 与 (3,): 2 与 3 冲突
# 需要把 row_sum 变成 (3,1)
row_sum_col = row_sum[:, np.newaxis] # shape (3, 1)
normalized = data / row_sum_col
print(normalized)
# [[0.00990099 0.99009901]
# [0.01477833 0.98522167]
# [0.01639344 0.98360656]][:, np.newaxis] 把一维向量转成列向量,是广播中常用的手段。
用 reshape(-1, 1) 转换
另一种常见写法是用 reshape(-1, 1):
import numpy as np
v = np.array([1, 2, 3])
print(v.reshape(-1, 1).shape) # (3, 1)
print(v[:, np.newaxis].shape) # (3, 1),等价1.6.4 就地广播与内存视图
就地运算
+=、*= 等就地运算符执行原地修改,不创建新数组,更省内存。就地运算同样遵循广播规则:
import numpy as np
a = np.arange(6).reshape(2, 3)
b = np.array([10, 20, 30])
a += b # 就地广播加法
print(a)
# [[10 21 32]
# [13 24 35]]就地运算要求右侧广播后的形状与左侧一致,且类型兼容。
广播不真正复制数据
广播在概念上"复制"数组,但实际通过内存视图实现,不占用额外内存。用 np.broadcast_to 可以显式查看广播结果:
import numpy as np
a = np.array([1, 2, 3])
b = np.broadcast_to(a, (4, 3)) # 把 (3,) 广播成 (4, 3)
print(b)
# [[1 2 3]
# [1 2 3]
# [1 2 3]
# [1 2 3]]
print(b.shape) # (4, 3)
# broadcast_to 返回只读视图,共享内存
print(np.shares_memory(a, b)) # Truebroadcast_to 返回的是只读视图,尝试写入会报错。需要可写的广播结果时,应显式调用 .copy()。
1.6.5 禁止广播(显式维度匹配)
需要精确形状的场景
有些函数不参与广播,要求输入形状精确匹配。例如 np.dot 对一维数组要求长度相等,np.linalg.solve 要求矩阵与向量维度匹配。需要精确逐元素对应时,可以用 np.broadcast_arrays 强制生成形状一致的数组:
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(np.broadcast_arrays(a, b)) # 两个 (3,) 数组
# 不同形状时强制广播成相同形状
c = np.array([1, 2, 3])
d = np.array([[1], [2]])
res = np.broadcast_arrays(c, d)
print(res[0].shape) # (2, 3)
print(res[1].shape) # (2, 3)用 assert 检查形状
调试时用 np.testing.assert_array_equal 或直接断言形状,防止广播产生意外的结果:
import numpy as np
a = np.zeros((3, 4))
b = np.ones((4,))
assert a.shape[1] == b.shape[0] # 形状匹配检查
result = a + b
print(result.shape) # (3, 4)练习题
第1题 概念理解
写出下面代码的输出结果(形状和值),并解释广播规则。
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([10, 20, 30])
print(a + b)
x = np.arange(3).reshape(3, 1)
y = np.arange(3)
print((x + y).shape)参考答案
[[11 22 33]
[14 25 36]]
(3, 3)(2,3) 与 (3,) 广播:最后一个维度 3 匹配,第一个维度 2 与缺省(视为 1)取大值 2。(3,1) 与 (3,) 广播:1 与 3 兼容,结果 (3,3)。
第2题 代码编写
给定矩阵 data = np.array([[1, 2], [3, 4], [5, 6]])。用广播计算每行的均值,然后让每行减去该行均值(行内中心化),要求利用 [:, np.newaxis] 把行均值转成列向量。
参考答案
import numpy as np
data = np.array([[1, 2], [3, 4], [5, 6]])
row_mean = data.mean(axis=1) # [1.5 3.5 5.5]
row_mean_col = row_mean[:, np.newaxis] # (3,1)
centered = data - row_mean_col
print(centered)
# [[-0.5 0.5]
# [-0.5 0.5]
# [-0.5 0.5]]不加 [:, np.newaxis] 时 (3,2) 与 (3,) 在第一个维度上 2 与 3 冲突,广播失败。
第3题 进阶练习
判断以下运算能否广播,能广播的写出结果形状,不能的说明原因: (1) (4, 1) 与 (3,); (2) (2, 3) 与 (4, 3); (3) (2, 1, 3) 与 (4, 3); (4) 标量 5 与 (2, 3)。
参考答案
(1) 能,结果 (4, 3):(4,1) 与 (3,) 从后往前,1 与 3 兼容,4 与缺省兼容。(2) 不能:最后一个维度 3 与 3 匹配,但第一个维度 2 与 4 不等且都不为 1。(3) 能,结果 (2, 4, 3):(2,1,3) 与 (4,3) 从后往前 3==3、1 与 4 兼容、2 与缺省兼容。(4) 能,结果 (2, 3):标量 () 与任何形状兼容。
第4题 项目实践
命令行任务管理器要按优先级对各任务的工时做加权。假设工时矩阵 hours = np.array([[2, 3], [4, 1], [3, 5]])(每行一个任务,每列一种工作类型),权重向量 weights = np.array([0.6, 0.4])。用广播计算每个任务的加权总工时。
参考答案
import numpy as np
hours = np.array([[2, 3], [4, 1], [3, 5]])
weights = np.array([0.6, 0.4])
weighted = hours * weights # 广播,每行乘权重
print(weighted)
# [[1.2 1.2]
# [2.4 0.4]
# [1.8 2. ]]
total = weighted.sum(axis=1) # [2.4 2.8 3.8],加权总工时
print(total)(3,2) 与 (2,) 广播,权重向量沿行方向扩展。
常见错误
错误 1 · ValueError: operands could not be broadcast together with shapes (2,3) (2,4)
原因:两个数组的某个维度大小不相等且都不为 1,广播失败。
解决:检查两侧 shape,用 reshape/newaxis 调整形状使维度兼容。
错误 2 · 逐列操作时忘记转成列向量
原因:想对每行做操作,但一维向量默认沿最后一个轴(列方向)广播,结果与预期相反。
解决:对行操作时用 v[:, np.newaxis] 或 v.reshape(-1, 1) 转成列向量。
错误 3 · 广播产生意外的维度扩展
原因:两个形状不同但"碰巧兼容"的数组运算,结果形状与直觉不符,后续索引起错。
解决:运算前打印两个数组的 shape,确认结果形状符合预期。
错误 4 · 对 broadcast_to 的结果赋值报错
原因:np.broadcast_to 返回只读视图,尝试写入会抛 ValueError: assignment destination is read-only。
解决:需要可写数组时,对广播结果调用 .copy() 生成独立副本。