动手学深度学习Kaggle:图像分类 (CIFAR-10和Dog Breed Identification), StratifiedShuffleSplit,数据集划分

目录

CIFAR-10

获取并组织数据集

下载数据集

整理数据集

组织数据集更一般的方式

图像增广

读取数据集 

torchvision.datasets.ImageFolder()的特点

定义模型

定义训练函数

训练和验证模型

对测试集进行分类并提交结果

(补充)超参数调整

Dog Breed Identification

特点

获取数据集

整理数据集

由字符型的类别标签得到数字类型的类别标签

划分出验证集

自定义数据集函数

图像增广

读取数据集

定义模型

定义训练函数

训练和验证模型

对测试集分类并在Kaggle提交结果

需要注意的函数

pd.read_csv

torchvision.datasets.ImageFolder()

collections.Counter()

**和*

sort

Series.apply()

StratifiedShuffleSplit和train_test_split

nn.CrossEntropyLoss

sort和sorted

torch的和numpy的transpose


这个练习本身不难,亮点在于对数据集的处理,值得研究

CIFAR-10

比赛的网址是CIFAR-10 - Object Recognition in Images | Kaggle

原教程网站:13.13. 实战 Kaggle 比赛:图像分类 (CIFAR-10) — 动手学深度学习 2.0.0-beta1 documentation

import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import pandas as pd
import os
import collections
import shutil
import math
from d2l import torch as d2l 


data_dir = '.\data\cifar-10'

获取并组织数据集

比赛数据集分为训练集和测试集,其中训练集包含50000张、测试集包含300000张图像。 在测试集中,10000张图像将被用于评估,而剩下的290000张图像将不会被进行评估,包含它们只是为了防止手动标记测试集并提交标记结果。 两个数据集中的图像都是png格式,高度和宽度均为32像素并有三个颜色通道(RGB)。 这些图片共涵盖10个类别:飞机、汽车、鸟类、猫、鹿、狗、青蛙、马、船和卡车。

下载数据集

登录Kaggle后,我们可以点击CIFAR-10图像分类竞赛网页上的“Data”选项卡,然后单击“Download All”按钮下载数据集。 在../data中解压下载的文件,将在以下路径中找到整个数据集:

  • ../data/cifar-10/train/[1-50000].png

  • ../data/cifar-10/test/[1-300000].png

  • ../data/cifar-10/trainLabels.csv

  • ../data/cifar-10/sampleSubmission.csv

traintest文件夹分别包含训练和测试图像,trainLabels.csv含有训练图像的标签, sample_submission.csv是提交文件的范例。其中train文件夹含有样本图片的如下图,每一个图片的名字都是一个数字编号。从中读取图片的时候要注意了,比如读取1号图片,在label中对应的标签是第0个

用pandas读取trainLabels.csv文件:

          id       label
0          1        frog
1          2       truck
2          3       truck
...      ...         ...
49998  49999  automobile
49999  50000  automobile

整理数据集

数据集只含有train和test数据集,而我们在训练的时候,一般还包含验证集,所以要划分出验证集处理。使用Google Colab这样的平台时,我们经常会将训练集、测试集、验证集压缩并上传,所以有时候要将它们划分、保存在不同的文件夹。

定义reorg_train_valid函数来将验证集从原始的训练集中拆分出来。 此函数中的参数valid_ratio是验证集中的样本数与原始训练集中的样本数之比。 更具体地说,令n等于样本最少的类别中的图像数量,而r是比率。 验证集将为每个类别拆分出max(⌊nr⌋,1)张图像。 让我们以valid_ratio=0.1为例,由于原始的训练集有50000张图像,因此train_valid_test/train路径中将有45000张图像用于训练,而剩下5000张图像将作为路径train_valid_test/valid中的验证集。 组织数据集后,同类别的图像将被放置在同一文件夹下

def copyfile(filename, target_dir):
    """将文件复制到目标目录"""
    os.makedirs(target_dir, exist_ok=True)  # 文件夹不存在则创建
    shutil.copy(filename, target_dir)

def reorg_train_valid(data_dir, labels, valid_ratio):
    """将验证集从原始的训练集中拆分出来"""
    # 训练数据集中样本最少的类别中的样本数,labels的类型是Series,可以用Counter()计数
    n = collections.Counter(labels).most_common()[-1][1]
    # 验证集中每个类别的样本数
    n_valid_per_label = max(1, math.floor(n * valid_ratio))
    label_count = {}
    for train_file in os.listdir(os.path.join(data_dir, 'train')):
        idx = train_file.split('.')[0]  # 图片的名称,本例中是编号(1-50000)        
        label = labels[int(idx)-1]  # csv文件中,每个编号对应的label
        fname = os.path.join(data_dir, 'train', train_file)  # 每个图片的文件名称
        # 用每个label建立一个文件夹,将所有图片复制到对应的文件夹中。这么做是为了使用ImageFolder
        copyfile(fname, os.path.join(data_dir, 'train_valid_test',
                                     'train_valid', label)) 
        # 每一个valid文件夹中,每一个类别的样本数量都是n_valid_per_label
        if label not in label_count or label_count[label] < n_valid_per_label:
            copyfile(fname, os.path.join(data_dir, 'train_valid_test',
                                         'valid', label))
            label_count[label] = label_count.get(label, 0) + 1
        else:
            copyfile(fname, os.path.join(data_dir, 'train_valid_test',
                                         'train', label))

    return n_valid_per_label

def reorg_test(data_dir):
    """在预测期间整理测试集,以方便读取"""
    for test_file in os.listdir(os.path.join(data_dir, 'test')):
        copyfile(os.path.join(data_dir, 'test', test_file),
                 os.path.join(data_dir, 'train_valid_test', 'test',
                              'unknown'))

读取label并执行上述函数

def reorg_cifar10_data(data_dir, valid_ratio):
    labels = pd.read_csv(os.path.join(data_dir, 'trainLabels.csv'))['label']    
    reorg_train_valid(data_dir, labels, valid_ratio)
    reorg_test(data_dir)
    
batch_size = 64
valid_ratio = 0.1
reorg_cifar10_data(data_dir, valid_ratio)

代码执行的效果是,创建了四个文件夹,分别是test,train(45000个样本),valid(5000个样本)和train_valid,其中train_valid是train和valid的合集。建立train_valid文件夹是因为,使用验证集筛选出最佳超参数之后,再使用train_valid训练一遍,得到最终模型

每一个文件夹下按照类别创建了10个分类文件夹,这是torchvision.datasets.ImageFolder()函数的要求。即使没有分类的test文件夹,下面也要有一个子文件夹(unknown)作为分类文件夹,否则torchvision.datasets.ImageFolder()会报错。因为ImageFolder()的find_classes()函数要从根文件夹下读取文件夹的名称,生成类别列表,没有这个列表就会导致错误

组织数据集更一般的方式

为了在训练集和验证集文件夹下按类别建立子文件夹,需要在读取每个样本图片名的时候,获得对应的类别标签label。然而,pandas一般根据表的index或者行数来选择数据,我没找到根据某一列的值索引其他列的数据的方法。上一节采取了一种讨巧的方法,样本的名字(也就是图片文件名)是从1开始编号,编号减一就是在表中的行号,但是有时候样本的文件名是不规则的,这时候就要使用更一般的方法,根据一列的数据索引另一列的数据,下面的read_csv_labels()函数起到这样的作用

def read_csv_labels(fname):
    """读取fname来给标签字典返回一个文件名"""
    with open(fname, 'r') as f:
        # 跳过文件头行(列名)
        lines = f.readlines()[1:]
    tokens = [l.rstrip().split(',') for l in lines]
    return dict(((name, label) for name, label in tokens))


def copyfile(filename, target_dir):
    """将文件复制到目标目录"""
    os.makedirs(target_dir, exist_ok=True)
    shutil.copy(filename, target_dir)


def reorg_train_valid(data_dir, labels, valid_ratio):
    """将验证集从原始的训练集中拆分出来"""
    # 训练数据集中样本最少的类别中的样本数
    n = collections.Counter(labels.values()).most_common()[-1][1]
    # 验证集中每个类别的样本数
    n_valid_per_label = max(1, math.floor(n * valid_ratio))
    label_count = {}
    for train_file in os.listdir(os.path.join(data_dir, 'train')):
        label = labels[train_file.split('.')[0]]
        fname = os.path.join(data_dir, 'train', train_file)
        copyfile(fname, os.path.join(data_dir, 'train_valid_test',
                                     'train_valid', label))
        if label not in label_count or label_count[label] < n_valid_per_label:
            copyfile(fname, os.path.join(data_dir, 'train_valid_test',
                                         'valid', label))
            label_count[label] = label_count.get(label, 0) + 1
        else:
            copyfile(fname, os.path.join(data_dir, 'train_valid_test',
                                         'train', label))
    return n_valid_per_label


def reorg_test(data_dir):
    """在预测期间整理测试集,以方便读取"""
    for test_file in os.listdir(os.path.join(data_dir, 'test')):
        copyfile(os.path.join(data_dir, 'test', test_file),
                 os.path.join(data_dir, 'train_valid_test', 'test',
                              'unknown'))

read_csv_labels函数返回的是一个字典格式的变量,该变量根据name可以索引label:label=labels[train_file.split('.')[0]]。并且labels.values()的格式是<class 'builtin_function_or_method'>,可以用于collections.Counter()方法

def reorg_cifar10_data(data_dir, valid_ratio):
    labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
    reorg_train_valid(data_dir, labels, valid_ratio)
    reorg_test(data_dir)


batch_size = 128
valid_ratio = 0.1
reorg_cifar10_data(data_dir, valid_ratio)

图像增广

使用图像增广来解决过拟合的问题。例如在训练中,可以随机水平翻转图像等。在测试期间,只对图像执行标准化,以消除评估结果中的随机性。

本例使用的是残差网络。由于残差网络没有使用全连接层,在pytorch中,残差网络的输入可以是任意值(即使输入像素大小为1*1)。但是DataLoader要求每一个样本的尺寸是相同的。本例数据集中图片像素大小都是32*32,所以可以直接使用,但是为了做图像增广,所以下面先将图像放大为40*40,然后进行随机裁剪,重新缩放到32*32。所以对于其他数据来说,其实第一步Resize(40)不是必须的,可以使用RandomResizedCrop一步到位裁剪到目标尺寸。

transform_train = torchvision.transforms.Compose([
    # 在高度和宽度上将图像放大到40像素的正方形
    torchvision.transforms.Resize(40),
    # 随机裁剪出一个面积为原始图像面积0.64到1倍, 高宽比为1.0-1.0的图像
    # 然后将其缩放为高度和宽度均为32像素的正方形
    torchvision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0),
                                                   ratio=(1.0, 1.0)),
    torchvision.transforms.RandomHorizontalFlip(),
    torchvision.transforms.ToTensor(),
    # 标准化图像的每个通道
    torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465],
                                     [0.2023, 0.1994, 0.2010])])

transforms_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize([0.4914, 0.4822, 0.4465],
                         [0.2023, 0.1994, 0.2010])
])

读取数据集 

读取由原始图像组成的数据集,每个样本都包括一张图片和一个标签。注意,当验证集在超参数调整过程中用于模型评估时,不应引入图像增广的随机性,所以valid数据集使用的transform是transform_test

train_ds, train_valid_ds = [torchvision.datasets.ImageFolder(
    os.path.join(data_dir, 'train_valid_test', folder),
    transform=transform_train) for folder in ['train', 'train_valid']]

valid_ds, test_ds = [torchvision.datasets.ImageFolder(
    os.path.join(data_dir, 'train_valid_test', folder),
    transform=transform_test) for folder in ['valid', 'test']]

train_iter, train_valid_iter = [torch.utils.data.DataLoader(
    dataset, batch_size, shuffle=True, drop_last=True)
    for dataset in (train_ds, train_valid_ds)]

valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False,
                                         drop_last=True)

test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False,
                                        drop_last=False)

torchvision.datasets.ImageFolder()的特点

具体解释见本文最后一部分。

  • torchvision.datasets.ImageFolder()方法自动将字符类型的label转变为int类型:

>>len(train_ds)

45000

>>type(train_ds[0])  #  train_ds的每一个元素是一个元祖

<class 'tuple'>

>>train_ds[0][0].shape  # 元祖第一个元素是图片向量

torch.Size([3, 32, 32])

>>train_ds[0][1]     #  元祖第二个元素是int类型的标签

0
  • 经过torchvision.datasets.ImageFolder()处理,数据集自动产生了类别这一属性:

>>train_ds.classes

['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
  • ImageFolder()读取样本的顺序是按照str顺序排序的

定义模型

由于是一个小模型,所以没有使用迁移学习,而是整个模型的参数都要训练。如果要迁移学习,那么模型下载的时候还要加上pretrained=True,并且要冻结参数

def get_net():
    num_classes = 10    
    net = torchvision.models.resnet18()
    net.fc = nn.Linear(512, num_classes)
    return net

loss = nn.CrossEntropyLoss(reduction="none") # 为了作图的时候,loss的量级和accuracy的量级相近

定义训练函数

根据模型在验证集上的表现来选择模型并调整超参数

def train(net, train_iter, num_epochs, lr, wd, lr_period, lr_decay, devices, valid_iter=None):
    optimizer = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9, weight_decay=wd)
    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, lr_period, lr_decay)
    # timer用于计算程序运行时间, timer.stop()之后再运行timer.start(),时间可以累积
    num_batches, timer = len(train_iter), d2l.Timer()  
    legend = ['train loss', 'train acc']
    if valid_iter is not None:
        legend.append('valid acc')
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], legend=legend)
    net = nn.DataParallel(net, device_ids=devices).to(devices[0])
    for epoch in range(num_epochs):
        net.train()
        running_loss, num, acc = 0, 0, 0 
        for i, (data, label) in enumerate(train_iter):
            timer.start()
            data
  • 0
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值