第3周学习:ResNet+ResNeXt

本文介绍了ResNet和ResNeXt的论文与实现,包括ResNet的残差模块、BatchNorm和迁移学习,以及ResNeXt的组卷积改进。通过PyTorch实现代码,探讨了这些技术如何提升模型性能和计算效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、论文阅读与视频学习

(一)ResNet

1、论文Deep Residual Learning for Image Recognition,CVPR2016及视频ResNet网络讲解

ResNet全名Deep Residual Learning for Image Recognition,网络中有超深的网络结构(突破1000层),提出residual模块,使用Batch Normalization加速训练
请添加图片描述
左侧适用于18/34层结构
请添加图片描述
Batch Normalization详解:
(1)训练时要将traning参数设置为True,验证时把这个参数设置为false。在 pytorch中可以通过创建模型的model.train()和model.eval()方法控制。
(2)batch size尽可能设置大点,设置小后表现可能很糟糕,设置的越大求的均值和方差约接近整个训练集的均值和方差。
(3)建议将bn层放在卷积层和激活层之间,且卷积层不要使用偏置bias。请添加图片描述
迁移学习(注意别人预处理的方式):
(1)能够快速训练出一个理想的结果
(2)当数据集较小时也能训练出理想的效果
常见的迁移学习方式:
(1)载入权重后训练所有参数
(2)载入权重后只训练最后几层参数
(3)载入权重后在原网络基础上在原网络基础上再添加一层全连接层,仅训练最后一个全连接层
请添加图片描述

2、视频Pytorch搭建ResNet网络

请添加图片描述

import torch.nn as nn
import torch

//18/34层残差结构 自定义网络继承自类module
class BasicBlock(nn.Module):
    expansion = 1//卷积核个数没变化 设为1

//in_channel输入特征矩阵的深度 out_channel输出特征矩阵的深度/主分支上卷积核的个数 stride步距1实线2虚线 downsample下采样参数/代表捷径中1*1的卷积层
    def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
                               kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channel)
        self.relu = nn.ReLU()
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
                               kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channel)
        self.downsample = downsample

    def forward(self, x):
   		//identity捷径分支的输出
        identity = x
        //none->对应实线
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        out += identity
        out = self.relu(out)
		//残差结构的最终输出
        return out

//对应50/101/152层残差结构
class Bottleneck(nn.Module):
    """
    注意:原论文中,在虚线残差结构的主分支上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。
    但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,
    这么做的好处是能够在top1上提升大概0.5%的准确率。
    可参考Resnet v1.5 https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch
    """
    //第三层卷积核个数是第一/二层卷积核个数的4倍
    expansion = 4

    def __init__(self, in_channel, out_channel, stride=1, downsample=None,
                 groups=1, width_per_group=64):
        super(Bottleneck, self).__init__()

        width = int(out_channel * (width_per_group / 64.)) * groups

        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=width,
                               kernel_size=1, stride=1, bias=False)  # squeeze channels
        self.bn1 = nn.BatchNorm2d(width)
        # -----------------------------------------
        self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups,
                               kernel_size=3, stride=stride, bias=False, padding=1)
        self.bn2 = nn.BatchNorm2d(width)
        # -----------------------------------------
        self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel*self.expansion,
                               kernel_size=1, stride=1, bias=False)  # unsqueeze channels
        self.bn3 = nn.BatchNorm2d(out_channel*self.expansion)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample

	//正向传播过程
    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        out += identity
        out = self.relu(out)

        return out


class ResNet(nn.Module):
	//block->basicblock/bottleneck
    def __init__(self,
                 block,
                 blocks_num,
                 num_classes=1000,
                 include_top=True,
                 groups=1,
                 width_per_group=64):
        super(ResNet, self).__init__()
        self.include_top = include_top
        self.in_channel = 64

        self.groups = groups
        self.width_per_group = width_per_group

        self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
                               padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(self.in_channel)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        //layer1对应conv2.x一系列结构
        self.layer1 = self._make_layer(block, 64, blocks_num[0])
        self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
        self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
        self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
        if self.include_top:
        	//自适应平均池化下采样
            self.avgpool = nn.AdaptiveAvgPool2d((1, 1))  # output size = (1, 1)
            self.fc = nn.Linear(512 * block.expansion, num_classes)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')

    def _make_layer(self, block, channel, block_num, stride=1):
        downsample = None
        if stride != 1 or self.in_channel != channel * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(channel * block.expansion))

        layers = []
        layers.append(block(self.in_channel,
                            channel,
                            downsample=downsample,
                            stride=stride,
                            groups=self.groups,
                            width_per_group=self.width_per_group))
        self.in_channel = channel * block.expansion

        for _ in range(1, block_num):
            layers.append(block(self.in_channel,
                                channel,
                                groups=self.groups,
                                width_per_group=self.width_per_group))
		//用于搭建神经网络的模块被按照被传入构造器的顺序添加到nn.Sequential()容器中
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        if self.include_top:
            x = self.avgpool(x)
            x = torch.flatten(x, 1)
            x = self.fc(x)

        return x


def resnet34(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet34-333f7ec4.pth
    return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)


def resnet50(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet50-19c8e357.pth
    return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)


def resnet101(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet101-5d3b4d8f.pth
    return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)


def resnext50_32x4d(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth
    groups = 32
    width_per_group = 4
    return ResNet(Bottleneck, [3, 4, 6, 3],
                  num_classes=num_classes,
                  include_top=include_top,
                  groups=groups,
                  width_per_group=width_per_group)


def resnext101_32x8d(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth
    groups = 32
    width_per_group = 8
    return ResNet(Bottleneck, [3, 4, 23, 3],
                  num_classes=num_classes,
                  include_top=include_top,
                  groups=groups,
                  width_per_group=width_per_group)


(二)ResNeXt

1、论文Aggregated Residual Transformations for Deep Neural Networks, CVPR 2017及视频ResNeXt网络讲解

ResNetXt更新了block
请添加图片描述
组卷积
请添加图片描述

2、视频Pytorch搭建ResNeXt网络

提出ResNeXt的主要原因在于,传统提高模型的准确率方式都是加深或加宽网络,但是随着超参数数量的增加,网络设计的难度和计算开销也会增加。因此 ResNeXt 结构可以在不增加参数复杂度的前提下提高准确率,同时得益于子模块的拓扑结构,可以减少超参数的数量。

二、代码作业

AI研习社“猫狗大战”

LeNet框架:
在学习了LeNet网络进行猫狗大战博客的基础上,尝试搭建LeNet网络进行分类。

Step 1:下载数据集

# 这个是训练集,猫狗各取了2000个
! wget https://gaopursuit.oss-cn-beijing.aliyuncs.com/2021/files/train.zip
! unzip train.zip
# 这个是测试集
! wget https://gaopursuit.oss-cn-beijing.aliyuncs.com/202007/dogs_cats_test.zip
! unzip dogs_cats_test.zip
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torchvision
from torchvision import models,transforms,datasets
import torch.nn.functional as F
from PIL import Image
import torch.optim as optim
import json, random
import os

# 判断是否存在GPU设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('Using gpu: %s ' % torch.cuda.is_available())
# 训练图片和测试图片的路径
train_path = './train/'
test_path = './test/'

Step 2:处理数据集

def get_data(file_path):
    file_lst = os.listdir(file_path) #获得所有文件名称 xxxx.jpg
    data_lst = []
    for i in range(len(file_lst)):
        clas = file_lst[i][:3] #cat和dog在文件名的开头
        img_path = os.path.join(file_path,file_lst[i])#将文件名与路径合并得到完整路径,以备读取
        if clas == 'cat':
            data_lst.append((img_path, 0))
        else:
            data_lst.append((img_path, 1))
    return data_lst

class catdog_set(torch.utils.data.Dataset):
    def __init__(self, path, transform):
        super(catdog_set).__init__()
        self.data_lst = get_data(path)#调用刚才的函数获得数据列表
        self.trans = torchvision.transforms.Compose(transform)
    def __len__(self):
        return len(self.data_lst)
    def __getitem__(self,index):
        (img,cls) = self.data_lst[index]
        image = self.trans(Image.open(img))
        label = torch.tensor(cls,dtype=torch.float32)
        return image,label
# 将输入图像缩放为 128*128,每一个 batch 中图像数量为128
# 训练时,每一个 epoch 随机打乱图像的顺序,以实现样本多样化
train_loader = torch.utils.data.DataLoader(
    catdog_set(train_path, [transforms.Resize((128,128)),transforms.ToTensor()]), 
    batch_size=128, shuffle=True)

Step 3:搭建LeNet网络
3*128*128->conv1(16*124*124)->maxpool(16*62*62)->conv2d(32*58*58)->maxpool(32*29*29)
LeNet学习博客

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, 5)    # C1
        self.pool1 = nn.MaxPool2d(2, 2)     # S2
        self.conv2 = nn.Conv2d(16, 32, 5)   # C3
        self.pool2 = nn.MaxPool2d(2, 2)     # S4
        self.fc1 = nn.Linear(32*29*29, 120)   # C5(用全连接代替)
        self.fc2 = nn.Linear(120, 84)       # F6
        self.fc3 = nn.Linear(84, 2)        # F7 结果分2类

    def forward(self, x):
        x = F.relu(self.conv1(x))    # input(3, 32, 32) output(16, 28, 28)
        x = self.pool1(x)            # output(16, 14, 14)
        x = F.relu(self.conv2(x))    # output(32, 10, 10)
        x = self.pool2(x)            # output(32, 5, 5)
        x = x.view(-1, 32*29*29)       # output(32*5*5) 展平
        x = F.relu(self.fc1(x))      # output(120)
        x = F.relu(self.fc2(x))      # output(84)
        x = self.fc3(x)              # output(10)
        return x

# 网络放到GPU上
net = LeNet().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)

Step 4:重复多次训练

for epoch in range(30):  # 重复多轮训练
    for i, (inputs, labels) in enumerate(train_loader):
        inputs = inputs.to(device)
        labels = labels.to(device)
        # 优化器梯度归零
        optimizer.zero_grad()
        # 正向传播 + 反向传播 + 优化 
        outputs = net(inputs)
        loss = criterion(outputs, labels.long())
        loss.backward()
        optimizer.step() 
    print('Epoch: %d loss: %.6f' %(epoch + 1, loss.item()))
print('Finished Training')

Epoch: 1 loss: 0.704636
Epoch: 2 loss: 0.654936
Epoch: 3 loss: 0.577431
Epoch: 4 loss: 0.540570
Epoch: 5 loss: 0.487358
Epoch: 6 loss: 0.352520
Epoch: 7 loss: 0.345092
Epoch: 8 loss: 0.325232
Epoch: 9 loss: 0.067634
Epoch: 10 loss: 0.031828
Epoch: 11 loss: 0.036147
Epoch: 12 loss: 0.012576
Epoch: 13 loss: 0.010833
Epoch: 14 loss: 0.004121
Epoch: 15 loss: 0.001717
Epoch: 16 loss: 0.001304
Epoch: 17 loss: 0.000462
Epoch: 18 loss: 0.000133
Epoch: 19 loss: 0.000179
Epoch: 20 loss: 0.000141
Epoch: 21 loss: 0.000439
Epoch: 22 loss: 0.000666
Epoch: 23 loss: 0.000298
Epoch: 24 loss: 0.000086
Epoch: 25 loss: 0.000156
Epoch: 26 loss: 0.000069
Epoch: 27 loss: 0.000371
Epoch: 28 loss: 0.000186
Epoch: 29 loss: 0.000025
Epoch: 30 loss: 0.000098
Finished Training

Step 5:输出分类结果
请添加图片描述

resfile = open('res0.csv', 'w')
for i in range(0,2000): 
    img_PIL = Image.open('./test/'+str(i)+'.jpg')
    img_tensor = transforms.Compose([transforms.Resize((128,128)),transforms.ToTensor()])(img_PIL)
    img_tensor = img_tensor.reshape(-1, img_tensor.shape[0], img_tensor.shape[1], img_tensor.shape[2])
    img_tensor = img_tensor.to(device)
    out = net(img_tensor).cpu().detach().numpy()
    if out[0, 0] < out[0, 1]:
        resfile.write(str(i)+','+str(1)+'\n')
    else:
        resfile.write(str(i)+','+str(0)+'\n')
resfile.close()

ResNet18:
Change 1:将图片resize为[224,224]
Change 2:网络结构替换为ResNet18

class Resblk(nn.Module):
    def __init__(self,ch_in,ch_out,stride1,stride2) -> None:
        super(Resblk,self).__init__()
        self.blk = nn.Sequential(
            nn.Conv2d(ch_in,ch_out,kernel_size=3,stride=stride1,padding=1),
            nn.BatchNorm2d(ch_out),
            nn.ReLU(),
            nn.Conv2d(ch_out,ch_out,kernel_size=3,stride=stride2,padding=1),
            nn.BatchNorm2d(ch_out)
        )
        self.extra = nn.Sequential()
        #输入输出通道数不同的话
        if ch_in != ch_out:
            self.extra = nn.Sequential(             
                nn.Conv2d(ch_in,ch_out,kernel_size=1,stride=2,padding=0),
                nn.BatchNorm2d(ch_out)
            )
 
    def forward(self,x):
        out = F.relu(self.blk(x)+self.extra(x))
        return out
 
 
class ResNet18(nn.Module):
    def __init__(self) -> None:
        super(ResNet18,self).__init__()
 
        self.preconv = nn.Sequential(
            nn.Conv2d(3,64,kernel_size=7,stride=2,padding=3),
            nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
        )
        self.conv1 = nn.Sequential(
            nn.Conv2d(64,128,kernel_size=1,stride=2,padding=0),
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(128,256,kernel_size=1,stride=2,padding=0),
        )
        self.conv3 = nn.Sequential(
            nn.Conv2d(256,512,kernel_size=1,stride=2,padding=0),
        )
        #由于残差块中 是通过控制stride来降维度的,
        #因此我在设置Resblk时将stride作为参数输入,
        #参数意义: 输入通道,输出通道,残差块中第一层卷积步长,残差块中第二层卷积的步长
        self.blk1 = Resblk(64,64,1,1)
        self.blk2 = Resblk(64,64,1,1)
        self.blk3 = Resblk(64,128,2,1)
        self.blk4 = Resblk(128,128,1,1)
        self.blk5 = Resblk(128,256,2,1)
        self.blk6 = Resblk(256,256,1,1)
        self.blk7 = Resblk(256,512,2,1)
        self.blk8 = Resblk(512,512,1,1)
        #池化操作
        self.avgpool = nn.AdaptiveAvgPool2d(output_size=(1, 1))
        #全连接层
        self.fc = nn.Linear(512,2) #2类
 
    def forward(self,x):
        #输入 224*224*3 输出 64*56*56
        #7*7 conv + maxpool
        x = self.preconv(x)
 
        #第一个残差块
        #输入 224*224*3 输出 64*56*56
        x = self.blk1(x)
        #第二个残差块
        #输入 64*56*56 输出 64*56*56
        x = self.blk2(x)
        #第三个残差块 + 1*1 subsample
        #输入 64*56*56 输出 128*28*28
        x = self.conv1(x) + self.blk3(x)
        #第四个残差块
        #输入 128*28*28 输出 128*28*28
        x = self.blk4(x)
        #第五个残差块 + 1*1 subsample
        #输入 128*28*28 输出 256*14*14
        x = self.conv2(x) + self.blk5(x)
        #第六个残差块
        #输入 256*14*14 输出 256*14*14
        x = self.blk6(x)
        #第七个残差块
        #输入 256*14*14 输出 512*7*7
        x = self.conv3(x) + self.blk7(x)
        #第八个残差块
        #输入 512*7*7 输出 512*7*7
        x = self.blk8(x)
        #平均池化 512*7*7-> 512*1*1
        x = self.avgpool(x)
        #Flatten 打平操作 后面俩维合并成一维
        x = x.view(x.size(0),-1) #[512,1]
        #全连接层 512,1 -> 1,1000
        x = self.fc(x)
         
        return x

请添加图片描述

三、思考题

1、Residual learning

神经网络叠的越深,学习出的效果不一定越好。因为当模型层数增加到某种程度,模型的效果将会不升反降,即发生了退化情况。退化原因可能是梯度爆炸或梯度消失现象,具体来说,反向传播结果的数值大小不止取决于求导的式子,很大程度上也取决于输入的模值。当计算图每次输入的模值都大于1,那么经过很多层回传,梯度将不可避免地呈几何倍数增长直到Nan,这就是梯度爆炸现象。如果我们每个阶段输入的模恒小于1,那么梯度也将不可避免地呈几何倍数下降直到0,这就是梯度消失现象。值得一提的是,由于人为的参数设置,梯度更倾向于消失而不是爆炸。
如果深层网络后面的层都是是恒等映射,那么模型就可以转化为一个浅层网络。那现在的问题就是如何得到恒等映射了。事实上,已有的神经网络很难拟合潜在的恒等映射函数H(x) = x。但如果把网络设计为H(x) = F(x) + x,即直接把恒等映射作为网络的一部分。就可以把问题转化为学习一个残差函数F(x) = H(x) - x.只要F(x)=0,就构成了一个恒等映射H(x) = x。 而且,拟合残差至少比拟合恒等映射容易得多。
请添加图片描述

2、Batch Normailization 的原理

Batch normalization 的 batch 是批数据, 把数据分成小批小批进行 stochastic gradient descent. 而且在每批数据进行前向传递 forward propagation 的时候, 对每一层都进行 normalization 的处理。
归一化的好处是:
(1)神经网络学习过程本质就是为了学习数据分布,一旦训练数据与预测数据分布不同,那么网络的泛化能力也大大降低。
(2)若训练过程中每批batch的数据分布也各不相同,那么网络每次迭代学习过程就会出现较大波动,使之更难趋于收敛,降低训练收敛速度。对于深层网络,网络前几层的微小变化都会被网络累积放大,则训练数据分布变化问题就会被放大,更加影响训练速度。一旦每批训练数据的分布各不相同(batch梯度下降),那么网络就要在每次迭代都去学习适应不同的分布,这样会大大降低网络的训练速度。

3、为什么分组卷积可以提升准确率?即然分组卷积可以提升准确率,同时还能降低计算量,分数数量尽量多不行吗?

(1)高效训练。因为卷积被分成了多个路径,每个路径都可由不同的 GPU 分开处理,所以模型可以并行方式在多个 GPU 上进行训练。相比于在单个 GPU 上完成所有任务,这样的在多个 GPU 上的模型并行化能让网络在每个步骤处理更多图像。
(2)模型会更高效。模型参数会随过滤器分组数的增大而减少,完整的标准 2D 卷积有 h x w x Din x Dout 个参数。具有 2 个过滤器分组的分组卷积有 (h x w x Din/2 x Dout/2) x 2 个参数,参数数量减少了一半。

分组数k越多,卷积操作的总参数量和总计算量就越少(减少k倍)。然而分组卷积有一个致命的缺点就是不同分组的通道间减少了信息流通,即输出的feature maps只考虑了输入特征的部分信息。解决方法是在分组卷积之后进行信息融合操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值