39 實(shí)戰(zhàn) Kaggle 比賽:圖像分類(CIFAR-10)【動(dòng)手學(xué)深度學(xué)習(xí)v2

CIFAR-10經(jīng)典分類實(shí)戰(zhàn)
在實(shí)踐中,圖像數(shù)據(jù)集通常以圖像文件的形式出現(xiàn)。 本節(jié)將從原始圖像文件開(kāi)始,然后逐步組織、讀取并將它們轉(zhuǎn)換為張量格式。
import collections import math import os import shutil '''shell util是Python的一個(gè)倒騰文件的東西''' import pandas as pd import torch import torchvision from torch import nn from d2l import torch as d2l
比賽數(shù)據(jù)集分為訓(xùn)練集和測(cè)試集,其中訓(xùn)練集包含50000張、測(cè)試集包含300000張圖像。 在測(cè)試集中,10000張圖像將被用于評(píng)估,而剩下的290000張圖像將不會(huì)被進(jìn)行評(píng)估,包含它們只是為了防止手動(dòng)標(biāo)記測(cè)試集并提交標(biāo)記結(jié)果。 兩個(gè)數(shù)據(jù)集中的圖像都是png格式,高度和寬度均為32像素并有三個(gè)顏色通道(RGB)。 這些圖片共涵蓋10個(gè)類別:飛機(jī)、汽車、鳥(niǎo)類、貓、鹿、狗、青蛙、馬、船和卡車。為了便于入門,此處提供包含前1000個(gè)訓(xùn)練圖像和5個(gè)隨機(jī)測(cè)試圖像的數(shù)據(jù)集的小規(guī)模樣本。
#@save d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip', '2068874e4b9a9f0fb07ebe0ad2b29754449ccacd') # 如果使用完整的Kaggle競(jìng)賽的數(shù)據(jù)集,設(shè)置demo為False demo = True if demo: data_dir = d2l.download_extract('cifar10_tiny') else: data_dir = '../data/cifar-10/'
#@save def read_csv_labels(fname): """讀取fname來(lái)給標(biāo)簽字典返回一個(gè)文件名""" with open(fname, 'r') as f: # 跳過(guò)文件頭行(列名) lines = f.readlines()[1:] tokens = [l.rstrip().split(',') for l in lines] return dict(((name, label) for name, label in tokens)) labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv')) print('# 訓(xùn)練樣本 :', len(labels)) print('# 類別 :', len(set(labels.values())))
# 訓(xùn)練樣本 : 1000 # 類別 : 10
#@save def copyfile(filename, target_dir): """將文件復(fù)制到目標(biāo)目錄""" os.makedirs(target_dir, exist_ok=True) shutil.copy(filename, target_dir) #@save def reorg_train_valid(data_dir, labels, valid_ratio): """將驗(yàn)證集從原始的訓(xùn)練集中拆分出來(lái)""" # 訓(xùn)練數(shù)據(jù)集中樣本最少的類別中的樣本數(shù) n = collections.Counter(labels.values()).most_common()[-1][1] # 驗(yàn)證集中每個(gè)類別的樣本數(shù) 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
#@save def reorg_test(data_dir): """在預(yù)測(cè)期間整理測(cè)試集,以方便讀取""" 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'))
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)
在這里,我們只將樣本數(shù)據(jù)集的批量大小設(shè)置為32。 在實(shí)際訓(xùn)練和測(cè)試中,應(yīng)該使用Kaggle競(jìng)賽的完整數(shù)據(jù)集,并將batch_size
設(shè)置為更大的整數(shù),例如128。 我們將10%的訓(xùn)練樣本作為調(diào)整超參數(shù)的驗(yàn)證集。
batch_size = 32 if demo else 128 valid_ratio = 0.1 reorg_cifar10_data(data_dir, valid_ratio)
使用圖像增廣來(lái)解決過(guò)擬合的問(wèn)題。例如在訓(xùn)練中,我們可以隨機(jī)水平翻轉(zhuǎn)圖像。 我們還可以對(duì)彩色圖像的三個(gè)RGB通道執(zhí)行標(biāo)準(zhǔn)化。 下面,我們列出了其中一些可以調(diào)整的操作。
transform_train = torchvision.transforms.Compose([ # 在高度和寬度上將圖像放大到40像素的正方形 torchvision.transforms.Resize(40), # 隨機(jī)裁剪出一個(gè)高度和寬度均為40像素的正方形圖像, # 生成一個(gè)面積為原始圖像面積0.64~1倍的小正方形, # 然后將其縮放為高度和寬度均為32像素的正方形 torchvision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0), ratio=(1.0, 1.0)), torchvision.transforms.RandomHorizontalFlip(), torchvision.transforms.ToTensor(), # 標(biāo)準(zhǔn)化圖像的每個(gè)通道 torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010])])
在測(cè)試期間,我們只對(duì)圖像執(zhí)行標(biāo)準(zhǔn)化,以消除評(píng)估結(jié)果中的隨機(jī)性。
transform_test = torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010])])
接下來(lái),我們讀取由原始圖像組成的數(shù)據(jù)集,每個(gè)樣本都包括一張圖片和一個(gè)標(biāo)簽。
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']]
在訓(xùn)練期間,我們需要指定上面定義的所有圖像增廣操作。 當(dāng)驗(yàn)證集在超參數(shù)調(diào)整過(guò)程中用于模型評(píng)估時(shí),不應(yīng)引入圖像增廣的隨機(jī)性。 在最終預(yù)測(cè)之前,我們根據(jù)訓(xùn)練集和驗(yàn)證集組合而成的訓(xùn)練模型進(jìn)行訓(xùn)練,以充分利用所有標(biāo)記的數(shù)據(jù)。
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)
定義了?7.6節(jié)中描述的Resnet-18模型。
def get_net(): num_classes = 10 net = d2l.resnet18(num_classes, 3) return net loss = nn.CrossEntropyLoss(reduction="none")
定義了模型訓(xùn)練函數(shù)train
。
注意:這里的lr_period和lr_decay是用于對(duì)learning rate進(jìn)行衰退的超參數(shù)。意思是在lr_period個(gè)輪次后lr將與lr_decay相乘從而衰退。
def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, lr_decay): trainer = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9, weight_decay=wd) scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay) 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() metric = d2l.Accumulator(3) for i, (features, labels) in enumerate(train_iter): timer.start() l, acc = d2l.train_batch_ch13(net, features, labels, loss, trainer, devices) metric.add(l, acc, labels.shape[0]) timer.stop() if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: animator.add(epoch + (i + 1) / num_batches, (metric[0] / metric[2], metric[1] / metric[2], None)) if valid_iter is not None: valid_acc = d2l.evaluate_accuracy_gpu(net, valid_iter) animator.add(epoch + 1, (None, None, valid_acc)) scheduler.step() measures = (f'train loss {metric[0] / metric[2]:.3f}, ' f'train acc {metric[1] / metric[2]:.3f}') if valid_iter is not None: measures += f', valid acc {valid_acc:.3f}' print(measures + f'\n{metric[2] * num_epochs / timer.sum():.1f}' f' examples/sec on {str(devices)}')
現(xiàn)在,我們可以訓(xùn)練和驗(yàn)證模型了,而以下所有超參數(shù)都可以調(diào)整。 例如,我們可以增加周期的數(shù)量。當(dāng)lr_period
和lr_decay
分別設(shè)置為4和0.9時(shí),優(yōu)化算法的學(xué)習(xí)速率將在每4個(gè)周期乘以0.9。 為便于演示,我們?cè)谶@里只訓(xùn)練20個(gè)周期。
devices, num_epochs, lr, wd = d2l.try_all_gpus(), 20, 2e-4, 5e-4 lr_period, lr_decay, net = 4, 0.9, get_net() train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, lr_decay)
train loss 0.668, train acc 0.781, valid acc 0.453 1022.7 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)]
知識(shí)補(bǔ)充:
數(shù)據(jù)增廣方法中,標(biāo)準(zhǔn)化處理normalize函數(shù)中的參數(shù)是從imagenet中得到的,實(shí)際取任何值都可以。影響不大
# 標(biāo)準(zhǔn)化圖像的每個(gè)通道 torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010])])
·weight decay和 lr decay是不同的。
ld是為了使得模型盡快收斂。
wd是為了normalization
ld的設(shè)置最好能使得lr在前期保持一個(gè)較大得值。后期再變小。比較常用的辦法是用cos函數(shù)來(lái)實(shí)現(xiàn)。