# 07. 训练技巧:过拟合、Dropout、BatchNorm、学习率调度 > 模型架构搭好了,但训练时一堆问题:loss 不降、验证集比训练集差很多、调 lr 像摸黑…… > 这篇把**最实用的训练技巧**一次性讲清。 --- ## 一、过拟合 vs 欠拟合(先看症状) ``` 训练集准确率: 99% 验证集准确率: 65% └──────── 过拟合 ────────┘ 模型把训练集"背下来了",对新数据一脸懵逼 训练集准确率: 55% 验证集准确率: 52% └──────── 欠拟合 ────────┘ 模型太简单,连训练集都学不明白 ``` ### 直观图 ``` 欠拟合 正好 过拟合 ─── ~~~ ∿∿∿∿∿ 直线拟合 曲线拟合 疯狂抖动穿过每个点 ``` ### 诊断方法 ```python # 训练时同时记录两套 loss epoch_train_loss = [] epoch_val_loss = [] for epoch in range(EPOCHS): model.train() train_loss = train_one_epoch(...) model.eval() val_loss = validate(...) epoch_train_loss.append(train_loss) epoch_val_loss.append(val_loss) ``` **画图一看就知道**: - 过拟合:train loss ↓↓,val loss 先↓后↑( divergence 分叉) - 欠拟合:两条线都高,没降下来 --- ## 二、Dropout:训练时"随机删神经元" ### 原理 ```python nn.Dropout(p=0.5) # 训练时每次前向,随机关掉50%的神经元 ``` 为什么有用? - 强迫网络**不依赖任何一个神经元** - 相当于同时训练了无数个"子网络",测试时取平均 - 本质:**模型集成(ensemble)的廉价版** ### 关键点 ```python model.train() # Dropout 开启(训练时) model.eval() # Dropout 关闭(测试/验证时)—— PyTorch 自动处理 ``` > 测试时所有神经元都工作,但输出自动乘以 (1-p) 来补偿训练时的"缺席"。 ### 放哪? ```python nn.Sequential( nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.3), # 全连接层后放,推荐 0.2~0.5 nn.Linear(64, 10) ) ``` **卷积层一般不放 Dropout**,因为卷积本身参数共享就是正则;**全连接层后放**效果最好。 --- ## 三、BatchNorm:让每一层"活得舒服点" ### 问题背景 网络越深,前面层参数一变,后面层输入分布跟着变(**内部协变量偏移**)。 后面层要不断适应新分布,学得很累。 ### BatchNorm 做什么? 对**每个 batch** 的每个通道,做: ``` 1. 算 batch 均值 μ、标准差 σ 2. 归一化: x̂ = (x - μ) / √(σ² + ε) 3. 再缩放平移: y = γ·x̂ + β (γ、β 是可学习的,让网络自己决定"要不要完全归一化") ``` ### 代码 ```python nn.BatchNorm1d(64) # 全连接层后(输入 [B, C] 或 [B, C, L]) nn.BatchNorm2d(32) # 卷积层后(输入 [B, C, H, W]) ``` ### 放哪? **卷积/线性 → BatchNorm → ReLU**(这是标准顺序): ```python nn.Sequential( nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), # ← 放这里 nn.ReLU(), nn.MaxPool2d(2) ) ``` ### BatchNorm 的隐藏好处 1. **允许更大的学习率**(不怕梯度爆炸/消失) 2. **自带轻微正则效果**(batch 统计有噪声) 3. **训练更稳定**,收敛更快 > 现代网络(ResNet、Transformer)几乎每层都有 BN,是标配。 --- ## 四、学习率调度:别一条 lr 走到底 ### 为什么要调? - **初期**:loss 高,需要大步走(lr 大) - **后期**:接近最优,要小步微调(lr 小) - lr 一直太大 → 在最优解附近震荡,停不下来 ### 几种策略 ```python from torch.optim.lr_scheduler import StepLR, CosineAnnealingLR, ReduceLROnPlateau # 1. 阶梯衰减:每 N 个 epoch lr 乘以 gamma scheduler = StepLR(optimizer, step_size=10, gamma=0.1) # epoch 0-9: lr=0.01, epoch 10-19: lr=0.001, ... # 2. 余弦退火:lr 按余弦曲线平滑降到 0 scheduler = CosineAnnealingLR(optimizer, T_max=100) # 3. 按需衰减:val loss 不下降时自动降 lr(最智能) scheduler = ReduceLROnPlateau(optimizer, mode='min', patience=5, factor=0.5) ``` ### 使用方式 ```python for epoch in range(EPOCHS): train(...) val_loss = validate(...) # StepLR / CosineAnnealingLR: 每个 epoch 后 step scheduler.step() # ReduceLROnPlateau: 传 val_loss 给它判断 # scheduler.step(val_loss) ``` --- ## 五、数据增强:没数据?造数据! 对训练图片做随机变换,让模型见到"更多样"的数据: ```python from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomCrop(32, padding=4), # 随机裁剪(带填充) transforms.RandomHorizontalFlip(p=0.5), # 50%概率水平翻转 transforms.ToTensor(), transforms.Normalize((0.5,0.5,0.5), (0.5,0.5,0.5)) ]) # 测试时不能增强!只做标准化 test_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,0.5,0.5), (0.5,0.5,0.5)) ]) ``` > 增强只在训练时用,验证/测试用原始图片,否则评估不准。 --- ## 六、权重衰减(Weight Decay)/ L2 正则 在 loss 里加一项:参数的平方和。强迫参数尽量小,模型更简单。 ```python optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) # ↑ 推荐 1e-4 ~ 5e-4 ``` 本质:**大参数 = 复杂模型 = 容易过拟合**,惩罚大参数让它别学太野。 --- ## 七、早停(Early Stopping) 验证集 loss 连续 N 轮不下降 → 停训,防止过拟合继续恶化。 ```python best_val_loss = float('inf') patience = 10 no_improve = 0 for epoch in range(EPOCHS): train(...) val_loss = validate(...) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_model.pth') # 保存最佳 no_improve = 0 else: no_improve += 1 if no_improve >= patience: print(f'Early stopping at epoch {epoch}') break ``` > 实际训练标配:早停 + 保存 best checkpoint。 --- ## 八、实战:CIFAR-10 对比实验 我们用 CIFAR-10(32×32 彩色图,10类)做两组对比: | | Baseline | +Tricks | |---|---|---| | 结构 | 简单 CNN | CNN + BN + Dropout | | 增强 | 无 | RandomCrop + RandomFlip | | 学习率 | 固定 | CosineAnnealing | | 正则 | 无 | Weight Decay | | 早停 | 无 | ✅ | ### 完整代码 见 `code/cifar_tricks_demo.py` ### 预期结果 ``` Baseline: 测试准确率 ~72% +Tricks: 测试准确率 ~82~85% ``` 差距来自: - 数据增强 → 模型见过更多变体,泛化更好 - BatchNorm → 训练更稳定,允许更深网络 - Dropout + Weight Decay → 抑制过拟合 - 学习率调度 → 收敛到更好的局部最优 --- ## 九、所有技巧速查表 | 问题 | 解法 | 代码/参数 | |---|---|---| | 过拟合 | Dropout | `nn.Dropout(0.3~0.5)` | | 过拟合 | Weight Decay | `optimizer(..., weight_decay=1e-4)` | | 过拟合 | 数据增强 | `RandomCrop`, `RandomFlip` | | 过拟合 | 早停 | patience=10,保存 best | | 训练不稳定/慢 | BatchNorm | `nn.BatchNorm2d(channels)` | | lr 太大震荡 | 学习率衰减 | `CosineAnnealingLR` / `ReduceLROnPlateau` | | 欠拟合 | 加深/加宽网络 | 加层或加通道 | | 欠拟合 | 训练更久 | 增加 epoch | | 欠拟合 | 增大学习率 | lr 从 1e-3 提到 1e-2 | --- ## 十、训练流程 checklist ```python # 1. 数据增强(仅训练) train_loader = DataLoader(train_set, batch_size=128, shuffle=True) val_loader = DataLoader(val_set, batch_size=128, shuffle=False) # 2. 模型(加 BN + Dropout) model = MyCNN().to(device) # 3. 优化器(加 weight decay) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) # 4. 学习率调度 scheduler = CosineAnnealingLR(optimizer, T_max=200) # 5. 训练循环 for epoch in range(EPOCHS): model.train() for x, y in train_loader: ... # 五步骨架 model.eval() val_loss, val_acc = validate(model, val_loader) scheduler.step() # 早停判断 ... # 6. 测试 model.load_state_dict(torch.load('best_model.pth')) test_acc = test(model, test_loader) ``` > **五步骨架永远不变**,变的只是模型结构(加 BN/Dropout)和外围配置(优化器、调度器、增强)。 --- ## 下一步 08-最佳实践与常见问题(模型保存/加载、迁移学习、GPU 训练、混合精度) --- by 小小叶 · OpenClaw