2019年5月12日 星期日

PyTorch - 練習 kaggle - Dogs vs. Cats - 使用 Transfer Learning - Fine tune 預訓練模型

PyTorch - 練習 kaggle - Dogs vs. Cats - 使用 Transfer Learning - Fine tune 預訓練模型

在PyTorch 中使用較常見的預訓練模型也非常方便,現在 AlexNet, VGG, ResNet, Inception v3…etc. 都可以直接從 TORCHVISION.MODELS 中直接套用下載預訓練好的權重,然後參考先前練習 Keras 使用預訓練模型的文章
只需要在原本的代碼中,將建構模型的地方稍作修改,即可丟入訓練,比自己建構 CNN 還要簡潔方便。
修改 model 代碼:

建構模型 - 使用VGG16

vgg16 = models.vgg16(pretrained=True) vgg16
VGG(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace)
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU(inplace)
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU(inplace)
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU(inplace)
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace)
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU(inplace)
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU(inplace)
    (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU(inplace)
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU(inplace)
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU(inplace)
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): ReLU(inplace)
    (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (27): ReLU(inplace)
    (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (29): ReLU(inplace)
    (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
  (classifier): Sequential(
    (0): Linear(in_features=25088, out_features=4096, bias=True)
    (1): ReLU(inplace)
    (2): Dropout(p=0.5)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU(inplace)
    (5): Dropout(p=0.5)
    (6): Linear(in_features=4096, out_features=1000, bias=True)
  )
)
可以看到最後一層,(6): Linear(in_features=4096, out_features=1000, bias=True),out_features=1000 ,因為是在 ImageNet 1000 分類比賽所使用的模型,所以最後一層 output 是 1000。
我們要凍結預訓練好的卷積層,然後修改 classifier layer。
# Freeze parameters so we don't backprop through them for param in vgg16.parameters(): param.requires_grad = False
fine tune 最後一層分類數目從 1000 修改成 2。
#fine tune the last classifier layer from 1000 to 2 dim(num. of classifier). vgg16.classifier[6] = nn.Linear(4096,2) vgg16 = vgg16.cuda() #use GPU
將 VGG16 model 打印出來:
from torchsummary import summary summary(vgg16.cuda(), (3, 224, 224))
選擇優化器 & loss function
optimizer = torch.optim.Adam(vgg16.parameters(), lr=LR) # optimize all cnn parameters criterion = nn.CrossEntropyLoss() # the target label is not one-hotted

訓練模型

接著就可以開始訓練,因為是使用預訓練過的模型,這邊只訓練 3 個 epochs。
if train_on_gpu: model.cuda() # number of epochs to train the model n_epochs = 3 valid_loss_min = np.Inf # track change in validation loss #train_losses,valid_losses=[],[] for epoch in range(1, n_epochs+1): # keep track of training and validation loss train_loss = 0.0 valid_loss = 0.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() # clear the gradients of all optimized variables optimizer.zero_grad() # forward pass: compute predicted outputs by passing inputs to the model output = model(data) # calculate the batch loss loss = criterion(output, target) # 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_loss += loss.item()*data.size(0) ###################### # 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) # update average validation loss valid_loss += loss.item()*data.size(0) # calculate average losses #train_losses.append(train_loss/len(train_loader.dataset)) #valid_losses.append(valid_loss.item()/len(valid_loader.dataset) train_loss = train_loss/len(train_loader.dataset) valid_loss = valid_loss/len(valid_loader.dataset) # print training/validation statistics print('\tTraining Loss: {:.6f} \tValidation Loss: {:.6f}'.format( train_loss, valid_loss)) # save model if validation loss has decreased if valid_loss <= valid_loss_min: print('Validation loss decreased ({:.6f} --> {:.6f}). Saving model ...'.format( valid_loss_min, valid_loss)) torch.save(model.state_dict(), 'VGG16.pth') valid_loss_min = valid_loss

評估模型

def test(loaders, model, criterion, use_cuda): # monitor test loss and accuracy test_loss = 0. correct = 0. total = 0. model.eval() for batch_idx, (data, target) in enumerate(loaders): # move to GPU if use_cuda: data, target = data.cuda(), target.cuda() # forward pass: compute predicted outputs by passing inputs to the model output = model(data) # calculate the loss loss = criterion(output, target) # update average test loss test_loss = test_loss + ((1 / (batch_idx + 1)) * (loss.data - test_loss)) # convert output probabilities to predicted class pred = output.data.max(1, keepdim=True)[1] # compare predictions to true label correct += np.sum(np.squeeze(pred.eq(target.data.view_as(pred))).cpu().numpy()) total += data.size(0) print('Test Loss: {:.6f}'.format(test_loss)) print('Test Accuracy: %2d%% (%2d/%2d)' % ( 100. * correct / total, correct, total))
use_cuda = torch.cuda.is_available() model.cuda() test(test_loader, model, criterion, use_cuda)
Test Loss: 0.036374
Test Accuracy: 98% (987/1000)
準確度高達98%,相當驚人的結果…

完整代碼請至 my_GitHub 下載

2019年5月11日 星期六

PyTorch - 練習 kaggle - Dogs vs. Cats - 使用自定義的 CNN model


PyTorch - 練習kaggle - Dogs vs. Cats - 使用自定義的 CNN model

先前已經使用 Keras 練習過貓狗辨識的 model ,見先前筆記 ,從最簡單的Linear NN,到CNN,到Transfer learning(fine tune) ,經過此題目的練習,會更了解到深度學習的過程。我們也同樣藉由此題目的練習,來更了解 PyTorch 在圖像分類辨識 model 的使用。
練習目標: (將會是一系列文章,本篇為第一篇)
  1. 先自定義CNN model train 一次看看,看能否成功跑起來!!
  2. 使用 transfer learning 的方法來 train ,看效能是否提高。
  3. 使用 transfer learning 加上 regularization & 一些 deep learning 技法(如 batch-normolize, residual…等) 看看效能是否提升。
以下代碼開始 CNN model練習:

資料預處理

訓練&驗證data,參考 kernal-data 請至連結下載 data,下載後放進根目錄中。
測試 data 為 原始 data,請至連結下載。
會用到的套件:
import torch import torch.nn as nn from torchvision import datasets ,models,transforms from torch.utils.data.sampler import SubsetRandomSampler from torch.optim import lr_scheduler from pathlib import Path from matplotlib import pyplot as plt import numpy as np import torch.nn.functional as F from torch.autograd import Variable from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt
在後續能使用 GPU 就使用 GPU 來訓練模型,省時間!
train_on_gpu = torch.cuda.is_available() if not train_on_gpu: print('CUDA is not available. Training on CPU ...') else: print('CUDA is available! Training on GPU ...')
給定以下路徑,將訓練&驗證&測試資料夾路經標示。
為了節省訓練時間,我們只取 train set 2000個data(貓狗各1000),validation set 1000個data(貓狗各500),test set 1000個data(貓狗各500)。
PATH_train="..\\cats_and_dogs\\train" PATH_val="..\\cats_and_dogs\\validation" PATH_test="..\\cats_and_dogs\\test"
TRAIN =Path(PATH_train) VALID = Path(PATH_val) TEST=Path(PATH_test) print(TRAIN) print(VALID) print(TEST)
..\cats_and_dogs_small\train
..\cats_and_dogs_small\validation
..\cats_and_dogs_small\test
設定一些參數 torch.utils.data.DataLoader 在讀取批量資料時,需要設定些參數,這邊train,val,test 都會用到,所以寫在一起。
# number of subprocesses to use for data loading num_workers = 0 # how many samples per batch to load batch_size = 32 # learning rate LR = 0.01
在 torchvision 裏頭,有個API TORCHVISION.TRANSFORMS ,這個 API 中包含resize、crop 等常見的 data augmentation 操作,基本上 PyTorch 中的 data augmentation 操作都可以透過該API來實現,如本次練習會用到如下:
將圖檔resize至(224,224)像素,然後轉換成tensor的資料型態,最後做narmalize(其中narmalize的係數是參考文檔中的example,也可以使用
transforms.Normalize(mean=[0.5, 0.5, 0.5],std=[0.5, 0.5, 0.5])
# convert data to a normalized torch.FloatTensor train_transforms = transforms.Compose([transforms.Resize((224,224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) valid_transforms = transforms.Compose([transforms.Resize((224,224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) test_transforms = transforms.Compose([transforms.Resize((224,224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
接著利用 pytorch Dataset 的 ImageFolder 將訓練集、驗證集、測試集打包,其使用方式是假設所有的文件按文件夾保存好,每個文件夾下面存放同一類別的圖片,文件夾的名字為分類的名字。如下: 其詳細用法參考 PyTorch 文檔
root/dog/xxx.png
root/dog/xxy.png
root/dog/xxz.png

root/cat/123.png
root/cat/nsdf3.png
root/cat/asd932_.png
# choose the training and test datasets train_data = datasets.ImageFolder(TRAIN, transform=train_transforms) valid_data = datasets.ImageFolder(VALID,transform=valid_transforms) test_data = datasets.ImageFolder(PATH1, transform=test_transforms)
其對應文件夾的label
print(train_data.class_to_idx) print(valid_data.class_to_idx)
{'cats': 0, 'dogs': 1}
{'cats': 0, 'dogs': 1}
接著就是利用 torch.utils.data.DataLoader 數據加載器,將打包好的數據可以跌代進模型中訓練。
# prepare data loaders (combine dataset and sampler) train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, num_workers=num_workers,shuffle=True) valid_loader = torch.utils.data.DataLoader(valid_data, batch_size=batch_size, num_workers=num_workers,shuffle=True) test_loader = torch.utils.data.DataLoader(test_data, batch_size=batch_size, num_workers=num_workers)
images,labels=next(iter(train_loader)) images.shape,labels.shape
(torch.Size([32, 3, 224, 224]), torch.Size([32]))
將 train set 的 20 張圖畫出來看看。因為 train_loader 中的資料已經是normalize過的,所以在畫出來前要先 denormalize。
import matplotlib.pyplot as plt %matplotlib inline classes = ['cat','dog'] mean , std = torch.tensor([0.485, 0.456, 0.406]),torch.tensor([0.229, 0.224, 0.225]) def denormalize(image): image = transforms.Normalize(-mean/std,1/std)(image) #denormalize image = image.permute(1,2,0) #Changing from 3x224x224 to 224x224x3 image = torch.clamp(image,0,1) return image # helper function to un-normalize and display an image def imshow(img): img = denormalize(img) plt.imshow(img)
將前20張圖畫出來,用 subplot 畫比較不占版面。
# obtain one batch of training images dataiter = iter(train_loader) images, labels = dataiter.next() # convert images to numpy for display # plot the images in the batch, along with the corresponding labels fig = plt.figure(figsize=(25, 8)) # display 20 images for idx in np.arange(20): ax = fig.add_subplot(2, 20/2, idx+1, xticks=[], yticks=[]) imshow(images[idx]) ax.set_title("{} ".format( classes[labels[idx]]))
看起來 train_loader 已經沒有問題了。

建立模型

這邊主要練習自己建構 CNN model 看看自己建的 model train 不 train 的起來。
model 後面註解每層 input 與 output_shape 的變化。
# Create CNN Model class CNN_Model(nn.Module): def __init__(self): super(CNN_Model, self).__init__() # Convolution 1 , input_shape=(3,224,224) self.cnn1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5, stride=1, padding=0) #output_shape=(16,220,220) #(224-5+1)/1 #(weigh-kernel+1)/stride 無條件進位 self.relu1 = nn.ReLU() # activation # Max pool 1 self.maxpool1 = nn.MaxPool2d(kernel_size=2) #output_shape=(16,110,110) #(220/2) # Convolution 2 self.cnn2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=0) #output_shape=(32,106,106) self.relu2 = nn.ReLU() # activation # Max pool 2 self.maxpool2 = nn.MaxPool2d(kernel_size=2) #output_shape=(32,53,53) # Convolution 3 self.cnn3 = nn.Conv2d(in_channels=32, out_channels=16, kernel_size=3, stride=1, padding=0) #output_shape=(16,51,51) self.relu3 = nn.ReLU() # activation # Max pool 3 self.maxpool3 = nn.MaxPool2d(kernel_size=2) #output_shape=(16,25,25) # Convolution 4 self.cnn4 = nn.Conv2d(in_channels=16, out_channels=8, kernel_size=3, stride=1, padding=0) #output_shape=(8,23,23) self.relu4 = nn.ReLU() # activation # Max pool 4 self.maxpool4 = nn.MaxPool2d(kernel_size=2) #output_shape=(8,11,11) # Fully connected 1 ,#input_shape=(8*12*12) self.fc1 = nn.Linear(8 * 11 * 11, 512) self.relu5 = nn.ReLU() # activation self.fc2 = nn.Linear(512, 2) self.output = nn.Softmax(dim=1) def forward(self, x): out = self.cnn1(x) # Convolution 1 out = self.relu1(out) out = self.maxpool1(out)# Max pool 1 out = self.cnn2(out) # Convolution 2 out = self.relu2(out) out = self.maxpool2(out) # Max pool 2 out = self.cnn3(out) # Convolution 3 out = self.relu3(out) out = self.maxpool3(out) # Max pool 3 out = self.cnn4(out) # Convolution 4 out = self.relu4(out) out = self.maxpool4(out) # Max pool 4 out = out.view(out.size(0), -1) # last CNN faltten con. Linear NN out = self.fc1(out) # Linear function (readout) out = self.fc2(out) out = self.output(out) return out
接著利用上一篇 model summary 的 function 印出自己建構的 model 看看。
model = CNN_Model() from torchsummary import summary summary(model.cuda(), (3, 224, 224))

訓練模型

在 Keras 中訓練模型都會有進度條,PyTorch 中沒有進度條,可以使用 tqdm 來達到相同的目的。
from tqdm import tqdm_notebook as tqdm
訓練過程與 MNIST 上練習的一樣。
if train_on_gpu: model.cuda() # number of epochs to train the model n_epochs = 50 valid_loss_min = np.Inf # track change in validation loss #train_losses,valid_losses=[],[] for epoch in range(1, n_epochs+1): # keep track of training and validation loss train_loss = 0.0 valid_loss = 0.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() # clear the gradients of all optimized variables optimizer.zero_grad() # forward pass: compute predicted outputs by passing inputs to the model output = model(data) # calculate the batch loss loss = criterion(output, target) # 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_loss += loss.item()*data.size(0) ###################### # 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) # update average validation loss valid_loss += loss.item()*data.size(0) # calculate average losses #train_losses.append(train_loss/len(train_loader.dataset)) #valid_losses.append(valid_loss.item()/len(valid_loader.dataset) train_loss = train_loss/len(train_loader.dataset) valid_loss = valid_loss/len(valid_loader.dataset) # print training/validation statistics print('\tTraining Loss: {:.6f} \tValidation Loss: {:.6f}'.format( train_loss, valid_loss)) # save model if validation loss has decreased if valid_loss <= valid_loss_min: print('Validation loss decreased ({:.6f} --> {:.6f}). Saving model ...'.format( valid_loss_min, valid_loss)) torch.save(model.state_dict(), 'model_CNN.pth') valid_loss_min = valid_loss
最後一 run 的讀條:

評估&測試模型

訓練結束後,要來測試模型效能,就要在 test data 上評估。
def test(loaders, model, criterion, use_cuda): # monitor test loss and accuracy test_loss = 0. correct = 0. total = 0. model.eval() for batch_idx, (data, target) in enumerate(loaders): # move to GPU if use_cuda: data, target = data.cuda(), target.cuda() # forward pass: compute predicted outputs by passing inputs to the model output = model(data) # calculate the loss loss = criterion(output, target) # update average test loss test_loss = test_loss + ((1 / (batch_idx + 1)) * (loss.data - test_loss)) # convert output probabilities to predicted class pred = output.data.max(1, keepdim=True)[1] # compare predictions to true label correct += np.sum(np.squeeze(pred.eq(target.data.view_as(pred))).cpu().numpy()) total += data.size(0) print('Test Loss: {:.6f}'.format(test_loss)) print('Test Accuracy: %2d%% (%2d/%2d)' % ( 100. * correct / total, correct, total))
use_cuda = torch.cuda.is_available() model.cuda() test(test_loader, model, criterion, use_cuda)
Test Loss: 0.701602
Test Accuracy: 60% (608/1000)
自己建構的 CNN model 跌代訓練 50 次後的 Accuracy 約 60%。其實跟 Keras 不做任何處理的 model差不多。

完整代碼請至 my_GitHub 下載