📑 本页目录(点开跳转)
02 · shape 和 axis 到底怎么数
⏱ 48 分钟 | ⭐ axis=1 不是「第 1 行」也不是「按列」,是「把 1 号轴消掉」—— 换成这个说法之后你再也不会数错
🎯 一句话
axis=k 的意思只有一个:这次归约要让 shape 里第 k 个数字消失。
不用去想「行」「列」「横着」「竖着」——那些说法在二维时勉强能用,
一到三维((B, L, D))就全乱。只看形状元组,看哪个数字没了。
📐 一、shape 是一个元组,从外往里读
import numpy as np
a = np.arange(24).reshape(2, 3, 4)
print("shape =", a.shape, " ndim =", a.ndim, " size =", a.size)
for ax in (0, 1, 2):
print(f"a.sum(axis={ax}).shape = {a.sum(axis=ax).shape} <- {ax} 号轴被消掉了")
print("a.sum(axis=(0, 2)).shape =", a.sum(axis=(0, 2)).shape, " <- 一次消两个")
print("a.sum().shape =", a.sum().shape, " <- 不给 axis 就全消光")
实跑输出:
对照
shape = (2, 3, 4) ndim = 3 size = 24
a.sum(axis=0).shape = (3, 4) <- 0 号轴被消掉了
a.sum(axis=1).shape = (2, 4) <- 1 号轴被消掉了
a.sum(axis=2).shape = (2, 3) <- 2 号轴被消掉了
a.sum(axis=(0, 2)).shape = (3,) <- 一次消两个
a.sum().shape = () <- 不给 axis 就全消光
⭐ 规律一眼可见:输入 (2,3,4),axis=k 的输出就是把第 k 位删掉。
axis 还能给一个元组,一次删多位;不给就全删光,得到一个 0 维数组(shape=())。
shape 元组该怎么念:从左到右是从外层到内层。(2, 3, 4) 念成
「2 个 block,每个 block 里 3 行,每行 4 个数」。深度学习里最常见的
(B, L, D) 就是「B 个样本,每个样本 L 个位置,每个位置 D 维」。
⭐ 心法:想不清楚
axis该填几,就问自己「我想让哪个数字从 shape 里消失」。 想得到「每个位置的 D 维向量的长度」?那要消掉 D,D 在第 2 位,所以axis=2(或axis=-1)。
🧩 二、二维的两个说法,为什么容易错
(3, 4) 表示「3 个样本 × 4 个特征」时:
import numpy as np
x = np.array([[1., 2., 3., 4.],
[5., 6., 7., 8.],
[9., 10., 11., 12.]])
print("x.shape =", x.shape, " 3 个样本 x 4 个特征")
print("x.mean(axis=0) =", x.mean(axis=0), " shape", x.mean(axis=0).shape,
" <- 消掉样本轴 = 每个特征的均值")
print("x.mean(axis=1) =", x.mean(axis=1), " shape", x.mean(axis=1).shape,
" <- 消掉特征轴 = 每个样本的均值")
print("\n标准化时减的是哪一个:")
print("x - x.mean(axis=0) 能算,shape", (x - x.mean(axis=0)).shape)
try:
x - x.mean(axis=1)
except ValueError as e:
print("x - x.mean(axis=1) ValueError:", e)
print("x - x.mean(axis=1, keepdims=True) 能算,shape",
(x - x.mean(axis=1, keepdims=True)).shape)
实跑输出:
算一算
x.shape = (3, 4) 3 个样本 x 4 个特征
x.mean(axis=0) = [5. 6. 7. 8.] shape (4,) <- 消掉样本轴 = 每个特征的均值
x.mean(axis=1) = [ 2.5 6.5 10.5] shape (3,) <- 消掉特征轴 = 每个样本的均值
标准化时减的是哪一个:
x - x.mean(axis=0) 能算,shape (3, 4)
x - x.mean(axis=1) ValueError: operands could not be broadcast together with shapes (3,4) (3,)
x - x.mean(axis=1, keepdims=True) 能算,shape (3, 4)
⚠️ 「axis=0 是按列」这个说法为什么危险:它描述的是结果(结果确实是每列一个数),
但你要填的是被消掉的那根轴。二维时这两个说法碰巧能对上,
到了 (B, L, D) 上「按列」就没有意义了,而「消掉 1 号轴」永远有意义。
⭐ 数据预处理里绝大多数情况用 axis=0 —— 标准化是「每个特征减自己的均值」,
要消掉的是样本那根轴。站内几处裸用都是这个形状:
theory_md/05b-PCA的推导.md:231 写 Xc = X - X.mean(axis=0)(PCA 的中心化),
theory_md/08-偏差方差分解.md:301 写 preds.mean(axis=0)(对多次重采样的预测求平均)。
🪜 三、keepdims 为什么存在
看上面那个 ValueError:x 是 (3,4),x.mean(axis=1) 是 (3,),两者不能广播。
因为轴被删掉之后,那根轴的位置信息也没了。
import numpy as np
x = np.arange(12, dtype=float).reshape(3, 4)
s1 = x.sum(axis=1)
s2 = x.sum(axis=1, keepdims=True)
print("keepdims=False ->", s1.shape, s1)
print("keepdims=True ->", s2.shape)
print(s2)
print("\n每行除以自己的行和(keepdims=True,能广播回去):")
print(x / s2)
print("\n换成 keepdims=False:")
try:
print(x / s1)
except ValueError as e:
print("ValueError:", e)
实跑输出:
关键信息
⭐ keepdims=True 的作用是把消掉的那根轴留成长度 1,(3,) 变成 (3,1)。
长度 1 的轴可以被广播拉伸回去(规则见第 03 章),
所以「消了轴还要广播回去」的场合一律加 keepdims=True。
⭐ 判据很好记:结果还要和原数组一起做运算 → 加 keepdims;结果就是最终答案 → 不加。
这就是 ML 基础 附录 C 第 1 题 那行
z = z - z.max(axis=1, keepdims=True)里keepdims的全部理由: 减完之后z还要保持(N, C)。 ⚠️ 那道题里keepdims只是顺手写的,没人解释过它为什么必须在。
➕ 四、加一根轴:None 和 np.newaxis
keepdims 只能在归约的时候顺手留轴。想凭空插一根,用 None:
import numpy as np
v = np.arange(4)
print("v.shape =", v.shape)
print("v[:, None].shape =", v[:, None].shape, " <- 在后面插一根长度 1 的轴(列向量)")
print("v[None, :].shape =", v[None, :].shape, " <- 在前面插(行向量)")
print("np.newaxis is None ->", np.newaxis is None)
print("\nv[:,None] - v[None,:] 得到一张差值表:")
print(v[:, None] - v[None, :])
x = np.arange(12).reshape(3, 4)
print("\nx.sum(axis=-1).shape =", x.sum(axis=-1).shape, " 和 axis=1 一样")
print("x.reshape(-1).shape =", x.reshape(-1).shape, " 这里的 -1 是'你替我算',不是轴号")
实跑输出:
关键信息
⭐ np.newaxis 就是 None 本身(np.newaxis is None 为 True),
两种写法完全等价 —— x[:, np.newaxis] 更长但更醒目,x[:, None] 更短。
⚠️ 站内 np.newaxis 这个写法命中 0 次,[:, None] 只在两处出现过
(data_md/06:184 的 MinHash、ml_md/附录C:399 的 K-means),两处都是裸用、没有一个字的解释 ——
但你在别人的代码里两种写法都会见到。
⭐ v[:,None] - v[None,:] 这个组合是全站最值钱的一个模式:
(4,1) 和 (1,4) 相减,广播成 (4,4),一次拿到所有两两组合。
第 03 章会用它算 400 个点的两两距离,
ml_md/附录C:399 那行 X[:, None, :] - C[None, :, :](K-means 算距离)也是同一个模式的三维版。
⚠️ 五、两个 -1 不是一回事(高频混淆)
这是本章唯一一个真正容易搞错的地方:
| 写法 | -1 的含义 |
例子 |
|---|---|---|
x.sum(axis=-1) |
轴号,从后往前数:-1 = 最后一根轴,-2 = 倒数第二根 |
(3,4) 的 axis=-1 等于 axis=1 |
x.reshape(-1) |
「这一位你替我算」,把总元素数除以其他维度得出 | (3,4).reshape(-1) = (12,);.reshape(2,-1) = (2,6) |
⭐ 区分办法:axis= 后面的 -1 是轴号,reshape() / view() 括号里的 -1 是占位符。
⭐ 为什么大家爱写 axis=-1:因为它不依赖数组有几维。
(B, L, D) 和 (L, D) 都可以写 axis=-1 来消掉最后那根特征轴,
换 batch 维、加一根 head 维,代码都不用改。写库的人几乎一律用负轴。
⚠️ reshape(-1) 只能出现一次。reshape(-1, -1) 会报
ValueError: can only specify one unknown dimension,因为两个未知数解不出来。
📋 六、一页速记
| 想干的事 | 写法 |
|---|---|
| 消掉第 k 根轴 | a.sum(axis=k) / a.mean(axis=k) / a.max(axis=k) |
| 消掉最后一根轴(不管几维) | a.sum(axis=-1) |
| 一次消好几根 | a.sum(axis=(0, 2)) |
| 全消光成标量 | a.sum() |
| 消掉轴但保留位置(还要广播回去) | a.sum(axis=k, keepdims=True) |
| 凭空插一根长度 1 的轴 | a[:, None] / a[None, :] / a[..., None] |
| 压平成一维 | a.reshape(-1) / a.ravel() |
| 只知道一部分形状 | a.reshape(2, -1) |
| 找最大值的位置而不是值 | a.argmax(axis=k)(⭐ 输出 shape 和 a.max(axis=k) 一样) |
⚠️ ...(Ellipsis) 是「剩下的轴全要」:a[..., 0] 不管 a 有几维,
都表示「最后那根轴取第 0 个」。和 a[:, :, 0] 相比,它不依赖维数。
🔗 这一章连到哪里
| 相关的地方 | 为什么 |
|---|---|
| 03 · 广播的三条规则 | ⭐ 本章的 keepdims 和 None 只说了「留一根长度 1 的轴」,为什么长度 1 就能拉伸是那一章的正题 |
| 05 · 内存布局、stride 与 C/F order | ⭐ 本章说 axis 是形状元组里的位置。那一章讲每根轴在内存里对应多大的跳步,以及为什么 sum(axis=0) 和 sum(axis=1) 耗时能差 2 倍 |
| 06 · 索引的四种形态 | a[:, None] 用的是索引语法。那一章讲清索引的四种形态各自返回什么 |
| 11 · 从 NumPy 到 PyTorch | ⚠️ PyTorch 里同一件事叫 dim 不叫 axis,报错信息和文档全用 dim |
| 数学原理 05b · PCA 的推导 | 那里的 Xc = X - X.mean(axis=0) 是本章第二节的现场:中心化要消掉样本轴 |
| 数学原理 08 · 偏差方差分解 | 那里的 preds.mean(axis=0) / preds.var(axis=0) 是同一个形状:多次重采样的预测堆成 (次数, 样本),消掉「次数」那根轴 |
| 数学原理 01b · KL 散度 | 那里的 np.sum(P * np.log(q), axis=1) 是「每个样本的分布求和」——消掉的是类别轴 |
| ML 基础 附录 C · 手撕代码速查 | ⭐ 第 1 题那行 z - z.max(axis=1, keepdims=True) 是本章第三节的正主。那是一道题的写法,这一章是它成立的理由 |
✅ 检查点
axis=k到底是什么意思?用一句不含「行」「列」的话说清楚。(2,3,4)的数组,sum(axis=1)输出什么形状?sum(axis=(0,2))呢?sum()呢?- 「
axis=0就是按列」这个说法哪里危险? - 标准化(每个特征减自己的均值)该用
axis=0还是axis=1?为什么? keepdims=True解决的是什么问题?什么时候必须加、什么时候不用加?np.newaxis和None是什么关系?v[:,None] - v[None,:]得到什么?- ⭐
x.sum(axis=-1)里的-1和x.reshape(-1)里的-1,含义分别是什么?怎么区分? - 为什么写库的人几乎一律用
axis=-1而不是axis=2?
👀 答案
axis=k表示这次归约要让shape元组里第 k 个数字消失。 不用想「横着竖着」——那些说法二维时勉强能用,到(B,L,D)就全乱。(2,3,4)→sum(axis=1)得(2,4)(第 1 位没了);sum(axis=(0,2))得(3,)(一次删两位);sum()得(),一个 0 维数组。- 它描述的是结果(结果确实是每列一个数),但你要填的是被消掉的那根轴。二维时碰巧能对上,到
(B,L,D)上「按列」就没有意义了,而「消掉 1 号轴」永远有意义。 axis=0。标准化是「每个特征减自己的均值」,要消掉的是样本那根轴,结果 shape 是(4,)(每个特征一个数)。theory_md/05b:231的Xc = X - X.mean(axis=0)就是这个形状。- 解决的是「轴被删掉之后,那根轴的位置信息也没了,没法广播回原数组」。实跑:
x是(3,4),x.sum(axis=1)是(3,),x / s1直接ValueError;keepdims=True给出(3,1),长度 1 的轴能被拉伸回去,x / s2就对了。判据:结果还要和原数组一起做运算就加,结果就是最终答案就不加。 np.newaxis就是None本身(np.newaxis is None为True),两种写法完全等价。v[:,None]是(4,1)、v[None,:]是(1,4),相减广播成(4,4),一次拿到所有两两组合(实跑那张差值表)。ml_md/附录C:399的X[:,None,:] - C[None,:,:]是同一个模式的三维版。axis=-1里的-1是轴号(从后往前数,-1是最后一根);reshape(-1)里的-1是「这一位你替我算」的占位符。区分办法:axis=后面的是轴号,reshape()/view()括号里的是占位符。 ⚠️reshape(-1,-1)会报can only specify one unknown dimension。- 因为
axis=-1不依赖数组有几维。(B,L,D)和(L,D)都能写axis=-1消掉最后那根特征轴,换 batch 维、加一根 head 维,代码都不用改。
🛑 可以停在这里
⚡ 走神救援
⭐
axis=k的意思只有一个:这次归约要让shape元组里第 k 个数字消失。别去想「行」「列」「横着竖着」——那些说法二维时勉强能用,到
(B, L, D)就全乱。shape 元组从左到右是从外层到内层,(2,3,4)念成「2 个 block,每个 3 行,每行 4 个数」。⭐ 心法:想不清
axis填几,就问「我想让哪个数字从 shape 里消失」。 ⚠️ 「axis=0就是按列」这个说法危险:它描述的是结果,而你要填的是被消掉的那根轴——二维碰巧对得上,三维就没意义了。⭐ 数据预处理绝大多数用
axis=0,因为标准化是「每个特征减自己的均值」,消掉的是样本轴。
keepdims为什么存在:轴被删掉之后位置信息也没了,结果再和原数组相除就会报「could not be broadcast together」;加上它保留一根长度为 1 的轴,广播就能把它拉回去。⭐ 判据:结果还要和原数组一起做运算 → 加;结果就是最终答案 → 不加。凭空插轴用
None(⭐np.newaxis就是None本身)。⭐
v[:,None] - v[None,:]是全站最值钱的一个模式:(4,1)和(1,4)相减广播成(4,4),一次拿到所有两两组合——K-means 算距离就是它的三维版。⚠️⚠️ 两个
-1不是一回事:axis=-1里的-1是轴号(从后往前数),reshape(-1)里的-1是「这一维你自己算」。
下一节 👉 03-广播的三条规则.md