記錄一次失敗的深度學習經歷之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
-
函式輸入:
引數 型別 說明 path str 資料集根目錄 mode str 模式(取值為"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
-
初始化引數:
引數 型別 說明 path str 檔案路徑,取值為字串 mode str 模式,取值為"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)
- 網路結構:

型別: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
- 網路結構:

訓練流程
函式:train
-
函式輸入:
引數 型別 說明 Model torch.nn.Module的派生類 儲存神經網路結構和引數 dataset torch.utils.data.Dataset的派生類 儲存訓練資料的位置 val_dataset torch.utils.data.Dataset的派生類 儲存驗證資料的位置 optimizer torch.optim中的類 儲存反向傳播演算法及引數 criterion torch.nn中的類 儲存損失函式 batch int 儲存批處理的規模 epochs int 儲存進行批處理的次數 -
函式輸出: 含有每個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的值

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

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

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

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

之前展示的程式碼均為最終版本。
經驗總結
-
深度學習程式的總體執行步驟是先把資料位置讀到一個Dataset的派生類裡,並且在這個類裡過載兩個虛擬函式。然後在train函式裡寫清楚訓練的過程,先後是通過dataloader生成一個可以迭代的物件,以batch_size的規模放進神經網路,然後通過optimizer進行反向傳播以更新引數。在具體構造model時,有兩個函式特別重要:初始化函式__init__裡定義模型如何構造的,從卷積層轉換到全連結層時要輸入正確的維數;forward函式,則是將所有定義的模組按照特定的方式組合起來。model和train函式應該是相互獨立的,更改任何一個都不會對另一個造成影響,否則修改起來很麻煩。
-
對於大資料集,Pytorch提供的記憶體清除機制不夠智慧,自己對反覆調參幾次視訊記憶體就會溢位,所以解決方案是顯式地呼叫del函式,當圖片處理完畢,結束迴圈的時候,要及時清理記憶體,但是也要注意不能把迴圈外儲存結果的列表也刪了。
-
權重初始化對於模型也有影響,不同的迭代起點會影響收斂情況,不過感覺影響也不是很大,具體使用哪個初始化方法需要實驗測試才能知道。
-
影象預處理時用到的torchvision.transforms模組高版本不能向下相容,很多網上的程式碼用的是舊版本,所以一直報錯,最好的辦法是閱讀官方文件,而且還要對應當前的版本,還有就是直接看原始碼,不能迷信網上別人的程式碼,即使他們能跑通,放在自己這裡也不一定可以成功,而且有些變換還有型別轉換的問題,比如Normailize,使用起來尤其麻煩。
-
儘管使用的兩個模型都在業界非常有名,但是我還是不能夠取得理想的效果,我覺得這些深度學習模型的泛化能力可能有很強的侷限性,如果不給出具體的引數,同樣的模型在同樣的資料集也可能跑出截然不同的效果。
