第 3.3 节 常见问题#
这一节收集了初学者在 NumPy 广播机制上最常卡的 10 个问题。每个问题都给出直接答案,能跑代码的都附上完整示例。
怎么用这一节 不用通读。遇到问题用 Ctrl+F 搜关键词,直接跳到对应那条。
准备一下数据,下面每题都会用到:
import numpy as np
sales = np.array([
[120.0, 135.0, 150.0, 165.0],
[ 80.0, 92.0, 88.0, 105.0],
[200.0, 210.0, 195.0, 230.0],
[ 60.0, 72.0, 85.0, 78.0],
[150.0, 145.0, 160.0, 175.0],
])
a = np.array([1.0, 2.0, 3.0])
b = np.array([10.0, 20.0, 30.0])Q1:什么是 NumPy 广播机制?为什么需要广播?#
答: 广播就是 NumPy 在运算前,悄悄把你的数组「拉伸」成同一个形状。
为什么需要它?因为真实数据里,「主体」和「系数」的长度往往不一样。
sales = np.zeros((5, 4)) # (5, 4) 5 个城市 × 4 个季度
factor = np.ones(5) # (5,) 5 个城市各自的系数如果没有广播,你得写一层 for 循环把系数贴到每一行;有广播,一行搞定:
print(sales * factor[:, None])拉伸的本质是「复制」——但 NumPy 在底层用「重复读取同一块内存」的方式实现,不占额外内存、不复制数据。
一句话:广播 = 隐式的
np.tile+ 隐式的逐元素运算。
Q2:NumPy 广播规则的核心判断条件是什么?#
答: 从右往左逐维比较,三条规则:
- 两个维度相等 → 直接用;
- 其中一个是 1 → 把它拉伸成另一个的大小;
- 两个维度不等,且都不是 1 → 报错。
# 规则 1:相等
print(np.array([1., 2]) + np.array([10., 20]))
# 规则 2:其中一个是 1
print(np.array([[1., 2.], [3., 4.]]) + np.array([10., 20.]))[11. 22.]
[[11. 22.]
[13. 24.]]规则 3:两个维度都不等、也都不是 1,直接报错:
print(np.array([1., 2., 3.]) + np.array([10., 20.]))ValueError: operands could not be broadcast together with shapes (3,) (2,)「从右往左」是关键词。 对齐时先比最后一维,再比倒数第二维…… 数组维数不够时,缺失的维度当成 1 来比。
Q3:当两个数组的形状不一致时,如何判断它们能否进行运算?#
答: 自己在纸上比一遍,就三步:
第一步:把两个形状写下来,从右往左对齐。
print(a.shape) # (3,)
print(b.shape) # (3,)第二步:逐维比,标记谁需要拉伸。
第三步:出现「两个都不是 1」就广播不了。
实战中最好用的是 np.broadcast_shapes()——它直接告诉你结果形状,形状不合法就报错:
print(np.broadcast_shapes((5, 4), (4,))) # (5, 4)
print(np.broadcast_shapes((5, 4), (5, 1))) # (5, 4)(5, 4)
(5, 4)形状对不上时,它会直接报错:
print(np.broadcast_shapes((5, 4), (1, 5)))ValueError: shape mismatch: objects cannot be broadcast to a single shape. Mismatch is between arg 0 with shape (5, 4) and arg 1 with shape (1, 5).调试广播报错时,就用这个函数把两个形状丢进去。 它比你自己在脑子里推要快得多,也不会推错。
Q4:如何使用 None 或 np.newaxis 增加数组维度?#
答: 在索引里加一个 None,就是在那一维插入一个长度为 1 的新维度。
a = np.array([1.0, 2.0, 3.0])
print(a[:, None].shape) # (3, 1)
print(a[None, :].shape) # (1, 3)
print(np.newaxis) # None,np.newaxis 就是 None 的别名None 和 np.newaxis 完全等价,np.newaxis 只是更好读:
print(a[:, np.newaxis].shape) # (3, 1) 和 a[:, None] 一模一样新维度加在哪,取决于 : 的位置:
| 写法 | 新维度加在 | 结果形状 |
|---|---|---|
a[:, None] | 最后 | (3, 1) 竖着 |
a[None, :] | 最前面 | (1, 3) 横着 |
顺便说个技巧:
a.reshape(-1, 1)和a[:, None]效果一样,都能变成(3, 1)。 但[:, None]更直观——一眼就看出「我加了一维」,而reshape你得在脑子里算-1是哪一维。
Q5:为什么形状为 (n,) 的数组与形状为 (n, 1) 的数组含义不同?#
答: 它们在广播里的「方向」完全不同。
a = np.array([1.0, 2.0, 3.0])
print(a.shape) # (3,)
print(a[:, None].shape) # (3, 1)(3,) 参与广播时,是「横着」的一行——它会沿行的方向拉伸:
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
])
print(matrix + a)[[2. 4. 6.]
[5. 7. 9.]]看结果:第 0 列全加了 1,第 1 列全加了 2,第 2 列全加了 3。
也就是说,a[j] 加到了第 j 列的每一行上。
(3, 1) 是「竖着」的一列——它会沿列的方向拉伸。
不过要注意,它的高度必须跟矩阵的行数对得上:
row_add = np.array([10.0, 20.0])[:, None] # 形状 (2, 1)
print(matrix + row_add)[[11. 12. 13.]
[24. 25. 26.]]它加的是「第 0 行全加 10、第 1 行全加 20」。
如果你硬把
(3, 1)跟(2, 3)加在一起,会报错:print(matrix + a[:, None])ValueError: operands could not be broadcast together with shapes (2,3) (3,1)原因:最后一维
3vs1可以拉伸 ✓,但再往左一维2vs3—— 两个都不是 1,拉不了。 记住:竖着的数组,它的「长度」要等于矩阵的行数**。**
对照表(这张表值得背下来):
形状 形象 跟 (3, 3)矩阵相加时(3,)横着的一行 每列加同一个数 (3, 1)竖着的一列 每行加同一个数 记忆:横着的加到「列」上,竖着的加到「行」上。
Q6:如何利用广播机制计算两个一维数组的外和?#
答: 一行代码:a[:, None] + b[None, :]。
a = np.array([1.0, 2.0, 3.0])
b = np.array([10.0, 20.0, 30.0])
print(a[:, None] + b[None, :])[[11. 21. 31.]
[12. 22. 32.]
[13. 23. 33.]]形状规律:m 个元素和 n 个元素做外和,结果是 (m, n)。
其他常用变体(同一招):
a[:, None] - b[None, :] # 两两差值
a[:, None] * b[None, :] # 两两乘积别忘了 NumPy 自带的
np.outer():print(np.outer(a, b))[[10. 20. 30.] [20. 40. 60.] [30. 60. 90.]]注意
np.outer做的是乘积,而a[:, None] + b[None, :]做的是加法。 名字都叫 outer,容易混——记住np.outer是乘。
Q7:如何通过广播机制对矩阵的每一行或每一列进行缩放?#
答: 看系数数组的长度。
import numpy as np
sales = np.array([
[120.0, 135.0, 150.0, 165.0],
[ 80.0, 92.0, 88.0, 105.0],
[200.0, 210.0, 195.0, 230.0],
[ 60.0, 72.0, 85.0, 78.0],
[150.0, 145.0, 160.0, 175.0],
])按行缩放(每行乘一个系数,系数长度 = 5 = 行数):
city_factor = np.array([1.2, 0.9, 1.5, 0.8, 1.0])
print(sales * city_factor[:, None])[[144. 162. 180. 198. ]
[ 72. 82.8 79.2 94.5]
[300. 315. 292.5 345. ]
[ 48. 57.6 68. 62.4]
[150. 145. 160. 175. ]]按列缩放(每列乘一个系数,系数长度 = 4 = 列数):
quarter_factor = np.array([1.0, 1.1, 1.2, 1.3])
print(sales * quarter_factor)[[120. 148.5 180. 214.5]
[ 80. 101.2 105.6 136.5]
[200. 231. 234. 299. ]
[ 60. 79.2 102. 101.4]
[150. 159.5 192. 227.5]]一张表记住:
系数长度 写法 作用 = 行数(5) f[:, None]每行一个系数 = 列数(4) f每列一个系数 一行代码搞错方向就会报
ValueError: operands could not be broadcast together,报错信息里直接写着你算错的形状。
Q8:如何计算两个数组之间的两两距离或差值矩阵?#
答: 用 [:, None] 和 [None, :] 造出两张「广播版」的数组再运算。
两两差值:
a = np.array([1.0, 2.0, 3.0])
b = np.array([10.0, 20.0, 30.0])
diff = a[:, None] - b[None, :]
print(diff)[[ -9. -19. -29.]
[ -8. -18. -28.]
[ -7. -17. -27.]]两两欧氏距离(坐标点之间的直线距离):
x = np.array([0.0, 3.0])
y = np.array([0.0, 4.0])
dist = np.sqrt((x[:, None] - x[None, :])**2 + (y[:, None] - y[None, :])**2)
print(dist)[[0. 5.]
[5. 0.]]读法:dist[i, j] 就是第 i 个点到第 j 个点的距离。
两个要点:
- 最后一定要
np.sqrt,平方和不是距离,是距离的平方; - 结果一定是对称的(
dist[i, j] == dist[j, i]),因为距离本来就是对称的。
两个维度同时做广播时,拆成中间变量更容易读:
dx = x[:, None] - x[None, :] dy = y[:, None] - y[None, :] dist = np.sqrt(dx**2 + dy**2)
Q9:如何使用广播机制对矩阵各列进行标准化或归一化?#
答: 用 axis=0 求出每一列的统计量,再广播回去。
普通归一化(除以每列最大值):
import numpy as np
sales = np.array([
[120.0, 135.0, 150.0, 165.0],
[ 80.0, 92.0, 88.0, 105.0],
[200.0, 210.0, 195.0, 230.0],
[ 60.0, 72.0, 85.0, 78.0],
[150.0, 145.0, 160.0, 175.0],
])
col_max = sales.max(axis=0) # (4,),长度 = 列数
normalized = sales / col_max # 直接广播
print(normalized)[[0.6 0.64285714 0.76923077 0.7173913 ]
[0.4 0.43809524 0.45128205 0.45652174]
[1. 1. 1. 1. ]
[0.3 0.34285714 0.43589744 0.33913043]
[0.75 0.69047619 0.82051282 0.76086957]]Z-Score 标准化(减每列均值、除每列标准差):
zscore = (sales - sales.mean(axis=0)) / sales.std(axis=0)
print(zscore)[[-0.04003204 0.08774867 0.33648534 0.26779681]
[-0.84067281 -0.81063056 -1.11227098 -0.84802323]
[ 1.5612495 1.65468918 1.38800202 1.47660186]
[-1.24099319 -1.22848136 -1.18237209 -1.35014225]
[ 0.56044854 0.29667407 0.57015571 0.45376682]]
axis=0的含义再强调一遍:sales.max(axis=0)是「对每一列求最大值」→ 得到 4 个数(每列一个)。sales.max(axis=1)是「对每一行求最大值」→ 得到 5 个数(每行一个)。压掉哪一维,结果就剩几个数。 要跟列对齐就用
axis=0(长度 = 列数),要跟行对齐就用axis=1(长度 = 行数)。
Q10:广播机制与显式复制数组相比有什么优势?广播运算有哪些形状限制?#
答: 三个优势:不占内存、代码短、不容易写错。
显式复制:
import numpy as np
sales = np.array([
[120.0, 135.0],
[ 80.0, 92.0],
])
f = np.array([1.2, 0.9])
# 显式复制:真的造出 2×2 的新数组
big = np.tile(f, (2, 1))
print(big)[[1.2 0.9]
[1.2 0.9]]广播:
print(sales * f)[[144. 121.5]
[ 96. 82.8]]一样的结果,但广播:
- 不额外占内存——
np.tile真的分配了 2×2 的空间,广播只是「假装」它有; - 代码更短——一行搞定,不用先造中间变量;
- 不容易写错维度——不用考虑 tile 几行、几列。
广播的形状限制(记住三条就够):
- 从右往左对齐,缺失的维度当成 1;
- 每个维度上,两个形状要么相等,要么其中一个是 1;
- 出现「都不等且都不是 1」就报错。
典型的失败情况:
print(np.array([1., 2., 3.]) + np.array([10., 20.]))ValueError: operands could not be broadcast together with shapes (3,) (2,)原因:从右往左比,最后一维 3 vs 2——两个都不是 1,没法拉伸,直接失败。
再看一个二维的:
matrix = np.array([[1., 2., 3.], [4., 5., 6.]]) # (2, 3)
f = np.array([1., 2., 3., 4.]) # (4,)
print(matrix + f)ValueError: operands could not be broadcast together with shapes (2,3) (4,)最后一维 3 vs 4 → 失败。规则 2 只允许「其中一个是 1」,长度不同但都不是 1 的,一律不行。
把长度对齐就能广播了:
print(matrix + f[:3])[[2. 4. 6.] [5. 7. 9.]]
小结#
这一节的 10 个问题,其实都围绕**「两个形状不一样的数组怎么一起算」**:
- 广播 = 隐式的拉伸复制,不占额外内存;
- 三条规则:相等就用、一个是 1 就拉伸、都不等就报错;
- 从右往左对齐,缺失维度当 1;
[:, None]加维度,np.newaxis是它的别名;(n,)横着、(n, 1)竖着,方向不同结果完全不同;- 外和:
a[:, None] + b[None, :],形状(m, n); - 按行用
f[:, None](长度=行数),按列直接用f(长度=列数); - 两两差值/距离:
a[:, None] - b[None, :],距离记得最后np.sqrt; - 归一化用
axis=0求每列统计量再广播回去; - 限制:每个维度要么相等,要么有一个是 1。
下一章预告 广播已经让你能一行算出一张表了。但真正的威力在于:把「对每个元素做点什么」的循环,彻底改写成「对整个数组做点什么」的一行代码。第 4 章讲向量化运算,还会介绍聚合统计、
sort/argsort、以及用np.linalg.norm()做向量长度——这些是机器学习代码里天天见的东西。