深度学习——常见问题与优化改进

发布于:2025-08-17 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、过拟合

过拟合(Overfitting)是指模型在训练数据上表现得非常好,但在测试数据或者实际应用中表现很差。换句话说,模型“记住了训练数据”,却没有学到真正的规律。

原因:

  • 模型过于复杂,参数太多。
  • 训练数据量太少,样本不够代表整体。
  • 特征选择不合理,噪声太多。

解决方案:

  • 简化模型:减少层数或神经元数量。
  • 正则化(Regularization):在损失函数里加入额外约束,让模型参数不变得过大,从而降低过拟合风险。
    • L1/L2 正则
    • Dropout(随机丢弃神经元):在训练过程中随机“关闭”一部分神经元,防止模型过度依赖某些特征。
  • 增加训练数据:更多的数据能帮助模型学到真实规律。
  • 提前停止训练(Early Stopping):监控验证集损失,避免训练过久。

1.1正则化

在损失函数中加入额外约束,使模型主动 “放弃” 或 “弱化” 对预测贡献小的参数。从而让模型更“简单”,更能泛化到新数据。
总损失 = 原损失 + λ ∗ 正则项 总损失=原损失 + λ * 正则项 总损失=原损失+λ正则项其中,λ 控制惩罚强度,>=0。

方法 原理 效果 特点
L1正则化 正则化项=权重的绝对值之和 部分参数 归零 稀疏参数
L2正则化 正则化项=权重的平方之和 所有参数被压缩 至接近 0 平滑参数

Dropout 随机让一部分神经元 “失效”(输出为 0),使模型无法依赖固定的神经元组合,被迫学习更广泛、更具代表性的特征。

在 PyTorch 中,L1 需手动加入损失,对于加了L1正则的神经网络,大部分深度学习框架自带的优化器训练获得不了稀疏解;如果稀疏解的模式是无规律的,这种稀疏意义不大。
L2 可通过 weight_decay,Dropout 内置在模型层中

import torch
import torch.nn as nn
# L1正则化
l1_norm = sum(p.abs().sum() for p in model.parameters())
loss = loss + lambda_l1 * l1_norm

# L2正则化
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=0.001)

# 定义一个带 Dropout 的神经网络
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(100, 200)  # 输入层→隐藏层1
        self.dropout1 = nn.Dropout(p=0.5)  # 隐藏层1的Dropout(50%丢弃率)
        self.fc2 = nn.Linear(200, 100)  # 隐藏层1→隐藏层2
        self.dropout2 = nn.Dropout(p=0.3)  # 隐藏层2的Dropout(30%丢弃率)
        self.fc3 = nn.Linear(100, 10)   # 隐藏层2→输出层(10分类)

1.2早停

在训练过程中,每个 epoch 计算验证集损失。如果验证集损失在 连续若干个 epoch 内没有改善,则停止训练。

  • patience(耐心值):允许验证集表现不提升的轮数。
  • delta:改善阈值,如果改善小于 delta,则认为没有改善。
  • restore_best_weights:训练结束后是否恢复最佳模型参数。
# 早停参数
patience = 10
best_val_loss = float('inf')
counter = 0
best_model_weights = None
# 训练循环
num_epochs = 100
for epoch in range(num_epochs):
    model.train()
    optimizer.zero_grad()
    outputs = model(X_train)
    loss = criterion(outputs, y_train)
    loss.backward()
    optimizer.step()
    
    # 验证集损失
    model.eval()
    with torch.no_grad():
        val_outputs = model(X_val)
        val_loss = criterion(val_outputs, y_val)
    print(f"Epoch {epoch+1} - Train Loss: {loss.item():.4f}, Val Loss: {val_loss.item():.4f}")
    # 早停逻辑
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        counter = 0
        best_model_weights = model.state_dict()  # 保存最佳模型
    else:
        counter += 1
        if counter >= patience:
            print("Early stopping triggered!")
            break
# 恢复最佳模型
if best_model_weights is not None:
    model.load_state_dict(best_model_weights)

二、欠拟合

欠拟合(Underfitting)是指模型在训练数据上都表现不好,说明模型太简单,无法捕捉数据中的规律。

原因:

  • 模型复杂度不足(层数太少、参数太少)。
  • 特征太少或不够有代表性。
  • 训练不够,模型还没学会规律。

解决方案:

  • 增加模型复杂度:增加网络层数或神经元数量。
  • 增加特征:尝试更多有用的特征或者做特征工程。
  • 延长训练时间:训练更多轮(epochs)。
  • 减少正则化:过强的正则化也可能导致欠拟合。

三、梯度消失 & 梯度爆炸

梯度问题是深度学习训练时常见问题,发生在反向传播(Backpropagation)阶段。
梯度消失:梯度太小,导致权重更新几乎停滞,模型无法学习。
梯度爆炸:梯度太大,权重更新幅度过大,模型不收敛甚至发散。

原因:

  • 网络太深,连续的矩阵乘积导致梯度指数级衰减或增长。
  • 激活函数选择不当,如 sigmoid/tanh 在大输入下梯度趋近零。

解决方案:

  • 使用 ReLU 激活函数(不会导致梯度消失)。
  • 权重初始化:比如 Xavier 或 He 初始化,防止梯度过大或过小。
    • 若初始权重过大:前向传播时,信号经过经多层放大后可能溢出(如激活值趋于无穷大),反向传播时梯度也会急剧增大(梯度爆炸)。
    • 若初始权重过小:信号经多层传递后,信号会逐渐衰减至接近 0(梯度消失),导致浅层参数几乎无法更新。
  • 归一化方法:Batch Normalization、Layer Normalization
  • 梯度裁剪(Gradient Clipping):限制梯度最大值,避免爆炸。

3.1激活函数

激活函数用于神经网络中,每个神经元的输出经过激活函数才能引入非线性,使神经网络能够拟合复杂函数。

激活函数 公式 输出范围 优点 缺点 PyTorch实现
Sigmoid σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1+e^{-x}} σ(x)=1+ex1 (0,1) 输出概率,常用于二分类 梯度消失,输出非零均值 nn.Sigmoid()
Tanh tanh ⁡ ( x ) = e x − e − x e x + e − x \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} tanh(x)=ex+exexex (-1,1) 输出零均值,梯度比 sigmoid 好 梯度消失 nn.Tanh()
ReLU ReLU ( x ) = max ⁡ ( 0 , x ) \text{ReLU}(x) = \max(0, x) ReLU(x)=max(0,x) [0,∞) 简单、高效,缓解梯度消失 死神经元(负区间梯度为0) nn.ReLU()
Leaky ReLU LeakyReLU ( x ) = max ⁡ ( 0.01 x , x ) \text{LeakyReLU}(x) = \max(0.01x, x) LeakyReLU(x)=max(0.01x,x) (-∞,∞) 改进 ReLU,避免死神经元 输出仍不零均值 nn.LeakyReLU(0.01)
ELU ELU ( x ) = x  if  x > 0 , α ( e x − 1 )  if  x ≤ 0 \text{ELU}(x) = x \text{ if } x>0, \alpha(e^x-1) \text{ if } x≤0 ELU(x)=x if x>0,α(ex1) if x0 (-α,∞) 平滑,负值接近零均值 计算稍复杂 nn.ELU(alpha=1.0)
Softmax Softmax ( x i ) = e x i ∑ j e x j \text{Softmax}(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}} Softmax(xi)=jexjexi (0,1), ∑=1 多分类输出概率 仅用于输出层 nn.Softmax(dim=1)
Swish Swish ( x ) = x ∗ σ ( x ) \text{Swish}(x) = x * \sigma(x) Swish(x)=xσ(x) (-∞,∞) 平滑,表现优于 ReLU 计算稍复杂 x * torch.sigmoid(x)
GELU GELU ( x ) = x ∗ Φ ( x ) \text{GELU}(x) = x * \Phi(x) GELU(x)=xΦ(x)(Φ为标准正态CDF) (-∞,∞) Transformer 常用 计算复杂 nn.GELU()

参考:https://blog.csdn.net/qq_42691298/article/details/126590726

3.2权重初始化

# Xavier 初始化(均匀分布),适合 sigmoid/tanh
init.xavier_uniform_(layer.weight)  
# He 初始化(正态分布),适合 ReLU
init.kaiming_uniform_(layer.weight, nonlinearity='relu')  

3.3批归一化BN

A全连接层 —————激活函数——B全连接层
A全连接层——BN——激活函数——B全连接层
对每一层的输入进行标准化处理,使输入分布稳定在均值为 0、方差为 1 附近,从而在进入激活函数之前数据间的差距能变得更小。

class NetWithBN(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(784, 256),
            nn.BatchNorm1d(256),  # 添加BN层
            nn.ReLU(),
            nn.Linear(256, 10)
        )

3.4残差连接(Residual Connection)【消失】

在某些层的输出上加上输入x,关键在于求导 “+1”

class ResidualBlock(nn.Module):
    def __init__(self, in_features):
        super().__init__()
        self.fc = nn.Linear(in_features, in_features)
        self.relu = nn.ReLU()
    
    def forward(self, x):
        out = self.fc(x)
        out = self.relu(out)
        out += x  # 残差连接
        return out

3.5梯度裁剪(Gradient Clipping)【爆炸】

当梯度的 “大小” 超过预设阈值时,按比例缩小梯度,使其控制在合理范围内,保证参数更新的稳定性。

optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for epoch in range(epochs):
    for inputs, targets in dataloader:
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, targets)
        loss.backward()
        # 梯度裁剪(关键步骤)
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()

四、学习率不合适

学习率决定每次更新参数的步长。如果学习率不合适,会导致训练问题。

  • 学习率太大:训练不稳定,损失震荡或发散。
  • 学习率太小:收敛太慢,训练时间长甚至停滞。

当学习率过大时:降低并稳定学习率直接减小学习率:

  • 通常按比例缩小(如除以 10),例如从 0.1→0.01,0.001→0.0001。初次调整后观察损失是否趋于稳定(不再剧烈波动)。若损失仍波动,继续缩小(如再除以 5),直到损失呈现下降趋势。
  • 配合梯度裁剪:学习率过大可能伴随梯度爆炸,通过梯度裁剪(如设置 L2 范数阈值 1.0)限制梯度幅度,再适当降低学习率,可快速稳定训练。
  • 使用学习率预热(Warm-up):初始用极小的学习率(如目标学习率的 1/10),逐步增大至目标值(如前 5 个 epoch 线性增长),避免训练初期的剧烈震荡。
  • 适用场景:大型模型(如 Transformer)、大批次训练(如批大小 > 512)。

当学习率过小时:增大并加速收敛直接增大学习率:

  • 按比例放大(如乘以 5 或 10),例如从 0.0001→0.0005,0.01→0.05。观察损失是否下降加快,但需避免过大导致震荡。
  • 动态调整学习率(学习率调度):初期用较大学习率快速下降,后期减小学习率精细优化,
    • 分段衰减(Step Decay):每训练一定 epoch(如 30 轮),学习率乘以衰减系数(如 0.1)。
    • 指数衰减(Exponential Decay):学习率随 epoch 指数下降. lr = lr 0 ⋅ γ epoch \text{lr} = \text{lr}_0 \cdot \gamma^{\text{epoch}} lr=lr0γepoch
    • 余弦退火(Cosine Annealing):学习率随 epoch 按余弦曲线周期性下降,适合需要精细收敛的场景。
    • 自适应调度(ReduceLROnPlateau):当验证集损失停止下降时,自动减小学习率(如乘以 0.5)。
  • 结合批大小调整:若增大批大小(如从 32→128),可按比例增大学习率(如从 0.01→0.04),保持参数更新幅度一致(线性缩放规则)。

五、数据不平衡

数据不平衡:训练数据中某些类别样本太少,导致模型偏向多的类别,分类效果差。

解决方案:

  • 重采样(Resampling):增加少数类别样本(过采样)或减少多数类别样本(欠采样)。
  • 类别权重(Class Weight):在损失函数中给少数类更高权重。
  • 使用 Focal Loss:专门关注难分类的样本。

六、泛化不足

模型在训练数据上表现很好,但在新数据上表现差。

原因:

  • 过拟合。
  • 数据分布变化(训练数据和实际应用数据差异大)。

解决方案:

  • 增加训练数据或做数据增强(Data Augmentation)。
  • 使用正则化。
  • 尝试迁移学习(Transfer Learning),用已有模型在新数据上微调。

七、训练不稳定

训练过程中损失波动大,模型难以收敛。

原因:

  • 学习率过大。
  • 网络结构不合理或初始化不当。
  • 数据噪声大或批量归一化问题。

解决方案:

  • 调小学习率,使用自适应优化器。
  • 使用 Batch Normalization,稳定各层输入分布。
  • 改善数据质量,清理异常值。

八、部署慢

模型训练完成后,在实际应用中运行缓慢,影响效率。

原因:

  • 模型太大,参数多。
  • 硬件性能不足。
  • 推理优化不足。

解决方案:

  • 模型压缩:剪枝(Pruning)、量化(Quantization)、知识蒸馏(Knowledge Distillation)。
  • 使用 GPU / TPU 或高性能硬件。
  • 使用高效推理框架,如 TensorRT、ONNX Runtime。

网站公告

今日签到

点亮在社区的每一天
去签到