迁移学习基于预训练ResNet网络的102种花卉分类任务(对唐宇迪课程的笔记和总结)<2>
接上篇文章
三.模型搭建和参数设置
经过数据预处理之后,我们接下来就可以搭建我们的ResNet了,但是由于算力有限,并且为了节约时间,提高训练效果,我们会利用迁移学习来加速我们的分类任务
迁移学习
所谓迁移学习就是利用别人已有的模型及训练好的参数,在其基础上微调,让其适合我们自己的任务的一种做法,本文将利用pytorch中已经封装好的ResNet进行训练
# 迁移学习:
# 当我们数据量小模型难以训练时,恰好之前有人训练过类似的项目,我们可以把别人的权重参数拿过来作为自己的初始化,可以加快训练
model_name = 'resnet'
# 是否用别人训练好的特征来做
feature_extract = True
以上是一些参数的提前初始化,具体用途后面会提到
# 是否用gpu训练
train_on_gpu = torch.cuda.is_available()
if not train_on_gpu:
print("CUDA is not available, training on CPU")
else :
print("CUDA is available, training on GPU")
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
这里是深度学习中常见的GPU设置模块
train_on_gpu = torch.cuda.is_available()
这条语句会返回电脑上自带的GPU是否能够正常运作
如果没有GPU或者GPU有问题,将会在CPU上训练
网络冻结
def set_parameter_requires_grad(model, feature_extracting):
if feature_extracting:
for param in model.parameters():
param.requires_grad = False
# 这段代码接受两个参数,一个是模型本身,一个是bool值,如果传入True,意味着这个模型只用于特征提取,不更新其参数
# 作用就是冻结模型,是迁移学习中很常见的情况
这里定义了一个函数,这个函数可以接受一个模型和一个布尔值,通过参数的设置,我们可以选择性的设置一些模型的requires_grad属性值,这个值在pytorch中是是否要计算梯度的意思,如果设置为了False,那么pytorch在就不会对他计算梯度,也不会更新其参数,直观上可以看作是把模型参数冻结了
这里博主解释一下冻结参数的原因:
因为深度学习是很耗时间和算力的,pytorch中封装好的ResNet层数是很多很深的,如果不适当的冻结一部分参数,训练难度会很高(算力很高的伙伴们可以尝试不冻结,理论上来讲,都进行参数更新效果应该会更好)博主仅仅对全连接层进行参数更新,其它层完全冻结,25次迭代,GPU依旧要一个多小时!
创建模型
model_ft = models.resnet152()
models是pytorch自带的一个包,里面封装了很多经典的网络,其中152意思是152层的意思,这里我们就创建了一个ResNet152网络
初始化模型
def initialize_model(model_name,num_classes, feature_extract,use_pretrained = True):
model_ft = None
input_size = 0 # 一开始的初始化
if model_name == 'resnet':
'''
resnet152
'''
model_ft = models.resnet152(pretrained = use_pretrained)
set_parameter_requires_grad(model_ft, feature_extract) # 冻结参数
num_ftrs = model_ft.fc.in_features # 获得原来模型最后全连接层输入的维度
model_ft.fc = nn.Sequential(nn.Linear(num_ftrs, num_classes), nn.LogSoftmax(dim=1))
# 将原来模型的全连接层改为自己的全连接层
input_size = 224
# 后面还可以用elif指定调用其他模型,如'alexnet''vgg'等,这里不再全部展示,resnet是现在主流的其他比较老了
return model_ft, input_size
这里是定义了一个用于初始化model的函数,model_name是我们要使用的内置的网络的名称,num_classes是类别的个数, feature_extract是是否要冻结参数,use_pretrained是是否要使用内置网络已经预训练过的参数,我们默认是使用的
这里关键就是对内置网络的一些小改动,因为预训练网络本身不是做102分类任务的,所以我们要把他变成适应我们任务的模型,也就是我们需要改变原先的全连接层,为了保证网络的正确性,我们的全连接层的输入维度要和他原来的一致,因此我们用model_ft.fc.in_features获取别人全连接层的输入维度,然后我们把别人的全连接层替换为我们自己的
model_ft.fc = nn.Sequential(nn.Linear(num_ftrs, num_classes), nn.LogSoftmax(dim=1))
后面的LogSoftmax是一种激活函数,其实也可以不激活,直接在后面使用nn.CrossEntropyLoss()来作为损失函数,如果已经用LogSoftmax激活了,后面直接用NLLoss作为损失函数,因为本质上nn.CrossEntropyLoss()就是二者的结合
至于input_size,因为我们预处理时指定的是224,因此初始化为224,还有一层原因是主流的模型大多用224的输入维度,内置的ResNet自然也是如此
决定具体训练哪些层
# 设置那些层需要训练
model_ft , input_size = initialize_model(model_name,102,feature_extract,use_pretrained=True)
# GPU计算
model_ft = model_ft.to(device)
# 模型保存
filename = 'checkpoint.pth'
# 是否训练所有层
params_to_update = model_ft.parameters() # 这相当于直接将原来模型的所有层都拿出来了
# 个人感觉这一步多余了
print('Params to learn:')
if feature_extract:
params_to_update = [] # 如果要冻结迁移的模型的参数,那么列表就设置为空
for name,param in model_ft.named_parameters(): # 再检查所有的层和参数,需要更新的参数就会加入列表
if param.requires_grad:
params_to_update.append(param)
print('\t',name)
else:
for name,param in model_ft.named_parameters():
if param.requires_grad:
print('\t',name)
这里是确定具体训练哪些层
首先是使用上面的函数初始化模型,然后是转移到GPU,后面是后面模型保存的预先准备,指定了一个文件名
具体作用后面会讲述
后面的代码主要是输出需要更新参数的层的名字
这里代码有一处让博主困惑的地方,params_to_update这个参数一开始赋值为所有层,后面仅仅在一个if分支里用到了,而且还是将其改为空列表,一开始的初始化操作似乎是没意义的,这里或许是唐老师多此一举了,博主觉得直接初始化为空列表即可
后面是对所有层进行遍历,如果有个层,是需要梯度的就把他加入列表,并输出他的名字
这段代码出现了两处拿出所有层的操作,分别是
model_ft.parameters()
model_ft.named_parameters()
区别主要是没name的只是拿出了所有层,遍历时不包括名字,另一个包括名字,仅此而已
以上是输出的结果,实际上我们只训练自己定义的全连接层,只更新权重和偏置,0表示第一个全连接层,我们只有一个全连接层,因此就是我们定义的那个
损失函数和优化器
from torch import optim
# 优化器设置
optimizer = optim.Adam(params_to_update, lr=0.01)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1) # 学习率每七个epoch就衰减为原来的十分之一
# 最后一层已经LogSoftmax()了,所以不能nn.CrossEntropyLoss()来计算了,nn.CrossEntropyLoss()相当于LogSoftmax()和nn.NLLoss的结合
criterion = nn.NLLLoss()
这里就是常见的损失函数和优化器的设置,scheduler是用于更新学习率的
具体的训练和验证模块
import copy
import time
# 训练模块
def train_model(model, dataloaders, criterion, optimizer,num_epochs=25,is_inception=False,filename = filename): # is_inception意思是要不要添加额外的网络,一般直接用resnet就够了,添加新的比较麻烦,效果也不一定好
since = time.time()
best_acc = 0 # 最好的准确率
'''
checkpoint = torch.load(filename)
best_acc = checkpoint['best_acc']
model.load_state_dict(checkpoint['state_dict'])
optimizer.load_state_dict(checkpoint['optimizer'])
model.class_to_idx = checkpoint['mapping']
'''
model.to(device) # 转移到GPU
val_acc_history = [] # 存放每次验证集正确率的列表
train_acc_history = []# 存放每次训练集正确率的列表
train_losses = []# 存放每次训练集损失的列表
val_losses = []# 存放每次验证集损失的列表
LRs = [optimizer.param_groups[0]['lr']] # 这是一个列表用于存放每次的学习率,这里先把初始学习率添加进去了
best_model_wts = copy.deepcopy(model.state_dict())
# 在我们这个模型中,是每训练一次就进行一次验证
for epoch in range(num_epochs): # 训练次数
print('Epoch {}/{}'.format(epoch, num_epochs-1))
print('-' * 10)
# 训练和验证
for phase in ['train', 'valid']:
if phase == 'train':
model.train() # 训练
else:
model.eval() # 验证
running_loss = 0.0
running_corrects = 0
# 遍历数据
for inputs, labels in dataloaders[phase]:
inputs, labels = inputs.to(device), labels.to(device) # 转移到GPU
# 梯度清零
optimizer.zero_grad()
# 只有训练的时候计算和更新梯度
with torch.set_grad_enabled(phase == 'train'):
if is_inception and phase == 'train': # 这段是考虑添加了其他网络inception的处理逻辑
outputs,aux_outputs = model(inputs)
loss1 = criterion(outputs, labels)
loss2 = criterion(aux_outputs, labels)
loss = loss1 + loss2*0.4
else: # resnet实际执行的是这组
outputs = model(inputs)
loss = criterion(outputs, labels)
_, preds = torch.max(outputs, 1) # 当前预测值中最大的类别
# 训练阶段更新参数
if phase == 'train':
loss.backward()
optimizer.step()
# 计算损失
running_loss += loss.item() * inputs.size(0)
# inputs.size(0): inputs 是一个输入数据的张量,通常在训练中代表一个批次的样本。.size(0) 返回张量的第一个维度的大小,这通常对应于批次中的样本数量。
running_corrects += torch.sum(preds == labels.data)
epoch_loss = running_loss / len(dataloaders[phase].dataset) # 计算平均损失
epoch_acc = running_corrects.double() / len(dataloaders[phase].dataset) # 计算正确率
time_elapsed = time.time() - since # 计算花费的时间
print('Time elapsed: {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
# 得到最好的那次模型
if phase == 'valid' and epoch_acc > best_acc: # 正确率比之前高了,记录新的最高正确率
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
state = {
'state_dict': model.state_dict(),
'best_acc': best_acc,
'optimizer': optimizer.state_dict()
}
torch.save(state, filename)
if phase == 'valid':
val_acc_history.append(epoch_acc)
val_losses.append(epoch_loss)
scheduler.step(epoch_loss) # 根据验证集的结果适当调整下一次训练时的学习率
if phase == 'train':
train_acc_history.append(epoch_acc) # 训练时我们是不调整学习率的
train_losses.append(epoch_loss)
print('Optimizer learning rate: {:.7f}'.format(optimizer.param_groups[0]['lr'])) # 打印当前的学习率
LRs.append(optimizer.param_groups[0]['lr'])# 把这次的学习率添加进列表
print()
time_elapsed = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('Best val Acc: {:4f}'.format(best_acc))
# 训练完用最好的一次当做模型的最终结果
model.load_state_dict(best_model_wts)
return model, val_acc_history, train_acc_history, val_losses,train_losses, LRs
这块博主对几个可能有些不容易理解的地方解释一下
since = time.time()
这是python中用于计时的函数,主要是为了打印训练时长
'''
checkpoint = torch.load(filename)
best_acc = checkpoint['best_acc']
model.load_state_dict(checkpoint['state_dict'])
optimizer.load_state_dict(checkpoint['optimizer'])
model.class_to_idx = checkpoint['mapping']
'''
这块是用于加载我们训练好后得到的参数的,在当前可以先忽略
if phase == 'valid' and epoch_acc > best_acc: # 正确率比之前高了,记录新的最高正确率
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
state = {
'state_dict': model.state_dict(),
'best_acc': best_acc,
'optimizer': optimizer.state_dict()
}
torch.save(state, filename)
-
if phase == 'valid' and epoch_acc > best_acc:
- 这是一个条件语句,用于检查当前阶段(
phase
)是否是验证阶段('valid'
),并且当前的周期(epoch
)准确率(epoch_acc
)是否超过了迄今为止记录的最佳准确率(best_acc
)。 - 如果条件为真,说明模型在当前的验证集上的表现比之前任何时候都好。
- 这是一个条件语句,用于检查当前阶段(
-
best_acc = epoch_acc
- 如果上述条件为真,就更新最佳准确率(
best_acc
)为当前周期的准确率(epoch_acc
)。
- 如果上述条件为真,就更新最佳准确率(
-
best_model_wts = copy.deepcopy(model.state_dict())
- 这行代码使用 Python 的
copy.deepcopy
函数来创建模型当前状态字典(model.state_dict()
)的一个深拷贝,并将这个拷贝赋值给best_model_wts
变量。 model.state_dict()
返回一个包含模型所有参数的字典。使用深拷贝是为了确保保存的模型参数不会在后续的训练过程中被修改。
- 这行代码使用 Python 的
-
state = { ... }
-
这行代码创建了一个名为
state
的字典,用于存储模型训练过程中的重要状态信息。这个字典包括:
'state_dict'
: 模型当前的状态字典,包含模型的参数。'best_acc'
: 当前记录的最佳准确率。'optimizer'
: 优化器的状态字典,包含优化器的参数和状态。
-
-
torch.save(state, filename)
- 这行代码使用 PyTorch 的
torch.save
函数将state
字典保存到文件中。filename
是保存文件的名称或路径。 - 这个保存的文件包含了模型的参数、最佳准确率和优化器的状态,可以在以后的训练中加载这些信息,或者用于模型的评估和测试。
- 这行代码使用 PyTorch 的
我们之前定义的filename就是为这里准备的
model.load_state_dict(best_model_wts)
这里是在彻底完成所有轮次训练和验证后,将模型参数重新调整为效果最好的那一次的参数,用于后面我们可能存在的一些进一步的测试
开始训练和验证
# 开始训练
model_ft,val_acc_history,train_acc_history,valid_losses,train_losses,LRs = train_model(model_ft,dataloaders,criterion,optimizer,num_epochs=25,is_inception=False,filename = filename)
这里直接调用上面定义好的训练和验证模块就可以开始训练了,同时期间一些结果和数据也会在训练后反馈给我们
下面是博主的最好的一次训练结果(总共花了一个多小时)
Epoch 21/24
----------
Time elapsed: 56m 48s
train Loss: 4.9846 Acc: 0.8214
Time elapsed: 57m 1s
valid Loss: 3.9935 Acc: 0.8961
Optimizer learning rate: 0.0100000
在验证集上达到了几乎90%的正确率
on,optimizer,num_epochs=25,is_inception=False,filename = filename)
这里直接调用上面定义好的训练和验证模块就可以开始训练了,同时期间一些结果和数据也会在训练后反馈给我们
下面是博主的最好的一次训练结果(总共花了一个多小时)
Epoch 21/24
Time elapsed: 56m 48s
train Loss: 4.9846 Acc: 0.8214
Time elapsed: 57m 1s
valid Loss: 3.9935 Acc: 0.8961
Optimizer learning rate: 0.0100000
在验证集上达到了几乎90%的正确率
本篇文章到此就结束了,感谢您的阅读!