需求
实现猫狗图像二分类,数据描述如下
- 这个数据集的训练数据集中一共有25000张猫和狗的图片,其中猫、狗各12500张。在测试数据集中有12500张图片,其中猫、狗图片无序混杂,且无对应的标签。
官方网站:https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/data
方法
利用pytorch构建CNN神经网络模型,进行交叉验证(没有使用测试机)。
一、数据的路径结构
不同类别放置于不同的路径,pytorch自动识别并利用One-Hot进行编码,此次路径结构如下。
其中valid文件下的图片是从train数据中移动过去,本次的测试集数量是2000张。
- data
- train
- cat(11500涨)
- dog(11500涨)
- valid
- cat(1000涨)
- dog(1000涨)
- train
移动图片的代码如下
import glob
import shutil
import sys
def gen_valid(images, cat_path, dog_path):
""" 生成验证数据集
:param images: 图像训练集的路径
:param cat_path: 猫图像验证集的路径
:param dog_path: 狗图像验证集的路径
:return:
"""
cat_num = 0
dog_num = 0
for i, image in enumerate(glob.glob(images)):
# 首先移动1000张猫到验证集中
if cat_num < 1001:
if 'cat' in image:
shutil.move(image, cat_path + image[17:])
cat_num = cat_num + 1
# 然后移动狗的图像
if dog_num < 1001:
if 'dog' in image:
shutil.move(image, dog_path + image[17:])
dog_num = dog_num + 1
if cat_num > 1001 and dog_num > 1001:
print('********已经移动了%d张猫,移动了%d张狗**********' % (cat_num, dog_num))
sys.exit()
def mv_cat_dog_in_different_file(images, cat_path, dog_path):
""" 将训练集的猫和狗放在不同文件夹
:param images: 图像训练集的路径
:param cat_path: 猫图像训练集的路径
:param dog_path: 狗图像训练集的路径
:return:
"""
for i, image in enumerate(glob.glob(images)):
if 'cat' in image:
shutil.move(image, cat_path + image[17:])
elif 'dog' in image:
shutil.move(image, image, dog_path + image[17:])
else:
print('数据集中存在异常数据:', image)
sys.exit()
if i % 5000 == 0:
print('已经移动了%d张图像了' % (i))
if __name__ == '__main__':
# 需要事先创建文件夹
mv_cat_dog_in_different_file('./data/train/*.jpg', './data/train/cat/', './data/train/dog/')
gen_valid('./data/train/*/*.jpg', './data/valid/cat/', './data/valid/dog/')
二、图像数据载入
图像数据载入分为两步
- 定义图像数据集
- 图像数据集的文件夹路径
- 可以加入对图像的预处理,这里的处理是重设图像大小,并转化成Tensor对象(张量,可以用于神经网络的运算)
- 数据载入
- 图像数据集
- batch_size:每一批训练的数量
- shuffle:洗牌
import torch
from torch.autograd import Variable
import torchvision
from torchvision import datasets, transforms
import os
import matplotlib.pyplot as plt
import time
data_dir = './data'
# 对图像的预处理
data_transform = {x: transforms.Compose([transforms.Resize([64, 64]),
transforms.ToTensor()])
for x in ["train", "valid"]}
# 定义图像数据集
image_datasets = {x: datasets.ImageFolder(root=os.path.join(data_dir, x),
transform=data_transform[x])
for x in ["train", "valid"]}
batch_size = 16
# 数据载入
dataloader = {x: torch.utils.data.DataLoader(dataset=image_datasets[x],
batch_size=batch_size,
shuffle=True)
for x in ["train", "valid"]}
注:在以上代码中数据的变换和导入都采用了字典的形式,这是因为我们需要分别对训练数据集和验证数据集的数据载入方法进行简单定义,使用字典可以简化代码,也方便之后进行相应的调用和操作。
更多预处理可以参考:Pytorch数据预处理:transforms的使用方法:
三、模型搭建
基于VGG16架构来搭建一个简化版的VGGNet模型,简化如下:
- 输入图片大小由224×224修改为64×64;
- 删除了连接在一起的两个卷积层和一个池化层;
- 改变了全连接层中的连接参数。
class Models(torch.nn.Module):
def __init__(self):
super(Models, self).__init__()
self.Conv = torch.nn.Sequential(
torch.nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size=2, stride=2),
torch.nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(128, 128, kernel_size=3, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size=2, stride=2),
torch.nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size=2, stride=2),
torch.nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size=2, stride=2)
)
self.Classes = torch.nn.Sequential(
torch.nn.Linear(4*4*512, 1024),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(1024, 1024),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(1024, 2)
)
def forward(self, input):
x = self.Conv(input)
x = x.view(-1, 4*4*512)
x = self.Classes(x)
return x
查看模型细节:
- 代码
model = Models()
print(model)
四、损失函数、优化函数定义
loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.00001)
五、模型训练和参数优化
训练模型,并进行交叉验证,输出结果到文件中
- 如果遇到内存不足的问题:减少batch_size的数量
# 修改处
# os.environ["CUDA_VISIBLE_DEVICES"] = "1" # 选择特定的GPU进行训练
Use_gpu = torch.cuda.is_available()
if Use_gpu:
model = model.cuda()
loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.00001)
epoch_n = 20
time_open = time.time()
f = open('./cnn-output.csv', 'w', encoding='utf-8') # 导出到文件中
for epoch in range(epoch_n):
print("Epoch {}/{}".format(epoch + 1, epoch_n))
print("-" * 10)
for phase in ["train", "valid"]:
if phase == "train":
print("Training...")
# 设置为True,会进行Dropout并使用batch mean和batch var
model.train(True)
else:
print("Validing...")
# 设置为False,不会进行Dropout并使用running mean和running var
model.train(False) # 等价于 model.eval()
running_loss = 0.0
running_corrects = 0
# enuerate(),返回的是索引和元素值,数字1表明设置start=1,即索引值从1开始
for batch, data in enumerate(dataloader[phase], 1):
# X: 图片,batch_size*3*64*64; y: batch_size,标签
X, y = data
# 修改处
if Use_gpu:
X, y = Variable(X.cuda()), Variable(y.cuda()) #
else:
X, y = Variable(X), Variable(y)
# y_pred: 预测概率矩阵,batch_size*2
# with torch.no_grad(): # 补丁1:用于减少内存占用
y_pred = model(X)
# pred,概率较大值对应的索引值,可看做预测结果
_, pred = torch.max(y_pred.data, 1)
# 梯度归零
optimizer.zero_grad()
# 计算损失
loss = loss_f(y_pred, y)
# 若是在进行模型训练,则需要进行后向传播及梯度更新
if phase == "train":
loss.backward()
optimizer.step()
# 计算损失和
running_loss += float(loss) # 避坑点,loss是tensor类型,占用内存很大
# 统计预测正确的图片数
running_corrects += float(torch.sum(pred == y.data))
# 共在第使用500个及1000个batch对模型进行训练之后,输出训练结果
if batch % 500 == 0 and phase == "train":
print("Batch {}, Train Loss:{:.4f}, Train ACC:{:.4F}%".format(batch, running_loss / batch,
100 * running_corrects / (
batch_size * batch)))
epoch_loss = running_loss * batch_size / len(image_datasets[phase])
epoch_acc = 100 * running_corrects / len(image_datasets[phase])
# 输出最终的结果
print("{} Loss:{:.4f} Acc:{:.4f}%".format(phase, epoch_loss, epoch_acc))
print(",".join([phase, str(epoch_loss), str(epoch_acc)]), file=f)
# 输出模型训练、参数优化用时
time_end = time.time() - time_open
print(time_end)
f.close()
总结
这是博主AlisaZqq实现的的一个vgg16的简化版本,Kaggle中的高赞NoteBook(CNN Architectures : VGG, ResNet, Inception + TL:)中介绍了几种常用的神经网络模型,大部分需要很高的GPU内存。
- VGG16
- VGG19
- InceptionNet
- Resnet
- XceptionNet
这些模型基本上在pytorch有实现,可以在torchvision.models包中调用,例如torchvision.models.vgg16的使用例子(pytorch入门学习:加载模型(torchvision.models)如下。如果是第一次调用,它会帮你从网上下载参数,约500M左右,参数的文件都这么大,那么需要多少大小的GPU才能跑得动呢
# 设置在后续的训练中不更新参数
for param in model.features.parameters(): param.requires_grad = False
# pretrained=True:保留官方已经训练好的参数;调用model.weight 可以查看训练好的权重
model=torchvision.models.vgg16(pretrained=True)
# 定义损失函数和优化函数
loss_f = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.classifier.parameters(), lr=0.0001, momentum=0.5)
- 具体的例子可以参照:Pytorch 学习笔记:迁移学习使用VGG16进行kaggle 猫狗分类
- 关于vgg16的理论知识
- 如果对CNN不是很了解,参考:【CNN】Youtube上迄今为止最好的卷积神经网络入门教程
ps:本博客大部分参考于PyTorch实战Kaggle之Dogs vs. Cats,在此基础上修改了部分代码并增添了一些内容