Kaggle - Cat and Dog (CNN基础)

前言

猫和狗的二分类任务算是CNN中最简单的任务了, 这个Kaggle的主要目的是学习如何用Pytorch来搭建CNN网络.

Kaggle数据集地址: Cat and Dog.

数据预处理

导入要用到的一些库, 以及定义一些常量

import glob
import os
import cv2
import numpy as np
from tqdm import tqdm
from PIL import Image, ImageStat
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings(action='ignore')

import torch
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from torchvision.models.resnet import resnet18
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.tensorboard import SummaryWriter
import torch.optim as optim

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
train_bs = 16
test_bs = 16
lr_init = 0.0001
max_epoch = 3
数据类别的分布

首先读取图片所在的路径

train_path = '../data/cat_dog/training_set/training_set/'
test_path = '../data/cat_dog/test_set/test_set/'

train_all = {}
test_all = {}

for trainable in [True, False]:
    if trainable:
        for category in ['cats', 'dogs']:
            train_all[category] = glob.glob(os.path.join(train_path + category, '*.jpg'))
    else:
        for category in ['cats', 'dogs']:
            test_all[category] = glob.glob(os.path.join(test_path + category, '*.jpg'))

随后获取类别分布数据, 训练集和测试集的数据分布基本都是1:1

print(
    'train cats: ',len(train_all['cats']),'\n',
    'train dogs:', len(train_all['dogs']),'\n',
    'test cats: ',len(test_all['cats']),'\n',
    'test dogs: ',len(test_all['dogs']),'\n',
    )
# train cats:  4000
# train dogs:  4005
# test cats :  1011
# test dogs :  1012
图片尺寸散点图

绘制训练集图片尺寸散点图以及测试集图片尺寸散点图.

# 训练集散点图分布
x, y = [], []
for category in ['cats', 'dogs']:
    for path in train_all[category]:
        img = Image.open(path)
        x.append(img.size[0])
        y.append(img.size[1])

在这里插入图片描述

可以看到训练集中有两个离群点. 我们尝试找出这两个图片并将其删除.

x, y = [], []
Outlier = []
for category in ['cats', 'dogs']:
    for path in train_all[category]:
        img = Image.open(path)
        x.append(img.size[0])
        y.append(img.size[1])
        if img.size[0] >= 600:
            Outlier.append(path)
print(Outlier)
# cat.835.jpg and dog.2317.jpg

找出离群点后, 我们将其删除.

# 测试集散点图分布
x, y = [], []
for category in ['cats', 'dogs']:
    for path in test_all[category]:
        img = Image.open(path)
        x.append(img.size[0])
        y.append(img.size[1])

在这里插入图片描述

测试集的图片尺寸分布并没有什么问题. 但现在距离我们将图片投入网络训练还有一段距离. 接下来, 让我们来制作数据集

制作数据集
构建TXT文件

我们读取文件路径以及标签, 将其存入txt文件中.

mapkey = {
    'cats':'0',
    'dogs':'1',
}

def gen_txt(txt_path, img_paths):
    
    f = open(txt_path, 'w')
    for key in img_paths.keys():
        label = mapkey[key]
        for path in img_paths[key]:
            line = path + ' ' + label + '\n'
            f.write(line)

gen_txt('../data/cat_dog/train.txt', train_all)
gen_txt('../data/cat_dog/test.txt', test_all)
构建Dataset子类
class MyDataset(Dataset):
    def __init__(self, txt_path, transform=None, target_transform=None):
        fh = open(txt_path, 'r')
        imgs = []
        for line in fh:
            line = line.rstrip()
            words = line.split()
            imgs.append((words[0], int(words[1])))

        self.imgs = imgs        # 最主要就是要生成这个list, 然后DataLoader中给index,通过getitem读取图片数据
        self.transform = transform
        self.target_transform = target_transform

    def __getitem__(self, index):
        fn, label = self.imgs[index]
        img = Image.open(fn).convert('RGB')     # 像素值 0~255,在transfrom.totensor会除以255,使像素值变成 0~1

        if self.transform is not None:
            img = self.transform(img)   # 在这里做transform,转为tensor等等

        return img, label

    def __len__(self):
        return len(self.imgs)

首先看看初始化, 初始化中从我们准备好的 txt 里获取图片的路径和标签, 并且存储在 self.imgs, self.imgs 就是上面提到的 list, 其一个元素对应一个样本的路径和标签, 其实就是 txt 中的一行.

初始化中还会初始化 transform,transform 是一个 Compose 类型, 里边有一个 list, list中就会定义了各种对图像进行处理的操作, 可以设置减均值, 除标准差, 随机裁剪, 旋转, 翻转, 仿射变换等操作.

在这里我们可以知道, 一张图片读取进来之后, 会经过数据处理 (数据增强), 最终变成输入模型的数据. 这里就有一点需要注意, PyTorch 的数据增强是将原始图片进行了处理, 并不会生成新的一份图片, 而是“覆盖”原图, 当采用 randomcrop 之类的随机操作时, 每个 epoch 输入进来的图片几乎不会是一模一样的,这达到了样本多样性的功能.

然后看看核心的 getitem 函数:

  • self.imgs 是一个 list, 也就是一开始提到的 list,self.imgs 的一个元素是一个 str, 包含图片路径,图片标签,这些信息是从 txt 文件中读取.

  • 利用 Image.open 对图片进行读取, img 类型为 Image ,mode=‘RGB’ 第三行与第四行: 对图片进行处理,这个 transform 里边可以实现减均值,除标准差, 随机裁剪, 旋转, 翻转, 放射变换, 等等操作,这个放在后面会详细讲解.

  • 当 Mydataset 构建好, 剩下的操作就交给 DataLoder, 在 DataLoder 中, 会触发Mydataset 中的 getiterm 函数读取一张图片的数据和标签, 并拼接成一个 batch 返回, 作为模型真正的输入.

训练集的图片的均值和标准差

我们要计算得到训练集图片的均值和标准差, 用于后续的标准化处理.

# 计算训练集的均值和标准差
train_paths = train_all['cats'] + train_all['dogs']
m_list, s_list = [], []
for path in tqdm(train_paths):
    img = cv2.imread(path)
    img = img / 255.0
    m, s = cv2.meanStdDev(img)
    m_list.append(m.reshape((3,)))
    s_list.append(s.reshape((3,)))
m_array = np.array(m_list)
s_array = np.array(s_list)
m = m_array.mean(axis=0, keepdims=True)
s = s_array.mean(axis=0, keepdims=True)
# BGR -> RGB
print(m[0][::-1])
print(s[0][::-1])

# [0.48827705 0.45510637 0.41741   ]
# [0.22971935 0.22475049 0.22525084]
构建 Train Loader 和 Test Loader

编写transforms进行数据增强.

# 数据预处理设置
normMean = [0.48827705, 0.45510637, 0.41741   ]
normStd = [0.22971935, 0.22475049, 0.22525084]
normTransform = transforms.Normalize(normMean, normStd)
trainTransform = transforms.Compose([
    transforms.RandomRotation(30),
    transforms.RandomHorizontalFlip(),
    transforms.Resize((280,280)),
    transforms.ToTensor(),
    normTransform,
])

testTransform = transforms.Compose([
    transforms.Resize((280,280)),
    transforms.ToTensor(),
    normTransform,
])

制作train_loader以及test_loader

train_txt_path = '../data/cat_dog/train.txt'
test_txt_path = '../data/cat_dog/test.txt'

train_data = MyDataset(txt_path=train_txt_path, transform=trainTransform)
test_data = MyDataset(txt_path=test_txt_path, transform=testTransform)

train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)
test_loader = DataLoader(dataset=test_data, batch_size=16)

至此, 数据预处理步骤全部完成.

定义网络结构

这里, 我们使用Res18, 来训练一个可以识别猫狗的神经网络.

Res18

ResNet的提出, 是为了解决随着网络深度增加而带来的性能退化问题, 因为深层网络会带来梯度消失的问题, 因此难以训练. 虽然Google提出的batchNorm在一定程度上缓解了梯度消失现象, 但是也没有彻底解决这个问题. 因此, ResNet构建一个恒等映射(Identity mapping)来解决这个问题, 保证深层网络的性能不退化.

在这里插入图片描述

原先的网络输入x,希望输出H(x)。我们令H(x)=F(x)+x,那么我们的网络就只需要学习输出一个残差F(x)=H(x)-x。学习残差F(x)=H(x)-x会比直接学习原始特征H(x)简单的多.

ResNet主要有五种主要形式:Res18,Res34,Res50,Res101,Res152. 输入部分、输出部分和中间卷积部分(中间卷积部分包括如图所示的Stage1到Stage4共计四个stage). 尽管ResNet的变种形式丰富,但都遵循上述的结构特点,网络之间的不同主要在于中间卷积部分的block参数和个数存在差异.

在这里插入图片描述

网络输入部分

resnet的输入部分由size=7x7, stride=2的大卷积核,以及一个size=3x3, stride=2的最大池化组成,通过这一步,一个224x224的输入图像就会变56x56大小的特征图.

中间卷积部分

不管是res有多深, 可以看到只是block的堆叠程度不同而已.

在这里插入图片描述

残差单元的实现

如下图所示的basic-block,输入数据分成两条路,一条路经过两个3*3卷积,另一条路直接短接,二者相加经过relu输出。

在这里插入图片描述

实现res18

pytorch中自带了resnet的实现, 我们可以直接拿来用.

### 自定义 Res18后面的全连接层
class fc_part(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(512,120)
        self.fc2 = nn.Linear(120, 2)
        
    def forward(self,x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x
加载预训练权重
model = resnet18(pretrained=True).to(device)
model.fc = fc_part().to(device)

使用tensorboard查看定义的网络

images, labels = next(iter(train_loader))
images = images.to(device)
grid = torchvision.utils.make_grid(images)
comment = f'batch_size{train_bs} lr{lr_init}'
tb = SummaryWriter(comment=comment)
tb.add_image('images', grid)
tb.add_graph(model, images)

在这里插入图片描述

打开其中一个Block的数据流向, 可以看到RELU这个单元这里确实有两个数据流, 一个数据流流向了卷积层, 另一个数据流跳过了卷积层.

至此, 我们的网络已经定义好了.

定义损失函数和优化器

按需设置学习率, 因为我们全连接层前面的权重全是预训练加载来的权重, 因此Finetune一下就行. 损失函数的话, 选取交叉熵.

ignored_params = list(map(id, model.fc.parameters()))
base_params = filter(lambda p: id(p) not in ignored_params, model.parameters())

optimizer = optim.SGD([
    {'params': base_params},
    {'params': model.fc.parameters(), 'lr': lr_init*10}],  lr_init, momentum=0.9, weight_decay=1e-4)

criterion = nn.CrossEntropyLoss()                                                   # 选择损失函数
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.1)     # 设置学习率下降策略

训练

我们训练3个epoch, 并且用tensorboard来记录结果.

for epoch in range(1, max_epoch+1):

    train_loss = 0.0    # 记录一个epoch的loss之和
    train_correct = 0.0
    train_total = 0.0
    
    scheduler.step()  # 更新学习率
    
    with tqdm(train_loader, desc = 'Train') as t:
        model.train()
        for data in t:
            # 获取图片和标签
            inputs, labels = data
            inputs = inputs.to(device)
            labels = labels.to(device)

            # forward, backward, update weights
            optimizer.zero_grad()
            outputs = model.forward(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            # 统计预测信息
            _, predicted = torch.max(outputs, axis = 1)
            train_total += labels.size(0)
            train_correct += torch.sum(predicted == labels).item()
            train_loss += loss.item()

            #设置进度条右边显示的信息
            t.set_postfix(train_loss = loss.item(), train_accuracy = train_correct / train_total)
    
    test_loss = 0.0
    test_correct = 0
    test_total = 0

    with torch.no_grad():
        model.eval()
        with tqdm(test_loader, desc = 'Test') as t:
            for data in t:                
                inputs, labels = data
                inputs = inputs.to(device)
                labels = labels.to(device)
                
                outputs = model.forward(inputs)
                loss = F.cross_entropy(outputs, labels)
                test_loss += loss.item()

                _, predicted = torch.max(outputs, axis = 1)
                test_total += labels.size(0)
                test_correct += torch.sum(predicted == labels).item()
                t.set_postfix(test_loss = loss.item(), test_accuracy = test_correct / test_total)
            
    tb.add_scalar('train_loss', train_loss/train_total, epoch)
    tb.add_scalar('train_accuracy', train_correct/train_total, epoch)
    tb.add_scalar('test_loss', test_loss/test_total, epoch)
    tb.add_scalar('test_accuracy', test_correct / test_total, epoch)

结果

跑了3个epoch后, 验证集的准确率就已经达到99.1%了, 看来还是任务太简单了.

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值