← 返回笔记列表
深度学习 第 2 / 49 篇

深度学习:归一化与标准化— 为什么、怎么做、什么时候做

查看“深度学习”专题目录(2/49)
本文目录 6 个小节

Normalization

问题的起点:一个不收敛的例子

数据与真实规律

外卖送餐时间预测,两个特征:

time=2×lights+0.01×distance+5\text{time} = 2 \times \text{lights} + 0.01 \times \text{distance} + 5
timelightsdistance
1921000
3132000
142500
151800
4343000

数据严格按照上述线性方程生成,没有噪声——理论上 loss 应该能降到接近 0。

梯度下降训练:

import torch

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
inputs = torch.tensor([[2, 1000], [3, 2000], [2, 500], [1, 800], [4, 3000]], dtype=torch.float, device=device)
labels = torch.tensor([[19], [31], [14], [15], [43]], dtype=torch.float, device=device)


w = torch.ones(2, 1, requires_grad=True, device=device)
b = torch.ones(1, requires_grad=True, device=device)

epoch = 200
lr = 0.0000001

for i in range(epoch):
    outputs = inputs @ w + b
    loss = torch.mean(torch.square(outputs - labels))
    print("loss", loss.item())
    loss.backward()
    print("w.grad", w.grad.tolist())
    with torch.no_grad():
        w -= w.grad * lr
        b -= b.grad * lr

    w.grad.zero_()
    b.grad.zero_()

现象:

  • 学习率稍大一点,loss 立刻爆炸,直接超出 float 表示范围
  • 即使设得很小,loss 降到 7 左右就下不去了,无法接近 0

根因分析:梯度尺度不一致

看第一次迭代的梯度

loss 2898583.75
w.grad [[8600.0], [5876040.0]]
  • lights 权重的梯度:8,600
  • distance 权重的梯度:5,876,040
  • distance 的梯度大约是 lights 的 ~680 倍

为什么差这么多?

两个特征的数值范围完全不同:

特征取值范围权重目标对loss的影响
lights1~4w0→2w_0 \rightarrow 2较小
distance500~3000w1→0.01w_1 \rightarrow 0.01非常大

distance 的值大约是 lights 的 1000 倍。同一个权重变化量,作用在 distance 上对 loss 的扰动,是作用在 lights 上的 1000 倍。所以 loss 对 w1w_1 的偏导数天然就大了两个数量级。

后果:共享学习率的困境

w1:=w1−lr⋅gradw1w_1 := w_1 - lr \cdot \text{grad}_{w1}

  • w1w_1 的梯度非常大,学习率必须极小,否则一步就跨过目标值 0.01,直接发散
  • 为了迁就 w1w_1,学习率被迫设得很小
  • 但 w0w_0 的梯度本来就小,学习率再这么小,w0w_0 几乎不动
  • 结果:w1w_1 勉强收敛,w0w_0 还没调到目标,loss 降不下去

本质原因:不同特征的取值范围不同 → 对应权重的梯度量级不同 → 共用一个学习率时无法同时兼顾。


解决方案一:按最大值缩放与 Min–Max Scaling

思路:

让所有特征的取值范围相同(都缩放到 [0, 1] 或 [-1, 1]),这样所有权重的梯度就在同一量级,可以用统一的学习率。

bias 的”输入”永远是 1,所以把其他特征也缩放到 1 附近即可。

实现:

下面的代码是按最大值缩放。对于本例的非负数据,它会将值压到不超过 1 的范围,但一般不等于 Min–Max Scaling。真正的 Min–Max 公式为 x′=(x−xmin)/(xmax−xmin)x'= (x-x_{min})/(x_{max}-x_{min}),常数特征需单独处理;含负值时不能只靠除最大值保证落在 [0,1][0,1]。

inputs = inputs / torch.tensor([4, 3000], device=device)
# lights:   [2, 3, 2, 1, 4] / 4  → [0.5, 0.75, 0.5, 0.25, 1.0]
# distance: [1000, 2000, 500, 800, 3000] / 3000 → [0.33, 0.67, 0.17, 0.27, 1.0]
import torch

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
inputs = torch.tensor([[2, 1000], [3, 2000], [2, 500], [1, 800], [4, 3000]], dtype=torch.float, device=device)
labels = torch.tensor([[19], [31], [14], [15], [43]], dtype=torch.float, device=device)

#进行归一化
inputs = inputs / torch.tensor([4, 3000], device=device)


w = torch.ones(2, 1, requires_grad=True, device=device)
b = torch.ones(1, requires_grad=True, device=device)

epoch = 1000
lr = 0.5

for i in range(epoch):
    outputs = inputs @ w + b
    loss = torch.mean(torch.square(outputs - labels))
    print("loss", loss.item())
    loss.backward()
    print("w.grad", w.grad.tolist())
    with torch.no_grad():
        w -= w.grad * lr
        b -= b.grad * lr

    w.grad.zero_()
    b.grad.zero_()

效果:

归一化后,学习率可以从 0.0000001 直接拉到 0.5:

lr = 0.5      # 原来设 0.5 直接爆炸,现在正常训练
epoch = 1000  # loss 很快接近 0

训练变得稳定且快速。

缺点:

只用一个最大值决定缩放比例。如果这个最大值是异常值(outlier),整个缩放就会被带偏。

解决方案二:标准化(Standardization / Z-Score)

思路:

对每个特征,减去自己的均值,除以自己的标准差,使其变成均值为 0、标准差为 1 的分布。

x′=x−μσx' = \frac{x - \mu}{\sigma}

实现:

mean = inputs.mean(dim=0)   # 每个特征的均值
std  = inputs.std(dim=0)    # 每个特征的标准差
inputs_norm = (inputs - mean) / std

对比:

归一化(Min-Max)标准化(Z-Score)
被谁决定仅由最大值、最小值决定由所有样本的均值和标准差决定
对异常值的敏感性敏感(一个异常值毁全局)同样敏感,均值和标准差均受异常值影响
结果范围[0, 1][0,\ 1]均值 0,标准差 1(理论上无界)

标准化也会受异常值影响;需要稳健缩放时可考虑以中位数和四分位距为基础的 RobustScaler。缩放方法应结合变量和模型选择,树模型通常不需要像距离模型那样统一尺度。

效果:

lr = 0.5       # 同样可以用大学习率
epoch = 1000   # loss 同样接近 0

注:

预测时也要归一化参数!

训练时你对数据做了标准化——记录了 mean 和 std。预测新数据时,必须用同样的 mean 和 std 对新输入做变换,否则模型看到的数值尺度完全不对,预测结果毫无意义。

归一化参数是训练数据的”属性”,预测时直接用,不要拿新数据重新算。

归一化改变了数据,为什么不影响模型?

归一化只是对数据做了一次可逆的线性变换,不改变数据之间的本质关系。模型可以通过调整权重来”补偿”这个变换——就像把单位从米换成千米,物理规律不变,只是数值变了。

y=w⋅x+b=(w⋅k)⋅(xk)+by = w \cdot x + b = (w \cdot k) \cdot \left(\frac{x}{k}\right) + b

总结

问题:特征取值范围不同 → 梯度量级不同 → 共享 lr 无法同时收敛

方案:把所有特征缩放到同一尺度(标准化 / 归一化)

关键:预测时用训练时的参数做同样的缩放

结论:按模型和变量选择缩放,统计量只从训练集估计,常数列避免除零。

参考资料

继续学习本专题