{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## This is the demo code for HKUST AIAA5023 Spring2022.\n\n### Based on a simple CNN (resnet34).","metadata":{}},{"cell_type":"markdown","source":"### 1. Take a glance at the data\nYou may delve deeper of the data (**Exploratory Data Analysis**) to provide foundation and insights for your later methods.","metadata":{}},{"cell_type":"code","source":"import os\ndata_path = '../input/cassava-leaf-disease-classification'\nprint(len(os.listdir('../input/cassava-leaf-disease-classification/train_images')))\nprint(len(os.listdir('../input/cassava-leaf-disease-classification/test_images')))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2022-03-01T14:48:15.013870Z","iopub.execute_input":"2022-03-01T14:48:15.014253Z","iopub.status.idle":"2022-03-01T14:48:15.033146Z","shell.execute_reply.started":"2022-03-01T14:48:15.014205Z","shell.execute_reply":"2022-03-01T14:48:15.031634Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"There is only one pic in the 'test_images' fold. Hence you need to separate some data as validation data from the 'train_images' fold by yourself.","metadata":{}},{"cell_type":"code","source":"import pandas as pd\ntrain_df = pd.read_csv('../input/cassava-leaf-disease-classification/train.csv')\ntrain_df.head()","metadata":{"execution":{"iopub.status.busy":"2022-03-01T14:48:15.034801Z","iopub.execute_input":"2022-03-01T14:48:15.035054Z","iopub.status.idle":"2022-03-01T14:48:15.059698Z","shell.execute_reply.started":"2022-03-01T14:48:15.035016Z","shell.execute_reply":"2022-03-01T14:48:15.058944Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Meaning of labels and the number of data in different classes.\n\n**Hint**: Class imbalance problem. Methods such as resampling, reweighting and focal loss may help.","metadata":{}},{"cell_type":"code","source":"import json\nimport seaborn as sns\nwith open('../input/cassava-leaf-disease-classification/label_num_to_disease_map.json') as file:\n    print(json.dumps(json.loads(file.read()), indent=4))\nsns.countplot(x = 'label', data = train_df)\ntrain_df['label'].value_counts()","metadata":{"execution":{"iopub.status.busy":"2022-03-01T14:48:15.060988Z","iopub.execute_input":"2022-03-01T14:48:15.061804Z","iopub.status.idle":"2022-03-01T14:48:15.252793Z","shell.execute_reply.started":"2022-03-01T14:48:15.061763Z","shell.execute_reply":"2022-03-01T14:48:15.252096Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### 2. Data preprocessing","metadata":{}},{"cell_type":"markdown","source":"#### 2.1 Read image","metadata":{}},{"cell_type":"code","source":"import cv2\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\n# def get_img(path):\n#     im_bgr = cv2.imread(path)\n#     im_rgb = im_bgr[:, :, ::-1]\n#     return im_rgb\n\n# a = get_img('../input/cassava-leaf-disease-classification/train_images/1000015157.jpg')\nImage.open('../input/cassava-leaf-disease-classification/train_images/1000015157.jpg')\n# print(a.shape)\n# plt.imshow(a)","metadata":{"execution":{"iopub.status.busy":"2022-03-01T14:48:15.254925Z","iopub.execute_input":"2022-03-01T14:48:15.255199Z","iopub.status.idle":"2022-03-01T14:48:15.433541Z","shell.execute_reply.started":"2022-03-01T14:48:15.255162Z","shell.execute_reply":"2022-03-01T14:48:15.432967Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### 2.2 Data split\nsplit the data in fold 'train_images' to training data and validation data.","metadata":{}},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\ndata_list = pd.read_csv('../input/cassava-leaf-disease-classification/train.csv')\ntrain_list, valid_list = train_test_split(data_list, test_size=0.2, random_state=42)\nprint(data_list)\nprint(train_list.shape)\nprint(valid_list.shape)","metadata":{"execution":{"iopub.status.busy":"2022-03-01T14:48:15.434553Z","iopub.execute_input":"2022-03-01T14:48:15.435242Z","iopub.status.idle":"2022-03-01T14:48:15.467776Z","shell.execute_reply.started":"2022-03-01T14:48:15.435207Z","shell.execute_reply":"2022-03-01T14:48:15.466972Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### 2.3 Prepare Dataset and Dataloader class for using pytorch\n\nUnlike datasets such as MNIST and CIFAR-10/100 which have existed sealed package, this new dataset does not have any. We need to create a Dataset class for the purpose of using Dataloader.","metadata":{}},{"cell_type":"code","source":"from torch.utils.data import Dataset,DataLoader\nfrom PIL import Image\n\nclass CassavaDataset(Dataset):\n    \n    def __init__(self,  df, transforms=None, target_transform=None):\n        super().__init__()\n        self.data_root = \"../input/cassava-leaf-disease-classification/train_images\"\n        self.df = df.reset_index(drop=True).copy()\n        self.transforms = transforms\n        self.target_transform = target_transform\n \n    def __getitem__(self, index: int):\n        img_path = \"{}/{}\".format(self.data_root, self.df.iloc[index]['image_id'])\n#         img  = get_img(img_path)\n        img = Image.open(img_path).convert('RGB')\n        \n        target = self.df.iloc[index]['label']\n\n        if self.transforms is not None:\n            img = self.transforms(img)\n            \n        if self.target_transform is not None:\n            target = self.target_transform(target)\n\n        return img, target\n    \n    def __len__(self):\n        return len(self.df)","metadata":{"execution":{"iopub.status.busy":"2022-03-01T14:48:15.469055Z","iopub.execute_input":"2022-03-01T14:48:15.469470Z","iopub.status.idle":"2022-03-01T14:48:15.479532Z","shell.execute_reply.started":"2022-03-01T14:48:15.469438Z","shell.execute_reply":"2022-03-01T14:48:15.478792Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import torch\nimport torchvision\nimport torchvision.transforms as transforms\n\n\ndef Get_Dataloader(train_list, valid_list):\n    \n    # data augmentation is more abundant for training data than validation data\n    train_transform = transforms.Compose([\n            transforms.Resize((256, 256)),\n            transforms.CenterCrop(224), # we will use resnet34, which requires the size of input to be 224*224*3, you can also change the network structure to accomodate the original image size\n            transforms.ColorJitter(0.2, 0.2, 0.2),\n            transforms.RandomHorizontalFlip(p=0.5),\n            transforms.ToTensor(),\n            transforms.Normalize(mean = [0.5,0.5,0.5], std = [0.5,0.5,0.5]),\n        ])\n    \n    valid_transform = transforms.Compose([\n            transforms.Resize((224, 224)),\n            transforms.ToTensor(),\n            transforms.Normalize(mean = [0.5,0.5,0.5], std = [0.5,0.5,0.5]),\n        ])\n \n    # training and validation datasets\n    train_data = CassavaDataset(train_list, transforms=train_transform, target_transform=None)\n    valid_data = CassavaDataset(valid_list, transforms=valid_transform, target_transform=None)\n    \n    # training and validation dataloaders\n    train_loader = torch.utils.data.DataLoader(\n        train_data,\n        batch_size=64,\n        num_workers=2,\n        shuffle=True, \n        pin_memory=True,\n    )\n\n    valid_loader = torch.utils.data.DataLoader(\n        valid_data, \n        batch_size=64,\n        num_workers=2,\n        shuffle=False,\n        pin_memory=True,\n    )\n    \n    return train_loader, valid_loader\n\ntrain_loader, valid_loader = Get_Dataloader(train_list, valid_list)","metadata":{"execution":{"iopub.status.busy":"2022-03-01T14:48:15.481210Z","iopub.execute_input":"2022-03-01T14:48:15.481832Z","iopub.status.idle":"2022-03-01T14:48:15.499456Z","shell.execute_reply.started":"2022-03-01T14:48:15.481791Z","shell.execute_reply":"2022-03-01T14:48:15.498602Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### 3. Model training","metadata":{}},{"cell_type":"markdown","source":"#### 3.1 Some useful tools","metadata":{}},{"cell_type":"code","source":"class AverageMeter(object):\n    \"\"\"Computes and stores the average and current value\"\"\"\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.val = 0\n        self.avg = 0\n        self.sum = 0\n        self.count = 0\n\n    def update(self, val, n=1):\n        self.val = val\n        self.sum += val * n\n        self.count += n\n        self.avg = self.sum / self.count\n        \ndef save_model(model, save_file):\n    state = {\n        'model': model.state_dict(),\n    }\n    torch.save(state, save_file)\n    del state\n    \n\ndef adjust_learning_rate(optimizer, epoch, lr, milestones):\n    '''decrease learning rate lr to 0.1*lr when train for milestone[0]th epoch, and decrease it to 0.01*lr when comes to milestone[1]th epoch\n    '''\n    lr_1 = lr*(0.1**(epoch>=milestones[0]))*(0.1**(epoch>=milestones[1]))\n\n    for param_group in optimizer.param_groups:\n        param_group['lr'] = lr_1\n\n    return lr_1","metadata":{"execution":{"iopub.status.busy":"2022-03-01T14:48:15.502170Z","iopub.execute_input":"2022-03-01T14:48:15.502790Z","iopub.status.idle":"2022-03-01T14:48:15.511791Z","shell.execute_reply.started":"2022-03-01T14:48:15.502703Z","shell.execute_reply":"2022-03-01T14:48:15.511108Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### 3.2 Train for one epoch","metadata":{}},{"cell_type":"code","source":"import numpy as np\nfrom sklearn.metrics import accuracy_score\ndef train(model, criterion, optimizer, train_loader, device, epoch):\n    losses = AverageMeter()\n    accs = AverageMeter()\n    \n    model.train()\n    for idx, (images, labels) in enumerate(train_loader):\n        images = images.to(device)\n        labels = labels.to(device)\n        bsz = labels.shape[0]\n        \n        output = model(images)\n        loss = criterion(output, labels)\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n        \n        _, predicted = torch.max(output.data, 1)\n        acc = accuracy_score(labels.cpu(), predicted.cpu())\n        \n        accs.update(acc)\n        losses.update(loss.item())\n        \n    return accs.avg, losses.avg","metadata":{"execution":{"iopub.status.busy":"2022-03-01T14:48:15.513192Z","iopub.execute_input":"2022-03-01T14:48:15.513517Z","iopub.status.idle":"2022-03-01T14:48:15.524366Z","shell.execute_reply.started":"2022-03-01T14:48:15.513483Z","shell.execute_reply":"2022-03-01T14:48:15.523671Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### 3.3 Valid for one epoch","metadata":{}},{"cell_type":"code","source":"def valid(model, criterion, valid_loader, device, epoch):\n    losses = AverageMeter()\n    accs = AverageMeter()\n    \n    model.eval()\n    with torch.no_grad():\n        for idx, (images, labels) in enumerate(valid_loader):\n            images = images.to(device)\n            labels = labels.to(device)\n            bsz = labels.shape[0]\n        \n            output = model(images)\n            loss = criterion(output, labels)\n        \n            _, predicted = torch.max(output.data, 1)\n            acc = accuracy_score(labels.cpu(), predicted.cpu())\n        \n            accs.update(acc)\n            losses.update(loss.item())\n      \n    return accs.avg, losses.avg","metadata":{"execution":{"iopub.status.busy":"2022-03-01T14:48:15.527498Z","iopub.execute_input":"2022-03-01T14:48:15.527722Z","iopub.status.idle":"2022-03-01T14:48:15.536934Z","shell.execute_reply.started":"2022-03-01T14:48:15.527690Z","shell.execute_reply":"2022-03-01T14:48:15.535976Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### 4. Parameters and run for some epochs\nFor your training, you should run for more epochs. There are also many other methods for adjusting learning rate. Maybe you can save the model which achieves best acc on validation dataset.\n\nYou can also split the data to train and validation data in other ways. For example, K-folds.\n\nThere are much room for improvement!","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision import models\n\nnum_classes = 5\nmodel = models.resnet34() # we use resnet34 as the backbone\nmodel.fc = nn.Linear(model.fc.in_features, num_classes) # we should change the last fc layer of the predefined resnet34 network to accomodate our classification problem\nlr = 0.1\noptimizer = optim.SGD(model.parameters(), lr=lr)\ncriterion = nn.CrossEntropyLoss()\nn_epochs = 50  # just a demo\nmilestones = [30, 40]\ndevice = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n\nmodel.to(device)\ntrain_loader, valid_loader = Get_Dataloader(train_list, valid_list)\n\nos.makedirs('cassava/model')\n\nbest_acc = 0\nfor epoch in range(1, n_epochs+1):\n    lr_now = adjust_learning_rate(optimizer, epoch, lr, milestones) # adjust learning rate\n    acc_train, loss_train = train(model, criterion, optimizer, train_loader, device, epoch)\n    print(\"Epoch[{}], Train acc: {:.3f}, Train loss: {:.3f}, lr {:.3f}.\".format(epoch, acc_train, loss_train, lr_now))\n    acc_val, loss_val = valid(model, criterion, valid_loader, device, epoch)\n    print(\"Epoch[{}], Valid acc: {:.3f}, Valid loss: {:.3f}.\".format(epoch, acc_val, loss_val))\n    if acc_val > best_acc:\n        save_model(model, 'cassava/model/best.pth') # save model\n        best_acc = acc_val","metadata":{"execution":{"iopub.status.busy":"2022-03-01T14:48:15.538817Z","iopub.execute_input":"2022-03-01T14:48:15.539170Z","iopub.status.idle":"2022-03-01T15:31:44.674374Z","shell.execute_reply.started":"2022-03-01T14:48:15.539092Z","shell.execute_reply":"2022-03-01T15:31:44.672579Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"If you use Kaggle notebook, I recommend you to download the saved model to your own computer and open another notebook to test your code, and submit it to kaggle.","metadata":{}},{"cell_type":"markdown","source":"### 5. Test and submit results to Kaggle\nYou may find that there is only one image in the fold 'test_images'. This image is for you to test the feasibility of your code. For this kaggle contest, you should submit your test code to the contest, and it will run your code on their private test dataset to get the performance of your model. Hence, following codes are what you should submit.","metadata":{}},{"cell_type":"code","source":"from glob import glob\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torchvision import models\nfrom torch.utils.data import Dataset,DataLoader\nfrom PIL import Image\nimport torchvision.transforms as transforms\n\n# def a new dataset function\nclass CassavaDataset_test(Dataset):\n    \n    def __init__(self,  df, transforms=None, target_transform=None):\n        super().__init__()\n        self.data_root = \"/kaggle/input/cassava-leaf-disease-classification/test_images\"\n        self.df = df.reset_index(drop=True).copy()\n        self.transforms = transforms\n        self.target_transform = target_transform\n \n    def __getitem__(self, index: int):\n        img_path = \"{}/{}\".format(self.data_root, self.df.iloc[index]['image_id'])\n#         img  = get_img(img_path)\n        img = Image.open(img_path).convert('RGB')\n        \n        img_id = self.df.iloc[index]['image_id']      # add\n        target = self.df.iloc[index]['label']\n\n        if self.transforms is not None:\n            img = self.transforms(img)\n            \n        if self.target_transform is not None:\n            target = self.target_transform(target)\n\n        return img_id, img, target                   # add\n    \n    def __len__(self):\n        return len(self.df)\n\n    \ndevice = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n\n# load model\nmodel_file = 'cassava/model/best.pth'\nmodel = models.resnet34()\nmodel.fc = nn.Linear(model.fc.in_features, 5)\ncheckpoint = torch.load(model_file)\nstate = checkpoint['model']\nmodel.load_state_dict(state)\nmodel.to(device)\n\n# test image loader\nresult = {}\ntest_images = glob('/kaggle/input/cassava-leaf-disease-classification/test_images/*.jpg')\nfor img in test_images:\n    img_name = img.split('/')[-1]\n    if img_name not in result:\n        result[img_name] = 99\ntest_list = pd.DataFrame.from_dict(result, orient='index', columns=['label']).reset_index().rename(columns={'index': 'image_id'})\n\nvalid_transform = transforms.Compose([transforms.Resize((224, 224)),transforms.ToTensor(), transforms.Normalize(mean = [0.5,0.5,0.5], std = [0.5,0.5,0.5]),])\ntest_data = CassavaDataset_test(test_list, transforms=valid_transform, target_transform=None)\ntest_loader = torch.utils.data.DataLoader(test_data,batch_size=64,num_workers=2,shuffle=False,pin_memory=True)\n\n#  use model to test image and produce a 'submission.csv' file\nmodel.eval()\nwith torch.no_grad():\n    for idx, (image_id, images, _) in enumerate(test_loader):\n        images = images.to(device)\n        bsz = images.shape[0]\n        output = model(images)\n        _, predicted = torch.max(output.data, 1)\n        for i in range(bsz):\n            result[image_id[i]] = predicted[i].item()  # record predicted label\n        \nresult = pd.DataFrame.from_dict(result, orient='index', columns=['label']).reset_index().rename(columns={'index': 'image_id'})\nresult.to_csv('submission.csv', index=False)\n","metadata":{"execution":{"iopub.status.busy":"2022-03-01T15:48:57.567810Z","iopub.execute_input":"2022-03-01T15:48:57.568099Z","iopub.status.idle":"2022-03-01T15:48:58.203133Z","shell.execute_reply.started":"2022-03-01T15:48:57.568067Z","shell.execute_reply":"2022-03-01T15:48:58.202206Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Try to do some coding!** **Good luck~**","metadata":{}}]}