深度残差网络(deep residual network)是2015年微软何凯明团队提出的一种全新的网络结构,其核心模块是残差块residual block。在后续的网络结构时常ResNet的影子。
网络加深的问题:
1.梯度消失和梯度爆炸 梯度消失:若每一层的误差梯度小于1,反向传播时,网络越深,梯度越趋近于0 梯度爆炸:若每一层的误差梯度大于1,反向传播时,网络越深,梯度越来越大
这两种情况都会导致参数是无法更新的,正则化(如Dropout)一定程度可以缓解这个问题,但是又存在退化问题
2.退化问题
随着网络层数增加,预测效果越来越差。 56层的测试误差要比20层的要高。从20->56层,新增层中都通过非线性激活函数处理,导致特征信息的损失(不论是有用的还是冗余的)。
残差块(residual block):
在残差块里面,输入可通过跨层数据线路更快地向前传播。残差块里首先有2个有相同输出通道数的3×3卷积层。每个卷积层后接一个批量归一化层(Batch Normalization,BN层,带有网络参数)和ReLU激活函数。输入通过跨层数据通路,跳过这2个卷积运算,将输入直接加在最后的ReLU激活函数前。为了满足相加处理(不是通道数的和并,而是特征图对应位置相加),输入与2个卷积层的输出的形状一样。如果想改变通道数,就需要引入一个额外的1×1卷积层来将输入变换成需要的形状后再做相加运算。
例如输入是 W * H * C(W:特征图的宽H:特征图的高C:通道数),那么输出必然是 W * H * C,经过相加计算得到的形状: W * H * C
残差块输入输出特征图形状计算例子:
Batch Normalization:
Batch Normalization是由Google于2015年提出,这是一个深度神经训练的技巧,它不仅可以加快了模型的收敛速度,使训练深层网络模型更加容易和稳定。Batch Normalization是指批标准化处理,将一批数据的特征图满足均值为0,方差为1的分布规律。
在BN出现之前,归一化操作一般都在数据输入层,对输入的数据进行求均值以及求方差做归一化,但是BN的出现打破了这一个规定,可以在网络中任意一层进行归一化处理。
因为现在所用的优化方法大多都是min-batch SGD(将整体数据进行分批次进行随机梯度下降),所以归一化操作就成为Batch Normalization。
归一化的原因:
归一化可以将有量纲转化为无量纲,同时将数据归一化至同一量级,解决数据间的可比性问题。量纲的量级不同可能会导致拥有较大量级的特征在进行反响传播占主导地位,从而影响学习结果。除此之外,提高求解最优解的速度。
BN操作步骤:
首先,求出一个批次数据的均值和方差。随后,按照Z-Score对数据进行归一化,为了防止方差出现零而导致分母为零的现象,在归一化的过程中为方差加上一个非零的微小整数。经过如此归一化后,数据被限制在正态分布中,使得网络表达能力下降。因此添加两个参数γ,β对数据进行尺度和平移变换。这两个参数是通过训练过程中学习到的。
尺度和平移变换的原因:
以sigmod激活函数为例。在图中,虚线内为数据分布形式,经过单纯归一化,数据分布形式如右侧所示,Sigmoid激活函数,在-1~1之间的梯度变化不大,那么非线性变换的作用就不能很好的体现,换言之就是,减均值除方差操作后可能会削弱网络的性能,20层的网络效果就可能和5层的保持一致。BN的本质就是利用优化变一下方差大小和均值位置,使得新的分布更切合数据的真实分布,保证模型的非线性表达力。
BN层的好处:
1.加速收敛
2.解决梯度爆炸和梯度消失问题
3.不需要小心地进行权重初始化
ResNet18模型代码:
import torch
from torch import nn
from torchsummary import summary #torchsummary可以打印网络结构和参数
'''
首先模型初始化
建立正向传播的过程
ResNet最重要的就是残差模块的结构,先定义残差模块的类
'''
class Residual(nn.Module):
def __init__(self,input_channel,out_channel,use_conv1 = False,strides = 1):
super(Residual,self).__init__()
self.ReLU = nn.ReLU()
self.conv1 = nn.Conv2d(in_channels=input_channel,out_channels=out_channel,kernel_size=3,stride=strides,padding = 1)
self.conv2 = nn.Conv2d(in_channels=out_channel,out_channels=out_channel,kernel_size=3,padding = 1)
self.bn1 = nn.BatchNorm2d(out_channel)
self.bn2 = nn.BatchNorm2d(out_channel)
#有两种跳转链接的方式,直接加,与1*1卷积核卷积修改通道数
if use_conv1:
self.conv3 = nn.Conv2d(in_channels=input_channel,out_channels=out_channel,kernel_size=1,stride = strides)
else:
self.conv3 = None
def forward(self,x):
y = self.conv1(x)
y = self.ReLU(self.bn1(y))
y = self.conv2(y)
y = self.bn2(y)
if self.conv3:
x = self.conv3(x)
y = self.ReLU(y + x)
return y
class ResNet18(nn.Module):
def __init__(self,Residual):
super(ResNet18,self).__init__()
self.b1 = nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=64,kernel_size=7,stride=2,padding=3),
nn.ReLU(),
nn.BatchNorm2d(64),
nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
)
self.b2 = nn.Sequential(
Residual(64,64,False,1),
Residual(64,64,False,1)
)
self.b3 = nn.Sequential(
Residual(64,128,True,2),
Residual(128,128,False,1),
)
self.b4 = nn.Sequential(
Residual(128,256,True,2),
Residual(256,256,False,1),
)
self.b5 = nn.Sequential(
Residual(256,512,True,2),
Residual(512,512,False,1),
)
self.b6 = nn.Sequential(
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten(),
nn.Linear(512,10)
)
def forward(self,x):
x = self.b1(x)
x = self.b2(x)
x = self.b3(x)
x = self.b4(x)
x = self.b5(x)
x = self.b6(x)
return x
#该部分仅仅为了验证网络搭建的没问题
if __name__ == "__main__":
#定义一个设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#print(device)
model = ResNet18(Residual).to(device)#对于上面的类的实例化 并且放到设备里面
#显示一下模型每一层的参数数量
print(summary(model,input_size=(1,224,224)))
ResNet18训练代码:
from torchvision.datasets import FashionMNIST
from torchvision import transforms
import torch.utils.data as Data
import torch
from torch import nn
import numpy as np
import matplotlib.pyplot as plt
from model import ResNet18,Residual
import copy
import time
import pandas as pd
#在训练部分使用FashionMNIST数据集,里面是一堆服装
def train_val_data_process():
train_data = FashionMNIST(root='./data',
train=True,
transform=transforms.Compose([transforms.Resize(size = 224),transforms.ToTensor()]),
download=True
)
train_data,val_data = Data.random_split(train_data,[round(0.8*len(train_data)),round(0.2*len(train_data))])
train_dataloader =Data.DataLoader(dataset = train_data,
batch_size = 32,
shuffle = True,
num_workers= 2,
)
val_dataloader =Data.DataLoader(dataset = val_data,
batch_size = 32,
shuffle = True,
num_workers= 2,
)
return train_dataloader,val_dataloader
def train_model_process(model,train_dataloader,val_dataloader,num_epochs):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
optimizer = torch.optim.Adam(model.parameters(),lr = 0.001) #定义优化器,基于梯度下降法的优化器 更新参数
criterion = nn.CrossEntropyLoss()#交叉熵损失函数,在分类里面。在回归模型中,使用均方差
model = model.to(device)#模型放入训练设备当中
best_model_wts = copy.deepcopy(model.state_dict())
#初始化参数
best_acc = 0.0#最高准确度
train_loss_all = []#训练集损失值列表
val_loss_all = []#验证集损失值列表
train_acc_all = []#训练集准确度列表
val_acc_all = []#验证集准确度列表
since = time.time()
for epoch in range(num_epochs):
print("Epoch {}/{}".format(epoch,num_epochs -1))
print("-"*10)
'''
初始化损失值与准确度
需要train_num 计算本轮所有图片计算出的损失值与准确度的平均值
'''
train_loss = 0.0
train_corrects =0
val_loss = 0.0
val_corrects =0
#训练集与验证集的样本数量
train_num = 0
val_num=0
#b_y是标签
for step,(b_x,b_y) in enumerate(train_dataloader):
b_x = b_x.to(device)
b_y =b_y.to(device)
#设置模型为训练模式
model.train()
output = model(b_x)#前向传播 输入一个batch 输出为一个batch 其是一个32行的10列的向量 多少行取决数据定义一捆里面有多少张
pre_lab = torch.argmax(output,dim = 1)#取十个输出的最大索引 dim表示不同维度。特别的在dim=0表示二维矩阵中的列,dim=1在二维矩阵中的行 生成一个128维的列向量
loss = criterion(output,b_y) #用到每一个标签的概率
#将梯度复位为零
optimizer.zero_grad()
#反响传播计算
loss.backward()
#更新网络参数
optimizer.step()
#对损失值的累加,loss.item()单个样本的平均损失值
train_loss += loss.item()*b_x.size(0)
train_corrects += torch.sum(pre_lab == b_y.data)
train_num += b_x.size(0)
'''
验证集合没有方向传播的过程
'''
for step,(b_x,b_y) in enumerate(val_dataloader):
b_x =b_x.to(device)
b_y = b_y.to(device)
output = model(b_x)
pre_lab = torch.argmax(output,dim = 1)
loss = criterion(output,b_y)
val_loss += loss.item()*b_x.size(0)
val_corrects += torch.sum(pre_lab == b_y.data)
val_num += b_x.size(0)
#train_loss / train_num 一个轮次的平均loss值
train_loss_all.append(train_loss / train_num)
val_loss_all.append(val_loss / val_num)
train_acc_all.append(train_corrects.double().item() / train_num)
val_acc_all.append(val_corrects.double().item() / val_num)
print('{} Train Loss: {:.4f} Train Acc{:.4f}'.format(epoch,train_loss_all[-1],train_acc_all[-1]))
print('{} Val Loss: {:.4f} Val Acc{:.4f}'.format(epoch,val_loss_all[-1],val_acc_all[-1]))
#训练的再好也是有答案的学习,在验证集上表现好,才是真的好参数
if val_loss_all[-1] > best_acc:
#保存最高准确度
best_acc = val_loss_all[-1]
#保存最好参数
best_model_wts = copy.deepcopy(model.state_dict())
use_time = time.time() - since
print("训练和验证耗费的时间:{:.0f}min{:.0f}s".format(use_time //60,use_time%60))
#选择最优参数,保存最优参数的模型model.load_state_dict(best_model_wts)
torch.save(model.state_dict(best_model_wts),'best_model.pth')
#或者这个代码改编成 torch.save(best_model_wts,'best_model.pth')
#可以用dataframe格式保存这些数据epoch,训练损失值,验证集损失值,训练精度,验证损失值
train_process = pd.DataFrame(data = {"epoch":range(num_epochs),
"train_loss_all":train_loss_all,
"val_loss_all":val_loss_all,
"train_acc_all":train_acc_all,
"val_acc_all":val_acc_all})
return train_process
def matplot_acc_loss(train_process):
plt.figure(figsize=(12,4))
plt.subplot(1,2,1)
#'-ro'表示节点用圆圈表示 红色的;'-bs'表示节点用方块表示,蓝色。
plt.plot(train_process["epoch"],train_process.train_loss_all,'ro-',label = "train loss")
plt.plot(train_process["epoch"],train_process.val_loss_all,'bs-',label = "val loss")
plt.legend()
plt.xlabel("epoch")
plt.ylabel("loss")
plt.subplot(1, 2, 2)
plt.plot(train_process["epoch"],train_process.train_acc_all,'ro-',label = "train acc")
plt.plot(train_process["epoch"],train_process.val_acc_all,'bs-',label = "val acc")
plt.legend()
plt.xlabel("epoch")
plt.ylabel("acc")
plt.show()
if __name__ == "__main__":
#将模型实例化
ResNet = ResNet18(Residual)
#加载数据集
train_dataloader,val_dataloader = train_val_data_process()
train_process = train_model_process(ResNet,train_dataloader,val_dataloader,20)
matplot_acc_loss(train_process)