过拟合问题#

什么是过拟合?#

过拟合(Overfitting) 是指模型在训练集上表现很好,但在测试集上表现较差的现象。

过拟合的原因#

  1. 模型过于复杂:参数过多,容量过大
  2. 训练数据不足:数据量小于模型容量
  3. 训练时间过长:过度拟合训练数据

如何识别过拟合?#

训练损失 ↓↓↓ 但 验证损失 ↑↑↑

指标

  • 训练准确率 » 验证准确率
  • 训练损失 « 验证损失

解决方案#

  1. 增加数据:数据增强、收集更多数据
  2. 简化模型:减少参数、降低复杂度
  3. 正则化:限制模型复杂度
  4. 早停:在验证损失不再下降时停止训练

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) 进行缩放

为什么有效?#

  1. 防止共适应:神经元不能过度依赖其他神经元
  2. 集成效果:相当于训练多个子网络
  3. 减少过拟合:降低模型复杂度

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. 加速训练:允许更大的学习率
  2. 减少内部协变量偏移:稳定输入分布
  3. 正则化效果:减少过拟合
  4. 减少对初始化的依赖

位置#

通常放在:

  • 卷积层后、激活函数前 (最常见)
  • 激活函数后 (也有使用)

其他正则化技术#

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

3. 权重初始化#

合适的初始化可以:

  • 加速收敛
  • 防止梯度消失/爆炸

方法

  • Xavier初始化:适合Sigmoid/Tanh
  • He初始化:适合ReLU
  • 正交初始化:保持梯度范数

4. 标签平滑(Label Smoothing)#

将硬标签转换为软标签:

原始标签:[1, 0, 0]
平滑标签:[0.9, 0.05, 0.05]  # α=0.1

5. 权重共享#

多个位置共享同一组权重:

  • 卷积层:空间权重共享
  • 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,))
])

总结#

  1. 过拟合:模型在训练集表现好但测试集表现差
  2. L1/L2正则化:通过惩罚大权重防止过拟合
  3. Dropout:随机丢弃神经元,防止共适应
  4. 批量归一化:归一化层输入,加速训练并正则化
  5. 其他技术:数据增强、早停、权重初始化等

关键要点

  • 正则化是防止过拟合的重要手段
  • 不同正则化技术可以组合使用
  • 根据任务和数据选择合适的正则化方法
  • 注意训练和测试时的差异(Dropout、BN)