{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ================================================================\n# GALAXY ZOO - RESNET50 TRAINING (ALL-IN-ONE CELL)\n# Training Device: Kaggle GPU (CUDA)\n# Epochs: 30 | Classes: 2 (Elliptical vs Spiral)\n# ================================================================\n\nimport os\nimport glob\nimport json\nimport time\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image, ImageFile\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    classification_report, \n    confusion_matrix, \n    f1_score, \n    recall_score,\n    accuracy_score\n)\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\n\nImageFile.LOAD_TRUNCATED_IMAGES = True","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:56:24.387659Z","iopub.execute_input":"2026-09-23T18:56:24.387896Z","iopub.status.idle":"2026-09-23T18:56:43.565146Z","shell.execute_reply.started":"2026-09-23T18:56:24.387873Z","shell.execute_reply":"2026-09-23T18:56:43.564225Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 1: SETUP GPU & VERIFY DEVICE\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(\"STEP 1: SETUP GPU & VERIFY DEVICE\")\nprint(\"=\"*70 + \"\\n\")\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"✓ Device: {device}\")\n\nif torch.cuda.is_available():\n    print(f\"✓ GPU Name: {torch.cuda.get_device_name(0)}\")\n    print(f\"✓ GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.2f} GB\")\nelse:\n    print(\"⚠️ WARNING: GPU not available, using CPU\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:56:49.107223Z","iopub.execute_input":"2026-09-23T18:56:49.108265Z","iopub.status.idle":"2026-09-23T18:56:49.440528Z","shell.execute_reply.started":"2026-09-23T18:56:49.108213Z","shell.execute_reply":"2026-09-23T18:56:49.438805Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======================================================================\n# STEP 2: DOWNLOAD / LOCATE GALAXY ZOO DATASET\n# ======================================================================\nimport os\nimport zipfile\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"STEP 2: DOWNLOAD GALAXY ZOO DATASET\")\nprint(\"=\"*70 + \"\\n\")\n\ndef find_competition_input_dir():\n    \"\"\"Recursively locate the Galaxy Zoo competition folder under\n    /kaggle/input/, regardless of how deeply Kaggle nests it.\"\"\"\n    base = \"/kaggle/input\"\n    if not os.path.isdir(base):\n        return None\n    for dirpath, _, filenames in os.walk(base):\n        if any(\"training_solutions_rev1\" in f for f in filenames) or \\\n           any(\"images_training_rev1\" in f for f in filenames):\n            return dirpath\n    return None\n\ninput_dir = find_competition_input_dir()\n\nprint(\"Checking raw input files...\")\nif input_dir is None:\n    available = []\n    if os.path.isdir(\"/kaggle/input\"):\n        for dirpath, dirnames, _ in os.walk(\"/kaggle/input\"):\n            available.append(dirpath)\n    raise FileNotFoundError(\n        \"❌ Could not find the Galaxy Zoo competition data under /kaggle/input/.\\n\"\n        f\"Directories found while searching: {available}\\n\"\n        \"Make sure you've added the competition dataset via '+ Add Input' in the \"\n        \"notebook sidebar, and that you've joined the competition at:\\n\"\n        \"https://www.kaggle.com/competitions/galaxy-zoo-the-galaxy-challenge/data\"\n    )\n\nprint(f\"✓ Found input directory: {input_dir}\")\nprint(\"Contents:\", os.listdir(input_dir))\n\nwork_dir = \"/kaggle/working/galaxy-zoo-the-galaxy-challenge\"\nos.makedirs(work_dir, exist_ok=True)\n\n\ndef find_image_dir(root, expected_name):\n    \"\"\"Return the folder containing the .jpg images, wherever the zip put them.\"\"\"\n    candidate = os.path.join(root, expected_name)\n    if os.path.isdir(candidate) and any(f.endswith(\".jpg\") for f in os.listdir(candidate)):\n        return candidate\n    for dirpath, _, filenames in os.walk(root):\n        if any(f.endswith(\".jpg\") for f in filenames):\n            return dirpath\n    return candidate  # not found yet; caller will report the failure\n\n\ndef extract_if_needed(zip_name, already_done):\n    \"\"\"Extract zip_name from input_dir into work_dir, unless already_done() is True.\"\"\"\n    if already_done():\n        print(f\"✓ Already extracted: {zip_name}\")\n        return\n    zip_path = os.path.join(input_dir, zip_name)\n    if not os.path.exists(zip_path):\n        print(f\"❌ {zip_name} not found in {input_dir}\")\n        return\n    print(f\"Extracting {zip_name} → {work_dir} (large file — this can take a few minutes)...\")\n    with zipfile.ZipFile(zip_path, \"r\") as zf:\n        zf.extractall(work_dir)\n    print(f\"✓ Extracted {zip_name}\")\n\n\n# --- Training images ---\nextract_if_needed(\n    \"images_training_rev1.zip\",\n    already_done=lambda: os.path.isdir(os.path.join(work_dir, \"images_training_rev1\"))\n    and len(os.listdir(os.path.join(work_dir, \"images_training_rev1\"))) > 0,\n)\ndataset_root = find_image_dir(work_dir, \"images_training_rev1\")\n\n# --- Training solutions (labels) ---\nsolutions_file = os.path.join(work_dir, \"training_solutions_rev1.csv\")\nextract_if_needed(\n    \"training_solutions_rev1.zip\",\n    already_done=lambda: os.path.exists(solutions_file),\n)\n\nprint(\"\\nFinal check:\")\nif os.path.isdir(dataset_root):\n    n_images = len([f for f in os.listdir(dataset_root) if f.endswith(\".jpg\")])\n    print(f\"✓ dataset_root: {dataset_root}  ({n_images} images)\")\nelse:\n    print(f\"❌ dataset_root still missing: {dataset_root}\")\n\nif os.path.exists(solutions_file):\n    print(f\"✓ solutions_file: {solutions_file}\")\nelse:\n    print(f\"❌ solutions_file still missing: {solutions_file}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:56:52.304692Z","iopub.execute_input":"2026-09-23T18:56:52.305048Z","iopub.status.idle":"2026-09-23T18:57:06.52614Z","shell.execute_reply.started":"2026-09-23T18:56:52.305023Z","shell.execute_reply":"2026-09-23T18:57:06.525461Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 3: LOAD & PREPARE GALAXY ZOO DATA\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(\"STEP 3: LOAD & PREPARE GALAXY ZOO DATA\")\nprint(\"=\"*70 + \"\\n\")\n\nprint(\"Loading labels...\")\nsolutions_df = pd.read_csv(solutions_file)\nprint(f\"✓ Solutions shape: {solutions_df.shape}\")\n\nprint(\"\\nFinding image files...\")\nimage_files = glob.glob(f\"{dataset_root}/*.jpg\")\nprint(f\"✓ Found {len(image_files)} images\")\n\n# Extract GalaxyID from filename\nimage_data = []\nfor img_path in image_files:\n    filename = os.path.basename(img_path)\n    galaxy_id = int(filename.split('.')[0])\n    image_data.append({\n        'galaxy_id': galaxy_id,\n        'file_path': img_path\n    })\n\nimages_df = pd.DataFrame(image_data)\n\n# Merge with solutions\nmerged_df = images_df.merge(\n    solutions_df,\n    left_on='galaxy_id',\n    right_on='GalaxyID',\n    how='inner'\n)\n\nprint(f\"✓ Merged dataset: {merged_df.shape}\")\n\n# ===== DETERMINE DOMINANT MORPHOLOGY =====\nprint(\"\\nDeriving dominant morphology classes...\")\n\n# Classify as \"Elliptical\" if Class1.1 > 0.5, else \"Spiral/Featured\"\nmerged_df['morphology_class'] = merged_df['Class1.1'].apply(\n    lambda x: 'elliptical' if x > 0.5 else 'spiral'\n)\n\nprint(\"\\nMorphology distribution:\")\nprint(merged_df['morphology_class'].value_counts())\n\nclass_names = ['elliptical', 'spiral']\nlabel_to_idx = {label: idx for idx, label in enumerate(class_names)}\nidx_to_label = {idx: label for label, idx in label_to_idx.items()}\n\nprint(f\"\\n✓ Classes: {class_names}\")\nprint(f\"✓ Total images: {len(merged_df)}\")\n\ndata_df = merged_df[['file_path', 'morphology_class']].copy()\ndata_df.columns = ['file_path', 'label']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:57:28.663003Z","iopub.execute_input":"2026-09-23T18:57:28.663478Z","iopub.status.idle":"2026-09-23T18:57:29.268891Z","shell.execute_reply.started":"2026-09-23T18:57:28.663447Z","shell.execute_reply":"2026-09-23T18:57:29.267883Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 4: VALIDATE IMAGES & CREATE TRAIN/VAL/TEST SPLIT\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(\"STEP 4: VALIDATE IMAGES & CREATE TRAIN/VAL/TEST SPLIT\")\nprint(\"=\"*70 + \"\\n\")\n\ndef is_valid_image(path):\n    try:\n        with Image.open(path) as img:\n            img.verify()\n        return True\n    except Exception:\n        return False\n\nprint(\"Validating images...\")\ndata_df[\"is_valid\"] = data_df[\"file_path\"].apply(is_valid_image)\ndata_df = data_df[data_df[\"is_valid\"] == True].drop(columns=[\"is_valid\"]).reset_index(drop=True)\nprint(f\"✓ Valid images: {data_df.shape[0]}\\n\")\n\n# First carve off the TEST set — untouched until Step 10\ntrain_df, test_df = train_test_split(\n    data_df,\n    test_size=0.15,\n    stratify=data_df[\"label\"],\n    random_state=42\n)\n\n# Then carve a VALIDATION set out of what's left (used for early stopping / LR schedule)\ntrain_df, val_df = train_test_split(\n    train_df,\n    test_size=0.15,\n    stratify=train_df[\"label\"],\n    random_state=42\n)\n\nprint(f\"Train set: {len(train_df)}\")\nprint(f\"Val set:   {len(val_df)}\")\nprint(f\"Test set:  {len(test_df)}\")\n\nprint(f\"\\nTrain class counts:\")\nprint(train_df[\"label\"].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:57:33.207457Z","iopub.execute_input":"2026-09-23T18:57:33.207906Z","iopub.status.idle":"2026-09-23T18:57:37.34853Z","shell.execute_reply.started":"2026-09-23T18:57:33.207878Z","shell.execute_reply":"2026-09-23T18:57:37.347698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 5: CREATE IMAGE TRANSFORMS\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(\"STEP 5: CREATE IMAGE TRANSFORMS\")\nprint(\"=\"*70 + \"\\n\")\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((256, 256)),\n    transforms.RandomResizedCrop(224, scale=(0.75, 1.0)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(20),\n    transforms.ColorJitter(brightness=0.1, contrast=0.1),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\ntest_transform = transforms.Compose([\n    transforms.Resize((256, 256)),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\nprint(\"✓ Transforms ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:57:41.065403Z","iopub.execute_input":"2026-09-23T18:57:41.065808Z","iopub.status.idle":"2026-09-23T18:57:41.073493Z","shell.execute_reply.started":"2026-09-23T18:57:41.06578Z","shell.execute_reply":"2026-09-23T18:57:41.072627Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 6: CREATE DATASET CLASS\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(\"STEP 6: CREATE DATASET CLASS\")\nprint(\"=\"*70 + \"\\n\")\n\nclass GalaxyZooDataset(Dataset):\n    def __init__(self, dataframe, label_to_idx, transform=None):\n        self.df = dataframe.reset_index(drop=True)\n        self.label_to_idx = label_to_idx\n        self.transform = transform\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        image_path = self.df.loc[idx, \"file_path\"]\n        label_name = self.df.loc[idx, \"label\"]\n        label = self.label_to_idx[label_name]\n        \n        image = Image.open(image_path).convert(\"RGB\")\n        \n        if self.transform:\n            image = self.transform(image)\n        \n        return image, label\n\nprint(\"✓ Dataset class ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:57:43.66976Z","iopub.execute_input":"2026-09-23T18:57:43.670369Z","iopub.status.idle":"2026-09-23T18:57:43.677511Z","shell.execute_reply.started":"2026-09-23T18:57:43.670338Z","shell.execute_reply":"2026-09-23T18:57:43.676658Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 7: CREATE DATALOADERS (+ SPEED TEST)\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(\"STEP 7: CREATE DATALOADERS\")\nprint(\"=\"*70 + \"\\n\")\n\nimport os, time\n\nbatch_size = 64\nn_workers = os.cpu_count()\nprint(f\"CPU cores available: {n_workers}\")\n\ntrain_dataset = GalaxyZooDataset(train_df, label_to_idx, transform=train_transform)\nval_dataset   = GalaxyZooDataset(val_df, label_to_idx, transform=test_transform)\ntest_dataset  = GalaxyZooDataset(test_df, label_to_idx, transform=test_transform)\n\ntrain_loader = DataLoader(\n    train_dataset, batch_size=batch_size, shuffle=True,\n    num_workers=n_workers, pin_memory=True,\n    persistent_workers=True, prefetch_factor=4\n)\n\nval_loader = DataLoader(\n    val_dataset, batch_size=batch_size, shuffle=False,\n    num_workers=n_workers, pin_memory=True,\n    persistent_workers=True, prefetch_factor=4\n)\n\ntest_loader = DataLoader(\n    test_dataset, batch_size=batch_size, shuffle=False,\n    num_workers=n_workers, pin_memory=True,\n    persistent_workers=True, prefetch_factor=4\n)\n\nimages, labels = next(iter(train_loader))\nprint(f\"✓ DataLoaders ready\")\nprint(f\"Batch shape: {images.shape}\")\nprint(f\"Train batches/epoch: {len(train_loader)} | Val batches: {len(val_loader)} | Test batches: {len(test_loader)}\")\n\n# ---- SPEED TEST: data loading only (no model) ----\nn_test = 50\nt0 = time.time()\nfor i, (imgs, lbls) in enumerate(train_loader):\n    if i + 1 >= n_test:\n        break\ndata_time = (time.time() - t0) / n_test\nest_epoch = data_time * len(train_loader)\nprint(f\"\\nData loading: {data_time*1000:.0f} ms/batch \"\n      f\"→ ~{est_epoch:.0f}s per epoch just to load training data\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:57:46.652665Z","iopub.execute_input":"2026-09-23T18:57:46.653428Z","iopub.status.idle":"2026-09-23T18:58:01.259579Z","shell.execute_reply.started":"2026-09-23T18:57:46.653387Z","shell.execute_reply":"2026-09-23T18:58:01.255153Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 8: INITIALIZE RESNET50 MODEL\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(\"STEP 8: INITIALIZE RESNET50 MODEL\")\nprint(\"=\"*70 + \"\\n\")\n\nimport random\ntorch.manual_seed(42); np.random.seed(42); random.seed(42)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(42)\n\nMODEL_NAME = \"ResNet50\"\nBEST_PATH  = \"/kaggle/working/resnet50_galaxyzoo_best.pth\"\n\nprint(f\"Device: {device}\\n\")\n\nmodel = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)\n\nnum_classes = len(class_names)\nmodel.fc = nn.Linear(model.fc.in_features, num_classes)   # Linear(2048, 2)\n\nmodel = model.to(device)\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)\nscaler = torch.amp.GradScaler('cuda')\n\nprint(f\"✓ {MODEL_NAME} initialized\")\nprint(f\"✓ Classes: {class_names}\")\nprint(f\"✓ Number of classes: {num_classes}\")\nprint(f\"✓ Mixed precision enabled\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:58:06.038012Z","iopub.execute_input":"2026-09-23T18:58:06.038419Z","iopub.status.idle":"2026-09-23T18:58:07.206092Z","shell.execute_reply.started":"2026-09-23T18:58:06.03837Z","shell.execute_reply":"2026-09-23T18:58:07.205412Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 9: TRAINING LOOP (30 EPOCHS - KAGGLE GPU)\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(f\"STEP 9: TRAINING LOOP (30 EPOCHS - GPU) - {MODEL_NAME}\")\nprint(\"=\"*70 + \"\\n\")\n\nSLUG = MODEL_NAME.lower()          # \"resnet50\" - used in all output filenames\n\nepochs = 30\nhistory = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': [], 'lr': []}\n\nbest_val_acc = 0\nbest_epoch = 0\npatience = 30\npatience_counter = 0\n\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode='min', factor=0.5, patience=2\n)\n\nstart_time = time.time()\n\nfor epoch in range(epochs):\n    epoch_start = time.time()\n\n    # ====== TRAINING PHASE ======\n    model.train()\n    running_loss, correct, total = 0.0, 0, 0\n\n    for images, labels in train_loader:\n        images = images.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True)\n\n        optimizer.zero_grad(set_to_none=True)\n\n        with torch.amp.autocast('cuda'):\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        running_loss += loss.item()\n        _, predicted = torch.max(outputs, 1)\n        total += labels.size(0)\n        correct += (predicted == labels).sum().item()\n\n    train_loss = running_loss / len(train_loader)\n    train_acc = correct / total\n\n    # ====== VALIDATION PHASE (val_loader, NOT test_loader) ======\n    model.eval()\n    val_running_loss, val_correct, val_total = 0.0, 0, 0\n\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images = images.to(device, non_blocking=True)\n            labels = labels.to(device, non_blocking=True)\n\n            with torch.amp.autocast('cuda'):\n                outputs = model(images)\n                loss = criterion(outputs, labels)\n\n            val_running_loss += loss.item()\n            _, predicted = torch.max(outputs, 1)\n            val_total += labels.size(0)\n            val_correct += (predicted == labels).sum().item()\n\n    val_loss = val_running_loss / len(val_loader)\n    val_acc = val_correct / val_total\n\n    scheduler.step(val_loss)\n    current_lr = optimizer.param_groups[0]['lr']\n\n    history['train_loss'].append(train_loss)\n    history['train_acc'].append(train_acc)\n    history['val_loss'].append(val_loss)\n    history['val_acc'].append(val_acc)\n    history['lr'].append(current_lr)\n\n    epoch_time = time.time() - epoch_start\n    print(f\"Epoch [{epoch+1:2d}/{epochs}] | \"\n          f\"Train: L={train_loss:.4f} A={train_acc:.4f} | \"\n          f\"Val: L={val_loss:.4f} A={val_acc:.4f} | \"\n          f\"LR={current_lr:.2e} | {epoch_time:.0f}s\", end=\"\")\n\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        best_epoch = epoch + 1\n        patience_counter = 0\n        # Save weights TOGETHER with the class list, so the ONNX export\n        # can verify the class order instead of trusting a separate file.\n        torch.save({\n            \"state_dict\": model.state_dict(),\n            \"classes\": class_names,\n            \"architecture\": \"torchvision.models.resnet50\",\n            \"head\": \"fc = Linear(2048, 2)\",\n            \"epoch\": best_epoch,\n            \"val_acc\": float(best_val_acc),\n        }, BEST_PATH)\n        print(\" ✓ BEST\")\n    else:\n        patience_counter += 1\n        print()\n        if patience_counter >= patience:\n            print(f\"\\n⏹ Early stopping at epoch {epoch+1}\")\n            break\n\nelapsed_time = (time.time() - start_time) / 60\nprint(f\"\\n✓ Training completed in {elapsed_time:.1f} minutes\")\n\nckpt = torch.load(BEST_PATH, map_location=device)\nassert ckpt[\"classes\"] == class_names, \"Class order in checkpoint does not match!\"\nmodel.load_state_dict(ckpt[\"state_dict\"])\nprint(f\"✓ Loaded best model (epoch {best_epoch}, val acc = {best_val_acc:.4f}) from {BEST_PATH}\")\n\nfrom IPython.display import FileLink\ndisplay(FileLink(os.path.basename(BEST_PATH)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T18:58:11.330446Z","iopub.execute_input":"2026-09-23T18:58:11.330744Z","iopub.status.idle":"2026-09-23T20:37:17.926788Z","shell.execute_reply.started":"2026-09-23T18:58:11.330722Z","shell.execute_reply":"2026-09-23T20:37:17.925911Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 10: EVALUATE ON TEST SET\n# ================================================================\nimport numpy as np\nfrom sklearn.metrics import (accuracy_score, f1_score, recall_score,\n                             confusion_matrix, classification_report)\n\nprint(\"\\n\" + \"=\"*70)\nprint(f\"STEP 10: EVALUATE ON TEST SET - {MODEL_NAME}\")\nprint(\"=\"*70 + \"\\n\")\n\nckpt = torch.load(BEST_PATH, map_location=device)\nassert ckpt[\"classes\"] == class_names, \"Class order in checkpoint does not match!\"\nmodel.load_state_dict(ckpt[\"state_dict\"])\nmodel.eval()\n\nall_preds, all_labels = [], []\nprint(\"Evaluating test set...\\n\")\n\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images = images.to(device, non_blocking=True)\n        with torch.amp.autocast('cuda'):\n            outputs = model(images)\n        all_preds.extend(torch.argmax(outputs, dim=1).cpu().numpy())\n        all_labels.extend(labels.numpy())\n\nall_preds = np.array(all_preds)\nall_labels = np.array(all_labels)\n\nlabel_ids = list(range(len(class_names)))\n\noverall_accuracy = accuracy_score(all_labels, all_preds)\nmacro_f1 = f1_score(all_labels, all_preds, labels=label_ids, average='macro', zero_division=0)\nmacro_recall = recall_score(all_labels, all_preds, labels=label_ids, average='macro', zero_division=0)\nconfusion_mat = confusion_matrix(all_labels, all_preds, labels=label_ids)\nclass_report = classification_report(all_labels, all_preds, labels=label_ids,\n                                     target_names=class_names, output_dict=True, zero_division=0)\n\nprint(f\"✓ Test Accuracy: {overall_accuracy:.4f}\")\nprint(f\"✓ Macro F1: {macro_f1:.4f}\")\nprint(f\"✓ Macro Recall: {macro_recall:.4f}\\n\")\nprint(classification_report(all_labels, all_preds, labels=label_ids,\n                            target_names=class_names, zero_division=0))\nprint(\"Confusion matrix (rows = true, cols = predicted):\")\nprint(confusion_mat)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T20:39:16.000927Z","iopub.execute_input":"2026-09-23T20:39:16.001473Z","iopub.status.idle":"2026-09-23T20:39:36.171216Z","shell.execute_reply.started":"2026-09-23T20:39:16.001428Z","shell.execute_reply":"2026-09-23T20:39:36.170339Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 11: TRAINING ACCURACY ON CLEAN (NON-AUGMENTED) IMAGES\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(f\"STEP 11: CALCULATE TRAINING ACCURACY - {MODEL_NAME}\")\nprint(\"=\"*70 + \"\\n\")\n\n# Same training images, but with test_transform (no random crop/flip/rotation/jitter),\n# so this number is comparable with the test accuracy.\ntrain_eval_dataset = GalaxyZooDataset(train_df, label_to_idx, transform=test_transform)\ntrain_eval_loader = DataLoader(\n    train_eval_dataset, batch_size=batch_size, shuffle=False,\n    num_workers=n_workers, pin_memory=True\n)\n\nmodel.eval()\ntrain_preds, train_labels = [], []\n\nwith torch.no_grad():\n    for images, labels in train_eval_loader:\n        images = images.to(device, non_blocking=True)\n        with torch.amp.autocast('cuda'):\n            outputs = model(images)\n        train_preds.extend(torch.argmax(outputs, dim=1).cpu().numpy())\n        train_labels.extend(labels.numpy())\n\ntrain_preds = np.array(train_preds)\ntrain_labels = np.array(train_labels)\n\ntrain_accuracy = accuracy_score(train_labels, train_preds)\n\nprint(f\"✓ Training Accuracy (clean images): {train_accuracy:.4f}\\n\")\nprint(\"=\"*70)\nprint(\"CLASSIFICATION REPORT (TEST SET)\")\nprint(\"=\"*70)\nprint(classification_report(all_labels, all_preds, target_names=class_names))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T20:39:54.210745Z","iopub.execute_input":"2026-09-23T20:39:54.211359Z","iopub.status.idle":"2026-09-23T20:41:24.947772Z","shell.execute_reply.started":"2026-09-23T20:39:54.211306Z","shell.execute_reply":"2026-09-23T20:41:24.94697Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 12: CREATE 4 VISUALIZATIONS\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(f\"STEP 12: CREATE 4 VISUALIZATIONS - {MODEL_NAME}\")\nprint(\"=\"*70 + \"\\n\")\n\nfig, axes = plt.subplots(2, 2, figsize=(14, 10))\nfig.suptitle(f\"{MODEL_NAME} on Galaxy Zoo\", fontsize=16, fontweight='bold')\n\n# Graph 1: Train vs Val Accuracy\naxes[0, 0].plot(history['train_acc'], label='Training', marker='o', linewidth=2)\naxes[0, 0].plot(history['val_acc'], label='Validation', marker='s', linewidth=2)\naxes[0, 0].set_xlabel('Epoch', fontsize=12)\naxes[0, 0].set_ylabel('Accuracy', fontsize=12)\naxes[0, 0].set_title('Training vs Validation Accuracy', fontsize=14, fontweight='bold')\naxes[0, 0].legend(fontsize=11)\naxes[0, 0].grid(True, alpha=0.3)\n\n# Graph 2: Train vs Val Loss\naxes[0, 1].plot(history['train_loss'], label='Training', marker='o', linewidth=2)\naxes[0, 1].plot(history['val_loss'], label='Validation', marker='s', linewidth=2)\naxes[0, 1].set_xlabel('Epoch', fontsize=12)\naxes[0, 1].set_ylabel('Loss', fontsize=12)\naxes[0, 1].set_title('Training vs Validation Loss', fontsize=14, fontweight='bold')\naxes[0, 1].legend(fontsize=11)\naxes[0, 1].grid(True, alpha=0.3)\n\n# Graph 3: Confusion Matrix\nsns.heatmap(confusion_mat, annot=True, fmt='d', cmap='Blues',\n            xticklabels=class_names, yticklabels=class_names,\n            ax=axes[1, 0], cbar=True, cbar_kws={'label': 'Count'})\naxes[1, 0].set_title('Confusion Matrix (Test Set)', fontsize=14, fontweight='bold')\naxes[1, 0].set_ylabel('True Label', fontsize=12)\naxes[1, 0].set_xlabel('Predicted Label', fontsize=12)\n\n# Graph 4: F1 & Recall per Class\nf1_scores = [class_report[c]['f1-score'] for c in class_names]\nrecalls = [class_report[c]['recall'] for c in class_names]\n\nx = np.arange(len(class_names))\nwidth = 0.35\n\naxes[1, 1].bar(x - width/2, f1_scores, width, label='F1 Score', alpha=0.8, color='skyblue')\naxes[1, 1].bar(x + width/2, recalls, width, label='Recall', alpha=0.8, color='orange')\naxes[1, 1].set_xlabel('Class', fontsize=12)\naxes[1, 1].set_ylabel('Score', fontsize=12)\naxes[1, 1].set_title('F1 Score & Recall per Class', fontsize=14, fontweight='bold')\naxes[1, 1].set_xticks(x)\naxes[1, 1].set_xticklabels(class_names, rotation=45, ha='right')\naxes[1, 1].legend(fontsize=11)\naxes[1, 1].grid(True, axis='y', alpha=0.3)\naxes[1, 1].set_ylim([0, 1.0])\n\nplt.tight_layout()\nfig_path = f'/kaggle/working/galaxyzoo_{SLUG}_results.png'\nplt.savefig(fig_path, dpi=300, bbox_inches='tight')\nprint(f\"✓ Graph saved: {os.path.basename(fig_path)}\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T20:41:55.288773Z","iopub.execute_input":"2026-09-23T20:41:55.289288Z","iopub.status.idle":"2026-09-23T20:41:57.542335Z","shell.execute_reply.started":"2026-09-23T20:41:55.289251Z","shell.execute_reply":"2026-09-23T20:41:57.541563Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 13: SAVE RESULTS JSON\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(f\"STEP 13: SAVE RESULTS JSON - {MODEL_NAME}\")\nprint(\"=\"*70 + \"\\n\")\n\nresults = {\n    \"model_name\": MODEL_NAME,\n    \"architecture_source\": \"torchvision.models.resnet50 (weights=IMAGENET1K_V2)\",\n    \"head\": \"fc = Linear(2048, 2)\",\n    \"checkpoint_format\": \"dict: state_dict + classes + architecture + head + epoch + val_acc\",\n    \"dataset\": \"Galaxy Zoo - The Galaxy Challenge (images_training_rev1)\",\n    \"label_rule\": \"Class1.1 > 0.5 -> elliptical, else spiral\",\n    \"split\": {\"train\": len(train_df), \"val\": len(val_df), \"test\": len(test_df),\n              \"method\": \"stratified train_test_split, test 0.15 then val 0.15, random_state=42\"},\n    \"seed\": 42,\n    \"training_device\": \"Kaggle GPU (CUDA)\",\n    \"classes\": class_names,\n    \"num_classes\": len(class_names),\n    \"model_path\": BEST_PATH,\n    \"class_mapping\": label_to_idx,\n    \"idx_to_label\": {str(k): v for k, v in idx_to_label.items()},\n    \"eval_preprocessing\": {\n        \"resize_exact\": [256, 256],\n        \"center_crop\": 224,\n        \"interpolation\": \"bilinear\",\n        \"mean\": [0.485, 0.456, 0.406],\n        \"std\": [0.229, 0.224, 0.225]\n    },\n    \"metrics\": {\n        \"test_accuracy\": float(overall_accuracy),\n        \"macro_f1\": float(macro_f1),\n        \"macro_recall\": float(macro_recall),\n        \"train_accuracy_clean\": float(train_accuracy),\n        \"best_val_accuracy\": float(best_val_acc),\n        \"best_epoch\": best_epoch,\n        \"training_epochs\": len(history['train_loss']),\n        \"training_time_min\": round(elapsed_time, 1)\n    },\n    \"per_class_metrics\": {},\n    \"confusion_matrix\": confusion_mat.tolist(),\n    \"training_history\": {\n        \"train_loss\": history['train_loss'],\n        \"train_acc\": history['train_acc'],\n        \"val_loss\": history['val_loss'],\n        \"val_acc\": history['val_acc'],\n        \"lr\": history['lr']\n    }\n}\n\nfor class_name in class_names:\n    results[\"per_class_metrics\"][class_name] = {\n        \"precision\": float(class_report[class_name]['precision']),\n        \"recall\": float(class_report[class_name]['recall']),\n        \"f1-score\": float(class_report[class_name]['f1-score']),\n        \"support\": int(class_report[class_name]['support'])\n    }\n\njson_path = f'/kaggle/working/galaxyzoo_{SLUG}_results.json'\nwith open(json_path, 'w') as f:\n    json.dump(results, f, indent=4)\n\nprint(f\"✓ Results JSON saved: {os.path.basename(json_path)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T21:18:28.944577Z","iopub.execute_input":"2026-09-23T21:18:28.945353Z","iopub.status.idle":"2026-09-23T21:18:28.956158Z","shell.execute_reply.started":"2026-09-23T21:18:28.94532Z","shell.execute_reply":"2026-09-23T21:18:28.955371Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# STEP 14: FINAL SUMMARY\n# ================================================================\nprint(\"\\n\" + \"=\"*70)\nprint(f\"✓ TRAINING COMPLETE - {MODEL_NAME.upper()} ON GALAXY ZOO\")\nprint(\"=\"*70)\n\nprint(f\"\\n MODEL RESULTS:\")\nprint(f\"  Architecture: {MODEL_NAME} (torchvision, ImageNet V2 weights)\")\nprint(f\"  Dataset: Galaxy Zoo (Elliptical vs Spiral)\")\nprint(f\"  Training Device: Kaggle GPU (CUDA)\")\n\nprint(f\"\\n📈 PERFORMANCE:\")\nprint(f\"  Training Accuracy (clean): {train_accuracy:.4f}\")\nprint(f\"  Test Accuracy: {overall_accuracy:.4f}\")\nprint(f\"  Macro F1 Score: {macro_f1:.4f}\")\nprint(f\"  Macro Recall: {macro_recall:.4f}\")\n\nprint(f\"\\n🔧 TRAINING INFO:\")\nprint(f\"  Epochs trained: {len(history['train_loss'])}\")\nprint(f\"  Best epoch: {best_epoch} (val acc {best_val_acc:.4f})\")\nprint(f\"  Training time: {elapsed_time:.1f} minutes\")\n\nprint(f\"\\n📁 FILES SAVED IN /kaggle/working/:\")\nfor p in [BEST_PATH, fig_path, json_path]:\n    size = os.path.getsize(p) / 1e6 if os.path.exists(p) else 0\n    print(f\"  {os.path.basename(p)}  ({size:.1f} MB)\")\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"✅ Done\")\nprint(\"=\"*70)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T21:18:33.720333Z","iopub.execute_input":"2026-09-23T21:18:33.720768Z","iopub.status.idle":"2026-09-23T21:18:33.728554Z","shell.execute_reply.started":"2026-09-23T21:18:33.720737Z","shell.execute_reply":"2026-09-23T21:18:33.727716Z"}},"outputs":[],"execution_count":null}]}