{"cells":[{"metadata":{},"cell_type":"markdown","source":"# Dog Breed Classification","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"In this project, we will try to classify 120 different dog species from over 10,000 images\n\nWe run a resnet34 model using Pytorch and achieve 75+% accuracy in around 30 minutes of training\n\nI detail out the steps and try to define the steps. \n\nHope this helps!","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# Import Libraries","execution_count":null},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n%matplotlib inline\nimport seaborn as sns\n\nimport cv2\nimport random\nfrom random import randint\nimport time\n\n\nimport torch\nfrom torch.utils.data import Dataset, random_split, DataLoader\nimport torch.nn.functional as F\nimport torch.nn as nn\n\nfrom PIL import Image\nfrom scipy import ndimage\n\nimport torchvision\nimport torchvision.models as models\nimport torchvision.transforms as T\nfrom torchvision.utils import make_grid\nfrom torchvision.datasets.utils import download_url\nfrom torchvision.datasets import ImageFolder\n\nfrom tqdm.notebook import tqdm\n\nfrom sklearn.metrics import f1_score\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Preparing the Data","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Define the data directories","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"DATA_DIR = '../input/dog-breed-identification'\n\n\nTRAIN_DIR = DATA_DIR + '/train'                           \nTEST_DIR = DATA_DIR + '/test'                             \n\nTRAIN_CSV = DATA_DIR + '/labels.csv'                     \nTEST_CSV = DATA_DIR + '/submission.csv' ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Read the files","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"data_df = pd.read_csv(TRAIN_CSV)\ndata_df.head(10)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Create a label dictionary","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"labels_names=data_df[\"breed\"].unique()\nlabels_sorted=labels_names.sort()\n\nlabels = dict(zip(range(len(labels_names)),labels_names))\nlabels ","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"markdown","source":"Add the numberical labels and path to the dataframe","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"\nlbl=[]\npath_img=[]\n\nfor i in range(len(data_df[\"breed\"])):\n    temp1=list(labels.values()).index(data_df.breed[i])\n    lbl.append(temp1)\n    temp2=TRAIN_DIR + \"/\" + str(data_df.id[i]) + \".jpg\"\n    path_img.append(temp2)\n\ndata_df['path_img'] =path_img  \ndata_df['lbl'] = lbl\n\ndata_df.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Lets check the number of files and classes (dog breeds) in the dataset","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"num_images = len(data_df[\"id\"])\nprint('Number of images in Training file:', num_images)\nno_labels=len(labels_names)\nprint('Number of dog breeds in Training file:', no_labels)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Are images equally distributed between all dog breeds?\n\nLet's plot a graph and see!","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"bar = data_df[\"breed\"].value_counts(ascending=True).plot.barh(figsize = (30,120))\nplt.title(\"Distribution of the Dog Breeds\", fontsize = 20)\nbar.tick_params(labelsize=16)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"data_df[\"breed\"].value_counts(ascending=False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We observe that the distribution is not equal. Scottish deerhound has 126 images while eskimo dog and briard breeds have 66 images","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# Image Analysis","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Let us display 20 picture of the dataset with their labels","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"fig, axes = plt.subplots(nrows=4, ncols=5, figsize=(15, 15),\n                        subplot_kw={'xticks': [], 'yticks': []})\n\nfor i, ax in enumerate(axes.flat):\n    ax.imshow(plt.imread(data_df.path_img[i]))\n    ax.set_title(data_df.breed[i])\nplt.tight_layout()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"What do you observe?\n\nAll images are of differnt sizes\n\nThe backgrounsd vary- some have humans, and other items in the backgrounds\n\nAlso some images are not vertical - e.g., the lakeland terrier in the lower night","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"# Image Transforms using Pytorch","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"class DogDataset(Dataset):\n    def __init__(self, df, root_dir, transform=None):\n        self.df = df\n        self.transform = transform\n        self.root_dir = root_dir\n        \n    def __len__(self):\n        return len(self.df)    \n    \n    def __getitem__(self, idx):\n        row = self.df.loc[idx]\n        img_id, img_label = row['id'], row['lbl']\n        img_fname = self.root_dir + \"/\" + str(img_id) + \".jpg\"\n        img = Image.open(img_fname)\n        if self.transform:\n            img = self.transform(img)\n        return img, img_label","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Lets perform image transforms the same using PyTorch\n\nfor a \n[Beginner's Guide: Image Augmentation & Transforms click here](https://www.kaggle.com/kmldas/beginner-s-guide-image-augmentation-transforms)","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"imagenet_stats = ([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n\ntrain_tfms = T.Compose([\n    T.Resize((300,300)),\n#    T.CenterCrop(256),\n    T.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.1),\n#    T.RandomCrop(32, padding=4, padding_mode='reflect'),\n    T.RandomHorizontalFlip(), \n    T.RandomRotation(10),\n    T.ToTensor(),\n    T.Normalize(*imagenet_stats,inplace=True), \n#    T.RandomErasing(inplace=True)\n])\n\nvalid_tfms = T.Compose([\n    T.Resize((300,300)),\n    #T.CenterCrop(256),\n    T.ToTensor(),\n    T.Normalize(*imagenet_stats)\n])\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"np.random.seed(42)\nmsk = np.random.rand(len(data_df)) < 0.8\n\ntrain_df = data_df[msk].reset_index()\nval_df = data_df[~msk].reset_index()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_ds = DogDataset(train_df, TRAIN_DIR, transform=train_tfms)\nval_ds = DogDataset(val_df, TRAIN_DIR, transform=valid_tfms)\nlen(train_ds), len(val_ds)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def show_sample(img, target, invert=True):\n    if invert:\n        plt.imshow(1 - img.permute((1, 2, 0)))\n    else:\n        plt.imshow(img.permute(1, 2, 0))\n    print('Labels:', labels[target])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# View Sample Images after Transform","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"We view images with inverted colours and normal colours","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"show_sample(*train_ds[241])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"show_sample(*train_ds[419], invert=False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Data holders","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"batch_size = 128","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_dl = DataLoader(train_ds, batch_size, shuffle=True, \n                      num_workers=3, pin_memory=True)\nval_dl = DataLoader(val_ds, batch_size*2, \n                    num_workers=3, pin_memory=True)\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def show_batch(dl, invert=True):\n    for images, labels in dl:\n        fig, ax = plt.subplots(figsize=(16, 8))\n        ax.set_xticks([]); ax.set_yticks([])\n        data = 1-images if invert else images\n        ax.imshow(make_grid(data, nrow=16).permute(1, 2, 0))\n        break","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# View Batch images","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"We view images with inverted colours and normal colours","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"show_batch(train_dl, invert=True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"show_batch(train_dl, invert=False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Model - Transfer Learning","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"We define accuracy as number of pictures correctly classified or predicted to belong to the accurate class\n","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def accuracy(output, label):\n    _, pred = torch.max(output, dim=1)\n    return torch.tensor(torch.sum(pred == label).item() / len(pred))\n\nclass ImageClassificationBase(nn.Module):\n    def training_step(self, batch):\n        images, targets = batch \n        out = self(images)                  # Generate predictions\n        loss = F.cross_entropy(out, targets) # Calculate loss\n        return loss\n    \n    def validation_step(self, batch):\n        images, targets = batch \n        out = self(images)                    # Generate predictions\n        loss = F.cross_entropy(out, targets)   # Calculate loss\n        acc = accuracy(out, targets)           # Calculate accuracy\n        return {'val_loss': loss.detach(), 'val_acc': acc}\n        \n    def validation_epoch_end(self, outputs):\n        batch_losses = [x['val_loss'] for x in outputs]\n        epoch_loss = torch.stack(batch_losses).mean()   # Combine losses\n        batch_accs = [x['val_acc'] for x in outputs]\n        epoch_acc = torch.stack(batch_accs).mean()      # Combine accuracies\n        return {'val_loss': epoch_loss.item(), 'val_acc': epoch_acc.item()}\n    \n    def epoch_end(self, epoch, result):\n        print(\"Epoch [{}], last_lr: {:.8f}, train_loss: {:.4f}, val_loss: {:.4f}, val_acc: {:.4f}\".format(\n            epoch, result['lrs'][-1], result['train_loss'], result['val_loss'], result['val_acc']))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We will use a Resnet34 model. We use a pretrained model","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"resnet34 = models.resnet34()\nresnet34","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class DogResnet34(ImageClassificationBase):\n    def __init__(self):\n        super().__init__()\n        # Use a pretrained model\n        self.network = models.resnet34(pretrained=True)\n        # Replace last layer\n        num_ftrs = self.network.fc.in_features\n        self.network.fc = nn.Linear(num_ftrs,120)\n    \n    def forward(self, xb):\n        return torch.sigmoid(self.network(xb))\n    \n    def freeze(self):\n        # To freeze the residual layers\n        for param in self.network.parameters():\n            param.require_grad = False\n        for param in self.network.fc.parameters():\n            param.require_grad = True\n    \n    def unfreeze(self):\n        # Unfreeze all layers\n        for param in self.network.parameters():\n            param.require_grad = True","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We recommend using a CUDA or GPU is available;\n\nif not this may be run using a CPU but will take a longer time","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_default_device():\n    \"\"\"Pick GPU if available, else CPU\"\"\"\n    if torch.cuda.is_available():\n        return torch.device('cuda')\n    else:\n        return torch.device('cpu')\n    \ndef to_device(data, device):\n    \"\"\"Move tensor(s) to chosen device\"\"\"\n    if isinstance(data, (list,tuple)):\n        return [to_device(x, device) for x in data]\n    return data.to(device, non_blocking=True)\n\nclass DeviceDataLoader():\n    \"\"\"Wrap a dataloader to move data to a device\"\"\"\n    def __init__(self, dl, device):\n        self.dl = dl\n        self.device = device\n        \n    def __iter__(self):\n        \"\"\"Yield a batch of data after moving it to device\"\"\"\n        for b in self.dl: \n            yield to_device(b, self.device)\n\n    def __len__(self):\n        \"\"\"Number of batches\"\"\"\n        return len(self.dl)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"device = get_default_device()\ndevice","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_dl = DeviceDataLoader(train_dl, device)\nval_dl = DeviceDataLoader(val_dl, device)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Training","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"Now lets get into training the model\n\nWe will use one cycle fit which is now the state of the art for fitting the model.","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"@torch.no_grad()\ndef evaluate(model, val_loader):\n    model.eval()\n    outputs = [model.validation_step(batch) for batch in val_loader]\n    return model.validation_epoch_end(outputs)\n\ndef get_lr(optimizer):\n    for param_group in optimizer.param_groups:\n        return param_group['lr']\n\ndef fit_one_cycle(epochs, max_lr, model, train_loader, val_loader, \n                  weight_decay=0, grad_clip=None, opt_func=torch.optim.SGD):\n    torch.cuda.empty_cache()\n    history = []\n    \n    # Set up cutom optimizer with weight decay\n    optimizer = opt_func(model.parameters(), max_lr, weight_decay=weight_decay)\n    # Set up one-cycle learning rate scheduler\n    sched = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr, epochs=epochs, \n                                                steps_per_epoch=len(train_loader))\n    \n    for epoch in range(epochs):\n        # Training Phase \n        model.train()\n        train_losses = []\n        lrs = []\n        for batch in tqdm(train_loader):\n            loss = model.training_step(batch)\n            train_losses.append(loss)\n            loss.backward()\n            \n            # Gradient clipping\n            if grad_clip: \n                nn.utils.clip_grad_value_(model.parameters(), grad_clip)\n            \n            optimizer.step()\n            optimizer.zero_grad()\n            \n            # Record & update learning rate\n            lrs.append(get_lr(optimizer))\n            sched.step()\n        \n        # Validation phase\n        result = evaluate(model, val_loader)\n        result['train_loss'] = torch.stack(train_losses).mean().item()\n        result['lrs'] = lrs\n        model.epoch_end(epoch, result)\n        history.append(result)\n    return history","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We load the mdodel in to the device","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"model = to_device(DogResnet34(), device)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We see what the default accuracy is","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"history = [evaluate(model, val_dl)]\nhistory","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The default accuracy is around 1% (0.01) as there are 120 breeds","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"model.freeze()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We use the following parameters for the model\n\nThis is what you should focus on. Please change the parameters and see how that improves or decreases the accuracy.\n\nUnderstanding the impact of the number of epochs, maximum learning rate, grad clip and weight decay will help you understand how to tune this and other models\n\n","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"epochs = 5\nmax_lr = 0.0001\ngrad_clip = 0.5\nweight_decay = 1e-4\nopt_func = torch.optim.Adam","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\nstarttime= time.time()\nhistory += fit_one_cycle(epochs, max_lr, model, train_dl, val_dl, \n                         grad_clip=grad_clip, \n                         weight_decay=weight_decay, \n                         opt_func=opt_func)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We store the values\nand unfreeze the model","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"model.unfreeze()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We run the model again\n\n\n\nI am only changing the max lr to a tenth.  You may change the different parameters and even the model here","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\nmax_lr = max_lr/10\n\n#epochs = epochs-1  \n#grad_clip = grad_clip/5\n#weight_decay = weight_decay/10\n\nhistory += fit_one_cycle(epochs, max_lr, model, train_dl, val_dl, \n                         grad_clip=grad_clip, \n                         weight_decay=weight_decay, \n                         opt_func=opt_func)\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"You may tun the model a third time as well","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"#model.unfreeze()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#%%time\n#max_lr = max_lr/10\n\n#history += fit_one_cycle(epochs, max_lr, model, train_dl, val_dl, \n#                         grad_clip=grad_clip, \n#                         weight_decay=weight_decay, \n#                         opt_func=opt_func)\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Note the time to model. This model by default should give you 75% accuracy in around 30 minutes of training with GPU","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"endtime=time.time()\n\nduration=endtime-starttime\ntrain_time=time.strftime('%M:%S', time.gmtime(duration))\ntrain_time","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Lets plot charts on the progress of some parameters ","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def plot_scores(history):\n    scores = [x['val_acc'] for x in history]\n    plt.plot(scores, '-x')\n    plt.xlabel('epoch')\n    plt.ylabel('accuracy')\n    plt.title('accuracy vs. No. of epochs');","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_scores(history)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def plot_losses(history):\n    train_losses = [x.get('train_loss') for x in history]\n    val_losses = [x['val_loss'] for x in history]\n    plt.plot(train_losses, '-bx')\n    plt.plot(val_losses, '-rx')\n    plt.xlabel('epoch')\n    plt.ylabel('loss')\n    plt.legend(['Training', 'Validation'])\n    plt.title('Loss vs. No. of epochs');","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_losses(history)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def plot_lrs(history):\n    lrs = np.concatenate([x.get('lrs', []) for x in history])\n    plt.plot(lrs)\n    plt.xlabel('Batch no.')\n    plt.ylabel('Learning rate')\n    plt.title('Learning Rate vs. Batch no.');","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_lrs(history)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Save and Commit","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"weights_fname = 'dog-resnet.pth'\ntorch.save(model.state_dict(), weights_fname)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"!pip install jovian --upgrade --quiet","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import jovian","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"jovian.reset()\njovian.log_hyperparams(arch='resnet34', \n                       epochs=3*epochs, \n                       lr=max_lr*10, \n                       scheduler='one-cycle', \n                       weight_decay=weight_decay, \n                       grad_clip=grad_clip,\n                       opt=opt_func.__name__)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"jovian.log_metrics(val_loss=history[-1]['val_loss'], \n                   val_score=history[-1]['val_acc'],\n                   train_loss=history[-1]['train_loss'],\n                   time=train_time)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"project_name='dog-breed-classification'","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"jovian.commit(project=project_name, environment=None, outputs=[weights_fname])","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}