最近剛好修了Pytorch相關的課,在Kaggle上也丟了個比賽,想說就來分享一下Pytorch的入門實戰,我會實作一個最入門的用CNN辨識貓狗的程式,但關於CNN理論的部分我不會提到太多,有興趣就麻煩再去搜尋了~
CNN是什麼?
先簡單介紹一下CNN,CNN的全名是(Convolutional Neural Network),中文是卷積神經網路,是機器學習中的深度學習的一種,也是目前應用於影像辨識非常熱門的一種模型。
資料集準備
我這次使用Kaggle的
貓狗資料集,可以先下載下來,總共有三個檔案分別是訓練集train(包含貓狗各12500張圖片),以及測試集test(12500張未分類的圖片),和sample_submission。由於小弟太窮沒錢買GPU且為了節省時間,我先把貓和狗各挑了100張圖片,並分別放到自建的dog和cat資料夾內。
程式實作
載入需要的Library
import torch
import torch.nn as nn
from torchvision import datasets ,models,transforms
from pathlib import Path
from matplotlib import pyplot as plt
import numpy as np
import torch.nn.functional as F
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
from torch.nn import Linear, ReLU, CrossEntropyLoss, Conv2d, MaxPool2d, Module
from torch.optim import Adam
import pandas as pd
import os
from os import listdir
from tqdm import tqdm_notebook as tqdm
from PIL import Image
準備資料、設定超參數
Path_train填入自己的資料夾路徑,我的train裡面有dog和cat的資料夾,分別有各100張圖,並設定Batch和Learning Rate,transforms函數可以將圖片轉成(224,224)的像素,同時將圖片轉成Pytorch能讀取的tensor格式。
PATH_train="...../train"
TRAIN =Path(PATH_train)
#Batch:每批丟入多少張圖片
batch_size = 8
#Learning Rate:學習率
LR = 0.0001
transforms = transforms.Compose([transforms.Resize((224,224)), transforms.ToTensor()]
切分訓練驗證集
用ImageFolder讀取檔案並套入前面transforms的轉換函數,ImageFolder會把圖片根據資料夾給予label,可以用class_to_idx查詢貓和狗分別對應的label,print出來的結果會像這樣。
{‘cat’: 0, ‘dog’: 1}
注意ImageFolder必須在資料夾內有子資料夾才可使用,所以我先分別把貓和狗的圖放進cat和dog的資料夾。
train_data = datasets.ImageFolder(TRAIN, transform=transforms)
#print(train_data.class_to_idx)
#切分70%當作訓練集、30%當作驗證集
train_size = int(0.7 * len(train_data))
valid_size = len(train_data) - train_size
train_data, valid_data = torch.utils.data.random_split(train_data, [train_size, valid_size])
#Dataloader可以用Batch的方式訓練
train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size,shuffle=True)
valid_loader = torch.utils.data.DataLoader(valid_data, batch_size=batch_size,shuffle=True)
建立CNN的架構
這邊要定義自己的CNN架構,我用最簡單的範例,基本上CNN最主要就是Convolutional和Maxpool兩種層所組成,Relu是激發函數,然後最後要用線性層輸出預測結果,因為貓狗是兩個種類,所以Linear後面的參數就是2,輸出結果如[0.487,0.9527],index為1的狗比較大,代表預測結果為狗。
最後也可以加一層Softmax讓兩者機率加起來為1如[0.7,0.3])
Pytorch最少要定義兩個function,一個是__init__,用來建立你forward需要用到哪些層,另一個是forward,也就是定義路徑要怎麼走,不需另外定義Backward,Pytorch會自動幫你設定Back-propagation的路徑。
至於參數的設定我這邊簡單講一下
self.cnn1=nn.Conv2d(3,16,kernal_size=5,stride=1)
3代表input的channel,因為圖片是RGB所以是3,16代表output的channel,這邊我用了16個hidden node所以為16,kernel_size是5*5的filter。
self.maxpool1 = nn.MaxPool2d(kernel_size=2)
這裡的kernel_size代表2*2的格子取最大的一格,會將8*8縮成4*4。
self.fc = nn.Linear(8 * 50 * 50, 2)
至於線性層為何是(8*50*50,2),根據下面這個公式算出下一層的shape,如原圖是(3,224,224)經過cnn1後,(224–5+1)/(1+1)=110,因此maxpool1的input就變成(16,110,110),而Maxpool1的kernal_size為2,因此output就變成(16,55,55),以此類推最後的Shape就變成(8*50*50,2)。
(weight-kernel+1)/stride+1 無條件進位
class CNN_Model(nn.Module):
#列出需要哪些層
def __init__(self):
super(CNN_Model, self).__init__()
# Convolution 1 , input_shape=(3,224,224)
self.cnn1 = nn.Conv2d(3, 16, kernel_size=5, stride=1)
self.relu1 = nn.ReLU(inplace=True)
# Max pool 1
self.maxpool1 = nn.MaxPool2d(kernel_size=2)
# Convolution 2
self.cnn2 = nn.Conv2d(16,8, kernel_size=11, stride=1)
self.relu2 = nn.ReLU(inplace=True)
# Max pool 2
self.maxpool2 = nn.MaxPool2d(kernel_size=2)
# Fully connected 1 ,#input_shape=(8*50*50)
self.fc = nn.Linear(8 * 50 * 50, 2)
#列出forward的路徑,將init列出的層代入
def forward(self, x):
out = self.cnn1(x)
out = self.relu1(out)
out = self.maxpool1(out)
out = self.cnn2(out)
out = self.relu2(out)
out = self.maxpool2(out)
out = out.view(out.size(0), -1)
out = self.fc(out)
return out
定義訓練過程、計算Loss、Accuracy
這邊先將訓練、驗證模組化,傳入的函數包含model(要使用的模型)、n_epochs(迭代次數)、train_loader、valid_loader(訓練、驗證集)、optimizer(優化器)、Criterion(損失函數)。
1.train_loss和valid loss是算出每個batch的平均loss
2.tqdm可以很好的跟data製作出進度條(如上圖)
3.model.eval()會關閉batchnorm、dropout,雖這範例沒有,但一般都會用到
4.output.data.max用來輸出較大的index如[0.487,0.9527],則輸出1 P
5.Validation階段不需做BP,所以少了幾步
def train(model,n_epochs,train_loader,valid_loader,optimizer,criterion):
train_acc_his,valid_acc_his=[],[]
train_losses_his,valid_losses_his=[],[]
for epoch in range(1, n_epochs+1):
# keep track of training and validation loss
train_loss,valid_loss = 0.0,0.0
train_losses,valid_losses=[],[]
train_correct,val_correct,train_total,val_total=0,0,0,0
train_pred,train_target=torch.zeros(8,1),torch.zeros(8,1)
val_pred,val_target=torch.zeros(8,1),torch.zeros(8,1)
count=0
count2=0
print('running epoch: {}'.format(epoch))
###################
# train the model #
###################
model.train()
for data, target in tqdm(train_loader):
# move tensors to GPU if CUDA is available
if train_on_gpu:
data, target = data.cuda(), target.cuda()
# forward pass: compute predicted outputs by passing inputs to the model
output = model(data)
# calculate the batch loss
loss = criterion(output, target)
#calculate accuracy
pred = output.data.max(dim = 1, keepdim = True)[1]
train_correct += np.sum(np.squeeze(pred.eq(target.data.view_as(pred))).cpu().numpy())
train_total += data.size(0)
# backward pass: compute gradient of the loss with respect to model parameters
loss.backward()
# perform a single optimization step (parameter update)
optimizer.step()
# update training loss
train_losses.append(loss.item()*data.size(0))
# clear the gradients of all optimized variables
optimizer.zero_grad()
if count==0:
train_pred=pred
train_target=target.data.view_as(pred)
count=count+1
else:
train_pred=torch.cat((train_pred,pred), 0)
train_target=torch.cat((train_target,target.data.view_as(pred)), 0)
train_pred=train_pred.cpu().view(-1).numpy().tolist()
train_target=train_target.cpu().view(-1).numpy().tolist()
######################
# validate the model #
######################
model.eval()
for data, target in tqdm(valid_loader):
# move tensors to GPU if CUDA is available
if train_on_gpu:
data, target = data.cuda(), target.cuda()
# forward pass: compute predicted outputs by passing inputs to the model
output = model(data)
# calculate the batch loss
loss =criterion(output, target)
#calculate accuracy
pred = output.data.max(dim = 1, keepdim = True)[1]
val_correct += np.sum(np.squeeze(pred.eq(target.data.view_as(pred))).cpu().numpy())
val_total += data.size(0)
valid_losses.append(loss.item()*data.size(0))
if count2==0:
val_pred=pred
val_target=target.data.view_as(pred)
count2=count+1
else:
val_pred=torch.cat((val_pred,pred), 0)
val_target=torch.cat((val_target,target.data.view_as(pred)), 0)
val_pred=val_pred.cpu().view(-1).numpy().tolist()
val_target=val_target.cpu().view(-1).numpy().tolist()
# calculate average losses
train_loss=np.average(train_losses)
valid_loss=np.average(valid_losses)
# calculate average accuracy
train_acc=train_correct/train_total
valid_acc=val_correct/val_total
train_acc_his.append(train_acc)
valid_acc_his.append(valid_acc)
train_losses_his.append(train_loss)
valid_losses_his.append(valid_loss)
# print training/validation statistics
print('\tTraining Loss: {:.6f} \tValidation Loss: {:.6f}'.format(
train_loss, valid_loss))
print('\tTraining Accuracy: {:.6f} \tValidation Accuracy: {:.6f}'.format(
train_acc, valid_acc))
return train_acc_his,valid_acc_his,train_losses_his,valid_losses_his,model
開始訓練囉~
首先初始化CNN_Model(),使用最常用的Adam作為Optimizer,由於是分類問題Loss Function選用CrossEntropy,代入函數即可以開始訓練囉!!
model1=CNN_Model()
n_epochs = 10
optimizer1 = torch.optim.Adam(model1.parameters(), lr=LR)
criterion = CrossEntropyLoss()
train_acc_his,valid_acc_his,train_losses_his,valid_losses_his,model1=train(model1,n_epochs,train_loader,valid_loader,optimizer1,criterion)
訓練結果、儲存Model
這邊就可以把剛剛訓練完的結果留下來,並畫成loss和accuracy(如上圖),檢驗訓練的狀況,由於訓練非常耗時間,可以用torch.save的函數把訓練好的model保存下來,之後就可以直接load進來用。
由於是做範例示範,所以用了很簡單的架構及非常少的data,所以從上面的圖可發現training和validation差非常遠,有非常嚴重的Overfitting的問題,因此之後可以再去進行調整。
plt.figure(figsize=(15,10))
plt.subplot(221)
plt.plot(train_losses_his, 'bo', label = 'training loss')
plt.plot(valid_losses_his, 'r', label = 'validation loss')
plt.title("Simple CNN Loss")
plt.legend(loc='upper left')
plt.subplot(222)
plt.plot(train_acc_his, 'bo', label = 'trainingaccuracy')
plt.plot(valid_acc_his, 'r', label = 'validation accuracy')
plt.title("Simple CNN Accuracy")
plt.legend(loc='upper left')
plt.show()
torch.save(model1, "....../Dogcat_resnet18")
#model1 = torch.load('..../Dogcat_resnet18')
接下來不斷調整找出最好的參數、架構即可,但注意調整太多次,Validation的效果可能會變差,也容易對Validation Set有Overfitting的問題,而影響模型對Test的泛用性,因此前面data_loader也用了shuffle的函數,把每次的batch洗散,切分資料集也沒使用random_state固定切分的資料。
用自己的CNN參加Kaggle競賽
由於網路上通常到上一步就結束了,但相信不少人也會想知道自己的模型到底好不好,所以我這邊會分享一下如何用自己設計的CNN參加Kaggle競賽。
由於Kaggle給的test集檔案為1.jpg,2.jpg…..因此先將檔案以PIL的格式讀取,在套入transforms的轉換。model的input是batch的方式讀取,因此會有4個維度(8,3,224,224),由於我們是一張一張讀取,所以model只有3個維度(3,224,224),因此在用unsqueeze(0)在0的index上加一個維度即變成(1,3,224,224),就可以預測了。
PS由於Dataloader輸出順序不固定,所以提交部分就另外寫了一個function
def test_submit(model,n_img,path):
model.eval()
pred_label=[]
for i in tqdm(range(1,n_img+1)):
path = image_path + str(i) +'.jpg'
img = Image.open(path).convert('RGB')
img = transforms(img)
img = img.unsqueeze(0)
with torch.no_grad():
output=model(img)
pred = output.data.max(dim = 1, keepdim = True)[1]
pred_label.append(int(pred))
return pred_label
最後把參數帶入,輸出成csv,提交到Kaggle的網站上,大功就告成啦!!
image_path =’.../test/’
n_img=12500
pred_label=test_submit(model1,n_img,image_path)
submit = pd.read_csv('.../sample_submission.csv')
submit['label'] = pred_label
submit['label'] = submit['label'].astype(int)
submit.to_csv('..../submit_dogcat.csv', index= False)
由於小弟我才疏學淺且第一次寫技術分享相關文章,可能有很多錯誤和不周延的地方,再請各位大神指教、糾正了,感謝大家~
若覺得有幫助可以追蹤我、按喜歡、收藏,我就會寫出更多相關文章,謝謝你~
若還有其他想問的或希望我介紹的,可以用FB私訊或在下面回應,我會盡我所能回答
你可能還會想看: