第J4周:ResNet与DenseNet结合--DPN(pytorch版)

>- **🍨 本文为[🔗365天深度学习训练营]中的学习记录博客**
>- **🍖 原作者:[K同学啊]**

 本人往期文章可查阅: 深度学习总结

📌本周任务:📌

● 任务类型:自主探索⭐⭐

● 任务难度:偏难

●任务描述:

1、请根据J1~J3周的内容自有探索ResNet与DenseNet结合的可能性

2、是否可以根据两种特性构建一个新的模型框架?

3、请用之前的任一图像识别任务验证改进后模型的效果

🏡 我的环境:

  • 语言环境:Python3.8
  • 编译器:Jupyter Notebook
  • 深度学习环境:Pytorch
    • torch==2.3.1+cu118
    • torchvision==0.18.1+cu118

一、论文导读

论文:Dual Path Networks
论文链接:https://arxiv.org/abs/1707.01629
代码:https://github.com/cypw/DPNs
MXNet框架下可训练模型的DPN代码:https://github.com/miraclewkf/DPN

残差网络[2]和DenseNet[3]是short-cut系列网络的最为经典的两个基础网络,其中残差网络通过单位加的方式直接将输入加到输出的卷积上,DenseNet则是通过拼接的方式将输出与之后的每一层的输入进行拼接。

DPN(Dual Path Networks)是一种网络结构,它结合了DensNet和ResNetXt两种思想的优点。这种结构的目的是通过不同的路径来利用神经网络的不同特性,从而提高模型的效率和性能。

DenseNet 的特点是其稠密连接路径,使得网络能够在不同层级之间持续地探索新的特征。这种连接方式允许网络在不增加参数的情况下学习到更丰富的特征表示。
ResNeXt(残差分组卷积)则是通过残差路径实现特征的复用,这有助于减少模型的大小和复杂度。
DPN的设计思想在于融合这两种思想,通过两个并行的路径来进行信息传递:

一条路径是通过DenseNet的方式,即通过稠密连接路径,这样可以持续地探索新的特征。
另一条路径是通过ResNeXt的方式,即通过残差路径,可以实现特征的复用。
此外,DPN使用了分组卷积来降低计算量,并且可以在不改变原有网络结构的前提下,提升性能,使其适合用于检测和分割任务作为新的Backbone网络。

总结:DPN可以说是融合了ResNeXt和DenseNet的核心思想:Dual Path Network(DPN)以ResNet为主要框架,保证了特征的低冗余度,并在其基础上添加了一个非常小的DenseNet分支,用于生成新的特征。

那么DPN到底有哪些优点呢?可以看以下两点:
1、关于模型复杂度,作者的原文是这么说的:The DPN-92 costs about 15% fewer parameters than ResNeXt-101 (32 4d), while the DPN-98 costs about 26% fewer parameters than ResNeXt-101 (64 4d).
2、关于计算复杂度,作者的原文是这么说的:DPN-92 consumes about 19% less FLOPs than ResNeXt-101(32 4d), and the DPN-98 consumes about 25% less FLOPs than ResNeXt-101(64 4d).

由上图可知,其实DPN和ResNeXt(ResNet)的结构很相似。最开始一个7*7的卷积层和max pooling层,然后是4个stage,每个stage包含几个sub-stage(后面会介绍),再接着是一个global average pooling和全连接层,最后是softmax层。重点在于stage里面的内容,也是DPN算法的核心。

因为DPN算法简单讲就是将ResNeXt和DenseNet融合成一个网络,因此在介绍DPN的每个stage里面的结构之前,先简单过一下ResNet(ResNeXt和ResNet的子结构在宏观上是一样的)和DenseNet的核心内容。

下图中的(a)是ResNet的某个stage中的一部分。(a)的左边竖着的大矩形框表示输入输出内容,对一个输入x,分两条线走,一条线还是x本身,另一条线是x经过1×1卷积,3×3卷积,1×1卷积(这三个卷积层的组合又称作bottleneck),然后把这两条线的输出做一个element-wise addition,也就是对应值相加,就是(a)中的加号,得到的结果又变成下一个同样模块的输入,几个这样的模块组合在一起就成了一个stage(比如Table1中的conv3)。

(b)表示DenseNet的核心内容。(b)的左边竖着的多边形框表示输入输出内容,对输入x,只走一条线,那就是经过几层卷积后和x做一个通道的合并(cancat),得到的结果又成了下一个小模块的输入,这样每一个小模块的输入都在不断累加,举个例子:第二个小模块的输入包含第一个小模块的输出和第一个小模块的输入,以此类推。

DPN是怎么做呢?简单讲就是将Residual Network 和 Densely Connected Network融合在一起。下图中的(d)和(e)是一个意思,所以就按(e)来讲吧。(e)中竖着的矩形框和多边形框的含义和前面一样。具体在代码中,对于一个输入x(分两种情况:一种是如果x是整个网络第一个卷积层的输出或者某个stage的输出,会对x做一个卷积,然后做slice,也就是将输出按照channel分成两部分:data_o1和data_o2,可以理解为(e)中竖着的矩形框和多边形框;另一种是在stage内部的某个sub-stage的输出,输出本身就包含两部分:data_o1和data_o2),走两条线,一条线是保持data_o1和data_o2本身,和ResNet类似;另一条线是对x做1×1卷积,3×3卷积,1×1卷积,然后再做slice得到两部分c1和c2,最后c1和data_o1做相加(element-wise addition)得到sum,类似ResNet中的操作;c2和data_o2做通道合并(concat)得到dense(这样下一层就可以得到这一层的输出和这一层的输入),也就是最后返回两个值:sum和dense。
以上这个过程就是DPN中 一个stage中的一个sub-stage。有两个细节,一个是3×3的卷积采用的是group操作,类似ResNeXt,另一个是在每个sub-stage的首尾都会对dense部分做一个通道的加宽操作。

作者在MXNet框架下实现了DPN算法,具体的symbol可以看:https://github.com/cypw/DPNs/tree/master/settings,介绍得非常详细也很容易读懂。

实验结果:
Table2是在ImageNet-1k数据集上和目前最好的几个算法的对比:ResNet,ResNeXt,DenseNet。可以看出在模型大小,GFLOP和准确率方面DPN网络都更胜一筹。不过在这个对比中好像DenseNet的表现不如DenseNet那篇论文介绍的那么喜人,可能是因为DenseNet的需要更多的训练技巧。

Figure3是关于训练速度和存储空间的对比。现在对于模型的改进,可能准确率方面的提升已经很难作为明显的创新点,因为幅度都不大,因此大部分还是在模型大小和计算复杂度上优化,同时只要准确率还能提高一点就算进步了。 

总结:
作者提出的DPN网络可以理解为在ResNeXt的基础上引入了DenseNet的核心内容,使得模型对特征的利用更加充分。原理方面并不难理解,而且在跑代码过程中也比较容易训练,同时文章中的实验也表明模型在分类和检测的数据集上都有不错的效果。 

参考文章:

DPN(Dual Path Network)算法详解_dpn(dual path network)算法详解-CSDN博客

DPN网络-CSDN博客

DPN详解(Dual Path Networks) - 知乎 (zhihu.com)

解读Dual Path Networks(DPN,原创) - 知乎 (zhihu.com)

二、 前期准备

1. 设置GPU

如果设备上支持GPU就使用GPU,否则使用CPU

import warnings
warnings.filterwarnings("ignore") #忽略警告信息

import torch
device=torch.device("cuda" if torch.cuda.is_available() else "CPU")
device

运行结果:

device(type='cuda')

2. 导入数据

import pathlib
data_dir=r'D:\THE MNIST DATABASE\J-series\J1\bird_photos'
data_dir=pathlib.Path(data_dir)

img_count=len(list(data_dir.glob('*/*')))
print("图片总数为:",img_count)

运行结果:

图片总数为: 565

3. 查看数据集分类

data_paths=list(data_dir.glob('*'))
classNames=[str(path).split('\\')[5] for path in data_paths]
classNames

运行结果:

['Bananaquit', 'Black Skimmer', 'Black Throated Bushtiti', 'Cockatoo']

4. 随机查看图片

随机抽取数据集中的10张图片进行查看

import PIL,random
import matplotlib.pyplot as plt
from PIL import Image
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号

data_paths2=list(data_dir.glob('*/*'))
plt.figure(figsize=(20,4))
plt.suptitle("OreoCC的案例",fontsize=15)
for i in range(10):
    plt.subplot(2,5,i+1)
    plt.axis("off")
    image=random.choice(data_paths2) #随机选择一个图片
    plt.title(image.parts[-2]) #通过glob对象取出他的文件夹名称,即分类名
    plt.imshow(Image.open(str(image)))  #显示图片

运行结果: 

 

5. 图片预处理    

import torchvision.transforms as transforms
from torchvision import transforms,datasets

train_transforms=transforms.Compose([
    transforms.Resize([224,224]), #将图片统一尺寸
    transforms.RandomHorizontalFlip(), #将图片随机水平翻转
    transforms.RandomRotation(0.2), #将图片按照0.2的弧度值随机旋转
    transforms.ToTensor(), #将图片转换为tensor
    transforms.Normalize(  #标准化处理->转换为正态分布,使模型更容易收敛
        mean=[0.485,0.456,0.406],
        std=[0.229,0.224,0.225]
    )
])

total_data=datasets.ImageFolder(
    r"D:\THE MNIST DATABASE\J-series\J1\bird_photos",
    transform=train_transforms
)
total_data

运行结果: 

Dataset ImageFolder
    Number of datapoints: 565
    Root location: D:\THE MNIST DATABASE\J-series\J1\bird_photos
    StandardTransform
Transform: Compose(
               Resize(size=[224, 224], interpolation=bilinear, max_size=None, antialias=True)
               RandomHorizontalFlip(p=0.5)
               RandomRotation(degrees=[-0.2, 0.2], interpolation=nearest, expand=False, fill=0)
               ToTensor()
               Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
           )

将数据集分类情况进行映射输出:

total_data.class_to_idx

运行结果:

{'Bananaquit': 0,
 'Black Skimmer': 1,
 'Black Throated Bushtiti': 2,
 'Cockatoo': 3}

6. 划分数据集

train_size=int(0.8*len(total_data))
test_size=len(total_data)-train_size

train_dataset,test_dataset=torch.utils.data.random_split(
    total_data,[train_size,test_size]
)
train_dataset,test_dataset

运行结果:

(<torch.utils.data.dataset.Subset at 0x270de0de310>,
 <torch.utils.data.dataset.Subset at 0x270de0de950>)

查看训练集和测试集的数据数量:

train_size,test_size

运行结果:

(452, 113)

7. 加载数据集

batch_size=16
train_dl=torch.utils.data.DataLoader(
    train_dataset,
    batch_size=batch_size,
    shuffle=True,
    num_workers=1
)
test_dl=torch.utils.data.DataLoader(
    test_dataset,
    batch_size=batch_size,
    shuffle=True,
    num_workers=1
)

查看测试集的情况:

for x,y in train_dl:
    print("Shape of x [N,C,H,W]:",x.shape)
    print("Shape of y:",y.shape,y.dtype)
    break

运行结果:

Shape of x [N,C,H,W]: torch.Size([16, 3, 224, 224])
Shape of y: torch.Size([16]) torch.int64

二、手动搭建DPN模型

1、搭建DPN模型

import torch
import torch.nn as nn

class Block(nn.Module):
    """
    param:in_channel--输入通道数
    mid_channel--中间经历的通道数
    out_channel--ResNet部分使用的通道数(sum操作,这部分输出仍然是out_channel 1个通道)
    dense_channel--DenseNet部分使用的通道数(concat操作,这部分输出是2*dense_channel 1个通道)
    groups--conv2中的分组卷积参数
    is_shortcut--ResNet前是否进行shortcut操作
    """
    def __init__(self,in_channel,mid_channel,out_channel,dense_channel,stride,groups,is_shortcut=False):
        super(Block,self).__init__()
        
        self.is_shortcut=is_shortcut
        self.out_channel=out_channel
        self.conv1=nn.Sequential(
            nn.Conv2d(in_channel,mid_channel,kernel_size=1,bias=False),
            nn.BatchNorm2d(mid_channel),
            nn.ReLU()
        )
        
        self.conv2=nn.Sequential(
            nn.Conv2d(mid_channel,mid_channel,kernel_size=3,stride=stride,padding=1,groups=groups,bias=False),
            nn.BatchNorm2d(mid_channel),
            nn.ReLU()
        )
        
        self.conv3=nn.Sequential(
            nn.Conv2d(mid_channel,out_channel+dense_channel,kernel_size=1,bias=False),
            nn.BatchNorm2d(out_channel+dense_channel)
        )
        
        if self.is_shortcut:
            self.shortcut=nn.Sequential(
                nn.Conv2d(in_channel,out_channel+dense_channel,kernel_size=3,padding=1,stride=stride,bias=False),
                nn.BatchNorm2d(out_channel+dense_channel)
            )
            
        self.relu=nn.ReLU(inplace=True)
        
    def forward(self,x):
        a=x
        x=self.conv1(x)
        x=self.conv2(x)
        x=self.conv3(x)
        if self.is_shortcut:
            a=self.shortcut(a)
            
        #a[:,:self.out_channel,:,:]+[:,:self.out_channel,:,:]是使用ResNet的方法,
        #即采用sum的方式将特征图进行求和,通道数不变,都是out-channel个通道
        #[a[:,self.out_channel,:,:],x[:,self.out_channel:,:,:]]是使用DenseNet的方法,
        #即采用concat的方式将特征图在channel维度上直接进行叠加,通道数加倍,即2*dense_channel
        x=torch.cat([a[:,:self.out_channel,:,:]+x[:,:self.out_channel,:,:],
                     a[:,self.out_channel:,:,:],x[:,self.out_channel:,:,:]],dim=1)
        x=self.relu(x)
        
        return x
    
class DPN(nn.Module):
    def __init__(self,cfg):
        super(DPN,self).__init__()
        
        self.group=cfg['group']
        self.in_channel=cfg['in_channel']
        mid_channels=cfg['mid_channels']
        out_channels=cfg['out_channels']
        dense_channels=cfg['dense_channels']
        num=cfg['num']
        
        self.conv1=nn.Sequential(
            nn.Conv2d(3,self.in_channel,7,stride=2,padding=3,bias=False,padding_mode='zeros'),
            nn.BatchNorm2d(self.in_channel),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3,stride=2,padding=0)
        )
        self.conv2=self._make_layers(mid_channels[0],out_channels[0],dense_channels[0],num[0],stride=1)
        self.conv3=self._make_layers(mid_channels[1],out_channels[1],dense_channels[1],num[1],stride=2)
        self.conv4=self._make_layers(mid_channels[2],out_channels[2],dense_channels[2],num[2],stride=2)
        self.conv5=self._make_layers(mid_cha
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值