同时训练两个模型遇到了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])