Resnet18结构如下:
可以看到,18层的网络有五个部分组成,从conv2开始,每层都有两个有残差块,并且每个残差块具有2个卷积层。开头没有残差块的结构为layer_1,之后每四个conv为一个layer(对应上图蓝(layer_2)、棕(layer_3)、黄(layer_4)、粉(layer_5)四种颜色)。
需要注意的是,从conv3开始,第一个残差块的第一个卷积层的stride为2,这是每层图片尺寸变化的原因。另外,stride为2的时候,每层的维度也就是channel也发生了变化,这这时候,残差与输出不是直接相连的,因为维度不匹配,需要进行升维,也就是上图中虚线连接的残差块,实线部分代表可以直接相加。
Resnet18代码块:
Resnet.py文件
import torch
from torch import nn
import torch.nn.functional as F
class BasicBlock(nn.Module):
def __init__(self,in_channels,out_channels,stride=[1,1],padding=1) -> None:
super(BasicBlock, self).__init__()
# 残差部分
self.layer = nn.Sequential(
nn.Conv2d(in_channels,out_channels,kernel_size=3,stride=stride[0],padding=padding,bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True), # 原地替换 节省内存开销
nn.Conv2d(out_channels,out_channels,kernel_size=3,stride=stride[1],padding=padding,bias=False),
nn.BatchNorm2d(out_channels)
)
# shortcut 部分
# 由于存在维度不一致的情况 所以分情况
self.shortcut = nn.Sequential()
if stride[0] != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
# 卷积核为1*1 进行升降维
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride[0], bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = self.layer(x)
out += self.shortcut(x)
out = F.relu(out)
return out
# 采用bn的网络中,卷积层的输出并不加偏置
class ResNet18(nn.Module):
def __init__(self, BasicBlock, num_classes=10) -> None:
super(ResNet18, self).__init__()
self.in_channels = 64
# 第一层作为单独的 因为没有残差快
self.conv1 = nn.Sequential(
nn.Conv2d(3,64,kernel_size=7,stride=2,padding=3,bias=False),
nn.BatchNorm2d(64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
self.conv2 = self._make_layer(BasicBlock,64,[[1,1],[1,1]])
self.conv3 = self._make_layer(BasicBlock,128,[[2,1],[1,1]])
self.conv4 = self._make_layer(BasicBlock,256,[[2,1],[1,1]])
self.conv5 = self._make_layer(BasicBlock,512,[[2,1],[1,1]])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512, num_classes)
#这个函数主要是用来,重复同一个残差块
def _make_layer(self, block , out_channels, strides): #在调用的时候要给出block是BasicBlock
layers = []
for stride in strides:
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels
return nn.Sequential(*layers)
def forward(self, x):
out = self.conv1(x)
out = self.conv2(out)
out = self.conv3(out)
out = self.conv4(out)
out = self.conv5(out)
out = self.avgpool(out)
out = out.reshape(x.shape[0], -1)
out = self.fc(out)
return out
if __name__ == '__main__':
lk = ResNet18(BasicBlock)
input = torch.ones((64,3,32,32))
output = lk(input)
print('output.shape = {}'.format(output.shape))
一、首先是BasicBlock残差块的基本结构:
1、主通道:两个卷积层,及两个Batchnorm和Relu。
2、捷径(shortcut):分两种情况:第一种是输入和输出通道数相等(in_channels == out_channels),这里还给出一个stride的判断条件(stride在这个结构中一般为1,若为2时,说明残差块的输入和输出通道数不相等),当输入和输出通道数相等时,不需要改变通道数的维度,直接执行:
self.shortcut = nn.Sequential() #此时捷径的输出等于输入
第二种是输入和输出通道数不相等(in_channels == out_channels),以及stride==2,此时为了使输入和输出通道数相等(方便后面进行矩阵相加操作),进行改变通道数维度操作:
if stride[0] != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
# 卷积核为1*1 进行升降维
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride[0], bias=False),
nn.BatchNorm2d(out_channels)
)
通过以上两种情况后,在每一个残差块的运行中,一个输入经过主通道(两次卷积)得到主通道输出,再加上输入经过捷径(shortcut)的输出,将得到的加和值通过激活函数Relu后得到一个残差块的最终输出out。
def forward(self, x):
out = self.layer(x) #主通道
out += self.shortcut(x) #主通道加捷径
out = F.relu(out) #激活函数
return out
二、Restnet块:
先执行larer_1,再执行后面四个layer,最后以一个平均池化和全连接层输出结束。
以layer_2举例(下图蓝色部分):
在layer_2里有两个残差块,要调用两次BasicBlock残差块,这里运用到了_make_layer方法,主要是用来,重复同一个残差块。
self.conv2 = self._make_layer(BasicBlock,64,[[1,1],[1,1]])
#这个函数主要是用来,重复同一个残差块
def _make_layer(self, block , out_channels, strides): #在调用的时候要给出block是BasicBlock
layers = []
for stride in strides: #strides是一个列表,如[[1,1],[1,1]],取出第一个stride[0]是[1,1],第二个stride[1]是[1,1],刚好四个s对应一个layer中的四个卷积层。
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels #将一个残差块的第二个卷积的输入通道维度等于第一个卷积输出的通道维度
return nn.Sequential(*layers)
_make_layer方法里需指出运用什么残差块(这里运用的是BasicBlock),第一层卷积所需输出的通道数,以及四个卷积的s值(这里用列表strides表示,strides=[[1,1],[1,1]],用for循环取出,因为一个残差块有两层卷积,故一次取出两个s值,stride[0]=[1,1],stride[1]=[1,1]),进入for循环后执行stride[0],block = BasicBlock的操作,并将执行一个残差块的运行步骤存入空列表layers中,将该残差块的第一个卷积输出的通道维度赋给第二个卷积的输入通道维度,再执行stride[1],block = BasicBlock的操作,并将执行的第二个残差块的运行步骤存入列表layers中,此时的列表含有两个残差块的运行步骤,最后return传给conv2。
layers的内容(两个layer组成一个layers):
[BasicBlock(
(layer): Sequential(
(0): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
(3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(shortcut): Sequential()
), BasicBlock(
(layer): Sequential(
(0): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
(3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(shortcut): Sequential()
)]
conv2的内容:
(conv2): Sequential(
(0): BasicBlock(
(layer): Sequential(
(0): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
(3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(shortcut): Sequential()
)
(1): BasicBlock(
(layer): Sequential(
(0): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
(3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(shortcut): Sequential()
)
)
在全连接层后,最后执行前向传播,将输入值x传入,按步骤计算所有layer,输出结果out。
def forward(self, x):
out = self.conv1(x)
out = self.conv2(out)
out = self.conv3(out)
out = self.conv4(out)
out = self.conv5(out)
out = self.avgpool(out)
out = out.reshape(x.shape[0], -1) #batch_size为64
out = self.fc(out)
return out
在外部调用Resnet时要传入残差块参数block,这里为BasicBlock
lk = ResNet18(BasicBlock)
完整代码(Resnet文件见上文)
import numpy as np
import torch.optim
import torchvision
import matplotlib.pyplot as plt
from torch import nn
from torch.utils.data import DataLoader
from torch.utils import tensorboard
from torch.utils.tensorboard import SummaryWriter
writer=SummaryWriter("../logs_train")
#from VGG16 import *
from Resnet import *
#from model import *
# 增强数据集transforms
train_dataset_transform = torchvision.transforms.Compose([
torchvision.transforms.RandomCrop(32,padding=4),
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
test_dataset_transform = torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
#准备训练数据集
train_data = torchvision.datasets.CIFAR10(root='../data',train=True,transform=train_dataset_transform
,download=True)
#准备测试数据集
test_data = torchvision.datasets.CIFAR10(root='../data',train=False,transform=test_dataset_transform
,download=True)
# #准备训练数据集
# train_data = torchvision.datasets.CIFAR10(root='../data',train=True,transform=torchvision.transforms.ToTensor()
# ,download=True)
#
# #准备测试数据集
# test_data = torchvision.datasets.CIFAR10(root='../data',train=False,transform=torchvision.transforms.ToTensor()
# ,download=True)
train_data_size = len(train_data)
test_data_size = len(test_data)
print('训练集的大小为{} \n测试集的大小为{}'.format(train_data_size,test_data_size))
#利用Dataloader来加载数据集
train_dataloader = DataLoader(train_data,batch_size=64)
test_dataloader = DataLoader(test_data,batch_size=64)
# # 查看图像大小
# for data in train_dataloader:
# imgs, targets = data
# print(imgs[0].shape)
# break
#创建网络模型
#lk = Links()
#lk = VGG16()
lk = ResNet18(BasicBlock)
lk = lk.cuda()
#损失函数
#loss_fn = nn.MSELoss() #交叉熵损失函数
loss_fn = nn.CrossEntropyLoss()
loss_fn = loss_fn.cuda()
#优化器
learning_rate = 0.01
optimizer = torch.optim.Adam(params=lk.parameters(),lr=learning_rate,betas=(0.9,0.999),eps=1e-08,weight_decay=0)
#optimizer = torch.optim.SGD(lk.parameters(),lr = learning_rate) #随机梯度下降
#设置学习率衰减
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer,lr_lambda=lambda epoch:1/(epoch+1))
#设置训练网络的一些参数
#记录训练的次数
total_train_step = 0
#记录测试的次数
total_test_step = 0
#训练的轮数
epoch = 50
for i in range(epoch):
print('-----------第{}轮训练开始-----------'.format(i+1)) #因为batch_size大小为64,50000/64=781.25,故每训练781次就会经过一轮epoch
#训练步骤开始
lk.train() #设置模型进入训练状态,仅对dropout,batchnorm...等有作用,如果有就要调用这里模型暂时没有可不调用
for data in train_dataloader: #train_dataloader的batch_size为64,从训练的train_dataloader中取数据
imgs , targets = data #
imgs = imgs.cuda()
targets = targets.cuda()
outputs = lk(imgs) #将img放入神经网络中进行训练
loss = loss_fn(outputs,targets) #计算预测值与真实值之间的损失
#优化器优化模型
optimizer.zero_grad() #运行前梯度清零
loss.backward() #反向传播
optimizer.step() #随机梯度下降更新参数
total_train_step = total_train_step + 1 #训练次数加一
if total_train_step % 100 == 0:
print('训练次数:{},Loss:{}'.format(total_train_step,loss.item())) #.item()的作用是输出数字,与训练次数格式相同
writer.add_scalar('train_loss',loss.item(),total_train_step)
#测试步骤开始
lk.eval() #设置模型进入验证状态,仅对dropout,batchnorm...等有作用,如果有就要调用这里模型暂时没有可不调用
total_test_loss = 0
total_test_accuracy = 0
total_accuracy = 0
with torch.no_grad():
for data in test_dataloader:
imgs,targets = data
imgs = imgs.cuda()
targets = targets.cuda()
outputs = lk(imgs)
loss = loss_fn(outputs,targets)
total_test_loss = total_test_loss + loss.item() #所有loss的加和,由于total_test_loss是数字,而loss是Tensor数据类型,故加.item()
accuracy = (outputs.argmax(dim=1) == targets).sum() #输出每次预测正确的个数
total_accuracy = total_accuracy + accuracy #测试集上10000个数据的正确个数总和
print('整体测试集上的loss:{}'.format(total_test_loss))
print('整体测试集上的正确率:{}'.format(total_accuracy / test_data_size))
writer.add_scalar('test_loss',total_test_loss,total_test_step)
writer.add_scalar('test_accuracy',total_accuracy / test_data_size,total_test_step)
total_test_step = total_test_step + 1
torch.save(lk,'lk_{}.pth'.format(i))
print('模型已保存')
scheduler.step()
writer.close()
训练效果
最后一轮epoch=50的正确率达到82%
-----------第50轮训练开始-----------
训练次数:38400,Loss:0.09794484823942184
训练次数:38500,Loss:0.16820301115512848
训练次数:38600,Loss:0.1316990852355957
训练次数:38700,Loss:0.16768933832645416
训练次数:38800,Loss:0.3024609684944153
训练次数:38900,Loss:0.07128152251243591
训练次数:39000,Loss:0.09017859399318695
训练次数:39100,Loss:0.06386926025152206
整体测试集上的loss:113.43860536813736
整体测试集上的正确率:0.8215999603271484
模型已保存
tensorboard上的测试集正确率如下:
但在二十轮之后测试集的损失反而有所上升,这与LeNet神经网络后期类似。
Resnet18训练效果与LeNet神经网络和VGG16模型的训练效果相比,Resnet18的测试集正确率最高,效果最好。
如何进一步提高Resnet18训练CIFAR10数据集的正确率呢?
1、由于CIFAR10的图片格式是3*32*32,输入尺寸为(64,3,32,32),而论文的Resnet18结构的第一层卷积用的过滤器尺寸为7*7(太大了)。
nn.Conv2d(3,64,kernel_size=7,stride=2,padding=3,bias=False)
可使用3*3大小的过滤器,stride=2,padding=1。这一层输出的维度依旧是(64,64,16,16)-->(batch_size,out_channels,W,H),注意是向下取整W=H=(
)向下取整 = 16。
更改为:nn.Conv2d(3,64,kernel_size=3,stride=2,padding=1,bias=False)/2、
2、输入图片高度和宽度维度为32,在到达layer_5时维度就以及降至2,而过滤器的尺寸依旧为3*3,不合理,或许可以将layer_5直接去除?