{"cells":[{"metadata":{},"cell_type":"markdown","source":"## General description\n\nIn this competition we work on developing solutions for identifying common diseases of cassava (plant). There are 4 different diseases and, of course, a plant could be healhy. As a resutl we have a classification problem with 5 classes.\n\nIn this notebook I'll explore the data, train a model using pytorch lightning and analyse the predictions.\n\n![](http://www.naro.go.ug/files/images/crops-naro.jpg)"},{"metadata":{},"cell_type":"markdown","source":"#### import libraries"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"from PIL import Image\nfrom albumentations.core.composition import Compose\nfrom albumentations.pytorch import ToTensorV2\nfrom collections import defaultdict, deque\n# from efficientnet_pytorch import EfficientNet\nfrom pytorch_lightning import Callback\nfrom pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping\nfrom sklearn import metrics\nfrom sklearn.model_selection import StratifiedKFold\nfrom torch import nn\nfrom torch.autograd import Variable\nfrom torch.utils.data import Dataset, TensorDataset, DataLoader\nfrom torchvision.transforms import functional as F\nfrom typing import Any, Dict, List, Union, Optional\nimport albumentations as A\nimport ast\nimport collections\nimport copy\nimport cv2\nimport datetime\nimport importlib\nimport json\nimport math\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport os\nimport pandas as pd\nimport pickle\n# import pretrainedmodels\nimport pytorch_lightning as pl\n%matplotlib inline\nimport random\nimport seaborn as sns\nimport shutil\nimport tempfile\nimport time\nimport torch\nimport torch.distributed as dist\nimport torch.nn.functional as F\nimport torch.utils.data\nimport torchvision\nfrom tqdm.notebook import tqdm\nsns.set_style('darkgrid')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"#### helper functions"},{"metadata":{"trusted":true},"cell_type":"code","source":"def visualize(images, transform):\n    \"\"\"\n    Plot images and their transformations\n    \"\"\"\n    fig = plt.figure(figsize=(32, 16))\n    \n    for i, im in enumerate(images):\n        ax = fig.add_subplot(2, 5, i + 1, xticks=[], yticks=[])\n        plt.imshow(im)\n        \n    for i, im in enumerate(images):\n        ax = fig.add_subplot(2, 5, i + 6, xticks=[], yticks=[])\n        plt.imshow(transform(image=im)['image'])\n\ndef set_seed(seed: int = 42) -> None:\n    np.random.seed(seed)\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    torch.backends.cudnn.benchmark = False\n    torch.backends.cudnn.deterministic = True\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\nset_seed()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Data Exploration"},{"metadata":{"trusted":true},"cell_type":"code","source":"path = \"../input/cassava-leaf-disease-classification/\"","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train = pd.read_csv(f'{path}train.csv')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"with open(f'{path}/label_num_to_disease_map.json', 'r') as f:\n    name_mapping = json.load(f)\n    \nname_mapping = {int(k): v for k, v in name_mapping.items()}\nname_mapping","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"As per description, there are 4 diseases and one class for healthy plants."},{"metadata":{"trusted":true},"cell_type":"code","source":"train.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train.shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sns.countplot(y=train['label'].map(name_mapping), orient='v')\nplt.title('Target distribution');","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Interestingly the most common class belongs to one of diseases and not to healthy plants."},{"metadata":{},"cell_type":"markdown","source":"### Let's have a look at cassava\n\nAt first, lets have a look at images belonging to different classes"},{"metadata":{"trusted":true},"cell_type":"code","source":"selected_images = []\nfig = plt.figure(figsize=(16, 16))\nfor class_id, class_name in name_mapping.items():\n    for i, (idx, row) in enumerate(train.loc[train['label'] == class_id].sample(4).iterrows()):\n        ax = fig.add_subplot(5, 4, class_id * 4 + i + 1, xticks=[], yticks=[])\n        img = cv2.imread(f\"{path}train_images/{row['image_id']}\")\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        plt.imshow(img)\n        ax.set_title(f\"Image: {row['image_id']}. Label: {row['label']}\")\n        if i == 0:\n            selected_images.append(img)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"As far as I can see, one of the common symptoms of desease is a change in color - usually yellow color with different patterns. We will need to be careful with augmentations.\n\nBy the way, let's see how different augmentation change the images.\n\nIn the first ror where are original images, in the second row there are augmented images. I selected one random image from each class."},{"metadata":{"trusted":true},"cell_type":"code","source":"visualize(selected_images, A.HorizontalFlip(p=1))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"visualize(selected_images, A.ShiftScaleRotate(p=1))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"visualize(selected_images, A.Cutout(max_h_size=64, max_w_size=64, p=1))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Preparing classes for pytorch-lightning\n\nTraining neural nets in pytorch-lightning requires writing several classes. Some of them are pure Pytorch classes, some are from pl."},{"metadata":{},"cell_type":"markdown","source":"### Dataset class"},{"metadata":{"trusted":true},"cell_type":"code","source":"class ImageClassificationDataset(Dataset):\n    def __init__(\n        self,\n        image_names: List,\n        transforms: Compose,\n        labels: Optional[List[int]],\n        img_path: str = '',\n        mode: str = 'train',\n        labels_to_ohe: bool = False,\n        n_classes: int = 5,\n    ):\n        \"\"\"\n        Image classification dataset.\n\n        Args:\n            df: dataframe with image id and bboxes\n            mode: train/val/test\n            img_path: path to images\n            transforms: albumentations\n        \"\"\"\n\n        self.mode = mode\n        self.transforms = transforms\n        self.img_path = img_path\n        self.image_names = image_names\n        if labels is not None:\n            if not labels_to_ohe:\n                self.labels = np.array(labels)\n            else:\n                self.labels = np.zeros((len(labels), n_classes))\n                self.labels[np.arange(len(labels)), np.array(labels)] = 1\n\n    def __getitem__(self, idx: int) -> Dict[str, np.array]:\n        image_path = self.img_path + self.image_names[idx]\n        image = cv2.imread(f'{image_path}', cv2.IMREAD_COLOR)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        if image is None:\n            raise FileNotFoundError(image_path)\n        target = self.labels[idx]\n\n        img = self.transforms(image=image)['image']\n        sample = {'image_path': image_path, 'image': img, 'target': np.array(target).astype('int64')}\n\n        return sample\n\n    def __len__(self) -> int:\n        return len(self.image_names)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Augmentations\n\nFor now I chose some augmentations at random."},{"metadata":{"trusted":true},"cell_type":"code","source":"train_augs = A.Compose([\n    A.RandomResizedCrop(height=256, width=256, p=1.0),\n    A.Flip(),\n    A.RandomBrightnessContrast(),\n    A.ShiftScaleRotate(),\n    A.Normalize(),\n    ToTensorV2(),\n])\n\nvalid_augs = A.Compose([\n    A.Resize(height=256, width=256, p=1.0),\n    A.Normalize(),\n    ToTensorV2(),\n])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### PL datamodule\n\nThis class prepares data. Here we initialize data classes and write code for dataloaders. Notice that in `setup` I split data into train and valid."},{"metadata":{"trusted":true},"cell_type":"code","source":"class CassavaDataModule(pl.LightningDataModule):\n    def __init__(self,\n                 df,\n                 train_augs,\n                 valid_augs,\n                 path):\n        super().__init__()\n        self.df = df\n        self.train_augs = train_augs\n        self.valid_augs = valid_augs\n        self.path = path\n\n    def prepare_data(self):\n        pass\n\n    def setup(self, stage=None):\n        \n        folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n        \n        train_indexes, valid_indexes = list(folds.split(self.df, self.df['label']))[0]\n        \n        train_df = self.df.iloc[train_indexes]\n        valid_df = self.df.iloc[valid_indexes]\n\n        \n        self.train_dataset = ImageClassificationDataset(image_names=train_df['image_id'].values,\n                                                        transforms=train_augs,\n                                                        labels=train_df['label'].values,\n                                                        img_path=self.path,\n                                                        mode='train',\n                                                        labels_to_ohe=False,\n                                                        n_classes=5)\n        self.valid_dataset = ImageClassificationDataset(image_names=valid_df['image_id'].values,\n                                                        transforms=valid_augs,\n                                                        labels=valid_df['label'].values,\n                                                        img_path=self.path,\n                                                        mode='valid',\n                                                        labels_to_ohe=False,\n                                                        n_classes=5)\n\n    def train_dataloader(self):\n        train_loader = torch.utils.data.DataLoader(\n            self.train_dataset,\n            batch_size=64,\n            num_workers=4,\n            shuffle=True,\n        )\n        return train_loader\n\n    def val_dataloader(self):\n        valid_loader = torch.utils.data.DataLoader(\n            self.valid_dataset,\n            batch_size=64,\n            num_workers=4,\n            shuffle=False,\n        )\n\n        return valid_loader\n\n    def test_dataloader(self):\n        return None","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Defining the model.\n\nI prefer a modular approach for defining the model.\n\nWhen I train models locally, I don't have to think how to get the weights, but in Kaggle notebooks without internet it is necessary to think about it. I wrote code only for torchvision for now."},{"metadata":{"trusted":true},"cell_type":"code","source":"def freeze_until(net: Any, param_name: str = None) -> None:\n    \"\"\"\n    Freeze net until param_name\n\n\n    Args:\n        net:\n        param_name:\n\n    \"\"\"\n    found_name = False\n    for name, params in net.named_parameters():\n        if name == param_name:\n            found_name = True\n        params.requires_grad = found_name\n\nclass BasicEncoder(nn.Module):\n    def __init__(\n        self,\n        arch: str = 'resnet18',\n        source: str = 'torchvision',\n        pretrained: str = None,\n        n_layers: int = -2,\n        freeze: bool = False,\n        to_one_channel: bool = False,\n        freeze_until_layer: str = None,\n    ) -> None:\n        \"\"\"\n        Initialize Encoder.\n\n        Args:\n            num_classes: the number of target classes, the size of the last layer's output\n            arch: the name of the architecture form pretrainedmodels\n            pretrained: the mode for pretrained model from pretrainedmodels\n            n_layers: number of layers to keep\n            freeze: to freeze model\n            freeze_until: freeze until this layer. If None, then freeze all layers\n        \"\"\"\n        super().__init__()\n        if 'eff' in arch:\n            net = EfficientNet.from_pretrained(arch)\n            self.output_dimension = net._fc.in_features\n        elif source == 'pretrainedmodels':\n            net = pretrainedmodels.__dict__[arch](pretrained=pretrained)\n            self.output_dimension = list(net.children())[-1].in_features\n        elif source == 'torchvision':\n            net = torchvision.models.__dict__[arch](pretrained=pretrained)\n            net.load_state_dict(torch.load(f'../input/pytorch-pretrained-image-models/{arch}.pth'))\n            self.output_dimension = list(net.children())[-1].in_features\n            \n        if freeze:\n            freeze_until(net, freeze_until_layer)\n\n        layers = list(net.children())[:n_layers]\n        if to_one_channel:\n            # https://www.kaggle.com/c/bengaliai-cv19/discussion/130311#745589\n            # saving the weights of the first conv in w\n            w = layers[0].weight\n            # creating new Conv2d to accept 1 channel\n            layers[0] = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)\n            # substituting weights of newly created Conv2d with w from but we have to take mean\n            # to go from  3 channel to 1\n            layers[0].weight = nn.Parameter(torch.mean(w, dim=1, keepdim=True))\n            layers = nn.Sequential(*layers)\n\n        self.layers = nn.Sequential(*layers)\n\n    def forward(self, x):\n        output = self.layers(x)\n\n        return output","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class BasicDecoder(nn.Module):\n    def __init__(self, pool_output_size: int = 2, n_classes: int = 1, output_dimension: int = 512) -> None:\n        \"\"\"\n        Initialize Decoder.\n\n        Args:\n            pool_output_size: the size of the result feature map after adaptive pooling layer\n            n_classes: n classes to output\n            output_dimension: output dimension of encoder\n        \"\"\"\n        super().__init__()\n        self.pool = nn.AdaptiveAvgPool2d(output_size=pool_output_size)\n        self.fc = nn.Linear(output_dimension * pool_output_size * pool_output_size, n_classes)\n\n    def forward(self, x):\n        x = self.pool(x)\n        output = self.fc(x.view(x.size()[0], -1))\n\n        return output","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class Net(nn.Module):\n    def __init__(self) -> None:\n        \"\"\"\n        Model class.\n\n        Args:\n            cfg: main config\n        \"\"\"\n        super().__init__()\n        self.encoder = BasicEncoder(arch='resnet34',\n                                    source='torchvision',\n                                    pretrained=None,\n                                    n_layers=-2,\n                                    freeze=False,\n                                    to_one_channel=False,\n                                    freeze_until_layer=None)\n        self.decoder = BasicDecoder(n_classes=5)\n        self.loss = nn.CrossEntropyLoss()\n\n    def forward(self, x, targets):\n        out = self.encoder(x)\n        logits = self.decoder(out)\n        loss = self.loss(logits, targets).view(1)\n        return logits, loss\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Main pl training class\n\nIn this class we define optimizers, schedulers and training itself."},{"metadata":{"trusted":true},"cell_type":"code","source":"class LitCassava(pl.LightningModule):\n    def __init__(self, model):\n        super(LitCassava, self).__init__()\n        self.model = model\n        self.metric = pl.metrics.Accuracy()\n        self.learning_rate = 1e-4\n\n    def forward(self, x, targets, *args, **kwargs):\n        return self.model(x, targets)\n\n    def configure_optimizers(self):\n        optimizer = torch.optim.AdamW(self.model.parameters(), lr=self.learning_rate, weight_decay=0.001)\n        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.1, patience=2)\n\n        return (\n            [optimizer],\n            [{'scheduler': scheduler, 'interval': 'epoch', 'monitor': 'valid_loss'}],\n        )\n\n    def training_step(\n        self, batch: torch.Tensor, batch_idx: int\n    ) -> Union[int, Dict[str, Union[torch.Tensor, Dict[str, torch.Tensor]]]]:\n        image = batch['image']\n        target = batch['target']\n        logits, loss = self(image, target)\n        score = self.metric(logits.argmax(1), target)\n        logs = {'train_loss': loss, f'train_accuracy': score}\n        return {\n            'loss': loss,\n            'log': logs,\n            'progress_bar': logs,\n            'logits': logits,\n            'target': target,\n            f'train_accuracy': score,\n        }\n\n    def training_epoch_end(self, outputs):\n        avg_loss = torch.stack([x['loss'] for x in outputs]).mean()\n        y_true = torch.cat([x['target'] for x in outputs])\n        y_pred = torch.cat([x['logits'] for x in outputs])\n        score = self.metric(y_pred.argmax(1), y_true)\n        \n        logs = {'train_loss': avg_loss, 'train_accuracy': score}\n        return {'log': logs, 'progress_bar': logs}\n\n    def validation_step(\n        self, batch: torch.Tensor, batch_idx: int\n    ) -> Union[int, Dict[str, Union[torch.Tensor, Dict[str, torch.Tensor]]]]:\n        image = batch['image']\n        target = batch['target']\n        logits, loss = self(image, target)\n        score = self.metric(logits.argmax(1), target)\n        logs = {'valid_loss': loss, f'valid_accuracy': score}\n\n        return {\n            'loss': loss,\n            'log': logs,\n            'progress_bar': logs,\n            'logits': logits,\n            'target': target,\n            f'valid_accuracy': score,\n        }\n\n    def validation_epoch_end(self, outputs):\n        avg_loss = torch.stack([x['loss'] for x in outputs]).mean()\n        y_true = torch.cat([x['target'] for x in outputs])\n        y_pred = torch.cat([x['logits'] for x in outputs])\n        score = self.metric(y_pred.argmax(1), y_true)\n\n        # score = torch.tensor(1.0, device=self.device)\n        logs = {'valid_loss': avg_loss, f'valid_accuracy': score, 'accuracy': score}\n        return {'valid_loss': avg_loss, 'log': logs, 'progress_bar': logs}","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Training the model"},{"metadata":{"trusted":true},"cell_type":"code","source":"model = Net()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"dm = CassavaDataModule(train, train_augs, valid_augs, f'{path}train_images/')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"trainer = pl.Trainer(\n        checkpoint_callback=ModelCheckpoint(monitor='train_loss',\n                                            save_top_k=1, filepath='{epoch}_{valid_loss:.4f}_{accuracy:.4f}', mode='min'),\n        gpus=1,\n        max_epochs=10,\n        num_sanity_val_steps=0,\n        weights_summary='top',\n        callbacks = [EarlyStopping(monitor='valid_loss', patience=10, mode='min')]\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"lit_model = LitCassava(model)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"trainer.fit(lit_model, dm)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Analyzing the predictions\n\nFirst of all, let's make predictions on validation data and collect them."},{"metadata":{"trusted":true},"cell_type":"code","source":"lit_model.model.eval();","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"image_paths = []\ntargets = []\npredictions = []\nfor i, batch in tqdm(enumerate(dm.val_dataloader())):\n    with torch.no_grad():\n        targets.append(batch['target'].detach().cpu().numpy())\n        image_paths.append(batch['image_path'])\n        pred = lit_model.model(batch['image'], batch['target'])[0]\n        predictions.append(pred.detach().cpu().numpy())","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"preds_df = pd.DataFrame({'target': np.concatenate(targets),\n              'prediction': np.concatenate(predictions).argmax(1),\n              'logits': np.concatenate(predictions).max(1),\n              'image_paths': [i for j in image_paths for i in j]})\npreds_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sns.countplot(y=preds_df['prediction'].map(name_mapping), orient='v')\nplt.title('Prediction distribution');","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"print(metrics.classification_report(preds_df['target'], preds_df['prediction']))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"metrics.confusion_matrix(preds_df['target'], preds_df['prediction'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"selected_images = []\nfig = plt.figure(figsize=(16, 16))\nc = 1\nfor class_id1, class_name1 in name_mapping.items():\n    for class_id2, class_name2 in name_mapping.items():\n        if class_id1 != class_id2:\n            img_path = preds_df.loc[(preds_df['target'] == class_id1)\n                                    & (preds_df['prediction'] == class_id2)].sort_values('logits', ascending=False)['image_paths'].values[0]\n\n            ax = fig.add_subplot(5, 4, c, xticks=[], yticks=[])\n            img = cv2.imread(img_path)\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            plt.imshow(img)\n            ax.set_title(f\"Correct class: {class_id1}. Predicted class: {class_id2}\")\n            c += 1","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We can see that the distribution of predictions is similar to the distribution original classes.\n\nRare classes have more errors in predictions, we will need to find a way to tacke that."},{"metadata":{},"cell_type":"markdown","source":"## Prediction"},{"metadata":{"trusted":true},"cell_type":"code","source":"sub = pd.read_csv(f'{path}/sample_submission.csv')\nsub.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_dataset = ImageClassificationDataset(image_names=sub['image_id'].values,\n                                                        transforms=valid_augs,\n                                                        labels=sub['label'].values,\n                                                        img_path=f'{path}test_images/',\n                                                        mode='test',\n                                                        labels_to_ohe=False,\n                                                        n_classes=5)\n\ntest_loader = torch.utils.data.DataLoader(\n            test_dataset,\n            batch_size=4,\n            num_workers=4,\n            shuffle=False,\n        )\n\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"lit_model.model.cuda()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"predictions = []\n\nfor batch in test_loader:\n\n    image = batch['image'].to('cuda')\n    target = batch['target'].to('cuda')\n    with torch.no_grad():\n        outputs = lit_model.model(image, target)[0]\n        preds = outputs.argmax(1).detach().cpu().numpy()\n\n        predictions.append(preds)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sub['label'] = np.concatenate(predictions)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sub","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sub.to_csv('submission.csv', index=False)","execution_count":null,"outputs":[]}],"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":4,"nbformat_minor":4}