返回首页

Roadmap
更新于 2026年10月25日 全文 1966 字未阅读

第 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. 两个维度相等 → 直接用;
  2. 其中一个是 1 → 把它拉伸成另一个的大小;
  3. 两个维度不等,且都不是 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)

原因:最后一维 3 vs 1 可以拉伸 ✓,但再往左一维 2 vs 3—— 两个都不是 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 个点的距离。

两个要点:

  1. 最后一定要 np.sqrt,平方和不是距离,是距离的平方;
  2. 结果一定是对称的(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;
  2. 每个维度上,两个形状要么相等,要么其中一个是 1;
  3. 出现「都不等且都不是 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. 广播 = 隐式的拉伸复制,不占额外内存;
  2. 三条规则:相等就用、一个是 1 就拉伸、都不等就报错;
  3. 从右往左对齐,缺失维度当 1;
  4. [:, None] 加维度,np.newaxis 是它的别名;
  5. (n,) 横着、(n, 1) 竖着,方向不同结果完全不同;
  6. 外和:a[:, None] + b[None, :],形状 (m, n);
  7. 按行用 f[:, None](长度=行数),按列直接用 f(长度=列数);
  8. 两两差值/距离:a[:, None] - b[None, :],距离记得最后 np.sqrt;
  9. 归一化用 axis=0 求每列统计量再广播回去;
  10. 限制:每个维度要么相等,要么有一个是 1。

下一章预告 广播已经让你能一行算出一张表了。但真正的威力在于:把「对每个元素做点什么」的循环,彻底改写成「对整个数组做点什么」的一行代码。第 4 章讲向量化运算,还会介绍聚合统计、sort / argsort、以及用 np.linalg.norm() 做向量长度——这些是机器学习代码里天天见的东西。

阅读 --