深度学习入门:用pytorch从零手写resnet残差网络
手写残差网络resnet—pytorch版
一、前言
ResNet(Residual Network,残差网络)由微软研究院在 2015 年提出,是深度学习中非常经典的一种网络架构。在深层神经网络中,随着网络层数的加深,模型的训练效果可能会变差,这被称为“退化问题”。ResNet 通过引入“残差连接(skip connection)”巧妙地解决了这一问题,使得网络更容易训练,并能构建更深的网络结构。
本文将通过 PyTorch 从零实现一个 ResNet 模型,主要包括两种残差块(BasicBlock 和 Bottleneck),并模拟实现了类似官方 resnet50 的结构,以帮助更好地理解其内部机制。
二、卷积后特征图尺寸计算公式
在设计 CNN 时,合理计算卷积输出尺寸非常重要,公式如下:

参数解释
Kernel Size(卷积核)
用于提取局部区域特征。常见如 3x3 或 1x1 卷积核。
Padding(填充)
作用:保持特征图尺寸不变,避免边缘信息损失。
如:padding=1 可在 3x3 卷积下保持尺寸不变。
Stride(步长)
控制卷积核滑动的步伐。stride=2 可实现特征图尺寸减半。
常见设置示例:
stride=1 且 padding=1,则尺寸不变;
stride=2 且 padding=1,尺寸减半。
三、残差块
下图对比了 ResNet 中两种残差结构:
图左为基础残差块(BasicBlock)用于浅层网络,如 ResNet18。由两个连续的 3x3 卷积层组成;图右为瓶颈残差块(Bottleneck)用于深层网络,如 ResNet50。采用 1x1->3x3-> 1x1的结构.

四、ResNet18 vs ResNet50 网络结构概览
下图是resnet18和 resnet50整体的网络结构

五、PyTorch 手写实现 ResNet
5.1 基础卷积块(适用于 ResNet18)
该结构是 ResNet 最原始的残差模块,包含两个 3x3 卷积层,残差连接通过 out += identity 实现。
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, in_channel, block_channel, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.downsample = downsample
self.conv1 = nn.Conv2d(in_channel, block_channel, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(block_channel)
self.relu1 = nn.ReLU()
self.conv2 = nn.Conv2d(block_channel, block_channel * self.expansion, kernel_size=3, stride=1, padding=1,
bias=False)
self.bn2 = nn.BatchNorm2d(block_channel)
self.relu2 = nn.ReLU()
def forward(self, x):
identity = x
if self.downsample is not None:
identity = self.downsample(x)
out = self.relu1(self.bn1(self.conv1(x))) # 卷积、批归一化、relu
out = self.bn2(self.conv2(out)) # 经过两层后的结果和原始输入进行相加,实现残差块的残差连接,
out += identity
out = self.relu2(out) # 可以注意到第二层卷积bn后先加了输入,再去relu
return out
5.2 Bottleneck(适用于 ResNet50)
class bottleneck(nn.Module):
expansion = 4 # 扩展通道数至:通道数*expansion
def __init__(self, in_channel, block_channel, stride=1, downsample=None):
super(bottleneck, self).__init__()
self.downsample = downsample
self.conv1 = nn.Conv2d(in_channel, block_channel, kernel_size=1, stride=stride, bias=False) # 这里padding = 0
self.bn1 = nn.BatchNorm2d(block_channel)
self.relu1 = nn.ReLU()
self.conv2 = nn.Conv2d(block_channel, block_channel, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(block_channel)
self.relu2 = nn.ReLU()
self.conv3 = nn.Conv2d(block_channel, block_channel * self.expansion, kernel_size=1, stride=1, bias=False)
self.bn3 = nn.BatchNorm2d(block_channel * self.expansion)
self.relu3 = nn.ReLU()
def forward(self, x):
identity = x
if self.downsample is not None:
identity = self.downsample(x)
out = self.relu1(self.bn1(self.conv1(x)))
out = self.relu2(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
out += identity
out = self.relu3(out)
return out
```python
5.3 构建完整 ResNet 网络
我们通过 _make_layer 方法构建多个残差层
class Resnet(nn.Module):
def __init__(self, in_channel=3, num_classes=100, block=bottleneck, num_blocks=[3, 4, 6, 3]):
super(Resnet, self).__init__()
self.in_channel = 64
self.conv1 = nn.Conv2d(in_channel, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
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.Sequential(
nn.Linear(512 * block.expansion * 7 * 7, num_classes),
nn.Softmax(dim=-1)
)
def forward(self, x):
out = self.maxpool1(self.bn1(self.conv1(x))) # 初始下采样
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = out.reshape(out.shape[0], -1) # flatten
out = self.fc(out)
return out
def _make_layer(self, block, block_channel, block_num, stride):
layers = []
downsample = nn.Conv2d(self.in_channel, block_channel * block.expansion, kernel_size=1, stride=stride, bias=False)
layers.append(block(self.in_channel, block_channel, stride, downsample))
self.in_channel = block_channel * block.expansion
for _ in range(1, block_num):
layers.append(block(self.in_channel, block_channel, stride=1))
return nn.Sequential(*layers)
最终的完整代码如下:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, in_channel, block_channel, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.downsample = downsample
self.conv1 = nn.Conv2d(in_channel, block_channel, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(block_channel)
self.relu1 = nn.ReLU()
self.conv2 = nn.Conv2d(block_channel, block_channel * self.expansion, kernel_size=3, stride=1, padding=1,
bias=False)
self.bn2 = nn.BatchNorm2d(block_channel)
self.relu2 = nn.ReLU()
def forward(self, x):
identity = x
if self.downsample is not None:
identity = self.downsample(x)
out = self.relu1(self.bn1(self.conv1(x))) # 卷积、批归一化、relu
out = self.bn2(self.conv2(out)) # 经过两层后的结果和原始输入进行相加,实现残差块的残差连接,
out += identity
out = self.relu2(out) # 可以注意到第二层卷积bn后先加了输入,再去relu
return out
# resnet50只用了这种残差块
'''
残差块只有第一个卷积层的步长用形参stride来传,其余地方写固定步长1。
实际上只有在每个layer的第一个卷积层里需要降尺寸。
'''
class bottleneck(nn.Module):
expansion = 4 # 扩展通道数至:通道数*expansion
def __init__(self, in_channel, block_channel, stride=1, downsample=None):
super(bottleneck, self).__init__()
self.downsample = downsample
self.conv1 = nn.Conv2d(in_channel, block_channel, kernel_size=1, stride=stride, bias=False) # 这里padding = 0
self.bn1 = nn.BatchNorm2d(block_channel)
self.relu1 = nn.ReLU()
self.conv2 = nn.Conv2d(block_channel, block_channel, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(block_channel)
self.relu2 = nn.ReLU()
self.conv3 = nn.Conv2d(block_channel, block_channel * self.expansion, kernel_size=1, stride=1, bias=False)
self.bn3 = nn.BatchNorm2d(block_channel * self.expansion)
self.relu3 = nn.ReLU()
def forward(self, x):
identity = x
if self.downsample is not None:
identity = self.downsample(x)
out = self.relu1(self.bn1(self.conv1(x)))
out = self.relu2(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
out += identity
out = self.relu3(out)
return out
class Resnet(nn.Module):
def __init__(self, in_channel=3, num_classes=100, block=bottleneck, num_blocks=[3, 4, 6, 3]):
super(Resnet, self).__init__()
self.in_channel = in_channel
self.conv1 = nn.Conv2d(in_channel, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.in_channel = 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.Sequential(
nn.Linear(512 * block.expansion*7*7, num_classes),
nn.Softmax(dim=-1)
)
def forward(self, x):
out = self.maxpool1(self.bn1(self.conv1(x))) # (1, 3, 224, 224) -> (1, 64, 56, 56)
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = out.reshape(out.shape[0], -1) # out = torch.flatten(out, 1)
out = self.fc(out)
return out
def _make_layer(self, block, block_channel, block_num, stride):
layers = []
# 注意:第二个layer的输入是256通道个56*56,下采样后是512通道个28*28。
downsample = nn.Conv2d(self.in_channel, block_channel * block.expansion, kernel_size=1, stride=stride,
bias=False)
# 先加一个带有下采样的layer
layers += [block(self.in_channel, block_channel, stride=stride, downsample=downsample)]
self.in_channel = block_channel * block.expansion
# 再加block_num-1个默认不带下采样的layer,由于输入输出通道数相同,所以不需要下采样
for _ in range(1, block_num):
layers += [block(self.in_channel, block_channel, stride=1)]
return nn.Sequential(*layers)
if __name__ == '__main__':
x = torch.randn(1, 3, 224, 224)
my_resnet50 = Resnet()
resnet50 = torchvision.models.resnet50() # 看源码对比官方的resnet50
print(my_resnet50)
# print(resnet50)
y = my_resnet50(x)
print(y.shape)
更多推荐
所有评论(0)