{"cells": [{"metadata": {"_uuid": "958731b43dd6cbe5d9ba824a7ee3fb4895fbde97", "_cell_guid": "764fe135-c09e-9769-5794-500867154d93"}, "outputs": [], "cell_type": "markdown", "source": ["Starter EDA and ConvNet implementation using Keras. \n", "\n", "Inspiration for this notebook comes from this [Keras blog post](https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html) and the [VGG ConvNet paper](https://arxiv.org/pdf/1409.1556.pdf). \n"], "execution_count": null}, {"metadata": {"_uuid": "b87d662e87c57e80822f7fba53cd516af951b414", "_cell_guid": "3d458c15-e131-f3c4-f756-843d6454bb37"}, "outputs": [], "cell_type": "code", "source": ["import os, cv2, random\n", "import numpy as np\n", "import pandas as pd\n", "\n", "import matplotlib.pyplot as plt\n", "from matplotlib import ticker\n", "import seaborn as sns\n", "%matplotlib inline \n", "\n", "from keras.models import Sequential\n", "from keras.layers import Input, Dropout, Flatten, Convolution2D, MaxPooling2D, Dense, Activation\n", "from keras.optimizers import RMSprop\n", "from keras.callbacks import ModelCheckpoint, Callback, EarlyStopping\n", "from keras.utils import np_utils"], "execution_count": null}, {"metadata": {"_uuid": "263f2e93b4b8d1bb7f9223660357e78dfb6b1fb9", "_cell_guid": "04681981-55ac-7820-a0ae-38f98c851c39"}, "outputs": [], "cell_type": "markdown", "source": ["## Preparing the Data\n", "\n", "This function resizes the images to 64x64 and samples 2000 images (8%) of the data to run efficiently as a Kaggle Kernel. I also separated cats and dogs for exploratory analysis. "], "execution_count": null}, {"metadata": {"_uuid": "73a75f63bb2269281025db5247daedc2ec0d58f9", "_cell_guid": "663d335e-1b84-a8cb-19ee-8f04839cf4e5"}, "outputs": [], "cell_type": "code", "source": ["TRAIN_DIR = '../input/train/'\n", "TEST_DIR = '../input/test/'\n", "\n", "ROWS = 64\n", "COLS = 64\n", "CHANNELS = 3\n", "\n", "train_images = [TRAIN_DIR+i for i in os.listdir(TRAIN_DIR)] # use this for full dataset\n", "train_dogs =   [TRAIN_DIR+i for i in os.listdir(TRAIN_DIR) if 'dog' in i]\n", "train_cats =   [TRAIN_DIR+i for i in os.listdir(TRAIN_DIR) if 'cat' in i]\n", "\n", "test_images =  [TEST_DIR+i for i in os.listdir(TEST_DIR)]\n", "\n", "\n", "# slice datasets for memory efficiency on Kaggle Kernels, delete if using full dataset\n", "train_images = train_dogs[:1000] + train_cats[:1000]\n", "random.shuffle(train_images)\n", "test_images =  test_images[:25]\n", "\n", "def read_image(file_path):\n", "    img = cv2.imread(file_path, cv2.IMREAD_COLOR) #cv2.IMREAD_GRAYSCALE\n", "    return cv2.resize(img, (ROWS, COLS), interpolation=cv2.INTER_CUBIC)\n", "\n", "\n", "def prep_data(images):\n", "    count = len(images)\n", "    data = np.ndarray((count, CHANNELS, ROWS, COLS), dtype=np.uint8)\n", "\n", "    for i, image_file in enumerate(images):\n", "        image = read_image(image_file)\n", "        data[i] = image.T\n", "        if i%250 == 0: print('Processed {} of {}'.format(i, count))\n", "    \n", "    return data\n", "\n", "train = prep_data(train_images)\n", "test = prep_data(test_images)\n", "\n", "print(\"Train shape: {}\".format(train.shape))\n", "print(\"Test shape: {}\".format(test.shape))"], "execution_count": null}, {"metadata": {"_uuid": "3b7cea0cb77aabc1ef6e12061a309a574fe13880", "_cell_guid": "ed0fc95a-53bc-3517-245d-cf1f5122bc2d"}, "outputs": [], "cell_type": "markdown", "source": ["### Generating the Labels\n", "\n", "We're dealing with a binary classification problem here - (1) dog (0) cat. The lables can be created by looping over the file names in the train directory. It's nice to see the training data is perfectly balanced. "], "execution_count": null}, {"metadata": {"_uuid": "938eed610685a8f96856265b24b1fe8d6bbacbf9", "_cell_guid": "c0f2fbf6-8d78-7d42-6579-5486a36c1e60"}, "outputs": [], "cell_type": "code", "source": ["labels = []\n", "for i in train_images:\n", "    if 'dog' in i:\n", "        labels.append(1)\n", "    else:\n", "        labels.append(0)\n", "\n", "sns.countplot(labels)\n", "sns.plt.title('Cats and Dogs')"], "execution_count": null}, {"metadata": {"_uuid": "266cbff1ba482d5ac20dacd9e2c0f4012d059f52", "_cell_guid": "4bb0bd2a-0512-aa72-c628-5b6ac946d97c"}, "outputs": [], "cell_type": "markdown", "source": ["### Checking out Cats and Dogs\n", "A quick side-by-side comparison of the animals."], "execution_count": null}, {"metadata": {"_uuid": "5866efecca0bce6336d4ffbcc11aef66e90036e3", "_cell_guid": "0b7c79ae-6543-2ed8-e3f5-af4f5beb9cf7"}, "outputs": [], "cell_type": "code", "source": ["def show_cats_and_dogs(idx):\n", "    cat = read_image(train_cats[idx])\n", "    dog = read_image(train_dogs[idx])\n", "    pair = np.concatenate((cat, dog), axis=1)\n", "    plt.figure(figsize=(10,5))\n", "    plt.imshow(pair)\n", "    plt.show()\n", "    \n", "for idx in range(0,5):\n", "    show_cats_and_dogs(idx)"], "execution_count": null}, {"metadata": {"_uuid": "96cd813f3d56e38fc0b88e216a8d2311f3e0c4ac", "_cell_guid": "599a11ba-aaa4-8266-03ea-d816518e2a81"}, "outputs": [], "cell_type": "markdown", "source": ["### Your Average Cat and Dog Photo\n", "\n", "Just for fun, the mean pixel values for cats and dogs. I can almost see a resemblance, a ghost, if you will..."], "execution_count": null}, {"metadata": {"_uuid": "ce978c457f596d22af36e45eb81e43426781b789", "_cell_guid": "7d3c6eb7-c370-9752-67c8-c780bab14abe"}, "outputs": [], "cell_type": "code", "source": ["dog_avg = np.array([dog[0].T for i, dog in enumerate(train) if labels[i]==1]).mean(axis=0)\n", "plt.imshow(dog_avg)\n", "plt.title('Your Average Dog')"], "execution_count": null}, {"metadata": {"_uuid": "660f5c4554cc3517819acf7d5724d5facf5e82a3", "_cell_guid": "a5a93063-de92-18ce-b48f-68a193e90002"}, "outputs": [], "cell_type": "code", "source": ["cat_avg = np.array([cat[0].T for i, cat in enumerate(train) if labels[i]==0]).mean(axis=0)\n", "plt.imshow(cat_avg)\n", "plt.title('Your Average Cat')"], "execution_count": null}, {"metadata": {"_uuid": "dc6b5f80e313c286f58d60cb48a843c7d736d16e", "_cell_guid": "51e403b6-bcfc-b4fa-3770-9850cc86bae3"}, "outputs": [], "cell_type": "markdown", "source": ["## CatdogNet-16\n", "\n", "A scaled down version of the VGG-16, with a few notable changes.\n", "\n", "- Number of convolution filters cut in half, fully connected (dense) layers scaled down. \n", "- Optimizer changed to `RMSprop`. \n", "- Output layer activation set to `sigmoid` for binary crossentropy. \n", "- Some layers commented out for efficiency.\n", "\n", "The full network takes about 80s per epoch on a GTX1070 (or 2hr+ on CPU) on the full dataset.  (This script only trains on 8% of the 25K images. )"], "execution_count": null}, {"metadata": {"_uuid": "1dc0291ec07d6e9228425a2fb6adbddc351e3d29", "_cell_guid": "4c477612-41d3-3112-5176-3c4ad9633080"}, "outputs": [], "cell_type": "code", "source": ["optimizer = RMSprop(lr=1e-4)\n", "objective = 'binary_crossentropy'\n", "\n", "\n", "def catdog():\n", "    \n", "    model = Sequential()\n", "\n", "    model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape=(3, ROWS, COLS), activation='relu'))\n", "    model.add(Convolution2D(32, 3, 3, border_mode='same', activation='relu'))\n", "    model.add(MaxPooling2D(pool_size=(2, 2)))\n", "\n", "    model.add(Convolution2D(64, 3, 3, border_mode='same', activation='relu'))\n", "    model.add(Convolution2D(64, 3, 3, border_mode='same', activation='relu'))\n", "    model.add(MaxPooling2D(pool_size=(2, 2)))\n", "    \n", "    model.add(Convolution2D(128, 3, 3, border_mode='same', activation='relu'))\n", "    model.add(Convolution2D(128, 3, 3, border_mode='same', activation='relu'))\n", "    model.add(MaxPooling2D(pool_size=(2, 2)))\n", "    \n", "    model.add(Convolution2D(256, 3, 3, border_mode='same', activation='relu'))\n", "    model.add(Convolution2D(256, 3, 3, border_mode='same', activation='relu'))\n", "#     model.add(Convolution2D(256, 3, 3, border_mode='same', activation='relu'))\n", "    model.add(MaxPooling2D(pool_size=(2, 2)))\n", "\n", "#     model.add(Convolution2D(256, 3, 3, border_mode='same', activation='relu'))\n", "#     model.add(Convolution2D(256, 3, 3, border_mode='same', activation='relu'))\n", "#     model.add(Convolution2D(256, 3, 3, border_mode='same', activation='relu'))\n", "#     model.add(MaxPooling2D(pool_size=(2, 2)))\n", "\n", "    model.add(Flatten())\n", "    model.add(Dense(256, activation='relu'))\n", "    model.add(Dropout(0.5))\n", "    \n", "    model.add(Dense(256, activation='relu'))\n", "    model.add(Dropout(0.5))\n", "\n", "    model.add(Dense(1))\n", "    model.add(Activation('sigmoid'))\n", "\n", "    model.compile(loss=objective, optimizer=optimizer, metrics=['accuracy'])\n", "    return model\n", "\n", "\n", "model = catdog()"], "execution_count": null}, {"metadata": {"_uuid": "08b75a410ad111c44b606a74d2ce5c9c67f9a0a4", "_cell_guid": "b7fbf156-2268-62ce-5fed-52a09af5e6f7"}, "outputs": [], "cell_type": "markdown", "source": ["### Train and Predict\n", "\n", "I'm using Keras's early stopping callback to end training when the validation loss stops improving, otherwise the model will overfit. I will also be tracking the loss history on each epoch to visualize the overfitting trend. \n", "\n", "Note: A slice of 1000 images was used to fit the model for CPU efficency. The model's perfrmance improves significantly when used on the entire dataset. "], "execution_count": null}, {"metadata": {"_uuid": "e7fa4720e958081664a8f41731e433a296fbcc14", "_cell_guid": "ca613169-4b41-e9d5-27c7-6629f8e035c8"}, "outputs": [], "cell_type": "code", "source": ["nb_epoch = 10\n", "batch_size = 16\n", "\n", "## Callback for loss logging per epoch\n", "class LossHistory(Callback):\n", "    def on_train_begin(self, logs={}):\n", "        self.losses = []\n", "        self.val_losses = []\n", "        \n", "    def on_epoch_end(self, batch, logs={}):\n", "        self.losses.append(logs.get('loss'))\n", "        self.val_losses.append(logs.get('val_loss'))\n", "\n", "early_stopping = EarlyStopping(monitor='val_loss', patience=3, verbose=1, mode='auto')        \n", "        \n", "def run_catdog():\n", "    \n", "    history = LossHistory()\n", "    model.fit(train, labels, batch_size=batch_size, nb_epoch=nb_epoch,\n", "              validation_split=0.25, verbose=0, shuffle=True, callbacks=[history, early_stopping])\n", "    \n", "\n", "    predictions = model.predict(test, verbose=0)\n", "    return predictions, history\n", "\n", "predictions, history = run_catdog()"], "execution_count": null}, {"metadata": {"_uuid": "ceaf0004286374b4e038773ac57c815af59bf505", "_cell_guid": "6266d01c-9d4c-a1dc-8cb9-ce7b3107b536"}, "outputs": [], "cell_type": "code", "source": ["loss = history.losses\n", "val_loss = history.val_losses\n", "\n", "plt.xlabel('Epochs')\n", "plt.ylabel('Loss')\n", "plt.title('VGG-16 Loss Trend')\n", "plt.plot(loss, 'blue', label='Training Loss')\n", "plt.plot(val_loss, 'green', label='Validation Loss')\n", "plt.xticks(range(0,nb_epoch)[0::2])\n", "plt.legend()\n", "plt.show()"], "execution_count": null}, {"metadata": {"_uuid": "b18a84022176db3b467e4872305eda4181c5b786", "_cell_guid": "d9382e7d-31db-0d75-6876-aaccb126e4f6"}, "outputs": [], "cell_type": "markdown", "source": ["## How'd We Do?\n", "\n", "I'm pretty sure I can distinguish a cat from a dog 100% of the time, but how confident is the model?...\n", "\n", "Tip: Run on the full dataset with a GPU for a LB logloss of ~0.4 and accuracy at approx 90%. "], "execution_count": null}, {"metadata": {"_uuid": "382cf861de3385c505e64ccb6eda1d28802593b6", "_cell_guid": "c665d01b-a5a7-2f7e-e5d4-09e108920e40"}, "outputs": [], "cell_type": "code", "source": ["for i in range(0,10):\n", "    if predictions[i, 0] >= 0.5: \n", "        print('I am {:.2%} sure this is a Dog'.format(predictions[i][0]))\n", "    else: \n", "        print('I am {:.2%} sure this is a Cat'.format(1-predictions[i][0]))\n", "        \n", "    plt.imshow(test[i].T)\n", "    plt.show()"], "execution_count": null}], "nbformat_minor": 0, "metadata": {"_is_fork": false, "_change_revision": 0, "language_info": {"mimetype": "text/x-python", "nbconvert_exporter": "python", "codemirror_mode": {"version": 3, "name": "ipython"}, "file_extension": ".py", "name": "python", "pygments_lexer": "ipython3", "version": "3.5.2"}, "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}}, "nbformat": 4}