我是靠谱客的博主 老迟到毛衣,最近开发中收集的这篇文章主要介绍ResNet 实现Cifar-10 识别以及一点思考,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

ResNet 实现Cifar-10 识别(PyTorch)以及一点思考

    • 1. 作者针对Cifar-10分类提出的网络结构
    • 2. 实现网络结构中遇到的问题
    • 3. 作者训练网络的细节
    • 4. 复现过程
    • 5. 总结分析
    • 6. 实现代码

最近看了ResNet的论文,由于刚从CS231n过渡过来,所以还在使用Cifar-10数据集。刚好论文后面就有一节是专门研究(经过简化后的)ResNet在Cifar-10数据集上的表现的,所以决定着手复现这一结构。再加上百度了一下好像也几乎没有针对这一节论文的复现的,所以记录下来自己复现的过程。最终32层(0.46M参数)准确率达到了90%,44层(0.66M参数)准确率达到91%,距离作者所述的92.5%+的准确度还有差距
ResNet具体是什么结构也不用细说了,我喜欢先说两句再上代码,想直接看代码的翻到最后就好了。
在代码之前,我先描述一下作者对这个结构的设计和训练细节,以及我在复现过程中遇到的问题和思考

1. 作者针对Cifar-10分类提出的网络结构

作者在论文中提到自己总体网络结构(节选):
The first layer is 3x3 convolutions.
Then we use a stack of 6n layers with 3x3 convolutions
on the feature maps of sizes {32; 16; 8} respectively,
with 2n layers for each feature map size. The numbers of filters are {16; 32; 64} respectively. The subsampling is performed by convolutions with a stride of 2. The network ends with a global average pooling, a 10-way fully-connected layer, and softmax. There are totally 6n+2 stacked weighted layers. The following table summarizes the architecture:

output map size32x3216x168x8
# layers1+2n2n2n
# filters163264

When shortcut connections are used, they are connected
to the pairs of 3x3 layers (totally 3n shortcuts). On this dataset we use identity shortcuts in all cases.
简单翻译一下:输入32x32的图片,第一层是3x3卷积层,接下来,对于每个相同分辨率的feature map做2n次卷积,每次卷积的通道数都相同。一共有3个这样的2n层,卷完8x8的特征图之后就做全局均值池化,最后用一个全连接层实现10类的分类。
高速通路只连接每对3x3卷积。因此最后有3n个高速通路。每个高速通路都是使用的恒等连接(不变换维度)

最后的结构就是:32x32输入->3x3卷积->2n个3x3卷积->步长为2的卷积->2n个3x3卷积->步长为2的卷积->2n个3x3卷积->步长为2的卷积->全局均值池化->全连接层->Softmax
每个2n卷积的通道数已经在上面的表格里给出。

2. 实现网络结构中遇到的问题

看起来还是蛮简单的,实现起来遇到了两个问题
第一个困扰我的问题就是,采用卷积做池化,这就不算有参数的层了吗?到底是把每个2n的第一层作为池化层,还是在每个2n的第一层之前加入了一个池化层?这里我抓住作者说“使用恒等链接作为高速通路”,所以倾向于后面的解释。
ResidualBlock在原论文示意图
图1 ResidualBlock在原论文示意图

第二个困扰我的问题就是,根据残差的结构图,最后一层的ReLU是放在残差和输入相加之后的,论文中作者只是轻描淡写地说自己采用了BN,但是BN是放在加操作之前,还是放在加操作之后?一开始我想当然地(不知为何这么想)放在了加之前。偶然地看起了Stochastic Depth(以下简称SD)的文章,文章里有一张很显眼的图画的就是带有BN的ResBlock结构。很明显BN放在加之前。SD的作者也没解释为什么,但是他指出,每一个残差模块的输出为:
Hl = ReLU( f(Hl-1) + Hl-1
当残差为0时,输出退化为:
Hl = ReLU( Hl-1 ) = Hl-1
懒得用公式打出来了,Hl 是某一层的输出,而Hl-1是上一层的输出,这是残差模块的基本原理。当残差为0,而本层的输入即上一层的输出也是通过ReLU激活的话,本来就全都非负,再经过一次ReLU自然就是原来的值了。
所以,如果我把BN放在加之后,就算残差为0,BN非常可能改变上一层的输出,e.g.使之非负。这样残差模块的意义就削弱了。当然,是不是可以考虑干脆把ReLU也放在加之前呢,有机会做做试验hhh
ps:单纯改了这个小结构就让我的准确率从87.5上升到89了。
ResidualBlock在SD论文中的示意图
图2 ResidualBlock在SD论文中的示意图

3. 作者训练网络的细节

对于训练的参数设定,作者文中如此描述(方括号内为原论文的引用序号,这里就懒得去掉了):
We use a weight decay of 0.0001 and momentum of 0.9,
and adopt the weight initialization in [13] and BN [16] but with no dropout. These models are trained with a minibatch size of 128 on two GPUs. We start with a learning rate of 0.1, divide it by 10 at 32k and 48k iterations, and terminate training at 64k iterations, which is determined on a 45k/5k train/val split. We follow the simple data augmentation in [24] for training: 4 pixels are padded on each side, and a 32x32 crop is randomly sampled from the padded image or its horizontal flip. For testing, we only evaluate the single view of the original 32x32 image.

提炼关键信息:SGD的m=0.9,weight_decay=1e-4,采用了He Kaiming权值初始化;采用了BN,minibatch=128,lr=0.1,32k和48k轮都会把学习率除以10,60k后停止训练;数据增强手段为4像素的pad并随机切割出32x32的图片、随机水平翻转。

4. 复现过程

由于是刚学完CS231n,所以学习率默认都是1e-3,1e-4级别的,优化器都是Adam,所以刚开始复现的时候,采用的n=5(根据前文的叙述,参数层共有6n+2因此共有32层),准确率只有85,61(分别表示在训练集和测试集上,下同),和作者描述的92.5%的准确率相差甚远。后来在网上找了一份ResNet-18的代码跑了一遍,发现准确率可以简单地上91%,所以进行了对比。
首先,单纯的把优化器换为SGD,并且SGD的参数也和作者相同,训练的效果就有了质的飞跃,达到了100%,87.5%,训练曲线如图3所示。
在这里插入图片描述
图3 ResNet损失和准确率曲线

最后训练准确率有几个点的差异,但是图形基本上就是这个图形,我也不想贴一堆长得差不多的图进行对比了。
后来又加入了原文的数据增强(水平翻转、4pad截取),好像准确率没有很大提升;然后把BN放在加之前,准确率提升到了89%+,接近90%的样子,最高到过90.4%。
再后来,把网络替换成n=7共44层,准确率可以比较轻松地达到90,最好能飙上91%

5. 总结分析

这应该是自己认真复现的第一个网络,比较简单,但是因为没啥经验所以一开始网络的效果很差。但是通过复现这个网络让我明白了Adam不是万能的,学习率是要调整的,怎么做数据增强等balabala的在训练网络中需要知道的细节知识。
这里使用的网络结构是比较高效的,32层的总参数不超过0.5M(个),还能在训练集上实现实现90%的准确率,网上找的ResNet18直接跑一通也就是91%,而且ResNet18的参数是它的20多倍了(直接比PyTorch保存下来的模型参数文件大小)。训练时间,按照作者60k次迭代,在2080Ti上大约是1小时20分钟。

6. 实现代码

初始化和数据集处理,还有两个常用的层

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

%matplotlib inline
plt.rcParams['figure.figsize'] = (10.0, 8.0) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

数据集的导入和预处理


transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),  #先四周填充0,再把图像随机裁剪成32*32
    transforms.RandomHorizontalFlip(),  #图像一半的概率翻转,一半的概率不翻转
    transforms.ToTensor(),
    transforms.Normalize((0, 0, 0), (1, 1, 1)), #R,G,B每层的归一化用到的均值和方差
])

transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0, 0, 0), (1, 1, 1)),
])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform_train)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128,
                                          shuffle=True, num_workers=0)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform_test)
testloader = torch.utils.data.DataLoader(testset, batch_size=128,
                                         shuffle=False, num_workers=0)

两个实用的层,第二个层常用于debug

class Flatten(nn.Module):
    def forward(self, x):
        N = x.shape[0]
        return x.view(N, -1)
class PrintParamShape(nn.Module):
    def forward(self, x):
        print(x.size())
        return x

画图

def DrawPlot(train_loss_summery, train_acc_summery, test_acc_summery):
    plt.subplot(2, 1, 1)
    plt.grid()
    plt.title('Training loss')
    plt.plot(train_loss_summery, 'o')
    plt.xlabel('Epoch')

    plt.subplot(2, 1, 2)
    plt.grid()
    plt.title('Accuracy')
    plt.plot(train_acc_summery, '-o', label='train')
    plt.plot(test_acc_summery, '-o', label='val')
    plt.plot([0.5] * len(test_acc_summery), 'k--')
    plt.xlabel('Epoch')
    plt.legend(loc='lower right')
    plt.gcf().set_size_inches(15, 12)
    plt.show()

定义训练函数。训练函数内部会记录每一次的loss和训练集、测试集的准确率,以用于后续作图。同时根据原文描述,还需要定义一个自动改变学习率的函数。同时还要有一个计算准确率的函数。

def AdjustLearningRate(optimizer, epoch, iter_times):
    if iter_times == 32000 or iter_times == 48000 or iter_times == 52000 or iter_times == 56000:
        for param_group in optimizer.param_groups:
            param_group['lr'] *= 0.1
        print("change lr")

def TrainNet(net, datasetLoader, criterion, optimizer, device, epoch_total):
    train_loss_summery = []
    train_acc_summery = []
    test_acc_summery = []
    (trainloader, testloader) = datasetLoader
    running_loss = 0.0
    iter_times = 0
    for epoch in range(epoch_total + 1):
        train_loss_summery.append(running_loss)
        train_acc_summery.append(PredictNet(net, trainloader, device, 0))
        test_acc_summery.append(PredictNet(net, testloader, device, 0))
        print('epoch %d, loss %.3f, train_acc %.3f %%, test_acc %.3f %%' % (epoch, running_loss / 1e2, train_acc_summery[epoch],test_acc_summery[epoch]))
        if epoch == epoch_total or iter_times > 64000:
            break
        running_loss = 0.0
        for i, data in enumerate(trainloader, 0):
            inputs, labels = data[0].to(device), data[1].to(device)
            optimizer.zero_grad()

            output = net(inputs)        

            loss = criterion(output, labels)
            loss.backward()
            optimizer.step()
            running_loss += loss.item()
            iter_times += 1
            AdjustLearningRate(optimizer, epoch, iter_times)
        
    print('Finished Training')
    return (train_loss_summery, train_acc_summery, test_acc_summery)   

def PredictNet(net, testloader, device, num = 0, isPrint=False):
    total = 0
    correct = 0
    accuracy = 0.0
    with torch.no_grad():
        for data in testloader:
            inputs, labels = data[0].to(device), data[1].to(device)
            outputs = net(inputs)
            _, predicted = torch.max(outputs, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
            if total > num and num != 0:
                break
    accuracy = 100 * correct / total
    if isPrint == True:
        print('Accuracy of the network on the %d test images: %.3f %%' % (
            total, accuracy))
    return accuracy
         

定义ResdualBlock,以及He Kaiming的权值初始化函数。由于我是个懒人,所以用了循环来创建网络结构,需要的参数是,{是否需要进行pooling,输入的通道数,微型结构细节(对应2n中的‘2’),微型结构重复次数(对应2n中的‘n’)}并且把所有的层都放入了torch的ModelList中。

def HeInitParam(layer):
#     return layer #如果不想初始化就把这个注释去掉
    if isinstance(layer, (torch.nn.Conv2d, torch.nn.Linear)):
        nn.init.kaiming_normal_(layer.weight, nonlinearity='relu')
        nn.init.zeros_(layer.bias)
    return layer
class ResidualBlock(nn.Module):
    def __init__(self, params):
        super(ResidualBlock,self).__init__()
        self.ifPoolConnect = params['ifPoolConnect']
        self.input_channel = params['input_channel']
        self.repeat_arch = params['repeat_arch']#repeat_arch[i][j] means ith layer, j=0 means kernel size, else means feature map cnt
        self.repeat_times = params['repeat_times']
        
        partModule = None
        self.poolLayer = torch.nn.ModuleList()
        self.ResBlockModules = torch.nn.ModuleList()
        if self.ifPoolConnect == True: 
            self.poolLayer.append(HeInitParam(torch.nn.Conv2d(self.input_channel, self.repeat_arch[len(self.repeat_arch)-1][1], kernel_size=2, stride=2)))

        for i in range(self.repeat_times):
            partModule = torch.nn.ModuleList()
            if self.ifPoolConnect == True:    
                partModule.append(self.getConvBNReLu(self.repeat_arch[len(self.repeat_arch)-1][1], self.repeat_arch[0][1], self.repeat_arch[0][0], _padding=(self.repeat_arch[0][0]-1)//2))
            else:
                partModule.append(self.getConvBNReLu(self.input_channel, self.repeat_arch[0][1], self.repeat_arch[0][0], _padding=(self.repeat_arch[0][0]-1)//2))

            for partLayerCnter in range(1, len(self.repeat_arch)):
                if partLayerCnter == len(self.repeat_arch) - 1:
                    partModule.append(HeInitParam(torch.nn.Conv2d(self.repeat_arch[partLayerCnter-1][1], self.repeat_arch[partLayerCnter][1], self.repeat_arch[partLayerCnter][0], padding=(self.repeat_arch[partLayerCnter][0]-1)//2)))
                    partModule.append(torch.nn.BatchNorm2d(self.repeat_arch[partLayerCnter][1]))
                else:
                    partModule.append(self.getConvBNReLu(self.repeat_arch[partLayerCnter-1][1], self.repeat_arch[partLayerCnter][1], self.repeat_arch[partLayerCnter][0], (self.repeat_arch[partLayerCnter][0]-1)//2))
                
            self.ResBlockModules.append(partModule)
                                                     
    def getConvBNReLu(self, channel_in, channel_out, kernel_size, _padding=0):
        return torch.nn.Sequential(HeInitParam(torch.nn.Conv2d(channel_in, channel_out, kernel_size, padding=_padding)),
                                        torch.nn.BatchNorm2d(channel_out),
                                        torch.nn.ReLU(),)
    def forward(self, x):
        #This may not be right.
        if(self.ifPoolConnect == True):
            x = self.poolLayer[0](x)
        for i in range(self.repeat_times):
            _x = x.clone()
            for partLayerCnter in range(0, len(self.repeat_arch)):
                x = self.ResBlockModules[i][partLayerCnter](x)
            x = _x + x
            x = torch.nn.functional.relu(x)
                                                     
                                                     
        return x

最后是整个网络的搭建

class ResidualNet(nn.Module):
    def __init__(self):
        super(ResidualNet,self).__init__()
        repeat_times = 7
        self.ResBlockTestParam1 = {'ifPoolConnect':False, 'input_channel':16, 
                        'repeat_arch':[[3,16], [3,16],],'repeat_times':repeat_times}
        self.ResBlockTestParam2 = {'ifPoolConnect':True, 'input_channel':16, 
                        'repeat_arch':[[3,32], [3,32],],'repeat_times':repeat_times}
        self.ResBlockTestParam3 = {'ifPoolConnect':True, 'input_channel':32, 
                        'repeat_arch':[[3,64], [3,64],],'repeat_times':repeat_times}
        self.ResNetModule = torch.nn.Sequential(HeInitParam(torch.nn.Conv2d(3, 16, kernel_size=3, padding = 1)),
                                                torch.nn.BatchNorm2d(16),
                                                torch.nn.ReLU(),
                                                ResidualBlock(self.ResBlockTestParam1),
                                                ResidualBlock(self.ResBlockTestParam2),
                                                ResidualBlock(self.ResBlockTestParam3),  
                                                torch.nn.AvgPool2d(8, 1),
                                                Flatten(),

                                                HeInitParam(torch.nn.Linear(64, 10)),)
    def forward(self,x):
        return self.ResNetModule(x)

训练的话,下面几行就够了

criterion = nn.CrossEntropyLoss()
net = ResidualNet().to(device)

optimizer = optim.SGD(net.parameters(), lr=1e-1, momentum=0.9, weight_decay=5e-4)
(train_loss_summery, train_acc_summery, test_acc_summery) = TrainNet(net, (trainloader, testloader), criterion, optimizer, device, 200)

PATH = './cifar_ResNet_.pth'
torch.save(net.state_dict(), PATH)

[1]: Deep Networks with Stochastic Depth
[2]: Deep Residual Learning for Image Recognition
[3]: Pytorch实战2:ResNet-18实现Cifar-10图像分类(测试集分类准确率95.170%)
[4]: pytorch中的学习率调整函数

最后

以上就是老迟到毛衣为你收集整理的ResNet 实现Cifar-10 识别以及一点思考的全部内容,希望文章能够帮你解决ResNet 实现Cifar-10 识别以及一点思考所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(49)

评论列表共有 0 条评论

立即
投稿
返回
顶部