返回首页

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

第 3.1 节 核心知识点#

预计学习时长: 40 分钟

本节目标:

  • 说得出广播的三条匹配规则;
  • 会用 None / np.newaxis 给数组加一个维度;
  • 会算外和(两个一维数组两两相加);
  • 会按行缩放、按列缩放,并且分得清 [:, None] 和 [None, :];
  • 会做两两差值和按列归一化。

先搞清楚:广播到底是什么#

广播 = NumPy 在运算之前,悄悄把你的数组「拉伸」成同一个形状。

拉伸?听起来很玄,其实就是复制——只不过复制是 NumPy 在底层帮你做的,不占你写的代码,也不占额外内存。

先看一个最简单的例子:

import numpy as np

a = np.array([1.0, 2.0, 3.0])
b = np.array([10.0, 20.0, 30.0])

print(a + b)
[11. 22. 33.]

形状都是 (3,),长度一样,不需要广播,直接逐个加。

现在把 b 缩短:

print(a + 5)
[6. 7. 8.]

5 是个标量,NumPy 把它当成「到处都是 5 的数组」——这就是最朴素的广播。

再看一个稍微复杂的:

matrix = np.array([
    [1, 2, 3],
    [4, 5, 6],
])
row = np.array([10, 20, 30])

print(matrix + row)
[[11 22 33]
 [14 25 36]]

matrix 是 (3, 2),row 是 (3,)。结果每行都加上了 row。

NumPy 是怎么做到的?它把 row 想象成这样:

[[10 20 30]
 [10 20 30]]

然后逐元素相加。这个「把 1 维的数组沿第 0 维复制多份」的动作,就是广播。

广播的三条匹配规则#

NumPy 广播的判断只有三条规则,从右往左对齐比较:

  1. 两个维度相等 → 直接用;
  2. 其中一个是 1 → 把它「拉长」到另一个的大小(复制多份);
  3. 两个维度都不等,且都不是 1 → 报错,广播不了。

只有一条是 1 的时候才能广播。 举个例子:

a = np.array([1.0, 2.0, 3.0])           # (3,)
b = np.array([10.0, 20.0, 30.0, 40.0])  # (4,)

print(a + b)
ValueError: operands could not be broadcast together with shapes (3,) (4,)

为什么不能? 从右往左比,最后一个维度 3 vs 4——两个都不是 1,规则 2 用不上,直接失败。

换成 (4,) 就能算了:

print(a + np.array([10.0, 20.0, 30.0, 40.0, 50.0]))
ValueError: operands could not be broadcast together with shapes (3,) (5,)

还是不行——长度必须一样,或者其中一个是 1,没别的商量。

一句话记住规则 从右往左逐维比,要么相等,要么其中一个是 1。两个都不是 1 就报错。 判断时把缺失的维度当成 1——(3,) 参与比较时先当成 (1, 3)。

加维度的利器:None 和 np.newaxis#

广播最大的问题是方向容易搞反。解法很简单:给数组加一个维度,改变它的「对齐方向」。

None 和 np.newaxis 效果完全一样,作用是在某一维插入一个长度为 1 的新维度。

a = np.array([1.0, 2.0, 3.0])

print(a.shape)          # (3,)
print(a[:, None].shape) # (3, 1)   ← 行向量
print(a[None, :].shape) # (1, 3)   ← 列向量

这两者的区别是本节最重要的知识点:

写法新维度加在哪形状形象理解
a[:, None]最后面(3, 1)竖着的 3 行 1 列
a[None, :]最前面(1, 3)横着的 1 行 3 列

记忆方法:: 在前面 → 新维度加在后面 → 竖着的列。 : 在后面 → 新维度加在前面 → 横着的行。

搞混了怎么办?记住一句话: 想让数组「竖着」叠起来,用 [:, None];想让数组「横着」排开,用 [None, :]。

知识点 1:外和运算(Outer Sum)#

问题: 有两个一维数组 a 和 b,我想算出每一对元素的和,排成一张表。

比如 a = [1, 2, 3]、b = [10, 20, 30],我想要:

11 21 31
12 22 32
13 23 33

用广播怎么做?

import numpy as np

a = np.array([1.0, 2.0, 3.0])
b = np.array([10.0, 20.0, 30.0])

outer = a[:, None] + b[None, :]
print(outer)
[[11. 21. 31.]
 [12. 22. 32.]
 [13. 23. 33.]]

拆开看这两行代码:

  • a[:, None] → 形状 (3, 1),每一行是一个 a 的元素:
    [[1.]
     [2.]
     [3.]]
  • b[None, :] → 形状 (1, 3),一行 3 个 b 的元素:
    [[10. 20. 30.]]
  • 相加时,NumPy 把 (3,1) 沿列方向复制 3 份,把 (1,3) 沿行方向复制 3 份,然后逐元素加。

形状规律:m 个元素和 n 个元素做外和,结果是 (m, n)。

print(outer.shape)   # (3, 3)

顺便记一下常用变体,都是同一招:

a[:, None] - b[None, :]    # 两两差值
a[:, None] * b[None, :]    # 两两乘积

这就是 NumPy 里的「外积」,只不过 NumPy 叫它「外和」,因为 + 更常用。

知识点 2:按行缩放(Row Scaling)#

问题: 有一张 5 行 4 列的销售表,每个城市(每行)要乘一个自己的系数。

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],
])
city_factor = np.array([1.2, 0.9, 1.5, 0.8, 1.0])

scaled = sales * city_factor[:, None]
print(scaled)
[[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. ]]

为什么是 city_factor[:, None]?

  • sales 的形状是 (5, 4)——5 行(5 个城市),每行 4 列(4 个季度);
  • 我们要的是每个城市乘自己的系数,也就是第 0 行乘 1.2、第 1 行乘 0.9……;
  • 所以系数数组必须变成 (5, 1)——5 行 1 列,这样它沿列方向广播,每行一个数。
print(city_factor.shape)            # (5,)
print(city_factor[:, None].shape)   # (5, 1)

如果写成 city_factor[None, :] 会怎样? 形状变成 (1, 5),沿行方向广播,结果的形状会是 (5, 4) 和 (1, 5) 对不上——直接报错:

ValueError: operands could not be broadcast together with shapes (5,4) (1,5)

一句话记住: [:, None] → 变竖的 → 每个「行」一个系数。 [None, :] → 变横的 → 每个「列」一个系数。 数一数:结果数组有几行,你的系数数组就要有几行。

知识点 3:两两差值计算(Pairwise Differences)#

问题: 两个一维数组,我要算出每一对的差值(a 减 b),排成一张表。

import numpy as np

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.]]

读法:第 0 行是 a 的第 1 个元素减 b 的三个元素。

print(diff[0])    # [-9. -19. -29.]   ← 1 减 10、1 减 20、1 减 30

这个「两两比较」的矩阵非常有用,最常见的场景是:

算距离。 比如两个城市的坐标,算它们之间的距离:

x = np.array([0.0, 3.0])     # 两个点的 x 坐标
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.]]

提示:两个维度同时做广播时,先各自算好二维的差,再平方,最后相加开根号。 写法有点长,但一旦习惯了,比写双层 for 循环快得多,也好懂得多。

知识点 4:按列缩放(Column Scaling)#

问题: 还是那张销售表,这次是每个季度(每列)乘一个自己的系数。

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],
])
quarter_factor = np.array([1.0, 1.1, 1.2, 1.3])

scaled = sales * quarter_factor
print(scaled)
[[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]]

注意这里连 [:, None] 都没写!

因为 quarter_factor 的形状是 (4,),而 sales 的形状是 (5, 4)。从右往左比:最后一个维度 4 vs 4 → 相等 ✓;再往左,sales 是 5,quarter_factor 没有这一维(当成 1)→ 规则 2,拉伸成 5 份 ✓。

结果就是每个季度的那一列乘上自己的系数。

对比记忆(本章最重要的一张表):

想干什么系数数组写法系数形状效果
每行一个系数f[:, None](5, 1)每个城市乘自己的
每列一个系数f(直接用)(4,)每个季度乘自己的

规律:

  • 系数数组的长度 = 列数 → 直接用,作用在列上;
  • 系数数组的长度 = 行数 → 加 [:, None],作用在行上。

sales 是 (5, 4),所以:长度 5 的系数 → [:, None];长度 4 的系数 → 直接用。

知识点 5:按列归一化(Normalize Columns)#

问题: 我想把每一列都变成「0 到 1 之间」——这一列的最大值是 1,最小值是 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],
])

# 每一列的最大值,长度 = 4(列数),可以直接广播
col_max = sales.max(axis=0)
print(col_max)
[200. 210. 195. 230.]

第二步:相除。

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]]

验证一下:每一列的最大值现在是不是 1?

print(normalized.max(axis=0))
[1. 1. 1. 1.]

对了。 这就是「按列归一化」。

注意 axis=0 在这里的意思:

  • sales.max(axis=0) → 对每一列求最大值 → 得到 4 个数(列的方向在压缩);
  • sales.max(axis=1) → 对每一行求最大值 → 得到 5 个数(行的方向在压缩)。

「压缩掉哪个维度,结果就有几个数」。 这一章的归一化要跟列对齐,所以用 axis=0。

顺便:Z-Score 标准化#

归一化还有一种更常见的变体:减均值、除标准差(也叫 Z-Score 标准化)。

z = (sales - sales.mean(axis=0)) / sales.std(axis=0)
print(z)
[[-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]]

它的形状和 sales 完全一样——因为 sales.mean(axis=0) 的形状是 (4,),广播之后每一列都减掉了自己的均值。

怎么选?

除以最大值Z-Score
结果范围0 ~ 1大约 -2 ~ 2
受极端值影响大(一个最大值撑全局)小
什么时候用图片像素、已经有明确上限的量机器学习特征、量纲不一的指标

广播的三个常见套路,一句话记住:

  1. 外和:a[:, None] + b[None, :]
  2. 按行:data * f[:, None]
  3. 按列:data * f

本节小结#

  1. 广播 = 悄悄把你的数组拉伸成同一形状,拉伸方向由「从右往左逐维比较」决定;
  2. 三条规则:相等就用;一个是 1 就拉伸;都不是 1 就报错;
  3. a[:, None] → (n, 1) 竖着;a[None, :] → (1, n) 横着;
  4. 外和:a[:, None] + b[None, :],形状 (m, n);
  5. 按行缩放:data * f[:, None](f 的长度 = 行数);
  6. 按列缩放:data * f(f 的长度 = 列数);
  7. 两两差值:a[:, None] - b[None, :],得到差值矩阵;
  8. 按列归一化:data / data.max(axis=0);Z-Score:(data - data.mean(axis=0)) / data.std(axis=0);
  9. 口诀:结果数组有几行,系数数组就要有几行(加 [:, None]);有几列,系数就要有几列(直接用)。

下一节预告 广播机制你已经会用了,但还有个更根本的问题:为什么用 NumPy 算就比用 Python 循环快? 第 4 章会讲向量化运算——把「对每个元素做点什么」的循环,全部改成「对整个数组做点什么」的一行代码,还会介绍排序、argsort、范数这些数组级利器。

阅读 --