使用pytorch实现猫狗大战
- 一.简介
- 二.理论
- 三.实现
- 1️⃣.实现准备
- 2️⃣.创建VGG16模型
- 3️⃣.训练模型
- 4️⃣.在验证集上测试训练的模型
- 5️⃣.在测试集上运行
- 四.总结
- 五.我又回来了
一.简介
猫狗大战其实是Kaggle公司(在墨尔本创立的) 于2013年举办的比赛,判断一张输入图像是“猫”还是“狗”,并分别用0,1标识出来。
AI研习社猫狗大战赛题的要求:https://god.yanxishe.com/41 (目前比赛已经结束,但仍可做为练习赛每天提交测试结果)
在这个比赛中,有25000张标记好的猫和狗的图片用做训练,有12500张图片用做测试。这个竞赛是2013年开展的,如果你能够达到80%的准确率,在当年是一个 state-of-the-art的成绩😎。
现在,参考和借鉴大师经验后,我们使用VGG模型实现猫狗大战😆。
二.理论
VGGNet是牛津大学计算机视觉组和Google DeepMind公司的研究员仪器研发的深度卷积神经网络,主要探究卷积神经网络的深度和其性能之间的关系,通过反复堆叠33的小卷积核和22的最大池化层,VGGNet成功的搭建了16-19层的深度卷积神经网络。与之前的state-of-the-art的网络结构相比,错误率大幅度下降;同时,VGG的泛化能力非常好,在不同的图片数据集上都有良好的表现。到目前为止,VGG依然经常被用来提取特征图像。
VGGNet使用的全部都是3x3的小卷积核和2x2的池化核,通过不断加深网络来提升性能。各个级别VGG的模型结构如下表所示,其下方为不同模型的参数数量。可以看到,虽然从A到E每一级网络逐渐变深,但是网络的参数量并没有增长很多,这是因为参数量主要都消耗在最后3个全连接层了。前面的卷积层数量虽多,但是参数量其实都不大,不过训练时候耗时的依然是卷积层,因为这部分计算量比较大。其中D,E分别为VGG16和VGG19。
VGG拥有5个卷积段,每一个卷积段有2-3个卷积层,同时每段的结尾都会连接一个最大池化层,来缩小图片尺寸。每段内的卷积核数量都一样,越靠后的段的卷积核数量越多:64-128-256-512-512。其中经常出现多个完全一样的3x3卷积层堆叠在一起的情况,这是个非常有用的设计。如下图所示,两层3x3的串联卷积结果相当于一个5x5的卷积,即最后一个像素会与周围5x5个像素产生关联,可以说感受野大小为5x5。而3层3x3的卷积核的串联结果则相当于1个7x7的卷积层。除此之外,3个串联的3x3卷积层的参数数量要比一个7x7卷积层的参数数量小得多,即333C2/77C2 = 55%,__更少的参数意味着减少过拟合,而且更重要的是3个3x3卷积层拥有比1个7x7的卷积层更少的非线性变换(前者拥有3次而后者只有一次),使得CNN对特征的学习能力更强。
参考链接:https://www.jianshu.com/p/15e413985f25
三.实现
首先说明的是,直到此刻我还是没有成功地在测试集上得到一个很好地预测结果,验证集上准确率正常,但测试集上准确率只有一半左右,搜索很多网站去找原因,还没找到具体是哪有问题,鉴于我需要提交任务,所以先写博客,回头再去debug,😭。
测试集上:
实现这个项目,是打算使用VGG神经网络,环境使用的是Google的colab。
colab文档连接:https://colab.research.google.com/drive/1kVua8TIQSa2w-vYQ7Y3DqFFKfy-1jlXq?usp=sharing
1️⃣.实现准备
(1)测试是否使用GPU上
import torch
import torch.nn as nn
# 判断是否存在GPU设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('Using gpu: %s ' % torch.cuda.is_available())
(2)上传及解压数据集
数据集格式:
训练集,作用显而易见
验证集,供我们的模型验证测试结果
测试集,不可见的数据集,检验模型
我们先通过train,valid来训练自己的模型,并测试准确率
参考链接:https://www.uud.me/qiwenzalun/colab-unzip-unrar.html #测试集在内的所有数据可以从https://static.leiphone.com/cat_dog.rar下载😴 💤,也可以去研习社。
#下载并解压数据,从大佬网站上下载训练数据集
! wget http://fenggao-image.stor.sinaapp.com/dogscats.zip
! unzip '/content/dogscats.zip'
#! unzip '/content/Test.zip'
#下载并解压数据
#! wget https://static.leiphone.com/cat_dog.rar
#########在colab上解压使用,注意文件名
#! unrar x '/content/drive/MyDrive/AI/cat_dog.rar'```
(3)由文件创建数据集
#导入头文件 定义为不同名
import numpy as np
import matplotlib.pyplot as plt
import os
###torch的tensor和numpy的array之间是内存共享的,
###这意味着二者的转化几乎不消耗什么资源,并且修改其中一个会同时改变另一个的值。
import torch
import torch.nn as nn
import torchvision
from torchvision import models,transforms,datasets
import time
import json
#transform.Normalize()则把0-1变换到(-1,1)
#mean:各通道的均值 std:各通道的标准差 output = (input - mean) / std
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
##Compose里面的参数实际上就是个列表,而这个列表里面的元素就是你想要执行的transform操作。
##生成一个CenterCrop类的对象,用来将图片从中心裁剪成224*224
##eg:out = transforms.ToTensor()(img)把一个取值范围是[0,255]的PIL.Image转换成Tensor张量
vgg_format = transforms.Compose([
transforms.Resize(224),
transforms.CenterCrop(224),
transforms.ToTensor(),
normalize,
])
########2333#########至此,我们已经生成了一个自定义的vgg格式😈#########2333###############
#文件路径,右击左侧文件名点击复制路径(相对路径一直出错,修改工作路径都不行-_-||)
#data_dir = '/content/drive/MyDrive/AI/cat_dog/data'
data_dir = '/content/dogscats'
#数据集格式:训练集,作用显而易见 验证集,供我们的模型验证测试结果 测试集,不可见的数据集,检验模型
#交叉验证参考:
dsets = {x: datasets.ImageFolder(os.path.join(data_dir, x), vgg_format)
for x in ['train', 'valid']}
##ImageFolder()格式:
###root:图片存储的根目录,即各类别文件夹所在目录的上一级目录
###transform:对图片进行预处理的操作(函数),原始图片作为输入,返回一个转换后的图片。
###target_transform:对图片类别进行预处理的操作,输入为 target,输出对其的转换。如果不传该参数,即对 target 不做任何转换,返回的顺序索引 0,1, 2…
###loader:表示数据集加载方式,通常默认加载方式即可。
###is_valid_file:获取图像文件的路径并检查该文件是否为有效文件的函数(用于检查损坏文件)
###对数据的放置有要求,必须在data_dir目录下放置train和val两个文件夹,然后每个文件夹下,每一类图片单独放在一个文件夹里。
#遍历数组获取文件数目
dset_sizes = {x: len(dsets[x]) for x in ['train', 'valid']}
#我猜啊,可能是数据集的种类🙄
dset_classes = dsets['train'].classes
(4)显示图片的函数
#直接使用预训练好的VGG模型。下载ImageNet1000个类的JSON 文件
#对输入的5个图片利用VGG模型进行预测,同时,使用softmax或Logsoftmax对结果进行处理,
!wget https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json
##显示图片的小程序😄
def imshow(inp, title=None):
# Imshow for Tensor.
inp = inp.numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
inp = np.clip(std * inp + mean, 0,1)
plt.imshow(inp)
if title is not None:
plt.title(title)
plt.pause(0.001) # pause a bit so that plots are updated
(5)装载数据集
import torch
import torch.nn as nn
##torch.utils.data.DataLoader()格式:
###1、dataset:(数据类型 dataset) 2、batch_size:(数据类型 int)3、shuffle:(数据类型 bool)
###7.num_workers:(数据类型 Int)工作者数量,默认是0。使用多少个子进程来导入数据。设置为0,
#就是使用主进程来导入数据。注意:这个数字必须是大于等于0的,负数估计会出错。
loader_train = torch.utils.data.DataLoader(dsets['train'], batch_size= 37,
shuffle=True, num_workers=6)
loader_valid = torch.utils.data.DataLoader(dsets['valid'], batch_size = 7,
shuffle=False, num_workers=6)
##valid 把第一个 batch 保存到 inputs_try, labels_try,分别查看
count = 1
for data in loader_valid:
#print(count, end='\n')
if count == 1:
inputs_try,labels_try = data
count +=1
print(labels_try)
print(inputs_try.shape)
## 显示 labels_try 的图片,即valid里第一个batch的图片
out = torchvision.utils.make_grid(inputs_try)
#注意,先运行imshow,它是自定义函数
#imshow(out, title=[dset_classes[x] for x in labels_try])
2️⃣.创建VGG16模型
(1)载入固定的vgg16
##pretrained=False 不导入网络结构,默认为false
model_vgg = models.vgg16(pretrained=True)
with open('./imagenet_class_index.json') as f: class_dict = json.load(f)
dic_imagenet = [class_dict[str(i)][1] for i in range(len(class_dict))]
#to(device) 就是保存到device上一份
inputs_try , labels_try = inputs_try.to(device), labels_try.to(device)
#释放无关内存
if hasattr(torch.cuda, 'empty_cache'):
torch.cuda.empty_cache()
model_vgg = model_vgg.to(device)
outputs_try = model_vgg(inputs_try)
"""
print(outputs_try)
print(outputs_try.shape)
"""
'''
可以看到结果为5行,1000列的数据,每一列代表对每一种目标识别的结果。
但是我也可以观察到,结果非常奇葩,有负数,有正数,
为了将VGG网络输出的结果转化为对每一类的预测概率,我们把结果输入到 Softmax 函数
'''
m_softm = nn.Softmax(dim=1)
probs = m_softm(outputs_try)
vals_try,pred_try = torch.max(probs,dim=1)
print( 'prob sum: ', torch.sum(probs,1))
print( 'vals_try: ', vals_try)
print( 'pred_try: ', pred_try)
print([dic_imagenet[i] for i in pred_try.data])
#imshow(torchvision.utils.make_grid(inputs_try.data.cpu()),
#title=[dset_classes[x] for x in labels_try.data.cpu()])
(2)展示网络结构
print(model_vgg)
#在model_vgg基础上创建一个新的vgg模型
model_vgg_new = model_vgg;
for param in model_vgg_new.parameters():
param.requires_grad = False
#nn.Linear 设置为全连接层 需要注意的是全连接层的输入与输出都是二维张量,
#一般形状为[batch_size, size],不同于卷积层要求输入输出是四维张量。
model_vgg_new.classifier._modules['6'] = nn.Linear(4096, 2)
model_vgg_new.classifier._modules['7'] = torch.nn.LogSoftmax(dim = 1)
""" #或者是增加一些层次
model_vgg_new.classifier._modules['6'] = nn.Linear(4096, 4096)
model_vgg_new.classifier._modules['7'] = torch.nn.Dropout()
model_vgg_new.classifier._modules['8'] = torch.nn.ReLU()
model_vgg_new.classifier._modules['9'] = nn.Linear(4096, 4096)
model_vgg_new.classifier._modules['10'] = torch.nn.ReLU()
model_vgg_new.classifier._modules['11'] = nn.Linear(4096, 4096)
model_vgg_new.classifier._modules['12'] = torch.nn.Dropout()
model_vgg_new.classifier._modules['13'] = torch.nn.ReLU()
model_vgg_new.classifier._modules['14'] = nn.Linear(4096, 2)
model_vgg_new.classifier._modules['15'] = torch.nn.LogSoftmax(dim = 1)
"""
model_vgg_new = model_vgg_new.to(device)
#print(model_vgg_new.classifier)
3️⃣.训练模型
(1)训练模型的函数
import torch.optim
from torch.optim import lr_scheduler
from PIL import Image, ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
'''
第一步:创建损失函数和优化器
损失函数 NLLLoss() 的 输入 是一个对数概率向量和一个目标标签.
它不会为我们计算对数概率,适合最后一层是log_softmax()的网络.
'''
criterion = nn.NLLLoss()
# 设置学习率
lr = 0.0007
##修改学习率的值,一般是将其改小,如果准确率有所提升,但是升的很慢,则说明是训练速度太慢了。这时候需要
##适当增加BATCH_SIZE的数量,但是BATCH_SIZE会影响最终的准确率,BATCH_SIZE越大,则最终网络的准确率越低。
# 随机梯度下降
optimizer_vgg = torch.optim.SGD(model_vgg_new.classifier[6].parameters(),lr = lr)
##torch.optim.SGD:
###params (iterable) – 待优化参数的iterable或者是定义了参数组的dict
###lr (float) – 学习率
###momentum (float, 可选) – 动量因子(默认:0)
###weight_decay (float, 可选) – 权重衰减(L2惩罚)(默认:0)
###dampening (float, 可选) – 动量的抑制因子(默认:0)
###nesterov (bool, 可选) – 使用Nesterov动量(默认:False)
####暂时用不到exp_lr_scheduler = lr_scheduler.StepLR(optimizer_vgg, step_size=7, gamma=0.1)
def train_model(model,dataloader,size,epochs=1,optimizer=None):
model.train()
for epoch in range(epochs):
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.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
print('Loss: {:.4f} Acc: {:.4f}'.format(
epoch_loss, epoch_acc))
(2)训练模型
# 模型训练
train_model(model_vgg_new,loader_train,size=dset_sizes['train'], epochs=17,
optimizer=optimizer_vgg)
4️⃣.在验证集上测试训练的模型
(1)模型测试函数
#pandas 大熊猫们??😱
#pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。
#Pandas 纳入了大量库和一些标准的数据模型,提供了高效地操作大型数据集所需的工具。
import pandas as pd
#在某个数据集上检测正确率,测试模型
def test_model(model, dataloader, size):
#model.eval(). 否则的话,有输入数据,即使不训练,它也会改变权值。
#eval()就是保证BN和dropout不发生变化,框架会自动把BN和DropOut固定住,不会取平均,
#而是用训练好的值,不然的话,一旦test的batch_size过小,很容易就会被BN层影响结果!!!
model.eval()
#.zeros 初始化为0
predictions = np.zeros(size)
all_classes = np.zeros(size)
all_proba = np.zeros((size,2))
i = 0
running_loss = 0.0
running_corrects = 0
#对于dataloader 这个加载器中的每一个变量
for inputs,classes in dataloader:
#将所有最开始读取数据时的tensor变量copy一份到device所指定的GPU上去,之后的运算都在GPU上进行。
inputs = inputs.to(device)
classes = classes.to(device)
outputs = model(inputs)
loss = criterion(outputs,classes)
#torch.max()简单来说是返回一个tensor中的最大值。
_,preds = torch.max(outputs.data,1)
# statistics
running_loss += loss.data.item()
running_corrects += torch.sum(preds == classes.data)
#t.numpy()将Tensor变量转换为ndarray变量,其中t是一个Tensor变量,
#可以是标量,也可以是向量,转换后dtype与Tensor的dtype一致。
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 predictions, all_proba, all_classes
(2)使用模型验证
predictions, all_proba, all_classes = test_model(model_vgg_new, loader_valid, size=dset_sizes['valid'])
(3)输出验证结果
# 单次可视化显示的图片个数
n_view = 7
correct = np.where(predictions==all_classes)[0]
from numpy.random import random, permutation
idx = permutation(correct)[:n_view]
print('random correct idx: ', idx)
loader_correct = torch.utils.data.DataLoader([dsets['valid'][x] for x in idx],
batch_size = n_view,shuffle=True)
for data in loader_correct:
inputs_cor,labels_cor = data
# Make a grid from batch
out = torchvision.utils.make_grid(inputs_cor)
imshow(out, title=[l.item() for l in labels_cor])
# 类似的思路,可以显示错误分类的图片,这里不再重复代码
(4)存储加载训练模型
参考:
#定义初始化model的函数
def set_parameter_requires_grad(model, feature_extracting):
if feature_extracting:
for param in model.parameters():
param.requires_grad = False
def initialize_model(num_classes, feature_extract, use_pretrained=True):
model_vgg = None
model_vgg = models.vgg16(pretrained=use_pretrained)
# 更改输出层
set_parameter_requires_grad(model_vgg, feature_extract)
model_vgg.classifier[6] = nn.Linear(4096, num_classes)
model_vgg.classifier.add_module('7',torch.nn.LogSoftmax(dim = 1))
return model_vgg
##存储训练模型😳(天秀的操作)
#不同的存储模式可能需要保存不同量级的参数,有的只保存网络结构而有的是保存全网络
model_path = '/content/drive/MyDrive/AI/models/model_vgg_1.pt'
torch.save(model_vgg_new.state_dict(), model_path)
#载入model
#注意保存的文件名😬
model_path = '/content/drive/MyDrive/AI/models/model_vgg_1.pt'
model_vgg_new = initialize_model(num_classes=2,feature_extract = True,use_pretrained=True)
model_vgg_new.to(device)
model_vgg_new.load_state_dict(torch.load(model_path))
5️⃣.在测试集上运行
import torch
from torch.utils.data import Dataset,DataLoader
#载入测试集上一级路径,这次是真刀真枪了😱
test_dir = '/content/Test'
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
vgg_format = transforms.Compose([
transforms.CenterCrop(224),
transforms.ToTensor(),
normalize,
])
test_set = datasets.ImageFolder(test_dir, vgg_format)
test_loader = DataLoader(test_set, batch_size = 1, drop_last = False, shuffle = False)
test_predictions, test_all_proba, test_all_classes = test_model(model_vgg_new, test_loader, size=len(test_loader.dataset))
with open("cat_dog_result.csv", 'w') as out:
for i in range(len(test_loader.dataset)):
out.write("{},{}\n".format(i, test_predictions[i]))
很多笔记我直接记在代码文件中了,看起来有些乱,表情也成斜体了。此外,代码中一定还有许多问题,像如代码的结构,随着学习需要不断修改。
四.总结
(1)
首先说一下存在的问题,看起来简单的实现,其实需要注意很多✍️。我的程序在验证集上正常预测,测试集正确率低,可能(概率很小)是由于过拟合(overfit),体现在数据量小,网络复杂,learning rate 比较高,又没有设置任何防止过拟合的机制。
解决方法主要包括:
1.简化模型,利用现有深度学习手段增加数据(翻转,平移,随机裁剪)
2.利用 dropout层
3.利用正则化
当然,也可能是没有把数据规格化normalize、没有在分验证集之前打乱数据shuffle、数据和标签没有对上、最后一层没有使用正确的激活函数等等,暂时我还没找到具体原因。
另外,设置batch normalization需要batch size至少16张,理论上,在数据集充足的条件下,batch size越大越好。越大其确定的下降方向越准,引起的训练震荡越小。
(2)在算力充足的条件下,可以尝试大的数据集,如果数据集不够,使用交叉验证(参考:)的方法。
(3)尤其需要注意数据集的格式✍️,一般分为train、valid、test三部分,我们通过train和valid训练数据集,使用test测试我们的model。
(4)在调试程序的过程中,要注意归一化是否训练集和测试集相统一,测试model的函数中要记得梯度归零,反向传播的操作。
(5)model.eval以及model.train参考:https://www.zhihu.com/question/363144860/answer/952524603 (6)模型优化的两种方法:牛顿法,梯度下降法。
参考链接: 最后,说一下自己的感受,自己勉强算是Python和pytorch刚刚入门了,前期不努力,后期要恶补😭。自己从头到尾大概把每一个语句及相关的内容搜索了一下,心里感觉挺充实的,不知道在以后的应用中能不能回想起来。
参考链接:https://www.yuque.com/gaopursuit/drqyg6/fvopym
https://www.jianshu.com/p/15e413985f25
表情来源:
五.我又回来了
上回说到我碰到了一个问题,验证集上正确率高,测试集正确率低(一半左右),今天修行了一上午一晚上时间,这里修一修,那里改一改,具体改的哪自己都忘了,诶嘿~😁😁😁还是没弄清楚到底哪有问题(我估计是文件输出部分不对),不过,虽然本地代码显示准确率仍然不高,提交上去还可以。
现在的心情,有点😂😂😂😂(容我乐一会儿)~~说一下,我能想起来改的地方:
(1)我是recruit,对于Python,pytorch理解不透彻,所以整篇代码都是仿照大佬代码修改的。因为验证集和测试集有差异,我怀疑是自己的set或者是loader与之前不一致,或者数据集的处理方式与之前不统一,所以,我修改了目录结构(Test下还有一个test,当时为了使用ImageFolder函数)。
可以再参考:
(2)第二处修改是:我把ImageFile.LOAD_TRUNCATED_IMAGES = True注释掉了,当时图片格式报错,加了句LOAD_TRUNCATED_IMAGES。
参考:
(3)第三处修改:我加了一段代码,用来改文件名称,我也不知道为什么,我搜的CSDN上的博客,怀疑可能是自己的问题,然后就加了。(当时自己的心理有点像被迫害妄想症,老是感觉这代码和我对着干😡 )
#####(2)修改文件名
import os
path = '/content/dogscats/Test/test'
file_list = os.listdir(path)
for file in file_list:
# 补0 10表示补0后名字共10位
filename = file.zfill(10)
# print(filename)
new_name = ''.join(filename)
os.rename(path + '/' + file, path + '/' + new_name)
参考:
(4)当我发现自己的测试集正确率怎么也不高时,为了手动debug加了一句打印语句,当时是想和其他人正确的结果比较一下,看看相差多少,然后才发现基本一致(可以用Excel选中两列 ctrl+‘\’ )…😤
for pre in test_predictions:
print(pre, end = '\t');
修改后的colab文档:https://colab.research.google.com/drive/121HKilp-kDQ531OMHyS4ywVstLzLrMNq?usp=sharing (因为代码结构不好,所以代码中存在的错误仍然有)
肝完,收工😸。