{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":3362,"databundleVersionId":31148,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-25T16:35:20.172641Z","iopub.execute_input":"2025-05-25T16:35:20.173093Z","iopub.status.idle":"2025-05-25T16:35:20.595620Z","shell.execute_reply.started":"2025-05-25T16:35:20.173063Z","shell.execute_reply":"2025-05-25T16:35:20.593538Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import zipfile\nimport os\nimport math\nimport imagehash\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nimport cv2\nfrom glob import glob\n\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.applications.resnet50 import preprocess_input\nfrom tensorflow.keras.models import Sequential, load_model\nfrom tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, GlobalAveragePooling2D, Dropout\nfrom tensorflow.keras.metrics import Precision, Recall, F1Score\nfrom tensorflow.keras.callbacks import EarlyStopping\n\nfrom IPython.display import FileLink\n\nfrom sklearn.metrics import confusion_matrix, classification_report, ConfusionMatrixDisplay","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T15:34:34.896247Z","iopub.execute_input":"2025-05-25T15:34:34.898551Z","iopub.status.idle":"2025-05-25T15:34:34.911497Z","shell.execute_reply.started":"2025-05-25T15:34:34.898462Z","shell.execute_reply":"2025-05-25T15:34:34.910327Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Load Data","metadata":{}},{"cell_type":"markdown","source":"First of all, let's proceed to unzip the files","metadata":{}},{"cell_type":"code","source":" #create a new folder inside /kaggle/working --> Full path /kaggle/working/data/train/train\n## the names of the images in train folder comes with the description if it is a cat or dog, but in the test folder we have just a numeric name \n\nwith zipfile.ZipFile('/kaggle/input/dogs-vs-cats/train.zip','r') as zip_ext:\n    zip_ext.extractall('data/train')  \n\nwith zipfile.ZipFile('/kaggle/input/dogs-vs-cats/test1.zip','r') as zip_ext:\n    zip_ext.extractall('data/test')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:01:35.479446Z","iopub.execute_input":"2025-05-25T06:01:35.480040Z","iopub.status.idle":"2025-05-25T06:01:51.344459Z","shell.execute_reply.started":"2025-05-25T06:01:35.480011Z","shell.execute_reply":"2025-05-25T06:01:51.343565Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dir = '/kaggle/working/data/train/train'\nfiles = os.listdir(train_dir)\n\ndata_train=[]\nfor i in files:\n    label = i.split('.')[0]  #dog or cat\n    path = os.path.join(train_dir,i)\n    data_train.append((path,label))\n\ntrain_df = pd.DataFrame(data_train, columns=['filepath','label'])\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:01:51.345463Z","iopub.execute_input":"2025-05-25T06:01:51.345818Z","iopub.status.idle":"2025-05-25T06:01:51.475265Z","shell.execute_reply.started":"2025-05-25T06:01:51.345772Z","shell.execute_reply":"2025-05-25T06:01:51.474503Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### EDA","metadata":{}},{"cell_type":"markdown","source":"Once we have our dataframe created and the image classified into dogs and cats, we should check first the class balancing. As we can see below, they are equally distributed so we don't need to worry about class weighting-","metadata":{}},{"cell_type":"code","source":"train_df['label'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-23T07:38:58.209176Z","iopub.execute_input":"2025-05-23T07:38:58.209570Z","iopub.status.idle":"2025-05-23T07:38:58.221219Z","shell.execute_reply.started":"2025-05-23T07:38:58.209541Z","shell.execute_reply":"2025-05-23T07:38:58.220155Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.iloc[2]['filepath']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-23T07:38:58.222410Z","iopub.execute_input":"2025-05-23T07:38:58.223417Z","iopub.status.idle":"2025-05-23T07:38:58.239337Z","shell.execute_reply.started":"2025-05-23T07:38:58.223379Z","shell.execute_reply":"2025-05-23T07:38:58.238323Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"It seems that our files have same size, but it would be nice to be 100% sure about this-","metadata":{}},{"cell_type":"code","source":"## Lets retrieve the size of the fist 5 files\n\nfor i in range(5):\n    path = train_df.iloc[2]['filepath']\n    with Image.open(path) as img:\n        print(f'The size of the image is: {img.size}')  #(width,height)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-23T07:38:58.240296Z","iopub.execute_input":"2025-05-23T07:38:58.241207Z","iopub.status.idle":"2025-05-23T07:38:58.261036Z","shell.execute_reply.started":"2025-05-23T07:38:58.241179Z","shell.execute_reply":"2025-05-23T07:38:58.260120Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_size(path):\n    try:\n        with Image.open(path) as img:\n            return img.size\n    except:\n        raise ValueError('Unexpected error in path: ' +path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:01:51.477738Z","iopub.execute_input":"2025-05-25T06:01:51.477998Z","iopub.status.idle":"2025-05-25T06:01:52.026903Z","shell.execute_reply.started":"2025-05-25T06:01:51.477979Z","shell.execute_reply":"2025-05-25T06:01:52.025817Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df['size'] = train_df['filepath'].apply(get_size)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:01:52.027859Z","iopub.execute_input":"2025-05-25T06:01:52.028186Z","iopub.status.idle":"2025-05-25T06:01:54.014569Z","shell.execute_reply.started":"2025-05-25T06:01:52.028155Z","shell.execute_reply":"2025-05-25T06:01:54.013656Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"As we see, the dataset contains different sizes although the first 5 rows seems to be uniform in that way. One of the step that we should do before move on is normalize the sizes.","metadata":{}},{"cell_type":"code","source":"train_df['size'].value_counts().sort_index()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-23T07:39:00.114254Z","iopub.execute_input":"2025-05-23T07:39:00.114578Z","iopub.status.idle":"2025-05-23T07:39:00.150369Z","shell.execute_reply.started":"2025-05-23T07:39:00.114547Z","shell.execute_reply":"2025-05-23T07:39:00.149296Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Another thing to consider, is that as we already have the images labeled, maybe some of them are labeled with a wrong classification. Suppose that we have a picture of a cat, labeled with dog. For this, the next step is to plot some random images to have a double check.","metadata":{}},{"cell_type":"code","source":"train_df[train_df['label'] == 'cat'].sample(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-23T07:39:00.152068Z","iopub.execute_input":"2025-05-23T07:39:00.152400Z","iopub.status.idle":"2025-05-23T07:39:00.177104Z","shell.execute_reply.started":"2025-05-23T07:39:00.152362Z","shell.execute_reply":"2025-05-23T07:39:00.176158Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Let's define here all the functions that we are going to use for EDA!","metadata":{}},{"cell_type":"code","source":"def show_random_img(df_subset, n=8):\n    cols = 4  # we can adjust this of course\n    rows = math.ceil (n/cols)  #this is to have a flexible parameter in case that we prefer to check more images and not assuming that the row will be 1\n    \n    sample_df = df_subset.sample(min(n,len(df_subset)))  #retrieve n random rows, but if the df has fewer than n rows, just retrieve how many it has.\n    fig, axs = plt.subplots(rows, cols, figsize=(4*cols,4*rows))\n    ## in case that we got nxm it will be a 2D array, but the loop expect 1D, that is the reason why we need to flatten first\n    axs = axs.flatten() if n>1 else [axs] \n\n    for i, (idx,row) in enumerate(sample_df.iterrows()):\n        img = Image.open(row['filepath'])\n        axs[i].imshow(img)\n        axs[i].set_title(f\"{idx} |{row['label']}\")   #we add the index, because in some display results, it could be usefull to know the index in case that there is any weird picture\n\n    plt.tight_layout()\n    plt.show()        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:01:54.015458Z","iopub.execute_input":"2025-05-25T06:01:54.015724Z","iopub.status.idle":"2025-05-25T06:01:54.022592Z","shell.execute_reply.started":"2025-05-25T06:01:54.015705Z","shell.execute_reply":"2025-05-25T06:01:54.021554Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## GRAYSCALE check\n\ndef is_grayscale(path):\n    ## returns true is the image is not RGB\n    try:\n        with Image.open(path) as img:\n            return img.mode != 'RGB'  #check https://pillow.readthedocs.io/en/stable/handbook/concepts.html#concept-modes\n    except:\n        raise ValueError('Unexpected error in path: ' +path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:01:54.023733Z","iopub.execute_input":"2025-05-25T06:01:54.024124Z","iopub.status.idle":"2025-05-25T06:01:54.043704Z","shell.execute_reply.started":"2025-05-25T06:01:54.024092Z","shell.execute_reply":"2025-05-25T06:01:54.042563Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## BLURRINESS CHECK --> For this, we will use the variance of Laplacian method, so low variance = blurry.\n\ndef is_blurry(path, threshold=100):  #https://docs.opencv.org/3.4/d5/db5/tutorial_laplace_operator.html\n    try:\n        img= cv2.imread(path, cv2.IMREAD_GRAYSCALE)   #convert to grayscale for edge detection\n        if img is None:\n            return False\n        variance = cv2.Laplacian(img,cv2.CV_64F).var()   #64-bit float\n        return variance < threshold\n    except:\n        raise ValueError(f'Error checking blur: {path}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:01:54.044749Z","iopub.execute_input":"2025-05-25T06:01:54.045054Z","iopub.status.idle":"2025-05-25T06:01:54.068895Z","shell.execute_reply.started":"2025-05-25T06:01:54.045033Z","shell.execute_reply":"2025-05-25T06:01:54.067886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### Duplicate images with hash library\n\ndef get_duplicate(path):\n    try:\n        with Image.open(path) as img:\n            return str(imagehash.average_hash(img))\n    except:\n        return None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:01:54.069859Z","iopub.execute_input":"2025-05-25T06:01:54.070171Z","iopub.status.idle":"2025-05-25T06:01:54.088868Z","shell.execute_reply.started":"2025-05-25T06:01:54.070143Z","shell.execute_reply":"2025-05-25T06:01:54.087992Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_metrics(val_generator, model): \n    y_true = val_generator.classes\n    y_pred_probs = model.predict(val_generator)\n    y_pred = (y_pred_probs > 0.5).astype(int)  #we need to binarize the predictions. \n\n    report_dic = classification_report(\n    y_true,\n    y_pred,\n    target_names=['cat','dog'],\n    output_dict=True\n    )\n    \n    report_df = pd.DataFrame(report_dic).transpose()\n    report_df = report_df.round(2)  #rounds values for clarity\n    report_df.style.background_gradient(cmap='Blues').format({'precision': \"{:.2f}\", 'recall': \"{:.2f}\", 'f1-score': \"{:.2f}\"})\n\n    ConfusionMatrixDisplay.from_predictions(\n        y_true,\n        y_pred,  #using labels,\n        display_labels=['cat','dog'],\n        cmap='Blues',\n        normalize=None\n    )\n\n    plt.title('Normalized Confusion Matrix')\n    plt.xticks(rotation=45)\n    plt.show()\n\n    return report_df.style.background_gradient(cmap='Blues').format({'precision': \"{:.2f}\", 'recall': \"{:.2f}\", 'f1-score': \"{:.2f}\"})\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T07:24:41.283670Z","iopub.execute_input":"2025-05-25T07:24:41.284003Z","iopub.status.idle":"2025-05-25T07:24:41.291275Z","shell.execute_reply.started":"2025-05-25T07:24:41.283979Z","shell.execute_reply":"2025-05-25T07:24:41.290411Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Are they cute, right? But we can assumed that they are labeled corrected! ","metadata":{}},{"cell_type":"code","source":"show_random_img(train_df[train_df['label'] =='cat'],n=6)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-23T07:45:19.045743Z","iopub.execute_input":"2025-05-23T07:45:19.046171Z","iopub.status.idle":"2025-05-23T07:45:20.737457Z","shell.execute_reply.started":"2025-05-23T07:45:19.046141Z","shell.execute_reply":"2025-05-23T07:45:20.735954Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"show_random_img(train_df[train_df['label'] =='dog'],n=6)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-23T07:45:52.055277Z","iopub.execute_input":"2025-05-23T07:45:52.056167Z","iopub.status.idle":"2025-05-23T07:45:53.736917Z","shell.execute_reply.started":"2025-05-23T07:45:52.056130Z","shell.execute_reply":"2025-05-23T07:45:53.735352Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Before resizing the pictures, there are a few things that could be interesting to double check -  As same as we have in tabular dataset - EDA process - checking outliers, missing values, here happened something similar, and it is checking visual sanity checks - look for weird filters (sepia for instance), low quality, strange angles, blurred pictures, etc.","metadata":{}},{"cell_type":"markdown","source":"##### Grayscale\n\nWe affirm that our pictures are in RGB and don't contain any weird filters.","metadata":{}},{"cell_type":"code","source":"train_df['is_grayscale'] = train_df['filepath'].apply(is_grayscale)\n\ntrain_df['is_grayscale'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:01:54.091243Z","iopub.execute_input":"2025-05-25T06:01:54.091574Z","iopub.status.idle":"2025-05-25T06:01:55.823876Z","shell.execute_reply.started":"2025-05-25T06:01:54.091552Z","shell.execute_reply":"2025-05-25T06:01:55.822775Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##### Blurry\n\nOk, we got that around ~11% of the records are blurry enough to cross the threshold = 100; but the majority of the dataset is mostrly sharp! Next step to perfom, is to take a quickly inspection to those images!","metadata":{}},{"cell_type":"code","source":"train_df['is_blurry'] = train_df['filepath'].apply(is_blurry)\n\ntrain_df['is_blurry'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:01:55.824837Z","iopub.execute_input":"2025-05-25T06:01:55.825662Z","iopub.status.idle":"2025-05-25T06:02:40.148587Z","shell.execute_reply.started":"2025-05-25T06:01:55.825629Z","shell.execute_reply":"2025-05-25T06:02:40.147648Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Alright, we can completely indentify each picture and is easy to know if it is a cat or dog.","metadata":{}},{"cell_type":"code","source":"show_random_img(train_df[train_df['is_blurry'] == True],n=12)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-23T07:46:54.828507Z","iopub.execute_input":"2025-05-23T07:46:54.828808Z","iopub.status.idle":"2025-05-23T07:46:58.098547Z","shell.execute_reply.started":"2025-05-23T07:46:54.828766Z","shell.execute_reply":"2025-05-23T07:46:58.097170Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##### Duplicate images","metadata":{}},{"cell_type":"code","source":"train_df['img_hash'] = train_df['filepath'].apply(get_duplicate)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:02:40.149411Z","iopub.execute_input":"2025-05-25T06:02:40.149665Z","iopub.status.idle":"2025-05-25T06:03:34.659365Z","shell.execute_reply.started":"2025-05-25T06:02:40.149645Z","shell.execute_reply":"2025-05-25T06:03:34.658656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"duplicates = train_df[train_df.duplicated('img_hash', keep=False)]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:03:34.660235Z","iopub.execute_input":"2025-05-25T06:03:34.660499Z","iopub.status.idle":"2025-05-25T06:03:34.674653Z","shell.execute_reply.started":"2025-05-25T06:03:34.660455Z","shell.execute_reply":"2025-05-25T06:03:34.673462Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ok, it seems that it works! Of course, we've just showed 12 pictures, so we are not going to see each duplicate values, but we have one example where it ocurrs. Also, there is a weird picture where it said photo unvailable labeled as dog, where we clearly see that is a cat draw.\nAs we didn't print the index (now is included in the function) - I can't go back and tag it now.\n\n*UPDATED* : As I have to re run the whole notebook becasue the session expired, now we have the index regarding the photo mentiones above, so we'll proceed to delete it.","metadata":{}},{"cell_type":"code","source":"train_df = train_df.drop([12224,14890])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:03:34.675655Z","iopub.execute_input":"2025-05-25T06:03:34.675910Z","iopub.status.idle":"2025-05-25T06:03:34.705630Z","shell.execute_reply.started":"2025-05-25T06:03:34.675890Z","shell.execute_reply":"2025-05-25T06:03:34.704619Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"show_random_img(duplicates,n=12)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-23T07:47:56.344814Z","iopub.execute_input":"2025-05-23T07:47:56.345186Z","iopub.status.idle":"2025-05-23T07:47:59.164144Z","shell.execute_reply.started":"2025-05-23T07:47:56.345153Z","shell.execute_reply":"2025-05-23T07:47:59.162747Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Proceed to delete duplicate files!\n\ntrain_df = train_df.drop_duplicates(subset='img_hash', keep='first')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:03:34.708556Z","iopub.execute_input":"2025-05-25T06:03:34.708884Z","iopub.status.idle":"2025-05-25T06:03:34.721527Z","shell.execute_reply.started":"2025-05-25T06:03:34.708860Z","shell.execute_reply":"2025-05-25T06:03:34.720644Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## Before jumping with the model, it could be nice to save a cleaned version of our dataset; although this is a small dataset compared with other, it is a good practice to include in each work.\n\ntrain_df.to_pickle('cleaned_train_df.pkl')\nFileLink('cleaned_train_df.pkl')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:03:34.722649Z","iopub.execute_input":"2025-05-25T06:03:34.722992Z","iopub.status.idle":"2025-05-25T06:03:34.765978Z","shell.execute_reply.started":"2025-05-25T06:03:34.722967Z","shell.execute_reply":"2025-05-25T06:03:34.764958Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_imagedata = ImageDataGenerator(\n    rescale=1./255,\n    validation_split=0.2,\n    rotation_range=15,  #randomly rotates images up to +-15º (helps model to be rotation-tolerant)\n    zoom_range=0.1,   #simulates camera zoom variation\n    horizontal_flip=True   #randomly flig image left <-> right || learn symmetrical patterns   \n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:03:34.767072Z","iopub.execute_input":"2025-05-25T06:03:34.767459Z","iopub.status.idle":"2025-05-25T06:03:34.772136Z","shell.execute_reply.started":"2025-05-25T06:03:34.767431Z","shell.execute_reply":"2025-05-25T06:03:34.771503Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ok, it is saying that 19972 are validated image. So, our train dataframe is build by 25000 rows. We said that validation_split = 20% -> it gaves us, around ~20000 images in training (~80%), this means that around 28 rows are corrupted.","metadata":{}},{"cell_type":"code","source":"train_generator = train_imagedata.flow_from_dataframe(\n    dataframe = train_df,\n    x_col = 'filepath',   #column w full path of images\n    y_col='label',\n    target_size=(128,128),\n    batch_size=32,\n    class_mode='binary',\n    subset='training',  #use the training part of the split\n    shuffle=True,\n    seed=42   \n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:03:34.772910Z","iopub.execute_input":"2025-05-25T06:03:34.773159Z","iopub.status.idle":"2025-05-25T06:03:35.013113Z","shell.execute_reply.started":"2025-05-25T06:03:34.773140Z","shell.execute_reply":"2025-05-25T06:03:35.012123Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### As we created a train_generator should do the same for val_generator to use it later in .fit\n\nval_generator = train_imagedata.flow_from_dataframe(\n    dataframe = train_df,\n    x_col='filepath',\n    y_col='label',\n    target_size=(128,128),\n    batch_size=32,\n    class_mode='binary',\n    subset='validation',\n    shuffle=False,\n    seed=42    \n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:03:35.014034Z","iopub.execute_input":"2025-05-25T06:03:35.014326Z","iopub.status.idle":"2025-05-25T06:03:35.223430Z","shell.execute_reply.started":"2025-05-25T06:03:35.014306Z","shell.execute_reply":"2025-05-25T06:03:35.222439Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"### Remember that train_generator is a generator object, not a shape\n\nmodel= Sequential([\n    Conv2D(32,(3,3), activation = 'relu', padding='same', input_shape=train_generator.image_shape),\n    MaxPooling2D(2,2),\n    Conv2D(64,(3,3), activation = 'relu',padding='same'),\n    MaxPooling2D(2,2),\n    Conv2D(128,(3,3), activation = 'relu',padding='same'),\n    MaxPooling2D(3,3),\n    GlobalAveragePooling2D(),\n    Dense(64, activation='relu'),\n    Dense(1, activation='sigmoid')\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:03:35.224427Z","iopub.execute_input":"2025-05-25T06:03:35.224772Z","iopub.status.idle":"2025-05-25T06:03:35.347426Z","shell.execute_reply.started":"2025-05-25T06:03:35.224745Z","shell.execute_reply":"2025-05-25T06:03:35.346428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-24T19:51:57.112900Z","iopub.execute_input":"2025-05-24T19:51:57.113198Z","iopub.status.idle":"2025-05-24T19:51:57.134380Z","shell.execute_reply.started":"2025-05-24T19:51:57.113179Z","shell.execute_reply":"2025-05-24T19:51:57.133432Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy',Precision(),Recall()])\nearly_stop = EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True)\nhistory = model.fit(train_generator,epochs=10,validation_data=val_generator,callbacks=[early_stop])\nmodel.evaluate(val_generator)\n\n## We save the model to reuse it later, so we dont have to re run everything again.\nmodel.save('cat_vs_dogs.keras')\nFileLink('cat_vs_dogs.keras')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T06:03:35.348653Z","iopub.execute_input":"2025-05-25T06:03:35.348987Z","iopub.status.idle":"2025-05-25T07:24:12.044568Z","shell.execute_reply.started":"2025-05-25T06:03:35.348961Z","shell.execute_reply":"2025-05-25T07:24:12.043529Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = load_model(\"cats_vs_dogs_model.keras\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Let's dig in the metrics that we got from the model. \n\n* Loss ~0.45 --> still to improve, I guess that the model's confidence isn't very strong.\n* Acc ~78.6% of total predictions were correct --> but cannot tell how it's getting wrong.\n* Precision 85% of predicted class were actually correct.\n* Recal 70% of the actual positive cases were correct --> Still to improve","metadata":{}},{"cell_type":"markdown","source":"### ResNet\n\nIn terms to see if we can improve our model based on the latest metrics -  I will use a pre trained NN","metadata":{}},{"cell_type":"code","source":"train_datagen_resnet = ImageDataGenerator(\n    preprocessing_function=preprocess_input,\n    validation_split=0.2,\n    rotation_range=15,  #randomly rotates images up to +-15º (helps model to be rotation-tolerant)\n    zoom_range=0.1,   #simulates camera zoom variation\n    horizontal_flip=True   #randomly flig image left <-> right || learn symmetrical patterns   \n)\n\ntrain_generator = train_datagen_resnet.flow_from_dataframe(\n    dataframe = train_df,\n    x_col = 'filepath',   #column w full path of images\n    y_col='label',\n    target_size=(256,256),\n    batch_size=32,\n    class_mode='binary',\n    subset='training',  #use the training part of the split\n    shuffle=True,\n    seed=42   \n)\n\nval_generator = train_datagen_resnet.flow_from_dataframe(\n    dataframe = train_df,\n    x_col='filepath',\n    y_col='label',\n    target_size=(256,256),\n    batch_size=32,\n    class_mode='binary',\n    subset='validation',\n    shuffle=False,\n    seed=42    \n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T07:27:57.041079Z","iopub.execute_input":"2025-05-25T07:27:57.041814Z","iopub.status.idle":"2025-05-25T07:27:57.467558Z","shell.execute_reply.started":"2025-05-25T07:27:57.041783Z","shell.execute_reply":"2025-05-25T07:27:57.466696Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## Load Resnet\n\nresnet_base = ResNet50(\n    weights='imagenet',\n    include_top=False,\n    input_shape=(256,256,3)\n)\n\nresnet_base.trainable = False  #freeze weights so we dont retrain Imagenet layers\n\nmodel_res = Sequential([\n    resnet_base,\n    GlobalAveragePooling2D(),\n    Dropout(0.3),\n    Dense(64, activation='relu'),\n    Dense(1, activation='sigmoid')\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T07:28:07.314637Z","iopub.execute_input":"2025-05-25T07:28:07.314935Z","iopub.status.idle":"2025-05-25T07:28:09.649024Z","shell.execute_reply.started":"2025-05-25T07:28:07.314915Z","shell.execute_reply":"2025-05-25T07:28:09.648240Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_res.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy',Precision(),Recall()])\nearly_stop = EarlyStopping(monitor='val_loss', patience=3, min_delta=0.001, restore_best_weights=True)\nhistory = model_res.fit(train_generator,epochs=10,validation_data=val_generator,callbacks=[early_stop])\nmodel_res.evaluate(val_generator)\n\n## We save the model to reuse it later, so we dont have to re run everything again.\nmodel_res.save('cat_vs_dogs_res.keras')\nFileLink('cat_vs_dogs_res.keras')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T07:28:22.956581Z","iopub.execute_input":"2025-05-25T07:28:22.956903Z","iopub.status.idle":"2025-05-25T14:59:44.157883Z","shell.execute_reply.started":"2025-05-25T07:28:22.956881Z","shell.execute_reply":"2025-05-25T14:59:44.154733Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"For the next steps, it would be nice to plot a Confusion Matrix to know precisely which cases are false negative and positive, a ROC Curve to plot the true positives vs False Positive.","metadata":{}},{"cell_type":"markdown","source":"Okay, so the model’s performance is symmetrical. It's not biased toward one class overall, we can detect a slightly difference btw cat and doqs, where 448 cats are missed, but in overall terms is balanced.","metadata":{}},{"cell_type":"code","source":"plot_metrics(val_generator,model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T07:24:45.485444Z","iopub.execute_input":"2025-05-25T07:24:45.485848Z","iopub.status.idle":"2025-05-25T07:25:32.893018Z","shell.execute_reply.started":"2025-05-25T07:24:45.485823Z","shell.execute_reply":"2025-05-25T07:25:32.891799Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"WOW! 99% of accuracy with the pre trained neuronal network! Let's jump with the test1 folder for the predictions ;)","metadata":{}},{"cell_type":"code","source":"plot_metrics(val_generator,model_res)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T15:00:31.510036Z","iopub.execute_input":"2025-05-25T15:00:31.510440Z","iopub.status.idle":"2025-05-25T15:12:07.926973Z","shell.execute_reply.started":"2025-05-25T15:00:31.510377Z","shell.execute_reply":"2025-05-25T15:12:07.925985Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Predictions","metadata":{}},{"cell_type":"code","source":"test_dir = '/kaggle/working/data/test/test1'\ntest_paths = sorted(glob(os.path.join(test_dir,\"*.jpg\")))\n\ntest_df = pd.DataFrame({'filename': test_paths})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T15:38:29.612453Z","iopub.execute_input":"2025-05-25T15:38:29.612885Z","iopub.status.idle":"2025-05-25T15:38:29.657684Z","shell.execute_reply.started":"2025-05-25T15:38:29.612859Z","shell.execute_reply":"2025-05-25T15:38:29.656652Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_datagen_resnet = ImageDataGenerator(preprocessing_function=preprocess_input)\n\ntest_generator = test_datagen_resnet.flow_from_dataframe(\n    test_df,\n    x_col='filename',\n    class_mode=None,\n    target_size=(256,256),\n    shuffle=False,\n    batch_size=32\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T15:46:30.629449Z","iopub.execute_input":"2025-05-25T15:46:30.630235Z","iopub.status.idle":"2025-05-25T15:46:30.890563Z","shell.execute_reply.started":"2025-05-25T15:46:30.630204Z","shell.execute_reply":"2025-05-25T15:46:30.889525Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_pred = model_res.predict(test_generator)\n\n#np.save('y_pred_resnet',y_pred)\n#FileLink('y_pred_resnet')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T15:49:47.669937Z","iopub.execute_input":"2025-05-25T15:49:47.670381Z","iopub.status.idle":"2025-05-25T16:15:50.014115Z","shell.execute_reply.started":"2025-05-25T15:49:47.670346Z","shell.execute_reply":"2025-05-25T16:15:50.012337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_pred_loaded = np.load(\"y_pred_resnet.npy\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_labels = (y_pred > 0.5).astype(int).flatten()\nids = [int(os.path.basename(path).split('.')[0]) for path in test_generator.filenames]\nsubmission_df = pd.DataFrame({\n    'id': ids,\n    'label': test_labels\n}).sort_values('id')  \n\nsubmission_df.to_csv(\"submission.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-25T16:20:17.319850Z","iopub.execute_input":"2025-05-25T16:20:17.320253Z","iopub.status.idle":"2025-05-25T16:20:17.370169Z","shell.execute_reply.started":"2025-05-25T16:20:17.320227Z","shell.execute_reply":"2025-05-25T16:20:17.369223Z"}},"outputs":[],"execution_count":null}]}