一、范数惩罚
当模型参数过多、网络太深,而训练数据量相对不足时,模型极易产生过拟合(Overfitting) 。此时模型会过度拟合训练集里的每一个噪声和细节,导致权重矩阵 www 的数值变得非常大或极端剧烈,从而丧失了泛化能力。
在机器学习和深度学习中,范数惩罚(Norm Penalty)就是我们常说的 L1 / L2 正则化 。它的核心思想是通过在原本的损失函数(Loss)后面加上一个对模型权重 www 的"惩罚项",以此来约束模型的复杂度,达到防止过拟合的目的。
根据惩罚方式的不同,最经典的分为 L1 范数 和 L2 范数:
3. L1 范数与 L2 范数深度对比
| 对比维度 | L1 范数(Lasso 回归) | L2 范数(Ridge 岭回归 / Weight Decay) |
|---|---|---|
| 数学几何形态 | 绝对值之和,几何边界呈现带尖角的菱形。 | 平方和开根号,几何边界呈现平滑的圆形。 |
| 对权重的作用 | 稀疏化:强行将大量不重要的权重直接抹为 0。 | 平滑化:将权重整体压低、缩小**,使其趋近于 0 但绝不等于 0。 |
| 核心效果 | 能够自动做特征选择,筛选出真正起作用的特征。 | 让每个特征都分担一点权重,使模型对输入数据的剧烈波动不敏感。 |
| 反向传播梯度 | 无论 www 多大,导数绝对值恒为 1,小权重会被一刀切地抹零。 | 导数与 www 自身大小成正比(梯度为 www),权重越大惩罚越重,越小惩罚越微弱。 |
补充:如何区分L1正则和L2正则
L1 正则是 1 ----> 联想到绝对值惩罚
L2 正则中有 2 -----> 对应惩罚项上的平方
在 PyTorch 中怎么用?
L2 正则化在 PyTorch 中不需要写出公式 :它已经被高度集成到了优化器(Optimizer)里,叫做 weight_decay(权重衰减)。
python
# 内部数学等价于在损失函数后面加了 L2 范数惩罚
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)
L1 正则化在 PyTorch 中需要手动写:因为 L1 会产生稀疏矩阵,并不适合所有网络层(如全连接或卷积),所以需要手动累加损失:
python
l1_loss = 0
for param in model.parameters():
l1_loss += torch.sum(torch.abs(param))
loss = criterion(output, target) + alpha * l1_loss
二、Dropout(随机失活)
在模型参数较多,数据量不足的情况下,模型很容易过拟合。Dropout 是在训练过程中,让神经元以超参数 p 的概率下停止工作或者被置为零,未被置为0的进行缩放,放大比例为1/(1-p)。由于不被激活的神经元被缩放,保证了训练和测试的一致。
Dropout 主要是让 w(权重 )随机失去活性。Dropout 的目的是 防止权重 www 之间产生"相互依赖" ------不让某几个权重形成小团体包揽所有活,而是逼着所有权重 www 都独立发展出强大的特征提取能力。
在代码结构上,Dropout 作为一个"特殊的网络层",通常放在隐藏层的激活函数之后,下一层计算之前。同时,为了实现可复现性,可以设置随机种子 torch.manual_seed(42)
环境模拟:
数据输入:一条数据,四个特征
网络设计:仅设计一个全连接层,后面有五个隐藏层(输出),一个激活层,一个Dropout
输入层 ---> 全连接层 ---> 激活层 ---> Dropout层 ---> 输出层
激活函数:使用 sigmoid 作为激活函数
代码实现:
python
import torch
import torch.nn as nn
# 设置固定种子,保证复现性
torch.manual_seed(42)
# 设置超参数 Dropout 激活概率
P = 0.5
# 1. 数据加载,这里采用固定数据输入,传入一条数据,四个特征
data = torch.tensor([1, 3, 5, 7], dtype=torch.float32)
print(f"输入的数据为{data},形状为{data.shape}")
# 2. 创建全连接层, 4个输入,7个隐藏神经元,进行加权求和
linear_layer = nn.Linear(4, 7) linear_output = linear_layer(data) print(f"加权求和的数值为{linear_output}")
# 3. 使用激活函数,计算激活值
linear_sigmoid = torch.sigmoid(linear_output) print(f"激活值为{linear_sigmoid}")
# 4. 创建随机失活层(设置失活概率 p),对激活值进行dropout处理(只针对训练阶段)
dropout = nn.Dropout(p=P)
dropout.train() # 开启训练模式,否则 Dropout 不会生效
drop = dropout(linear_sigmoid)
print(f"展示随机失活神经元为{drop}")
# 5. 原理验证
print('原理验证', linear_sigmoid * 1/(1-p)) # 【修正】将未定义的 h 改为你的激活值 linear_sigmoid
================输出===========================
输入的数据为tensor([1., 3., 5., 7.]),形状为torch.Size([4])
加权求和的数值为tensor([ 4.3039, 0.5376, 2.6199, 0.8591, 0.7182, 0.9833, -4.0589],
grad_fn=<ViewBackward0>)
激活值为tensor([0.9867, 0.6313, 0.9321, 0.7025, 0.6722, 0.7278, 0.0170],
grad_fn=<SigmoidBackward0>)
展示随机失活神经元为tensor([1.9733, 1.2625, 0.0000, 1.4050, 0.0000, 0.0000, 0.0339],
grad_fn=<MulBackward0>)
原理验证 tensor([1.9733, 1.2625, 1.8643, 1.4050, 1.3444, 1.4555, 0.0339],
grad_fn=<DivBackward0>)
代码补充:
- dropout.train() 确保了Dropout 激活生效
- drop 中结果会不一致,因为概率的不同
- 原理验证中,不失活的神经元和激活值是一致的
PyTorch 为什么要对未失活神经元乘以 11−p\frac{1}{1-p}1−p1?
这种机制是现代深度学习框架标准的 Inverted Dropout(反向随机失活) 机制。
它的核心目的,是为了在模型'训练'和'推理'之间,维持统计学上的期望守恒(能量守恒)。
- 传统的 Vanilla Dropout 做法很死板 :它在训练时把部分神经元抹零,导致能量缩水;等到了推理测试时 (
model.eval()),全员复活,它又必须在代码里把整层输出集体乘以 1−p1-p1−p 来压低能量。这严重污染了推理阶段,导致算法和下游的部署工程严重耦合。 - 而 PyTorch 颠覆了这一逻辑 :它的哲学是把工程代价全压在训练期,换取推理期极致的纯粹与高效 。既然训练时有概率为 ppp 的人'死去了',那剩下 (1−p)(1-p)(1−p) 活着的人就必须在训练时原地打鸡血,强行把业绩背起来 。所以 PyTorch 训练时直接对未抹零的神经元乘以 11−p\frac{1}{1-p}1−p1(比如 p=0.5p=0.5p=0.5 时,存活的数值直接放大 222 倍)。
这样在数学上,训练期的输出期望就是:存活率(1−p)×放大倍数11−p=1\text{存活率} (1-p) \times \text{放大倍数} \frac{1}{1-p} = 1存活率(1−p)×放大倍数1−p1=1。
带来的好处是:当线上推理调用 model.eval() 时,Dropout 直接关闭,网络不需要做任何多余的乘法缩放,输入是多少,输出期望就是多少。这保证了推理的高性能,实现了算法与跨平台部署的完美解耦。
三、批量归一化
批量归一化是为了解决内部协变量偏移问题,加速模型收敛,先对数据标准化,再对数据重构(缩放+平移),即,最后的输出是标准化的数值之后加上缩放和平移。
批量归一化是针对网络中间隐藏神经元的输出,功能类似实现了标准化和归一化的操作,让数值收敛在一个范围内。
归一化的生效位置是在:全连接层/卷积层计算之后,激活函数(如 Relu/Sigmoid)之前。
批量归一化层在计算机视觉领域使用较多。
因此针对不同的维度使用不同的批量归一化函数
python
BatchNorm1d:处理一维样本(散点特征数据),它接收形状为(N, num_features=C)的张量作为输入
BatchNorm2d:处理二维样本(图片或特征图),它接收形状为(N, C, H, W)的张量作为输入。
BatchNorm3d:处理三维样本(视频或图片组),它接收形状为(N, C, D, H, W)的张量作为输入。
- C 代表有几个特征通道,H代表高,W代表宽,N代表一个批次有多少样本,D代表特征通道有多长(深度)
环境模拟:
输入两个样本,每个样本四个特征(一维)
网络架构:设计一个全连接层,输出为五个隐藏层,一个BN层进行数据标准化处理,一个激活层
输入层 ---> 全连接层 ---> 隐藏层 ---> BN层 ---> 激活层 ---> 输出层
使用 relu 作为激活函数
代码实现:
python
# 导包
import torch
import torch.nn as nn
# 1. 创建散点特征样本(最少需要2个样本,4个特征)
data = torch.randn(size=(2, 4)) # 一维批处理,必须要导入大于两个的样本
print(f"数据展示{data}")
# 2. 创建全连接层,处理输入数据(加权求和),输出为5个
linear = nn.Linear(4, 5)
linear_output = linear(data) print(f'加权平均后数据{linear_output}')
print(f'加权平均后数据均值{linear_output.mean()}')
print(f'加权平均后数据标准差{linear_output.std()}')
# 3. 创建BN层
bn = nn.BatchNorm1d(num_features=5) # num_features 相当是【通道】C
bn.train() # 让 BN 层处于训练状态,否则动态均值无法生效
# 对线性层的结果进行标准化处理
bn_output = bn(linear_output) print(f'bn层数据{bn_output}')
# 4. 进行激活处理
out_relu = torch.relu(bn_output) print(f'激活后数据{out_relu}')
===================输出=======================
数据展示tensor([[-0.6441, -0.6061, -0.1425, 0.9727],
[ 2.0038, 0.6622, 0.5332, 2.7489]])
加权平均后数据tensor([[ 0.0178, -0.3218, -0.1438, 0.1689, -0.4014],
[ 1.0908, -1.8289, -0.1211, -2.2368, -0.2730]],
grad_fn=<AddmmBackward0>)
加权平均后数据均值-0.4049370288848877
加权平均后数据标准差0.9604332447052002
bn层数据tensor([[-1.0000, 1.0000, -0.9630, 1.0000, -0.9988],
[ 1.0000, -1.0000, 0.9630, -1.0000, 0.9988]],
grad_fn=<NativeBatchNormBackward0>)
激活后数据tensor([[0.0000, 1.0000, 0.0000, 1.0000, 0.0000],
[1.0000, 0.0000, 0.9630, 0.0000, 0.9988]], grad_fn=<ReluBackward0>)
代码补充:
- 对于批量归一化,传入数据最少是两个
批量归一化可以和Dropout 一块使用吗?
可以一块用,但不能盲目乱加。
在数学原理上,Dropout 的随机性会干扰 BN 对当前批次均值和方差的稳定计算,从而引发特征方差偏移(Variance Shift),导致模型性能下降。
为了解决这个冲突,我在工程实践中会采取两种策略:
一是严格控制拓扑顺序 ,坚持'先做 BN 格式化,后做 Dropout 随机失活 ';
二是分工隔离,在前面的特征提取层主要靠 BN 稳定数据并加速收敛,而在网络末端的高维全连接层,专门引入 Dropout 来抑制过拟合。这样就能让两尊大佛各司其职,发挥出 1+1 > 2 的效果。
为什么 BN 层在标准化之后,还要做仿射变换(加 γ\gammaγ 和 β\betaβ)?
标准化的目的是为了稳定数据分布,避免梯度消失和爆炸 ;
而加上缩放 γ\gammaγ 和 平移 β\betaβ 的目的是为了恢复网络的非线性表达能力 。它通过两个可学习的参数,允许网络在'享受数据稳定性'的同时,能够自适应地将数据分布调整到激活函数最合理的表达区间,防止多层网络退化为线性模型。 这本质上是一种先破后立、张弛有度的数学平衡。