1.數據集準備
本例采用了pytorch教程提供的蜜蜂、螞蟻二分類數據集(點擊可直接下載)。該數據集的文件夾結構如下圖所示。這里面有些黑白的照片,我把它們刪掉了,因為黑白照片的通道數是1,會造成Tensor的維度不一致。可以看出數據集分為訓練集和測試集,訓練集用于訓練模型,測試集用于測試模型的泛化能力。在訓練集和測試集下又包含了"ants"和"bees"兩個文件夾,這兩個文件夾的名稱即圖片的標簽,在加載數據的時候需要用到這一點。有了數據,我們就想辦法把這些數據處理成pytorch框架下的Dataset需要的格式。
2.pytorch Dataset 處理圖片數據
pytorch為我們處理數據提供了一個模板,這個模板就是Dataset,我們在處理數據時繼承這個類。在處理數據時要注意以下幾點:
- 可以用PIL的Image加載圖片,但要將圖片處理成tensor,而且tensor的維度要一致。這是因為nn模型的輸入都是tensor格式,而且要求一個batchsize的tensor維度是一樣的。實現上述可能可以使用torchvision的transforms。由于我用的CPU訓練模型,所以對圖片壓縮的比較厲害,全壓縮成33232的圖片了。
- "ants"和"bees"兩個文件夾的名稱就是圖片的標簽,但是getitem的返回值應該是一個值。在這里"ants"標簽返回0,"bees"標簽返回1。
- 看數據的預處理對不對,可以用一段代碼測試一下,將數據加載到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。
輸出高H,和寬度W計算公式如下所示(注意dilation默認為0)。
因此,通過第一層卷積后,高度H為,
同理寬度W也為32。所以輸出的大小就32*32*32。接下來,再用一個max-Pooling進行一次池化,池化核的大小是2*2。該池化層的代碼是nn.MaxPool2d(2)。池化輸出高H,和寬度W計算公式和卷積計算方式一摸一樣。在默認的情況下,stride和池化和的大小一樣,pading=0,dilation=0。所以第一次池化后,輸出的高度H為,
同理,輸出的寬度H為16。因此,輸出的維度是32*16*16。
后面的輸出維度計算方式同上,不再羅嗦了。然后再通過兩次卷積和兩次池化,后面的輸出維度計算方式同上,不再羅嗦了,最終得到一個維度為64*4*4的特征。在做分類之前,首先要把這個三維Tensor拉直成一維Tensor,代碼是nn.Flatten()。拉直之后的一維Tensor大小就是。最后通過一個全連接層完成分類任務,全連接層的輸入大小是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開始訓練的,隔壁在預訓練模型的基礎上進行訓練得到的效果好像沒好多少。。。