{"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":33679,"databundleVersionId":3212216,"sourceType":"competition"},{"sourceId":13983005,"sourceType":"datasetVersion","datasetId":8912875}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Capstone Project Maybe final version - If so, hello class\n\nthis is the 6th time i have though \"maybe this is it\". Im down to 6 hours on my 30 hour limit so we shall see","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2025-12-04T02:44:24.457137Z","iopub.execute_input":"2025-12-04T02:44:24.457324Z","iopub.status.idle":"2025-12-04T02:44:27.790757Z","shell.execute_reply.started":"2025-12-04T02:44:24.457307Z","shell.execute_reply":"2025-12-04T02:44:27.789915Z"}}},{"cell_type":"code","source":"# imports & global config\n\nimport os\nimport json\nimport time\nfrom math import inf\n\nimport numpy as np\nimport pandas as pd\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (confusion_matrix,precision_recall_fscore_support,roc_auc_score,average_precision_score)\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom PIL import Image\nimport torchvision.transforms as transforms\nimport torchvision.models as models\n\nimport torch.cuda.amp as amp\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\n\n# Kaggle herbarium data root\nDATA_DIR = \"/kaggle/input/herbarium-2022-fgvc9\"\n\ntrain_meta_path = os.path.join(DATA_DIR, \"train_metadata.json\")\ntest_meta_path  = os.path.join(DATA_DIR, \"test_metadata.json\")\n\nprint(\"DATA_DIR:\", DATA_DIR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-04T03:16:48.341646Z","iopub.execute_input":"2025-12-04T03:16:48.342234Z","iopub.status.idle":"2025-12-04T03:16:57.04683Z","shell.execute_reply.started":"2025-12-04T03:16:48.342201Z","shell.execute_reply":"2025-12-04T03:16:57.045763Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Building my core df with img paths\n\nLoad the json \nmerg annotations, img's and categories  \ncreate my core df with the column i need along with a img path to each file \n","metadata":{}},{"cell_type":"code","source":"with open(train_meta_path, \"r\") as f:\n    train_meta = json.load(f)\n\nann_df = pd.DataFrame(train_meta[\"annotations\"])\nimg_df = pd.DataFrame(train_meta[\"images\"])\ncat_df = pd.DataFrame(train_meta[\"categories\"])\n\nmerged = ann_df.merge(img_df, on=\"image_id\", how=\"left\")\nmerged = merged.merge(cat_df, on=\"category_id\", how=\"left\")\n\nmerged[\"image_path\"] = merged[\"file_name\"].apply(\n    lambda fn: os.path.join(DATA_DIR, \"train_images\", fn)\n)\n\ncore_cols = [\n    \"image_id\",\n    \"image_path\",\n    \"category_id\",\n    \"genus_id\",\n    \"family\",\n    \"genus\",\n    \"species\",\n    \"scientificName\",\n]\n\ncore_df = merged[core_cols].copy()\nprint(\"core_df shape:\", core_df.shape)\ncore_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-04T03:18:40.012251Z","iopub.execute_input":"2025-12-04T03:18:40.012849Z","iopub.status.idle":"2025-12-04T03:18:56.134145Z","shell.execute_reply.started":"2025-12-04T03:18:40.012819Z","shell.execute_reply":"2025-12-04T03:18:56.133392Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### do not touch label\n\nToxicodendron = poison ivy, oak, sumac\\\nEuphorbia = spurges, skin irritants\\\nUrtica = nettles\\\nCicuta = water hemlock\\\nConium = poison hemlock\\\nHeracleum = hogweed, burns","metadata":{}},{"cell_type":"code","source":"toxic_genus_list = [\n    \"Toxicodendron\", \n    \"Euphorbia\",     \n    \"Urtica\",         \n    \"Cicuta\",         \n    \"Conium\",        \n    \"Heracleum\",     \n]\n\ncore_df[\"do_not_touch\"] = core_df[\"genus\"].isin(toxic_genus_list).astype(int)\n\nprint(\"Toxic genera used:\", toxic_genus_list)\nprint(\"\\ndo_not_touch counts:\")\nprint(core_df[\"do_not_touch\"].value_counts())\ncore_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-04T03:22:11.478212Z","iopub.execute_input":"2025-12-04T03:22:11.478593Z","iopub.status.idle":"2025-12-04T03:22:11.551404Z","shell.execute_reply.started":"2025-12-04T03:22:11.478567Z","shell.execute_reply":"2025-12-04T03:22:11.550494Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Test vs pool\n\nSplit into training pool and a realistic test set\n\nsave 5% for testing, rest is training\n\ntoxic_df = all toxic imgs\nsafe_df = all safe img's","metadata":{}},{"cell_type":"code","source":"TEST_FRACTION = 0.05\n\nstrat_col = core_df[\"do_not_touch\"]\nrest_df, test_df = train_test_split(\n    core_df,\n    test_size=TEST_FRACTION,\n    random_state=42,\n    stratify=strat_col,\n)\n\nprint(\"Realistic test size:\", len(test_df))\nprint(test_df[\"do_not_touch\"].value_counts())\n\n# Training pool = rest_df\ntoxic_df = rest_df[rest_df[\"do_not_touch\"] == 1].reset_index(drop=True)\nsafe_df  = rest_df[rest_df[\"do_not_touch\"] == 0].reset_index(drop=True)\n\nprint(\"\\nToxic (train pool) count:\", len(toxic_df))\nprint(\"Safe  (train pool) count:\", len(safe_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-04T03:26:44.036523Z","iopub.execute_input":"2025-12-04T03:26:44.036865Z","iopub.status.idle":"2025-12-04T03:26:45.332389Z","shell.execute_reply.started":"2025-12-04T03:26:44.03684Z","shell.execute_reply":"2025-12-04T03:26:45.331446Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Transormation & dataset ","metadata":{}},{"cell_type":"code","source":"image_transforms = transforms.Compose([\n    transforms.Resize((224, 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\nstrong_transforms = transforms.Compose([\n    transforms.RandomResizedCrop(224, scale=(0.75, 1.0)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ColorJitter(\n        brightness=0.25,\n        contrast=0.25,\n        saturation=0.25,\n        hue=0.03,\n    ),\n    transforms.RandomRotation(10),\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\nclass PlantDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = row[\"image_path\"]\n\n        try:\n            img = Image.open(img_path).convert(\"RGB\")\n        except Exception:\n            img = Image.new(\"RGB\", (224, 224), (0, 0, 0))\n\n        if self.transform:\n            img = self.transform(img)\n\n        toxic_label = int(row[\"do_not_touch\"])\n        return img, torch.tensor(toxic_label, dtype=torch.long)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-04T03:27:25.973139Z","iopub.execute_input":"2025-12-04T03:27:25.973909Z","iopub.status.idle":"2025-12-04T03:27:25.983239Z","shell.execute_reply.started":"2025-12-04T03:27:25.973882Z","shell.execute_reply":"2025-12-04T03:27:25.982119Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Model + load the version 1\n\nI am loading version 1 from a semi successful + acedemically kind of acceptable notebook to continue my training. the hope is, I see progress","metadata":{}},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Using device:\", device)\n\n# base efficienet with imagenet weights\nmodel = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.IMAGENET1K_V1)\nin_features = model.classifier[1].in_features\nmodel.classifier = nn.Sequential(\n    nn.Dropout(0.2),\n    nn.Linear(in_features, 1)  \n)\nmodel = model.to(device)\n\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\nscheduler = ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=2)\nPATIENCE = 4\n\n# final checkpoint from the half decent notebook i have decided to place my faith in\nPRETRAINED_CKPT_PATH = \"/kaggle/input/runv1-rotation-checkpoints/final_rich_checkpoint.pth\" \n\nif os.path.exists(PRETRAINED_CKPT_PATH):\n    print(f\"Loading pre-trained checkpoint from: {PRETRAINED_CKPT_PATH}\")\n    ckpt = torch.load(PRETRAINED_CKPT_PATH, map_location=device)\n\n    # Try to guess whether it's a pure state_dict or full checkpoint\n    if isinstance(ckpt, dict) and \"model_state_dict\" in ckpt:\n        model.load_state_dict(ckpt[\"model_state_dict\"])\n        print(\"loaded model state dict from checkpoint\")\n    else:\n        model.load_state_dict(ckpt)\n        print(\"raw state from checkpoint\")\nelse:\n    print(\"No good, time to starting over \")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-04T03:32:38.114404Z","iopub.execute_input":"2025-12-04T03:32:38.115134Z","iopub.status.idle":"2025-12-04T03:32:38.414134Z","shell.execute_reply.started":"2025-12-04T03:32:38.115104Z","shell.execute_reply":"2025-12-04T03:32:38.413162Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Build dataloaders","metadata":{}},{"cell_type":"code","source":"BATCH_SIZE   = 64\nNUM_WORKERS  = 4\nNUM_ROTATIONS = 40\nSAFE_PER_ROTATION = 20000\n\nsafe_sample_initial = safe_df.sample(n=len(toxic_df), random_state=42)\nbalanced_df = (\n    pd.concat([toxic_df, safe_sample_initial])\n    .sample(frac=1.0, random_state=42)\n    .reset_index(drop=True)\n)\n\ntrain_df, val_df = train_test_split(\n    balanced_df,\n    test_size=0.2,\n    random_state=42,\n    stratify=balanced_df[\"do_not_touch\"]\n)\n\ntrain_dataset = PlantDataset(train_df, transform=image_transforms)\nval_dataset   = PlantDataset(val_df,   transform=image_transforms)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,  num_workers=NUM_WORKERS)\nval_loader   = DataLoader(val_dataset,   batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n\ntest_dataset = PlantDataset(test_df, transform=image_transforms)\ntest_loader  = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n\nprint(\"Train batches:\", len(train_loader))\nprint(\"Val batches:  \", len(val_loader))\nprint(\"Realistic test samples:\", len(test_df))\nprint(\"Realistic test batches:\", len(test_loader))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-04T03:33:31.739634Z","iopub.execute_input":"2025-12-04T03:33:31.740121Z","iopub.status.idle":"2025-12-04T03:33:31.816478Z","shell.execute_reply.started":"2025-12-04T03:33:31.740097Z","shell.execute_reply":"2025-12-04T03:33:31.815639Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Making things less confusing\n\nTo make life easier below are helpers for precision, recall, f1, my confusion matrix, and the ever important timing info so i know how much more gpu time to waste","metadata":{}},{"cell_type":"code","source":"def compute_metrics(y_true, y_probs, threshold=0.5):\n    y_pred = (y_probs >= threshold).astype(int)\n    precision, recall, f1, _ = precision_recall_fscore_support(\n        y_true, y_pred, average='binary', zero_division=0\n    )\n    cm = confusion_matrix(y_true, y_pred)\n\n    metrics = {\n        \"precision\": float(precision),\n        \"recall\":    float(recall),\n        \"f1\":        float(f1),\n        \"confusion_matrix\": cm.tolist(),\n    }\n\n    try:\n        metrics[\"roc_auc\"] = float(roc_auc_score(y_true, y_probs))\n    except Exception:\n        metrics[\"roc_auc\"] = None\n\n    try:\n        metrics[\"pr_auc\"] = float(average_precision_score(y_true, y_probs))\n    except Exception:\n        metrics[\"pr_auc\"] = None\n\n    return metrics\n\ndef evaluate_loader_timed(model, loader, device, use_amp=True, print_progress_every=500):\n    model.eval()\n    all_probs = []\n    all_labels = []\n    batch_times = []\n    start = time.time()\n\n    with torch.no_grad():\n        for i, (images, labels) in enumerate(loader, start=1):\n            t0 = time.time()\n            images = images.to(device)\n            labels = labels.to(device)\n\n            if use_amp and device.startswith(\"cuda\"):\n                with torch.amp.autocast('cuda'):\n                    outputs = model(images)\n            else:\n                outputs = model(images)\n\n            probs = torch.sigmoid(outputs).squeeze(1).cpu().numpy()\n            all_probs.append(probs)\n            all_labels.append(labels.cpu().numpy())\n\n            batch_time = time.time() - t0\n            batch_times.append(batch_time)\n\n            if i % print_progress_every == 0:\n                elapsed = time.time() - start\n                avg_b   = sum(batch_times) / len(batch_times)\n                processed = i * loader.batch_size\n                rate = processed / elapsed if elapsed > 0 else 0.0\n                print(f\"[eval] batches={i} avg_batch_s={avg_b:.4f} processed={processed} rate_samples/s={rate:.2f}\")\n\n    if not all_probs:\n        return np.array([]), np.array([]), 0.0\n\n    all_probs  = np.concatenate(all_probs, axis=0)\n    all_labels = np.concatenate(all_labels, axis=0)\n    avg_batch_time = sum(batch_times) / len(batch_times) if batch_times else 0.0\n\n    return all_labels, all_probs, avg_batch_time\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-04T03:36:08.64617Z","iopub.execute_input":"2025-12-04T03:36:08.64646Z","iopub.status.idle":"2025-12-04T03:36:08.658063Z","shell.execute_reply.started":"2025-12-04T03:36:08.64644Z","shell.execute_reply":"2025-12-04T03:36:08.656798Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Continue my rotating training\nnow is the time to explain why i am emphasizing rotating training\n","metadata":{}},{"cell_type":"code","source":"safe_shuffled = safe_df.sample(frac=1.0, random_state=123).reset_index(drop=True)\nrot_model_path = \"balanced_do_not_touch_model_rotating_safe_20rotations_v2.pth\"\nbest_val_f1 = 0.0   # track best rotating-validation score\n\nprint(\"\\nstarting rotating training \\n\")\n\nfor rot in range(NUM_ROTATIONS):\n    print(\"=\" * 60)\n    print(f\"=== ROTATION {rot+1}/{NUM_ROTATIONS} ===\")\n\n    start = (rot * SAFE_PER_ROTATION) % len(safe_shuffled)\n    end   = start + SAFE_PER_ROTATION\n\n    if end <= len(safe_shuffled):\n        safe_chunk = safe_shuffled.iloc[start:end]\n    else:\n        safe_chunk = pd.concat([\n            safe_shuffled.iloc[start:], \n            safe_shuffled.iloc[:end - len(safe_shuffled)]\n        ]).reset_index(drop=True)\n\n    train_chunk_df = pd.concat([toxic_df, safe_chunk]).sample(\n        frac=1.0, random_state=42 + rot\n    ).reset_index(drop=True)\n\n    train_dataset_rot = PlantDataset(train_chunk_df, transform=strong_transforms)\n    train_loader_rot = DataLoader(\n        train_dataset_rot, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS\n    )\n\n    # one rotation\n    model.train()\n    train_loss = 0.0\n    train_total = 0\n\n    for images, labels in train_loader_rot:\n        images = images.to(device)\n        labels = labels.float().unsqueeze(1).to(device)\n\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        train_loss += loss.item() * images.size(0)\n        train_total += images.size(0)\n\n    avg_train_loss = train_loss / train_total\n\n    # validate\n    val_labels, val_probs, _ = evaluate_loader_timed(\n        model, val_loader, device, use_amp=True, print_progress_every=200\n    )\n    val_metrics = compute_metrics(val_labels, val_probs, threshold=0.5)\n\n    print(f\"Rotation {rot+1} Train Loss: {avg_train_loss:.4f}\")\n    print(f\"Val Metrics: {val_metrics}\")\n\n    # save checpoints\n    ckpt = {\n        \"model_state_dict\": model.state_dict(),\n        \"optimizer_state_dict\": optimizer.state_dict(),\n        \"rotation\": rot,\n        \"val_metrics\": val_metrics,\n    }\n    torch.save(ckpt, f\"rotation_{rot+1}_checkpoint_v2.pth\")\n\n    # best by f1 score\n    current_f1 = val_metrics[\"f1\"]\n    if current_f1 > best_val_f1:\n        best_val_f1 = current_f1\n        torch.save(ckpt, rot_model_path)\n        print(f\"  New BEST rotating model saved → {rot_model_path} (F1={current_f1:.4f})\")\n\nprint(\"\\finished rotating training.\")\nprint(f\"Best rotating-validation F1: {best_val_f1:.4f}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## last cell","metadata":{}},{"cell_type":"code","source":"print(\"\\nevaluating final model on realistic test set and hope for an imrpovemnt\")\n\ntest_labels, test_probs, avg_batch_s_test = evaluate_loader_timed(\n    model, test_loader, device, use_amp=True, print_progress_every=500\n)\ntest_metrics = compute_metrics(test_labels, test_probs, threshold=0.5)\n\nestimated_full_time_h = (len(test_loader) * avg_batch_s_test) / 3600.0 if avg_batch_s_test > 0 else None\n\nprint(f\"Avg batch seconds during full eval approx: {avg_batch_s_test:.4f}\")\nprint(f\"Estimated full-test hours: {estimated_full_time_h:.2f}\")\nprint(\"Realistic test metrics:\", test_metrics)\n\nfinal_ckpt = {\n    \"model_state_dict\": model.state_dict(),\n    \"optimizer_state_dict\": optimizer.state_dict(),\n    \"final_test_metrics\": test_metrics,\n    \"note\": \"Version 2 —continued from V1, trained with rotating safe strategy; tune threshold for different risk profiles.\",\n}\ntorch.save(final_ckpt, \"final_v2_rich_checkpoint.pth\")\nprint(\"saved final rich checkpoint: final_v2_rich_checkpoint.pth\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}