尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【Bug已解决】Pytorch vs. Keras: Pytorch model overfits heavily 解决方案

【Bug已解决】Pytorch vs. Keras: Pytorch model overfits heavily 解决方案 【Bug已解决】Pytorch vs. Keras: Pytorch model overfits heavily 解决方案问题描述很多开发者在从 Keras 迁移到 PyTorch 后发现同样的网络架构和数据集PyTorch 模型比 Keras 模型严重过拟合。训练集准确率快速上升到接近 100%但验证集准确率停滞甚至下降训练损失远低于验证损失。常见的困惑包括同样的模型结构Keras 不会过拟合但 PyTorch 会PyTorch 的训练损失下降太快了是不是学习率设置有问题Keras 默认的优化器配置和 PyTorch 有什么不同这个问题的根本原因通常不是 PyTorch 本身的问题而是两个框架在默认配置和训练细节上的差异。Keras 在很多地方做了隐式的优化和正则化而 PyTorch 要求开发者显式地配置这些参数。错误复现以下代码演示了 PyTorch 模型过拟合的典型场景import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset # 创建数据 x_train torch.randn(2000, 100) y_train torch.randint(0, 10, (2000,)) x_val torch.randn(500, 100) y_val torch.randint(0, 10, (500,)) train_loader DataLoader(TensorDataset(x_train, y_train), batch_size32, shuffleTrue) val_loader DataLoader(TensorDataset(x_val, y_val), batch_size32) # PyTorch 模型容易过拟合 model nn.Sequential( nn.Linear(100, 512), nn.ReLU(), nn.Linear(512, 512), nn.ReLU(), nn.Linear(512, 256), nn.ReLU(), nn.Linear(256, 10), ) optimizer optim.Adam(model.parameters(), lr1e-3) # 默认 lr1e-3 criterion nn.CrossEntropyLoss() # 训练 for epoch in range(50): model.train() train_loss 0 train_correct 0 for batch_x, batch_y in train_loader: optimizer.zero_grad() output model(batch_x) loss criterion(output, batch_y) loss.backward() optimizer.step() train_loss loss.item() train_correct output.argmax(1).eq(batch_y).sum().item() # 验证 model.eval() val_loss 0 val_correct 0 with torch.no_grad(): for batch_x, batch_y in val_loader: output model(batch_x) val_loss criterion(output, batch_y).item() val_correct output.argmax(1).eq(batch_y).sum().item() print(fEpoch {epoch1}: fTrain Acc: {train_correct/len(train_loader.dataset)*100:.1f}%, fVal Acc: {val_correct/len(val_loader.dataset)*100:.1f}%, fTrain Loss: {train_loss/len(train_loader):.4f}, fVal Loss: {val_loss/len(val_loader):.4f}) # 典型输出 # Epoch 1: Train Acc: 20.0%, Val Acc: 10.0% # Epoch 5: Train Acc: 65.0%, Val Acc: 10.0% # Epoch 10: Train Acc: 95.0%, Val Acc: 10.0% # Epoch 20: Train Acc: 100.0%, Val Acc: 10.0% # - 严重过拟合训练集 100%验证集 10%随机猜测水平根因分析1. 权重初始化差异Keras 默认使用glorot_uniformXavier 均匀分布初始化全连接层权重而 PyTorch 的nn.Linear默认使用kaiming_uniform_He 初始化。对于使用 ReLU 激活函数的网络He 初始化是合理的但它可能导致更大的初始激活值使模型更快地拟合训练数据。2. 优化器默认参数差异参数Keras (Adam)PyTorch (Adam)learning_rate0.0010.001beta_10.90.9beta_20.9990.999epsilon1e-71e-8PyTorch 的epsilon更小1e-8 vs 1e-7这意味着 Adam 优化器在更新参数时更激进可能导致更快地过拟合。3. Dropout 的实现差异Keras 的Dropout层在训练时以概率rate丢弃神经元并将保留的神经元乘以1/(1-rate)inverted dropout。PyTorch 的nn.Dropout使用相同的机制但开发者经常忘记在 PyTorch 中添加 Dropout 层因为 Keras 的 Sequential API 更方便添加。4. BatchNorm 的差异Keras 的BatchNormalization默认使用momentum0.99移动平均的衰减率而 PyTorch 的nn.BatchNorm1d使用momentum0.1注意PyTorch 的 momentum 含义与 Keras 相反PyTorch 的0.1等价于 Keras 的0.9。如果配置不当BatchNorm 的运行统计量更新速度不同影响正则化效果。5. 学习率调度Keras 默认不使用学习率调度但很多 Keras 教程和示例中包含了ReduceLROnPlateau。PyTorch 开发者经常使用固定的学习率不进行衰减导致后期训练过拟合。6. 数据增强和预处理Keras 的ImageDataGenerator提供了丰富的数据增强功能很多开发者在使用 Keras 时不知不觉地进行了大量数据增强。迁移到 PyTorch 后如果没有使用torchvision.transforms进行等价的数据增强模型可用的有效数据减少更容易过拟合。7. 损失函数的差异Keras 的categorical_crossentropy在内部对 logits 和 softmax 进行了数值稳定的融合计算。PyTorch 的nn.CrossEntropyLoss也做了类似处理但如果开发者错误地使用了nn.LogSoftmaxnn.NLLLoss的组合可能引入数值不稳定。解决方案方案一添加正则化Dropout Weight Decayimport torch import torch.nn as nn import torch.optim as optim class RegularizedModel(nn.Module): 带完整正则化的模型 def __init__(self, input_dim100, num_classes10, dropout_rate0.5): super().__init__() self.network nn.Sequential( nn.Linear(input_dim, 512), nn.BatchNorm1d(512), # BatchNorm 正则化 nn.ReLU(), nn.Dropout(dropout_rate), # Dropout 正则化 nn.Linear(512, 512), nn.BatchNorm1d(512), nn.ReLU(), nn.Dropout(dropout_rate), nn.Linear(512, 256), nn.BatchNorm1d(256), nn.ReLU(), nn.Dropout(dropout_rate * 0.5), # 后面的 Dropout 率降低 nn.Linear(256, num_classes), ) # 自定义权重初始化 self._init_weights() def _init_weights(self): 使用 Xavier 初始化 for m in self.modules(): if isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight) if m.bias is not None: nn.init.zeros_(m.bias) def forward(self, x): return self.network(x) # 使用 weight_decayL2 正则化 model RegularizedModel(dropout_rate0.5) # 关键Adam weight_decay optimizer optim.AdamW( model.parameters(), lr1e-3, weight_decay1e-2, # L2 正则化 )方案二使用学习率调度import torch import torch.optim as optim model RegularizedModel() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-2) # 方法1: CosineAnnealingLR scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50, eta_min1e-5) # 方法2: ReduceLROnPlateau scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5, min_lr1e-6 ) # 训练循环中使用 for epoch in range(50): # ... 训练代码 ... # 方法1: CosineAnnealingLR scheduler.step() # 方法2: ReduceLROnPlateau # scheduler.step(val_loss)方案三使用早停Early Stoppingclass EarlyStopping: 早停机制 def __init__(self, patience5, min_delta0.001): self.patience patience self.min_delta min_delta self.wait 0 self.best_loss float(inf) self.should_stop False def __call__(self, val_loss): if val_loss self.best_loss - self.min_delta: self.best_loss val_loss self.wait 0 else: self.wait 1 if self.wait self.patience: self.should_stop True return self.should_stop # 使用 early_stopping EarlyStopping(patience10) for epoch in range(100): # ... 训练 ... if early_stopping(val_loss): print(fEarly stopping at epoch {epoch}) break完整修复代码以下是一个完整的、包含所有正则化技术的 PyTorch 训练方案import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset, Dataset import numpy as np import copy import time from typing import Optional, List, Dict class AntiOverfitModel(nn.Module): 防过拟合的完整模型。 包含 Dropout、BatchNorm、残差连接和权重正则化。 def __init__(self, input_dim100, hidden_dims[512, 512, 256], num_classes10, dropout_rate0.5, use_residualTrue): super().__init__() self.use_residual use_residual ![配图](https://i-blog.csdnimg.cn/img_convert/a1a25547de597325d71a1003faf9b7ec.png) layers [] prev_dim input_dim for i, dim in enumerate(hidden_dims): block nn.Sequential( nn.Linear(prev_dim, dim), nn.BatchNorm1d(dim), nn.ReLU(), nn.Dropout(dropout_rate if i len(hidden_dims) - 1 else dropout_rate * 0.5), ) layers.append(block) # 残差连接维度匹配时 if use_residual and prev_dim dim: layers.append(ResidualBlock(dim, dropout_rate)) prev_dim dim self.features nn.Sequential(*layers) self.classifier nn.Linear(prev_dim, num_classes) self._init_weights() def _init_weights(self): for m in self.modules(): if isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight, gain0.5) # 较小的 gain if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.BatchNorm1d): nn.init.ones_(m.weight) nn.init.zeros_(m.bias) def forward(self, x): x self.features(x) return self.classifier(x) class ResidualBlock(nn.Module): 残差块 def __init__(self, dim, dropout_rate0.3): super().__init__() self.block nn.Sequential( nn.Linear(dim, dim), nn.BatchNorm1d(dim), nn.ReLU(), nn.Dropout(dropout_rate), nn.Linear(dim, dim), nn.BatchNorm1d(dim), ) self.relu nn.ReLU() def forward(self, x): return self.relu(x self.block(x)) class MixupData: Mixup 数据增强 def __init__(self, alpha0.2): self.alpha alpha def __call__(self, x, y): if self.alpha 0: lam np.random.beta(self.alpha, self.alpha) else: lam 1.0 batch_size x.size(0) index torch.randperm(batch_size, devicex.device) mixed_x lam * x (1 - lam) * x[index] y_a, y_b y, y[index] return mixed_x, y_a, y_b, lam def mixup_criterion(criterion, pred, y_a, y_b, lam): Mixup 损失函数 return lam * criterion(pred, y_a) (1 - lam) * criterion(pred, y_b) class LabelSmoothingLoss(nn.Module): 标签平滑损失 def __init__(self, num_classes, smoothing0.1): super().__init__() self.num_classes num_classes self.smoothing smoothing self.confidence 1.0 - smoothing def forward(self, pred, target): pred pred.log_softmax(dim-1) with torch.no_grad(): true_dist torch.zeros_like(pred) true_dist.fill_(self.smoothing / (self.num_classes - 1)) true_dist.scatter_(1, target.unsqueeze(1), self.confidence) return torch.mean(torch.sum(-true_dist * pred, dim-1)) class AntiOverfitTrainer: 防过拟合训练器。 集成所有正则化技术。 def __init__(self, model, deviceauto, learning_rate1e-3, weight_decay1e-2, use_mixupFalse, mixup_alpha0.2, label_smoothing0.1, grad_clip1.0): self.device torch.device( cuda if device auto and torch.cuda.is_available() else cpu ) self.model model.to(self.device) # 使用 AdamW带解耦 weight decay self.optimizer optim.AdamW( model.parameters(), lrlearning_rate, weight_decayweight_decay, betas(0.9, 0.999), eps1e-8, ) # 损失函数带标签平滑 self.criterion LabelSmoothingLoss( num_classes10, smoothinglabel_smoothing ) # Mixup self.use_mixup use_mixup self.mixup MixupData(alphamixup_alpha) if use_mixup else None # 梯度裁剪 self.grad_clip grad_clip # 学习率调度 self.scheduler None # 训练历史 self.history {train_loss: [], val_loss: [], train_acc: [], val_acc: []} self.best_model None self.best_val_loss float(inf) def set_scheduler(self, scheduler_typecosine, **kwargs): 设置学习率调度器 if scheduler_type cosine: T_max kwargs.get(T_max, 50) eta_min kwargs.get(eta_min, 1e-6) self.scheduler optim.lr_scheduler.CosineAnnealingLR( self.optimizer, T_maxT_max, eta_mineta_min ) elif scheduler_type plateau: factor kwargs.get(factor, 0.5) patience kwargs.get(patience, 5) self.scheduler optim.lr_scheduler.ReduceLROnPlateau( self.optimizer, modemin, factorfactor, patiencepatience, min_lr1e-6 ) def train_epoch(self, dataloader): self.model.train() total_loss 0 correct 0 total 0 for batch_x, batch_y in dataloader: batch_x batch_x.to(self.device) batch_y batch_y.to(self.device) # Mixup if self.use_mixup and self.mixup: mixed_x, y_a, y_b, lam self.mixup(batch_x, batch_y) self.optimizer.zero_grad() output self.model(mixed_x) loss mixup_criterion(self.criterion, output, y_a, y_b, lam) loss.backward() if self.grad_clip: torch.nn.utils.clip_grad_norm_( self.model.parameters(), self.grad_clip ) self.optimizer.step() total_loss loss.item() # Mixup 下准确率计算较复杂简化处理 correct (output.argmax(1) y_a).float().mean().item() * batch_y.size(0) total batch_y.size(0) else: self.optimizer.zero_grad() output self.model(batch_x) loss self.criterion(output, batch_y) loss.backward() if self.grad_clip: torch.nn.utils.clip_grad_norm_( self.model.parameters(), self.grad_clip ) self.optimizer.step() total_loss loss.item() correct output.argmax(1).eq(batch_y).sum().item() total batch_y.size(0) return { loss: total_loss / len(dataloader), accuracy: 100. * correct / total, } torch.no_grad() def validate(self, dataloader): self.model.eval() total_loss 0 correct 0 total 0 for batch_x, batch_y in dataloader: batch_x batch_x.to(self.device) batch_y batch_y.to(self.device) output self.model(batch_x) loss self.criterion(output, batch_y) total_loss loss.item() correct output.argmax(1).eq(batch_y).sum().item() total batch_y.size(0) return { loss: total_loss / len(dataloader), accuracy: 100. * correct / total, } def fit(self, train_loader, val_loader, epochs50, patience10): 完整训练 early_stop_counter 0 print(f{*60}) print(f防过拟合训练 | 设备: {self.device} | 轮数: {epochs}) print(f正则化: weight_decay{self.optimizer.param_groups[0][weight_decay]}, fmixup{self.use_mixup}, grad_clip{self.grad_clip}) print(f{*60}) for epoch in range(1, epochs 1): start time.time() train_m self.train_epoch(train_loader) val_m self.validate(val_loader) # 学习率调度 if self.scheduler: if isinstance(self.scheduler, optim.lr_scheduler.ReduceLROnPlateau): self.scheduler.step(val_m[loss]) else: self.scheduler.step() # 记录 self.history[train_loss].append(train_m[loss]) self.history[val_loss].append(val_m[loss]) self.history[train_acc].append(train_m[accuracy]) self.history[val_acc].append(val_m[accuracy]) elapsed time.time() - start lr self.optimizer.param_groups[0][lr] print(fEpoch {epoch}/{epochs} [{elapsed:.1f}s] lr{lr:.6f} | fTrain: {train_m[accuracy]:.1f}% (loss{train_m[loss]:.4f}) | fVal: {val_m[accuracy]:.1f}% (loss{val_m[loss]:.4f})) # 保存最佳模型 if val_m[loss] self.best_val_loss: self.best_val_loss val_m[loss] self.best_model copy.deepcopy(self.model.state_dict()) early_stop_counter 0 else: early_stop_counter 1 if early_stop_counter patience: print(fEarly stopping at epoch {epoch}) break # 恢复最佳模型 if self.best_model: self.model.load_state_dict(self.best_model) print(f\n恢复最佳模型 (val_loss{self.best_val_loss:.4f})) return self.history # 对比实验 if __name__ __main__: # 创建数据 np.random.seed(42) torch.manual_seed(42) x_train torch.randn(2000, 100) y_train torch.randint(0, 10, (2000,)) x_val torch.randn(500, 100) y_val torch.randint(0, 10, (500,)) train_loader DataLoader(TensorDataset(x_train, y_train), batch_size32, shuffleTrue) val_loader DataLoader(TensorDataset(x_val, y_val), batch_size32) # 实验1无正则化过拟合 print(\n * 60) print(实验1无正则化预期过拟合) print( * 60) model1 nn.Sequential( nn.Linear(100, 512), nn.ReLU(), nn.Linear(512, 512), nn.ReLU(), nn.Linear(512, 256), nn.ReLU(), nn.Linear(256, 10), ) trainer1 AntiOverfitTrainer( model1, learning_rate1e-3, weight_decay0, label_smoothing0, grad_clip0 ) trainer1.set_scheduler(cosine, T_max30) trainer1.fit(train_loader, val_loader, epochs30, patience30) # 实验2完整正则化 print(\n * 60) print(实验2完整正则化预期减轻过拟合) print( * 60) model2 AntiOverfitModel( input_dim100, hidden_dims[512, 512, 256], num_classes10, dropout_rate0.5, use_residualTrue ) trainer2 AntiOverfitTrainer( model2, learning_rate1e-3, weight_decay1e-2, use_mixupTrue, mixup_alpha0.2, label_smoothing0.1, grad_clip1.0 ) trainer2.set_scheduler(cosine, T_max50, eta_min1e-5) trainer2.fit(train_loader, val_loader, epochs50, patience10) # 对比 print(\n * 60) print(对比结果) print( * 60) if trainer1.history[val_acc] and trainer2.history[val_acc]: print(f实验1 最佳验证准确率: {max(trainer1.history[val_acc]):.1f}%) print(f实验2 最佳验证准确率: {max(trainer2.history[val_acc]):.1f}%) gap1 max(trainer1.history[train_acc]) - max(trainer1.history[val_acc]) gap2 max(trainer2.history[train_acc]) - max(trainer2.history[val_acc]) print(f实验1 训练-验证准确率差距: {gap1:.1f}%) print(f实验2 训练-验证准确率差距: {gap2:.1f}%)常见陷阱与注意事项1. Dropout 率的选择Dropout 率通常在 0.2-0.5 之间。太低没有正则化效果太高会导致欠拟合。对于大型网络可以使用较高的 Dropout 率小型网络使用较低的。2. Weight Decay 的调优weight_decayL2 正则化系数通常在 1e-4 到 1e-2 之间。使用AdamW而非Adam weight_decay因为 AdamW 的权重衰减是解耦的效果更好。3. BatchNorm 与小 Batch Size当 batch size 太小如 1-4时BatchNorm 的统计量不稳定。此时考虑使用 GroupNorm 或 LayerNorm 替代。4. 标签平滑的适用场景标签平滑适用于类别数较多10的分类任务。对于二分类或类别数很少的任务标签平滑可能效果不明显。5. Mixup 的局限Mixup 在某些任务中可能不适用如目标检测、图像分割。对于分类任务Mixup 通常能有效减少过拟合。6. 学习率预热对于使用 AdamW 和 cosine 调度的训练添加学习率预热warmup可以稳定训练初期scheduler optim.lr_scheduler.LinearLR( optimizer, start_factor0.1, total_iters5 )总结PyTorch 模型比 Keras 模型更容易过拟合通常是因为缺少了 Keras 默认包含的正则化机制。核心解决策略如下添加 Dropout在全连接层之间添加nn.Dropout(0.3-0.5)是最有效的正则化手段。使用 Weight DecayAdamW(weight_decay1e-2)提供解耦的 L2 正则化。添加 BatchNormnn.BatchNorm1d/2d不仅能加速训练还有轻微的正则化效果。使用学习率调度CosineAnnealingLR或ReduceLROnPlateau在训练后期降低学习率。标签平滑LabelSmoothingLoss(smoothing0.1)防止模型对训练数据过度自信。数据增强Mixup、CutMix 等技术增加有效训练数据量。早停在验证损失不再下降时停止训练恢复最佳模型。梯度裁剪clip_grad_norm_防止梯度爆炸导致的训练不稳定。通过系统性地应用这些正则化技术PyTorch 模型可以达到与 Keras 模型相当甚至更好的泛化性能。
返回列表