深度学习与Pytorch基础-第三周学习笔记
ResNet
ResNet详解
ResNet在2015年由微软实验室提出,网络亮点:
- 超深的网络结构(突破1000层)
- 提出residual(残差)模块(解决退化问题)
- 使用Batch Normalization加速训练(丢弃dropout)
加深网络深度效果变差的原因:
- 梯度消失或梯度爆炸:若每一层的误差梯度小于1,反向传播时,网络越深,梯度越趋近于0
反之,若每一层的误差梯度大于1,反向传播时,网路越深,梯度越来越大 - 退化问题(degradation problem):在解决了梯度消失、爆炸问题后,仍然存在深层网络的效果可能比浅层网络差的现象
从上图中可以看到,20层的网络反而比56层的效果好。
对于梯度消失或梯度爆炸问题,ResNet论文提出通过数据的预处理以及在网络中使用 BN(Batch Normalization)层来解决。
对于退化问题,ResNet论文提出了 残差结构来减轻退化问题,下图是使用residual结构的卷积网络,可以看到随着网络的不断加深,效果并没有变差,而是变的更好了。(虚线是train error,实线是test error)
同深度的ResNet网络结构配置,表中的残差结构给出了主分支上卷积核的大小与卷积核个数,表中残差块×N 表示将该残差结构重复N次。
ResNeXt
ResNeXt是一个用于图像分类的简单、高度模块化的网络结构。
ResNeXt的改进主要是把ResNet中的block从上图左侧改成上图右侧。
作者提出 ResNeXt 的主要原因在于:传统的要提高模型的准确率,都是加深或加宽网络,但是随着超参数数量的增加(比如channels数,filter size等等),网络设计的难度和计算开销也会增加。因此本文提出的 ResNeXt 结构可以在不增加参数复杂度的前提下提高准确率,同时还减少了超参数的数量。
可以看到ResNet-50和ResNeXt-50(32x4d)拥有相同的参数,但是精度却更高。
普通卷积与分组卷积:
常规卷积输出的特征图上,每一个点是由输入特征图 h1 x w1 x c1个点计算得到的;而分组卷积输出的特征图上,每一个点是由输入特征图 h1 x w1 x (c1/g)个点计算得到的。自然,分组卷积的参数量是标准卷积的1/g。
代码练习
AI研习社 “猫狗大战” 比赛
ResNet实现
1.下载数据集到Google Drive并解压,然后在colab里挂载Google Drive。
from google.colab import drive
drive.mount('/content/drive')
2.初始化环境,设置随机种子方便复现。
import numpy as np
import matplotlib.pyplot as plt
import os
import torch
import torch.nn as nn
import torchvision
from torchvision import models,transforms,datasets
import time
import json
import shutil
from PIL import Image
import csv
# 判断是否存在GPU设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('Using gpu: %s ' % torch.cuda.is_available())
# 设置随机种子,方便复现
torch.manual_seed(10000) # 为CPU设置随机种子
torch.cuda.manual_seed(10000) # 为当前GPU设置随机种子
torch.cuda.manual_seed_all(10000) # 为所有GPU设置随机种子
3.进入数据集所在的绝对路径,并将训练集验证集的猫狗图像分别放入单独文件夹内,方便ImageFolder读取。
%cd "/content/drive/MyDrive/ResNet"
for x in ['train','val']:
imgPath = "cat_dog/"+x
pathlist=os.listdir(imgPath)
data_destination = 'cat_dog/'+x+'/cat/'
label_destination = 'cat_dog/'+x+'/dog/'
if not (os.path.exists(data_destination) and os.path.exists(label_destination)):
os.makedirs(data_destination)
os.makedirs(label_destination)
# 根据文件名的特征进行分类并复制相应的文件到新文件夹
for item in pathlist:
# print(os.path.splitext(item)[0],os.path.splitext(item)[1])
if os.path.splitext(item)[1] == '.jpg' and 'cat' in os.path.splitext(item)[0]:
print(os.path.join(imgPath,item))
shutil.move(os.path.join(imgPath,item), data_destination)
elif os.path.splitext(item)[1] == '.jpg' and 'dog' in os.path.splitext(item)[0]:
print(os.path.join(imgPath,item))
shutil.move(os.path.join(imgPath,item), label_destination)
4.载入数据集,并对数据进行处理
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
resnet_format = transforms.Compose([
transforms.CenterCrop(224),
transforms.ToTensor(),
normalize,
])
data_dir = './cat_dog'
dsets = {x: datasets.ImageFolder(os.path.join(data_dir, x), resnet_format)
for x in ['train', 'val']}
dset_sizes = {x: len(dsets[x]) for x in ['train', 'val']}
dset_classes = dsets['train'].classes
loader_train = torch.utils.data.DataLoader(dsets['train'], batch_size=48, shuffle=True, num_workers=6)
loader_valid = torch.utils.data.DataLoader(dsets['val'], batch_size=5, shuffle=False, num_workers=6)
5.载入ResNet152模型并修改全连接层
model = models.resnet152(pretrained=True)
model_new = model;
model_new.fc = nn.Linear(2048, 2,bias=True)
model_new = model_new.to(device)
print(model_new)
6.设定部分参数
#采用交叉熵损失函数
criterion = nn.CrossEntropyLoss()
# 学习率0.001,每10epoch *0.1
lr = 0.001
# 随机梯度下降,momentum加速学习,Weight decay防止过拟合
optimizer = torch.optim.SGD(model_new.parameters(), lr=lr, momentum=0.9, weight_decay=5e-4)
7.训练过程
def val_model(model,dataloader,size):
model.eval()
predictions = np.zeros(size)
all_classes = np.zeros(size)
all_proba = np.zeros((size,2))
i = 0
running_loss = 0.0
running_corrects = 0
with torch.no_grad():
for inputs,classes in dataloader:
inputs = inputs.to(device)
classes = classes.to(device)
outputs = model(inputs)
loss = criterion(outputs,classes)
_,preds = torch.max(outputs.data,1)
# statistics
running_loss += loss.data.item()
running_corrects += torch.sum(preds == classes.data)
#predictions[i:i+len(classes)] = preds.to('cpu').numpy()
#all_classes[i:i+len(classes)] = classes.to('cpu').numpy()
#all_proba[i:i+len(classes),:] = outputs.data.to('cpu').numpy()
i += len(classes)
#print('Testing: No. ', i, ' process ... total: ', size)
epoch_loss = running_loss / size
epoch_acc = running_corrects.data.item() / size
#print('Loss: {:.4f} Acc: {:.4f}'.format(epoch_loss, epoch_acc))
return epoch_loss, epoch_acc
def train_model(model,dataloader,size,epochs=1,optimizer=None):
for epoch in range(epochs):
model.train()
running_loss = 0.0
running_corrects = 0
count = 0
for inputs,classes in dataloader:
inputs = inputs.to(device)
classes = classes.to(device)
outputs = model(inputs)
loss = criterion(outputs,classes)
optimizer = optimizer
optimizer.zero_grad()
loss.backward()
optimizer.step()
_,preds = torch.max(outputs.data,1)
# statistics
running_loss += loss.data.item()
running_corrects += torch.sum(preds == classes.data)
count += len(inputs)
#print('Training: No. ', count, ' process ... total: ', size)
epoch_loss = running_loss / size
epoch_acc = running_corrects.data.item() / size
epoch_Valloss, epoch_Valacc = val_model(model,loader_valid,dset_sizes['val'])
print('epoch: ',epoch,' Loss: {:.5f} Acc: {:.5f} ValLoss: {:.5f} ValAcc: {:.5f}'.format(
epoch_loss, epoch_acc,epoch_Valloss,epoch_Valacc))
scheduler.step()
#学习率衰减
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=2, gamma=0.1)
# 模型训练
train_model(model_new,loader_train,size=dset_sizes['train'], epochs=1,
optimizer=optimizer)
8.测试
model_new.eval()
csvfile = open('csv.csv', 'w')
writer = csv.writer(csvfile)
test_root='./cat_dog/test/'
img_test=os.listdir(test_root)
img_test.sort(key= lambda x:int(x[:-4]))
for i in range(len(img_test)):
img = Image.open(test_root+img_test[i])
img = img.convert('RGB')
input=resnet_format(img)
input=input.unsqueeze(0)
input = input.to(device)
output=model_new(input)
_,pred = torch.max(output.data,1)
print(i,pred.tolist()[0])
writer.writerow([i,pred.tolist()[0]])
csvfile.close()
遇到的问题:一开始设置epoch次数为20,但是每次训练到一半要不就是网断了要不就是执行中止,尝试一次训练发现使用ResNet即使是一次epoch的准确率也很高。
尝试把csv导出提交,然后增加训练次数,发现epoch=5时准确率几乎没有提高,epoch=10的尝试跑了两次都没训练完,可能是由于我的数据集在Google Drive里,Google Drive中数据和服务器计算是分离的,每次读取数据都需要向Drive进行网络请求,导致训练速度被网络速度拖慢,特别是在传输大量小图片数据时。
GPU使用达到限额。。。
思考题
1、Residual learning
为了解决深层网络中的退化问题,可以人为地让神经网络某些层跳过下一层神经元的连接,隔层相连,弱化每层之间的强联系。这种神经网络被称为 残差网络 (ResNets)。
残差网络由许多隔层相连的神经元子模块组成,我们称之为 残差块Residual block。单个残差块的结构如下图所示:
由多个残差块组成的神经网络就是残差网络,这种模型结构对于训练非常深的神经网络,效果很好。另外,为了便于区分,我们把 非残差网络称为 Plain Network。
实际应用中,残差结构的 short cut 不一定是隔一层连接,也可以中间隔多层,ResNet所提出的残差网络中就是隔多层。
2、Batch Normailization 的原理
传统的神经网络,只是在将样本x输入到输入层之前对x进行标准化处理,以降低样本间的差异性。Batch Normailization是在此基础上,不仅仅只对输入层的输入数据x进行标准化,还对每个隐藏层的输入进行标准化。
Convariate shift是BN论文作者提出来的概念,指的是具有不同分布的输入值对深度网络学习的影响。当神经网络的输入值的分布不同时,我们可以理解为输入特征值的scale差异较大,与权重进行矩阵相乘后,会产生一些偏离较大的差异值;而深度学习网络需要通过训练不断更新完善,那么差异值产生的些许变化都会深深影响后层,偏离越大表现越为明显;因此,对于反向传播来说,这些现象都会导致梯度发散,从而需要更多的训练步骤来抵消scale不同带来的影响,也就是说,这种分布不一致将减缓训练速度。
BN的作用就是将这些输入值进行标准化,降低scale的差异至同一个范围内。这样做的好处在于一方面提高梯度的收敛程度,加快模型的训练速度;另一方面使得每一层可以尽量面对同一特征分布的输入值,减少了变化带来的不确定性,也降低了对后层网络的影响,各层网络变得相对独立,缓解了训练中的梯度消失问题。
3.为什么分组卷积可以提升准确率?即然分组卷积可以提升准确率,同时还能降低计算量,分数数量尽量多不行吗?
分组卷积(Group Convolution)最早出现在AlexNet中。受限于当时的硬件资源,在AlexNet网络训练时,难以把整个网络全部放在一个GPU中进行训练,因此,作者将卷积运算分给多个GPU分别进行计算,最终把多个GPU的结果进行融合。因此分组卷积的概念应运而生。
分组卷积可以减少参数量。
分组卷积可以看成是稀疏操作,有时可以在较少参数量的情况下获得更好的效果(相当于正则化操作)。
当分组数量等于输入feature map数量时,输出feature map数量也等于输入feature map数量,这时分组卷积就成了Depthwise卷积,可以使参数量进一步缩减。
分组卷积如果组数太多,各通道之间的信息交互太过困难,甚至导致没有交流,导致特征提取效果不好,影响准确率