# DLP Homework 4 電控碩 0860077 王國倫 # 1. Introduction In this lab, we will implement the ResNet18, ResNet50 architecture and load parameters from a pretrained model to analysis diabetic retinopathy, create own dataloader to provide model train, compare and visualize the accuracy between the pretrained model and without pretrained, plot their confusion matrix. # 2. Experiment setups ## A. The details of your model (ResNet) The ResNet18 and ResNet50 are available from torchvision models, so I use these to train diabe-tic retinopathy data and redesigned these, add fully connected and activation function on the last layers, below is my ResNet18 and ResNet50. ```python #!/usr/bin/env python3 import torch.nn as nn from torchvision import models class ResNet18(nn.Module): def __init__(self, num_class, feature_extract, use_pretrained): super(ResNet18,self).__init__() self.model = models.resnet18(pretrained=use_pretrained) set_parameter_requires_grad(self.model, feature_extract) num_ftrs = self.model.fc.in_features self.model.fc = nn.Linear(num_ftrs, 256) self.activate = nn.LeakyReLU() self.fc = nn.Linear(256, num_class) def forward(self, x): x = self.model(x) x = self.activate(x) out = self.fc(x) return out class ResNet50(nn.Module): def __init__(self, num_class, feature_extract, use_pretrained): super(ResNet50,self).__init__() self.model = models.resnet50(pretrained=use_pretrained) set_parameter_requires_grad(self.model, feature_extract) num_ftrs = self.model.fc.in_features self.model.fc = nn.Linear(num_ftrs, 512) self.activate = nn.LeakyReLU() self.fc = nn.Linear(512, 256) self.fc2 = nn.Linear(256, num_class) def forward(self, x): x = self.model(x) x = self.activate(x) x = self.fc(x) x = self.activate(x) out = self.fc2(x) return out def set_parameter_requires_grad(model, feature_extracting): if feature_extracting: for param in model.parameters(): param.requires_grad = False ``` ## B. The details of your Dataloader I create RetinopathyDataset by myself, and write some useful function, like calculate weight loss, transform and download data. The purpose of weight loss is handle imbalanced data, because this lab provided data is very imbalanced data, the "0" class is accounted for 70%. The transform make image to be processed, including rotate, normalize, convert tensor and so on. You can use download data function, download data to dataset folder, and get the dataset path. ```python #!/usr/bin/env python3 import pandas as pd import numpy as np from PIL import Image import os from torch.utils.data.dataset import Dataset import torchvision.transforms as transforms import gdown from zipfile import ZipFile import shutil class RetinopathyDataset(Dataset): def __init__(self, mode): """ Args: mode : Indicate procedure status(training or testing) self.img_name (string list): String list that store all image names. self.label (int or float list): Numerical list that store all ground truth label values. """ self.__downloaddata() self.img_name, self.label = self.__getData(mode) self.trans = self.__trans(mode) self.weight_loss = self.__cal_we_loss() print("> Found %d images..." % (len(self.img_name))) def __len__(self): """'return the size of dataset""" return len(self.img_name) def __getitem__(self, index): # read rgb images rgb_image = Image.open(os.path.join(self.root, self.img_name[index] + ".jpeg")).convert('RGB') # convert images img = self.trans(rgb_image) label = self.label[index] return img, label def __cal_we_loss(self): weight = [0 for x in range(len(set(self.label)))] for i in range(len(weight)): weight[i] = self.label[self.label == i].size return np.true_divide(np.max(weight), weight) def __trans(self, mode): if mode == "train": transform = transforms.Compose([ transforms.RandomVerticalFlip(), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) else: transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) return transform def __downloaddata(self): # download dataset and extract zip to current path model_url = 'https://drive.google.com/u/1/uc?id=1RTmrk7Qu9IBjQYLczaYKOvXaHWBS0o72' model_name = 'RetinopathyDataset' if not os.path.isdir(os.path.join(os.getcwd(),"dataset",model_name)): gdown.download(model_url, output=model_name + '.zip', quiet=False) zip1 = ZipFile(model_name + '.zip') zip1.extractall(model_name) zip1.close() # move folder shutil.move(os.path.join(os.getcwd(),model_name), os.path.join(os.getcwd(),"dataset",model_name)) # delete zip file os.remove(os.path.join(os.getcwd(),model_name + '.zip')) self.root = os.path.join(os.getcwd(), "dataset", model_name, "data") def __getData(self, mode): if mode == 'train': img = pd.read_csv('./dataset/train_img.csv', header = None) label = pd.read_csv('./dataset/train_label.csv', header = None) return np.squeeze(img.values), np.squeeze(label.values) elif mode == "test": img = pd.read_csv('./dataset/test_img.csv', header = None) label = pd.read_csv('./dataset/test_label.csv', header = None) return np.squeeze(img.values), np.squeeze(label.values) else: raise Excption("Error! Please input train or test") ``` ## C. Describing your evaluation through the confusion matrix The confusion matrix can help me understand real result between predicted data and ground truth data, we can see Fig.1 and Fig.2, the ideal result is the higher the diagonal value, the model is better, so the Fig.2 is better than Fig.1. The plot confusion matrix code will show below. \ ![](https://i.imgur.com/Iidliju.png)\ Fig.1 ResNet50 without pretrained and weight loss ![](https://i.imgur.com/lyr3IWH.png)\ Fig.2 ResNet50 with pretrained and weight loss ```python def plot_confusion_matrix(self, num_class): matrix = np.zeros((num_class,num_class)) self.model.eval() for i, (data, label) in enumerate(self.testloader): data, label = Variable(data),Variable(label) # using cuda if self.args.cuda: data, label = data.cuda(), label.cuda() with torch.no_grad(): prediction = self.model(data) pred = prediction.data.max(1, keepdim=True)[1] ground_truth = pred.cpu().numpy().flatten() actual = label.cpu().numpy().astype('int') for j in range(len(ground_truth)): matrix[actual[j]][ground_truth[j]] += 1 for i in range(num_class): matrix[i,:] /= sum(matrix[i,:]) plt.figure(1) plt.imshow(matrix, interpolation="nearest", cmap=plt.cm.Blues) plt.title("Normalized confusion matrix") plt.colorbar() thresh = np.max(matrix) / 2. for i, j in itertools.product(range(matrix.shape[0]), range(matrix.shape[1])): plt.text(j, i, format(matrix[i, j], '.2f'), horizontalalignment="center", color="white" if matrix[i, j] > thresh else "black") tick_marks = np.arange(num_class) plt.xticks(tick_marks) plt.yticks(tick_marks) plt.ylabel('True label') plt.xlabel('Predicted label') plt.savefig(os.path.join(self.picture_path, self.file_name + '_confusion_matrix.png')) ``` # 3. Experimental results ## A. The highest testing accuracy ### Screenshot ![](https://i.imgur.com/kXDeRqk.png)\ Fig.3 ResNet50 with pretrained and no weight loss ![](https://i.imgur.com/7FY88FR.png)\ Fig.4 ResNet18 with pretrained and no weight loss #### Weight loss The provided dataset is imbalanced data, so I add weight loss to cross entropy, try to get the best result. Although the accuracy is not much change, the confusion matrix can present is good(you can see Fig.5 and Fig.6), these model has theirs advantages and disadvantages, depend on the case which you want to use. > ResNet18 * learning rate: 0.001 * batch size: 16 * epochs: 20 * with pretrained | weight loss | with | without | |:-----------:|:--------:|:--------:| | Accuracy | 76.74% | **82.14%** | > ResNet18 * learning rate: 0.001 * batch size: 16 * epochs: 20 * without pretrained | weight loss | with | without | |:-----------:|:--------:|:--------:| | Accuracy | 65.81% | 75.23% | > ResNet50 * learning rate: 0.001 * batch size: 8 * epochs: 20 * with pretrained | weight loss | with | without | |:-----------:|:--------:|:--------:| | Accuracy | 78.61% | **82.21%** | > ResNet50 * learning rate: 0.001 * batch size: 8 * epochs: 20 * without pretrained | weight loss | with | without | |:-----------:|:--------:|:--------:| | Accuracy | 73.36% | 73.36% | ![](https://i.imgur.com/GC7A1fc.png)\ Fig.5 ResNet50 with pretrained and without weight loss \ ![](https://i.imgur.com/gmdwLVA.png)\ Fig.6 ResNet50 with pretrained and with weight loss #### finetune and feature extract First I adjust feature extract and finetune to see result, I use ResNet18 with pretrained and without weight loss, the other parameters are same, the result show below, use feature extract is easy to overfitting so that the other experiment will adopt finetune mode to train. > ResNet18 * learning rate: 0.001 * batch size: 16 * epochs: 20 | | finetune | feature extract | |:-----------:|:--------:|:----------------:| | Accuracy | 82.14% | 75.58% | ![](https://i.imgur.com/SXAS7GD.png)\ Fig.7 ResNet18 finetune ![](https://i.imgur.com/PzuSfll.png)\ Fig.8 ResNet18 feature extract #### Save the best model In order to save the best model, I will store the best parameters in the training, on the other hand, the test accuracy is flow, so the final result maybe not the best, I add it to ensure each training will store the best model so that I can easily keep training next time or show test accuracy. ```python if(te_ac > best_accuracy): best_accuracy = te_ac best_model_weight = copy.deepcopy(self.model.state_dict()) # save the best model torch.save(best_model_weight, os.path.join(self.weight_path, self.file_name + '.pkl')) ``` ## B. Comparison figures ### Plotting the comparison figures This lab will comparison use pretrained or not, I also add weight loss to comparison, so there will four pictures below, and code show below the picture. ![](https://i.imgur.com/2gs9S53.png)\ Fig.9 ResNet18 without weight loss ![](https://i.imgur.com/p7HF9Pi.png)\ Fig.10 ResNet18 with weight loss ![](https://i.imgur.com/YoNPQ9f.png)\ Fig.11 ResNet50 without weight loss ![](https://i.imgur.com/F7b26UX.png)\ Fig.12 ResNet50 with weight loss ```python #!/usr/bin/env python3 import numpy as np import argparse import matplotlib.pyplot as plt def read(file): train_accurancy = [] test_accurancy = [] with open(file) as f: Lines = f.readlines() for line in Lines: _, _, tr, te = line.split(",") train_accurancy.append((float)(tr.split(":")[1])) test_accurancy.append((float)(te.split(":")[1])) return np.array(train_accurancy), np.array(test_accurancy) def plt_result(with_pretrained, without_pretrained, title): plt.title("Result comparison {0}".format(title), fontsize = 18) w_train, w_test = read(with_pretrained) wo_train, wo_test = read(without_pretrained) e = [x for x in range(1,21)] plt.plot(e, w_train, label="Train(w pretrained)") plt.plot(e, w_test, label="Test(w pretrained)") plt.plot(e, wo_train, label="Train(w/o pretrained)") plt.plot(e, wo_test, label="Test(w/o pretrained)") plt.legend(loc='upper left') plt.xlabel("Epochs") plt.ylabel("Accuracy(%)") plt.xticks(e) plt.savefig('{0}.png'.format(title)) plt.show() def create_argparse(): parser = argparse.ArgumentParser(prog="DLP homework 4", description='This code will show with pretrained and without pretrained result \ with matpltlib') parser.add_argument("with_pretrained_file", type=str, default="none", help="input txt file with pretrained result") parser.add_argument("without_pretrained_file", type=str, default="none", help="input txt file without pretrained result") parser.add_argument("title", type=str, default="none", help="save title in the figure") return parser if __name__ == "__main__": parser = create_argparse() args = parser.parse_args() plt_result(args.with_pretrained_file, args.without_pretrained_file, args.title) ``` # 4. Discussion ## A. Virtual environment I create the virtual environment for this lab by used virtualenv, and write the command into script "install.sh", and required libraries in requirements.txt, the code show below. And then, I can use DLP_homework4 to activate this virtual environment, and use deactivate to exit this virtual environment. ```script #!/usr/bin/env bash echo "Install required libraries" pip3 install virtualenv virtualenv_name="DLP_homework4" VIRTUALENV_FOLDER=$(pwd)/${virtualenv_name} virtualenv ${virtualenv_name} source ${VIRTUALENV_FOLDER}/bin/activate python3 -m pip install -r requirements.txt deactivate echo "alias DLP_homework4='source ${VIRTUALENV_FOLDER}/bin/activate '" >> ~/.bashrc source ~/.bashrc ``` In the requirements.txt. ```txt torch==1.8.1 torchvision==0.9.1 torchaudio==0.8.1 matplotlib==3.3.4 tqdm==4.60.0 pandas==1.1.5 gdown==3.12.2 ``` ## B. Model select In order to select different networkt, I use same approach(Activation function select) to access model, and write this function in "__init\__.py" of model folder. ```python from .ResNet import ResNet18, ResNet50 def get_model(name, num_class, feature_extract, use_pretrained): models = { 'ResNet18' : ResNet18(num_class, feature_extract, use_pretrained), 'ResNet50' : ResNet50(num_class, feature_extract, use_pretrained) } return models[name] ``` ## C. Train and evaluation I write training and evaluation function in Trainer class, trainer will load dataset, setting parameter, create network and so on, I also write plt_lr_cur and recore to plot learning curve and record each epoch accuracy and loss. In order to demo, I write save model weight and load model weight, if use load model weight, it will evaluate test dataset, and show test accuracy. ```python #!/usr/bin/env python3 import numpy as np import matplotlib.pyplot as plt from tqdm import tqdm import os import itertools import copy import torch from torch.utils.data import DataLoader from torch.autograd import Variable import torch.nn as nn from dataset.dataset import RetinopathyDataset from model import get_model class Trainer(): def __init__(self,args): self.args = args # data trainset = RetinopathyDataset("train") testset = RetinopathyDataset("test") # file name text = "with_pretrained" if args.pretrained else "without_pretrained" finetune = "feature_extract" if args.feature_extract else "finetune" we = "with_weight_loss" if args.weight_loss else "without_weight_loss" self.file_name ="{0}_{1}_{2}_{3}_{4}_{5}_{6}".format(args.network, text, finetune, we, args.batch_size, args.epochs, args.learning_rate) # crate folder self.weight_path = os.path.join(args.save_folder,"weight") if not os.path.exists(self.weight_path): os.makedirs(self.weight_path) self.picture_path = os.path.join(args.save_folder,"picture") if not os.path.exists(self.picture_path): os.makedirs(self.picture_path) self.record_path = os.path.join(args.save_folder,"record") if not os.path.exists(self.record_path): os.makedirs(self.record_path) # dataloader self.trainloader = DataLoader(dataset=trainset, batch_size=args.batch_size, shuffle=True, num_workers=4) self.testloader = DataLoader(dataset=testset, batch_size=args.batch_size, shuffle=False, num_workers=4) weight_loss = torch.from_numpy(trainset.weight_loss).float() print(weight_loss) # model self.model = get_model(args.network, 5, args.feature_extract, args.pretrained) # show model parameter print(self.model) # optimizer self.optimizer = torch.optim.SGD(self.model.parameters(), lr=args.learning_rate, momentum=0.9, weight_decay = 5e-4) # criterion self.criterion = nn.CrossEntropyLoss(weight=weight_loss) \ if args.weight_loss else nn.CrossEntropyLoss() # using cuda if args.cuda: self.model = self.model.cuda() self.criterion = self.criterion.cuda() print("using cuda") # load model if need if(args.load_path != None): if os.path.exists(args.load_path): self.model.load_state_dict(torch.load(args.load_path)) print("Load model weight.") else: raise("This path is not exist!") self.evaluate(self.testloader) if args.test else self.training() # training def training(self): best_model_weight = None best_accuracy = 0.0 self.loss_record = [] for e in range(self.args.epochs): train_loss = 0.0 tbar = tqdm(self.trainloader) self.model.train() for i, (data, label) in enumerate(tbar): data, label = Variable(data),Variable(label) # using cuda if self.args.cuda: data, label = data.cuda(), label.cuda() prediction = self.model(data) loss = self.criterion(prediction, label.long()) self.optimizer.zero_grad() loss.backward() self.optimizer.step() train_loss += loss.item() tbar.set_description('Train loss: \ {0:.6f}'.format(train_loss / (i + 1))) self.loss_record.append(train_loss / (i + 1)) tr_ac = self.evaluate(self.trainloader) te_ac = self.evaluate(self.testloader) self.record(e+1, train_loss / (i + 1), tr_ac, te_ac) if(te_ac > best_accuracy): best_accuracy = te_ac best_model_weight = copy.deepcopy(self.model.state_dict()) self.plot_confusion_matrix(5) # save the best model torch.save(best_model_weight, os.path.join(self.weight_path, self.file_name + '.pkl')) # plot learning curve self.plt_lr_cur() # evaluation def evaluate(self, d): correct = 0.0 total = 0.0 tbar = tqdm(d) self.model.eval() for i, (data, label) in enumerate(tbar): data, label = Variable(data),Variable(label) # using cuda if self.args.cuda: data, label = data.cuda(), label.cuda() with torch.no_grad(): prediction = self.model(data) pred = prediction.data.max(1, keepdim=True)[1] correct += np.sum(np.squeeze(pred.eq(label.data.view_as(pred))) \ .cpu().numpy()) total += data.size(0) text = "Train accuracy" if d == self.trainloader else "Test accuracy" tbar.set_description('{0}: {1:2.2f}% '.format(text, 100. * correct / total)) return 100.0 * correct / total # plot learning curve def plt_lr_cur(self): plt.figure(2) plt.title("learning curve", fontsize = 18) ep = [x for x in range(1, self.args.epochs + 1)] plt.xlabel("epochs") plt.ylabel("loss") plt.plot(ep, self.loss_record) plt.savefig(os.path.join(self.picture_path, self.file_name + '_learning_curve.png')) # plt.show() # record information def record(self, epochs, loss, tr_ac, te_ac): file_path = os.path.join(self.record_path, self.file_name + '.txt') with open(file_path, "a") as f: f.writelines("Epochs : {0}, train loss : {1:.6f}, train accurancy : \ {2:.2f}, test accurancy : {3:.2f}\n".format(epochs, loss, tr_ac, te_ac)) ``` ## D. Argparse In order to easy select different model hyper-parameters, I use argparse make user can modify own parameters, the code write in option.py. ```python #!/usr/bin/env python3 import argparse import torch import os class Option(): def __init__(self): parser = argparse.ArgumentParser(prog="DLP homework 4", description='This lab implement ResNet-18 and ResNet-50 to analysis \ diabetic retinopathy') # training hyper parameters parser.add_argument("--learning_rate", type=float, default=0.001, help="Learning rate for the trainer, default is 1e-3") parser.add_argument("--epochs", type=int, default=10, help="The number of the training, default is 10") parser.add_argument("--batch_size", type=int, default=8, help="Input batch size for training, default is 8") # save name and load model path parser.add_argument("--save_folder", type=str, default=os.getcwd(), help="save model in save folder, default is current path") parser.add_argument("--load_path", type=str, default=None, help="path to load model weight") parser.add_argument("--test", action="store_true", default=False, help="True is test model, False is keep train, default is False") # cuda parser.add_argument('--no_cuda', action='store_true', default=False, help='disables CUDA training, default is False') # network parser.add_argument("--network", type=str, default="ResNet18", help="select network ResNet18 and ResNet50, default is ResNet18.") parser.add_argument("--pretrained", action='store_true', default=False, help="load pretrained weight to train, default is False") parser.add_argument("--feature_extract", action='store_true', default=False, help="If feature_extract = False, the model is finetuned and \ all model parameters are updated. If feature_extract = True, only the last \ layer parameters are updated, the others remain fixed. Default is False") parser.add_argument("--weight_loss", action="store_true",default=False, help="use weight loss to solve imbalance data. default is False.") self.parser = parser def create(self): args = self.parser.parse_args() args.cuda = not args.no_cuda and torch.cuda.is_available() print(args) return args ``` ## E. Code architecture In this lab, I code archtitecture show below, the dataset in charge of data proccessing, load data, combine ground truth and label. The network will write on model folder, including network archi-tecture and forward. Picture, record and weight folder will save each training result. "main.py" is main code, and excute it start training or test. homework4\ ├── dataset\ │   ├── dataset.py\ │   ├── RetinopathyDataset \ │   ├── train_img.csv │   ├── test_img.csv \ │   ├── train_label.csv \ │   └── test_label.csv \ ├── install.sh\ ├── main.py\ ├── model\ │   ├── ResNet.py \ │   └── __init\__.py\ ├── option.py\ ├── picture\ ├── record\ ├── requirements.txt\ ├── trainer.py\ ├── visual.py\ └── weight ```python #!/usr/bin/env python3 from option import Option from trainer import Trainer import time if __name__ == '__main__': # paramters args = Option().create() time_start = time.time() # trainer trainer = Trainer(args) time_end = time.time() print("Use {0} finished.".format(time_end - time_start)) ``` ###### tags: `DLP2021`