1. 程式人生 >實用技巧 >其它 >記錄一次失敗的深度學習經歷之Kaggle:貓狗大戰

記錄一次失敗的深度學習經歷之Kaggle:貓狗大戰


實驗環境

  • 直譯器版本: Python 3.8.5

  • 作業系統: ubuntu 18.04

  • 硬體型號: Xeon E7 v3,RTX 3060

  • 學習框架: PyTorch 1.8.1,scikit-learn 0.24.1,torchvision 0.9.1,cuda11.2

  • 資料來源: https://www.kaggle.com/c/dogs-vs-cats/data

基本模組

匯入模組

import os
import sys
import random
import numpy as np
import pandas as pd
import torch
from torch.nn import functional as F
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data import Dataset, DataLoader, ConcatDataset
from torchvision import transforms, datasets
import matplotlib.pyplot as plt
import copy
import multiprocessing
from sklearn.model_selection import KFold
from sklearn.metrics import accuracy_score
from pathlib import Path
from PIL import Image

函式:load_data

  • 函式輸入:

    引數型別說明
    pathstr資料集根目錄
    modestr模式(取值為"train"或者"validate"或者"test")
  • 函式輸出: 如果模式是"test",那麼只返回檔名列表,如果模式不是"test",那麼返回檔名列表以及對應的標籤

  • 程式碼展示:

#定義讀取資料的方式,這裡只是夾在train資料夾下打好標籤的資料
def load_data(path, mode):
    data_list = []
    label_list = []
    for dirs in os.listdir(path):
        if dirs == "train" and (mode == "train" or mode == "validate"):
            for file in os.listdir(path + dirs):
                #根據檔名,給訓練集合打標籤,狗是1,貓是0
                if "cat" in (file.split('/')[-1]).split('.'):
                    label_list.append(0)
                else:
                    label_list.append(1)
                data_list.append(path + dirs + '/' + file)
            return (data_list, label_list)
        
        elif dirs == "test" and mode == "test":
            #print("load test dataset")
            for file in os.listdir(path + dirs):
                data_list.append(path + dirs + '/' + file)
            return data_list
  • 評價: 這個函式的設計思路是不對資料集進行進一步的操作,比如新建資料夾,把驗證集和訓練集合分開,這樣做的好處是本程式不用對檔案系統裡的圖片進行IO操作,減少了程式執行的時間,此外將本程式移植到其他計算機上時,也可以直接計算。壞處是之後驗證模型準確性的時候還需要建立一個數據集物件,顯得有點低效和冗餘,所以這裡做了一個取捨,最後選擇這樣寫了。

型別:CatsDogsDataset

  • 父類: torch.utils.data.Dataset

  • 初始化引數:

    引數型別說明
    pathstr檔案路徑,取值為字串
    modestr模式,取值為"train"或者"validate"或者"test"
  • 程式碼展示:

class CatsDogsDataset(torch.utils.data.Dataset):
    def __init__(self, path, mode):
        self.mode = mode
        self.path = path
        self.IMAGE_H = 200
        self.IMAGE_W = 200
        self.img_list = []
        self.label_list = []
        self.transform = transforms.Compose([transforms.ToTensor(),
                                             transforms.Resize((self.IMAGE_H,self.IMAGE_W))])
        self.resize = transforms.Compose([transforms.ToTensor(),
                                         transforms.Resize((self.IMAGE_H,self.IMAGE_W))])
        if self.mode == 'train':    
            self.img_list, self.label_list = load_data(self.path, self.mode)
            self.img_list = self.img_list[0:int(len(self.img_list)*0.9)]
            self.label_list = self.label_list[0:int(len(self.label_list)*0.9)]
        elif self.mode == 'validate':
            self.img_list, self.label_list = load_data(self.path, self.mode)
            self.img_list = self.img_list[int(len(self.img_list)*0.9): -1]
            self.label_list = self.label_list[int(len(self.label_list)*0.9): -1]
        elif self.mode == 'test':
            self.img_list = load_data(self.path, self.mode)
        else:
            return print('MODE ERROR!')

    def __getitem__(self, item):
        if self.mode == 'train':
            img = Image.open(self.img_list[item])
            img = self.transform(img)
            label = self.label_list[item]
            return img, torch.LongTensor([label])
        elif self.mode == 'test':
            img = Image.open(self.img_list[item])
            img = np.array(img)[:, :, :3]
            return self.resize(img)
        elif self.mode == 'validate':
            img = Image.open(self.img_list[item])
            img = self.resize(img)
            print(type(img))
            label = self.label_list[item]
            return self.resize(img), torch.LongTensor([label])
        else:
            print("MODE ERROR!")

    def __len__(self):
        if self.mode == "train":
            return len(self.label_list)
        elif self.mode == "validate":
            return len(self.label_list)
  • 成員方法:

    方法引數說明
    _getitem_(index)index:int函式被之後的子類呼叫,根據下表index,以張量的形式,返回預處理過的圖片
    _len_()函式為子類物件返回資料集樣本數量,保證可以正確讀取資料,不會下標越界
  • 評價: 構建這個資料集類目的有兩個,第一個是預處理圖片,因為開啟資料集發現每張圖片的尺寸都是不一樣的,所以需要對原先的資料進行預處理,至少需要把圖片調整成同樣的大小,使得輸入向量的維度是統一的;第二是為之後當成引數傳入用於載入一個batch數量資料的函式torch.utils.data.Dataloader,提供介面,還要保證不會越界。以上兩點都體現在過載的兩個函式上。

型別:BasicModule

  • 父類: torch.nn.Module
class BasicModule(nn.Module):
    def __init__(self):
        super(BasicModule,self).__init__()
        self.model_name=str(type(self))

    def load(self, path):
        self.load_state_dict(t.load(path))

    def save(self, name=None):
        if name is None:
            prefix = 'checkpoints/' + self.model_name + '_'
            name = time.strftime(prefix + '%m%d_%H:%M:%S.pth')
        t.save(self.state_dict(), name)
        return name
  • 評價: 作為之後兩個深度神經網路的基類,定義了載入已經訓練好的神經網路引數的辦法,同樣也定義了匯出神經網路引數的辦法,方便之後進行模型之間的比較。

深度網路

型別:ResNet34

  • 父類: BasicModule
  • 初始化引數: 分類種類數量,預設為2
  • 程式碼展示:
class ResNet34(BasicModule):
    def __init__(self, num_classes=2):
        super(ResNet34, self).__init__()
        self.model_name = 'resnet34'
        self.pre = nn.Sequential(
                nn.Conv2d(3, 64, 7, 2, 3, bias=False),
                nn.BatchNorm2d(64),
                nn.ReLU(inplace=True),
                nn.MaxPool2d(3, 2, 1))
        self.layer1 = self._make_layer( 64, 128, 3)
        self.layer2 = self._make_layer( 128, 256, 4, stride=2)
        self.layer3 = self._make_layer( 256, 512, 6, stride=2)
        self.layer4 = self._make_layer( 512, 512, 3, stride=2)
        self.fc = nn.Linear(512, num_classes)
        
        def initialize_weights():
            for m in self.modules():
                if isinstance(m, nn.Conv2d):
                    torch.nn.init.kaiming_uniform_(m.weight.data) 
                    if m.bias is not None:
                        m.bias.data.zero_()
                elif isinstance(m, nn.BatchNorm2d):
                    m.weight.data.fill_(1)
                    m.bias.data.zero_()
                elif isinstance(m, nn.Linear):
                    torch.nn.init.kaiming_uniform_(m.weight.data, mode='fan_in', nonlinearity='relu')
                    # m.weight.data.normal_(0, 0.01) 
                    m.bias.data.zero_()
        initialize_weights()
    
    def _make_layer(self,  inchannel, outchannel, block_num, stride=1):
        shortcut = nn.Sequential(
                nn.Conv2d(inchannel,outchannel,1,stride, bias=False),
                nn.BatchNorm2d(outchannel))
        
        layers = []
        layers.append(ResidualBlock(inchannel, outchannel, stride, shortcut))
        
        for i in range(1, block_num):
            layers.append(ResidualBlock(outchannel, outchannel))
        return nn.Sequential(*layers)
        
    def forward(self, x):
        x = x.float()
        x = self.pre(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = F.avg_pool2d(x, 7)
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return F.softmax(x,dim=1)
  • 網路結構:

記錄一次失敗的深度學習經歷之Kaggle:貓狗大戰

型別:AlexNet

  • 父類: BasicModule
  • 初始化引數: 分類種類數量,預設為2
  • 程式碼展示:
class AlexNet(BasicModule):
    
    def __init__(self, num_classes=2):
        super(AlexNet, self).__init__()
        self.model_name = 'alexnet'
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(64, 192, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),)
        
        self.classifier = nn.Sequential(
            nn.Dropout(),
            nn.Linear(256 * 5 * 5, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )
        
        def initialize_weights():
            for m in self.modules():
                if isinstance(m, nn.Conv2d):
                    torch.nn.init.xavier_normal_(m.weight.data) 
                    if m.bias is not None:
                        m.bias.data.zero_()
                elif isinstance(m, nn.BatchNorm2d):
                    m.weight.data.fill_(1)
                    m.bias.data.zero_()
                elif isinstance(m, nn.Linear):
                    torch.nn.init.kaiming_uniform_(m.weight.data, mode='fan_in', nonlinearity='relu') 
                    # m.weight.data.normal_(0, 0.01) 
                    m.bias.data.zero_()    
        initialize_weights()

    def forward(self, x):
        x = x.float()
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        return x
  • 網路結構:

記錄一次失敗的深度學習經歷之Kaggle:貓狗大戰

訓練流程

函式:train

  • 函式輸入:

    引數型別說明
    Modeltorch.nn.Module的派生類儲存神經網路結構和引數
    datasettorch.utils.data.Dataset的派生類儲存訓練資料的位置
    val_datasettorch.utils.data.Dataset的派生類儲存驗證資料的位置
    optimizertorch.optim中的類儲存反向傳播演算法及引數
    criteriontorch.nn中的類儲存損失函式
    batchint儲存批處理的規模
    epochsint儲存進行批處理的次數
  • 函式輸出: 含有每個eporch結束之後模型在驗證集上的正確率的列表

  • 程式碼展示:

    def train(model, dataset, val_dataset, optimizer, criterion, batch_size, epochs):
      # 呼叫父類的cuda()方法,用GPU加速運算
      model = model.cuda()
      # 記錄訓練圖片數量
      eporch_acc = []
      num_threads = multiprocessing.cpu_count()
      for epoch in range(epochs):
          cnt = 0
          img_label = list(zip(dataset.img_list,dataset.label_list))
          random.shuffle(img_label)
          dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)
          print("<<<<<<< enter epoch {0} >>>>>>>>>".format(epoch+1))
          for img, label in dataloader:
              # 將資料放置在PyTorch的Variable節點中,並送入GPU中作為網路計算起點
              img, label = Variable(img).cuda(), Variable(label).cuda()
              out = model(img)
              predict = torch.max(out,1)[1].tolist()
              fact = label.tolist()
              loss = criterion(out.squeeze(), label.squeeze())
              # 誤差反向傳播
              loss.backward()
              # 優化採用設定的優化方法對網路中的各個引數進行調整
              optimizer.step()
              optimizer.zero_grad()
              cnt += 1
              # 列印一個batch size的訓練結果
              if cnt%20 == 0:
                  print('Sample {0}, train_loss {1}'.format(cnt*batch_size, loss/batch_size)) 
              del img
              del label
          acc_out = validate(model, val_dataset)
          eporch_acc.append(acc_out)
          print("<<<< enter epoch {0} is over with final result {1} >>>>".format(epoch+1, acc_out))
      torch.save(model.state_dict(), './model.weight')
      return eporch_acc
    
  • 思路: 總體流程是先將Dataset裡儲存的資料位置用dataloader讀進來,形成一個可以迭代的資料型別,然後進行向前傳播得到預測結果,將結果與正確結果進行比較,然後誤差進行向後傳播,每訓練3200張圖片輸出一次損失函式值,檢視一下執行情況,如此迴圈若干次,最後把訓練得到的模型儲存到本地,可供下次呼叫。

結果比較

下面比較AlexNet和ResNet兩個深度神經網路在貓狗二分類問題上的表現,由於算力有限,所以這裡的比較都是基於eporch=10的。優化器選擇Adam,驗證集訓練集為一九開,batch_size=64。第一個版本的訓練器執行結果是下面這樣的,縱座標是準確率,橫座標是eporch的值

記錄一次失敗的深度學習經歷之Kaggle:貓狗大戰

AlexNet比ResNet略好一些,但是總體來說兩個模型的結果都非常不理想,在之後幾個eporch內效能沒有任何提升,我仔細回顧了一下程式碼,覺得問題可能出在沒有將儲存資料位置的列表打亂造成的,因為這樣有可能造成將同樣的資料以同樣的順序反覆讀取然後訓練的情況,於是加入shuffle函式,在進行一次測試,發現結果如下

記錄一次失敗的深度學習經歷之Kaggle:貓狗大戰

結果依然不是很理想,還需要進一步調整。然後我又想到資料集中存在很多不夠典型的樣本點,所以為了進一步增強模型,我又改進了預處理的過程,原先僅僅將圖片縮放成200$\times$200大小,現在在訓練集上加入如隨機高斯模糊、水平翻轉、顏色偏移等操作,驗證集為了模擬測試集,還是隻保留調整大小這一個操作,然後再嘗試一次

記錄一次失敗的深度學習經歷之Kaggle:貓狗大戰

在程式執行的過程中,我發現儘管ResNet的損失函式是在緩慢下降的,但是驗證集上的精確度卻沒有明顯改變,說明可能存在過擬合的現象;AlexNet的情況略好一些,但是也在50%上下,由於算力和時間有限,不能測試更多的eporch了。我還嘗試了在全連結層上進行初始化操作,選擇的初始化函式是Kaiming_uniform_,結果如下

記錄一次失敗的深度學習經歷之Kaggle:貓狗大戰

不清楚什麼原因,ResNet在驗證集上的準確率一直不變,而AlexNet的正確率甚至低於50%。最後我將一起運用預處理和初始化兩個操作,結果如下

記錄一次失敗的深度學習經歷之Kaggle:貓狗大戰

之前展示的程式碼均為最終版本。

經驗總結

  • 深度學習程式的總體執行步驟是先把資料位置讀到一個Dataset的派生類裡,並且在這個類裡過載兩個虛擬函式。然後在train函式裡寫清楚訓練的過程,先後是通過dataloader生成一個可以迭代的物件,以batch_size的規模放進神經網路,然後通過optimizer進行反向傳播以更新引數。在具體構造model時,有兩個函式特別重要:初始化函式__init__裡定義模型如何構造的,從卷積層轉換到全連結層時要輸入正確的維數;forward函式,則是將所有定義的模組按照特定的方式組合起來。model和train函式應該是相互獨立的,更改任何一個都不會對另一個造成影響,否則修改起來很麻煩。

  • 對於大資料集,Pytorch提供的記憶體清除機制不夠智慧,自己對反覆調參幾次視訊記憶體就會溢位,所以解決方案是顯式地呼叫del函式,當圖片處理完畢,結束迴圈的時候,要及時清理記憶體,但是也要注意不能把迴圈外儲存結果的列表也刪了。

  • 權重初始化對於模型也有影響,不同的迭代起點會影響收斂情況,不過感覺影響也不是很大,具體使用哪個初始化方法需要實驗測試才能知道。

  • 影象預處理時用到的torchvision.transforms模組高版本不能向下相容,很多網上的程式碼用的是舊版本,所以一直報錯,最好的辦法是閱讀官方文件,而且還要對應當前的版本,還有就是直接看原始碼,不能迷信網上別人的程式碼,即使他們能跑通,放在自己這裡也不一定可以成功,而且有些變換還有型別轉換的問題,比如Normailize,使用起來尤其麻煩。

  • 儘管使用的兩個模型都在業界非常有名,但是我還是不能夠取得理想的效果,我覺得這些深度學習模型的泛化能力可能有很強的侷限性,如果不給出具體的引數,同樣的模型在同樣的資料集也可能跑出截然不同的效果。