我的實踐:通過螞蟻、蜜蜂二分類問題了解如何基于Pytorch構建分類模型

1.數據集準備

本例采用了pytorch教程提供的蜜蜂、螞蟻二分類數據集(點擊可直接下載)。該數據集的文件夾結構如下圖所示。這里面有些黑白的照片,我把它們刪掉了,因為黑白照片的通道數是1,會造成Tensor的維度不一致。可以看出數據集分為訓練集和測試集,訓練集用于訓練模型,測試集用于測試模型的泛化能力。在訓練集和測試集下又包含了"ants"和"bees"兩個文件夾,這兩個文件夾的名稱即圖片的標簽,在加載數據的時候需要用到這一點。有了數據,我們就想辦法把這些數據處理成pytorch框架下的Dataset需要的格式。

請添加圖片描述

2.pytorch Dataset 處理圖片數據

pytorch為我們處理數據提供了一個模板,這個模板就是Dataset,我們在處理數據時繼承這個類。在處理數據時要注意以下幾點:

  1. 可以用PIL的Image加載圖片,但要將圖片處理成tensor,而且tensor的維度要一致。這是因為nn模型的輸入都是tensor格式,而且要求一個batchsize的tensor維度是一樣的。實現上述可能可以使用torchvision的transforms。由于我用的CPU訓練模型,所以對圖片壓縮的比較厲害,全壓縮成33232的圖片了。
  2. "ants"和"bees"兩個文件夾的名稱就是圖片的標簽,但是getitem的返回值應該是一個值。在這里"ants"標簽返回0,"bees"標簽返回1。
  3. 看數據的預處理對不對,可以用一段代碼測試一下,將數據加載到DataLoader,然后循環取出數據,并把這些數據及其標簽打印出來,或者記錄到tensorboard上去,看每一次迭代返回的數據是否和自己預想的一樣。

下面是代碼,保存在dataProcess.py文件中。

rom torch.utils.data import Dataset
from torch.utils.data import DataLoader
from PIL import Image
import os
from torchvision import transforms
from torch.utils.tensorboard import SummaryWriter

class MyData(Dataset):
    # 把圖片所在的文件夾路徑分成兩個部分,一部分是根目錄,一部分是標簽目錄,這是因為標簽目錄的名稱我們需要用到
    def __init__(self, root_dir, label_dir):
        self.root_dir = root_dir
        self.label_dir = label_dir
        # 圖片所在的文件夾路徑由根目錄和標簽目錄組成
        self.path = os.path.join(self.root_dir, self.label_dir)
        # 獲取文件夾下所有圖片的名稱
        self.img_names = os.listdir(self.path)

    def __getitem__(self, idx):
        img_name = self.img_names[idx]
        img_item_path = os.path.join(self.root_dir, self.label_dir, img_name)
        img = Image.open(img_item_path)
        # 將圖片處理成Tensor格式,并將維度設置成32*32的
        # 圖片的維度可能不一致,這里一定要用resize統一一下,否則會出錯
        trans = transforms.Compose(
            [
                transforms.ToTensor(),
                transforms.Resize((32, 32))
            ])
        img_tensor = trans(img)
        # 根據標簽目錄的名稱來確定圖片是哪一類,如果是"ants",標簽設置為0,如果是"bees",標簽設置為1
        # 這個地方要注意,我們在計算loss的時候用交叉熵nn.CrossEntropyLoss()
        # 交叉熵的輸入有兩個,一個是模型的輸出outputs,一個是標簽targets,注意targets是一維tensor
        # 例如batchsize如果是2,ants的targets的應該[0,0],而不是[[0][0]]
        # 因此label要返回0,而不是[0]
        label = 0 if self.label_dir == "ants" else 1
        return img_tensor,  label

    def __len__(self):
        return len(self.img_names)

# 用下面這段代碼測試一下加載數據有沒有問題
if __name__ == "__main__":
    # 注意hymenoptera_data和代碼在同一級目錄
    root_dir = "hymenoptera_data/train"
    ants_label = "ants"
    bees_label = "bees"
    # 螞蟻數據集
    ants_dataset = MyData(root_dir, ants_label)
    # 蜜蜂數據集
    bees_dataset = MyData(root_dir, bees_label)
    # 螞蟻數據集和蜜蜂數據集合并
    train_dataset = ants_dataset + bees_dataset
    # 利用dataLoader加載數據集
    train_dataloader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
    # tensorboard的writer
    writer = SummaryWriter("logs")
    for step, train_data in enumerate(train_dataloader):
        imgs, targets = train_data
        # 每迭代一次就把一個batch的圖片記錄到tensorboard
        writer.add_images("test", imgs, step)
        # 每迭代一次就把一個batch的圖片標簽打印出來
        print(targets)
    writer.close()

在測試時tensorboard記錄的信息在logs文件夾,在terminal輸入tensorboard --logdir=logs啟動tensorboard,將tensorboard給出的網址輸入到網頁,可以看到每一個batch的圖片。下圖展示了第一個batch的圖片。可以看到,取出了64張圖片,和batchsize=64是對應的。另外可以看到,把圖片壓縮成32*32后,確實很模糊了,人眼都很難看出哪個是螞蟻,哪個是蜜蜂。


請添加圖片描述

下面這個圖展示了第一個batch所有圖片的標簽,0表示螞蟻,1表示蜜蜂,仔細看一下圖片和標簽應該是對應的。


請添加圖片描述

3.網絡模型設計

我們把圖片處理成3*32*32的tensor了,用如下圖所示的卷積神經網絡模型。第一層卷積網絡采用5*5的卷積核,stride=1,pading=2。第一層卷積的代碼是:nn.Conv2d(3, 32, 5, 1, 2),第一個參數3是輸入的通道數,第二個參數32是輸出的通道數,第三個參數5是卷積核的大小,第四個參數1是stride,第五個參數2是padding。


卷積網絡模型.png

輸出高H,和寬度W計算公式如下所示(注意dilation默認為0)。

H_{out} = \left\lfloor\frac{H_{in} + 2 \times \text{padding}[0] - \text{dilation}[0] \times (\text{kernel\_size}[0] - 1) - 1}{\text{stride}[0]} + 1\right\rfloor
W_{out} = \left\lfloor\frac{W_{in} + 2 \times \text{padding}[1] - \text{dilation}[1] \times (\text{kernel\_size}[1] - 1) - 1}{\text{stride}[1]} + 1\right\rfloor
因此,通過第一層卷積后,高度H為,
H_{out}=\frac{32+2 \times 2 -1\times(5-1)-1}{1}+1=32
同理寬度W也為32。所以輸出的大小就32*32*32。接下來,再用一個max-Pooling進行一次池化,池化核的大小是2*2。該池化層的代碼是nn.MaxPool2d(2)。池化輸出高H,和寬度W計算公式和卷積計算方式一摸一樣。在默認的情況下,stride和池化和的大小一樣,pading=0,dilation=0。所以第一次池化后,輸出的高度H為,
H_{out}=\frac{32+2 \times 0 -1\times(2-1)-1}{2}+1=16
同理,輸出的寬度H為16。因此,輸出的維度是32*16*16。
后面的輸出維度計算方式同上,不再羅嗦了。然后再通過兩次卷積和兩次池化,后面的輸出維度計算方式同上,不再羅嗦了,最終得到一個維度為64*4*4的特征。在做分類之前,首先要把這個三維Tensor拉直成一維Tensor,代碼是nn.Flatten()。拉直之后的一維Tensor大小就是64\times4\times4=1024。最后通過一個全連接層完成分類任務,全連接層的輸入大小是1024,輸出的大小是類別的個數,即2,代碼是nn.Linear(64 * 4 * 4, 2)。

當完成所有模型的構建后,可以用一段代碼來測試一下模型是否有誤。例如這里模型的輸入在[3,32,32]Tensor的基礎上,還需要再增加一維batchsize,所以輸入的維度應該是[batchsize,3,32,32]。我們可以生成一個這樣維度的數據,例如假設batchsize=3,可以這樣生成一個輸入:x = torch.ones((3, 3, 32, 32))。然后把x送給模型,看模型是否能正常輸出,輸出的維度是否是我們預期的。我們還可以借助于Tensorboard來將模型可視化,通過界面把模型展開,看是否正確。
下面是所有的代碼,保存在model.py文件中。

from torch import nn
import torch
from torch.utils.tensorboard import SummaryWriter

class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.model = nn.Sequential(
            nn.Conv2d(3, 32, 5, 1, 2),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 32, 5, 1, 2),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 5, 1, 2),
            nn.MaxPool2d(2),
            nn.Flatten(),
            nn.Linear(64 * 4 * 4, 2)
        )

    def forward(self, x):
        x = self.model(x)
        return x

# 這段代碼測試model是否正確
if __name__ == "__main__":
    my_model = MyModel()
    x = torch.ones((3, 3, 32, 32))
    y = my_model(x)
    print(y.shape)
    # 利用tensorboard可視化模型
    writer = SummaryWriter("graph_logs")
    writer.add_graph(my_model, x)
    writer.close()

模型測試代碼打印的輸出維度是[3,2],3是batchsize,2是全連接層最后的輸出維度,和類別的個數是一致的。利用Tensorboard將模型可視化后,如下圖所示,還可以進一步展開。


請添加圖片描述

4.模型的訓練與測試

模型的訓練與測試就不細講了,和其他模型訓練的套路一樣的,基本思路可以看我的第一篇[pytorch入門文章](我的實踐:通過一個簡單線性回歸入門pytorch - 簡書 (jianshu.com)
)。下面直接給出代碼。

from model import *
from dataProcess import *
import matplotlib.pyplot as plt
import time

# 加載訓練數據
train_root_dir = "hymenoptera_data/train"
train_ants_label = "ants"
train_bees_label = "bees"
train_ants_dataset = MyData(train_root_dir, train_ants_label)
train_bees_dataset = MyData(train_root_dir, train_bees_label)
train_dataset = train_ants_dataset + train_bees_dataset
train_data_loader = DataLoader(dataset=train_dataset, batch_size=128, shuffle=True)
train_data_len = len(train_dataset)
# 加載測試數據
test_root_dir = "hymenoptera_data/val"
test_ants_label = "ants"
test_bees_label = "bees"
test_ants_dataset = MyData(test_root_dir, test_ants_label)
test_bees_dataset = MyData(test_root_dir, test_bees_label)
test_dataset = test_ants_dataset + test_bees_dataset
test_data_loader = DataLoader(dataset=test_dataset, batch_size=256, shuffle=True)
test_data_len = len(test_dataset)
print(f"訓練集長度:{train_data_len}")
print(f"測試集長度:{test_data_len}")
# 創建網絡模型
my_model = MyModel()

# 損失函數
loss_fn = nn.CrossEntropyLoss()

# 優化器
learning_rate = 5e-3
optimizer = torch.optim.SGD(my_model.parameters(), lr=learning_rate)
# Adam 參數betas=(0.9, 0.99)
# optimizer = torch.optim.Adam(my_model.parameters(), lr=learning_rate, betas=(0.9, 0.99))
# 總共的訓練步數
total_train_step = 0
# 總共的測試步數
total_test_step = 0
step = 0
epoch = 500

writer = SummaryWriter("logs")
train_loss_his = []
train_totalaccuracy_his = []
test_totalloss_his = []
test_totalaccuracy_his = []
start_time = time.time()
my_model.train()
for i in range(epoch):
    print(f"-------第{i}輪訓練開始-------")
    train_total_accuracy = 0
    for data in train_data_loader:
        imgs, targets = data
        writer.add_images("tarin_data", imgs, total_train_step)
        output = my_model(imgs)
        loss = loss_fn(output, targets)
        train_accuracy = (output.argmax(1) == targets).sum()
        train_total_accuracy = train_total_accuracy + train_accuracy
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_train_step = total_train_step + 1
        train_loss_his.append(loss)
        writer.add_scalar("train_loss", loss.item(), total_train_step)
    train_total_accuracy = train_total_accuracy / train_data_len
    print(f"訓練集上的準確率:{train_total_accuracy}")
    train_totalaccuracy_his.append(train_total_accuracy)
    # 測試開始
    total_test_loss = 0
    my_model.eval()
    test_total_accuracy = 0
    with torch.no_grad():
        for data in test_data_loader:
            imgs, targets = data
            output = my_model(imgs)
            loss = loss_fn(output, targets)
            total_test_loss = total_test_loss + loss
            test_accuracy = (output.argmax(1) == targets).sum()
            test_total_accuracy = test_total_accuracy + test_accuracy
        test_total_accuracy = test_total_accuracy / test_data_len
        print(f"測試集上的準確率:{test_total_accuracy}")
        print(f"測試集上的loss:{total_test_loss}")
        test_totalloss_his.append(total_test_loss)
        test_totalaccuracy_his.append(test_total_accuracy)
        writer.add_scalar("test_loss", total_test_loss.item(), i)
end_time = time.time()
total_train_time = end_time-start_time
print(f'訓練時間: {total_train_time}秒')
writer.close()
plt.plot(train_loss_his, label='Train Loss')
plt.legend(loc='best')
plt.xlabel('Steps')
plt.show()
plt.plot(test_totalloss_his, label='Test Loss')
plt.legend(loc='best')
plt.xlabel('Steps')
plt.show()

plt.plot(train_totalaccuracy_his, label='Train accuracy')
plt.plot(test_totalaccuracy_his, label='Test accuracy')
plt.legend(loc='best')
plt.xlabel('Steps')
plt.show()

通過上述代碼,訓練得到的結果如下圖所示,


請添加圖片描述

結果雖然不是很好,但是我覺得已經很不多了,在測試集上的準確率差不多達到0.7了。為了節省計算資源,我把圖片壓縮成32*32,連我們人眼都很難分辨出哪個是螞蟻,哪個是蜜蜂。另外,我這個模型是完全從0開始訓練的,隔壁在預訓練模型的基礎上進行訓練得到的效果好像沒好多少。。。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371