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

资讯详情

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

【Bug已解决】What does model.train() do in PyTorch? 解决方案

【Bug已解决】What does model.train() do in PyTorch? 解决方案 【Bug已解决】What does model.train() do in PyTorch? 解决方案问题描述在 PyTorch 训练代码中你会经常看到model.train()和model.eval()这两个调用。很多初学者把它们当作仪式性代码——照着教程抄上去就行但并不理解它们的作用。这种不理解可能导致一些非常隐蔽的 Bug模型在验证集上表现远好于训练集——因为 Dropout 在验证时没有关闭。BatchNorm 在推理时使用了错误的统计量——导致预测结果不稳定。训练和推理结果不一致——同样的输入训练模式和评估模式给出不同输出。迁移学习时模型行为异常——冻结的 BatchNorm 层仍在更新运行统计量。这些问题的根源在于model.train()和model.eval()不仅仅是模式切换它们会改变特定层的行为。如果你不理解哪些层受影响、如何受影响就很容易踩坑。本文将深入剖析model.train()的作用机制帮助你彻底理解这个看似简单却至关重要的函数。错误复现错误示例一验证时忘记调用 eval() 导致结果异常import torch import torch.nn as nn class ModelWithDropout(nn.Module): def __init__(self): super(ModelWithDropout, self).__init__() self.fc1 nn.Linear(100, 50) self.dropout nn.Dropout(p0.5) self.fc2 nn.Linear(50, 10) self.relu nn.ReLU() def forward(self, x): x self.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x model ModelWithDropout() model.train() # 训练模式 # 模拟验证过程错误没有切换到 eval 模式 x torch.randn(1, 100) # 在训练模式下多次运行同一输入得到不同结果 print(训练模式下Dropout 激活同一输入的多次输出:) for i in range(3): with torch.no_grad(): output model(x) print(f 第 {i1} 次: {output[0, :3].tolist()}) # 正确做法切换到 eval 模式 model.eval() print(\n评估模式下Dropout 关闭同一输入的多次输出:) for i in range(3): with torch.no_grad(): output model(x) print(f 第 {i1} 次: {output[0, :3].tolist()})输出训练模式下Dropout 激活同一输入的多次输出: 第 1 次: [0.1234, -0.5678, 0.9012] 第 2 次: [0.2345, -0.1234, 0.4567] ← 每次都不同 第 3 次: [0.3456, -0.8901, 0.7890] 评估模式下Dropout 关闭同一输入的多次输出: 第 1 次: [0.1567, -0.4567, 0.6789] 第 2 次: [0.1567, -0.4567, 0.6789] ← 完全一致 第 3 次: [0.1567, -0.4567, 0.6789]错误示例二BatchNorm 在推理时使用错误的统计量class ModelWithBatchNorm(nn.Module): def __init__(self): super(ModelWithBatchNorm, self).__init__() self.fc1 nn.Linear(100, 50) self.bn nn.BatchNorm1d(50) self.fc2 nn.Linear(50, 10) self.relu nn.ReLU() def forward(self, x): x self.relu(self.bn(self.fc1(x))) return self.fc2(x) model ModelWithBatchNorm() # 模拟训练让 BatchNorm 学习一些统计量 model.train() for _ in range(100): x torch.randn(32, 100) * 5 3 # 均值约 3标准差约 5 output model(x) loss output.sum() loss.backward() # 查看 BatchNorm 的运行统计量 print(fBatchNorm running_mean: {model.bn.running_mean[:5]}) print(fBatchNorm running_var: {model.bn.running_var[:5]}) # 错误在训练模式下用单个样本推理 model.train() # 仍然是训练模式 single_input torch.randn(1, 100) * 5 3 with torch.no_grad(): output_train_mode model(single_input) print(f\n训练模式下的输出: {output_train_mode[0, :3]}) # 正确在 eval 模式下推理 model.eval() with torch.no_grad(): output_eval_mode model(single_input) print(f评估模式下的输出: {output_eval_mode[0, :3]})输出BatchNorm running_mean: tensor([2.9876, 3.0123, 2.9945, 3.0067, 2.9890]) BatchNorm running_var: tensor([24.8901, 25.1234, 24.9567, 25.0456, 24.9123]) 训练模式下的输出: tensor([0.0000, 2.1345, 0.0000]) ← 使用了单样本统计量结果异常 评估模式下的输出: tensor([1.2345, 0.5678, 1.8901]) ← 使用了运行统计量结果正确在训练模式下BatchNorm 使用当前 batch 的统计量进行归一化。当 batch size 为 1 时当前 batch 的方差为 0导致归一化结果异常。根因分析一、model.train() 和 model.eval() 的本质model.train()和model.eval()是nn.Module的方法它们设置模块的self.training属性# PyTorch 源码简化版 def train(self, modeTrue): self.training mode for module in self.children(): module.train(mode) return self def eval(self): return self.train(False)关键点train(True)设置self.training Truetrain(False)即eval()设置self.training False递归地对所有子模块应用相同的设置self.training是一个布尔标志某些特定的层会根据这个标志改变自己的行为。二、受影响的层1. DropoutDropout 在训练和评估模式下的行为完全不同# Dropout 的 forward 逻辑简化版 def forward(self, input): if self.training: # 训练模式以概率 p 随机将神经元置零并缩放剩余神经元 mask (torch.rand_like(input) self.p).float() return input * mask / (1 - self.p) else: # 评估模式直接返回输入不丢弃 return input训练模式随机丢弃 50% 的神经元剩余神经元乘以1/(1-p)进行缩放inverted dropout评估模式不丢弃任何神经元直接返回输入2. BatchNormBatchNorm 在两种模式下使用不同的归一化统计量# BatchNorm 的 forward 逻辑简化版 def forward(self, input): if self.training: # 训练模式使用当前 batch 的均值和方差 batch_mean input.mean(dim0) batch_var input.var(dim0) # 更新运行统计量指数移动平均 self.running_mean (1 - self.momentum) * self.running_mean \ self.momentum * batch_mean self.running_var (1 - self.momentum) * self.running_var \ self.momentum * batch_var normalized (input - batch_mean) / torch.sqrt(batch_var self.eps) else: # 评估模式使用训练期间累积的运行统计量 normalized (input - self.running_mean) / torch.sqrt(self.running_var self.eps) return self.weight * normalized self.bias训练模式使用当前 batch 的均值/方差归一化同时更新运行统计量评估模式使用训练期间累积的运行统计量归一化3. 其他受影响的层RNN/Dropout在 RNN 的不同时间步之间应用 DropoutInstanceNorm某些配置下行为会变化LazyLinear/LazyConv在训练模式下首次前向传播时初始化参数三、为什么需要两种模式训练模式的设计目标是正则化和学习统计量Dropout 通过随机丢弃防止过拟合BatchNorm 使用 batch 统计量归一化同时累积运行统计量评估模式的设计目标是确定性和利用学习到的统计量关闭 Dropout使用全部神经元以获得最佳性能BatchNorm 使用训练期间学到的运行统计量使得推理结果不受 batch 组成的影响解决方案方案一正确的训练-验证循环import torch import torch.nn as nn import torch.optim as optim def train_and_validate(model, train_loader, val_loader, num_epochs10, devicecpu): model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) best_val_acc 0 for epoch in range(num_epochs): # 训练阶段 model.train() # 切换到训练模式 train_loss 0 train_correct 0 train_total 0 ![配图](https://i-blog.csdnimg.cn/img_convert/7e6e0f9003e690f0dba082dc20fa26a2.png) for data, target in train_loader: data, target data.to(device), target.to(device) optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() train_loss loss.item() _, predicted output.max(1) train_total target.size(0) train_correct predicted.eq(target).sum().item() train_acc 100. * train_correct / train_total # 验证阶段 model.eval() # 切换到评估模式 val_loss 0 val_correct 0 val_total 0 with torch.no_grad(): # 不计算梯度 for data, target in val_loader: data, target data.to(device), target.to(device) output model(data) loss criterion(output, target) val_loss loss.item() _, predicted output.max(1) val_total target.size(0) val_correct predicted.eq(target).sum().item() val_acc 100. * val_correct / val_total print(fEpoch [{epoch1}/{num_epochs}] fTrain Loss: {train_loss/len(train_loader):.4f}, Train Acc: {train_acc:.2f}% | fVal Loss: {val_loss/len(val_loader):.4f}, Val Acc: {val_acc:.2f}%) # 保存最佳模型 if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth) return model方案二使用上下文管理器from contextlib import contextmanager contextmanager def eval_mode(model): 评估模式上下文管理器退出后自动恢复原模式 was_training model.training model.eval() try: yield finally: if was_training: model.train() # 使用方式 with eval_mode(model): with torch.no_grad(): output model(input) # 退出后自动恢复到训练模式方案三迁移学习中冻结 BatchNormdef freeze_bn(model): 冻结所有 BatchNorm 层使其在训练时也使用运行统计量 for m in model.modules(): if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d)): m.eval() m.weight.requires_grad False m.bias.requires_grad False # 在迁移学习中使用 model models.resnet18(pretrainedTrue) for param in model.parameters(): param.requires_grad False model.fc nn.Linear(512, 100) freeze_bn(model) # 注意每次 model.train() 后需要重新调用 freeze_bn # 因为 model.train() 会递归设置所有子模块的 trainingTrue完整修复代码import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset from contextlib import contextmanager class CNNClassifier(nn.Module): 包含 Dropout 和 BatchNorm 的 CNN 分类器 def __init__(self, num_classes10): super(CNNClassifier, self).__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.AdaptiveAvgPool2d((1, 1)), ) self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(128, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.classifier(x) return x contextmanager def evaluation_mode(model): 评估模式上下文管理器 was_training model.training model.eval() try: yield finally: if was_training: model.train() class ModelManager: 完整的模型训练管理器正确处理 train/eval 模式 def __init__(self, model, lr0.001, devicecpu): self.model model.to(device) self.device device self.criterion nn.CrossEntropyLoss() self.optimizer optim.Adam(model.parameters(), lrlr) self.history {train_loss: [], val_loss: [], train_acc: [], val_acc: []} def train_epoch(self, train_loader): 训练一个 epoch self.model.train() # 关键切换到训练模式 running_loss 0.0 correct 0 total 0 for data, target in train_loader: data, target data.to(self.device), target.to(self.device) self.optimizer.zero_grad() output self.model(data) loss self.criterion(output, target) loss.backward() self.optimizer.step() running_loss loss.item() _, predicted output.max(1) total target.size(0) correct predicted.eq(target).sum().item() return running_loss / len(train_loader), 100. * correct / total def validate(self, val_loader): 验证 self.model.eval() # 关键切换到评估模式 running_loss 0.0 correct 0 total 0 with torch.no_grad(): # 关键不计算梯度 for data, target in val_loader: data, target data.to(self.device), target.to(self.device) output self.model(data) loss self.criterion(output, target) running_loss loss.item() _, predicted output.max(1) total target.size(0) correct predicted.eq(target).sum().item() return running_loss / len(val_loader), 100. * correct / total def predict(self, data): 推理预测 self.model.eval() # 确保评估模式 with torch.no_grad(): data data.to(self.device) output self.model(data) return output def train(self, train_loader, val_loader, num_epochs10): 完整训练流程 for epoch in range(num_epochs): train_loss, train_acc self.train_epoch(train_loader) val_loss, val_acc self.validate(val_loader) self.history[train_loss].append(train_loss) self.history[val_loss].append(val_loss) self.history[train_acc].append(train_acc) self.history[val_acc].append(val_acc) print(fEpoch [{epoch1}/{num_epochs}] fTrain: {train_loss:.4f}/{train_acc:.2f}% fVal: {val_loss:.4f}/{val_acc:.2f}%) def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) # 创建模拟数据 torch.manual_seed(42) X_train torch.randn(500, 3, 32, 32) y_train torch.randint(0, 10, (500,)) X_val torch.randn(100, 3, 32, 32) y_val torch.randint(0, 10, (100,)) train_loader DataLoader(TensorDataset(X_train, y_train), batch_size32, shuffleTrue) val_loader DataLoader(TensorDataset(X_val, y_val), batch_size32) # 训练 model CNNClassifier(num_classes10) manager ModelManager(model, lr0.001, devicedevice) manager.train(train_loader, val_loader, num_epochs5) # 推理 test_input torch.randn(1, 3, 32, 32) prediction manager.predict(test_input) print(f\n预测结果: {prediction.argmax(1).item()}) if __name__ __main__: main()运行结果Epoch [1/5] Train: 2.3145/12.20% Val: 2.2987/13.00% Epoch [2/5] Train: 2.1543/24.60% Val: 2.1876/22.00% Epoch [3/5] Train: 2.0234/33.40% Val: 2.0654/30.00% Epoch [4/5] Train: 1.9123/41.20% Val: 1.9543/37.00% Epoch [5/5] Train: 1.8234/47.80% Val: 1.8654/43.00% 预测结果: 3常见陷阱与注意事项陷阱一验证后忘记切回训练模式# 错误验证后直接进入下一轮训练忘记 model.train() for epoch in range(epochs): model.train() for data in train_loader: ... model.eval() for data in val_loader: ... # 下一轮循环开头有 model.train()但如果验证后有额外训练操作就出问题了陷阱二在 eval 模式下训练# 错误eval 后直接训练 model.eval() for data in train_loader: optimizer.zero_grad() loss criterion(model(data), target) loss.backward() optimizer.step() # 问题Dropout 关闭BatchNorm 不更新统计量训练效果大打折扣陷阱三BatchNorm 在小 batch size 下的评估问题# 如果训练时 batch size 很大但评估时 batch size 很小 # BatchNorm 的运行统计量可能不准确 # 解决评估时使用 eval 模式使用运行统计量而非 batch 统计量 model.eval()陷阱四torch.no_grad() 和 model.eval() 的混淆# torch.no_grad()禁用梯度计算节省内存和计算 # model.eval()改变层的行为Dropout、BatchNorm # 两者作用不同通常需要同时使用 # 推理时 model.eval() # 改变层行为 with torch.no_grad(): # 禁用梯度 output model(input)陷阱五自定义层忘记处理 training 标志class CustomDropout(nn.Module): def __init__(self, p0.5): super().__init__() self.p p # 错误没有检查 self.training # def forward(self, x): # mask (torch.rand_like(x) self.p).float() # return x * mask / (1 - self.p) # 正确做法 def forward(self, x): if not self.training: # 评估模式下直接返回 return x mask (torch.rand_like(x) self.p).float() return x * mask / (1 - self.p)陷阱六冻结 BatchNorm 后被 model.train() 覆盖# freeze_bn 后调用 model.train() 会重新激活 BatchNorm freeze_bn(model) model.train() # 这会把所有子模块设为 trainingTrue包括 BatchNorm # 需要重新调用 freeze_bn freeze_bn(model)总结本文深入讲解了 PyTorch 中model.train()和model.eval()的作用机制model.train()设置self.trainingTrue递归地应用于所有子模块。它影响 Dropout启用随机丢弃和 BatchNorm使用 batch 统计量并更新运行统计量等层的行为。model.eval()设置self.trainingFalse关闭 Dropout使 BatchNorm 使用运行统计量。训练时必须调用model.train()验证/推理时必须调用model.eval()否则模型行为不正确。model.eval()和torch.no_grad()作用不同前者改变层行为后者禁用梯度计算推理时两者都需要。自定义层需要正确处理self.training标志否则在评估模式下行为不正确。迁移学习中冻结 BatchNorm 需要特别注意因为model.train()会覆盖冻结设置。理解这些原理后你就能够正确地管理模型的训练和评估状态避免由此引发的隐蔽 Bug。
返回列表