0


ResNet 训练CIFAR10数据集,并做图片分类

1. ResNet 介绍

ResNet 的亮点:

  • 超深的网络结构,可以突破1000层
  • 提出residual 模块
  • 使用Batch Normalization 抑制过拟合,丢弃Dropout方法

针对第一点,我们知道加深网络层对于提升网络性能至关重要。然而实际情况中,网络层的加深会导致学习无法进行,性能会更差。因为网络的深度会导致梯度消失或者梯度爆炸的问题

因为层数的加深,反向传播的时候,更新权重w的时候,根据链式法则可能要乘上很多项。

例如传过来的是个0.5,那么网络层数深就会导致 0.5 的n次方,从而导致梯度消失,网络无法学习。同理,梯度爆炸是 > 1的概念。

ResNet 提出了一个residual block 残差块的概念,如图。通过右面的一种shortcut 捷径的方式,这样在反向传播的时候,shortcut 就可以将梯度传递过来,从而解决梯度消失或者爆炸的问题。通过这个快捷路径,从而解决了网络加深的问题。

然后,ResNet 使用了 BN (Batch Normalization)代替了Dropout 方法。

根据之前的预处理方式,我们知道,将数据限定到相似的范围内可以帮助网络更好的训练(之前看吴恩达视频的时候,说是特征缩放到类似的区间,这样损失函数就会是一个类似于碗的样子,这样方便梯度下降。否则,可能会是一个碗被挤压的样子,在steep的一边,梯度就会来回横跳)。例如之前的ToTensor,Normalize 等等。

但是之前都是对数据进行特征缩放,然而我们Normalization后的数据经过网络的时候就已经不是之前归一化之类的数据了,所以BN的思想是在每一层后也正规化一下

经过上面的变换,就可以将每一个 batch 的数据变成mean = 0 ,std = 1的分布

这里要注意以下两点:

  • 因为bias 是上下平移,而归一化的时候bias有没有都一样,所以这里的卷积可以不需要bias
  • 这里是将每一个batch 所以的图像都做BN变换,具体的设置如下

2. ResNet 网络介绍(ResNet34)

ResNet 因为加入了shortcut 捷径的方式可以加深更多的网络层。这里常见的resnet有如下的几种,这里只介绍ResNet34

这里50、101、152 每个residual block 第一个都是 1*1 是为了改变特征图的size。观察可以发现,例如conv3_x 、conv4_x、conv5_x 的第一个残差块的第一层右面的shortcut都是虚线

这是因为这里的shortcut需要用1 * 1 的卷积核改变size

并且在conv3_x 、conv4_x、conv5_x 残差块的第一层 3*3 卷积核的stride也发生了变化

因为步长发生了变化,所以这里输出特征图和输入的size以及不是一样了。而右面的shortcut想要和左面的发生叠加,要保证shape一样,所以右面的shortcut上面需要一个1*1 的卷积核来改变size

3. 搭建ResNet 网络

residual block

因为ResNet 网络里面包含很多的residual block

3*3 代表卷积核的size ,64 代表卷积核的个数 = 图像输出的深度

就是网络中上面的结构,所以先将它们封装成一个类

因为residual 结构都是两个3*3 卷积构成的,所以这里先将Conv2d的kernel_size 设置为3

网络结构中没有标stride的都是默认为1,但是这里第一个stride不能直接等于1,因为有的层第一个是2,不是1---------> 所有shortcut是虚线对应的第一个卷积核

然后,经过BN层,ReLU层,残差块的第二个卷积都是3*3 ,且默认stride = 1

这里padding =1 的原因

  • 图像输出的size = (in - 3 + 2*1) / 1 + 1 = in 才能保证输出的size是等于输入的
  • 如果stride = 2,那么out = (in - 3 + 21) / 2 + 1 =in / 2 + 0.5 = in / 2 (向下取整),这样就会将输出的size变成一半,所以conv3_x 、conv4_x、conv5_x 残差块的第一层和上面的size是不一样的。那么右面的捷径就不能由上面直接流过来,因为size不一样没法相加。所以对应的shortcut是虚线,需要特殊的 11 卷积核改变size
  • 并且,由于BN,所以不需要偏置 b

接下来定义forward:

残差块的传播分为两类,一类是左边主路的传播,这里定义成left。

右边是快捷路径,这里分为两种情况,一种是上游直接传过来,另一种是图像size被改变需要1*1卷积核做运算。如果是第一种,那么为None,residual = x,如果被改变,就为新的shortcut。最后和左边的out相加,经过ReLU输出就可以了

ResNet

为了方便图像维度的讲解,这里会传入一个batch = 10 的2242243 的彩色图像

为了不引起误会,后面描述的依旧是batch而不是10

例如:图像shape:batch645656 而不是10645656

pre 传播

首先定义ResNet 前面的传播

因为这里图像的输入是2242243 的彩色图像。

首先经过7*7 ,stride = 2 的卷积核,这里padding = 3的原因是为了保证输出size是输入的一半

再经过33 最大池化层,就可以将图像的输出维度控制在:batch645656

这里的stride、padding 都是为了保证图像的size 满足

layer1

这里_make_layer 第一个参数为残差块里面卷积核的size,3、4、6、3为每层包含残差块的个数

首先看第一层

  • 用裂变将每一层网络存到layers里面
  • 观察网络的结构,conv2_x 的所有卷积核stride 都为1,也就是说conv2_x 里面,图像的shape都是一样的,所有这也是为什么这里shortcut 都是实线,因为它不需要1*1的卷积核改变shape
  • 所有第一层(conv2_x)里面的stride = 1,那么就会进入里面的else(这里的if else是为了搭建每个layer 的第一个residual block,因为有stride变化的残差块只在每一个layer的第一个)
  • 后面的残差块都是一样的,根据传入的个数(这里是3、4、6、3)搭建残差块就行了
  • 经过第一层之后,图像的size是:batch6456*56
  • 这里的*layers 是将layers每个residual block模块取出来

layer2

如图,第二层(conv3_x)的第一个残差块已经发生了变化,

这里的变化是指:

  • stride 由 1变成了2,所以会导致图像的size 变为原来的一半
  • 并且图像的深度变成了原来的2倍(因为size减少,少了一部分的特征,所以就拿深度来弥补了)

  • 那么这里就会进入if判断,shortcut捷径就不能直接从上游传过来,因为上面的图像是现在的2倍。所以shortcut 需要一个1*1的卷积核,stride =2 就能把上游size减半传到捷径上了。并且这里的第一个residual block 的深度要保证一致,即输入的channel是上面的,输出因为size减少,这里将channel 变成2倍
  • 至于后面的残差块都是一样的,保证channel 都是128,size 都是28*28就行了
  • 所以经过conv3_x 图像的shape是:batch12828*28

layer3、4

layer3和layer4都是和layer2一样的

经过layer3的shape是:batch25614*14

经过layer4的shape是:batch5127*7

全连接层的forward

全连接层:

forward:

ResNet 网络的参数个数

网络的参数个数可由以下得到:

model = ResNet()
print("Total number of paramerters in networks is {}  ".format(sum(x.numel() for x in model.parameters())))

ResNet 参数个数为:

summary

  • ResNet 网络基本上都是3*3 的卷积核组成的
  • 3*3 卷积核在stride = 1、padding = 1的情况下,不改变图像的size。所以大部分的shortcut捷径可以直接传递和输出相加
  • 而conv3_x、conv4_x、conv5_x 由于卷积核的stride =2,导致输出特征图是输入特征图的一半。那么捷径从上游传过来的size就是2倍了,所以这里需要1*1 stride = 2的卷积核去减半shortcut 的size
  • 图像的size减半,相当于提取的特征减少。那么为了更好的提取特征,就将输出特征图的个数变为原来的2倍 = 卷积核的个数

4. 训练网络

训练网络的代码不做讲解,具体的可以看这篇文章:

pytorch 搭建 LeNet 网络对 CIFAR-10 图片分类https://blog.csdn.net/qq_44886601/article/details/127498256

主要对GPU训练的部分做讲解:

首先判断设备:

然后将网络扔到设备上:

然后训练的时候,输入也需要传到GPU

最后就是测试的时候

5. 预测图片

这里读取的时候需要注意,因为训练的时候实在GPU上,这里我预测的时候实在cpu上,所以读取网络参数的时候,代码要变成下面这样

预测图像

这里有一个注意点:

因为CIFAR10 图像的size 都是3232 的,而ResNet网络的输入是224224。那么预处理时候需要将图像放大,这里就会很模糊。所以在预测的时候,有个smart point就是,将预测的图像也转换成3232,然后再放大成224224。这样预测的精度就会上升

处理结果:


这里网上随便找了几张图片,都能预测对且有较好的准确率:

6. Code

ResNet 网络部分:

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

class ResidualBlock(nn.Module):     # 搭建 残差结构
    def __init__(self, inchannel, outchannel, stride=1, shortcut=None):
        super(ResidualBlock, self).__init__()
        self.left = nn.Sequential(
            # stride 不是1,因为有的残差块第一层 的stride = 2;对应残差块的虚线实线
            nn.Conv2d(inchannel,outchannel,kernel_size=3,stride =stride,padding = 1,bias = False),
            nn.BatchNorm2d(outchannel),
            nn.ReLU(inplace=True),
            # 第二个卷积核的stride = 1
            nn.Conv2d(outchannel,outchannel,kernel_size=3,stride = 1,padding=1,bias=False), # 输入和输出的特征图size一样
            nn.BatchNorm2d(outchannel),
        )
        self.right = shortcut       # 捷径块

    def forward(self,x):
        out = self.left(x)
        # 实线,特征图的输入和输出size一样;虚线,shortcut部分要经过1*1卷积核改变特征图size
        residual = x if self.right is None else self.right(x)
        out += residual             # 加完之后在 ReLU
        out = F.relu(out)
        return out

class ResNet(nn.Module):    # 搭建ResNet 网络
    def __init__(self, num_classes=10):
        super(ResNet,self).__init__()
        self.pre = nn.Sequential(
            # 输入:batch * 3 * 224 * 224
            # 输出特征图size = (224 - 7 + 2*3) / 2 + 1 = 112 +0.5 = 112 向下取整
            nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False),   # 输出 [batch,64,112,112]
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            # 输出特征图size = (112 - 3 + 2*1) / 2 + 1 = 56 +0.5 = 56 向下取整
            nn.MaxPool2d(kernel_size=3,stride=2,padding=1) # 输出 [batch,64,56,56]
         )

        self.layer1 = self._make_layer(64, 3,stride = 1)    # conv2_x 有三个残差块
        self.layer2 = self._make_layer(128,4,stride = 2)    # conv3_x 有四个残差块
        self.layer3 = self._make_layer(256,6,stride = 2)    # conv4_x 有六个残差块
        self.layer4 = self._make_layer(512,3,stride = 2)    # conv5_x 有三个残差块

        self.fc = nn.Linear(512,num_classes)  # 分类层

    def _make_layer(self,channel, block_num,stride = 1):        # 构建layer,每个层包含3 4 6 3 个残差块block_num
        shortcut = None
        layers = []  # 网络层

        if stride != 1:     # conv3/4/5_x 的第一层 stride 都是2,并且shortcut 需要特殊操作
            # 定义shortcut 捷径,都是1*1 的kernel ,需要保证和left最后相加的shape一样
            shortcut = nn.Sequential(
                nn.Conv2d(int(channel / 2),channel,kernel_size=1,stride=stride,bias=False),
                nn.BatchNorm2d(channel )
            )
            layers.append(ResidualBlock(int(channel / 2),channel,stride,shortcut))
        else:
            layers.append(ResidualBlock(channel, channel, stride, shortcut))

        for i in range(1,block_num):    # 残差块后面几层的卷积是一样的
            layers.append(ResidualBlock(channel,channel))
        return nn.Sequential(*layers)

    def forward(self,x):
        x = self.pre(x)         # batch*64*56*56
        x = self.layer1(x)      # batch*64*56*56
        x = self.layer2(x)      # batch*128*28*28
        x = self.layer3(x)      # batch*256*14*14
        x = self.layer4(x)      # batch*512*7*7
        x = F.avg_pool2d(x,7)   # batch*512*1*1
        x = x.view(x.size(0),-1)        # x.size(0)是batch ,保持batch,其余的压成1维
        x = self.fc(x)          # batch*10(分类的个数)
        return x

model = ResNet()
#import torch
#input = torch.randn((10,3,224,224))
#model(input)

# 计算网络参数个数
print("Total number of paramerters in networks is {}  ".format(sum(x.numel() for x in model.parameters())))

训练部分:

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms
from model import ResNet
import torchvision

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 判断是cpu还是gpu运行
print("using {} device.".format(device))

data_transform =transforms.Compose([
                                 transforms.Resize((224,224)),  # 变换成(224,224)满足ResNet的输入
                                 transforms.ToTensor(),         # 变成Tensor,改变通道顺序等等
                                 transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# 训练集 5W 张图片
trainset = torchvision.datasets.CIFAR10(root = './data',train = True,download= True,transform=data_transform)
trainloader = torch.utils.data.DataLoader(trainset,batch_size = 36,shuffle = True)

# 测试集 1W 张图片
testset = torchvision.datasets.CIFAR10(root = './data',train = False,download= True,transform=data_transform)
testloader = torch.utils.data.DataLoader(testset,batch_size = 36,shuffle = False)

# CIFAR10 十个分类类别的label
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

net = ResNet()      #  载入网络
net.to(device)
loss_function = nn.CrossEntropyLoss()   # 定义损失函数
optimizer = optim.Adam(net.parameters() , lr=0.0001)    # 定义优化器

best_acc = 0.0
save_path = './resNet34.pth'        # 网络权重文件保存路径

for epoch in range(5):
    # train
    net.train()
    running_loss = 0.0

    for step, data in enumerate(trainloader,start= 0):
        images, labels = data
        optimizer.zero_grad()                               # 梯度清零
        out = net(images.to(device))                        # 前向传播
        loss = loss_function(out, labels.to(device))        # 计算损失
        loss.backward()                                     # 反向传播
        optimizer.step()                                    # 梯度下降

        running_loss += loss.item()    # 损失值

    # test
    net.eval()
    acc = 0.0
    total = 0
    with torch.no_grad():

        for test_data in testset:
            test_images, test_labels = test_data    # 取出测试集的image和label
            outputs = net(test_images.to(device))           # 前向传播
            predict_y = torch.max(outputs, dim=1)[1]        # 取出最大的预测值
            acc += (predict_y == test_labels.to(device)).sum().item()    # 正确 +1
            total+= test_labels.size(0)

    accurate = acc / total              # 计算整个test上面的正确率
    print('[epoch %d] train_loss: %.3f  test_accuracy: %.3f' %
          (epoch + 1, running_loss / step, accurate))

    if accurate > best_acc:
        best_acc = accurate
        torch.save(net.state_dict(), save_path)

print('Finished Training')

预测部分:

import torchvision.transforms as transforms     # 预处理
import torch
from PIL import Image
from model import ResNet

data_transform =transforms.Compose([
                                 transforms.Resize((32,32)),
                                 transforms.Resize((224,224)),
                                 transforms.ToTensor(),
                                 transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

classes = ('plane','car','bird','cat','deer','dog','frog','horse','ship','truck')

net = ResNet()
net.load_state_dict(torch.load('./resNet34.pth',map_location = 'cpu'))  # 加载网络训练的参数

im = Image.open('./2.png')
im = data_transform(im)  # 图像维度 (C,H,W)
im = torch.unsqueeze(im,dim  = 0)  # 增加维度,第0维增加1 ,维度(1,C,H,W)

net.eval()          # 打开eval模式
with torch.no_grad():
    outputs = net(im)       # 预测图像
    predict = torch.max(outputs,dim = 1)[1].data.numpy()  # 取出最大的结果
    pro = torch.softmax(outputs, dim=1)
    rate = pro.max().numpy() * 100
    print('prediction:%s , accuracy: %.3f %%'%( classes[int(predict)],rate))

7. 迁移学习

迁移学习其实就是将别人学习过的权重拿来用,将它们作为初始值训练的过程

因为最后的分类往往是根据自己的设置,建议将官方的权重copy,然后加一个分类层就可以了

例如:将原网络的输出再加一个全连接层


本文转载自: https://blog.csdn.net/qq_44886601/article/details/127762455
版权归原作者 Henry_zhangs 所有, 如有侵权,请联系我们删除。

“ResNet 训练CIFAR10数据集,并做图片分类”的评论:

还没有评论