{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":5441,"databundleVersionId":38425,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\n\nimport os\nimport random\nimport shutil\nimport zipfile\nfrom pathlib import Path\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, regularizers, initializers\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom sklearn.metrics import classification_report, confusion_matrix, f1_score, precision_score, recall_score\nimport itertools\nimport seaborn as sns\n\n# ----------------------------- Reproducibility -----------------------------\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\nos.environ['PYTHONHASHSEED'] = str(SEED)\n\n# ----------------------------- Paths & params ------------------------------\nTRAIN_ZIP = \"/kaggle/input/dogs-vs-cats-redux-kernels-edition/train.zip\"\nTEST_ZIP  = \"/kaggle/input/dogs-vs-cats-redux-kernels-edition/test.zip\"\nWORK_DIR = Path(\"/kaggle/working/dvc_scratch\")\nDATA_DIR = WORK_DIR / \"data\"\nTRAIN_DIR = DATA_DIR / \"train\"\nVAL_DIR = DATA_DIR / \"val\"\nTEST_DIR = DATA_DIR / \"test\"\n\nIMG_SIZE = (128, 128)\nBATCH_SIZE = 64\nEPOCHS = 50\nAUTOTUNE = tf.data.AUTOTUNE\n\n# L2 regularization weight\nL2 = 1e-4\n\n# Create working dirs\nfor d in [WORK_DIR, DATA_DIR, TRAIN_DIR, VAL_DIR, TEST_DIR]:\n    d.mkdir(parents=True, exist_ok=True)\n\n# ----------------------------- Helper functions ----------------------------\n\ndef extract_zip(zip_path, extract_to):\n    \"\"\"Extract zip if not already extracted.\"\"\"\n    zip_path = Path(zip_path)\n    if not zip_path.exists():\n        raise FileNotFoundError(f\"Zip file not found: {zip_path}\")\n    with zipfile.ZipFile(zip_path, 'r') as z:\n        z.extractall(path=extract_to)\n\n\ndef organize_train_images(extracted_train_dir, train_out_dir, val_out_dir, val_split=0.15):\n    \"\"\"\n    Move images to class subfolders and split into train/val.\n    expected filenames: cat.<id>.jpg and dog.<id>.jpg\n    \"\"\"\n    extracted_train_dir = Path(extracted_train_dir)\n    train_out_dir = Path(train_out_dir)\n    val_out_dir = Path(val_out_dir)\n\n    # Ensure class folders exist\n    for cls in [\"cats\", \"dogs\"]:\n        (train_out_dir / cls).mkdir(parents=True, exist_ok=True)\n        (val_out_dir / cls).mkdir(parents=True, exist_ok=True)\n\n    # List files\n    files = list(extracted_train_dir.glob('*.jpg'))\n    print(f\"Found {len(files)} training images in extracted folder\")\n\n    # Separate by class\n    cat_files = [f for f in files if f.name.startswith('cat')]\n    dog_files = [f for f in files if f.name.startswith('dog')]\n    print(f\"Cats: {len(cat_files)}, Dogs: {len(dog_files)}\")\n\n    def split_and_copy(file_list, cls_name):\n        random.shuffle(file_list)\n        n_val = int(len(file_list) * val_split)\n        val_files = file_list[:n_val]\n        train_files = file_list[n_val:]\n        for f in train_files:\n            shutil.copy(f, train_out_dir / cls_name / f.name)\n        for f in val_files:\n            shutil.copy(f, val_out_dir / cls_name / f.name)\n        print(f\"Class {cls_name}: train={len(train_files)}, val={len(val_files)}\")\n\n    split_and_copy(cat_files, 'cats')\n    split_and_copy(dog_files, 'dogs')\n\n\n# ----------------------------- Extract & prepare ---------------------------\n# 1) Extract\nprint(\"Extracting zips (this may take a while)...\")\nEXTRACTED_TRAIN_DIR = WORK_DIR / \"extracted_train\"\nEXTRACTED_TEST_DIR = WORK_DIR / \"extracted_test\"\nEXTRACTED_TRAIN_DIR.mkdir(parents=True, exist_ok=True)\nEXTRACTED_TEST_DIR.mkdir(parents=True, exist_ok=True)\n\n# Only extract if empty to avoid re-extracting in reruns\nif not any(EXTRACTED_TRAIN_DIR.iterdir()):\n    extract_zip(TRAIN_ZIP, EXTRACTED_TRAIN_DIR)\nelse:\n    print(\"Train already extracted.\")\n\nif not any(EXTRACTED_TEST_DIR.iterdir()):\n    extract_zip(TEST_ZIP, EXTRACTED_TEST_DIR)\nelse:\n    print(\"Test already extracted.\")\n\n# 2) Organize training and validation sets\n# The train.zip in this competition contains a folder named 'train' with images inside.\n# Sometimes extraction produces a top-level folder 'train' or directly images. We find the folder with images.\n\ndef find_image_dir(root):\n    root = Path(root)\n    # Prefer direct jpg files at root\n    jpgs = list(root.glob('*.jpg'))\n    if jpgs:\n        return root\n    # Otherwise search immediate children\n    for child in root.iterdir():\n        if child.is_dir():\n            jpgs = list(child.glob('*.jpg'))\n            if jpgs:\n                return child\n    raise FileNotFoundError(f\"No jpg images found under {root}\")\n\nEX_TRAIN_IMG_DIR = find_image_dir(EXTRACTED_TRAIN_DIR)\nEX_TEST_IMG_DIR = find_image_dir(EXTRACTED_TEST_DIR)\nprint(\"Images dir:\", EX_TRAIN_IMG_DIR, EX_TEST_IMG_DIR)\n\n# Only organize once (copies) to save time on re-runs\nif not any((TRAIN_DIR).iterdir()):\n    organize_train_images(EX_TRAIN_IMG_DIR, TRAIN_DIR, VAL_DIR, val_split=0.15)\nelse:\n    print(\"Train/val already prepared.\")\n\n# Copy test images to test dir (keep original filenames)\nif not any(TEST_DIR.iterdir()):\n    for f in EX_TEST_IMG_DIR.glob('*.jpg'):\n        shutil.copy(f, TEST_DIR / f.name)\n    print(f\"Copied {len(list(TEST_DIR.glob('*.jpg')))} test images\")\nelse:\n    print(\"Test already prepared.\")\n\n# ----------------------------- Data generators -----------------------------\nprint(\"Preparing data generators...\")\n\ntrain_datagen = ImageDataGenerator(\n    rescale=1./255,\n    rotation_range=25,\n    width_shift_range=0.15,\n    height_shift_range=0.15,\n    shear_range=0.15,\n    zoom_range=0.15,\n    horizontal_flip=True,\n    vertical_flip=False,\n    fill_mode='nearest'\n)\n\nval_datagen = ImageDataGenerator(rescale=1./255)\n\ntrain_gen = train_datagen.flow_from_directory(\n    TRAIN_DIR,\n    target_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    class_mode='binary',\n    shuffle=True,\n    seed=SEED\n)\n\nval_gen = val_datagen.flow_from_directory(\n    VAL_DIR,\n    target_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    class_mode='binary',\n    shuffle=False\n)\n\n# Optional: compute class weights (in case of imbalance)\nfrom sklearn.utils.class_weight import compute_class_weight\nclasses = train_gen.classes\nclass_weights = compute_class_weight('balanced', classes=np.unique(classes), y=classes)\nclass_weights = dict(enumerate(class_weights))\nprint(\"Class weights:\", class_weights)\n\n# ----------------------------- Build model --------------------------------\nprint(\"Building model...\")\n\ndef conv_block(x, filters, kernel_size=3, pool=True, dropout_rate=0.2):\n    x = layers.Conv2D(filters, kernel_size, padding='same',\n                      kernel_initializer=initializers.HeNormal(seed=SEED),\n                      kernel_regularizer=regularizers.l2(L2))(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Activation('relu')(x)\n    x = layers.Conv2D(filters, kernel_size, padding='same',\n                      kernel_initializer=initializers.HeNormal(seed=SEED),\n                      kernel_regularizer=regularizers.l2(L2))(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Activation('relu')(x)\n    if pool:\n        x = layers.MaxPooling2D(pool_size=(2,2))(x)\n    if dropout_rate and dropout_rate > 0:\n        x = layers.Dropout(dropout_rate, seed=SEED)(x)\n    return x\n\ninput_shape = IMG_SIZE + (3,)\ninputs = layers.Input(shape=input_shape)\n\nx = conv_block(inputs, 32, dropout_rate=0.15)\nx = conv_block(x, 64, dropout_rate=0.2)\nx = conv_block(x, 128, dropout_rate=0.25)\nx = conv_block(x, 256, dropout_rate=0.3)\n\nx = layers.GlobalAveragePooling2D()(x)\nx = layers.Dense(512, kernel_regularizer=regularizers.l2(L2),\n                 kernel_initializer=initializers.HeNormal(seed=SEED))(x)\nx = layers.BatchNormalization()(x)\nx = layers.Activation('relu')(x)\nx = layers.Dropout(0.5, seed=SEED)(x)\n\noutputs = layers.Dense(1, activation='sigmoid')(x)\n\nmodel = models.Model(inputs, outputs)\n\nmodel.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n              loss='binary_crossentropy',\n              metrics=['accuracy'])\n\nmodel.summary()\n\n# ----------------------------- Callbacks ----------------------------------\ncheckpoint_path = WORK_DIR / 'best_model.h5'\ncallbacks = [\n    tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=7, restore_best_weights=True),\n    tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6, verbose=1),\n    tf.keras.callbacks.ModelCheckpoint(str(checkpoint_path), monitor='val_loss', save_best_only=True, save_weights_only=False)\n]\n\n# ----------------------------- Training -----------------------------------\nsteps_per_epoch = max(1, train_gen.samples // BATCH_SIZE)\nvalidation_steps = max(1, val_gen.samples // BATCH_SIZE)\n\nhistory = model.fit(\n    train_gen,\n    steps_per_epoch=steps_per_epoch,\n    epochs=EPOCHS,\n    validation_data=val_gen,\n    validation_steps=validation_steps,\n    callbacks=callbacks,\n    class_weight=class_weights\n)\n\n# ----------------------------- Plots -------------------------------------\nprint(\"Plotting training history...\")\nplt.figure(figsize=(12,4))\nplt.subplot(1,2,1)\nplt.plot(history.history['loss'], label='train_loss')\nplt.plot(history.history['val_loss'], label='val_loss')\nplt.legend()\nplt.title('Loss')\n\nplt.subplot(1,2,2)\nplt.plot(history.history['accuracy'], label='train_acc')\nplt.plot(history.history['val_accuracy'], label='val_acc')\nplt.legend()\nplt.title('Accuracy')\nplt.show()\n\n# ----------------------------- Evaluation --------------------------------\nprint(\"Evaluating on validation set...\")\n# Predict on validation set\nval_gen.reset()\npreds = model.predict(val_gen, steps=validation_steps, verbose=1)\n# Because we used flow_from_directory with shuffle=False, we can get labels in order\ny_true = val_gen.classes[:validation_steps * BATCH_SIZE]\n# Truncate preds to same length\npreds = preds.ravel()[:len(y_true)]\n\ny_pred = (preds >= 0.5).astype(int)\n\nprint(\"Classification report:\")\nprint(classification_report(y_true, y_pred, target_names=['cats','dogs']))\n\ncm = confusion_matrix(y_true, y_pred)\nplt.figure(figsize=(6,5))\nax = sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')\nax.set_xlabel('Predicted')\nax.set_ylabel('True')\nax.set_xticklabels(['cats','dogs'])\nax.set_yticklabels(['cats','dogs'])\nplt.title('Confusion Matrix')\nplt.show()\n\nprec = precision_score(y_true, y_pred)\nrec = recall_score(y_true, y_pred)\nf1 = f1_score(y_true, y_pred)\nacc = (y_true == y_pred).mean()\n\nprint(f\"Validation accuracy: {acc:.4f}\")\nprint(f\"Precision: {prec:.4f}, Recall: {rec:.4f}, F1-score: {f1:.4f}\")\n\n# ----------------------------- Test predictions (optional) -----------------\n# The competition test set is unlabeled - below code shows how to predict and save a CSV\n# If you only want validation metrics, you can skip this.\n\nmake_test_preds = True\nif make_test_preds:\n    print(\"Predicting on test set (no labels expected)...\")\n    test_files = sorted(TEST_DIR.glob('*.jpg'))\n    out_preds = []\n    batch = []\n    names = []\n    for i, f in enumerate(test_files):\n        img = tf.keras.preprocessing.image.load_img(f, target_size=IMG_SIZE)\n        arr = tf.keras.preprocessing.image.img_to_array(img) / 255.0\n        batch.append(arr)\n        names.append(f.name)\n        # Predict in batches\n        if len(batch) == BATCH_SIZE or i == len(test_files)-1:\n            batch_arr = np.stack(batch, axis=0)\n            p = model.predict(batch_arr)\n            out_preds.extend(p.ravel().tolist())\n            batch = []\n    # Example of saving predictions\n    import pandas as pd\n    submission = pd.DataFrame({\n        'id': [n.split('.')[0] for n in names],\n        'label': out_preds\n    })\n    submission.to_csv(WORK_DIR / 'test_predictions.csv', index=False)\n    print(f\"Saved test predictions to {WORK_DIR / 'test_predictions.csv'}\")\n\nprint(\"Done. Best model saved to:\", checkpoint_path)\nprint(\"You can adjust architecture, augmentations, batch size, and regularization to push accuracy higher.\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-07T19:00:57.958945Z","iopub.execute_input":"2025-12-07T19:00:57.959226Z","iopub.status.idle":"2025-12-07T19:45:16.402618Z","shell.execute_reply.started":"2025-12-07T19:00:57.959205Z","shell.execute_reply":"2025-12-07T19:45:16.401981Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------------------- Test Set Evaluation (fast) -----------------------------\nprint(\"=== Test Set Evaluation (using organized folders) ===\")\n\ntest_generator = val_datagen.flow_from_directory(\n    test_dir,                # path to your test_organized folder\n    target_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    class_mode='binary',\n    shuffle=False\n)\n\n# Evaluate the model on the test set\ntest_loss, test_accuracy = model.evaluate(test_generator, verbose=1)\nprint(f\"\\nTest Accuracy: {test_accuracy:.4f}\")\n\n# Predict labels\ny_pred_probs = model.predict(test_generator, verbose=1)\ny_pred = (y_pred_probs > 0.5).astype(int).flatten()\ny_true = test_generator.classes\n\n# Classification report\nprint(\"\\nTest Classification Report:\")\nprint(classification_report(y_true, y_pred, target_names=['Cat','Dog']))\n\n# Confusion matrix\ntest_conf_matrix = confusion_matrix(y_true, y_pred)\nplt.figure(figsize=(8,6))\nsns.heatmap(test_conf_matrix, annot=True, fmt='d', cmap='Greens',\n            xticklabels=['Cat','Dog'], yticklabels=['Cat','Dog'])\nplt.title('Test Confusion Matrix')\nplt.ylabel('True Label')\nplt.xlabel('Predicted Label')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-07T20:06:15.784917Z","iopub.execute_input":"2025-12-07T20:06:15.785573Z","iopub.status.idle":"2025-12-07T20:06:29.016568Z","shell.execute_reply.started":"2025-12-07T20:06:15.785547Z","shell.execute_reply":"2025-12-07T20:06:29.015773Z"}},"outputs":[],"execution_count":null}]}