Skip to content

第 21 章 · 广播机制:不同形状怎么一起算 ​

这一章要解决什么:让形状不同的数组能一起运算,并写出按列标准化的代码。 这一章藏着一个惊喜:Z-score 标准化的公式,就是 sklearn 的 StandardScaler 的全部内容。

本章你需要完成的代码:demos/ch21-broadcasting.py

21.1 从最简单的"广播"说起 ​

python
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)

实例:列向量 + 行向量 = 一张加法表 ​

python
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       │
    └─────────────────────┘

这个例子很重要,因为"给每一行减去它自己的均值"就是用这个模式:

python
# X 形状 (n, m),想给第 i 行减去 row_mean[i]
X - row_mean[:, np.newaxis]          # row_mean 形状 (n,) → (n, 1) → 广播成 (n, m)

21.3 axis 到底是什么意思 ​

这是 NumPy 里最容易记反的一个参数。必须先搞清楚它,才能理解广播的实战用法。

python
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 倍):

python
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 维消失"),因为它可以验证:

python
X = np.zeros((2, 3, 4))          # 三维
print(X.sum(axis=1).shape)       # (2, 4)   ← 1 号维(3)消失了

背这句话就够了:axis=k → 结果里第 k 维消失。

更保险的做法:不确定就 print(.shape)。这是本课程反复出现的纪律。

keepdims=True:保住维度,方便广播 ​

python
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 版本更显式:

python
# 不写 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 是必需的:当你想减的方向和默认广播方向不一致时。

python
# 给每一行减去它自己的均值(按行标准化)
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 ​

这是本章的核心实战,也是本课程第一个真正的"机器学习"公式。

点开:为什么需要标准化?先看问题

假设你有一张特征表,三个特征:

python
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)靠"样本之间的距离"工作:

python
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 标准化:

z=x−μσ
  • μ = 该列的均值
  • σ = 该列的标准差

含义:把每个值变成"离均值有多少个标准差"。

python
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   ✅

验证结果 ​

python
print(Xs.mean(axis=0).round(10))     # [-0. -0.  0.]    每列均值为 0
print(Xs.std(axis=0).round(10))      # [1. 1. 1.]       每列标准差为 1

这就是标准化的定义性结果:标准化后每列均值 0、标准差 1。

为什么必须验证:这是数据预处理里最容易出错的一步 (比如轴搞错了、用了全局均值而不是每列均值)。两行代码就能确认:

python
assert np.allclose(Xs.mean(axis=0), 0), "标准化后均值不为 0"
assert np.allclose(Xs.std(axis=0), 1), "标准化后标准差不为 1"

这就是 StandardScaler ​

python
# 手写版(本章)
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 内部就是这两行, 它额外做了三件事:

  1. 记住 mean_ 和 scale_(保存模型时需要一起存);
  2. 处理缺失值;
  3. 参数校验。

★ 最重要的纪律:fit_transform 里包含了"计算统计量"这一步。 统计量只能在训练集上算:

python
# ✅ 正确
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 广播失败的排查 ​

python
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)

最常见的三种"差一维":

python
# 情况一:想按行减均值,但 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) 才得到两两之差

情况三是计算"两两距离矩阵"的标准开头:

python
# 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 实战:给整批电芯算相对偏差 ​

python
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),才能正确地"每行减自己的均值"。

验证:

python
print(dev.mean(axis=1).round(10))
# [ 0. -0. -0.  0. -0.  0.  0.  0. -0.  0.  0.  0.]   ← 每支电芯的偏差均值为 0

这个操作叫"组内去均值",它的意义是:消除每支电芯自身的水平差异,只看它的波动。

Pandas 里做同一件事(第 28 章):

python
df["soh_dev"] = df["soh"] - df.groupby("cell_id")["soh"].transform("mean")

两者是同一个数学操作,只是 NumPy 版本用"二维数组 + axis", Pandas 版本用"长表 + groupby"。理解了 NumPy 版本,Pandas 版本就只是换了种表达。

这个操作的用途:

  • 异常检测:偏离自己均值太远的点,可能是异常(比全局均值更准);
  • 特征工程:去掉个体差异,让不同电芯可比;
  • 去除批次效应:每组减自己的均值,消除系统性偏移。

21.7 完整示例与输出 ​

bash
cd code
python demos/ch21-broadcasting.py

真实输出(节选):

text
=== ③ 列向量 + 行向量 = 一张表 ===
  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。

参考答案
python
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 的等价写法:

python
Xs2 = (X - X.mean(axis=0, keepdims=True)) / X.std(axis=0, keepdims=True)
print(np.allclose(Xs, Xs2))     # True

keepdims=True 让 mu 和 sd 的形状变成 (1, 3),广播意图更明显 ("1 行会被拉伸成 5 行"),不需要读者自己推。

完整练习:exercises/ex21-01-broadcasting

练习 2:axis 到底是什么
python
import numpy as np
X = np.arange(1, 25).reshape(2, 3, 4)     # 三维

要求:不运行,先写出下面三个表达式的形状,再运行验证:

python
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:

python
print(X.sum(axis=1, keepdims=True).shape)    # (2, 1, 4)

这个规律在高维张量上依然成立,这是它比"axis=0 是列方向"好用的地方。 在处理图像数据 (N, C, H, W) 时:

python
images.mean(axis=(0, 2, 3))          # 每个通道的均值 → 形状 (C,)
                                      # 第 0、2、3 维消失,只剩第 1 维(通道)

这是图像预处理里的标准操作:算整个数据集的 RGB 均值, 用于标准化(ImageNet 的 mean=[0.485, 0.456, 0.406] 就是这么来的)。

验证负索引也可以:axis=-1 表示最后一维 —— 在高维张量里比数"第几维"方便:

python
X.sum(axis=-1).shape     # (2, 3)   ← 最后一维(4)消失

记住这一条就够了:axis=k 让第 k 维消失;axis=-1 是最后一维。 不确定就 print(.shape)。

练习 3:为什么这段代码报错?
python
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(推荐,最清晰)

python
row_mean = X.mean(axis=1, keepdims=True)     # (3, 1)
result = X - row_mean
print(result.shape)                          # (3, 4)

修法二:np.newaxis

python
row_mean = X.mean(axis=1)[:, np.newaxis]     # (3,) → (3, 1)
result = X - row_mean
print(result.shape)                          # (3, 4)

两种都行。本书推荐 keepdims=True,因为它在计算均值的那一行就把形状写对了, 不需要读者额外注意后面那个 [:, np.newaxis]。

验证结果:

python
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)。

python
# 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)。

python
# pos 形状 (T, 1),div 形状 (1, d_model)
# 广播出 (T, d_model) 的位置编码矩阵
pe = pos / div

③ 掩码(Masking)。

python
# 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 份。

④ 每样本/每组的归一化。

python
# 每个样本减自己的均值(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。 它让形状显式写在代码里,别人(和三个月后的你)不用在脑子里推广播规则。


下一章:第 22 章 · 聚合与布尔索引:读真实电池文件做统计