同时训练两个模型遇到了bug,记录今天由这个bug引发一系列bug的过程
使用resnet18进行图像分类
1 2 3 4 5 6 7
| model_ft = models.resnet18(pretrained=True) num_ftrs = model_ft.fc.in_features model_ft.fc = nn.Linear(num_ftrs, len(class_names)) model_ft = model_ft.to(device) cross_entropy_loss = nn.CrossEntropyLoss() optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-3, momentum=0.9) exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=5, gamma=0.1)
|
单独训练这个模型在我的数据集的正确率在0.96左右
需求:同时跑两个一样模型
于是我定义了一个函数,使用deepcopy复制得到全新的模型,并且在train函数中同时跑这两个模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| def train_model(models, num_epochs=10): for epoch in range(num_epochs): for phase in ['train', 'val']: for inputs, labels, weights in dataloaders[phase]: inputs = inputs.to(device) labels = labels.to(device) weights = weights.to(device) for model in models: model['optimizer'].zero_grad() with torch.set_grad_enabled(phase == 'train'): outputs = models['model'](inputs) preds = outputs.argmax(1) loss = model['criterion'](outputs, labels) if phase == 'train': loss.backward() model['optimizer'].step()
def get_model(name): model = copy.deepcopy(model_ft) criterion = copy.deepcopy(cross_entropy_loss) optimizer = copy.deepcopy(optimizer_ft) scheduler = copy.deepcopy(exp_lr_scheduler) return {'name': name, 'model': model, 'criterion': criterion, 'optimizer': optimizer, 'scheduler': scheduler}
model1, model2 = get_model('model1'), get_model('model2') train_model(models=[model1, model2], num_epochs=num_epochs)
|
按道理,这样两个模型在训练过程中应该是完全一样的,正确率也应该是相同的
但实际上,这样训练得到的两个模型并不相同,正确率不同,而且正确率只有0.04左右
于是,我们思考会不会是deepcopy的问题,于是我们尝试不使用deepcopy函数:
1 2 3 4 5 6 7 8 9
| def get_model(name): model_ft = models.resnet18(pretrained=True) num_ftrs = model_ft.fc.in_features model_ft.fc = nn.Linear(num_ftrs, len(class_names)) model_ft = model_ft.to(device) cross_entropy_loss = nn.CrossEntropyLoss() optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-3, momentum=0.9) exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=5, gamma=0.1) return {'name': name, 'model': model_ft, 'criterion': cross_entropy_loss, 'optimizer': optimizer_ft, 'scheduler': exp_lr_scheduler}
|
结果显示正确率恢复正常了,0.96左右
但是两个模型的正确率还是不同,于是我们认为是随机种子的问题,于是:
1 2 3 4 5 6 7 8 9 10 11
| def get_model(name): seed = 42 torch.manual_seed(seed) model_ft = models.resnet18(pretrained=True) num_ftrs = model_ft.fc.in_features model_ft.fc = nn.Linear(num_ftrs, len(class_names)) model_ft = model_ft.to(device) cross_entropy_loss = nn.CrossEntropyLoss() optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-3, momentum=0.9) exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=5, gamma=0.1) return {'name': name, 'model': model_ft, 'criterion': cross_entropy_loss, 'optimizer': optimizer_ft, 'scheduler': exp_lr_scheduler}
|
修改后,发现还是不行。后来,我们想到了可能是cuda的随机性算法的问题:
1
| torch.backends.cudnn.deterministic = True
|
这个设置会强制 cuDNN 使用确定性的算法,以确保相同的输入能够产生相同的输出。
至此,两个模型的正确率相同,并且正常(0.96左右),我们已经实现了需求
所以deepcopy不行?
但是,我们还是不知道为什么deepcopy函数不行,deepcopy是python的内置函数,不应该啊?
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| model_ft = models.resnet18(pretrained=True) num_ftrs = model_ft.fc.in_features model_ft.fc = nn.Linear(num_ftrs, len(class_names)) model_ft = model_ft.to(device) cross_entropy_loss = nn.CrossEntropyLoss() optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-3, momentum=0.9) exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=5, gamma=0.1)
def get_model(name): model = copy.deepcopy(model_ft) criterion = copy.deepcopy(cross_entropy_loss) optimizer = copy.deepcopy(optimizer_ft) scheduler = copy.deepcopy(exp_lr_scheduler) return {'name': name, 'model': model, 'criterion': criterion, 'optimizer': optimizer, 'scheduler': scheduler}
|
思考着这个问题,我们找到了这个帖子
问题出在了这:
1 2 3
| optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-3, momentum=0.9) def get_model(name): optimizer = copy.deepcopy(optimizer_ft)
|
在复制了优化器optimizer后,这个新的优化器携带的是原来模型model_ft的参数,导致新的优化器去优化原来模型model_ft
所以这里的代码写错了,如果还是想要用deepcopy,应该这样写:
1 2 3 4 5
| def get_model(name): model = copy.deepcopy(model_ft) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=1e-3, momentum=0.9) scheduler = lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)
|
至此,用deepcopy也实现了需求
番外:关于前向传播会改变模型状态的乌龙事件
前情提要:在前面的debug过程中,我们错误的deepcopy了优化器,导致deepcopy得到的新模型的优化器优化了原来的模型
1 2 3
| optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-3, momentum=0.9) def get_model(name): optimizer = copy.deepcopy(optimizer_ft)
|
所以正常来说,我们跑这个新模型和原来模型,应该只有原来模型的状态会被修改
1 2 3
| model1 = {'name': 'model1', 'model': model_ft, 'criterion': cross_entropy_loss, 'optimizer': optimizer_ft, 'scheduler': exp_lr_scheduler} model2 = get_model('model2') train_model(models=[model1, model2], num_epochs=num_epochs)
|
于是我们在训练过程中,打印新模型model2的状态:
1
| print(models[1]['name'], models[1]['model'].state_dict()['layer4.1.bn2.running_var'][:10])
|
结果发现新模型model2的状态也是在变化的,我们又一次陷入了沉思和debug的阶段
我们在train函数中一步一步排除所有会影响这个状态的代码
最终我们发现,就算只有模型前向传播,前后打印’layer4.1.bn2.running_var’的值也是不一致的
1 2 3 4
| with torch.set_grad_enabled(phase == 'train'): print(models[-1]['name'], models[-1]['model'].state_dict()['layer4.1.bn2.running_var'][:10]) outputs = model['model'](inputs) print(models[-1]['name'], models[-1]['model'].state_dict()['layer4.1.bn2.running_var'][:10])
|
难道,前向传播还会改变模型的状态吗?我们沉思,最后发现打印的这个状态有问题,'layer4.1.bn2.running_var’代表Batch Normalization(BN)层的状态
而Batch Normalization(BN)层在训练和推断阶段的行为是不同的。在训练过程中,BN 层会根据当前批次的统计信息(均值和方差)进行归一化,并更新内部的滑动平均统计信息。而在推断阶段,通常使用保存的滑动平均统计信息进行归一化。
所以在前向传播过程中,两次打印 ‘layer4.1.bn2.running_var’ 的值有差异可太正常的。
总结,我们不应该看模型bn层的状态,要想查看新模型model2的状态是否在变化,应该查看与batch无关的层,比如’layer1.0.conv1.weight’的状态,这个状态与训练过程的batch无关
1
| print(models[1]['name'], models[1]['model'].state_dict()['layer1.0.conv1.weight'][:10])
|
结果也显示,在训练过程中,新模型model2的这个状态是不变的,所以新模型的优化器确实是优化了原来的模型,并没有优化自己的模型,符合预期
番外:关于随机种子
首先,我们已经知道下面代码这样获得的两个模型训练的正确率是相同的,
1 2 3 4 5 6 7 8 9 10 11
| def get_model(name): seed = 42 torch.manual_seed(seed) model_ft = models.resnet18(pretrained=True) num_ftrs = model_ft.fc.in_features model_ft.fc = nn.Linear(num_ftrs, len(class_names)) model_ft = model_ft.to(device) cross_entropy_loss = nn.CrossEntropyLoss() optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-3, momentum=0.9) exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=5, gamma=0.1) return {'name': name, 'model': model_ft, 'criterion': cross_entropy_loss, 'optimizer': optimizer_ft, 'scheduler': exp_lr_scheduler}
|
但是如果,我们把种子设置在函数外面(全局),那么两个模型的正确率却又不一样了
我们认为原因是:把种子设置在外面的话,两个模型是在同时使用同个种子生成器;而如果种子设置在函数里,则两个模型使用各自的种子生成器(两个生成器的seed都是42)
后来,我们也实现了通过deepcopy复制得到模型:
1 2 3 4 5
| def get_model(name): model = copy.deepcopy(model_ft) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=1e-3, momentum=0.9) scheduler = lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)
|
事实上,通过deepcopy得到的两个模型,尽管没有设置种子,两个模型的正确率也是相同的
GPT回答:使用 deepcopy 得到的两个模型在训练正确率上相同,而不设置全局的随机种子也是相同的,这是因为 deepcopy 会复制模型的状态,包括模型的权重和其他参数,这就包含了模型初始化时的随机性。
后续更新
deepcopy还是有问题
把batch size设置为1后,用deepcopy方式构建model1和model2,两者在训练时的准确率不同,但在测试集上的准确率是相同的
所以deepcopy方式还是有问题,可能存在参数共享问题
但这里我也不想深究了,不用deepcopy复制模型就完事了。
设置种子后dataloader加载图片固定
首先dataloader是设置shuffle了的:
1 2 3 4 5 6
| torch.utils.data.DataLoader( dataset=image_datasets[x], batch_size=64, shuffle=True, num_workers=4, )
|
用固定的种子得到模型:
1 2 3 4 5
| def get_model(name): seed = 42 torch.manual_seed(seed) ... return model
|
这样设置后,模型每次训练得到的准确率都是相同的,即每个batch加载的数据都是固定的,所以设置种子还会导致dataloader的每个batch加载数据固定
不过问题不大,想要数据随机的话,可以手动shuffle一下:
1
| train_set, test_set = torch.utils.data.random_split(origin_image_datasets, [train_size, test_size])
|