《動手學深度學習》task10_1 圖像分類案例2

系統學習《動手學深度學習》點擊下面這個鏈接,有全目錄哦~
https://blog.csdn.net/Shine_rise/article/details/104754764

Kaggle上的狗品種識別(ImageNet Dogs)

在本節中,我們將解決Kaggle競賽中的犬種識別挑戰,比賽的網址是https://www.kaggle.com/c/dog-breed-identification 在這項比賽中,我們嘗試確定120種不同的狗。該比賽中使用的數據集實際上是著名的ImageNet數據集的子集。

# 在本節notebook中,使用後續設置的參數在完整訓練集上訓練模型,大致需要40-50分鐘
# 請大家合理安排GPU時長,儘量只在訓練時切換到GPU資源
# 也可以在Kaggle上訪問本節notebook:
# https://www.kaggle.com/boyuai/boyu-d2l-dog-breed-identification-imagenet-dogs
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import os
import shutil
import time
import pandas as pd
import random
# 設置隨機數種子
random.seed(0)
torch.manual_seed(0)
torch.cuda.manual_seed(0)

整理數據集

我們可以從比賽網址上下載數據集,其目錄結構爲:

| Dog Breed Identification
    | train
    |   | 000bec180eb18c7604dcecc8fe0dba07.jpg
    |   | 00a338a92e4e7bf543340dc849230e75.jpg
    |   | ...
    | test
    |   | 00a3edd22dc7859c487a64777fc8d093.jpg
    |   | 00a6892e5c7f92c1f465e213fd904582.jpg
    |   | ...
    | labels.csv
    | sample_submission.csv

train和test目錄下分別是訓練集和測試集的圖像,訓練集包含10,222張圖像,測試集包含10,357張圖像,圖像格式都是JPEG,每張圖像的文件名是一個唯一的id。labels.csv包含訓練集圖像的標籤,文件包含10,222行,每行包含兩列,第一列是圖像id,第二列是狗的類別。狗的類別一共有120種。

我們希望對數據進行整理,方便後續的讀取,我們的主要目標是:

  • 從訓練集中劃分出驗證數據集,用於調整超參數。劃分之後,數據集應該包含4個部分:劃分後的訓練集、劃分後的驗證集、完整訓練集、完整測試集
  • 對於4個部分,建立4個文件夾:train, valid, train_valid, test。在上述文件夾中,對每個類別都建立一個文件夾,在其中存放屬於該類別的圖像。前三個部分的標籤已知,所以各有120個子文件夾,而測試集的標籤未知,所以僅建立一個名爲unknown的子文件夾,存放所有測試數據。

我們希望整理後的數據集目錄結構爲:

| train_valid_test
    | train
    |   | affenpinscher
    |   |   | 00ca18751837cd6a22813f8e221f7819.jpg
    |   |   | ...
    |   | afghan_hound
    |   |   | 0a4f1e17d720cdff35814651402b7cf4.jpg
    |   |   | ...
    |   | ...
    | valid
    |   | affenpinscher
    |   |   | 56af8255b46eb1fa5722f37729525405.jpg
    |   |   | ...
    |   | afghan_hound
    |   |   | 0df400016a7e7ab4abff824bf2743f02.jpg
    |   |   | ...
    |   | ...
    | train_valid
    |   | affenpinscher
    |   |   | 00ca18751837cd6a22813f8e221f7819.jpg
    |   |   | ...
    |   | afghan_hound
    |   |   | 0a4f1e17d720cdff35814651402b7cf4.jpg
    |   |   | ...
    |   | ...
    | test
    |   | unknown
    |   |   | 00a3edd22dc7859c487a64777fc8d093.jpg
    |   |   | ...
data_dir = '/home/kesci/input/Kaggle_Dog6357/dog-breed-identification'  # 數據集目錄
label_file, train_dir, test_dir = 'labels.csv', 'train', 'test'  # data_dir中的文件夾、文件
new_data_dir = './train_valid_test'  # 整理之後的數據存放的目錄
valid_ratio = 0.1  # 驗證集所佔比例
def mkdir_if_not_exist(path):
    # 若目錄path不存在,則創建目錄
    if not os.path.exists(os.path.join(*path)):
        os.makedirs(os.path.join(*path))
        
def reorg_dog_data(data_dir, label_file, train_dir, test_dir, new_data_dir, valid_ratio):
    # 讀取訓練數據標籤
    labels = pd.read_csv(os.path.join(data_dir, label_file))
    id2label = {Id: label for Id, label in labels.values}  # (key: value): (id: label)

    # 隨機打亂訓練數據
    train_files = os.listdir(os.path.join(data_dir, train_dir))
    random.shuffle(train_files)    

    # 原訓練集
    valid_ds_size = int(len(train_files) * valid_ratio)  # 驗證集大小
    for i, file in enumerate(train_files):
        img_id = file.split('.')[0]  # file是形式爲id.jpg的字符串
        img_label = id2label[img_id]
        if i < valid_ds_size:
            mkdir_if_not_exist([new_data_dir, 'valid', img_label])
            shutil.copy(os.path.join(data_dir, train_dir, file),
                        os.path.join(new_data_dir, 'valid', img_label))
        else:
            mkdir_if_not_exist([new_data_dir, 'train', img_label])
            shutil.copy(os.path.join(data_dir, train_dir, file),
                        os.path.join(new_data_dir, 'train', img_label))
        mkdir_if_not_exist([new_data_dir, 'train_valid', img_label])
        shutil.copy(os.path.join(data_dir, train_dir, file),
                    os.path.join(new_data_dir, 'train_valid', img_label))

    # 測試集
    mkdir_if_not_exist([new_data_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(new_data_dir, 'test', 'unknown'))
reorg_dog_data(data_dir, label_file, train_dir, test_dir, new_data_dir, valid_ratio)

圖像增強

transform_train = transforms.Compose([
    # 隨機對圖像裁剪出面積爲原圖像面積0.08~1倍、且高和寬之比在3/4~4/3的圖像,再放縮爲高和寬均爲224像素的新圖像
    transforms.RandomResizedCrop(224, scale=(0.08, 1.0),  
                                 ratio=(3.0/4.0, 4.0/3.0)),
    # 以0.5的概率隨機水平翻轉
    transforms.RandomHorizontalFlip(),
    # 隨機更改亮度、對比度和飽和度
    transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4),
    transforms.ToTensor(),
    # 對各個通道做標準化,(0.485, 0.456, 0.406)和(0.229, 0.224, 0.225)是在ImageNet上計算得的各通道均值與方差
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])  # ImageNet上的均值和方差
])

# 在測試集上的圖像增強只做確定性的操作
transform_test = transforms.Compose([
    transforms.Resize(256),
    # 將圖像中央的高和寬均爲224的正方形區域裁剪出來
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

讀取數據

# new_data_dir目錄下有train, valid, train_valid, test四個目錄
# 這四個目錄中,每個子目錄表示一種類別,目錄中是屬於該類別的所有圖像
train_ds = torchvision.datasets.ImageFolder(root=os.path.join(new_data_dir, 'train'),
                                            transform=transform_train)
valid_ds = torchvision.datasets.ImageFolder(root=os.path.join(new_data_dir, 'valid'),
                                            transform=transform_test)
train_valid_ds = torchvision.datasets.ImageFolder(root=os.path.join(new_data_dir, 'train_valid'),
                                            transform=transform_train)
test_ds = torchvision.datasets.ImageFolder(root=os.path.join(new_data_dir, 'test'),
                                            transform=transform_test)
batch_size = 128
train_iter = torch.utils.data.DataLoader(train_ds, batch_size=batch_size, shuffle=True)
valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size=batch_size, shuffle=True)
train_valid_iter = torch.utils.data.DataLoader(train_valid_ds, batch_size=batch_size, shuffle=True)
test_iter = torch.utils.data.DataLoader(test_ds, batch_size=batch_size, shuffle=False)  # shuffle=False

定義模型

這個比賽的數據屬於ImageNet數據集的子集,我們使用微調的方法,選用在ImageNet完整數據集上預訓練的模型來抽取圖像特徵,以作爲自定義小規模輸出網絡的輸入。

此處我們使用與訓練的ResNet-34模型,直接複用預訓練模型在輸出層的輸入,即抽取的特徵,然後我們重新定義輸出層,本次我們僅對重定義的輸出層的參數進行訓練,而對於用於抽取特徵的部分,我們保留預訓練模型的參數。

def get_net(device):
    finetune_net = models.resnet34(pretrained=False)  # 預訓練的resnet34網絡
    finetune_net.load_state_dict(torch.load('/home/kesci/input/resnet347742/resnet34-333f7ec4.pth'))
    for param in finetune_net.parameters():  # 凍結參數
        param.requires_grad = False
    # 原finetune_net.fc是一個輸入單元數爲512,輸出單元數爲1000的全連接層
    # 替換掉原finetune_net.fc,新finetuen_net.fc中的模型參數會記錄梯度
    finetune_net.fc = nn.Sequential(
        nn.Linear(in_features=512, out_features=256),
        nn.ReLU(),
        nn.Linear(in_features=256, out_features=120)  # 120是輸出類別數
    )
    return finetune_net

定義訓練函數

def evaluate_loss_acc(data_iter, net, device):
    # 計算data_iter上的平均損失與準確率
    loss = nn.CrossEntropyLoss()
    is_training = net.training  # Bool net是否處於train模式
    net.eval()
    l_sum, acc_sum, n = 0, 0, 0
    with torch.no_grad():
        for X, y in data_iter:
            X, y = X.to(device), y.to(device)
            y_hat = net(X)
            l = loss(y_hat, y)
            l_sum += l.item() * y.shape[0]
            acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
    net.train(is_training)  # 恢復net的train/eval狀態
    return l_sum / n, acc_sum / n
def train(net, train_iter, valid_iter, num_epochs, lr, wd, device, lr_period,
          lr_decay):
    loss = nn.CrossEntropyLoss()
    optimizer = optim.SGD(net.fc.parameters(), lr=lr, momentum=0.9, weight_decay=wd)
    net = net.to(device)
    for epoch in range(num_epochs):
        train_l_sum, n, start = 0.0, 0, time.time()
        if epoch > 0 and epoch % lr_period == 0:  # 每lr_period個epoch,學習率衰減一次
            lr = lr * lr_decay
            for param_group in optimizer.param_groups:
                param_group['lr'] = lr
        for X, y in train_iter:
            X, y = X.to(device), y.to(device)
            optimizer.zero_grad()
            y_hat = net(X)
            l = loss(y_hat, y)
            l.backward()
            optimizer.step()
            train_l_sum += l.item() * y.shape[0]
            n += y.shape[0]
        time_s = "time %.2f sec" % (time.time() - start)
        if valid_iter is not None:
            valid_loss, valid_acc = evaluate_loss_acc(valid_iter, net, device)
            epoch_s = ("epoch %d, train loss %f, valid loss %f, valid acc %f, "
                       % (epoch + 1, train_l_sum / n, valid_loss, valid_acc))
        else:
            epoch_s = ("epoch %d, train loss %f, "
                       % (epoch + 1, train_l_sum / n))
        print(epoch_s + time_s + ', lr ' + str(lr))

調參

num_epochs, lr_period, lr_decay = 20, 10, 0.1
lr, wd = 0.03, 1e-4
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
net = get_net(device)
train(net, train_iter, valid_iter, num_epochs, lr, wd, device, lr_period, lr_decay)

在完整數據集上訓練模型

# 使用上面的參數設置,在完整數據集上訓練模型大致需要40-50分鐘的時間
net = get_net(device)
train(net, train_valid_iter, None, num_epochs, lr, wd, device, lr_period, lr_decay)

對測試集分類並提交結果

用訓練好的模型對測試數據進行預測。比賽要求對測試集中的每張圖片,都要預測其屬於各個類別的概率。

preds = []
for X, _ in test_iter:
    X = X.to(device)
    output = net(X)
    output = torch.softmax(output, dim=1)
    preds += output.tolist()
ids = sorted(os.listdir(os.path.join(new_data_dir, 'test/unknown')))
with open('submission.csv', 'w') as f:
    f.write('id,' + ','.join(train_valid_ds.classes) + '\n')
    for i, output in zip(ids, preds):
        f.write(i.split('.')[0] + ',' + ','.join(
            [str(num) for num in output]) + '\n')

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章