迁移学习——基于ResNet网络(附案例)
ResNet 网络是在 2015年 由微软实验室中的何凯明等几位大神提出,斩获当年ImageNet竞赛中分类任务第一名,目标检测第一名。获得COCO数据集中目标检测第一名,图像分割第一名。Resnet在cnn 图像方面有着非常突出的表现,它利用 shortcut 短路连接,解决了深度网络中模型退化的问题。相比普通网络每两层/三层之间增加了短路机制,通过残差学习使深层的网络发挥出作用。如何解决传统神
一、传统神经网络存在的问题
卷积神经网络都是通过卷积层和池化层的叠加组成的。
在实际的试验中发现,随着卷积层和池化层的叠加,学习效果不会逐渐变好,反而出现2个问题:
1、梯度消失和梯度爆炸
梯度消失:若每一层的误差梯度小于1,反向传播时,网络越深,梯度越趋近于0
梯度爆炸:若每一层的误差梯度大于1,反向传播时,网络越深,梯度越来越大
2、退化问题
当网络加深到一定程度后,训练变得困难,模型性能趋于饱和甚至显著下降。
那么该怎么解决这两个问题呢?
这里采用ResNet网络来解决这些问题。
二、ResNet网络
简介
ResNet 网络是在 2015年 由微软实验室中的何凯明等几位大神提出,斩获当年ImageNet竞赛中分类任务第一名,目标检测第一名。获得COCO数据集中目标检测第一名,图像分割第一名。
Resnet在cnn 图像方面有着非常突出的表现,它利用 shortcut 短路连接,解决了深度网络中模型退化的问题。相比普通网络每两层/三层之间增加了短路机制,通过残差学习使深层的网络发挥出作用。
如何解决传统神经网络存在的问题的呢?
方案:
为了解决梯度消失或梯度爆炸问题,论文提出通过数据的预处理以及在网络中使用 BN(Batch Normalization)层来解决。
为了解决深层网络中的退化问题,可以人为地让神经网络某些层跳过下一层神经元的连接,隔层相连,弱化每层之间的强联系。这种神经网络被称为 残差网络 (ResNets)。

残差结构
residual结构使用了一种shortcut的连接方式,也可理解为捷径。让特征矩阵隔层相加,注意F(X)和X形状要相同,所谓相加是特征矩阵相同位置上的数字进行相加。

它添加了一个短路连接到第二层激活函数之前。那么激活函数的输入就由原来的输出H(x)=F(x)变为了H(x)=F(x)+x。在RestNet中,这种输出=输入的操作成为恒等映射,通过这种操作,使得网络在最差的情况下也能获得和输入一样的输出,即增加的层什么也不学习,仅仅复制输入的特征,至少使得网络不会出现退化的问题
在resnet结构中,主分支与shortcut的输出特征矩阵shape必须相同,在捷径分支上通过1x1的卷积核进行降维处理,并通过设置步长为2来改变分辨率,最终实现维度的匹配
ResNet的网络结构图如下:

观察结构图发现:
都是一个7x7的卷积层,然后是一个3x3的最大池化下采样。
然后按照图中的conv2_x、conv3_x、conv4_x、conv5_x中的残差结构。
最后再跟一个平均池化下采样,和全连接层,sofmax输出。
注意:
REsNet网络采用的默认输入尺寸是(224, 224, 3),RGB图像,三通道(ImagesNet数据集)
Resnet-18网络具体示例下图所示

三、迁移学习
简介
迁移学习是指利用已经训练好的模型,在新的任务上进行微调。迁移学习可以加快模型训练速度,提高模型性能,并且在数据稀缺的情况下也能很好地工作。
步骤
1、选择预训练的模型和适当的层:通常,我们会选择在大规模图像数据集(如ImageNet)上预训练的模型,如VGG、ResNet等。然后,根据新数据集的特点,选择需要微调的模型层。对于低级特征的任务(如边缘检测),最好使用浅层模型的层,而对于高级特征的任务(如分类),则应选择更深层次的模型。
2、冻结预训练模型的参数:保持预训练模型的权重不变,只训练新增加的层或者微调一些层,避免因为在数据集中过拟合导致预训练模型过度拟合。
3、在新数据集上训练新增加的层:在冻结预训练模型的参数情况下,训练新增加的层。这样,可以使新模型适应新的任务,从而获得更高的性能。
4、微调预训练模型的层:在新层上进行训练后,可以解冻一些已经训练过的层,并且将它们作为微调的目标。这样做可以提高模型在新数据集上的性能。
5、评估和测试:在训练完成之后,使用测试集对模型进行评估。如果模型的性能仍然不够好,可以尝试调整超参数或者更改微调层。
接下来以水果分类为例
测试集如下:

分别为文件路径及对应的类别
测试集如下:

分别为文件路径及对应的类别
这里采用ResNet-18为例,只改变了全连接层输出神经元的个数由1000改为20
接下来该思考怎样不修改ResNet-18网络中除了最后的全连接层以外的所有参数呢?
通过指定ResNet-18网络中的保存权重及偏置参数(即models.resnet18)中的参数requires_grad为false,后续重新设置全连接层参数后(requires_grad为true),循环遍历判断requires_grad的值即可完成。

这里采用了数据增强及调整学习率。
import torch
import torchvision.models as models
from torch import nn
from torch.utils.data import Dataset,DataLoader
from torchvision import transforms
from PIL import Image
import numpy as np
#不再需要自己来搭建模型了,预训练的文件也加载进去了。
'''将resnet18模型迁移到食物分类项目中''' #现网络是固定的网络结构,不需要你自己来类定义了。
resnet_model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)#即调用了resnet18网络,又使用了训练好的模型
#weights=models.ResNet18_Weights.DEFAULT表示使用在 ImageNet 数据集上预先训练好的权重来初始化模型参数,可进入源代码查看
for param in resnet_model.parameters():#" #" #按层去除权重参数
print(param)
param.requires_grad = False #冻结
#模型所有参数(即权重和偏差)的requires_grad属性设置为False,从而冻结所有模型参数。
in_features = resnet_model.fc.in_features #获取模型原输入的特征个数
resnet_model.fc = nn.Linear(in_features, 20) #创建一个全连接层,输入特征为in_features,输出为20,这里水果分类共20个类别,而原来resnet共有1000个
params_to_update = [] #保存需要训练的参数,仅仅包含全连接层的参数 #" #"
for param in resnet_model.parameters():#'' #
if param.requires_grad == True: #'' #
params_to_update.append(param) #'' #
#不冻结,需要全部重头训练,注释13、14、20~23行,并修改82行代码
data_transforms = {
'train':
transforms.Compose([
transforms.Resize([300,300]), #是图像变换大小
transforms.RandomRotation(45),#随机旋转,-45到45度之间随机选
transforms.CenterCrop(224),#从中心开始裁剪,resnet要求输入图像的尺寸为3 * 224* 224
transforms.RandomHorizontalFlip(p=0.5),#随机水平翻转 选择一个概率概率
transforms.RandomVerticalFlip(p=0.5),#随机垂直翻转
# transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),#参数1为亮度,参数2为对比度,参数3
transforms.RandomGrayscale(p=0.1),#概率转换成灰度率,3通道就是R=G=B
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])#归一化,均值,标准差
]),
'valid':
transforms.Compose([
transforms.Resize([224,224]),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}
class food_dataset(Dataset): #food_dataset是自己创建的类名称,可以改为你需要的名称
def __init__(self, file_path,transform=None): #类的初始化
self.file_path = file_path
self.imgs = []
self.labels = []
self.transform = transform
with open(self.file_path) as f:
samples = [x.strip().split(' ') for x in f.readlines()]
for img_path, label in samples:
self.imgs.append(img_path)
self.labels.append(label)
def __len__(self): #类实例化对象后,可以使用len函数测量对象的个数
return len(self.imgs)
def __getitem__(self, idx): #关键,可通过索引的形式获取每一个图片数据及标签
image = Image.open(self.imgs[idx]) #
if self.transform:
image = self.transform(image)
label = self.labels[idx]
label = torch.from_numpy(np.array(label,dtype = np.int64))
return image, label
training_data = food_dataset(file_path = './trainda.txt',transform = data_transforms['train'])
test_data = food_dataset(file_path = './testda.txt',transform = data_transforms['valid'])
train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True) # 64张图片为一个包,
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
print(f"Using {device} device")
model = resnet_model.to(device) #为什么不需要加括号,之前是model = CNN().to(device)
loss_fn = nn.CrossEntropyLoss() #创建交叉熵损失函数对象,因为手写字识别中一共有20个数字,输出会有20个结果
optimizer = torch.optim.Adam(params_to_update, lr=0.001)#仅训练最后一层参数
# optimizer = torch.optim.Adam(resnet_model.parameters(), lr=0.001)#训练所有层参数?
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) #调整学习率函数
def train(dataloader, model, loss_fn, optimizer):
model.train()
for X, y in dataloader: #其中batch为每一个数据的编号
X, y = X.to(device), y.to(device) #把训练数据集和标签传入cpu或GPU
pred = model.forward(X) #自动初始化 w 权重
loss = loss_fn(pred, y) #通过交叉熵损失函数计算损失值Loss
optimizer.zero_grad() #梯度清零
loss.backward() #反向传播计算得到每个参数的梯度值
optimizer.step() #根据梯度更新网络参数
best_acc = 0
def test(dataloader, model, loss_fn):
global best_acc
size = len(dataloader.dataset)
num_batches = len(dataloader)
model.eval() #
test_loss, correct = 0, 0
with torch.no_grad(): #一个上下文管理器,关闭梯度计算。当你确认不会调用Tensor.backward()的时候,这可以减少计算所用内存消耗。
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model.forward(X)
test_loss += loss_fn(pred, y).item() #
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= num_batches
correct /= size
print(f"Test result: \n Accuracy: {(100*correct)}%, Avg loss: {test_loss}")
acc_s.append(correct)
loss_s.append(test_loss)
if correct > best_acc:
best_acc = correct
'''训练模型'''
epochs = 10
acc_s = []
loss_s = []
for t in range(epochs):
print(f"Epoch {t+1}\n-------------------------------")
train(train_dataloader, model, loss_fn, optimizer)
scheduler.step()
test(test_dataloader, model, loss_fn)
print('最优训练结果为:',best_acc)

更多推荐
所有评论(0)