神经网络正则化:防止过拟合的七种核心手段

一、范数惩罚

当模型参数过多、网络太深,而训练数据量相对不足时,模型极易产生过拟合(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(权重衰减)

# 内部数学等价于在损失函数后面加了 L2 范数惩罚optimizer=torch.optim.SGD(model.parameters(),lr=0.01,weight_decay=1e-4)

L1 正则化在 PyTorch 中需要手动写:因为 L1 会产生稀疏矩阵,并不适合所有网络层(如全连接或卷积),所以需要手动累加损失:

l1_loss=0forparaminmodel.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 作为激活函数
代码实现:

importtorchimporttorch.nnasnn# 设置固定种子,保证复现性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}1p1

这种机制是现代深度学习框架标准的Inverted Dropout(反向随机失活)机制。
它的核心目的,是为了在模型‘训练’和‘推理’之间,维持统计学上的期望守恒(能量守恒)

  • 传统的 Vanilla Dropout 做法很死板:它在训练时把部分神经元抹零,导致能量缩水;等到了推理测试时model.eval()),全员复活,它又必须在代码里把整层输出集体乘以1−p1-p1p来压低能量。这严重污染了推理阶段,导致算法和下游的部署工程严重耦合。
  • 而 PyTorch 颠覆了这一逻辑:它的哲学是把工程代价全压在训练期,换取推理期极致的纯粹与高效。既然训练时有概率为ppp的人‘死去了’,那剩下(1−p)(1-p)(1p)活着的人就必须在训练时原地打鸡血,强行把业绩背起来。所以 PyTorch 训练时直接对未抹零的神经元乘以11−p\frac{1}{1-p}1p1(比如p=0.5p=0.5p=0.5时,存活的数值直接放大222倍)。

这样在数学上,训练期的输出期望就是:存活率(1−p)×放大倍数11−p=1\text{存活率} (1-p) \times \text{放大倍数} \frac{1}{1-p} = 1存活率(1p)×放大倍数1p1=1

带来的好处是:当线上推理调用model.eval()时,Dropout 直接关闭,网络不需要做任何多余的乘法缩放,输入是多少,输出期望就是多少。这保证了推理的高性能,实现了算法与跨平台部署的完美解耦。

三、批量归一化

批量归一化是为了解决内部协变量偏移问题,加速模型收敛,先对数据标准化,再对数据重构(缩放+平移),即,最后的输出是标准化的数值之后加上缩放和平移。

批量归一化是针对网络中间隐藏神经元的输出,功能类似实现了标准化和归一化的操作,让数值收敛在一个范围内。

归一化的生效位置是在:全连接层/卷积层计算之后,激活函数(如 Relu/Sigmoid)之前

批量归一化层在计算机视觉领域使用较多。
因此针对不同的维度使用不同的批量归一化函数

BatchNorm1d:处理一维样本(散点特征数据),它接收形状为(N,num_features=C)的张量作为输入 BatchNorm2d:处理二维样本(图片或特征图),它接收形状为(N,C,H,W)的张量作为输入。 BatchNorm3d:处理三维样本(视频或图片组),它接收形状为(N,C,D,H,W)的张量作为输入。
  • C 代表有几个特征通道,H代表高,W代表宽,N代表一个批次有多少样本,D代表特征通道有多长(深度)

环境模拟:
输入两个样本,每个样本四个特征(一维)
网络架构:设计一个全连接层,输出为五个隐藏层,一个BN层进行数据标准化处理,一个激活层
输入层 —> 全连接层 —> 隐藏层 —> BN层 —> 激活层 —> 输出层
使用 relu 作为激活函数
代码实现:

# 导包importtorchimporttorch.nnasnn# 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 相当是【通道】Cbn.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.9604332447052002bn层数据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β的目的是为了恢复网络的非线性表达能力。它通过两个可学习的参数,允许网络在‘享受数据稳定性’的同时,能够自适应地将数据分布调整到激活函数最合理的表达区间,防止多层网络退化为线性模型。 这本质上是一种先破后立、张弛有度的数学平衡。