第 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 → 把它「拉长」到另一个的大小(复制多份);
- 两个维度都不等,且都不是 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 |
| 受极端值影响 | 大(一个最大值撑全局) | 小 |
| 什么时候用 | 图片像素、已经有明确上限的量 | 机器学习特征、量纲不一的指标 |
广播的三个常见套路,一句话记住:
- 外和:
a[:, None] + b[None, :]- 按行:
data * f[:, None]- 按列:
data * f
本节小结#
- 广播 = 悄悄把你的数组拉伸成同一形状,拉伸方向由「从右往左逐维比较」决定;
- 三条规则:相等就用;一个是
1就拉伸;都不是1就报错; a[:, None]→(n, 1)竖着;a[None, :]→(1, n)横着;- 外和:
a[:, None] + b[None, :],形状(m, n); - 按行缩放:
data * f[:, None](f的长度 = 行数); - 按列缩放:
data * f(f的长度 = 列数); - 两两差值:
a[:, None] - b[None, :],得到差值矩阵; - 按列归一化:
data / data.max(axis=0);Z-Score:(data - data.mean(axis=0)) / data.std(axis=0); - 口诀:结果数组有几行,系数数组就要有几行(加
[:, None]);有几列,系数就要有几列(直接用)。
下一节预告 广播机制你已经会用了,但还有个更根本的问题:为什么用 NumPy 算就比用 Python 循环快? 第 4 章会讲向量化运算——把「对每个元素做点什么」的循环,全部改成「对整个数组做点什么」的一行代码,还会介绍排序、
argsort、范数这些数组级利器。