{"cells":[{"metadata":{},"cell_type":"markdown","source":"# Introduction\n\n## Simple Implementing Dog/Cat classifier in Pytorch with more than 98% accuracy\n\n### Data Source\n\n- Images are taken unaltered from `25000` images `train.zip` of [Kaggle dataset](https://www.kaggle.com/c/dogs-vs-cats)\n- Images are augmented with random *roation, horizontal-flips, and crops to `224` width and height*\n- Input image R, G, B channels are normalized with means `0.485`, `0.456`, `0.406` and std-dev `0.229`, `0.224` `0.225` respectively as in official docs.\n\n### Model Architecture (Trabsfer Learning)\n\n- Pretrained [Densenet-121](https://pytorch.org/docs/stable/torchvision/models.html#id16) is used. It's layers are freezed and used only as feature embedding **extractor**.\n- Fully connected linear layers used as **classifier** (without dropout or batch-norm)\n\n### Objective Function and Optimizer\n\n- [Negative log Likelihood Loss](https://pytorch.org/docs/stable/generated/torch.nn.NLLLoss.html) is minimized using [Adam](https://pytorch.org/docs/stable/optim.html#torch.optim.Adam) optimizer\n- Only the weights of FC linear units are trained\n- Very Small learning rate is used to avoid distortions"},{"metadata":{},"cell_type":"markdown","source":"# 1. Setup Environent and Load Data"},{"metadata":{"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"!pip install torch==1.4.0 torchvision==0.5.0","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-output":true,"trusted":true},"cell_type":"code","source":"!unzip ../input/dogs-vs-cats/train.zip","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"\"\"\"\nAll necessary imports\n\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nimport torch\nimport torch.nn.functional as F\nfrom torch import nn, optim\nfrom torchvision import transforms, datasets, models\n\nfrom PIL import Image\nfrom collections import OrderedDict\nimport os, shutil, random, time\nfrom tqdm import tqdm","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\ndevice","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 2. Train Test Split"},{"metadata":{"trusted":true},"cell_type":"code","source":"\"\"\"\ncheck duplicates\n\"\"\"\n\nall_files = os.listdir('train')\n\nprint(len(all_files))\nprint(len(np.unique(all_files)))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"> No duplicate image names"},{"metadata":{"trusted":true},"cell_type":"code","source":"\"\"\"\nshuffle and organize into train/test dirs\n\"\"\"\n\nrandom.shuffle(all_files)\n\ndog_files = []\ncat_files = []\nerr_cntr = 0\nfor file in all_files:\n    if 'dog' in file:\n        dog_files.append(file)\n    elif 'cat' in file:\n        cat_files.append(file)\n    else:\n        err_cntr = 0\n        \nprint(\"=\"*60)\nprint('cat images:', len(cat_files))\nprint('dog images:', len(dog_files))\nprint('other files:', err_cntr)\nprint(\"=\"*60)\n\nplt.title('Data distribution')\nplt.ylabel('No. of images')\nplt.bar(\n    ['dog images', 'cat_images'],\n    [len(dog_files), len(cat_files)]\n)\nplt.show()\n\n\n\n\"\"\"\nsplit and move to train/test dirs\n\"\"\"\n\nratio = 0.8\nlim = int(0.8*len(dog_files)) # prefectly balanced i.e len(dog_files) = len(cat_files)\n\nfor _dir in ['traindata', 'testdata']:\n    os.makedirs(f\"{_dir}/dog\", exist_ok=True)    \n    os.makedirs(f\"{_dir}/cat\", exist_ok=True)\n    \ntrain_dog_files = dog_files[:lim]\ntest_dog_files  = dog_files[lim:]\ntrain_cat_files = cat_files[:lim]\ntest_cat_files  = cat_files[lim:]\n\nfor file in test_dog_files:\n    shutil.move(f'train/{file}', f'testdata/dog/{file}')\nfor file in test_cat_files:\n    shutil.move(f'train/{file}', f'testdata/cat/{file}')\nfor file in train_dog_files:\n    shutil.move(f'train/{file}', f'traindata/dog/{file}')\nfor file in train_cat_files:\n    shutil.move(f'train/{file}', f'traindata/cat/{file}')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"for _dir in ['traindata', 'testdata']:\n    print(f'+ {_dir}')\n    dog_cnt = len(os.listdir(f'{_dir}/dog/'))\n    cat_cnt = len(os.listdir(f'{_dir}/cat/'))\n    print('\\t+ Dog images: ', dog_cnt)    \n    print('\\t+ Cat images: ', cat_cnt)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# 3. Data Preprocessing and Augmentation"},{"metadata":{"trusted":true},"cell_type":"code","source":"\"\"\"\nData loader w/ augmentation and Preprocession \n\"\"\"\n\ntrain_transforms = transforms.Compose([transforms.RandomRotation(30),\n                                       transforms.RandomResizedCrop(224),\n                                       transforms.RandomHorizontalFlip(),\n                                       transforms.ToTensor(),\n                                       transforms.Normalize([0.485, 0.456, 0.406],\n                                                            [0.229, 0.224, 0.225])])\n\ntest_transforms = transforms.Compose([transforms.Resize(255),\n                                      transforms.CenterCrop(224),\n                                      transforms.ToTensor(),\n                                      transforms.Normalize([0.485, 0.456, 0.406],\n                                                           [0.229, 0.224, 0.225])])\n\n\ntrain_data = datasets.ImageFolder('traindata/', transform=train_transforms)\ntest_data = datasets.ImageFolder('testdata/', transform=test_transforms)\ntrainloader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)\ntestloader = torch.utils.data.DataLoader(test_data, batch_size=64)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"x, y = next(iter(trainloader))\nprint(x.shape, y.shape)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"\"\"\"\nvisualize preprocessed images\n\"\"\"\n\nrow, col = 1, 3\nfig, axarr = plt.subplots(row, col)\naxarr = axarr.flatten()\n\nfor i in range(row*col):\n    axarr[i].set_title('Dog' if y[i] == 1 else 'Cat')\n    axarr[i].imshow(x[i].numpy().transpose((1, 2, 0)).astype('uint8'))\n    axarr[i].axis('off')\n\nplt.show()\n\n# Note: preprocessed as in official docs. ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"> - *Images may look wierd and unintuitive but preprocessed as in official docs*\n> - *Pretrained model need images in same distribution to extract better features*\n> - *Target `0` corresponds to `Cat` and `1` to `Dog`*"},{"metadata":{},"cell_type":"markdown","source":"# 4. Model Architecture for Transfer Learning"},{"metadata":{"trusted":true},"cell_type":"code","source":"\"\"\"\ndefine architecture\n\"\"\"\nmodel = models.densenet121(pretrained=True).to(device)\n\n\"\"\"\nfreeze extractor and append classifier\n\"\"\"\nfor param in model.parameters():\n    param.requires_grad = False\n\nfrom collections import OrderedDict\nclassifier = nn.Sequential(\n    OrderedDict([\n        # ----------------------------\n        ('fc1', nn.Linear(1024, 512)),\n        ('relu1', nn.ReLU()),\n        # ----------------------------\n        ('fc2', nn.Linear(512,256)),\n        ('relu2', nn.ReLU()),\n        # ----------------------------\n        ('fc3', nn.Linear(256, 2)),\n        ('output', nn.LogSoftmax(dim=1))\n        # ----------------------------\n    ]))\n    \nmodel.classifier = classifier.to(device)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"\"\"\"\nDisplay architecture\n\"\"\"\n\nmodel","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"\"\"\"\ntest behaviour\n\"\"\"\n\nimg = torch.rand(64, 3, 224, 224).to(device)\nmodel(img).shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Start Training"},{"metadata":{"trusted":true},"cell_type":"code","source":"\"\"\"\nConfigure training parameters\n\"\"\"\n\ncriterion = nn.NLLLoss().to(device)\noptimizer = optim.Adam(model.classifier.parameters(), lr=0.0001)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Note:** Need small learning rate as we are fine-tuning"},{"metadata":{"trusted":true},"cell_type":"code","source":"\"\"\"\nStart training\n\"\"\"\n\n# history for post-training visualisation\nclass hist:\n    traininglosses = []\n    testinglosses = []\n    testaccuracy = []\n    totalsteps = []\n\nepochs = 1\nsteps = 0\n\nrunning_loss = 0\nprint_every = 50\n\nfor epoch in range(epochs):\n    for inputs, labels in trainloader:\n        steps += 1\n        inputs, labels = inputs.to(device), labels.to(device)\n            \n        optimizer.zero_grad()\n        \n        # forward prop\n        logps = model.forward(inputs)\n        loss = criterion(logps, labels)\n        loss.backward()\n        optimizer.step()\n\n        # avg taken every `print_every` step\n        # and logged\n        running_loss += loss.item()\n        \n        # Evaluate on testloader\n        # and log after every `print_every` step\n        if steps % print_every == 0:\n            test_loss = 0\n            accuracy = 0\n            model.eval()\n            with torch.no_grad():\n                for inputs, labels in testloader:\n                    inputs, labels = inputs.to(device), labels.to(device)\n                    # predictions in log scale\n                    # Take exponents and get preds\n                    # using `topk` which returns \n                    # top-preds and respective top-classes\n                    logps = model.forward(inputs)\n                    # Loss and simple categorical accuracy \n                    # accumulated for avg taken to be taken \n                    # for whole epoch\n                    batch_loss = criterion(logps, labels)\n                    test_loss += batch_loss.item()\n                    ps = torch.exp(logps)\n                    _, top_class = ps.topk(1, dim=1)\n                    equals = top_class == labels.view(*top_class.shape)\n                    accuracy += torch.mean(equals.type(torch.FloatTensor)).item()\n            \n            # Change back to train after evaluation\n            model.train()\n            running_loss = 0\n            \n            # ------------------------------------------------------------------\n            # log performance ans populate history for vsiualisation\n            hist.traininglosses.append(running_loss/print_every)\n            hist.testinglosses.append(test_loss/len(testloader))\n            hist.testaccuracy.append(accuracy/len(testloader))\n            hist.totalsteps.append(steps)\n            print(f\"Epoch {epoch+1}of{epochs} \"\n                  f\"Step {steps} \\t\"\n                  f\"Train loss: {running_loss/print_every:.3f}.. \"\n                  f\"Test loss: {test_loss/len(testloader):.3f}.. \"\n                  f\"Test accuracy: {accuracy/len(testloader):.3f}.. \")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"> **Note:** Training for only 1 epoch(significant performance). Not implementing **early stopping** as model needs to see whole data at least once."},{"metadata":{},"cell_type":"markdown","source":"# Visualize Training Process"},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.plot(hist.totalsteps, hist.traininglosses, label='Train Loss')\nplt.plot(hist.totalsteps, hist.testinglosses, label='Test Loss')\nplt.plot(hist.totalsteps, hist.testaccuracy, label='Test Accuracy')\n\nplt.title('First epoch')\nplt.xlabel('Steps')\n\nplt.legend()\nplt.grid()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Finally,\n\nWe have more than `98`% accuate model (on unseen dataset) for classification of dogs and cats."}],"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}