2018.8.8 kaggle 圖像分類項目

1.今天接到一個電話面試,上來就問我怎麼檢查單鏈表有環。
思路:如果一個單鏈表中有環,用一個指針去遍歷,永遠不會結束,所以可以用兩個指針,一個指針一次走一步,另一個指針一次走兩步,如果存在環,則這兩個指針會在環內相遇,時間複雜度爲O(n)。

2.Kaggle 比賽:圖像分類(CIFAR-10)

首先,導入實驗所需的包或模塊。

import sys
sys.path.insert(0, '..')

import datetime
import gluonbook as gb
from mxnet import autograd, gluon, init
from mxnet.gluon import data as gdata, nn, loss as gloss
import os
import pandas as pd
import shutil

比賽數據分爲訓練集和測試集。訓練集包含 5 萬張圖片。測試集包含 30 萬張圖片:其中有 1 萬張圖片用來計分,其他 29 萬張不計分的圖片是爲了防止人工標註測試集。兩個數據集中的圖片格式都是 png,高和寬均爲 32 像素,並含有 RGB 三個通道(彩色)。圖片一共涵蓋 10 個類別,分別爲飛機、汽車、鳥、貓、鹿、狗、青蛙、馬、船和卡車,如圖 9.16 所示。
這裏寫圖片描述

下載數據集
登錄 Kaggle 後,我們可以點擊圖 9.15 所示的 CIFAR-10 圖像分類比賽網頁上的“Data”標籤,並分別下載訓練數據集“train.7z”、測試數據集“test.7z”和訓練數據集標籤“trainLabels.csv”。

解壓數據集
下載完訓練數據集“train.7z”和測試數據集“test.7z”後請解壓縮。解壓縮後,將訓練數據集、測試數據集和訓練數據集標籤分別存放在以下路徑:

../data/kaggle_cifar10/train/[1-50000].png
../data/kaggle_cifar10/test/[1-300000].png
../data/kaggle_cifar10/trainLabels.csv

爲方便快速上手,我們提供了上述數據集的小規模採樣,例如僅含 100 個訓練樣本的“train_tiny.zip”和 1 個測試樣本的“test_tiny.zip”。它們解壓後的文件夾名稱分別爲“train_tiny”和“test_tiny”。此外,訓練數據集標籤的壓縮文件解壓後得到“trainLabels.csv”。如果你將使用上述 Kaggle 比賽的完整數據集,還需要把下面demo變量改爲False。

# 如果使用下載的 Kaggle 比賽的完整數據集,把下面 demo 變量改爲 False。
demo = True
if demo:
    import zipfile
    for f in ['train_tiny.zip', 'test_tiny.zip', 'trainLabels.csv.zip']:
        with zipfile.ZipFile('../data/kaggle_cifar10/' + f, 'r') as z:
            z.extractall('../data/kaggle_cifar10/')


整理數據集
我們接下來定義reorg_cifar10_data函數來整理數據集。整理後,同一類圖片將被放在同一個文件夾下,便於我們稍後讀取。該函數中的參數valid_ratio是驗證集樣本數與原始訓練集樣本數之比。以valid_ratio=0.1爲例,由於原始訓練數據集有 50,000 張圖片,調參時將有 45,000 張圖片用於訓練並存放在路徑“input_dir/train”,而另外 5,000 張圖片爲驗證集並存放在路徑“input_dir/valid”。
def reorg_cifar10_data(data_dir, label_file, train_dir, test_dir, input_dir,
                       valid_ratio):
    # 讀取訓練數據集標籤。
    with open(os.path.join(data_dir, label_file), 'r') as f:
        # 跳過文件頭行(欄名稱)。
        lines = f.readlines()[1:]
        tokens = [l.rstrip().split(',') for l in lines]
        idx_label = dict(((int(idx), label) for idx, label in tokens))
    labels = set(idx_label.values())

    n_train_valid = len(os.listdir(os.path.join(data_dir, train_dir)))
    n_train = int(n_train_valid * (1 - valid_ratio))
    assert 0 < n_train < n_train_valid
    n_train_per_label = n_train // len(labels)
    label_count = {}

    def mkdir_if_not_exist(path):
        if not os.path.exists(os.path.join(*path)):
            os.makedirs(os.path.join(*path))

    # 整理訓練和驗證集。
    for train_file in os.listdir(os.path.join(data_dir, train_dir)):
        idx = int(train_file.split('.')[0])
        label = idx_label[idx]
        mkdir_if_not_exist([data_dir, input_dir, 'train_valid', label])
        shutil.copy(os.path.join(data_dir, train_dir, train_file),
                    os.path.join(data_dir, input_dir, 'train_valid', label))
        if label not in label_count or label_count[label] < n_train_per_label:
            mkdir_if_not_exist([data_dir, input_dir, 'train', label])
            shutil.copy(os.path.join(data_dir, train_dir, train_file),
                        os.path.join(data_dir, input_dir, 'train', label))
            label_count[label] = label_count.get(label, 0) + 1
        else:
            mkdir_if_not_exist([data_dir, input_dir, 'valid', label])
            shutil.copy(os.path.join(data_dir, train_dir, train_file),
                        os.path.join(data_dir, input_dir, 'valid', label))

    # 整理測試集。
    mkdir_if_not_exist([data_dir, input_dir, 'test', 'unknown'])
    for test_file in os.listdir(os.path.join(data_dir, test_dir)):
        shutil.copy(os.path.join(data_dir, test_dir, test_file),
                    os.path.join(data_dir, input_dir, 'test', 'unknown'))
```

我們在這裏僅僅使用 100 個訓練樣本和 1 個測試樣本。訓練和測試數據集的文件夾名稱分別爲“train_tiny”和“test_tiny”。相應地,我們僅將批量大小設爲 1。實際訓練和測試時應使用 Kaggle 比賽的完整數據集,並將批量大小batch_size設爲一個較大的整數,例如 128。我們將 10% 的訓練樣本作爲調參時的驗證集。
if demo:
    # 注意:此處使用小訓練集。
    train_dir = 'train_tiny'
    # 注意:此處使用小測試集。
    test_dir = 'test_tiny'
    # 注意:此處將批量大小相應設小。使用 Kaggle 比賽的完整數據集時可設較大整數。
    batch_size = 1
else:
    train_dir = 'train'
    test_dir = 'test'
    batch_size = 128

data_dir = '../data/kaggle_cifar10'
label_file = 'trainLabels.csv'
input_dir = 'train_valid_test'
valid_ratio = 0.1
reorg_cifar10_data(data_dir, label_file, train_dir, test_dir, input_dir,
                   valid_ratio)

圖片增廣
爲應對過擬合,我們在這裏使用transforms來增廣數據。例如,加入transforms.RandomFlipLeftRight()即可隨機對圖片做鏡面反轉。我們也通過transforms.Normalize()對彩色圖像 RGB 三個通道分別做標準化。以下列舉了部分操作。這些操作可以根據需求來決定是否使用或修改。

transform_train = gdata.vision.transforms.Compose([
    # 將圖片放大成高和寬各爲 40 像素的正方形。
    gdata.vision.transforms.Resize(40),
    # 隨機對高和寬各爲 40 像素的正方形圖片裁剪出面積爲原圖片面積 0.64 到 1 倍之間的小正方
    # 形,再放縮爲高和寬各爲 32 像素的正方形。
    gdata.vision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0),
                                              ratio=(1.0, 1.0)),
    # 隨機左右翻轉圖片。
    gdata.vision.transforms.RandomFlipLeftRight(),
    # 將圖片像素值按比例縮小到 0 和 1 之間,並將數據格式從“高 * 寬 * 通道”改爲
    # “通道 * 高 * 寬”。
    gdata.vision.transforms.ToTensor(),
    # 對圖片的每個通道做標準化。
    gdata.vision.transforms.Normalize([0.4914, 0.4822, 0.4465],
                                      [0.2023, 0.1994, 0.2010])
])

# 測試時,無需對圖像做標準化以外的增強數據處理。
transform_test = gdata.vision.transforms.Compose([
    gdata.vision.transforms.ToTensor(),
    gdata.vision.transforms.Normalize([0.4914, 0.4822, 0.4465],
                                      [0.2023, 0.1994, 0.2010])
])

接下來,我們可以使用ImageFolderDataset類來讀取整理後的數據集,其中每個數據樣本包括圖像和標籤。需要注意的是,我們要在DataLoader中調用剛剛定義好的圖片增廣函數。其中transform_first函數指明對每個數據樣本中的圖像做數據增廣。

# 讀取原始圖像文件。flag=1 說明輸入圖像有三個通道(彩色)。
train_ds = gdata.vision.ImageFolderDataset(
    os.path.join(data_dir, input_dir, 'train'), flag=1)
valid_ds = gdata.vision.ImageFolderDataset(
    os.path.join(data_dir, input_dir, 'valid'), flag=1)
train_valid_ds = gdata.vision.ImageFolderDataset(
    os.path.join(data_dir, input_dir, 'train_valid'), flag=1)
test_ds = gdata.vision.ImageFolderDataset(
    os.path.join(data_dir, input_dir, 'test'), flag=1)

train_data = gdata.DataLoader(train_ds.transform_first(transform_train),
                              batch_size, shuffle=True, last_batch='keep')
valid_data = gdata.DataLoader(valid_ds.transform_first(transform_test),
                              batch_size, shuffle=True, last_batch='keep')
train_valid_data = gdata.DataLoader(train_valid_ds.transform_first(
    transform_train), batch_size, shuffle=True, last_batch='keep')
test_data = gdata.DataLoader(test_ds.transform_first(transform_test),
                             batch_size, shuffle=False, last_batch='keep')

定義模型

我們在這裏定義 ResNet-18 模型,並使用混合式編程來提升執行效率。


class Residual(nn.HybridBlock):
    def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs):
        super(Residual, self).__init__(**kwargs)
        self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1,
                               strides=strides)
        self.conv2 = nn.Conv2D(num_channels, kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2D(num_channels, kernel_size=1,
                                   strides=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm()
        self.bn2 = nn.BatchNorm()

    def hybrid_forward(self, F, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        return F.relu(Y + X)

def resnet18(num_classes):
    net = nn.HybridSequential()
    net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1),
            nn.BatchNorm(), nn.Activation('relu'))

    def resnet_block(num_channels, num_residuals, first_block=False):
        blk = nn.HybridSequential()
        for i in range(num_residuals):
            if i == 0 and not first_block:
                blk.add(Residual(num_channels, use_1x1conv=True, strides=2))
            else:
                blk.add(Residual(num_channels))
        return blk

    net.add(resnet_block(64, 2, first_block=True),
            resnet_block(128, 2),
            resnet_block(256, 2),
            resnet_block(512, 2))
    net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes))
    return net

def get_net(ctx):
    num_classes = 10
    net = resnet18(num_classes)
    net.initialize(ctx=ctx, init=init.Xavier())
    return net

定義訓練函數

我們將根據模型在驗證集上的表現來選擇模型並調節超參數。下面定義了模型的訓練函數train。我們記錄了每個迭代週期的訓練時間。這有助於比較不同模型的時間開銷。

loss = gloss.SoftmaxCrossEntropyLoss()

def train(net, train_data, valid_data, num_epochs, lr, wd, ctx, lr_period,
          lr_decay):
    trainer = gluon.Trainer(net.collect_params(), 'sgd',
                            {'learning_rate': lr, 'momentum': 0.9, 'wd': wd})
    prev_time = datetime.datetime.now()
    for epoch in range(num_epochs):
        train_l = 0.0
        train_acc = 0.0
        if epoch > 0 and epoch % lr_period == 0:
            trainer.set_learning_rate(trainer.learning_rate * lr_decay)
        for X, y in train_data:
            y = y.astype('float32').as_in_context(ctx)
            with autograd.record():
                y_hat = net(X.as_in_context(ctx))
                l = loss(y_hat, y)
            l.backward()
            trainer.step(batch_size)
            train_l += l.mean().asscalar()
            train_acc += gb.accuracy(y_hat, y)
        cur_time = datetime.datetime.now()
        h, remainder = divmod((cur_time - prev_time).seconds, 3600)
        m, s = divmod(remainder, 60)
        time_s = "time %02d:%02d:%02d" % (h, m, s)
        if valid_data is not None:
            valid_acc = gb.evaluate_accuracy(valid_data, net, ctx)
            epoch_s = ("epoch %d, loss %f, train acc %f, valid acc %f, "
                       % (epoch, train_l / len(train_data),
                          train_acc / len(train_data), valid_acc))
        else:
            epoch_s = ("epoch %d, loss %f, train acc %f, " %
                       (epoch, train_l / len(train_data),
                        train_acc / len(train_data)))
        prev_time = cur_time
        print(epoch_s + time_s + ', lr ' + str(trainer.learning_rate))

訓練並驗證模型

現在,我們可以訓練並驗證模型了。以下的超參數都是可以調節的,例如增加迭代週期。

ctx = gb.try_gpu()
num_epochs = 1
# 學習率。
lr = 0.1
# 權重衰減參數。
wd = 5e-4
# 優化算法的學習率將在每 80 個迭代週期時自乘 0.1。
lr_period = 80
lr_decay = 0.1

net = get_net(ctx)
net.hybridize()
train(net, train_data, valid_data, num_epochs, lr, wd, ctx, lr_period,
      lr_decay)

epoch 0, loss 7.106153, train acc 0.100000, valid acc 0.100000, time 00:00:02, lr 0.1

對測試集分類並在 Kaggle 提交結果

當得到一組滿意的模型設計和超參數後,我們使用所有訓練數據集(含驗證集)重新訓練模型,並對測試集分類。

net = get_net(ctx)
net.hybridize()
train(net, train_valid_data, None, num_epochs, lr, wd, ctx, lr_period,
      lr_decay)

preds = []
for X, _ in test_data:
    y_hat = net(X.as_in_context(ctx))
    preds.extend(y_hat.argmax(axis=1).astype(int).asnumpy())
sorted_ids = list(range(1, len(test_ds) + 1))
sorted_ids.sort(key=lambda x:str(x))
df = pd.DataFrame({'id': sorted_ids, 'label': preds})
df['label'] = df['label'].apply(lambda x: train_valid_ds.synsets[x])
df.to_csv('submission.csv', index=False)

epoch 0, loss 7.351089, train acc 0.100000, time 00:00:01, lr 0.1

執行完上述代碼後,會生成一個“submission.csv”文件。這個文件符合 Kaggle 比賽要求的提交格式。這時我們可以在 Kaggle 上把對測試集分類的結果提交併查看分類準確率。你需要登錄 Kaggle 網站,訪問 CIFAR-10 比賽網頁,並點擊右側“Submit Predictions”或“Late Submission”按鈕。然後,點擊頁面下方“Upload Submission File”選擇需要提交的分類結果文件。最後,點擊頁面最下方的“Make Submission”按鈕就可以查看結果了。

小結

CIFAR-10 是計算機視覺領域的一個重要的數據集。
我們可以應用卷積神經網絡、圖片增廣和混合式編程來實戰圖像分類比賽。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章