过拟合问题#
什么是过拟合?#
过拟合(Overfitting) 是指模型在训练集上表现很好,但在测试集上表现较差的现象。
过拟合的原因#
- 模型过于复杂:参数过多,容量过大
- 训练数据不足:数据量小于模型容量
- 训练时间过长:过度拟合训练数据
如何识别过拟合?#
训练损失 ↓↓↓ 但 验证损失 ↑↑↑指标:
- 训练准确率 » 验证准确率
- 训练损失 « 验证损失
解决方案#
- 增加数据:数据增强、收集更多数据
- 简化模型:减少参数、降低复杂度
- 正则化:限制模型复杂度
- 早停:在验证损失不再下降时停止训练
L1和L2正则化#
L2正则化(权重衰减)#
原理#
在损失函数中添加权重的平方和:
$$ L_{\text{new}} = L_{\text{original}} + \frac{\lambda}{2} \sum_i w_i^2 $$其中 $\lambda$ 是正则化系数。
梯度更新#
$$ \frac{\partial L_{\text{new}}}{\partial w} = \frac{\partial L_{\text{original}}}{\partial w} + \lambda w $$ $$ w \leftarrow w - \eta\left(\frac{\partial L_{\text{original}}}{\partial w} + \lambda w\right) = (1 - \eta\lambda)w - \eta\frac{\partial L_{\text{original}}}{\partial w} $$效果#
- 权重衰减:权重会逐渐变小
- 平滑权重:倾向于产生较小的权重
- 防止过拟合:限制模型复杂度
L1正则化#
原理#
在损失函数中添加权重的绝对值:
$$ L_{\text{new}} = L_{\text{original}} + \lambda \sum_i |w_i| $$梯度更新#
$$ \frac{\partial L_{\text{new}}}{\partial w} = \frac{\partial L_{\text{original}}}{\partial w} + \lambda \cdot \text{sign}(w) $$效果#
- 特征选择:倾向于将不重要的权重置为0
- 稀疏权重:产生稀疏的权重矩阵
- 可解释性:更容易识别重要特征
L1 vs L2对比#
| 特性 | L1正则化 | L2正则化 |
|---|---|---|
| 公式 | $\lambda\|w\|$ | $\lambda\|w\|^2$ |
| 权重 | 稀疏(很多0) | 平滑(小值) |
| 特征选择 | 是 | 否 |
| 计算 | 不可微(需次梯度) | 可微 |
| 应用 | 特征选择、压缩模型 | 一般正则化 |
Elastic Net#
结合L1和L2:
$$ L_{\text{new}} = L_{\text{original}} + \lambda_1 \sum_i |w_i| + \frac{\lambda_2}{2} \sum_i w_i^2 $$Dropout#
原理#
Dropout 在训练时随机"关闭"一部分神经元,防止过拟合。
训练阶段#
对于每个神经元:
以概率 p 保留
以概率 (1-p) 丢弃(输出设为0)测试阶段#
所有神经元都保留
输出乘以 (1-p) 进行缩放为什么有效?#
- 防止共适应:神经元不能过度依赖其他神经元
- 集成效果:相当于训练多个子网络
- 减少过拟合:降低模型复杂度
Dropout率选择#
- 隐藏层:通常
p = 0.5 - 输入层:通常
p = 0.2或更小 - 输出层:通常不使用Dropout
变体#
Dropout2D#
用于卷积层,按通道丢弃。
Alpha Dropout#
用于SELU激活函数,保持自归一化特性。
批量归一化#
原理#
批量归一化(Batch Normalization) 对每层的输入进行归一化:
$$ \mu_B = \frac{1}{m} \sum_{i=1}^{m} x_i \quad \text{(批量均值)} $$ $$ \sigma_B^2 = \frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_B)^2 \quad \text{(批量方差)} $$ $$ \hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \varepsilon}} \quad \text{(归一化)} $$ $$ y_i = \gamma \hat{x}_i + \beta \quad \text{(缩放和偏移)} $$其中:
- $\gamma$ :可学习的缩放参数
- $\beta$ :可学习的偏移参数
- $\varepsilon$ :防止除零的小常数
训练 vs 测试#
训练时:使用当前批量的统计量
测试时:使用移动平均的统计量
# 训练时
running_mean = momentum * running_mean + (1 - momentum) * batch_mean
running_var = momentum * running_var + (1 - momentum) * batch_var
# 测试时
x_norm = (x - running_mean) / sqrt(running_var + epsilon)优点#
- 加速训练:允许更大的学习率
- 减少内部协变量偏移:稳定输入分布
- 正则化效果:减少过拟合
- 减少对初始化的依赖
位置#
通常放在:
- 卷积层后、激活函数前 (最常见)
- 激活函数后 (也有使用)
其他正则化技术#
1. 数据增强#
通过变换训练数据增加数据量:
图像:
- 旋转、翻转、缩放
- 颜色变换、噪声添加
- Cutout、Mixup
文本:
- 同义词替换
- 回译
- 随机删除
2. 早停(Early Stopping)#
在验证损失不再下降时停止训练:
best_val_loss = float('inf')
patience = 10
counter = 0
for epoch in range(max_epochs):
train_loss = train_one_epoch()
val_loss = validate()
if val_loss < best_val_loss:
best_val_loss = val_loss
counter = 0
save_checkpoint()
else:
counter += 1
if counter >= patience:
break3. 权重初始化#
合适的初始化可以:
- 加速收敛
- 防止梯度消失/爆炸
方法:
- Xavier初始化:适合Sigmoid/Tanh
- He初始化:适合ReLU
- 正交初始化:保持梯度范数
4. 标签平滑(Label Smoothing)#
将硬标签转换为软标签:
原始标签:[1, 0, 0]
平滑标签:[0.9, 0.05, 0.05] # α=0.15. 权重共享#
多个位置共享同一组权重:
- 卷积层:空间权重共享
- RNN:时间步权重共享
代码实现#
L2正则化(权重衰减)#
import torch
import torch.nn as nn
import torch.optim as optim
model = nn.Sequential(
nn.Linear(784, 128),
nn.ReLU(),
nn.Linear(128, 10)
)
# 方法1:在优化器中设置weight_decay
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=0.0001)
# 方法2:手动添加L2项
def l2_regularization(model, lambda_reg=0.0001):
l2_loss = 0
for param in model.parameters():
l2_loss += torch.sum(param ** 2)
return lambda_reg * l2_loss
# 训练循环
for epoch in range(epochs):
# 前向传播
output = model(x)
loss = criterion(output, y)
# 添加L2正则化
loss += l2_regularization(model)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()Dropout#
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(784, 128)
self.dropout1 = nn.Dropout(p=0.5)
self.fc2 = nn.Linear(128, 64)
self.dropout2 = nn.Dropout(p=0.5)
self.fc3 = nn.Linear(64, 10)
def forward(self, x):
x = F.relu(self.fc1(x))
x = self.dropout1(x) # 训练时随机丢弃,测试时自动处理
x = F.relu(self.fc2(x))
x = self.dropout2(x)
x = self.fc3(x)
return x
# 训练时
model.train() # 启用Dropout
output = model(x)
# 测试时
model.eval() # 禁用Dropout
with torch.no_grad():
output = model(x)批量归一化#
# 全连接层
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(784, 128)
self.bn1 = nn.BatchNorm1d(128)
self.fc2 = nn.Linear(128, 64)
self.bn2 = nn.BatchNorm1d(64)
self.fc3 = nn.Linear(64, 10)
def forward(self, x):
x = self.fc1(x)
x = self.bn1(x)
x = F.relu(x)
x = self.fc2(x)
x = self.bn2(x)
x = F.relu(x)
x = self.fc3(x)
return x
# 卷积层
class ConvNet(nn.Module):
def __init__(self):
super(ConvNet, self).__init__()
self.conv1 = nn.Conv2d(3, 64, 3)
self.bn1 = nn.BatchNorm2d(64)
self.conv2 = nn.Conv2d(64, 128, 3)
self.bn2 = nn.BatchNorm2d(128)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = F.relu(x)
x = self.conv2(x)
x = self.bn2(x)
x = F.relu(x)
return x早停实现#
class EarlyStopping:
def __init__(self, patience=7, min_delta=0, restore_best_weights=True):
self.patience = patience
self.min_delta = min_delta
self.restore_best_weights = restore_best_weights
self.best_loss = None
self.counter = 0
self.best_weights = None
def __call__(self, val_loss, model):
if self.best_loss is None:
self.best_loss = val_loss
self.save_checkpoint(model)
elif val_loss < self.best_loss - self.min_delta:
self.best_loss = val_loss
self.counter = 0
self.save_checkpoint(model)
else:
self.counter += 1
if self.counter >= self.patience:
if self.restore_best_weights:
model.load_state_dict(self.best_weights)
return True
return False
def save_checkpoint(self, model):
self.best_weights = model.state_dict().copy()
# 使用
early_stopping = EarlyStopping(patience=10)
for epoch in range(epochs):
train_loss = train_one_epoch()
val_loss = validate()
if early_stopping(val_loss, model):
print("Early stopping triggered")
break数据增强#
from torchvision import transforms
# 图像数据增强
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(10),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.RandomCrop(32, padding=4),
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])总结#
- 过拟合:模型在训练集表现好但测试集表现差
- L1/L2正则化:通过惩罚大权重防止过拟合
- Dropout:随机丢弃神经元,防止共适应
- 批量归一化:归一化层输入,加速训练并正则化
- 其他技术:数据增强、早停、权重初始化等
关键要点:
- 正则化是防止过拟合的重要手段
- 不同正则化技术可以组合使用
- 根据任务和数据选择合适的正则化方法
- 注意训练和测试时的差异(Dropout、BN)