第 21 章 · 广播机制:不同形状怎么一起算
这一章要解决什么:让形状不同的数组能一起运算,并写出按列标准化的代码。 这一章藏着一个惊喜:Z-score 标准化的公式,就是 sklearn 的
StandardScaler的全部内容。
本章你需要完成的代码:demos/ch21-broadcasting.py
21.1 从最简单的"广播"说起
import numpy as np
caps = np.array([2.50, 2.45, 2.40, 2.35])
print(caps - 0.05) # [2.45 2.4 2.35 2.3 ]
print(caps * 1000) # [2500. 2450. 2400. 2350.]一个标量和一个数组运算,标量会自动"铺"到每个元素上。
这就是广播(broadcasting):NumPy 在背后把小的那个"复制"到和大的同样形状, 然后做逐元素运算 —— 但不真的复制内存,所以没有额外开销。
这是最自然的一种广播,你可能已经在不知不觉中用了。
21.2 广播三条规则
当两个数组形状不同时,NumPy 按这三条决定能不能算:
① 从最右边一维开始,逐维比较两个数组的形状;
② 这一维若「相等」或「其中一个是 1」→ 兼容,1 会被拉伸成另一个的长度;
③ 若都不满足 → ValueError: operands could not be broadcast together点开:形状对齐的四种典型情况(重点看图)
从右往左逐维对齐,就像小学数学列竖式:
情况 A:末尾维度相同
(3, 4)
+ ( 4) ← 看作 (1, 4),第 0 维是 1 → 拉伸成 3
---------
(3, 4) ✅
情况 B:有一个维度是 1
(3, 4)
+ (1, 4) ← 第 0 维是 1 → 拉伸成 3
---------
(3, 4) ✅
情况 C:列向量 + 行向量
(3, 1)
+ ( 4) ← 看作 (1, 4);第 0 维 1→3,第 1 维 1→4
---------
(3, 4) ✅ 铺成一张 3×4 的表
情况 D:维度不够时前面补 1
(4,)
+ (3, 1)
---------
(3, 4) ✅
← (4,) 先补成 (1, 4),再按情况 C 处理失败的情况:
(3, 4)
+ (3, 5)
---------
第 1 维:4 vs 5,既不相等也没有 1
→ ValueError: operands could not be broadcast together with shapes (3,4) (3,5)实例:列向量 + 行向量 = 一张加法表
col = np.arange(3).reshape(3, 1) # [[0], [1], [2]]
row = np.arange(3) # [0, 1, 2]
print((col + row).shape) # (3, 3)
print(col + row)
# [[0 1 2]
# [1 2 3]
# [2 3 4]]直觉:col 的每个元素往上加 row 的每个元素,正好铺成一张 3×3 的表。
row: 0 1 2
col ┌─────────────────────┐
0 │ 0 1 2 │
1 │ 1 2 3 │
2 │ 2 3 4 │
└─────────────────────┘这个例子很重要,因为"给每一行减去它自己的均值"就是用这个模式:
# X 形状 (n, m),想给第 i 行减去 row_mean[i]
X - row_mean[:, np.newaxis] # row_mean 形状 (n,) → (n, 1) → 广播成 (n, m)21.3 axis 到底是什么意思
这是 NumPy 里最容易记反的一个参数。必须先搞清楚它,才能理解广播的实战用法。
X = np.arange(1, 13, dtype=float).reshape(3, 4)
# [[ 1. 2. 3. 4.]
# [ 5. 6. 7. 8.]
# [ 9. 10. 11. 12.]]
print(X.sum()) # 78.0 不指定 axis 就全加起来
print(X.sum(axis=0)) # [15. 18. 21. 24.] 形状 (4,)
print(X.sum(axis=1)) # [10. 26. 42.] 形状 (3,)记忆法:axis=k 表示"结果里第 k 维消失了"。
| 调用 | 结果形状 | 怎么理解 |
|---|---|---|
X.sum() | 标量 | 全部压平 |
X.sum(axis=0) | (4,) | 第 0 维(行)消失了,剩下列 |
X.sum(axis=1) | (3,) | 第 1 维(列)消失了,剩下行 |
验证方法(比背诵可靠 100 倍):
print(X.sum(axis=0).shape) # (4,) ← 看到 3 消失了,就知道压的是"行方向""沿行方向压扁"这句话是什么意思:把每一列竖着加起来。
axis=0(沿行方向压):
[ 1 2 3 4]
[ 5 6 7 8] 列方向相加
[ 9 10 11 12] ↓
─────────────
[15 18 21 24] ← 每一列的和,4 个数
axis=1(沿列方向压):
[ 1 2 3 4] → 行方向相加 → 10
[ 5 6 7 8] → 26
[ 9 10 11 12] → 42点开:为什么"axis=0 是列,axis=1 是行"这个说法会让人混乱?
因为存在两种说法,而且都在用:
| 说法 | 内容 |
|---|---|
| A | "axis=0 是列方向运算"(结果是每列一个数) |
| B | "axis=0 是沿行方向压扁"(行这个维度消失了) |
两句说的其实是同一件事,只是描述角度不同:
- A 说"结果是什么"(结果的每个元素对应一列)
- B 说"发生了什么"(行的维度被消掉了)
本书统一用 B 的说法("结果里第 k 维消失"),因为它可以验证:
X = np.zeros((2, 3, 4)) # 三维
print(X.sum(axis=1).shape) # (2, 4) ← 1 号维(3)消失了背这句话就够了:axis=k → 结果里第 k 维消失。
更保险的做法:不确定就 print(.shape)。这是本课程反复出现的纪律。
keepdims=True:保住维度,方便广播
print(X.mean(axis=0).shape) # (4,)
print(X.mean(axis=0, keepdims=True).shape) # (1, 4)
print((X - X.mean(axis=0)).shape) # (3, 4) 自动广播
print((X - X.mean(axis=0, keepdims=True)).shape) # (3, 4) 结果一样两者结果完全相同,但 keepdims 版本更显式:
# 不写 keepdims:(4,) 和 (3, 4) 广播 —— 需要读者自己推 (4,) 会怎么铺
X - X.mean(axis=0)
# 写 keepdims:(1, 4) 和 (3, 4) 广播 —— 一眼看出"1 行会被拉伸成 3 行"
X - X.mean(axis=0, keepdims=True)什么时候 keepdims 是必需的:当你想减的方向和默认广播方向不一致时。
# 给每一行减去它自己的均值(按行标准化)
row_mean = X.mean(axis=1, keepdims=True) # (3, 1)
print((X - row_mean).shape) # (3, 4) ✅ 每行减自己的均值
print((X - X.mean(axis=1)).shape) # ❌ 报错!最后一行为什么会报错:X.mean(axis=1) 得到的形状是 (3,), NumPy 会把它当成 (1, 3)(前面补 1),而 (3, 4) 和 (1, 3) 在第 1 维上是 4 vs 3,不兼容。
这正是 keepdims 存在的意义:把 (3,) 显式变成 (3, 1),广播才能按你想要的方向进行。
21.4 ★ 按列标准化:Z-score
这是本章的核心实战,也是本课程第一个真正的"机器学习"公式。
点开:为什么需要标准化?先看问题
假设你有一张特征表,三个特征:
X = np.array([
[3.70, 45.2, 6.1], # 电压(V)、内阻(mΩ)、温升(℃)
[3.68, 52.8, 9.4],
[3.65, 61.3, 13.7],
])注意三列的量纲差多远:
| 列 | 范围 | 数值大小 |
|---|---|---|
| 电压 | 3.65 ~ 3.70 | 个位数 |
| 内阻 | 45.2 ~ 61.3 | 几十 |
| 温升 | 6.1 ~ 13.7 | 十几 |
问题一:距离计算被大数值主导。
很多算法(KNN、K-means、SVM)靠"样本之间的距离"工作:
d = np.sqrt(((X[0] - X[1]) ** 2).sum())
# = sqrt(0.02² + 7.6² + 3.3²)
# = sqrt(0.0004 + 57.76 + 10.89)
# ≈ 8.28看出问题了吗:内阻一项贡献了 57.76,电压只贡献 0.0004。 电压这一列几乎等于"不存在" —— 但它是电池最直接的指标。
问题二:梯度下降被大数值特征带偏。
神经网络的梯度正比于输入值。内阻列(几十)会让它对应的权重梯度远大于电压列(个位数), 导致训练时"内阻方向走得很猛,电压方向几乎不动",收敛又慢又不稳。
解决方案:把所有列拉到同一个尺度。
公式与实现
Z-score 标准化:
= 该列的均值 = 该列的标准差
含义:把每个值变成"离均值有多少个标准差"。
X = np.array([
[3.70, 45.2, 6.1],
[3.68, 52.8, 9.4],
[3.65, 61.3, 13.7],
], dtype=float)
mu = X.mean(axis=0) # 每列均值,(3,)
sd = X.std(axis=0) # 每列标准差,(3,)
Xs = (X - mu) / sd # ★ 两行完成标准化
print(Xs)
# [[ 1.13555 -1.201298 -1.167664]
# [ 0.162221 -0.045619 -0.107125]
# [-1.297771 1.246917 1.274789]]验算第一行第一列:
(3.70 - 3.6767) / 0.0205 = 0.0233 / 0.0205 ≈ 1.1356 ✅验证结果
print(Xs.mean(axis=0).round(10)) # [-0. -0. 0.] 每列均值为 0
print(Xs.std(axis=0).round(10)) # [1. 1. 1.] 每列标准差为 1这就是标准化的定义性结果:标准化后每列均值 0、标准差 1。
为什么必须验证:这是数据预处理里最容易出错的一步 (比如轴搞错了、用了全局均值而不是每列均值)。两行代码就能确认:
assert np.allclose(Xs.mean(axis=0), 0), "标准化后均值不为 0"
assert np.allclose(Xs.std(axis=0), 1), "标准化后标准差不为 1"这就是 StandardScaler
# 手写版(本章)
Xs = (X - X.mean(axis=0)) / X.std(axis=0)
# sklearn 版
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
Xs = scaler.fit_transform(X)两者结果完全一致(误差在 1e-15 级别)。StandardScaler 内部就是这两行, 它额外做了三件事:
- 记住
mean_和scale_(保存模型时需要一起存); - 处理缺失值;
- 参数校验。
★ 最重要的纪律:fit_transform 里包含了"计算统计量"这一步。 统计量只能在训练集上算:
# ✅ 正确
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train) # 在训练集上算 mean/std
X_test_s = scaler.transform(X_test) # 用训练集的统计量转换测试集
# ❌ 错误(数据泄漏)
X_test_s = StandardScaler().fit_transform(X_test) # 测试集自己算了一套统计量为什么错:测试集应该模拟"模型没见过的新数据"。 如果让测试集参与统计量的计算,评估结果会偏乐观,上线后表现不达预期。
点开:除了 Z-score,还有哪些标准化方法?
| 方法 | 公式 | 结果范围 | 适用 |
|---|---|---|---|
| Z-score 标准化 | (x - μ) / σ | 均值 0、标准差 1(无固定范围) | 默认选择;近似正态的数据 |
| Min-Max 归一化 | (x - min) / (max - min) | [0, 1] | 需要固定范围(图像像素);有明确上下界 |
| Robust 标准化 | (x - median) / IQR | 类似 Z-score | 有异常值时更稳健 |
| MaxAbs 缩放 | x / max(abs(x)) | [-1, 1] | 稀疏数据(不破坏稀疏性) |
| L2 归一化 | x / ||x||₂ | 向量长度 1 | 文本向量、余弦相似度 |
怎么选:
有异常值吗? → 有 → Robust 标准化(用中位数和 IQR)
↓ 没有
数据近似正态吗? → 是 → Z-score
↓ 不是
有明确的物理上下界吗? → 有 → Min-Max
↓ 都不是
Z-score(最通用)什么时候不需要标准化:
- 树模型(决策树、随机森林、XGBoost/LightGBM)—— 它们按阈值切分,对尺度不敏感;
- 已经同量纲的特征(比如都是百分比)。
这是很实用的知识:如果你的模型是 XGBoost,跳过标准化能省一步、少一个出错的环节。
一个补充:Robust 标准化用的 IQR,正是第 33 章要讲的异常检测主角。 同一个统计量,既能用来找异常,也能用来做稳健缩放 —— 因为它们都是"抗异常值"的度量。
21.5 广播失败的排查
a = np.ones((3, 4))
b = np.ones((3, 5))
a + b
# ValueError: operands could not be broadcast together with shapes (3,4) (3,5)排查三步:
① 打印两个 .shape
print(a.shape, b.shape) # (3, 4) (3, 5)
② 从右往左逐维比较
第 1 维:4 vs 5 ← 既不相等、也没有 1 → 不兼容
③ 想清楚"我本来想让它们怎么对齐",然后 reshape 补维
b 想按行加 → b[:, :4] 或者 b 本来就该是 (3, 4)
a 想按列加 → a[:, np.newaxis] 把 (3,) 变成 (3, 1)最常见的三种"差一维":
# 情况一:想按行减均值,但 mean 是一维的
X - X.mean(axis=1) # ❌ (3,4) - (3,) → 不兼容
X - X.mean(axis=1, keepdims=True) # ✅ 变成 (3,1)
X - X.mean(axis=1)[:, np.newaxis] # ✅ 同上,另一种写法
# 情况二:一维数组想当列用
X / sd # ❌ 如果 sd 是每列的 (m,),X 是 (n, m)
# 哦这个其实可以 ✅((n,m) / (m,) 兼容)
X / sd[:, np.newaxis] # ❌ (n,m) / (m,1) 不兼容
# (m,1) 广播到 (n,m) 需要 m==n,通常不成立
# 情况三:把 (n,) 当 (n,1) 用
(a - b) ** 2 # 想算两两距离,但 (n,) - (n,) 是逐元素相减
a[:, np.newaxis] - b[np.newaxis, :] # ✅ (n,1) - (1,m) → (n,m) 才得到两两之差情况三是计算"两两距离矩阵"的标准开头:
# 1000 个样本两两之间的差,形状 (1000, 1000, m)
diff = X[:, np.newaxis, :] - X[np.newaxis, :, :]
dist = np.sqrt((diff ** 2).sum(axis=-1))注意内存:(1000, 1000, 3) 的 diff 是 300 万个 float64 = 24 MB, 还能接受。如果是 10 万个样本,就是 2.4 亿个数字 = 1.9 GB —— 这时要用第 20 章讲的 "展开成 ||a||² + ||b||² - 2ab"技巧,避免产生三维中间数组。
21.6 实战:给整批电芯算相对偏差
rng = np.random.default_rng(42)
caps = np.round(2.5 - 0.01 * rng.random((12, 5)) * np.arange(1, 6), 4)
# 12 支电芯 × 5 个循环
cell_mean = caps.mean(axis=1, keepdims=True) # (12, 1)
dev = caps - cell_mean # (12, 5)keepdims=True 在这里是必需的: caps.mean(axis=1) 得到 (12,),广播时会当成 (1, 12),和 (12, 5) 不兼容。 keepdims=True 得到 (12, 1),才能正确地"每行减自己的均值"。
验证:
print(dev.mean(axis=1).round(10))
# [ 0. -0. -0. 0. -0. 0. 0. 0. -0. 0. 0. 0.] ← 每支电芯的偏差均值为 0这个操作叫"组内去均值",它的意义是:消除每支电芯自身的水平差异,只看它的波动。
Pandas 里做同一件事(第 28 章):
df["soh_dev"] = df["soh"] - df.groupby("cell_id")["soh"].transform("mean")两者是同一个数学操作,只是 NumPy 版本用"二维数组 + axis", Pandas 版本用"长表 + groupby"。理解了 NumPy 版本,Pandas 版本就只是换了种表达。
这个操作的用途:
- 异常检测:偏离自己均值太远的点,可能是异常(比全局均值更准);
- 特征工程:去掉个体差异,让不同电芯可比;
- 去除批次效应:每组减自己的均值,消除系统性偏移。
21.7 完整示例与输出
cd code
python demos/ch21-broadcasting.py真实输出(节选):
=== ③ 列向量 + 行向量 = 一张表 ===
col.shape = (3, 1)
row.shape = (3,) [0 1 2]
col + row → shape (3, 3)
[[0 1 2]
[1 2 3]
[2 3 4]]
=== ④ axis 到底是什么意思(务必亲手验证) ===
X.sum(axis=0) = [15. 18. 21. 24.] ← 形状变成 (4,):沿行方向压扁,「剩下列」
X.sum(axis=1) = [10. 26. 42.] ← 形状变成 (3,):沿列方向压扁,「剩下行」
记忆法:axis=0 → 结果里的 0 号维消失了。
=== ⑤ 按列标准化:Z-score ===
每列均值 mu = [ 3.6767 53.1 9.7333]
每列标准差 sd = [0.0205 6.5762 3.1116]
注意:内阻的数值是几十,电压只有 3.x —— 直接算距离时内阻会「一家独大」。
校验:标准化后每列均值 = [-0. -0. 0.] ← 全 0 ✅
校验:标准化后每列标准差 = [1. 1. 1.] ← 全 1 ✅
这就是 sklearn 的 StandardScaler 干的事 —— 它内部就是这两行。
=== ⑥ 广播失败长什么样 ===
(3,4) + (3,5) → ValueError: operands could not be broadcast together with shapes (3,4) (3,5)
排查顺序:① 打印两个 .shape;② 从右往左比;③ 差一维就用 reshape/[:, np.newaxis] 补上。本章练习
练习 1:按列标准化
对 (5, 3) 的矩阵按列做 Z-score 标准化(每列减自己均值、除自己标准差),并验证每列均值 ≈ 0、标准差 ≈ 1。
参考答案
import numpy as np
X = np.arange(15, dtype=float).reshape(5, 3)
print(X.shape) # (5, 3)
# 逐列统计用 axis=0
mu = X.mean(axis=0)
sd = X.std(axis=0)
print(mu) # [6. 7. 8.]
print(sd.round(4)) # [4.2426 4.2426 4.2426]
# 一行完成标准化(广播把 (3,) 铺到 5 行上)
Xs = (X - mu) / sd
# 验证
print(Xs.mean(axis=0).round(10)) # [-0. -0. -0.]
print(Xs.std(axis=0).round(10)) # [1. 1. 1.]
assert np.allclose(Xs.mean(axis=0), 0)
assert np.allclose(Xs.std(axis=0), 1)为什么用 axis=0:
X 形状 (5, 3):5 个样本(行)、3 个特征(列)
X.mean(axis=0) → 第 0 维(行,长度 5)消失 → 结果 (3,)
含义:每个特征(列)自己的均值 ✅ 这才对
X.mean(axis=1) → 第 1 维(列,长度 3)消失 → 结果 (5,)
含义:每个样本(行)自己的均值 ❌ 不是我们要的为什么能广播:(5, 3) - (3,) 里 (3,) 会先补成 (1, 3), 第 0 维的 1 被拉伸成 5 —— 每个特征列的均值被减到所有 5 行上。
keepdims=True 的等价写法:
Xs2 = (X - X.mean(axis=0, keepdims=True)) / X.std(axis=0, keepdims=True)
print(np.allclose(Xs, Xs2)) # Truekeepdims=True 让 mu 和 sd 的形状变成 (1, 3),广播意图更明显 ("1 行会被拉伸成 5 行"),不需要读者自己推。
完整练习:exercises/ex21-01-broadcasting
练习 2:axis 到底是什么
import numpy as np
X = np.arange(1, 25).reshape(2, 3, 4) # 三维要求:不运行,先写出下面三个表达式的形状,再运行验证:
X.sum(axis=0).shape
X.sum(axis=1).shape
X.sum(axis=2).shape参考答案
(3, 4)
(2, 4)
(2, 3)规律:axis=k → 结果里第 k 维消失。
原始形状: (2, 3, 4)
↑ ↑ ↑
第0 第1 第2 维
axis=0 → 第 0 维消失 → (3, 4)
axis=1 → 第 1 维消失 → (2, 4)
axis=2 → 第 2 维消失 → (2, 3)如果还想保留那一维(变成 1),用 keepdims=True:
print(X.sum(axis=1, keepdims=True).shape) # (2, 1, 4)这个规律在高维张量上依然成立,这是它比"axis=0 是列方向"好用的地方。 在处理图像数据 (N, C, H, W) 时:
images.mean(axis=(0, 2, 3)) # 每个通道的均值 → 形状 (C,)
# 第 0、2、3 维消失,只剩第 1 维(通道)这是图像预处理里的标准操作:算整个数据集的 RGB 均值, 用于标准化(ImageNet 的 mean=[0.485, 0.456, 0.406] 就是这么来的)。
验证负索引也可以:axis=-1 表示最后一维 —— 在高维张量里比数"第几维"方便:
X.sum(axis=-1).shape # (2, 3) ← 最后一维(4)消失记住这一条就够了:axis=k 让第 k 维消失;axis=-1 是最后一维。 不确定就 print(.shape)。
练习 3:为什么这段代码报错?
import numpy as np
X = np.arange(12, dtype=float).reshape(3, 4)
# 想给每一行减去它自己的均值
row_mean = X.mean(axis=1)
print(row_mean.shape) # (3,)
result = X - row_mean # ← 这里报错要求:说出报错内容、原因,以及两种修法。
参考答案
报错:
ValueError: operands could not be broadcast together with shapes (3,4) (3,)原因:(3,) 在广播时会在前面补 1 变成 (1, 3):
(3, 4)
- (1, 3) ← (3,) 补成 (1, 3)
---------
第 1 维:4 vs 3 ← 既不相等、也没有 1 → 不兼容关键认知:一维数组 (3,) 广播时永远是"当作一行",不是当一列。 但我们要减的是"每一行自己的均值",需要一个列向量 (3, 1)。
修法一:keepdims=True(推荐,最清晰)
row_mean = X.mean(axis=1, keepdims=True) # (3, 1)
result = X - row_mean
print(result.shape) # (3, 4)修法二:np.newaxis
row_mean = X.mean(axis=1)[:, np.newaxis] # (3,) → (3, 1)
result = X - row_mean
print(result.shape) # (3, 4)两种都行。本书推荐 keepdims=True,因为它在计算均值的那一行就把形状写对了, 不需要读者额外注意后面那个 [:, np.newaxis]。
验证结果:
print(result.mean(axis=1).round(10)) # [-0. -0. -0.] ← 每行均值为 0 ✅"按行"和"按列"的对照表(值得记下来):
| 目标 | 统计量写法 | 形状 |
|---|---|---|
| 给每列减自己的均值 | X.mean(axis=0) | (m,) 或 (1, m) |
| 给每行减自己的均值 | X.mean(axis=1, keepdims=True) | (n, 1) |
规律:
- 按列操作 → 算
axis=0(列消失了)→ 结果自带(1, m)的形状,能直接广播; - 按行操作 → 算
axis=1(行消失了)→ 需要keepdims=True把它变回(n, 1)。
这个区别在标准化、去均值、组内归一化里每天都会遇到。
面向 AI 开发:广播是"零拷贝的特征对齐"
广播在 AI 里的地位,可以用一句话概括:
它是"让不同形状的东西能算在一起"的通用机制,而且不花内存。
四个高频场景:
① 批量归一化(BatchNorm)。
# x 形状 (N, C, H, W),要按通道归一化
mean = x.mean(axis=(0, 2, 3), keepdims=True) # (1, C, 1, 1)
std = x.std(axis=(0, 2, 3), keepdims=True) # (1, C, 1, 1)
x_norm = (x - mean) / std # 广播到 (N, C, H, W)keepdims 在这里是关键:(1, C, 1, 1) 才能正确广播到 (N, C, H, W)。 去掉它得到 (C,),会错误地广播到最后一维。
这也是为什么 BatchNorm 层的参数是一个长度为 C 的向量 —— 每个通道一组 gamma/beta,靠广播作用到所有空间位置。
② 位置编码(Transformer)。
# pos 形状 (T, 1),div 形状 (1, d_model)
# 广播出 (T, d_model) 的位置编码矩阵
pe = pos / div③ 掩码(Masking)。
# attention 分数 (batch, heads, T, T)
# mask 形状 (T, T),广播到所有 batch 和 head
scores = scores.masked_fill(mask == 0, -1e9)这是广播"不复制内存"价值的极致体现:一个 (T, T) 的掩码 应用到 (32, 8, T, T) 的张量上,不需要复制 32×8 份。
④ 每样本/每组的归一化。
# 每个样本减自己的均值(Instance Norm、Layer Norm 的基础)
x = x - x.mean(axis=-1, keepdims=True)为什么 AI 里到处是归一化:深层网络里,每层输出经过激活函数后, 数值分布会漂移(内部协变量偏移)。归一化把它拉回稳定区间, 训练才能用较大的学习率、收敛更快。
而所有归一化方法的实现差别,本质就是"沿哪些 axis 求统计量":
| 方法 | 统计量的 axis | 含义 |
|---|---|---|
| Batch Norm | (0, 2, 3) | 跨 batch 和空间,每个通道一组 |
| Layer Norm | (-1) | 每个样本的特征维 |
| Instance Norm | (2, 3) | 每个样本每个通道 |
| Group Norm | 通道分组 | 折中方案 |
理解了本章的 axis 和 keepdims,这四种归一化就变成了"改一个参数"的区别。
一条实用建议:写归一化代码时永远用 keepdims=True。 它让形状显式写在代码里,别人(和三个月后的你)不用在脑子里推广播规则。