【Pytorch】动手学深度学习-梳理与笔记小结|现代卷积神经网络

现代卷积神经网络

电子版教材传送门
p.s. 因为这一章没有对代码过多的拓展与实现,可以参照官方给出的代码

深度卷积神经网络(AlexNet)

【背景】
早期的卷积神经网络和一些流行的机器学习方法相比未受到重视,就是因为——
①卷积网络的输入往往是原始或者简单预处理后的像素级输入,而机器学习方法中的输入是更加精细、鲁棒的特征;
②神经网络训练的一些关键技巧如启发式参数初始化、随机梯度下降的变体、非挤压激活函数和有效正则化技术在当时仍然缺失;

【图像领域的关键影响因素】
数据特征相较于学习算法更加能够推动该领域的进步,因此我们需要更大的纯净数据集、更好的特征提取技巧…

【AlexNet设计特点】
①使用ReLU激活替代Sigmoid函数,计算更加简单,且该函数在正区间的梯度总是1,对于不同的参数初始化方法都可以使模型得到有效训练;
②使用Dropout控制全连接层的模型复杂度;
③使用如反转、裁切和变色等图像增强方法,使模型更加健壮且可以有效减少过拟合;
在这里插入图片描述
Dropout、ReLU、预处理是提升计算机视觉任务性能的关键步骤;
【附录与补充】
dropout原理与实现


使用块的网络(VGG)

【动机与发展】
为了可以提供一个通用的模板来指导后续的研究人员设计新的网络,神经网络架构的设计也逐渐抽象化:研究人员从单个神经元开始思考,并往整个网络层、网络块、重复层的模式进行发展。
将网络用块的形式封装起来,就可以通过循环和子程序的形式实现重复的架构。

【VGG】
可以使用一个超参数变量conv_arch记录每个vgg_block中卷积层的个数和输出的通道数;
VGG-11含有8个卷积层和3个全连接层(FC的设计和AlexNet一致)
在这里插入图片描述
【重要结论】
通过对各种架构的尝试,发现:深层且窄的卷积相较于浅层且宽的卷积更加有效


网络中的网络(NiN)

【动机与背景】
前文所述的网络基本遵循深度卷积用以提取特征+全连接层用以进行表征后续处理的模式,但是FC的设计可能会忽视特征之间的空间结构;
NiN对上述问题考虑在每个像素的通道上分别使用多层感知机;

【NiN】
①Idea:考虑在每个像素位置(每个高度和宽度)应用一个全连接层,针对不同空间位置(通道)的像素进行权重连接,其实也就是1x1卷积层。
②设计特点:

  • 相较于AlexNet,其完全取消了全连接层,将其替换成全局平均池化层,在减少参数的同时防止过拟合;
  • 该网络的子块由一个卷积层和多个1x1卷积层共同组成

在这里插入图片描述


含并行连结的网络(GoogleNet)

【Inception块】
①有效性:因为有各种尺寸的滤波器(filter)进行组合,可以有效识别不同空间范围的图像细节;
②具体结构:
在这里插入图片描述


【GoogleNet】
在这里插入图片描述


批量规范化(BN)

【结论】
所谓的批量规范化,就是——在模型的训练过程中,利用小批量的均值和标准差,不断调整神经网络的中间输出,使整个神经网络各层的中间输出值更加稳定;

Tips

【动机与背景】
深层神经网络的训练因为收敛而十分困难,残差块和批量归一化使得深层网络的训练得以实现;

以及在对神经网络进行训练时,存在以下问题——
①数据预处理的方式会对最终的结果产生巨大影响:比如在训练之前先对输入特征进行标准化,可以使得其余优化器更好地配合,将参数的量级进行统一;
②在对模型进行训练时,中间层的变量的数据范围会发生变化,导致参数的分布也发生偏移,这使得大部分猜想这会影响网络的收敛,因为它需要对学习率进行补偿调整;
③对于深层网络,为了防止过拟合,需要加入一些正则化措施

【批量规范化的原理】
因为中间层的参数变化幅度不能过于剧烈,因此批量规范化的思想就是使得每一层参数都主动居中(标准化);
因为批量规范化是对整个小批量内的数据进行均值和方差的计算,因此对于大小为1的minibatch进行规范化是没有意义的——只有足够大的minibatch才能使得该方法是有效且稳定的。
在这里插入图片描述

Codes&Homework

Ref:①动手学深度学习(二十三)——批量归一化(BN)
对于代码运行过程中torchvision的报错解决

T1:使用BN层前面的FC或CNN可以将偏置项移除,因为在BN层中的减去均值的中心化操作相当于对网络变量分布中的直流分量进行了滤除;

T3:BN层主要是用于网络的中间层用于均衡参数的分布,而在网络的最后输出层——如果对应于分类任务,那么我们只需要求解argmax即可,BN的操作让不同数值的分布更加平滑,但对于最终结果确定没有任何意义;如果对应于回归任务,BN也可能会导致网络学习到的一个特征分布发生改变,因此也不适用于进行BN操作;

T4:(只是自己的想法,不能确定正确性)因为dropout的实现中是随机化在[0,1]范围内生成一个分布向量,通过这个分布来确定哪些参数(神经元)得以保留,是一个没有任何先验的随机采样过程
而BN的计算过程(如下面的代码batch_norm这个函数中所示)是将整个输入的向量通过归一化到[0,1]范围内,那么这个归一化后的向量反映的就是参数的数值分布,若用这个分布来决定原始参数的保留与否——相当于是优先保留那些参数值比较大的部分(认为学习到了有效的特征和权值),此时我们只需要指定一个权重阈值retain_prob来表示多大数值的参数可以被保留下来
从另一观点来说,dropout的目的是在于正则化防止神经元过多造成过拟合,而batchnorm也有正则化的作用。

T5:随着网络层数的加深,gamma和beta的数值分别稳定在[1,2]和0附近,说明随着训练网络层数的增多,参数的分布的确会发生一定的偏移,而BN计算也的确可以对分布偏移进行矫正

import torch
from torch import nn
from d2l import torch as d2l
import numpy as np
import matplotlib.pyplot as plt

#实现一个具有张量的批量规范化层
def batch_norm(x,gamma,beta,moving_mean,moving_var,eps,momentum):
    #对训练/预测模式进行判断
    if not torch.is_grad_enabled():
        #在预测模式下,直接使用传入的移动平均所得的均值和方差
        x_hat = (x- moving_mean) / torch.sqrt(moving_var+eps)
    else:
        assert len(x.shape) in (2,4)
        if len(x.shape) == 2:#全连接[B,L]
            mean = x.mean(dim = 0)
            var = ((x-mean) ** 2).mean(dim = 0,keepdim = True)
        else:#[B,C,H,W]
            mean = x.mean(dim = (0,2,3),keepdim = True)
            var = ((x-mean) ** 2).mean(dim = (0,2,3),keepdim = True)
        x_hat = (x-mean) / torch.sqrt(var+eps)
        #更新移动平均的均值和方差
        moving_mean = momentum * moving_mean + (1.0-momentum) * mean
        moving_var = momentum * moving_var + (1.0 - momentum) *var
    Y = gamma * x_hat + beta #进行缩放和移位
    return Y,moving_mean.data,moving_var.data

#利用删除定义的算法函数实现一个自定义的BN层:
#应当保持gamma和beta参数,并且在训练中要保持更新
#并且对均值和方差的移动平均结果也进行保留,以便后续进行使用

'''
[实现自定义网络层的基础设计模式]
①首先用一个单独的函数定义数学原理(比如上面的batch_norm)
②然后将这个功能集成到一个自定义网络层中,代码需要处理——
数据在训练设备上的移动/变量的初始化/变量和值的追踪
'''

class BatchNorm(nn.Module):
    # num_features表示和其连结的前一层的输出通道数
    # num_dims表示前一层是FC还是CNN
    def __init__(self,num_features,num_dims):
        super().__init__()
        #shape是进行mean运算后的张量维度
        if num_dims == 2:
            shape = (1,num_features)
        else:
            shape = (1,num_features,1,1)
        #对拉伸和偏移的参数进行初始化
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        # 对需要后续追踪的参数数值进行占位和初始化
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.ones(shape)

    def forward(self,x):
        # 需要将追踪和数值和输入的数据放在同一个设备上
        if self.moving_mean.device != x.device:
            self.moving_mean = self.moving_mean.to(x.device)
            self.moving_var = self.moving_var.to(x.device)
        #对更新后的统计量和追踪保存
        Y, self.moving_mean,self.moving_var = batch_norm(x,self.gamma,self.beta,self.moving_mean,self.moving_var,eps = 1e-5,momentum=0.9)
        return Y

#使用FashionMNIST来训练加上了BN层后改进的LeNet网络
net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
    nn.Linear(16*4*4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(),
    nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(),
    nn.Linear(84, 10))

lr, num_epochs, batch_size = 1.0, 10, 256
#因为有BN层的调整,学习率相较于原始LeNet训练时显著增大
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

#查看第一个BN层中学习到两个参数
gamma1 = net[1].gamma.data.reshape((-1,)).numpy()
print('gamma1:',net[1].gamma.reshape((-1,)))
print('beta1:',net[1].beta.reshape((-1,)))

print('gamma1:',net[5].gamma.reshape((-1,)))
print('beta1:',net[5].beta.reshape((-1,)))

print('gamma1:',net[10].gamma.reshape((-1,)))
print('beta1:',net[10].beta.reshape((-1,)))

print('gamma1:',net[13].gamma.reshape((-1,)))
print('beta1:',net[13].beta.reshape((-1,)))
#对gamma和beta变量进行可视化
# sp = np.arange(-5,5,1)
# plt.bar(gamma1,sp,width=0.1)
# plt.xticks(gamma1)
# plt.show()

#上面自定义的BatchNorm类在torch.nn中封装实现
nn.BatchNorm2d(num_features=2,eps=1e-5,momentum=0.9)


#-----------------Homework---------------------
#dropout的官方源码实现
def dropout(x,level):
    #level用于指定神经元丢弃的概率
    if level < 0 or level >=1:
        raise Exception('Dropout level must be in interval [0,1].')
    retain_prob = 1. - level
    sample = np.random.binormal(n = 1,p = retain_prob,size = x.shape)
    #生成一个和输入数据等长的在[0,1]范围内的分布向量,n为每个神经元参与的次数
    x *= sample
    x /= retain_prob
    return x


残差网络(ResNet)

【小结】

  • 残差映射可以更容易地学习同一个函数,比如将权重层参数都置为0;
  • 残差块的设计和引入使得深层神经网络的训练更加有效——输入可以通过层次间的残余连接更快地向前传播;

Tips

【动机与背景】
①前文都是对于网络块的抽象,以及如何将网络块搭建成深层次网络(比如串行、并行等)进行探讨,现在需要理解“新添加的网络层应该怎样提升神经网络的性能”;
②如果把神经网络训练看做是一个mapping的优化寻找问题,问题可以按照如下形式建模——
在这里插入图片描述
要使得我们在给定架构F中找到的最优解fF*尽可能接近于全局最优解f*,就需要使得寻找范围F尽可能地接近全局最优解,我们往往会采取不断扩大寻找范围的方法,但这个方法却并不一定能保证正确——
在这里插入图片描述
③基于上述问题,我们希望我们希望设计出来的较为复杂的函数类能够包含较小的函数类,若将新添加的层训练成恒等映射,则可以保证新模型和源模型是同样有效的,同时新模型还能够探索得到更优的解来拟合数据集。


【残差块与残差网络】
①残差网络的核心思想:每个附加层都应该更容易地包含原始函数作为其元素之一;
在这里插入图片描述
②ResNet前两层和GoogleNet中的设计一样,但是在卷积层后先加入了BN运算再进行最大池化;而GoogleNet中接的4个由Inception块组成的模块,ResNet使用4个残差模块:
ResNet只通过每个残差块中步幅为2的最大池化来减小高和宽,之后层级连接的每个残差块将一个模块中的通道数翻倍。
在这里插入图片描述
③ 虽然ResNet的主体架构跟GoogLeNet类似,但ResNet架构更简单,修改也更方便。这些因素都导致了ResNet迅速被广泛使用

Codes&Homework

T1:Inception Module v.s. Residual Block
①两者的设计理念不一样,前者是对网络块搭建复杂深度网络的一种探索,后者是对怎样使得深度复杂网络更加有效的一种思考;
②前者是一个多分支并行的模块,主要是为了使用不同尺寸的卷积核以得到多尺度特征;后者本质上就只是一个单分支的网络块(skpi connection的跨层连接是为了更好传达输入数据)

T2:不同的残差网络的模块设计(超参数的不同配置和选择)
在这里插入图片描述
T3:瓶颈层的原理及其实现
Ref:CNBlog-ResNet详解与实现在这里插入图片描述
首先,无论是左边还是右边的单元,都是由卷积层和激活函数构成的。左边的单元由2个大小为3x3的卷积层与2个ReLU激活函数构成,右边的单元由2个1x1、1个3x3的卷积层和3个ReLU激活函数构成。至于卷积核的的通道(channel)数,则由单元的具体位置决定。通常卷积神经网络在提取图像特征的过程种,卷积核都会从“大卷积核、低通道数量“层层递进到到”小卷积核、多通道数量“。因此,在实际应用的过程种,短连接跨越的卷积层的通道数量有所改变是非常常见的。原论文中给出了直接连接、填零连接和映射连接三种具体的短连接形式。当实际单元的输入通道数与输出相同时,如上图左,则短连接直接连接即可。若输入通道数与输出不相同,如上图右,输入的维度为64,输出却是256,此时输入x无法与F(x)直接相加。填零连接就是将多出的维度全部填充0后再进行相加,这个方法不会引入额外的参数。映射连接则是通过矩阵变换,利用大小为1x1的卷积层扩展输入的维度后再进行相加。

T4:为了增加残差网络块的表达能力
具体参考文章《ResNet残差网络及变体详解》

T5:限制函数的复杂性——防止拟合出来的函数过拟合,不具有泛化性能;计算资源的有限性。

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

class Residual(nn.Module):
    '''
    残差块计算逻辑的实现,包含是否使用1x1卷积来处理两个卷积层输入出通道不一致的情况
    '''
    def __init__(self,input_channels,num_channels,use_1x1conv = False,strides = 1):
        '''
        初始化残差块
        :param input_channels: 输入数据的通道数
        :param num_channels: 两个卷积层的输出通道数,
        :param use_1x1conv: 是否使用1x1卷积,取决于input_channels == num_channels
        :param strides: 卷积运算的步长
        '''
        super().__init__()
        self.conv1 = nn.Conv2d(input_channels,num_channels,kernel_size=3,padding=1,stride=strides)
        self.conv2 = nn.Conv2d(num_channels,num_channels,kernel_size=3,padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2d(input_channels,num_channels,kernel_size=1,stride=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)

    def forward(self,x):
        y = F.relu(self.bn1(self.conv1(x)))
        y = self.bn2(self.conv2(y))
        if self.conv3:
            x = self.conv3(x)
        y += x
        return F.relu(y)

#计算验证
blk = Residual(3,3)
#若x的C与Residual的out_channels则需要使得use_conv1x1 = True
#通过改变stride可以改变输出数据的[H,W]维度
x = torch.rand(4,3,6,6)#[B,C,H,W]
y = blk(x)
print('残差块计算后的输出:',y)
print('残差块计算后的输出大小:',y.shape)

#对ResNet进行实现
#ResNet的前两层和GoogleNet一致,额外在卷积层后加入BN
b1 = nn.Sequential(nn.Conv2d(1,64,kernel_size=7,stride=2,padding=3),
                   nn.BatchNorm2d(64),nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3,stride=2,padding=1))

def resnet_block(input_channels,num_channels,num_residuals,first_block=False):
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.append(Residual(input_channels,num_channels,use_1x1conv=True,strides=2))
        else:
            blk.append(Residual(num_channels,num_channels))
    return blk

#在ResNet中加入所有残差块,每个模块使用2个残差块
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))

#在所有残差块之后,通过全局平均池化汇总特诊图,并FC进行分类输出
net = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1,1)),
                    nn.Flatten(), nn.Linear(512, 10))

#模型前向传播验证
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

#模型训练
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

#-----------------------Homework---------------------------------------
#基于论文的描述对于ResNet进行复现,其中对于shortcut和bottleneck都有具体的实现
#并且可以根据论文中给出的关于ResNet[18,34,50,101,152]的超参数均进行网络实例化
class ResidualBlock(nn.Module):
    expansion = 1
    #扩展系数,表示输入出通道数之比值
    def __init__(self,in_channels,out_channels,stride = 1):
        super(Residual,self).__init__()
        self.conv1 = nn.Conv2d(in_channels,out_channels,kernel_size=3,
                               stride = stride,padding = 1,bias = False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels,out_channels,kernel_size=3,
                               stride = 1,padding=1,bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.shortcut = nn.Sequential()

        if stride != 1 or in_channels != out_channels*self.expansion:
            self.shortcut = nn.Sequential(#等效于use_1x1conv的判断
                nn.Conv2d(in_channels, out_channels * self.expansion, kernel_size=1,
                          stride=stride, bias=False),
                nn.BatchNorm2d(out_channels * self.expansion)
            )

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = self.bn2(self.conv2(x))
        x = x + self.shortcut(x)
        out = F.relu(x)

        return out

class Bottleneck(nn.Module):
    #瓶颈层的实现
    #也就是深层设计下的ResNet Block
    expansion = 4#只有深层ResNet的设计中需要使用Bottleneck,且此时输出通道存在倍数关系
    def __init__(self,in_channels,out_channels, stride = 1):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(in_channels,out_channels,kernel_size=1,bias=False)
        #在这里的实现中对于卷积层都设定bias=False,是因为BN操作的特性?
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(in_channels,out_channels,kernel_size=3,
                               stride = stride,padding=1,bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.conv3 = nn.Conv2d(out_channels,self.expansion * out_channels,kernel_size=1,bias=False)
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != self.expansion * out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, self.expansion * out_channels,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(self.expansion * out_channels)
                #对于在有expansion机制下输入出维度依然不“匹配”的情况使用1x1卷积将输入维度改成输出维度
            )

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = F.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        out += self.shortcut(x)
        out = F.relu(out)
        return out
#搭建具体的ResNet网络
class ResNet(nn.Module):
    def __init__(self,block,num_blocks,num_classes):
        super(ResNet,self).__init__()
        self.in_channels = 64
        self.conv1 = nn.Conv2d(3,64,kernel_size=3,stride=1,padding=1,bias=False)
        self.bn1 = nn.BatchNorm2d(64)

        self.layer1 = self.__make_layer(block,64,num_blocks[0],stride = 1)
        self.layer2 = self.__make_layer(block, 128, num_blocks[1], stride=2)
        self.layer3 = self.__make_layer(block, 256, num_blocks[2], stride=2)
        self.layer4 = self.__make_layer(block, 512, num_blocks[3], stride=2)
        self.fc = nn.Linear(512 * block.expansion, num_blocks)

    def __make_layer(self,block,out_channels,num_blocks,stride):
        strides = [stride] + [1] * (num_blocks-1)
        #每个残差网络块的首层可以自己选择stride,后续各CNN的stride均为1
        #因为使用步幅为2pool来减小尺寸,所以卷积运算中无需减小高和块
        layers = []
        for s in strides:
            layers.append(block(self.in_channels,out_channels,s))
            self.in_channels = out_channels*block.expansion
        return nn.Sequential(*layers)

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = F.avg_pool2d(x, 4)
        out = self.fc(x)

        return out
#对论文中给出的5种ResNet进行实例化
resnet18 = ResNet(ResidualBlock, [2, 2, 2, 2])
resnet34 = ResNet(ResidualBlock, [3, 4, 6, 3])
resnet50 = ResNet(Bottleneck, [3, 4, 6, 3])
resnet101 = ResNet(Bottleneck, [3, 4, 23, 3])
resnet152 = ResNet(Bottleneck, [3, 8, 36, 3])

稠密连接网络(DenseNet)

【小结】

  • DenseNet通过稠密块和过渡层来构建,其中过渡层用于控制网络的维度,从而可以减少通道数量,精简模型的尺寸;
  • DenseNet与ResNet的不同之处在于:在跨层连接中后者将输入和输出直接相加,而前者在通道维度将输入和输出进行concat

Tips

【从ResNet到DenseNet】
①前者可以看做是将f分解成两项;
②后者则是提供了将f分解成多项的一种方法;
③两个的根本区别在于对输入输出连结的方式,前者简单相加,后者在维度上进行concat
在这里插入图片描述
【DenseNet】
①名字来源于变量之间进行的是“稠密连接”(最后一层与之前所有层都紧密相连);
②Idea:在DenseNet出现之前,CNN的进化一般通过层数的加深(ResNet)或者加宽(Inception)的思想进行,DenseNet通过对特征的复用提出了一种新的结构,不但减缓了梯度消失的现象参数量也更少;
③组成——

  • 稠密块:由多个输出通道数相同的卷积块组成,定义如何连接输入与输出;
  • 过渡层:因为过多的稠密块使得通道数不断增加,过渡层使用1x1卷积减少通道,并使用stride=2的平均池化层度特征图的尺寸进行减半,以约束模型复杂度。
    在这里插入图片描述

③增长率(growth rate):对于一个有2个输出通道数为10的DenseBlock,使用通道数为3的输入时,我们会得到通道数为3+2x10=23的输出。 卷积块的通道数控制了输出通道数相对于输入通道数的增长,因此也被称为增长率。

Codes&Homework

Refs:
《知乎-DenseNet详解》
《知乎-深入解析DenseNet(含大量可视化及计算)》
T1:在过渡块的平均池化直接首先利用1x1卷积对特征图的通道进行折半压缩,为了使得后面的信息尽量完整,使用平均池化以保留大部分背景信息;且因为DenseNet的设计能够对前面所有的feature map进行重用,因此不需要考虑层层抽取之后的最显著特征,只需要将本次抽取得到的局部特征表示得到就行

T2:DenseNet模型的参数效率更高
由于DenseNet对输入进行cat操作,一个直观的影响就是每一层学到的feature map都能被之后所有层直接使用,这使得特征可以在整个网络中重用,也使得模型更加简洁.在这里插入图片描述
在这里插入图片描述
T3:DenseNet占用更多显存——
显存大致由该模型的参数和数据输入、梯度等占位符组成,因为该模型对前面的特征都会重用,因此在所有的计算block中都会对输入数据进行重复备份;
解决措施:可以通过共享内存分配减少网络的中间层对显存的占用。

import torch
from torch import nn
from d2l import torch as d2l

#以下DenseNet的实现遵循[BN-ReLU-CNN]的架构
def conv_block(in_channels,num_channels):
    return nn.Sequential(
        #BN在CNN之前实现才能保证“参数分布均衡化”的意义
        nn.BatchNorm2d(in_channels),nn.ReLU(),
        nn.Conv2d(in_channels,num_channels,kernel_size=3,padding=1,)
    )
#稠密块
class DenseBlock(nn.Module):
    def __init__(self,num_convs,in_channels,num_channels):
        super(DenseBlock,self).__init__()
        layer = []
        for i in range(num_convs):
            #在DenseNet的设计中,该层的输入是前面所有层输出的通道叠加
            layer.append(conv_block(num_channels*i+in_channels,num_channels))
        self.net = nn.Sequential(*layer)
    def forward(self,x):
        for blk in self.net:
            y = blk(x)
            #在通道维度进行连接 [B,C,H,W]
            x = torch.cat((x,y),dim = 1)
        return x
#计算验证
blk = DenseBlock(2, 3, 10)
X = torch.randn(4, 3, 8, 8)
Y = blk(X)
print(Y,Y.shape)#torch.Size([4, 23, 8, 8]),3+2x10 = 23
#过渡块
def transition_block(in_channels,num_channels):
    return nn.Sequential(
        nn.BatchNorm2d(in_channels),nn.ReLU(),
        nn.Conv2d(in_channels,num_channels,kernel_size=1),
        nn.AvgPool2d(kernel_size=2,stride=2)
    )
transition_blk = transition_block(23,10)#对稠密块输出的23个通道减少至10个通道
#构造DenseNet模型
b1 = nn.Sequential(#该部分的模块设计和ResNet保持一致:单卷积层和最大池化层
    nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
    nn.BatchNorm2d(64), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
#网络的超参和ResNey18保持一致,故随后连接4个稠密块,每个稠密块使用4个卷积层,卷积层的通道数设为32
# num_channels为当前的通道数(处于动态变化)
num_channels, growth_rate = 64, 32
num_convs_in_dense_blocks = [4, 4, 4, 4]
'''
对于论文中不同的DenseNet[121,169,201,161],
只需要将上面参数的growth_rate和num_convs_in_dense_blocks相应修改即可
'''
blks = []
for i,num_convs in enumerate(num_convs_in_dense_blocks):
    blks.append(DenseBlock(num_convs,num_channels,growth_rate))
    #计算稠密块的输出通道数
    num_channels += num_convs * growth_rate
    #添加过渡层,将当前通道数减半
    if i!= len(num_convs_in_dense_blocks)-1:#最后一层不添加
        blks.append(transition_block(num_channels,num_channels//2))
        num_channels = num_channels // 2#对输入通道数再次动态更新
net = nn.Sequential(
    b1, *blks,
    #最后连接池化和FC层输出结果
    nn.BatchNorm2d(num_channels), nn.ReLU(),
    nn.AdaptiveAvgPool2d((1, 1)),
    nn.Flatten(),
    nn.Linear(num_channels, 10))

#----------Homework:基于DenseNet的思想设计一个用户房价估计的MLP-------
# def linear_block(in_features,num_features):
#     return nn.Sequential(
#         #BN在CNN之前实现才能保证“参数分布均衡化”的意义
#         nn.BatchNorm1d(in_features),nn.ReLU(),
#         nn.Linear(in_features,num_features)
#     )
class DenseLinearBlock(nn.Module):
    def __init__(self,num_linears,in_features,num_features):
        super(DenseLinearBlock,self).__init__()
        layer = []
        for i in range(num_linears):
            #在DenseNet的设计中,该层的输入是前面所有层输出的通道叠加
            layer.append(nn.Linear(num_features*i+in_features,num_features))
        self.net = nn.Sequential(*layer)
    def forward(self,x):
        for blk in self.net:
            y = blk(x)
            #在通道维度进行连接 [B,L]
            x = torch.cat((x,y),dim = 1)
        return x
#过渡块
# def transition_linearblock(in_features,num_features):
#     return nn.Sequential(
#         nn.BatchNorm1d(in_features),nn.ReLU(),
#         nn.Conv1d(in_features,num_features,kernel_size=1),
#         nn.AvgPool1d(kernel_size=2,stride=2)
#     )
class DenseMLP(nn.Module):
    #这里遵循第四章会对数据进行相应预处理,输入出的数据保持不变
    def __init__(self,num_features,num_linears_in_dense_blocks):
        super(DenseMLP,self).__init__()
        self.num_features = num_features
        self.num_linears_in_dense_blocks = num_linears_in_dense_blocks
        # self.out_features = self.num_features // 2
        self.net = nn.Sequential()
        blks = []
        for i, num_linears in enumerate(self.num_linears_in_dense_blocks):
            # 计算稠密块的输出通道数
            # self.out_features = self.num_features // 2
            blks.append(DenseLinearBlock(num_linears,self.num_features,self.num_features//2))
            self.num_features += self.num_features // 2 * num_linears
            # 添加过渡层,将当前通道数减半
            # if i != len(self.num_linears_in_dense_blocks) - 1:  # 最后一层不添加
            #     blks.append(transition_linearblock(self.num_features, self.num_features // 2))
            #     self.num_features = self.num_features // 2  # 对输入通道数再次动态更新
        self.net = nn.Sequential(
            *blks,
            # 最后连接池化和FC层输出结果
            nn.BatchNorm1d(self.num_features), nn.ReLU(),
            nn.Linear(self.num_features, 1))
    def forward(self,x):
        for blk in self.net:
            x = blk(x)
            print(x.shape)
        return x
        # return self.net(x)
#计算验证
num_features = 20
num_linears_in_dense_blocks = [4, 4, 4, 4]
x = torch.rand((2,20))
DenseMLP = DenseMLP(num_features,num_convs_in_dense_blocks)
print(DenseMLP(x).shape)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值