{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"}},"cells":[{"cell_type":"markdown","source":"# Competition: dogs-vs-cats-redux-kernels-edition\n\n**Generated by Alexandria Research Assistant**\n\n**Dataset:** dogs-vs-cats-redux-kernels-edition\n\n**Task:** competition - kaggle-competition\n\n---\n\n⚠️ **Note:** This notebook contains Alexandria markers (lines starting with `# ⚠️ ALEXANDRIA MARKER`) at the top of each code cell. These markers enable the 'Sync from Kaggle' feature to track cell outputs. Please do not delete them.","metadata":{}},{"cell_type":"markdown","source":"## Setup & Imports\n\nInstall and import all necessary libraries for data handling, image processing, modeling, and visualization.","metadata":{}},{"cell_type":"code","source":"# ⚠️ ALEXANDRIA MARKER - DO NOT DELETE (used for syncing outputs from Kaggle)\nprint(\"===ALEXANDRIA_CELL_1_START===\")\n\n# Setup & Imports\n\n# Install any missing packages (most are pre-installed in Kaggle, but include for completeness)\ntry:\n    import numpy\n    import pandas\n    import matplotlib\n    import sklearn\n    import torch\n    import torchvision\n    import PIL\n    import zipfile\n    import tqdm\nexcept ImportError:\n    import sys\n    !{sys.executable} -m pip install numpy pandas matplotlib scikit-learn torch torchvision pillow tqdm\n\n# Import libraries for data handling, image processing, modeling, and visualization\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom zipfile import ZipFile\nfrom tqdm import tqdm\nfrom sklearn.model_selection import train_test_split\nimport torch\nimport torchvision\nimport torchvision.transforms as transforms\n\n# Set random seeds for reproducibility\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed(SEED)\n\n# Define dataset path\nDATA_PATH = \"/kaggle/input/dogs-vs-cats-redux-kernels-edition\"\n\n# Print progress and check files\nprint(\"Setup complete. Imported all libraries and set random seeds.\")\nprint(\"Dataset files in DATA_PATH:\")\nfor file in os.listdir(DATA_PATH):\n    print(f\" - {file}\")","outputs":[],"metadata":{},"execution_count":null},{"cell_type":"markdown","source":"## Data Loading\n\nExtract images from ZIP files and load sample_submission.csv for later use.","metadata":{}},{"cell_type":"code","source":"# ⚠️ ALEXANDRIA MARKER - DO NOT DELETE (used for syncing outputs from Kaggle)\nprint(\"===ALEXANDRIA_CELL_2_START===\")\n\n# ===ALEXANDRIA_CELL_2_START===\n# Data Loading: Extract images from ZIP files and load sample_submission.csv\n\nimport shutil\n\n# Define extraction directories\nTRAIN_EXTRACT_DIR = os.path.join(DATA_PATH, \"train_images\")\nTEST_EXTRACT_DIR = os.path.join(DATA_PATH, \"test_images\")\n\n# Function to extract zip files\ndef extract_zip(zip_path, extract_to):\n    try:\n        with ZipFile(zip_path, 'r') as zip_ref:\n            zip_ref.extractall(extract_to)\n        print(f\"Extracted '{os.path.basename(zip_path)}' to '{extract_to}'.\")\n    except Exception as e:\n        print(f\"Error extracting '{zip_path}': {e}\")\n\n# Extract train.zip\ntrain_zip_path = os.path.join(DATA_PATH, \"train.zip\")\nif not os.path.exists(TRAIN_EXTRACT_DIR):\n    extract_zip(train_zip_path, TRAIN_EXTRACT_DIR)\nelse:\n    print(f\"Train images already extracted at '{TRAIN_EXTRACT_DIR}'.\")\n\n# Extract test.zip\ntest_zip_path = os.path.join(DATA_PATH, \"test.zip\")\nif not os.path.exists(TEST_EXTRACT_DIR):\n    extract_zip(test_zip_path, TEST_EXTRACT_DIR)\nelse:\n    print(f\"Test images already extracted at '{TEST_EXTRACT_DIR}'.\")\n\n# Load sample_submission.csv\nsample_submission_path = os.path.join(DATA_PATH, \"sample_submission.csv\")\ntry:\n    sample_submission = pd.read_csv(sample_submission_path)\n    print(f\"Loaded sample_submission.csv with shape: {sample_submission.shape}\")\nexcept Exception as e:\n    print(f\"Error loading sample_submission.csv: {e}\")\n\n# List image files in train and test directories\ndef list_image_files(directory):\n    try:\n        files = [f for f in os.listdir(directory) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]\n        print(f\"Found {len(files)} images in '{directory}'.\")\n        return sorted([os.path.join(directory, f) for f in files])\n    except Exception as e:\n        print(f\"Error listing images in '{directory}': {e}\")\n        return []\n\ntrain_image_paths = list_image_files(TRAIN_EXTRACT_DIR)\ntest_image_paths = list_image_files(TEST_EXTRACT_DIR)\n\n# Show a few sample paths\nprint(\"Sample train image paths:\", train_image_paths[:3])\nprint(\"Sample test image paths:\", test_image_paths[:3])","outputs":[],"metadata":{},"execution_count":null},{"cell_type":"markdown","source":"## Exploratory Data Analysis (EDA)\n\nAnalyze the distribution and characteristics of the dataset, including class balance and image properties.","metadata":{}},{"cell_type":"code","source":"# ⚠️ ALEXANDRIA MARKER - DO NOT DELETE (used for syncing outputs from Kaggle)\nprint(\"===ALEXANDRIA_CELL_3_START===\")\n\n# ===ALEXANDRIA_CELL_3_START===\n# Exploratory Data Analysis (EDA): Analyze class balance, visualize samples, check image properties\n\n# Count number of cat and dog images in training set\ncat_images = []\ndog_images = []\nfor path in train_image_paths:\n    fname = os.path.basename(path).lower()\n    if fname.startswith(\"cat\"):\n        cat_images.append(path)\n    elif fname.startswith(\"dog\"):\n        dog_images.append(path)\n\nnum_cats = len(cat_images)\nnum_dogs = len(dog_images)\nnum_total = len(train_image_paths)\n\nprint(f\"Total training images: {num_total}\")\nprint(f\"Number of cat images: {num_cats}\")\nprint(f\"Number of dog images: {num_dogs}\")\n\n# Visualize sample images from each class\ndef plot_sample_images(image_paths, title, n=5):\n    plt.figure(figsize=(15,3))\n    for i in range(n):\n        try:\n            img = Image.open(image_paths[i])\n            plt.subplot(1, n, i+1)\n            plt.imshow(img)\n            plt.axis('off')\n            plt.title(f\"{title} {i+1}\")\n        except Exception as e:\n            print(f\"Error loading image {image_paths[i]}: {e}\")\n    plt.suptitle(f\"Sample {title} Images\")\n    plt.show()\n\nprint(\"Displaying sample cat images:\")\nplot_sample_images(cat_images, \"Cat\", n=5)\n\nprint(\"Displaying sample dog images:\")\nplot_sample_images(dog_images, \"Dog\", n=5)\n\n# Check image dimensions and formats\nimage_shapes = []\nimage_modes = []\nimage_files_checked = 1000  # Check up to 1000 images for efficiency\n\nfor path in tqdm(train_image_paths[:image_files_checked], desc=\"Checking image properties\"):\n    try:\n        with Image.open(path) as img:\n            image_shapes.append(img.size)  # (width, height)\n            image_modes.append(img.mode)\n    except Exception as e:\n        print(f\"Error reading image {path}: {e}\")\n\n# Summarize image dimensions\nif image_shapes:\n    widths, heights = zip(*image_shapes)\n    print(f\"Checked {len(image_shapes)} images for size and format.\")\n    print(f\"Image width: min={min(widths)}, max={max(widths)}, mean={np.mean(widths):.1f}, median={np.median(widths)}\")\n    print(f\"Image height: min={min(heights)}, max={max(heights)}, mean={np.mean(heights):.1f}, median={np.median(heights)}\")\n    unique_shapes = set(image_shapes)\n    print(f\"Unique image sizes found: {len(unique_shapes)}\")\n    print(f\"Most common image size: {max(set(image_shapes), key=image_shapes.count)}\")\n    print(f\"Image modes: {set(image_modes)}\")\nelse:\n    print(\"No image shapes found.\")\n\n# Visualize distribution of image sizes\nplt.figure(figsize=(8,4))\nplt.hist(widths, bins=30, alpha=0.6, label='Widths')\nplt.hist(heights, bins=30, alpha=0.6, label='Heights')\nplt.xlabel('Pixels')\nplt.ylabel('Count')\nplt.title('Distribution of Image Widths and Heights (Sample)')\nplt.legend()\nplt.show()\n\n# Summarize findings\nprint(\"=== EDA Summary ===\")\nprint(f\"- Class balance: {num_cats} cats, {num_dogs} dogs (total: {num_total})\")\nif abs(num_cats - num_dogs) < 0.05 * num_total:\n    print(\"- The dataset is approximately balanced between cats and dogs.\")\nelse:\n    print(\"- The dataset is imbalanced between cats and dogs.\")\nprint(f\"- Most images are in mode(s): {set(image_modes)}\")\nprint(f\"- Most common image size: {max(set(image_shapes), key=image_shapes.count) if image_shapes else 'N/A'}\")\nprint(f\"- Unique image sizes in sample: {len(set(image_shapes))}\")\nprint(\"Sample images from each class displayed above.\")","outputs":[],"metadata":{},"execution_count":null},{"cell_type":"markdown","source":"## Preprocessing\n\nPrepare images for modeling by resizing, normalizing, and encoding labels.","metadata":{}},{"cell_type":"code","source":"# ⚠️ ALEXANDRIA MARKER - DO NOT DELETE (used for syncing outputs from Kaggle)\nprint(\"===ALEXANDRIA_CELL_4_START===\")\n\n# ===ALEXANDRIA_CELL_4_START===\n# Preprocessing: Resize, normalize images; encode labels; split train/val sets\n\nIMG_SIZE = (128, 128)  # Fixed image size (width, height)\nVAL_SPLIT = 0.2        # Validation split ratio\n\n# Helper: Encode label from filename ('cat'->0, 'dog'->1)\ndef encode_label(filename):\n    fname = os.path.basename(filename).lower()\n    if fname.startswith(\"cat\"):\n        return 0\n    elif fname.startswith(\"dog\"):\n        return 1\n    else:\n        raise ValueError(f\"Unknown label in filename: {filename}\")\n\n# Preprocess images: resize, normalize, encode label\ndef preprocess_image(path):\n    try:\n        img = Image.open(path).convert('RGB')\n        img = img.resize(IMG_SIZE, Image.LANCZOS)\n        arr = np.array(img, dtype=np.float32) / 255.0  # Normalize to [0,1]\n        label = encode_label(path)\n        return arr, label\n    except Exception as e:\n        print(f\"Error processing image {path}: {e}\")\n        return None, None\n\n# Prepare all training data\nX = []\ny = []\nprint(f\"Preprocessing {len(train_image_paths)} training images...\")\nfor path in tqdm(train_image_paths, desc=\"Preprocessing train images\"):\n    arr, label = preprocess_image(path)\n    if arr is not None:\n        X.append(arr)\n        y.append(label)\nX = np.array(X)\ny = np.array(y)\nprint(f\"Finished preprocessing. X shape: {X.shape}, y shape: {y.shape}\")\n\n# Split into train/validation sets\ntry:\n    X_train, X_val, y_train, y_val = train_test_split(\n        X, y, test_size=VAL_SPLIT, random_state=SEED, stratify=y\n    )\n    print(f\"Train/val split: {X_train.shape} train, {X_val.shape} val\")\nexcept Exception as e:\n    print(f\"Error splitting train/val: {e}\")\n\n# Show class distribution in splits\nprint(f\"Train class counts: cats={np.sum(y_train==0)}, dogs={np.sum(y_train==1)}\")\nprint(f\"Val class counts: cats={np.sum(y_val==0)}, dogs={np.sum(y_val==1)}\")","outputs":[],"metadata":{},"execution_count":null},{"cell_type":"markdown","source":"## Feature Engineering\n\nApply data augmentation and prepare image tensors for model input.","metadata":{}},{"cell_type":"code","source":"# ⚠️ ALEXANDRIA MARKER - DO NOT DELETE (used for syncing outputs from Kaggle)\nprint(\"===ALEXANDRIA_CELL_5_START===\")\n\nprint(\"===ALEXANDRIA_CELL_5_START===\")\n\n# Feature Engineering: Data Augmentation and PyTorch Dataset Preparation\n\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as transforms\n\n# Data augmentation and preprocessing transforms\ntrain_transforms = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomVerticalFlip(p=0.2),\n    transforms.RandomRotation(degrees=20),\n    transforms.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.15, hue=0.05),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nval_transforms = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# Custom Dataset for numpy arrays (already preprocessed and split)\nclass CatsDogsDataset(Dataset):\n    def __init__(self, images, labels, transform=None):\n        self.images = images\n        self.labels = labels\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.images)\n\n    def __getitem__(self, idx):\n        img = self.images[idx]\n        label = self.labels[idx]\n        try:\n            # Convert numpy image [H,W,C] in [0,1] to uint8 for PIL\n            img_uint8 = (img * 255).astype('uint8')\n            if self.transform:\n                img = self.transform(img_uint8)\n            else:\n                img = torch.from_numpy(img.transpose(2,0,1)).float()\n        except Exception as e:\n            print(f\"Error transforming image at idx {idx}: {e}\")\n            img = torch.zeros(3, *IMG_SIZE, dtype=torch.float32)\n        return img, label\n\n# Instantiate datasets\ntrain_dataset = CatsDogsDataset(X_train, y_train, transform=train_transforms)\nval_dataset = CatsDogsDataset(X_val, y_val, transform=val_transforms)\n\n# DataLoader parameters\nBATCH_SIZE = 64\nNUM_WORKERS = 2\n\n# DataLoaders for batching and shuffling\ntrain_loader = DataLoader(\n    train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS, pin_memory=True\n)\nval_loader = DataLoader(\n    val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True\n)\n\nprint(f\"Train DataLoader: {len(train_loader)} batches, batch size {BATCH_SIZE}\")\nprint(f\"Val DataLoader: {len(val_loader)} batches, batch size {BATCH_SIZE}\")\n\n# Show a batch of augmented images for sanity check\ndef show_augmented_batch(data_loader, class_names=[\"Cat\", \"Dog\"], n=8):\n    import matplotlib.pyplot as plt\n    batch = next(iter(data_loader))\n    images, labels = batch\n    plt.figure(figsize=(16,2))\n    for i in range(min(n, images.size(0))):\n        img = images[i].permute(1,2,0).cpu().numpy()\n        # Undo normalization for display\n        img = img * np.array([0.229, 0.224, 0.225]) + np.array([0.485, 0.456, 0.406])\n        img = np.clip(img, 0, 1)\n        plt.subplot(1, n, i+1)\n        plt.imshow(img)\n        plt.title(class_names[labels[i].item()])\n        plt.axis('off')\n    plt.suptitle(\"Augmented Training Images\")\n    plt.show()\n\nprint(\"Displaying a batch of augmented training images:\")\nshow_augmented_batch(train_loader, n=8)","outputs":[],"metadata":{},"execution_count":null},{"cell_type":"markdown","source":"## Model Training\n\nBuild and train a convolutional neural network (CNN) using transfer learning or a custom architecture.","metadata":{}},{"cell_type":"code","source":"# ⚠️ ALEXANDRIA MARKER - DO NOT DELETE (used for syncing outputs from Kaggle)\nprint(\"===ALEXANDRIA_CELL_6_START===\")\n\nprint(\"===ALEXANDRIA_CELL_6_START===\")\n\n# Model Training: Build and train a CNN using transfer learning (ResNet18), monitor validation, save best weights\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision import models\nfrom torch.optim import lr_scheduler\nimport copy\nimport time\nimport os\n\n# Device configuration\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {device}\")\n\n# Define model: Transfer learning with ResNet18 (pretrained on ImageNet)\ntry:\n    model = models.resnet18(pretrained=True)\n    # Replace final layer for binary classification\n    num_ftrs = model.fc.in_features\n    model.fc = nn.Linear(num_ftrs, 2)\n    model = model.to(device)\n    print(\"Loaded ResNet18 with modified final layer for 2 classes.\")\nexcept Exception as e:\n    print(f\"Error loading ResNet18: {e}\")\n    raise\n\n# Loss function and optimizer\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(model.parameters(), lr=1e-4)\nscheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2, verbose=True)\n\n# Training parameters\nNUM_EPOCHS = 10\nbest_val_acc = 0.0\nbest_model_wts = copy.deepcopy(model.state_dict())\nMODEL_SAVE_PATH = \"best_cats_dogs_resnet18.pth\"\n\ndef train_model(model, criterion, optimizer, scheduler, num_epochs=10):\n    since = time.time()\n    best_acc = 0.0\n    best_wts = copy.deepcopy(model.state_dict())\n    history = {\"train_loss\": [], \"val_loss\": [], \"train_acc\": [], \"val_acc\": []}\n    for epoch in range(num_epochs):\n        print(f\"\\nEpoch {epoch+1}/{num_epochs}\")\n        print('-' * 20)\n        for phase in ['train', 'val']:\n            if phase == 'train':\n                model.train()\n                dataloader = train_loader\n            else:\n                model.eval()\n                dataloader = val_loader\n            running_loss = 0.0\n            running_corrects = 0\n            total = 0\n            for inputs, labels in tqdm(dataloader, desc=f\"{phase.capitalize()} Batches\", leave=False):\n                inputs = inputs.to(device, non_blocking=True)\n                labels = labels.to(device, non_blocking=True)\n                optimizer.zero_grad()\n                with torch.set_grad_enabled(phase == 'train'):\n                    outputs = model(inputs)\n                    _, preds = torch.max(outputs, 1)\n                    loss = criterion(outputs, labels)\n                    if phase == 'train':\n                        loss.backward()\n                        optimizer.step()\n                running_loss += loss.item() * inputs.size(0)\n                running_corrects += torch.sum(preds == labels.data).item()\n                total += inputs.size(0)\n            epoch_loss = running_loss / total\n            epoch_acc = running_corrects / total\n            history[f\"{phase}_loss\"].append(epoch_loss)\n            history[f\"{phase}_acc\"].append(epoch_acc)\n            print(f\"{phase.capitalize()} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}\")\n            if phase == 'val':\n                scheduler.step(epoch_loss)\n                # Save best model\n                if epoch_acc > best_acc:\n                    best_acc = epoch_acc\n                    best_wts = copy.deepcopy(model.state_dict())\n                    try:\n                        torch.save(best_wts, MODEL_SAVE_PATH)\n                        print(f\"Saved new best model weights at epoch {epoch+1} with val acc {best_acc:.4f}\")\n                    except Exception as e:\n                        print(f\"Error saving model: {e}\")\n    time_elapsed = time.time() - since\n    print(f\"\\nTraining complete in {time_elapsed//60:.0f}m {time_elapsed%60:.0f}s\")\n    print(f\"Best val Acc: {best_acc:.4f}\")\n    model.load_state_dict(best_wts)\n    return model, history\n\ntry:\n    trained_model, history = train_model(model, criterion, optimizer, scheduler, num_epochs=NUM_EPOCHS)\n    print(\"Model training finished.\")\nexcept Exception as e:\n    print(f\"Error during training: {e}\")\n\n# Plot training history\ntry:\n    plt.figure(figsize=(12,5))\n    plt.subplot(1,2,1)\n    plt.plot(history[\"train_loss\"], label=\"Train Loss\")\n    plt.plot(history[\"val_loss\"], label=\"Val Loss\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Loss\")\n    plt.title(\"Loss over Epochs\")\n    plt.legend()\n    plt.subplot(1,2,2)\n    plt.plot(history[\"train_acc\"], label=\"Train Acc\")\n    plt.plot(history[\"val_acc\"], label=\"Val Acc\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Accuracy\")\n    plt.title(\"Accuracy over Epochs\")\n    plt.legend()\n    plt.tight_layout()\n    plt.show()\nexcept Exception as e:\n    print(f\"Error plotting training history: {e}\")\n\nprint(f\"Best model weights saved to: {MODEL_SAVE_PATH}\")","outputs":[],"metadata":{},"execution_count":null},{"cell_type":"markdown","source":"## Evaluation\n\nAssess model performance using accuracy and visualize training history.","metadata":{}},{"cell_type":"code","source":"# ⚠️ ALEXANDRIA MARKER - DO NOT DELETE (used for syncing outputs from Kaggle)\nprint(\"===ALEXANDRIA_CELL_7_START===\")\n\nprint(\"===ALEXANDRIA_CELL_7_START===\")\n\n# Evaluation: Assess model performance, plot training history, analyze misclassified examples\n\nimport torch\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# Load best model weights\ntry:\n    trained_model.load_state_dict(torch.load(MODEL_SAVE_PATH, map_location=device))\n    print(f\"Loaded best model weights from {MODEL_SAVE_PATH}\")\nexcept Exception as e:\n    print(f\"Error loading model weights: {e}\")\n\n# Evaluate on validation set\ntrained_model.eval()\nval_loss = 0.0\nval_correct = 0\nval_total = 0\nall_preds = []\nall_labels = []\nall_probs = []\nmisclassified = []\n\nwith torch.no_grad():\n    for inputs, labels in tqdm(val_loader, desc=\"Evaluating on validation set\"):\n        inputs = inputs.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True)\n        outputs = trained_model(inputs)\n        loss = criterion(outputs, labels)\n        val_loss += loss.item() * inputs.size(0)\n        probs = torch.softmax(outputs, dim=1)\n        _, preds = torch.max(outputs, 1)\n        val_correct += torch.sum(preds == labels).item()\n        val_total += inputs.size(0)\n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(labels.cpu().numpy())\n        all_probs.extend(probs.cpu().numpy())\n        # Collect misclassified examples\n        for i in range(inputs.size(0)):\n            if preds[i] != labels[i]:\n                misclassified.append({\n                    \"img\": inputs[i].cpu(),\n                    \"true\": labels[i].item(),\n                    \"pred\": preds[i].item(),\n                    \"prob\": probs[i].cpu().numpy()\n                })\n\nval_loss /= val_total\nval_acc = val_correct / val_total\nprint(f\"\\nValidation Loss: {val_loss:.4f}\")\nprint(f\"Validation Accuracy: {val_acc:.4f} ({val_correct}/{val_total})\")\n\n# Plot training and validation accuracy/loss curves\ntry:\n    plt.figure(figsize=(12,5))\n    plt.subplot(1,2,1)\n    plt.plot(history[\"train_loss\"], label=\"Train Loss\")\n    plt.plot(history[\"val_loss\"], label=\"Val Loss\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Loss\")\n    plt.title(\"Loss over Epochs\")\n    plt.legend()\n    plt.subplot(1,2,2)\n    plt.plot(history[\"train_acc\"], label=\"Train Acc\")\n    plt.plot(history[\"val_acc\"], label=\"Val Acc\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Accuracy\")\n    plt.title(\"Accuracy over Epochs\")\n    plt.legend()\n    plt.tight_layout()\n    plt.show()\nexcept Exception as e:\n    print(f\"Error plotting training history: {e}\")\n\n# Analyze misclassified examples\ndef show_misclassified(misclassified, class_names=[\"Cat\", \"Dog\"], n=8):\n    if len(misclassified) == 0:\n        print(\"No misclassified examples to display.\")\n        return\n    plt.figure(figsize=(16,2))\n    for i in range(min(n, len(misclassified))):\n        img = misclassified[i][\"img\"].permute(1,2,0).numpy()\n        # Undo normalization for display\n        img = img * np.array([0.229, 0.224, 0.225]) + np.array([0.485, 0.456, 0.406])\n        img = np.clip(img, 0, 1)\n        true_label = class_names[misclassified[i][\"true\"]]\n        pred_label = class_names[misclassified[i][\"pred\"]]\n        prob = misclassified[i][\"prob\"][misclassified[i][\"pred\"]]\n        plt.subplot(1, n, i+1)\n        plt.imshow(img)\n        plt.axis('off')\n        plt.title(f\"T:{true_label}\\nP:{pred_label}\\n{prob:.2f}\")\n    plt.suptitle(\"Misclassified Validation Images\")\n    plt.show()\n\nprint(f\"Number of misclassified validation examples: {len(misclassified)}\")\nshow_misclassified(misclassified, n=8)\n\n# Show confusion matrix\nfrom sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay\n\ntry:\n    cm = confusion_matrix(all_labels, all_preds)\n    disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=[\"Cat\", \"Dog\"])\n    disp.plot(cmap=plt.cm.Blues)\n    plt.title(\"Confusion Matrix - Validation Set\")\n    plt.show()\nexcept Exception as e:\n    print(f\"Error displaying confusion matrix: {e}\")\n\nprint(\"Evaluation complete.\")","outputs":[],"metadata":{},"execution_count":null},{"cell_type":"markdown","source":"## Submission/Results\n\nGenerate predictions for test set and format results for Kaggle submission.","metadata":{}},{"cell_type":"code","source":"# ⚠️ ALEXANDRIA MARKER - DO NOT DELETE (used for syncing outputs from Kaggle)\nprint(\"===ALEXANDRIA_CELL_8_START===\")\n\nprint(\"===ALEXANDRIA_CELL_8_START===\")\n\n# Submission/Results: Generate predictions for test set and format results for Kaggle submission\n\nimport torch\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm import tqdm\nimport os\n\n# Paths and constants\nTEST_EXTRACT_DIR = os.path.join(DATA_PATH, \"test_images\")\nSAMPLE_SUB_PATH = os.path.join(DATA_PATH, \"sample_submission.csv\")\nSUBMISSION_PATH = \"submission.csv\"\nIMG_SIZE = (128, 128)\n\n# Use the same normalization as validation\ntest_transforms = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# Helper: Preprocess test image (no label)\ndef preprocess_test_image(path):\n    try:\n        img = Image.open(path).convert('RGB')\n        img = img.resize(IMG_SIZE, Image.LANCZOS)\n        arr = np.array(img, dtype=np.uint8)\n        img_tensor = test_transforms(arr)\n        return img_tensor\n    except Exception as e:\n        print(f\"Error processing test image {path}: {e}\")\n        return None\n\n# Load sample_submission.csv to get test IDs and required format\ntry:\n    sample_submission = pd.read_csv(SAMPLE_SUB_PATH)\n    print(f\"Loaded sample_submission.csv with shape: {sample_submission.shape}\")\nexcept Exception as e:\n    print(f\"Error loading sample_submission.csv: {e}\")\n    raise\n\n# Map test image filenames to their full paths\ntest_image_files = [f for f in os.listdir(TEST_EXTRACT_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]\ntest_image_files_sorted = sorted(test_image_files, key=lambda x: int(os.path.splitext(x)))\ntest_image_paths = [os.path.join(TEST_EXTRACT_DIR, fname) for fname in test_image_files_sorted]\n\n# Sanity check: Ensure number and order match sample_submission\nif len(sample_submission) != len(test_image_paths):\n    print(f\"Warning: sample_submission ({len(sample_submission)}) and test images ({len(test_image_paths)}) count mismatch.\")\n\n# Prepare model for inference\ntrained_model.eval()\n\n# Predict on test set in batches\nBATCH_SIZE = 64\ntest_preds = []\ntest_ids = []\n\nwith torch.no_grad():\n    for i in tqdm(range(0, len(test_image_paths), BATCH_SIZE), desc=\"Predicting on test set\"):\n        batch_paths = test_image_paths[i:i+BATCH_SIZE]\n        batch_imgs = []\n        batch_ids = []\n        for path in batch_paths:\n            img_tensor = preprocess_test_image(path)\n            if img_tensor is not None:\n                batch_imgs.append(img_tensor)\n                img_id = int(os.path.splitext(os.path.basename(path)))\n                batch_ids.append(img_id)\n            else:\n                # If image fails to load, predict 0.5 (neutral)\n                batch_imgs.append(torch.zeros(3, *IMG_SIZE))\n                batch_ids.append(int(os.path.splitext(os.path.basename(path))))\n        if len(batch_imgs) == 0:\n            continue\n        batch_tensor = torch.stack(batch_imgs).to(device)\n        outputs = trained_model(batch_tensor)\n        probs = torch.softmax(outputs, dim=1)[:,1]  # Probability for class 'dog' (label 1)\n        test_preds.extend(probs.cpu().numpy())\n        test_ids.extend(batch_ids)\n\n# Format predictions for submission\nsubmission_df = pd.DataFrame({\n    \"id\": test_ids,\n    \"label\": test_preds\n})\n\n# Ensure correct order as in sample_submission\nsubmission_df = submission_df.set_index(\"id\").loc[sample_submission[\"id\"]].reset_index()\n\n# Save to CSV\ntry:\n    submission_df.to_csv(SUBMISSION_PATH, index=False)\n    print(f\"Submission file saved: {SUBMISSION_PATH} (shape: {submission_df.shape})\")\nexcept Exception as e:\n    print(f\"Error saving submission file: {e}\")\n\n# Show first few rows of submission\nprint(\"Sample submission preview:\")\nprint(submission_df.head())","outputs":[],"metadata":{},"execution_count":null}],"nbformat":4,"nbformat_minor":4}