一.残差定义
什么是残差:残差是目标特征与输入特征之间的差异,我的理解就是输出与输入的关系。
残差网络ResNet学习的主体呢是残差函数,这个最早是由微软研究院提出的,它是将输入直接传递到输出,用输入直接影响输出,而不是输入通过中间的种种学习后生成的完整映射后得到输出。
二.残差的核心原理
深度卷积学习中,当网络的层数增加时,容易出现梯度消失/爆炸的问题。因为当这个层数过深的时候,也就是Epoch过大的时候,他这个反向传播时,梯度会逐层相乘,导致浅层权重更新困难。一旦出现了梯度消失,趋于零了,或者梯度爆炸,1000以上,这个连乘就会导致极大的偏离。假如有100层,每层的梯度系数为0.5,那么0.5的100次方也是一个趋近于零的数,同理如果每层的梯度系数为2,那么2的100次方就会非常大,模型就会无法收敛。这就会导致深层的特征没办法有效地提取,因为传不出来。所以当我们这个深度学习的层数多时,我们就要来使用这个残差网络或者说残差网络的变体。
所以残差的核心,就是为了防止深层网络的梯度消失和网络的退化,以下是残差的数学表达式。
假设某一层的理想映射为 ,残差块通过学习残差函数
,实际上这就是实际输出减去输入了。将输出表达为:
其中为输入特征,
为残差块的权重参数,
为残差函数,通常由多层卷积组成。
残差的关键,就是使用了残差路径,和主路径共同组成了一个残差块。
主路径由多层网络构成,负责学习残差函数。残差路径直接将输入传递到后续的层,保证信息能够无损传递。
所以在反向传播的过程中,梯度通过这两条路径回传,即使像之前所说的0.5的100次方使主路径趋近于零,残差路径的梯度仍能有效传递,避免了梯度消失。
残差路径,如果输入的维度和输出的维度匹配时,直接传递输入x,即恒等映射。
def residual_block(x):
shortcut = x # 残差路径:无操作
x = Conv2D(64, (3,3))(x)
x = ReLU()(x)
x = Conv2D(64, (3,3))(x)
return Add()([x, shortcut]) # H(x) = F(x) + x
如果输入输出的维度不一样,残差路径需要通过卷积来调整维度。
def residual_block(x):
shortcut = Conv2D(128, (1,1))(x) # 残差路径:1x1卷积调整维度
x = Conv2D(128, (3,3))(x)
x = ReLU()(x)
x = Conv2D(128, (3,3))(x)
return Add()([x, shortcut])
三.残差块的结构
残差块共有两种常见结构。
3.1Basic Block
适用于浅层网络。解决网络退化问题,提升训练稳定性。包含两个3*3的卷积层,残差路径直接输入,即恒等捷径。
import torch.nn as nn
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__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)
# 处理维度不匹配的恒等捷径(如步长大于1或通道数不同)
self.shortcut = nn.Sequential()
if stride !=1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
shortcut = self.shortcut(x)
x = nn.ReLU()(self.bn1(self.conv1(x)))
x = self.bn2(self.conv2(x))
x += shortcut
return nn.ReLU()(x)
3.2Bottleneck Block
适用于深层网络,通过降维,卷积,升维来减少计算量。
class BottleneckBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1, expansion=4):
super().__init__()
mid_channels = out_channels // expansion # 通道数下降为1/4(如256→64)
self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1, stride=1, bias=False)
self.bn1 = nn.BatchNorm2d(mid_channels)
self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(mid_channels)
self.conv3 = nn.Conv2d(mid_channels, out_channels, kernel_size=1, stride=1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channels)
self.shortcut = nn.Sequential()
if stride !=1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
shortcut = self.shortcut(x)
x = nn.ReLU()(self.bn1(self.conv1(x)))
x = nn.ReLU()(self.bn2(self.conv2(x)))
x = self.bn3(self.conv3(x))
x += shortcut
return nn.ReLU()(x)
当网络层数小于34层时一般用第一种,大于34层时用第二种 ,并且第二种能显著降低计算量。