殘差神經網絡(Residual Neural Network,簡稱 ResNet)是深度學習領域的里程碑式模型,由何凱明等人在 2015 年提出,成功解決了深層神經網絡訓練中的梯度消失 / 爆炸問題,使訓練超深網絡(如 152 層)成為可能。以下從核心原理、結構設計、優勢與應用等方面進行詳解。
一、核心問題:深層網絡的訓練困境
在 ResNet 提出前,隨著網絡層數增加,模型性能會先提升,然后迅速下降 —— 這種下降并非由過擬合導致,而是因為深層網絡的梯度難以有效傳遞到淺層,導致淺層參數無法被充分訓練(梯度消失 / 爆炸)。
ResNet 通過引入 “殘差連接”(Residual Connection)解決了這一問題。
二、核心原理:殘差連接與恒等映射
1. 傳統網絡的映射方式
傳統深層網絡中,每一層的目標是學習一個 “直接映射”(Direct Mapping):
設輸入為x,經過多層非線性變換后,輸出為H(x),即網絡需要學習H(x)。
2. 殘差網絡的映射方式
ResNet 提出:不直接學習H(x),而是學習 “殘差”F(x)=H(x)?x。
此時,原映射可表示為:H(x)=F(x)+x
其中,F(x)是殘差函數(由若干卷積層 / 激活函數組成),x通過 “跳躍連接”(Skip Connection)直接與F(x)相加,形成最終輸出。
3. 為什么殘差連接有效?
- 梯度傳遞更順暢:反向傳播時,梯度可通過x直接傳遞到淺層(避免梯度消失)。例如,若F(x)=0,則H(x)=x,形成 “恒等映射”,網絡可輕松學習到這種簡單映射,再在此基礎上優化殘差。
- 簡化學習目標:學習殘差F(x)比直接學習H(x)更簡單。例如,當目標映射接近恒等映射時,F(x)接近 0,網絡只需微調即可,無需重新學習復雜的映射。
三、ResNet 的基本結構:殘差塊(Residual Block)
殘差塊是 ResNet 的基本單元,分為兩種類型:
1. 基本殘差塊(Basic Block,用于 ResNet-18/34)
由 2 個卷積層組成,結構如下:x→Conv2d(64,3x3)→BN→ReLU→Conv2d(64,3x3)→BN→(+x)→ReLU
- 輸入x先經過兩個 3x3 卷積層(帶批歸一化 BN 和 ReLU 激活),得到殘差F(x)。
- 若輸入x與F(x)的維度相同(通道數、尺寸一致),則直接相加(恒等映射);若維度不同(如 stride > 1 或通道數變化),則需通過 1x1 卷積調整x的維度(稱為 “投影捷徑”,Projection Shortcut):x→Conv2d(out_channels,1x1,stride)→BN→(+F(x))
2. 瓶頸殘差塊(Bottleneck Block,用于 ResNet-50/101/152)
為減少計算量,用 3 個卷積層(1x1 + 3x3 + 1x1)組成,結構如下:x→Conv2d(C,1x1)→BN→ReLU→Conv2d(C,3x3)→BN→ReLU→Conv2d(4C,1x1)→BN→(+x′)→ReLU
- 1x1 卷積用于 “降維”(減少通道數),3x3 卷積用于提取特征,最后 1x1 卷積 “升維”(恢復通道數),顯著降低計算量。
- 同樣支持投影捷徑(當維度不匹配時)。
四、完整 ResNet 的網絡架構
ResNet 通過堆疊殘差塊形成深層網絡,不同層數的 ResNet 結構如下表:
網絡類型 | 殘差塊類型 | 卷積層配置(每個階段的殘差塊數量) | 總層數 |
---|---|---|---|
ResNet-18 | 基本塊 | [2, 2, 2, 2] | 18 |
ResNet-34 | 基本塊 | [3, 4, 6, 3] | 34 |
ResNet-50 | 瓶頸塊 | [3, 4, 6, 3] | 50 |
ResNet-101 | 瓶頸塊 | [3, 4, 23, 3] | 101 |
ResNet-152 | 瓶頸塊 | [3, 8, 36, 3] | 152 |
- 整體流程:輸入圖像 → 7x7 卷積(步長 2)+ 最大池化 → 4 個階段的殘差塊堆疊(每個階段通道數翻倍,尺寸減半) → 全局平均池化 → 全連接層(輸出分類結果)。
五、ResNet 的優勢
- 解決深層網絡訓練難題:通過殘差連接實現梯度有效傳遞,可訓練數百層甚至上千層的網絡。
- 性能優異:在 ImageNet 等數據集上,ResNet 的錯誤率顯著低于 VGG、GoogLeNet 等模型。
- 泛化能力強:殘差結構可遷移到其他任務(如目標檢測、語義分割),成為許多深度學習模型的基礎組件(如 Faster R-CNN、U-Net++)。
六、ResNet 的變體與延伸
- ResNeXt:引入 “分組卷積”(Group Convolution),在保持性能的同時減少參數。
- DenseNet:將殘差連接的 “相加” 改為 “拼接”(Concatenate),強化特征復用。
- Res2Net:在殘差塊中引入多尺度特征融合,提升細粒度特征提取能力。
- 應用擴展:從圖像分類擴展到目標檢測(如 FPN)、視頻分析(如 I3D)、自然語言處理(如殘差 LSTM)等領域。
七、總結
ResNet 通過殘差連接的創新設計,突破了深層網絡的訓練瓶頸,不僅推動了計算機視覺的發展,也為其他領域的深層模型設計提供了重要思路。其核心思想 ——通過簡化學習目標(學習殘差)提升模型性能—— 已成為深度學習的經典范式。
import torch
import torch.nn as nn
import torch.nn.functional as Fclass BasicBlock(nn.Module):"""基本殘差塊,用于ResNet-18/34"""expansion = 1 # 輸出通道數是輸入的多少倍def __init__(self, in_channels, out_channels, stride=1, downsample=None):super(BasicBlock, 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)# 第二個卷積層(步長固定為1)self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)self.bn2 = nn.BatchNorm2d(out_channels)self.relu = nn.ReLU(inplace=True)self.downsample = downsample # 用于調整輸入x的維度以匹配殘差def forward(self, x):identity = x # 保存輸入用于殘差連接# 計算殘差F(x)out = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)# 如果需要調整維度,則對輸入x進行下采樣if self.downsample is not None:identity = self.downsample(x)# 殘差連接:H(x) = F(x) + xout += identityout = self.relu(out)return outclass Bottleneck(nn.Module):"""瓶頸殘差塊,用于ResNet-50/101/152"""expansion = 4 # 輸出通道數是中間層的4倍def __init__(self, in_channels, out_channels, stride=1, downsample=None):super(Bottleneck, self).__init__()# 1x1卷積降維self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, bias=False)self.bn1 = nn.BatchNorm2d(out_channels)# 3x3卷積提取特征self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)self.bn2 = nn.BatchNorm2d(out_channels)# 1x1卷積升維self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion, kernel_size=1, stride=1, bias=False)self.bn3 = nn.BatchNorm2d(out_channels * self.expansion)self.relu = nn.ReLU(inplace=True)self.downsample = downsampledef forward(self, x):identity = x# 計算殘差F(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)# 調整輸入維度if self.downsample is not None:identity = self.downsample(x)# 殘差連接out += identityout = self.relu(out)return outclass ResNet(nn.Module):"""ResNet主網絡"""def __init__(self, block, layers, num_classes=1000):super(ResNet, self).__init__()self.in_channels = 64 # 初始輸入通道數# 初始卷積層self.conv1 = nn.Conv2d(3, self.in_channels, kernel_size=7, stride=2, padding=3, bias=False)self.bn1 = nn.BatchNorm2d(self.in_channels)self.relu = nn.ReLU(inplace=True)self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)# 四個階段的殘差塊self.layer1 = self._make_layer(block, 64, layers[0], stride=1)self.layer2 = self._make_layer(block, 128, layers[1], stride=2)self.layer3 = self._make_layer(block, 256, layers[2], stride=2)self.layer4 = self._make_layer(block, 512, layers[3], stride=2)# 分類頭self.avgpool = nn.AdaptiveAvgPool2d((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')elif isinstance(m, nn.BatchNorm2d):nn.init.constant_(m.weight, 1)nn.init.constant_(m.bias, 0)def _make_layer(self, block, out_channels, blocks, stride=1):"""構建一個由多個殘差塊組成的層"""downsample = None# 如果步長不為1或輸入輸出通道數不匹配,需要下采樣調整維度if stride != 1 or self.in_channels != out_channels * block.expansion:downsample = nn.Sequential(nn.Conv2d(self.in_channels, out_channels * block.expansion,kernel_size=1, stride=stride, bias=False),nn.BatchNorm2d(out_channels * block.expansion),)layers = []# 添加第一個殘差塊(可能包含下采樣)layers.append(block(self.in_channels, out_channels, stride, downsample))self.in_channels = out_channels * block.expansion# 添加剩余的殘差塊(步長固定為1)for _ in range(1, blocks):layers.append(block(self.in_channels, out_channels))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)# 分類x = self.avgpool(x)x = torch.flatten(x, 1)x = self.fc(x)return x# 定義不同層數的ResNet
def resnet18(num_classes=1000):return ResNet(BasicBlock, [2, 2, 2, 2], num_classes)def resnet34(num_classes=1000):return ResNet(BasicBlock, [3, 4, 6, 3], num_classes)def resnet50(num_classes=1000):return ResNet(Bottleneck, [3, 4, 6, 3], num_classes)def resnet101(num_classes=1000):return ResNet(Bottleneck, [3, 4, 23, 3], num_classes)def resnet152(num_classes=1000):return ResNet(Bottleneck, [3, 8, 36, 3], num_classes)# 測試代碼
if __name__ == "__main__":# 創建ResNet-18模型model = resnet18(num_classes=10)# 隨機生成一個3通道輸入(模擬224x224圖像)x = torch.randn(2, 3, 224, 224) # batch_size=2# 前向傳播output = model(x)print(f"輸入形狀: {x.shape}")print(f"輸出形狀: {output.shape}") # 應輸出(2, 10)