{"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":"# Cats versus Dogs classifier","metadata":{}},{"cell_type":"markdown","source":"According to the documentation of this competitions, we have: the zip files train.zip and test1.zip that contain RGB images of cats and dogs. Dogs are labelled as 1 and cats as 0.\n\nThere is 25,000 images of cats and dogs. We will build a custom CNN (Convolutional Neural Network) or also called convenet, using Tensorflow and Keras API. To improve our result we will use a preprocessing method called data augmentation.","metadata":{}},{"cell_type":"code","source":"# Imports\nimport os\nimport random\nimport warnings\nimport zipfile\n\n# Filter unnecessary warnings\nwarnings.filterwarnings(\"ignore\")\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport tensorflow as tf\n\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom sklearn.model_selection import train_test_split","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2023-08-02T09:33:16.352243Z","iopub.execute_input":"2023-08-02T09:33:16.352618Z","iopub.status.idle":"2023-08-02T09:33:24.138165Z","shell.execute_reply.started":"2023-08-02T09:33:16.352586Z","shell.execute_reply":"2023-08-02T09:33:24.137125Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We define some constants and functions that we will use later in the notebook.","metadata":{}},{"cell_type":"code","source":"# Constants\nZIP_PATH = '/kaggle/input/dogs-vs-cats/train.zip'\nTEST_PATH = '/kaggle/input/dogs-vs-cats/test1.zip'\n\nNUM_TEST_IMG = 10\nIMG_SIZE = (128, 128)\nLABELS_NAME = {0 : 'cat', 1 : 'dog'}","metadata":{"execution":{"iopub.status.busy":"2023-08-02T10:27:31.433857Z","iopub.execute_input":"2023-08-02T10:27:31.434556Z","iopub.status.idle":"2023-08-02T10:27:31.439626Z","shell.execute_reply.started":"2023-08-02T10:27:31.434520Z","shell.execute_reply":"2023-08-02T10:27:31.438479Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Fonctions\ndef show_image(images: np.array, labels: np.array, names: dict = LABELS_NAME) -> None:\n    \"\"\"\n    This function take the a random image from the \n    the array that contains images, and plot it,\n    with as title the class label of the image.\n    \"\"\"\n    # Get a random index\n    idx = random.randint(0, len(images) - 1)\n    \n    # Plot a random image\n    plt.figure(figsize=(6,6))\n    plt.imshow(images[idx])\n    plt.title(f\"Class: {names[labels[idx]]}\")\n    plt.axis('off')\n    plt.show()\n    \ndef predict_image(images: np.array, predictions: np.array, names: dict = LABELS_NAME) -> None:\n    \"\"\"\n    This function take the images from the array\n    that contains test images, and plot them,\n    with as title the predicted class of the image.\n    \"\"\"\n    # Plot a random image\n    plt.figure(figsize=(20, 8))\n    for idx, (image, prediction) in enumerate(zip(images, predictions), start=1):\n        plt.subplot(2, len(images) // 2, idx)\n        plt.imshow(image)\n        plt.title(f\"Predicted class: {names[prediction]}\")\n        plt.axis('off')\n    plt.show()\n    \ndef extract_img(path: str) -> np.array:\n    \"\"\"\n    This function unzip a folder that contains\n    images, load these images, resize them, and\n    convert them into a numpy array. It also\n    gets the labels from the path and create an\n    array of them.\n    \"\"\"\n    # Define lists to store images and labels\n    img_list = []\n    labels = []\n    \n    # Open the zipfile\n    zip_ref = zipfile.ZipFile(path, 'r')\n    # Get the list of files in the zipfile\n    images = zip_ref.namelist()\n    \n    for image in images:\n        if image.endswith('.jpg'):\n            # Load and resize the image\n            img_path = zip_ref.extract(image)\n            img = tf.keras.utils.load_img(img_path).resize(IMG_SIZE)\n            # Remove the path\n            os.remove(img_path)\n            \n            # Convert the image into an array\n            img_list.append(tf.keras.utils.img_to_array(img, dtype=np.uint8))\n            \n            # Get the label from the image name\n            labels.append(0) if 'cat' in image else labels.append(1)\n            \n    zip_ref.close()\n    \n    # Shuffle the train\n    combined_list = list(zip(img_list, labels))\n    random.shuffle(combined_list)\n    img_list, labels = zip(*combined_list)\n    \n    # Convert images and labels into an array\n    img_array = np.array(img_list, dtype=np.uint8)\n    labels_array = np.array(labels, dtype=np.uint8)\n    \n    return img_array, labels_array\n\ndef extract_test(path: str, num_image: int = NUM_TEST_IMG) -> np.array:\n    \"\"\"\n    This function unzip a folder that contains\n    images, load random images, resize them, \n    and convert them into a numpy array.\n    \"\"\"\n    # Define lists to store images and labels\n    img_list = []\n\n    # Open the zipfile\n    zip_ref = zipfile.ZipFile(path, 'r')\n    # Get the list of files in the zipfile\n    images = zip_ref.namelist()\n    \n    # Create the random index\n    idx = random.sample(range(len(images)), NUM_TEST_IMG)\n    \n    for i in idx:\n        # Get a random image\n        image = images[i]\n        if image.endswith('.jpg'):\n            # Load and resize the image\n            img_path = zip_ref.extract(image)\n            img = tf.keras.utils.load_img(img_path).resize(IMG_SIZE)\n            # Remove the path\n            os.remove(img_path)\n            \n            # Convert the image into an array\n            img_list.append(tf.keras.utils.img_to_array(img, dtype=np.uint8))\n            \n    zip_ref.close()\n    \n    # Convert images and labels into an array\n    img_array = np.array(img_list, dtype=np.uint8)\n    \n    return img_array","metadata":{"execution":{"iopub.status.busy":"2023-08-02T10:31:02.154467Z","iopub.execute_input":"2023-08-02T10:31:02.154833Z","iopub.status.idle":"2023-08-02T10:31:02.174034Z","shell.execute_reply.started":"2023-08-02T10:31:02.154804Z","shell.execute_reply":"2023-08-02T10:31:02.172817Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Preprocessing","metadata":{}},{"cell_type":"markdown","source":"First we extract the data using the `extract_img` function defined ealier. This function allows us to extract each image from the zip file, resize it, and convert it to a numpy format. It also allows us to get the label using the name of the file (note that it shuffles the train set).","metadata":{}},{"cell_type":"code","source":"# Extract the data into a train and a test set\nX_train, y_train = extract_img(ZIP_PATH)","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:33:24.172982Z","iopub.execute_input":"2023-08-02T09:33:24.173786Z","iopub.status.idle":"2023-08-02T09:34:47.660397Z","shell.execute_reply.started":"2023-08-02T09:33:24.173761Z","shell.execute_reply":"2023-08-02T09:34:47.659387Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Now that the data is extracted, we can verify some assumptions about it; like that we have the same number of images and labels, or the same number of dogs and cats images. For this we can use the `assert` key word, it returns an assertion error if the condition is not true, otherwise it returns nothing.","metadata":{}},{"cell_type":"code","source":"print(f'Number of training images: {len(X_train)}')\n\n# Check that there is the same number of images and labels\nassert len(X_train) == len(y_train)","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:34:47.664302Z","iopub.execute_input":"2023-08-02T09:34:47.666375Z","iopub.status.idle":"2023-08-02T09:34:47.674124Z","shell.execute_reply.started":"2023-08-02T09:34:47.666346Z","shell.execute_reply":"2023-08-02T09:34:47.673171Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Check that there is the same number of cat and dog images\n_, count = np.unique(y_train, return_counts=True)\nassert count[0]==count[1]","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:34:47.675744Z","iopub.execute_input":"2023-08-02T09:34:47.676566Z","iopub.status.idle":"2023-08-02T09:34:47.686403Z","shell.execute_reply.started":"2023-08-02T09:34:47.676534Z","shell.execute_reply":"2023-08-02T09:34:47.685357Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Nice! It seems that everything is ok, we can display a random image with its label to see a little bit more about the dataset.","metadata":{}},{"cell_type":"code","source":"show_image(X_train, y_train)","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:34:47.687963Z","iopub.execute_input":"2023-08-02T09:34:47.688396Z","iopub.status.idle":"2023-08-02T09:34:47.870710Z","shell.execute_reply.started":"2023-08-02T09:34:47.688365Z","shell.execute_reply":"2023-08-02T09:34:47.869879Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Now we can split the data into a train and validation set. Since we have a large amount of data, we can use only 10% of the data for the validation. Specify the stratify attribute of the method `train_test_split` allow to keep the same class proportion in both sets.","metadata":{}},{"cell_type":"code","source":"# Create a train and a validation set\nX_train, X_val, y_train, y_val = train_test_split(\n    X_train, \n    y_train, \n    train_size=0.9,\n    stratify=y_train,\n    random_state=42\n)\n\nprint(X_train.shape)\nprint(X_val.shape)","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:34:47.871902Z","iopub.execute_input":"2023-08-02T09:34:47.872953Z","iopub.status.idle":"2023-08-02T09:34:48.238048Z","shell.execute_reply.started":"2023-08-02T09:34:47.872902Z","shell.execute_reply":"2023-08-02T09:34:48.236991Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Data Augmentation","metadata":{}},{"cell_type":"markdown","source":"Data augmentation is a method to increase the size of the training dataset by applying various transformations to the existing images. This helps to prevent overfitting and improve the generalization ability of the trained model.\n\nCommon image augmentations include random cropping, flipping, rotating, scaling, and adjusting brightness, contrast, and color. By applying these transformations to the images, we can create new variations of the same image, which can help the model learn to recognize the object despite changes in the appearance or orientation of the object.","metadata":{}},{"cell_type":"code","source":"# Instantiate the ImageDataGenerator class for the training\ntrain_datagen = ImageDataGenerator(\n    rescale=1.0/255.0,\n    rotation_range=60,\n    width_shift_range=0.3,\n    height_shift_range=0.3,\n    shear_range=0.1,\n    zoom_range=0.2,\n    horizontal_flip=True,\n    fill_mode='nearest'\n)\n\n# Get the data from the train arrays\ntrain_generator = train_datagen.flow(\n    x=X_train,\n    y=y_train,\n    batch_size=32\n)\n\n# Repeat the same process for the validation\nvalidation_datagen = ImageDataGenerator(\n    rescale=1.0/255.0\n)\n\nvalidation_generator = validation_datagen.flow(\n    x=X_val,\n    y=y_val,\n    batch_size=32\n)","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:34:48.239857Z","iopub.execute_input":"2023-08-02T09:34:48.240258Z","iopub.status.idle":"2023-08-02T09:34:49.408309Z","shell.execute_reply.started":"2023-08-02T09:34:48.240223Z","shell.execute_reply":"2023-08-02T09:34:49.407327Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Free some RAM\ndel X_train, X_val, y_train, y_val","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:34:49.411909Z","iopub.execute_input":"2023-08-02T09:34:49.412248Z","iopub.status.idle":"2023-08-02T09:34:49.418801Z","shell.execute_reply.started":"2023-08-02T09:34:49.412223Z","shell.execute_reply":"2023-08-02T09:34:49.417726Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Model","metadata":{}},{"cell_type":"markdown","source":"Now we can create a model using the sequential API. We use a basic CNN here. Let's train it with 20 epochs using the validation set to control overfitting.","metadata":{}},{"cell_type":"code","source":"# Create a sequential model\nmodel = tf.keras.Sequential([\n    tf.keras.layers.Conv2D(16, 3, activation='relu', input_shape=IMG_SIZE + (3,)),\n    tf.keras.layers.MaxPooling2D(2),\n    tf.keras.layers.Conv2D(32, 3, activation='relu'),\n    tf.keras.layers.MaxPooling2D(2),\n    tf.keras.layers.Conv2D(64, 3, activation='relu'),\n    tf.keras.layers.MaxPooling2D(2),\n    tf.keras.layers.Conv2D(128, 3, activation='relu'),\n    tf.keras.layers.MaxPooling2D(2),\n    tf.keras.layers.Flatten(),\n    tf.keras.layers.Dense(512, activation='relu'),\n    tf.keras.layers.Dense(1, activation='sigmoid')\n])\n\nmodel.summary()","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:34:49.420472Z","iopub.execute_input":"2023-08-02T09:34:49.420820Z","iopub.status.idle":"2023-08-02T09:34:51.910912Z","shell.execute_reply.started":"2023-08-02T09:34:49.420789Z","shell.execute_reply":"2023-08-02T09:34:51.910211Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Compile the model\nmodel.compile(\n    optimizer='adam',\n    loss='binary_crossentropy',\n    metrics=['accuracy']\n)\n\nhistory = model.fit(\n    train_generator,\n    epochs=20,\n    validation_data=validation_generator\n)","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:34:51.911923Z","iopub.execute_input":"2023-08-02T09:34:51.912273Z","iopub.status.idle":"2023-08-02T09:37:15.114889Z","shell.execute_reply.started":"2023-08-02T09:34:51.912241Z","shell.execute_reply":"2023-08-02T09:37:15.113790Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can see that we have pretty good result. We can plot the loss and the accuracy of the train and validation set.","metadata":{}},{"cell_type":"code","source":"# Get loss and accuracy from the training\nacc = history.history['accuracy']\nval_acc = history.history['val_accuracy']\nloss = history.history['loss']\nval_loss = history.history['val_loss']\n\nepochs=range(len(acc)) # Get number of epochs\n\nplt.figure(figsize=(12,5))\nplt.plot(epochs, acc, label=\"Training accuracy\")\nplt.plot(epochs, val_acc, label=\"Validation accuracy\")\nplt.title('Training and validation accuracy')\nplt.legend()\nplt.show()\n\nprint(\"\")\n\nplt.figure(figsize=(12,5))\nplt.plot(epochs, loss, label=\"Training loss\")\nplt.plot(epochs, val_loss, label=\"Validation loss\")\nplt.title('Training and validation loss')\nplt.legend()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:37:15.116695Z","iopub.execute_input":"2023-08-02T09:37:15.117169Z","iopub.status.idle":"2023-08-02T09:37:15.684785Z","shell.execute_reply.started":"2023-08-02T09:37:15.117132Z","shell.execute_reply":"2023-08-02T09:37:15.683840Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The variation in the validation loss/accuracy is due to the data augmentation, but we can see that the result are good, there is no overfitting.\n\nFinally, we can save the model to re-use it later.","metadata":{}},{"cell_type":"code","source":"# Save model\nmodel.save(filepath=\"catdog_cnn\", save_format=\"tf\")","metadata":{"execution":{"iopub.status.busy":"2023-08-02T09:37:15.686331Z","iopub.execute_input":"2023-08-02T09:37:15.686696Z","iopub.status.idle":"2023-08-02T09:37:17.522924Z","shell.execute_reply.started":"2023-08-02T09:37:15.686662Z","shell.execute_reply":"2023-08-02T09:37:17.521966Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Prediction","metadata":{}},{"cell_type":"markdown","source":"Now we can predict the classes of some of the test images. We will take randomly ten images, pass them to an image data generator, and use the model trained just before to predict their class.","metadata":{}},{"cell_type":"code","source":"# Load random images from the test sets\nX_test = extract_test(TEST_PATH)\n\n# Instantiate the ImageDataGenerator class for the testing\ntest_datagen = ImageDataGenerator(\n    rescale=1.0/255.0\n)\n\ntest_generator = test_datagen.flow(\n    x=X_test,\n    batch_size=32\n)","metadata":{"execution":{"iopub.status.busy":"2023-08-02T10:28:08.638058Z","iopub.execute_input":"2023-08-02T10:28:08.638437Z","iopub.status.idle":"2023-08-02T10:28:08.926977Z","shell.execute_reply.started":"2023-08-02T10:28:08.638405Z","shell.execute_reply":"2023-08-02T10:28:08.925967Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Predict the classes\ny_pred = model.predict(test_generator.x)\ny_pred = np.where(y_pred > .5, 1, 0).reshape(-1)","metadata":{"execution":{"iopub.status.busy":"2023-08-02T10:28:12.170794Z","iopub.execute_input":"2023-08-02T10:28:12.171516Z","iopub.status.idle":"2023-08-02T10:28:12.324231Z","shell.execute_reply.started":"2023-08-02T10:28:12.171482Z","shell.execute_reply":"2023-08-02T10:28:12.323251Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can now plot the images with their predicted class and see if the model is accurate. We can expect that the result on the test set to be lower than on the validation set.","metadata":{}},{"cell_type":"code","source":"# Show the images with their predictions\npredict_image(X_test, y_pred)","metadata":{"execution":{"iopub.status.busy":"2023-08-02T10:31:09.442537Z","iopub.execute_input":"2023-08-02T10:31:09.442913Z","iopub.status.idle":"2023-08-02T10:31:10.699528Z","shell.execute_reply.started":"2023-08-02T10:31:09.442882Z","shell.execute_reply":"2023-08-02T10:31:10.698406Z"},"trusted":true},"execution_count":null,"outputs":[]}]}