草庐IT

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

Henry_zhangs 2023-10-26 原文

目录

1. ResNet 介绍

2. ResNet 网络介绍(ResNet34)

3. 搭建ResNet 网络

residual block

ResNet 

pre 传播

layer1

layer2

layer3、4

全连接层的forward

ResNet 网络的参数个数

summary

4. 训练网络

5. 预测图片

6. Code

7. 迁移学习


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

接下来定义forward:

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

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

ResNet 

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

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

例如:图像shape:batch*64*56*56  而不是10*64*56*56

pre 传播

首先定义ResNet 前面的传播

因为这里图像的输入是224*224*3 的彩色图像。

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

再经过3*3 最大池化层,就可以将图像的输出维度控制在:batch*64*56*56

这里的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是:batch*64*56*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是:batch*128*28*28

layer3、4

layer3和layer4都是和layer2一样的

经过layer3的shape是:batch*256*14*14

经过layer4的shape是:batch*512*7*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 都是32*32 的,而ResNet网络的输入是224*224。那么预处理时候需要将图像放大,这里就会很模糊。所以在预测的时候,有个smart point就是,将预测的图像也转换成32*32,然后再放大成224*224。这样预测的精度就会上升

处理结果:


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

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,然后加一个分类层就可以了

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

有关ResNet 训练CIFAR10数据集,并做图片分类的更多相关文章

  1. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  2. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  3. ruby-on-rails - Ruby on Rails - 为文本区域和图片生成列 - 2

    我是Rails的新手,所以请原谅简单的问题。我正在为一家公司创建一个网站。那家公司想在网站上展示它的客户。我想让客户自己管理这个。我正在为“客户”生成一个表格,我想要的三列是:公司名称、公司描述和Logo。对于名称,我使用的是name:string但不确定如何在脚本/生成脚手架终端命令中最好地创建描述列(因为我打算将其设置为文本区域)和图片。我怀疑描述(我想成为一个文本区域)应该仍然是描述:字符串,然后以实际形式进行调整。不确定如何处理图片字段。那么……说来话长:我在脚手架命令中输入什么来生成描述和图片列? 最佳答案 对于“文本”数

  4. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  5. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  6. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  7. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  8. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

  9. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

  10. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

随机推荐