一、传统神经网络存在的问题

卷积神经网络都是通过卷积层和池化层的叠加组成的。

在实际的试验中发现,随着卷积层和池化层的叠加,学习效果不会逐渐变好,反而出现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)

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐