{"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 CNN (Convolutional Neural Network) using Transfer Learning with Tensorflow, Tensorflow Hub and the Keras API.","metadata":{}},{"cell_type":"code","source":"# Imports\nimport os\nimport random\nimport shutil\nimport warnings\nimport zipfile\n\nwarnings.filterwarnings('ignore')\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport tensorflow as tf\nimport tensorflow_hub as hub\n\nfrom tensorflow.keras import layers\nfrom tensorflow.keras.optimizers import RMSprop\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.callbacks import EarlyStopping\nfrom tensorflow.keras.callbacks import Callback","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2023-09-03T13:18:41.912392Z","iopub.execute_input":"2023-09-03T13:18:41.912691Z","iopub.status.idle":"2023-09-03T13:18:51.392367Z","shell.execute_reply.started":"2023-09-03T13:18:41.912664Z","shell.execute_reply":"2023-09-03T13:18:51.391383Z"},"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":"# Constant\nSOURCE_DIR = '/kaggle/working/'\n\nTRAIN_DIR = os.path.join(SOURCE_DIR, 'train/')\nTEST_DIR = os.path.join(SOURCE_DIR, 'test/')\n\nTRAIN_CATS_DIR = os.path.join(TRAIN_DIR, \"cats/\")\nTRAIN_DOGS_DIR = os.path.join(TRAIN_DIR, \"dogs/\")\n\nTEST_CATS_DIR = os.path.join(TEST_DIR, \"cats/\")\nTEST_DOGS_DIR = os.path.join(TEST_DIR, \"dogs/\")\n\nCATS_DOGS_DIR = (\n    TRAIN_CATS_DIR,\n    TRAIN_DOGS_DIR,\n    TEST_CATS_DIR,\n    TEST_DOGS_DIR,\n)\n\nSPLIT_SIZE = 0.8\nIMG_SIZE = (224, 224)\nLABELS_NAME = {0 : 'cat', 1 : 'dog'}","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:18:51.397706Z","iopub.execute_input":"2023-09-03T13:18:51.400407Z","iopub.status.idle":"2023-09-03T13:18:51.410853Z","shell.execute_reply.started":"2023-09-03T13:18:51.400369Z","shell.execute_reply":"2023-09-03T13:18:51.408886Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Fonctions\ndef split_data(source: str, train: str, test: str, SPLIT_SIZE: float) -> None:\n    \"\"\"\n    This function take the data available and split it into\n    train and test into different paths. SPLIT_SIZE \n    control the size allocated to the train.\n    \"\"\"\n    # Get the list of all files in the source directory\n    list_dir = os.listdir(source)\n\n    # Create empty folder for training and test data\n    for directory in (train, test):\n        if not os.path.exists(directory):\n            os.makedirs(directory)\n\n    # Shuffle and split the file list into training and test file list\n    shuffle_list = random.sample(list_dir, len(list_dir))\n    train_list = shuffle_list[:int(len(shuffle_list) * SPLIT_SIZE)]\n    test_list = shuffle_list[int(len(shuffle_list) * SPLIT_SIZE):]\n\n    # Copy each file in the corresponding folder\n    for filename in train_list :\n        shutil.copyfile(\n            os.path.join(source, filename), \n            os.path.join(train, filename)\n        )\n\n    for filename in test_list:\n        shutil.copyfile(\n            os.path.join(source, filename), \n            os.path.join(test, filename)\n        )\n\ndef fill_data_dirs(\n    tmp_train: str,\n    tmp_test: str,\n    train_cats: str,\n    train_dogs: str,\n    test_cats: str,\n    test_dogs: str,\n) -> None:\n    \"\"\"\n    This function take the data into the train and test\n    paths and copy images of cat or dog in the right subfolder.\n    \"\"\"\n    # Define lists\n    tmp_list = [tmp_train, tmp_test]\n    path_list = [\n        (train_dogs, train_cats), \n        (test_dogs, test_cats)\n    ]\n    \n    # Copy dog and cat images into the right folder\n    for tmp_path, path in zip(tmp_list, path_list):\n        for filename in os.listdir(tmp_path):\n            if filename.startswith('dog'):\n                shutil.copyfile(\n                    os.path.join(tmp_path, filename),\n                    os.path.join(path[0], filename)\n                )\n            else:\n                shutil.copyfile(\n                    os.path.join(tmp_path, filename),\n                    os.path.join(path[1], filename)\n                )\n\ndef show_image(path: str, title: str) -> None:\n    \"\"\"\n    This function take a random images from a path\n    and plot it. The title is used to specify if it\n    is from a cats or dogs folder.\n    \"\"\"\n    \n    # Get a random index\n    idx = random.randint(0, len(os.listdir(path)))\n    \n    # Plot a random image\n    plt.figure(figsize=(6,6))\n    plt.imshow(tf.keras.utils.load_img(f'{os.path.join(path, os.listdir(path)[idx])}').resize(IMG_SIZE))\n    plt.title(f\"Class: {title}\")\n    plt.axis('off')\n    plt.show()    ","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:19:23.474080Z","iopub.execute_input":"2023-09-03T13:19:23.474465Z","iopub.status.idle":"2023-09-03T13:19:23.491942Z","shell.execute_reply.started":"2023-09-03T13:19:23.474433Z","shell.execute_reply":"2023-09-03T13:19:23.490728Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Preprocessing","metadata":{}},{"cell_type":"markdown","source":"First of all we extract the data in a temporary folder, after we shuffle it and split it using the `split_data` function defined above. Then we create folders for train and test (each containing folders for cat and dog), and we fill these folders with the correct images.\n\nWe do this in order to load the data efficiently using Tensorflow API's ImageDataGenerator.","metadata":{}},{"cell_type":"code","source":"# Extract all the files\ntrain_local_zip = '/kaggle/input/dogs-vs-cats/train.zip'\nzip_ref = zipfile.ZipFile(train_local_zip, 'r')\nzip_ref.extractall('/tmp')\n\n# Close the zip reference\nzip_ref.close()\n\n# Split the data into a train and a validation set\nsplit_data('/tmp/train', '/tmp/training', '/tmp/testing', SPLIT_SIZE)","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:19:29.011320Z","iopub.execute_input":"2023-09-03T13:19:29.011715Z","iopub.status.idle":"2023-09-03T13:19:43.028750Z","shell.execute_reply.started":"2023-09-03T13:19:29.011682Z","shell.execute_reply":"2023-09-03T13:19:43.027731Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Create empty folders for training and validation data\nfor directory in CATS_DOGS_DIR:\n    # If the directory exist delete it\n    if os.path.exists(directory):\n        shutil.rmtree(directory)\n        \n    os.makedirs(directory)\n    \n# Fill training and validation folders\nfill_data_dirs(\n    '/tmp/training',\n    '/tmp/testing',\n    TRAIN_CATS_DIR,\n    TRAIN_DOGS_DIR,\n    TEST_CATS_DIR,\n    TEST_DOGS_DIR,\n)\n\n# Remove temporary folders\nshutil.rmtree('/tmp')","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:19:49.510871Z","iopub.execute_input":"2023-09-03T13:19:49.511254Z","iopub.status.idle":"2023-09-03T13:19:56.474563Z","shell.execute_reply.started":"2023-09-03T13:19:49.511225Z","shell.execute_reply":"2023-09-03T13:19:56.473548Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Now we can check the number of images that we will use for the training and the test.\n\nAnd plot an image of cat and one of a dog.","metadata":{}},{"cell_type":"code","source":"# Check the number of images per sets\nnum_img_train = len(os.listdir(TRAIN_CATS_DIR)) + len(os.listdir(TRAIN_DOGS_DIR))\nnum_img_test = len(os.listdir(TEST_CATS_DIR)) + len(os.listdir(TEST_DOGS_DIR))\n\nprint(f'Number of train images: {num_img_train:,}')\nprint(f'Number of test images: {num_img_test:,}')","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:20:00.536629Z","iopub.execute_input":"2023-09-03T13:20:00.537107Z","iopub.status.idle":"2023-09-03T13:20:00.570606Z","shell.execute_reply.started":"2023-09-03T13:20:00.537070Z","shell.execute_reply":"2023-09-03T13:20:00.569701Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Display image of cat and dog\nshow_image(TRAIN_CATS_DIR, \"cat\")\nshow_image(TRAIN_DOGS_DIR, \"dog\")","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:20:04.369693Z","iopub.execute_input":"2023-09-03T13:20:04.370066Z","iopub.status.idle":"2023-09-03T13:20:05.047546Z","shell.execute_reply.started":"2023-09-03T13:20:04.370034Z","shell.execute_reply":"2023-09-03T13:20:05.046712Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can now use the ImageDataGenerator to load the data. We split the train data into a train and validation generator by specifying the validation split.","metadata":{}},{"cell_type":"code","source":"# Instantiate the ImageDataGenerator class\ntrain_datagen = ImageDataGenerator(\n    rescale=1./255., \n    validation_split=0.2\n)\n\n# Get the data from the directory\n# Note that we fetch firt the train data\ntrain_generator = train_datagen.flow_from_directory(\n    directory=TRAIN_DIR,\n    batch_size=32,\n    class_mode='binary',\n    target_size=IMG_SIZE,\n    subset='training'\n)\n\n# And after the validation data\nvalidation_generator = train_datagen.flow_from_directory(\n    directory=TRAIN_DIR,\n    batch_size=32,\n    class_mode='binary',\n    target_size=IMG_SIZE,\n    subset='validation'\n)\n\n# Same process for the test data\ntest_datagen = ImageDataGenerator(rescale=1./255.)\n\ntest_generator = test_datagen.flow_from_directory(\n    directory=TEST_DIR,\n    batch_size=32,\n    class_mode='binary',\n    target_size=IMG_SIZE\n)","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:20:17.554494Z","iopub.execute_input":"2023-09-03T13:20:17.554844Z","iopub.status.idle":"2023-09-03T13:20:18.294625Z","shell.execute_reply.started":"2023-09-03T13:20:17.554816Z","shell.execute_reply":"2023-09-03T13:20:18.293714Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Model","metadata":{}},{"cell_type":"markdown","source":"We can now define our model using the Tensorflow Hub API. We use a lightweight version of the mobilenet v2 model, with a depth multiplier of 0.5. Not that we are also using the model's feature vector, as our problem isn't the same as for the mobilenet training, we are going to add the layers on the top of the model ourselves. This feature vector already has weights configured to detect specific shapes and details related to image classification, which will help us a lot to improve our results and speed up the training.\n\nThe advantage of using the Tensorflow Hub API is that we can easily convert the downloaded feature vector into a Keras layer. According to the documentation, the feature vector output is already flattened with a shape of 1280.","metadata":{}},{"cell_type":"code","source":"# Load the pretraarangeined model\nfeature_extractor = 'https://tfhub.dev/google/imagenet/mobilenet_v2_050_224/feature_vector/5'\npre_trained_model = hub.KerasLayer(feature_extractor, input_shape=IMG_SIZE + (3,), trainable=False)\n\n# Check the type after loading the pre_trained_model\nprint(type(pre_trained_model))","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:20:49.007294Z","iopub.execute_input":"2023-09-03T13:20:49.007661Z","iopub.status.idle":"2023-09-03T13:20:53.842072Z","shell.execute_reply.started":"2023-09-03T13:20:49.007631Z","shell.execute_reply":"2023-09-03T13:20:53.841102Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"As you can see we have freezon the trainable weight of the model. Since we will add the output layer to model and it is initialized randomly it will make large errors (at least during the first few epochs), so there will be large errors gradients that may wreck the reused weights. \n\nTo avoid this, one approach is to freeze the reused layers during the first few epochs, giving the new layer some time to learn reasonable weights.","metadata":{}},{"cell_type":"code","source":"# Check there is no trainable parameters in this model\nnum_trainable_params = sum([w.shape.num_element() for w in pre_trained_model.trainable_weights])\nprint(f\"There are {num_trainable_params} trainable parameters in this model.\")","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:20:57.332545Z","iopub.execute_input":"2023-09-03T13:20:57.333024Z","iopub.status.idle":"2023-09-03T13:20:57.338349Z","shell.execute_reply.started":"2023-09-03T13:20:57.332986Z","shell.execute_reply":"2023-09-03T13:20:57.337430Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We add the top layer to the model, compile and train it for 5 epochs. ","metadata":{}},{"cell_type":"code","source":"# Define the model\nmodel = tf.keras.Sequential([\n    pre_trained_model,\n    layers.Dense(1, activation='sigmoid')\n])\n\n# Compile the model\nmodel.compile(\n    optimizer=RMSprop(learning_rate=1e-2),\n    loss='binary_crossentropy',\n    metrics=['accuracy']\n)\n\n# Inspect parameters\ntotal_params = model.count_params()\nnum_trainable_params = sum([w.shape.num_elements() for w in model.trainable_weights])\n\nprint(f'There are {total_params:,} total parameters in this model')\nprint(f'There are {num_trainable_params:,} trainable parameters in this model')","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:21:00.500489Z","iopub.execute_input":"2023-09-03T13:21:00.501498Z","iopub.status.idle":"2023-09-03T13:21:01.301712Z","shell.execute_reply.started":"2023-09-03T13:21:00.501437Z","shell.execute_reply":"2023-09-03T13:21:01.300763Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Train the model on 5 epochs\nhistory = model.fit(\n    train_generator,\n    validation_data=validation_generator,\n    epochs=5\n)","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:21:04.701306Z","iopub.execute_input":"2023-09-03T13:21:04.701731Z","iopub.status.idle":"2023-09-03T13:25:21.330500Z","shell.execute_reply.started":"2023-09-03T13:21:04.701697Z","shell.execute_reply":"2023-09-03T13:25:21.329414Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Now we can unfreeze the reused layers (which requires compiling the model again) and continue training to fine-tune the reused layers for our cats and dogs classification.","metadata":{}},{"cell_type":"code","source":"# Unfreeze the layer of the pre-trained model\nfor layer in model.layers:\n    layer.trainable = True\n    \n# Compile again the model (with a lower learning rate)\nmodel.compile(\n    optimizer=RMSprop(learning_rate=1e-5),\n    loss='binary_crossentropy',\n    metrics=['accuracy']\n)\n\n# Check the model summary\nmodel.summary()","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:25:27.124772Z","iopub.execute_input":"2023-09-03T13:25:27.125888Z","iopub.status.idle":"2023-09-03T13:25:27.168564Z","shell.execute_reply.started":"2023-09-03T13:25:27.125840Z","shell.execute_reply":"2023-09-03T13:25:27.167454Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can see that the number of trainable prameters has considerably increase. So we will add callbacks to stop the training in some cases. If we rich 98% of accuracy or if the validation loss stops to increase at least for two epochs in a row.","metadata":{}},{"cell_type":"code","source":"# Define some callbakcs\nstop_train = EarlyStopping(\n    monitor='val_loss',\n    patience=2,\n    restore_best_weights=True\n)\n\n# Define a custom callback to stop training\n# when reache a given accuracy\nclass MyCallback(Callback):\n    def on_epoch_end(self, epoch, logs={}):\n        if (logs.get('accuracy') > 0.98):\n            print(\"\\nReached 98% accuracy so cancelling the training!\")\n            self.model.stop_training = True\n\n# Instantiate the custom callback\nreach_accuracy = MyCallback()","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:25:37.391990Z","iopub.execute_input":"2023-09-03T13:25:37.392336Z","iopub.status.idle":"2023-09-03T13:25:37.398971Z","shell.execute_reply.started":"2023-09-03T13:25:37.392308Z","shell.execute_reply":"2023-09-03T13:25:37.397883Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Then we can fit our model again.","metadata":{}},{"cell_type":"code","source":"# Train the model\nhistory = model.fit(\n    train_generator,\n    validation_data=validation_generator,\n    epochs=15,\n    callbacks=[stop_train, reach_accuracy],\n    initial_epoch=5\n)","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:26:01.125571Z","iopub.execute_input":"2023-09-03T13:26:01.126004Z","iopub.status.idle":"2023-09-03T13:32:14.819453Z","shell.execute_reply.started":"2023-09-03T13:26:01.125968Z","shell.execute_reply":"2023-09-03T13:32:14.818302Z"},"trusted":true},"execution_count":null,"outputs":[]},{"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-09-03T13:32:28.114530Z","iopub.execute_input":"2023-09-03T13:32:28.114897Z","iopub.status.idle":"2023-09-03T13:32:28.739843Z","shell.execute_reply.started":"2023-09-03T13:32:28.114867Z","shell.execute_reply":"2023-09-03T13:32:28.737413Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Predictions","metadata":{}},{"cell_type":"markdown","source":"Now we can test our model on the test set and see its performance.","metadata":{}},{"cell_type":"code","source":"# Evaluate the model\n_, test_accuracy = model.evaluate(test_generator)\n\nprint(f\"The model accuracy on the test set is: {test_accuracy * 100:.2f}%\")","metadata":{"execution":{"iopub.status.busy":"2023-09-03T13:40:00.939053Z","iopub.execute_input":"2023-09-03T13:40:00.939427Z","iopub.status.idle":"2023-09-03T13:40:11.277340Z","shell.execute_reply.started":"2023-09-03T13:40:00.939396Z","shell.execute_reply":"2023-09-03T13:40:11.276340Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Good ! We have very nice result using Transfer Learning thanks to the Tensorflow Hub API. Note that Transfer Learning work best essentially with deep convolutional networks, which tend to learn feature detectors that are much more general (especially in the lower layers).","metadata":{}}]}