1、残差神经网络的作用
按我们的认识,都会不难理解增加网络的宽度和深度可以很好的提高网络的性能,深的网络一般都比浅的的网络效果好,因此在训练模型的时候,会想到加深网络模型的层数,但是这也随即带来梯度消失和退化问题。
-
梯度消失(Gradient Vanishing):在传统的深度神经网络中,梯度在反向传播过程中会逐层递减,导致较深层的权重更新较小。这会导致较深层的网络参数难以得到有效的训练,而浅层的网络参数则可能过度拟合训练数据。梯度消失问题使得网络难以收敛和优化。
-
网络退化(Degradation Problem):随着网络层数的增加,网络的性能反而下降。即使网络的深度增加,其训练误差也会逐渐增加,这与我们期望的随着网络深度增加而获得更好性能的直觉相悖。这种现象被ResNet团队称为网络退化问题,它限制了深层网络的有效性和可扩展性。
因此,为了解决这些问题,ResNet(Residual Network)应运而生。
残差神经网络是一种深度卷积神经网络,由微软研究院的何恺明、张祥雨、任少卿、孙剑等人提出的。它通过引入残差连接(residual connections)解决了深层网络训练中的梯度消失和网络退化问题,成为了深度学习领域的重要里程碑。并在2015 年的ILSVRC(ImageNet Large Scale Visual Recognition Challenge)中取得了冠军。
2、残差神经网络的原理
残差块:
上述图片是残差网络的核心--残差块,残差块是ResNet中的基本构建块,用于构建深层网络。它可由多个卷积层组成,通常还包括批量归一化和激活函数等操作。其基本操作就是
输入 x 经过两个卷积层,得到 F(x)。
F(x) 与输入 x 相加,即 x + F(x)。
最后的输出是 x + F(x),这个输出被传递到下一个残差块。
具体例子如下:
上面残差块是由2个相同输出通道数的3 * 3卷积层,每个卷积层后面接一个批量归一化层和ReLu激活函数,这种顺序可以防止梯度消失,并且保持特征的非线性表达能力;此外,在残差块的末尾,通过残差连接将输入特征直接添加到最后的输出特征上。这通过简单地将输入特征与输出特征相加来实现,这种跳跃连接允许网络学习残差部分,即输入与输出之间的差异,助于减轻网络退化问题,使得更深的网络能够更有效地进行训练和优化。
这种设计要求2个卷积层的输出与输入形状一样,这样才能使第二个卷积层的输出和原始的输入形状相同,才能进行相加。如果想要改变通道数,就需要引入一个额外的1 * 1的卷积层来将输入变换成需要的形状后再做相加运算,即上述右图。
原理是1*1卷积层在空间维度上不做任何的东西,主要是在通道维度上做改变,选择一个1*1卷积使得输出通道是输入通道的两倍,这样就能将残差连接的输入和输出对应起来。在ResnNet里面,如果把输出通道数翻了两倍,那么输入的高和宽会减小一半,所以这里步幅设置为2,使在高宽和通道上都能匹配上。此外还有两种方式:填零和投影,Resnet作者进行了实验对比选择了上述方案
参考大佬:https://blog.csdn.net/qq_54499870/article/details/127011866,
各种不同层数的残差结构:
我们可以看出ResNet-18、ResNet-32使用的残差块与ResNet-50、ResNet-101、和ResNet-152不一样,具体原因如下:
首先要想学的更深更多,可以将通道数增加。但是这将增加计算复杂度。右图是先通过1个1*1的卷积,将256维投影回到64维,然后再做通道数不变的卷积,然后再投影回256(将输入和输出的通道数进行匹配,便于进行对比)。这等价于先对特征维度降一次维,在降一次维的上面再做一个空间上的东西,然后再投影回去。因此,虽然通道数是之前的4倍,但是在这种设计之下,二者的算法复杂度是差不多的。此外1*1的卷积也可以用于不同通道上特征的融合。
3、使用CIFAR10数据集ResNet18残差网络模型的图像识别网络
参考大佬代码:https://blog.csdn.net/weixin_44839559/article/details/134649584?spm=1001.2014.3001.5501
在参考上述大佬代码下,我进行了一些修改,运行环境是在kaggle上进行运行的,具体代码已放置Github上:https://github.com/Lily4323/ResNet-18-CIFAR10
以下对代码的主要部分进行理解和分析:
残差块:(与上述原理对应理解)
#定义带两个卷积路径和一条捷径的残差基本块类
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, in_planes, planes, stride=1): #初始化函数,in_planes为输入通道数,planes为输出通道数,步长默认为1
super(BasicBlock, self).__init__()
#定义第一个卷积,默认卷积前后图像大小不变但可修改stride使其变化,通道可能改变
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride,padding=1, bias=False)
#定义第一个批归一化
self.bn1 = nn.BatchNorm2d(planes)
#定义第二个卷积,卷积前后图像大小不变,通道数不变
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,stride=1, padding=1, bias=False)
#定义第二个批归一化
self.bn2 = nn.BatchNorm2d(planes)
#定义一条捷径,若两个卷积前后的图像尺寸有变化(stride不为1导致图像大小变化或通道数改变),捷径通过1×1卷积用stride修改大小
#以及用expansion修改通道数,以便于捷径输出和两个卷积的输出尺寸匹配相加
self.shortcut = nn.Sequential()
if stride != 1 or in_planes != self.expansion*planes:
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, self.expansion*planes,kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(self.expansion*planes)
)
#定义前向传播函数,输入图像为x,输出图像为out
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x))) #第一个卷积和第一个批归一化后用ReLU函数激活
out = self.bn2(self.conv2(out))
out += self.shortcut(x) #第二个卷积和第二个批归一化后与捷径相加
out = F.relu(out) #两个卷积路径输出与捷径输出相加后用ReLU激活
return out
这段代码定义了一个带有两个卷积路径和一条捷径的残差基本块类,名为BasicBlock
,用于构建残差网络。
在在__init__
方法中,定义了该基本块的结构:
第一个卷积层,使用nn.Conv2d
定义,输入通道数为in_planes
,输出通道数为planes
,卷积核大小为3x3,填充为1,步长为stride(没有给定步长的具体数据是应为我们在进行跳跃连接时需要通过修改步长来进行调整)
第二个卷积层,使用nn.Conv2d
定义,输入通道数为planes
,输出通道数为planes
,卷积核大小为3x3,填充为1,步长为1。
第一、二个批归一化层,都是使用nn.BatchNorm2d
定义,输入通道数为planes
。
通过上述原理我们知道残差块的一个特点就是有一个残差连接,这个残差连接在残差跳跃时需要进行1*1的卷积,因此在进行定义捷径时,使用nn.Sequential
定义一个序列容器,用于处理捷径分支。如果卷积路径中的图像尺寸或通道数发生变化(由stride
不等于1或in_planes
不等于self.expansion*planes
判断),则通过1x1卷积核大小和stride
来修改图像大小,并通过expansion
来修改通道数,以便使捷径输出和卷积路径的输出尺寸匹配相加。
在forward
方法中,定义了该基本块的前向传播过程:
将输入x
经过第一个卷积层、第一个批归一化层和ReLU激活函数,得到out
。
将out
经过第二个卷积层和第二个批归一化层,得到out
。
将捷径self.shortcut(x)
与out
相加,实现残差连接。
将相加后的结果经过ReLU激活函数,得到最终的输出out
。
残差网络:ResNet-18:
#定义残差网络ResNet18
class ResNet(nn.Module):
#定义初始函数,输入参数为残差块,残差块数量,默认参数为分类数10
def __init__(self, block, num_blocks, num_classes=10):
super(ResNet, self).__init__()
#设置第一层的输入通道数
self.in_planes = 64
#定义输入图片先进行一次卷积与批归一化,使图像大小不变,通道数由3变为64得两个操作
self.conv1 = nn.Conv2d(3, 64, kernel_size=3,stride=1, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(64)
#定义第一层,输入通道数64,有num_blocks[0]个残差块,残差块中第一个卷积步长自定义为1
self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
#定义第二层,输入通道数128,有num_blocks[1]个残差块,残差块中第一个卷积步长自定义为2
self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
#定义第三层,输入通道数256,有num_blocks[2]个残差块,残差块中第一个卷积步长自定义为2
self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
#定义第四层,输入通道数512,有num_blocks[3]个残差块,残差块中第一个卷积步长自定义为2
self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
#定义全连接层,输入512*block.expansion个神经元,输出10个分类神经元
self.linear = nn.Linear(512*block.expansion, num_classes)
#定义创造层的函数,在同一层中通道数相同,输入参数为残差块,通道数,残差块数量,步长
def _make_layer(self, block, planes, num_blocks, stride):
#strides列表第一个元素stride表示第一个残差块第一个卷积步长,其余元素表示其他残差块第一个卷积步长为1
strides = [stride] + [1]*(num_blocks-1)
#创建一个空列表用于放置层
layers = []
#遍历strides列表,对本层不同的残差块设置不同的stride
for stride in strides:
layers.append(block(self.in_planes, planes, stride)) #创建残差块添加进本层
self.in_planes = planes * block.expansion #更新本层下一个残差块的输入通道数或本层遍历结束后作为下一层的输入通道数
return nn.Sequential(*layers) #返回层列表
#定义前向传播函数,输入图像为x,输出预测数据
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x))) #第一个卷积和第一个批归一化后用ReLU函数激活
out = self.layer1(out) #第一层传播
out = self.layer2(out) #第二层传播
out = self.layer3(out) #第三层传播
out = self.layer4(out) #第四层传播
out = F.avg_pool2d(out, 4) #经过一次4×4的平均池化
out = out.view(out.size(0), -1) #将数据flatten平坦化
out = self.linear(out) #全连接传播
return out
#将ResNet类(参数为BasicBlock基本残差块,[2,2,2,2]四层中每层2个基本残差块)赋给对象net
net = ResNet(BasicBlock, [2, 2, 2, 2])
通过代码,我们可以了解到,ResNet-18中有四层残差块层,并且每层残差块层包含2个基本的残差块:[2, 2, 2, 2]。通过打印,可以参考网络的结构:
第一层卷积层,输入通道数为3(RGB图像),输出通道数为64,卷积核大小为3x3,填充为1,步长为1。第一层批归一化层,输入通道数为64
第一层包含2个残差块 每个卷积层均为:输入通道数为64,输出通道数为64,残差块数量为2,卷积核大小为3x3,填充为1,步长为1;批量归一化输入通道数为64
第二层包含2个残差块,第一个残差块卷积层输入通道数为64,输出通道数为128,此时步长从1改为2,能够实现图片的大小和通道数在模型上能配备上。当然,在进行跳跃连接时,输入的图片需要进行1*1的卷积,使其通道数变为原来的2倍。
第三层同第2层,包含2个残差块,第一个残差块卷积层输入通道数为128,输出通道数为256,此时步长从1改为2,能够实现图片的大小和通道数在模型上能配备上。当然,在进行跳跃连接时,输入的图片需要进行1*1的卷积,使其通道数变为原来的2倍。
第四层同第2层,包含2个残差块,第一个残差块卷积层输入通道数为256,输出通道数为512,此时步长从1改为2,能够实现图片的大小和通道数在模型上能配备上。当然,在进行跳跃连接时,输入的图片需要进行1*1的卷积,使其通道数变为原来的2倍。
最后一层是进行池化,输入通道数为512,输入通道数为10,即将图片进行10分类。
上述是模型的构建,在模型构建完成后,采用CIFAR10数据集,对模型进行训练和测试
训练结果展示:
模型准确性
通过上述结果,我们可以看出在训练过程中模型的性能逐渐提升,在模型迭代至50次时,模型训练准确率已达到0.97+,测试准确率也在0.90+.