目录
原理
目的
创新
输入输出
恒等变换原理
残差的思想
设计
卷积公式
实现
ResNet 全称 residual neural network ,主要是解决过深的网络带来的梯度弥散,梯度爆炸,网络退化(即网络层数越深时,在数据集上表现的性能却越差)的问题。
把输入 x 和网络的输出 f(x) 加了起来,作为原本的输出 F(x)。
网络输入是 x,网络的输出是 F(x),假设我们想要网络学习到的映射(要拟合的目标)为H(x),而直接学习H(x)是很难学习到的。但我们学习另一个残差函数F(x) = H(x) - x可以很容易学习,因为此时网络块的训练目标是将F(x)逼近于0,而不是某一特定映射。因此,最后的映射 H(x) 就是将 输出F(x) 和 输入x 相加,H(x) = F(x) + x。
现在我们要训练一个深层的网络,它可能过深,假设存在一个性能最强的完美网络N,与它相比我们的网络中必定有一些层是多余的,那么这些多余的层的训练目标是恒等变换,只有达到这个目标我们的网络性能才能跟N一样。那么对于这些需要实现恒等变换的多余的层,要拟合的目标就成了H(x) = x,在传统网络中,网络的输出目标是 F(x) = x,这比较困难,而在残差网络中,拟合的目标成了 x - x = 0,网络的输出目标为 F(x) = 0,这比前者要容易得多。
F(x) + x 的原因:因为多余的层的目标是恒等变换,即 F(x) + x = x,那 F(x) 的训练目标就是0,比较容易训练。如果是其他,比如 x/2,那 F(x) 的训练目标就是 x/2,是一个非 0 的值,比 0 难实现。
去掉相同的主体部分,从而突出微小的变化。比如,F 是求和前网络映射,H 是从输入到求和后的网络映射。比如把 5 映射到 5.1,那么引入残差前是 F'(5) = 5.1,引入残差后是 H(5) = 5.1, H(5) = F(5) + 5, F(5) = 0.1。这里的 F' 和 F 都表示网络参数映射,引入残差后的映射对输出的变化更敏感。比如 s 输出从 5.1 变到 5.2,映射 F' 的输出增加了 1/51=2%,而对于残差结构输出从 5.1 到 5.2 ,映射 F 是从 0.1 到 0.2,增加了100%。明显后者输出变化对权重的调整作用更大,所以效果更好。
BasicBlock结构用于ResNet34及以下的网络,BottleNeck结构用于ResNet50及以上的网络。理解了这两个基础块,ResNet就是这些基础块的叠加
公式:
解释:
# Resnet网络是为了解决深度网络中的退化问题,即网络层数越深时,在数据集上表现的性能却越差
# 假设我们想要网络块学习到的映射为H(x),而直接学习H(x)是很难学习到的。
# 若我们学习另一个残差函数F(x) = H(x) - x可以很容易学习,
# 因为此时网络块的训练目标是将F(x)逼近于0,而不是某一特定映射。
# 因此,最后的映射H(x)就是将 输出F(x) 和 输入x 相加,H(x) = F(x) + x
# 假设存在一个性能最强的完美网络N,
# 与它相比我们的网络中必定有一些层是多余的,那么这些多余的层的训练目标是恒等变换
# 因为多余的层的目标是恒等变换,即F(x)+x=x,那F(x)的训练目标就是0,比较容易
# BasicBlock结构用于ResNet34及以下的网络,
# BotteNeck结构用于ResNet50及以上的网络。理解了这两个基础块,ResNet就是这些基础块的叠加
import torch # pytorch
import torch.nn as nn # 卷积网络
Layers = [3, 4, 6, 3] # 某层有多少个卷积层
class Block(nn.Module):
def __init__(self, in_channels, filters, stride=1, is_1x1conv=False): # 构建实例
# 6 * 6 * 3 , 6 * 6 是二维图像, 3 就是通道数
# 一般的RGB图片,channels 数量是 3 (红、绿、蓝);而monochrome图片,channels 数量是 1
# 一般 channels 的含义是,每个卷积层中卷积核的数量,因为想要卷积出同样的输出通道,就得有多个卷积核
# 最初输入的图片样本的 channels ,取决于图片类型,比如RGB;
# 卷积操作完成后输出的 out_channels ,取决于卷积核的数量。此时的 out_channels 也会作为下一次卷积时的卷积核的 in_channels
# 相当于数据是一个3*3*3的魔方,而卷积核必须至少是一个1*1*3的魔方条才能卷积,至少得有一维一样,而这个维我们选择通道数
super(Block, self).__init__() # python3可以直接super().__init__()
filter1, filter2, filter3 = filters # filter就是输出通道数
self.is_1x1conv = is_1x1conv # 浅层特征只进行一次卷积
self.relu = nn.ReLU(inplace=True) # 激活
self.conv1 = nn.Sequential(
# nn.Sequential() 一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,
# 同时以神经网络模块为元素的有序字典也可以作为传入参数。
nn.Conv2d(in_channels, filter1, kernel_size=1, stride=stride, bias=False), # 二维卷积
nn.BatchNorm2d(filter1), # BatchNorm2d是一个深度神经网络训练的技巧,它不仅可以加快了模型的收敛速度,
# 而且更重要的是在一定程度缓解了深层网络中“梯度弥散”的问题,
# 从而使得训练深层网络模型更加容易和稳定。
# 所以目前BN已经成为几乎所有卷积神经网络的标配技巧了。
# Batch Normalization强行将数据拉回到均值为0,方差为1的正态分布上,
# 一方面使得数据分布一致,另一方面避免梯度消失。
nn.ReLU() # 激活
)
self.conv2 = nn.Sequential(
nn.Conv2d(filter1, filter2, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(filter2),
nn.ReLU()
)
self.conv3 = nn.Sequential(
nn.Conv2d(filter2, filter3, kernel_size=1, stride=1, bias=False),
nn.BatchNorm2d(filter3)
) # 在写self.conv3 这个卷积的时候没有加上Relu()函数,主要是这里需要判断这个板块是否激活了self.shortcut,只有加上这个之后才能一起Relu
if is_1x1conv:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, filter3, kernel_size=1, stride=stride, bias=False), # 这里的stride针对in_channels
nn.BatchNorm2d(filter3)
)# 直接将浅层的特征图仅仅经历一次卷积的捷径,正常情况下应该是三次卷积
def forward(self, x): # 前向传播 一个 Block 所做的事
x_shortcut = x # 浅层特征保存
x = self.conv1(x) # 卷
x = self.conv2(x) # 卷
x = self.conv3(x) # 卷 # 没有relu
if self.is_1x1conv:
x_shortcut = self.shortcut(x_shortcut) # 浅层特征图就经历一次卷积
x = x + x_shortcut # 直接与进行三次卷积之后的特征图相加 所谓的残差
x = self.relu(x) # 在这里relu激活
return x
class ResNet50(nn.Module): # 利用Block
def __init__(self):
super().__init__()
# super(ResNet50,self).__init__()
# 计算特征图经过卷积之后的大小 : 公式 (n + 2*padding + 1 - kernel) / stride
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU()
)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.conv2 = self._make_layer(64, (64,64,256), Layers[0]) # stride=1
self.conv3 = self._make_layer(256, (128,128,512), Layers[1], 2) # stride=2
self.conv4 = self._make_layer(512, (256,256,1024), Layers[2], 2)
self.conv5 = self._make_layer(1024, (512,512,2048), Layers[3], 2)
self.avgpool = nn.AdaptiveAvgPool2d((1,1)) # 指定输出固定尺寸 1x1
self.fc = nn.Sequential(
nn.Linear(2048, 1000) # 输入2048, 输出1000
)
def forward(self, input):
x = self.conv1(input)
x = self.maxpool(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.conv5(x)
x = torch.flatten(x, 1)
# flatten(x,1)是按照x的第1个维度拼接,按照列来拼接,横向拼接 torch.Size([2, 3, 4]) 变 torch.Size([2, 12])
# flatten(x,0)是按照x的第0个维度拼接,按照行来拼接,纵向拼接 torch.Size([2, 3, 4]) 变 torch.Size([24])
# flatten(x, start_dim, end_dimension),此时flatten函数执行的功能是将从start_dim到end_dim之间的所有维度值乘起来,其他的维度保持不变
x = self.fc(x) # Flatten层用来将输入“压平”,即把多维的输入一维化,常用在从卷积层到全连接层的过渡。Flatten不影响batch的大小
return x
def _make_layer(self, in_channels, filters, blocks, stride=1):
layers = [] # 生成的层
block_1 = Block(in_channels, filters, stride=stride, is_1x1conv=True) # 第一层stride不一样
layers.append(block_1)
for i in range(1, blocks): # Layers[i] # 几个block
layers.append(Block(filters[2], filters, stride=1, is_1x1conv=False)) # 后几层
return nn.Sequential(*layers) # 返回容器
def test():
net = ResNet50() # 实例化一个ResNet50网络 # 返回服从均匀分布的初始化后的tenosr,外形是其参数size
# torch.rand(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor
# size (int...) – a sequence of integers defining the shape of the output tensor.
# Can be a variable number of arguments or a collection like a list or tuple.
x = torch.rand((10, 3, 224, 224)) # 生成一个输入tensor 大小224*224*3 共10个
# print(x.view(-1)) # 直接变一维 tensor([0.3568, 0.2306, 0.2379, ..., 0.9008, 0.1921, 0.1106])
# print(x.view(-1).shape) # torch.Size([1505280])
# print(x.view(1, -1)) # tensor([[0.3568, 0.2306, 0.2379, ..., 0.9008, 0.1921, 0.1106]])
# print(x.view(1, -1).shape) # torch.Size([1, 1505280])
for name,layer in net.named_children():
# named_children() 返回包含子模块的迭代器,同时产生模块的名称以及模块本身 输出第一层
# named_modules() 返回网络中所有模块的迭代器,同时产生模块的名称以及模块本身 递归输出所有
if name != 'fc':
x = layer(x) # 送入网络层
print(name, 'ouput shape:', x.shape) # 输出网络层名字 以及处理后的x
else:
x = x.view(x.size(0), -1) # 展开 reshape
# tensor.view()把原先tensor中的数据按照行优先的顺序排成一个一维的数据(这里应该是因为要求地址是连续存储的),然后按照参数组合成其他维度的tensor
# 比如说是不管你原先的数据是[[[1,2,3],[4,5,6]]]还是[1,2,3,4,5,6],因为它们排成一维向量都是6个元素,所以只要view后面的参数一致,得到的结果都是一样的
x = layer(x)
print(name, 'output shape:', x.shape)
if __name__ == '__main__':
test()
参考文章:
pytorch 实现resnet模型 细节讲解_视觉盛宴的博客-CSDN博客_pytorch resnet模型
ResNet50网络结构图及结构详解 - 知乎
Pytorch的nn.Conv2d()详解_风雪夜归人o的博客-CSDN博客_nn是什么意思
深度学习中Flatten层的作用_Microstrong0305的博客-CSDN博客_flatten层
torch.flatten()函数_行者无疆哇的博客-CSDN博客_torch.flatten(x,1)
PyTorch的nn.Linear()详解_风雪夜归人o的博客-CSDN博客_nn.linear
Pytorch学习笔记(五):nn.AdaptiveAvgPool2d()函数详解_ZZY_dl的博客-CSDN博客_adaptiveavgpool2d
PyTorch中view的用法_York1996的博客-CSDN博客_pytorch view
Pytorch中named_children()和named_modules()的区别_叫我西瓜超人的博客-CSDN博客_model.named_modules()