🏠 总目录📚 本教程 02 · shape 与 axis 到底怎么数 ← →
📑 本页目录(点开跳转)

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=False -> (3,) [ 6. 22. 38.]
keepdims=True -> (3, 1)
[[ 6.]
[22.]
[38.]]
每行除以自己的行和(keepdims=True,能广播回去):
[[0. 0.16666667 0.33333333 0.5 ]
[0.18181818 0.22727273 0.27272727 0.31818182]
[0.21052632 0.23684211 0.26315789 0.28947368]]
换成 keepdims=False:
ValueError: operands could not be broadcast together with shapes (3,4) (3,)

⭐ 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 是'你替我算',不是轴号")

实跑输出:

关键信息

v.shape = (4,)
v[:, None].shape = (4, 1) <- 在后面插一根长度 1 的轴(列向量)
v[None, :].shape = (1, 4) <- 在前面插(行向量)
np.newaxis is None -> True
v[:,None] - v[None,:] 得到一张差值表:
[[ 0 -1 -2 -3]
[ 1 0 -1 -2]
[ 2 1 0 -1]
[ 3 2 1 0]]
x.sum(axis=-1).shape = (3,) 和 axis=1 一样
x.reshape(-1).shape = (12,) 这里的 -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) 是本章第三节的正主。那是一道题的写法,这一章是它成立的理由

✅ 检查点

  1. axis=k 到底是什么意思?用一句不含「行」「列」的话说清楚。
  2. (2,3,4) 的数组,sum(axis=1) 输出什么形状?sum(axis=(0,2)) 呢?sum() 呢?
  3. 「axis=0 就是按列」这个说法哪里危险?
  4. 标准化(每个特征减自己的均值)该用 axis=0 还是 axis=1?为什么?
  5. keepdims=True 解决的是什么问题?什么时候必须加、什么时候不用加?
  6. np.newaxis 和 None 是什么关系?v[:,None] - v[None,:] 得到什么?
  7. ⭐ x.sum(axis=-1) 里的 -1 和 x.reshape(-1) 里的 -1,含义分别是什么?怎么区分?
  8. 为什么写库的人几乎一律用 axis=-1 而不是 axis=2?
👀 答案
  1. axis=k 表示这次归约要让 shape 元组里第 k 个数字消失。 不用想「横着竖着」——那些说法二维时勉强能用,到 (B,L,D) 就全乱。
  2. (2,3,4) → sum(axis=1) 得 (2,4)(第 1 位没了);sum(axis=(0,2)) 得 (3,)(一次删两位);sum() 得 (),一个 0 维数组。
  3. 它描述的是结果(结果确实是每列一个数),但你要填的是被消掉的那根轴。二维时碰巧能对上,到 (B,L,D) 上「按列」就没有意义了,而「消掉 1 号轴」永远有意义。
  4. axis=0。标准化是「每个特征减自己的均值」,要消掉的是样本那根轴,结果 shape 是 (4,)(每个特征一个数)。theory_md/05b:231 的 Xc = X - X.mean(axis=0) 就是这个形状。
  5. 解决的是「轴被删掉之后,那根轴的位置信息也没了,没法广播回原数组」。实跑:x 是 (3,4),x.sum(axis=1) 是 (3,),x / s1 直接 ValueError;keepdims=True 给出 (3,1),长度 1 的轴能被拉伸回去,x / s2 就对了。判据:结果还要和原数组一起做运算就加,结果就是最终答案就不加。
  6. 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,:,:] 是同一个模式的三维版。
  7. axis=-1 里的 -1 是轴号(从后往前数,-1 是最后一根);reshape(-1) 里的 -1 是「这一位你替我算」的占位符。区分办法:axis= 后面的是轴号,reshape() / view() 括号里的是占位符。 ⚠️ reshape(-1,-1) 会报 can only specify one unknown dimension。
  8. 因为 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

打卡记录保存在你的浏览器里,首页能看到总进度