ResNet学习笔记

一、什么是ResNet

1.1.ResNet的提出

残差网络(ResNet) 是由来自Microsoft Research的4位学者提出的卷积神经网络,在2015年的ImageNet大规模视觉识别竞赛(ImageNet Large Scale Visual Recognition Challenge, ILSVRC)中获得了图像分类和物体识别的优胜。

网络出自论文《Deep Residual Learning for Image Recognition》

1.2.ResNet的特性

容易优化,并且能够通过增加相当的深度来提高准确率。其内部的残差块使用了跳跃连接,缓解了在深度神经网络中增加深度带来的梯度消失问题

二、ResNet的数学原理

2.1.残差学习

在加深网络的过程中,会导致梯度消失或者梯度爆炸,对于该问题的解决方法实正则化初始化和中间的正则化层,可以训练几十层的网络。

但是随着网络层数的继续增加,准确率会下降。原因是更深层次的网络似乎训练的难度更大,因此我们应采用其他方式保证网络深度增加,提取特征增加,同时不会使得训练难度增大,也就是优化训练的方式,使得学习过程的难度随网络层数的增加变化更小。

因此我们目前的目标就是简化多层网络的学习过程。如果我们通过许多层网络拟合一个函数,是比较困难的,需要大量的解空间搜寻。但如果我们让其中的一些层只拟合上面的层的输出与真实值的差距,即只拟合残差,那么这一些层的运算就得到了相当规模的简化,这样一来,相当于一些层的目标函数是向真实值靠近,一些层的目标函数是另一些层向目标靠得更近,经过这样的训练过程优化,多层网络就变得可行了。
在这里插入图片描述

2.2.残差模块的构建

Res的残差模块构建是一个至关重要的问题。我们考虑如何学习残差,用来学习残差的网络层数应当大于1,否则退化为线性(这里不是很理解),因此我们把每一个学习残差的网络成为一个残差模块,文章中出现了2层和3层的残差模块,实际上更多层的残差模块也是可以的。具体残差模块应该由几层组成是个问题。
在这里插入图片描述

2.3.学习策略

一层拟合函数,其余层学习残差效果好;还是一些层学习拟合函数,一些层学习残差效果好?我们考虑残差学习的本源目的就是使得学习变得简洁,可以承受更深的网络层数,因此选择了一层拟合函数,其余所有层学习残差。
在这里插入图片描述

三、ResNet的网络结构

ResNet论文中结构有18层、34层、50层、101层和152层,可以看到,的确比googlenet和VGG有了明显的网络深度提升。

比较常用的ResNet深度是50层、101层和152层。其中,有结论说明网络深度的加深可以提升训练出的模型的精度,但会增加内存消耗,我的电脑就只能跑18,50就会让我买新的ram。
在这里插入图片描述

当然本质上FC层才是内存消耗大户,ResNet只有1个FC层,可以看到内存消耗上甚至是小于VGG网络的。
在这里插入图片描述

四、ResNet的pytorch实现与应用

用ResNet实现自定义数据集分类的pytorch代码

贴一段ResNet18的网络代码

import torch
import torch.nn as nn
import torch.nn.functional as F

class ResidualBlock(nn.Module):
    def __init__(self, inchannel, outchannel, stride=1):
        super(ResidualBlock, self).__init__()
        self.left = nn.Sequential(
            nn.Conv2d(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(outchannel), #BatchNorm2d最常用于卷积网络中(防止梯度消失或爆炸),设置的参数就是卷积的输出通道数
            nn.ReLU(inplace=True),
            nn.Conv2d(outchannel, outchannel, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(outchannel)
        )
        self.shortcut = nn.Sequential()
        if stride != 1 or inchannel != outchannel:
            self.shortcut = nn.Sequential(
                nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(outchannel)
            )

    def forward(self, x):
        out = self.left(x)
        out += self.shortcut(x)
        out = F.relu(out)
        return out

class ResNet(nn.Module):
    def __init__(self, ResidualBlock, num_classes=5):
        super(ResNet, self).__init__()
        self.inchannel = 64
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(),
        )
        #followed 4 blocks

        self.layer1 = self.make_layer(ResidualBlock, 64,  2, stride=1)
        self.layer2 = self.make_layer(ResidualBlock, 128, 2, stride=2)
        self.layer3 = self.make_layer(ResidualBlock, 256, 2, stride=2)
        self.layer4 = self.make_layer(ResidualBlock, 512, 2, stride=2)
        self.fc = nn.Linear(512, num_classes)

    def make_layer(self, block, channels, num_blocks, stride):
        strides = [stride] + [1] * (num_blocks - 1)   #strides=[1,1]
        layers = []
        for stride in strides:
            layers.append(block(self.inchannel, channels, stride))
            self.inchannel = channels
        return nn.Sequential(*layers)  # 不加*号,会报错 TypeError: list is not a Module subclass

    def forward(self, x):
        out = self.conv1(x)
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = F.avg_pool2d(out, 4)
        out = out.view(out.size(0), -1) #将多维数据转成一维
        out = self.fc(out)
        return out


def ResNet18():

    return ResNet(ResidualBlock)

之后是自定义数据集并进行训练的代码

数据集结构是data文件夹下面三个文件夹,每个文件夹名称代表文件夹内容的所属类别,图片大小不一,裁剪方式是完全服从ResNet论文中的224x224x3。

# coding=gbk

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import argparse
from ResNet import ResNet18

import torch
import os, glob
import random, csv
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image

# 定义是否使用GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 参数设置,使得我们能够手动输入命令行参数,就是让风格变得和Linux命令行差不多
parser = argparse.ArgumentParser(description='PyTorch CIFAR10 Training')
parser.add_argument('--outf', default='./model/', help='folder to output images and model checkpoints') #输出结果保存路径
parser.add_argument('--net', default='./model/Resnet18.pth', help="path to net (to continue training)")  #恢复训练时的模型路径
args = parser.parse_args()

# 超参数设置
EPOCH = 135   #遍历数据集次数
pre_epoch = 0  # 定义已经遍历数据集的次数
BATCH_SIZE = 256     #批处理尺寸(batch_size)
LR = 0.1        #学习率



class FireAndSmoke(Dataset):
    def __init__(self, root, resize, mode):
        super(FireAndSmoke, self).__init__()
        self.root = root
        self.resize = resize
        self.name2label = {}
        # 返回指定目录下的文件列表,并对文件列表进行排序,
        # os.listdir每次返回目录下的文件列表顺序会不一致,
        # 排序是为了每次返回文件列表顺序一致
        for name in sorted(os.listdir(os.path.join(root))):
            # 过滤掉非目录文件
            if not os.path.isdir(os.path.join(root, name)):
                continue
            # 构建字典,名字:0~4数字
            self.name2label[name] = len(self.name2label.keys())
        # eg: {'squirtle': 4, 'bulbasaur': 0, 'pikachu': 3, 'mewtwo': 2, 'charmander': 1}
        # print(self.name2label)
        # image, label
        self.images, self.labels = self.load_csv("images.csv")
        # 对数据集进行划分
        if mode == "train":  # 70%
            self.images = self.images[:int(0.7 * len(self.images))]
            self.labels = self.labels[:int(0.7 * len(self.labels))]
        else:  # 30%
            self.images = self.images[int(0.7 * len(self.images)):]
            self.labels = self.labels[int(0.7 * len(self.labels)):]
    # 将目录下的图片路径与其对应的标签写入csv文件,
    # 并将csv文件写入的内容读出,返回图片名与其标签
    def load_csv(self, filename):
        """
        :param filename:
        :return:
        """
        # 是否已经存在了cvs文件
        if not os.path.exists(os.path.join(self.root, filename)):
            images = []
            for name in self.name2label.keys():
                # 获取指定目录下所有的满足后缀的图像名
                # pokemon/mewtwo/00001.png
                images += glob.glob(os.path.join(self.root, name, "*.png"))
                images += glob.glob(os.path.join(self.root, name, "*.jpg"))
                images += glob.glob(os.path.join(self.root, name, "*.jpeg"))
            # 1165 'pokemon/pikachu/00000058.png'
            print(len(images), images)
            # 将元素打乱
            random.shuffle(images)
            with open(os.path.join(self.root, filename), mode="w", newline="") as f:
                writer = csv.writer(f)
                for img in images:  # 'pokemon/pikachu/00000058.png'
                    name = img.split(os.sep)[-2]
                    label = self.name2label[name]
                    # 将图片路径以及对应的标签写入到csv文件中
                    # 'pokemon/pikachu/00000058.png', 0
                    writer.writerow([img, label])
                print("writen into csv file: ", filename)
        # 如果已经存在了csv文件,则读取csv文件
        images, labels = [], []
        with open(os.path.join(self.root, filename)) as f:
            reader = csv.reader(f)
            for row in reader:
                # 'pokemon/pikachu/00000058.png', 0
                img, label = row
                label = int(label)
                images.append(img)
                labels.append(label)
        assert len(images) == len(labels)
        return images, labels
    def __len__(self):
        return len(self.images)
    def denormalize(self, x_hat):
        mean = [0.485, 0.456, 0.406]
        std = [0.229, 0.224, 0.225]
        # x_hat = (x-mean)/std
        # x = x_hat*std = mean
        # x: [c, h, w]
        # mean: [3] => [3, 1, 1]
        mean = torch.tensor(mean).unsqueeze(1).unsqueeze(1)
        std = torch.tensor(std).unsqueeze(1).unsqueeze(1)
        # print(mean.shape, std.shape)
        x = x_hat * std + mean
        return x
    def __getitem__(self, idx):
        # idx~[0~len(images)]
        # self.images, self.labels
        # img: 'pokemon/bulbasaur/00000000.png'
        # label: 0
        img, label = self.images[idx], self.labels[idx]
        tf = transforms.Compose([
            lambda x: Image.open(x).convert("RGB"),  # string path => image data
            transforms.Resize((int(self.resize * 1.25), int(self.resize * 1.25))),
            transforms.RandomRotation(15),
            transforms.CenterCrop(self.resize),
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.485, 0.456, 0.406],
                                 std=[0.229, 0.224, 0.225])
        ])
        img = tf(img)
        label = torch.tensor(label)
        return img, label

trainset = FireAndSmoke("data",224,"train") #训练数据集
trainloader = torch.utils.data.DataLoader(trainset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)   #生成一个个batch进行批训练,组成batch的时候顺序打乱取

testset = FireAndSmoke("data",224,"test")
testloader = torch.utils.data.DataLoader(testset, batch_size=210, shuffle=False, num_workers=2) # 多线程来读数据
# # Cifar-10的标签
# classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

# 模型定义-ResNet
net = ResNet152().to(device)

# 定义损失函数和优化方式
criterion = nn.CrossEntropyLoss()  #损失函数为交叉熵,多用于多分类问题
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9, weight_decay=5e-4) #优化方式为mini-batch momentum-SGD,并采用L2正则化(权重衰减)

# 训练
if __name__ == "__main__":
    best_acc = 85  #2 初始化best test accuracy
    print("Start Training, Resnet!")  # 定义遍历数据集的次数
    with open("acc.txt", "w") as f:
        with open("log.txt", "w")as f2:
            for epoch in range(pre_epoch, EPOCH):
                print('\nEpoch: %d' % (epoch + 1))
                net.train()
                # model.train() :启用 BatchNormalization 和 Dropout
                sum_loss = 0.0
                correct = 0.0
                total = 0.0
                for i, data in enumerate(trainloader, 0):
                    # 准备数据
                    length = len(trainloader)
                    inputs, labels = data
                    inputs, labels = inputs.to(device), labels.to(device)
                    optimizer.zero_grad()

                    # forward + backward
                    outputs = net(inputs)
                    loss = criterion(outputs, labels)
                    loss.backward()
                    optimizer.step()

                    # 每训练1个batch打印一次loss和准确率
                    sum_loss += loss.item()
                    _, predicted = torch.max(outputs.data, 1)
                    total += labels.size(0)
                    correct += predicted.eq(labels.data).cpu().sum()
                    print('[epoch:%d, iter:%d] Loss: %.03f | Acc: %.3f%% '
                          % (epoch + 1, (i + 1 + epoch * length), sum_loss / (i + 1), 100. * correct / total))
                    f2.write('%03d  %05d |Loss: %.03f | Acc: %.3f%% '
                          % (epoch + 1, (i + 1 + epoch * length), sum_loss / (i + 1), 100. * correct / total))
                    f2.write('\n')
                    f2.flush()

                # 每训练完一个epoch测试一下准确率
                print("Waiting Test!")
                with torch.no_grad():
                    # with torch.no_grad()或者@torch.no_grad()中的数据不需要计算梯度,也不会进行反向传播
                    correct = 0
                    total = 0
                    for data in testloader:
                        net.eval()
                        # model.eval() :不启用 BatchNormalization 和 Dropout
                        images, labels = data
                        images, labels = images.to(device), labels.to(device)
                        outputs = net(images)
                        # 取得分最高的那个类 (outputs.data的索引号)
                        _, predicted = torch.max(outputs.data, 1)
                        total += labels.size(0)
                        correct += (predicted == labels).sum()
                    print('测试分类准确率为:%.3f%%' % (100 * correct / total))
                    acc = 100. * correct / total
                    # 将每次测试结果实时写入acc.txt文件中
                    print('Saving model......')
                    torch.save(net.state_dict(), '%s/net_%03d.pth' % (args.outf, epoch + 1)) #torch.save报错,原因是没有新建model文件夹。
                    f.write("EPOCH=%03d,Accuracy= %.3f%%" % (epoch + 1, acc))
                    f.write('\n')
                    f.flush()
                    # 记录最佳测试分类准确率并写入best_acc.txt文件中
                    if acc > best_acc:
                        f3 = open("best_acc.txt", "w")
                        f3.write("EPOCH=%d,best_acc= %.3f%%" % (epoch + 1, acc))
                        f3.close()
                        best_acc = acc
            print("Training Finished, TotalEPOCH=%d" % EPOCH)

五、评价ResNet

5.1.进步性

研究表明,网络层数的增加可以提高网络的拟合效果,可是随之而来的是因为层数过多带来梯度消失、梯度爆炸的问题,ResNet致力于解决这个问题。
ResNet通过优化损失函数,使得损失计算得到了很大程度的简化,进而适用于层数更多的网络模型。

同时,ResNet成功验证了“深层网络的拟合效果更好”这个推测。

5.2.局限性

ResNet在适度增加了网络深度后,发现进一步增加网络深度后,当层数达到10^3数量级,依然会出现拟合效果明显下降的问题。

我个人推测,原因是网络层数越多,网络本身需要调整与学习的部分就越多,网络所需要的训练数据就越多,因此无限制的增加网络层数,的确可以保证模型训练的上限在不断增加,可是如何达到这个上限,需要更多的数据以及更优化的训练方法进行保证。

最后,说一点我的小想法,传播残差的时候,如果只有一层网络拟合真实值,其他网络不断学习上一层网络的残差,这种学习方式会使得第一层网络起到了不该起到的决定性作用,可以预见这种方式会使得网络的学习效果变差,那么ResNet是否应该在这个方向上有所改进呢?

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值