{"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":[{"sourceType":"competition","sourceId":24800,"datasetId":1042002,"databundleVersionId":1831594},{"sourceType":"datasetVersion","sourceId":13224382,"datasetId":8382328,"databundleVersionId":13919222}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -U ultralytics","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T18:33:23.44692Z","iopub.execute_input":"2025-10-01T18:33:23.4472Z","iopub.status.idle":"2025-10-01T18:33:27.579039Z","shell.execute_reply.started":"2025-10-01T18:33:23.447182Z","shell.execute_reply":"2025-10-01T18:33:27.578342Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Run in a single notebook cell\n!pip install -q ultralytics==8.0.114  # YOLOv8 (pick a working version)\n!pip install -q grad-cam pydicom\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-01T16:12:39.650749Z","iopub.execute_input":"2025-10-01T16:12:39.651016Z","iopub.status.idle":"2025-10-01T16:12:51.199528Z","shell.execute_reply.started":"2025-10-01T16:12:39.650997Z","shell.execute_reply":"2025-10-01T16:12:51.198826Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install torch==2.5.1 torchvision==0.20.1\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T16:07:02.195055Z","iopub.execute_input":"2025-10-01T16:07:02.195564Z","iopub.status.idle":"2025-10-01T16:07:05.367033Z","shell.execute_reply.started":"2025-10-01T16:07:02.19552Z","shell.execute_reply":"2025-10-01T16:07:05.366273Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pip install -U ultralytics","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T16:10:06.932507Z","iopub.execute_input":"2025-10-01T16:10:06.932827Z","iopub.status.idle":"2025-10-01T16:10:10.530593Z","shell.execute_reply.started":"2025-10-01T16:10:06.932796Z","shell.execute_reply":"2025-10-01T16:10:10.529523Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, sys, math, time, random, shutil, json\nfrom pathlib import Path\nimport numpy as np, pandas as pd\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nimport torchvision.transforms.functional as TF\n\nimport pydicom\n\n# Paths - Kaggle competition dataset\nINPUT_DIR = Path(\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection\")\nTRAIN_DICOM_DIR = INPUT_DIR / \"train\"\nTEST_DICOM_DIR  = INPUT_DIR / \"test\"\nTRAIN_CSV = INPUT_DIR / \"train.csv\"\nSAMPLE_SUB = INPUT_DIR / \"sample_submission.csv\"\n\n# Working directories (where we'll write converted images and labels, and save models)\nWORK_DIR = Path(\"/kaggle/input/chest-xray/kaggle/working/vindr\")\nIMG_DIR = WORK_DIR / \"images\"\nLAB_DIR = WORK_DIR / \"labels\"\nMODEL_DIR = Path(\"/kaggle/working/models\")\nMODEL_DIR.mkdir(parents=True, exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T16:07:28.3329Z","iopub.execute_input":"2025-10-01T16:07:28.333708Z","iopub.status.idle":"2025-10-01T16:07:28.339948Z","shell.execute_reply.started":"2025-10-01T16:07:28.333675Z","shell.execute_reply":"2025-10-01T16:07:28.339311Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, random\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom PIL import Image\nimport pydicom\n\n# =====================\n# CONFIG\n# =====================\nOUT_IMG_SIZE = (512, 512)   # None -> keep original resolution\nMAX_IMAGES_PER_SPLIT = None  # set to an int for debugging\nRANDOM_SEED = 42\nrandom.seed(RANDOM_SEED)\nnp.random.seed(RANDOM_SEED)\n\n# Paths (update these according to Kaggle dataset mount)\nTRAIN_CSV = Path(\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/train.csv\")\nSAMPLE_SUB = Path(\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/sample_submission.csv\")\nTRAIN_DICOM_DIR = Path(\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/train\")\nTEST_DICOM_DIR = Path(\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/test\")\n\n\n# Create folders\nfor split in [\"train\", \"val\", \"test\"]:\n    (IMG_DIR / split).mkdir(parents=True, exist_ok=True)\n    (LAB_DIR / split).mkdir(parents=True, exist_ok=True)\n\n# =====================\n# Load train.csv\n# =====================\ndf = pd.read_csv(TRAIN_CSV)\nprint(\"Total annotation rows:\", len(df))\nunique_train_image_ids = df['image_id'].unique().tolist()\nprint(\"Unique images with annotations:\", len(unique_train_image_ids))\n\n# Collect all dicom files in train dir\nall_train_files = [f.stem for f in TRAIN_DICOM_DIR.iterdir() if f.suffix.lower() == '.dicom']\nimage_ids = sorted(list(set(all_train_files)))\nprint(\"Available dicom images in train folder:\", len(image_ids))\n\n# Train/val split (90/10)\nrandom.shuffle(image_ids)\nval_frac = 0.1\nn_val = int(len(image_ids) * val_frac)\nval_ids = image_ids[:n_val]\ntrain_ids = image_ids[n_val:]\nprint(\"Train ids:\", len(train_ids), \"Val ids:\", len(val_ids))\n\n# =====================\n# Conversion helpers\n# =====================\ndef dicom_to_pil(dicom_path):\n    ds = pydicom.dcmread(str(dicom_path))\n    img = ds.pixel_array.astype(np.float32)\n    lo, hi = np.percentile(img, (0.5, 99.5))\n    img = np.clip(img, lo, hi)\n    img = img - img.min()\n    if img.max() > 0:\n        img = img / img.max()\n    img = (img * 255).astype(np.uint8)\n    pil = Image.fromarray(img).convert(\"RGB\")\n    return pil\n\ndef write_yolo_label(image_id, boxes_for_image, out_label_path, img_w, img_h):\n    lines = []\n    for box in boxes_for_image:\n        x_min, y_min, x_max, y_max = box['x_min'], box['y_min'], box['x_max'], box['y_max']\n        class_id = int(box['class_id'])\n        x_min = max(0, x_min); y_min = max(0, y_min)\n        x_max = min(img_w-1, x_max); y_max = min(img_h-1, y_max)\n        width = x_max - x_min\n        height = y_max - y_min\n        if width <= 0 or height <= 0: \n            continue\n        cx = x_min + width/2.0\n        cy = y_min + height/2.0\n        cx_norm = cx / img_w\n        cy_norm = cy / img_h\n        w_norm = width / img_w\n        h_norm = height / img_h\n        lines.append(f\"{class_id} {cx_norm:.6f} {cy_norm:.6f} {w_norm:.6f} {h_norm:.6f}\")\n    if len(lines) == 0:\n        open(out_label_path, 'w').close()\n        return\n    with open(out_label_path, 'w') as f:\n        f.write(\"\\n\".join(lines))\n\n# =====================\n# Converters with SKIP logic\n# =====================\ndef convert_split(ids_list, split_name, max_images=None):\n    pbar = ids_list if max_images is None else ids_list[:max_images]\n    count, skipped = 0, 0\n    for img_id in pbar:\n        out_img_path = IMG_DIR / split_name / f\"{img_id}.jpg\"\n        out_lbl_path = LAB_DIR / split_name / f\"{img_id}.txt\"\n\n        # ✅ Skip if already exists\n        if out_img_path.exists() and out_lbl_path.exists():\n            skipped += 1\n            continue\n\n        dicom_path = TRAIN_DICOM_DIR / f\"{img_id}.dicom\"\n        if not dicom_path.exists():\n            continue\n        try:\n            pil = dicom_to_pil(dicom_path)\n        except Exception as e:\n            print(\"Failed to read\", dicom_path, e)\n            continue\n\n        if OUT_IMG_SIZE is not None:\n            pil = pil.resize(OUT_IMG_SIZE)\n\n        out_img_path.parent.mkdir(parents=True, exist_ok=True)\n        pil.save(out_img_path, quality=95)\n\n        boxes = df[df['image_id'] == img_id]\n        write_yolo_label(img_id, boxes.to_dict('records'), out_lbl_path, pil.width, pil.height)\n\n        count += 1\n        if count % 500 == 0:\n            print(f\"{split_name}: converted {count} new images (skipped {skipped})...\")\n    print(f\"Finished {split_name} -> converted {count}, skipped {skipped}.\")\n\ndef convert_test(ids_list, max_images=None):\n    pbar = ids_list if max_images is None else ids_list[:max_images]\n    count, skipped = 0, 0\n    for img_id in pbar:\n        out_img_path = IMG_DIR / \"test\" / f\"{img_id}.jpg\"\n        out_lbl_path = LAB_DIR / \"test\" / f\"{img_id}.txt\"\n\n        # ✅ Skip if already exists\n        if out_img_path.exists() and out_lbl_path.exists():\n            skipped += 1\n            continue\n\n        dicom_path = TEST_DICOM_DIR / f\"{img_id}.dicom\"\n        if not dicom_path.exists():\n            continue\n        try:\n            pil = dicom_to_pil(dicom_path)\n        except Exception as e:\n            print(\"Failed to read\", dicom_path, e)\n            continue\n\n        if OUT_IMG_SIZE is not None:\n            pil = pil.resize(OUT_IMG_SIZE)\n\n        out_img_path.parent.mkdir(parents=True, exist_ok=True)\n        pil.save(out_img_path, quality=95)\n\n        # empty label\n        open(out_lbl_path, 'w').close()\n\n        count += 1\n        if count % 500 == 0:\n            print(f\"test: converted {count} new images (skipped {skipped})...\")\n    print(f\"Finished test -> converted {count}, skipped {skipped}.\")\n\n# =====================\n# Run conversions\n# =====================\nprint(\"Converting TRAIN split ...\")\nconvert_split(train_ids, \"train\", max_images=MAX_IMAGES_PER_SPLIT)\n\nprint(\"Converting VAL split ...\")\nconvert_split(val_ids, \"val\", max_images=MAX_IMAGES_PER_SPLIT)\n\nprint(\"Converting TEST split ...\")\nsample_sub = pd.read_csv(SAMPLE_SUB)\ntest_ids = sample_sub['image_id'].tolist()\nconvert_test(test_ids, max_images=MAX_IMAGES_PER_SPLIT)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\ndef print_folder_structure(root_dir, indent=\"\"):\n    \"\"\"\n    Prints only the folder structure (ignores files).\n    \"\"\"\n    items = sorted([item for item in os.listdir(root_dir) if os.path.isdir(os.path.join(root_dir, item))])\n    for i, item in enumerate(items):\n        path = os.path.join(root_dir, item)\n        connector = \"└── \" if i == len(items) - 1 else \"├── \"\n        print(indent + connector + item)\n        print_folder_structure(path, indent + (\"    \" if i == len(items) - 1 else \"│   \"))\n\n\nprint_folder_structure(\"/kaggle/input/\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T15:12:32.867477Z","iopub.status.idle":"2025-10-01T15:12:32.867755Z","shell.execute_reply.started":"2025-10-01T15:12:32.867624Z","shell.execute_reply":"2025-10-01T15:12:32.867635Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Run this single shell cell\n!pip install -q ultralytics==8.0.114 grad-cam pydicom timm\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T17:49:00.342488Z","iopub.execute_input":"2025-10-01T17:49:00.342776Z","iopub.status.idle":"2025-10-01T17:49:03.790287Z","shell.execute_reply.started":"2025-10-01T17:49:00.342751Z","shell.execute_reply":"2025-10-01T17:49:03.789385Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Python cell\nfrom pathlib import Path\nimport os, random, time, json, math\nimport numpy as np, pandas as pd\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nimport torchvision.transforms.functional as TF\n\nfrom sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, classification_report, confusion_matrix\n\n# ultralytics\nfrom ultralytics import YOLO\n\n# global paths (update if needed)\nWORK_DIR = Path(\"/kaggle/working/vindr\")\nIMG_DIR = WORK_DIR / \"images\"\nLAB_DIR = WORK_DIR / \"labels\"\nMODEL_DIR = Path(\"/kaggle/working/models\")\nMODEL_DIR.mkdir(parents=True, exist_ok=True)\n\nTRAIN_CSV = Path(\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/train.csv\")\nSAMPLE_SUB = Path(\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/sample_submission.csv\")\nTEST_DICOM_DIR = Path(\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/test\")\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T17:49:07.271776Z","iopub.execute_input":"2025-10-01T17:49:07.272037Z","iopub.status.idle":"2025-10-01T17:49:07.279944Z","shell.execute_reply.started":"2025-10-01T17:49:07.272015Z","shell.execute_reply":"2025-10-01T17:49:07.279152Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build class list (match earlier)\nclass_names = [\n    \"Aortic_enlargement\",\"Atelectasis\",\"Calcification\",\"Cardiomegaly\",\n    \"Consolidation\",\"ILD\",\"Infiltration\",\"Lung_Opacity\",\"Nodule_Mass\",\n    \"Other_lesion\",\"Pleural_effusion\",\"Pleural_thickening\",\n    \"Pneumothorax\",\"Pulmonary_fibrosis\",\"No_finding\"\n]\nNUM_CLASSES = len(class_names)\n\n# Read train.csv and build multi-hot label dict\ndf = pd.read_csv(TRAIN_CSV)\n# group by image_id\ntargets = {}\nfor img_id, g in df.groupby(\"image_id\"):\n    vec = np.zeros(NUM_CLASSES, dtype=np.float32)\n    for cid in g['class_id'].values:\n        vec[int(cid)] = 1.0\n    targets[img_id] = vec\n\n# Some images may be missing in df (no annotation) -> treat as No_finding\n# Ensure that for converted images, we have label vectors\ntrain_img_dir = IMG_DIR / \"train\"\nval_img_dir   = IMG_DIR / \"val\"\n\ntrain_ids = [p.stem for p in train_img_dir.glob(\"*.jpg\")]\nval_ids   = [p.stem for p in val_img_dir.glob(\"*.jpg\")]\n\n# if an image not in targets -> treat as no finding (class 14 = No_finding)\nfor img in train_ids + val_ids:\n    if img not in targets:\n        vec = np.zeros(NUM_CLASSES, dtype=np.float32)\n        vec[14] = 1.0\n        targets[img] = vec\n\n# Dataset class\nclass MultiLabelCXRDataset(Dataset):\n    def __init__(self, image_dir, img_ids, targets_dict, transform=None):\n        self.image_dir = Path(image_dir)\n        self.img_ids = img_ids\n        self.targets = targets_dict\n        self.transform = transform\n\n    def __len__(self): return len(self.img_ids)\n\n    def __getitem__(self, idx):\n        img_id = self.img_ids[idx]\n        img_path = self.image_dir / f\"{img_id}.jpg\"\n        img = Image.open(img_path).convert(\"RGB\")\n        if self.transform:\n            img = self.transform(img)\n        label = torch.tensor(self.targets[img_id], dtype=torch.float32)\n        return img, label, img_id\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T17:49:14.344963Z","iopub.execute_input":"2025-10-01T17:49:14.345674Z","iopub.status.idle":"2025-10-01T17:49:15.00935Z","shell.execute_reply.started":"2025-10-01T17:49:14.34565Z","shell.execute_reply":"2025-10-01T17:49:15.008726Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH = 32\n\ntrain_tfms = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(5),\n    transforms.ColorJitter(brightness=0.1, contrast=0.1),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n])\n\nval_tfms = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n])\n\ntrain_ds = MultiLabelCXRDataset(train_img_dir, train_ids, targets, transform=train_tfms)\nval_ds   = MultiLabelCXRDataset(val_img_dir, val_ids, targets, transform=val_tfms)\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH, shuffle=True, num_workers=4, pin_memory=True)\nval_loader   = DataLoader(val_ds, batch_size=BATCH, shuffle=False, num_workers=4, pin_memory=True)\nprint(\"Train / Val sizes:\", len(train_ds), len(val_ds))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T17:49:36.737803Z","iopub.execute_input":"2025-10-01T17:49:36.738481Z","iopub.status.idle":"2025-10-01T17:49:36.754228Z","shell.execute_reply.started":"2025-10-01T17:49:36.738457Z","shell.execute_reply":"2025-10-01T17:49:36.753578Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision.models import resnet50, ResNet50_Weights\nfrom torch.amp import autocast, GradScaler\n\n# Model\nNUM_CLASSES = 15  # change as needed\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nmodel_cls = resnet50(weights=ResNet50_Weights.IMAGENET1K_V1)\nmodel_cls.fc = nn.Linear(model_cls.fc.in_features, NUM_CLASSES)\nmodel_cls = model_cls.to(device)\n\n# Loss, optimizer, scheduler\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = optim.AdamW(model_cls.parameters(), lr=1e-4, weight_decay=1e-5)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2)\n\n# AMP\nscaler = GradScaler(\"cuda\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T17:49:49.23081Z","iopub.execute_input":"2025-10-01T17:49:49.231095Z","iopub.status.idle":"2025-10-01T17:49:49.757268Z","shell.execute_reply.started":"2025-10-01T17:49:49.231064Z","shell.execute_reply":"2025-10-01T17:49:49.756715Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision.models import resnet50, ResNet50_Weights\nfrom torch.amp import autocast, GradScaler\nfrom sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score\n\n# 🔹 Config\nNUM_CLASSES = 15   # VinBig competition has 14 findings\nEPOCHS = 20\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# 🔹 Model\nmodel_cls = resnet50(weights=ResNet50_Weights.IMAGENET1K_V1)\nmodel_cls.fc = nn.Linear(model_cls.fc.in_features, NUM_CLASSES)  # multilabel\nmodel_cls = model_cls.to(device)\n\n# 🔹 Loss, optimizer, scheduler\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = optim.AdamW(model_cls.parameters(), lr=1e-4, weight_decay=1e-5)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2)\n\n# 🔹 AMP scaler\nscaler = GradScaler(\"cuda\")\n\nbest_val_loss = float(\"inf\")\nbest_path = MODEL_DIR / \"resnet50_multilabel_best.pth\"\n\nfor epoch in range(EPOCHS):\n    print(f\"\\n===== Epoch {epoch+1}/{EPOCHS} =====\")\n    \n    # ================= TRAIN =================\n    model_cls.train()\n    running_loss = 0.0\n    all_preds, all_labels = [], []\n\n    for imgs, labels, _ in train_loader:  # assuming your dataset returns (img, label, id)\n        imgs, labels = imgs.to(device), labels.to(device).float()\n        optimizer.zero_grad()\n\n        with autocast(\"cuda\"):\n            outputs = model_cls(imgs)\n            loss = criterion(outputs, labels)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        running_loss += loss.item() * imgs.size(0)\n\n        # collect preds for metrics\n        preds = (torch.sigmoid(outputs) > 0.5).int().cpu()\n        all_preds.append(preds)\n        all_labels.append(labels.int().cpu())\n\n    # concat predictions\n    all_preds = torch.cat(all_preds).numpy()\n    all_labels = torch.cat(all_labels).numpy()\n\n    train_loss = running_loss / len(train_loader.dataset)\n    train_acc = accuracy_score(all_labels, all_preds)\n    train_f1 = f1_score(all_labels, all_preds, average=\"macro\", zero_division=0)\n    train_prec = precision_score(all_labels, all_preds, average=\"macro\", zero_division=0)\n    train_rec = recall_score(all_labels, all_preds, average=\"macro\", zero_division=0)\n\n    # ================= VALIDATION =================\n    model_cls.eval()\n    val_loss = 0.0\n    val_preds, val_labels = [], []\n\n    with torch.no_grad():\n        for imgs, labels, _ in val_loader:\n            imgs, labels = imgs.to(device), labels.to(device).float()\n            with autocast(\"cuda\"):\n                outputs = model_cls(imgs)\n                loss = criterion(outputs, labels)\n            val_loss += loss.item() * imgs.size(0)\n\n            preds = (torch.sigmoid(outputs) > 0.5).int().cpu()\n            val_preds.append(preds)\n            val_labels.append(labels.int().cpu())\n\n    val_preds = torch.cat(val_preds).numpy()\n    val_labels = torch.cat(val_labels).numpy()\n\n    val_loss = val_loss / len(val_loader.dataset)\n    val_acc = accuracy_score(val_labels, val_preds)\n    val_f1 = f1_score(val_labels, val_preds, average=\"macro\", zero_division=0)\n    val_prec = precision_score(val_labels, val_preds, average=\"macro\", zero_division=0)\n    val_rec = recall_score(val_labels, val_preds, average=\"macro\", zero_division=0)\n\n    # 🔹 Scheduler step\n    scheduler.step(val_loss)\n\n    print(f\"Train Loss: {train_loss:.4f} | Acc: {train_acc:.4f} | F1: {train_f1:.4f} | \"\n          f\"Prec: {train_prec:.4f} | Rec: {train_rec:.4f}\")\n    print(f\"Val   Loss: {val_loss:.4f} | Acc: {val_acc:.4f} | F1: {val_f1:.4f} | \"\n          f\"Prec: {val_prec:.4f} | Rec: {val_rec:.4f}\")\n\n    # save best model\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        torch.save(model_cls.state_dict(), best_path)\n        print(\"✅ Saved best model at\", best_path)\n\n# Save final model\ntorch.save(model_cls.state_dict(), MODEL_DIR / \"resnet50_multilabel_final.pth\")\nprint(\"🏁 Training finished. Best val loss:\", best_val_loss)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T17:50:21.256165Z","iopub.execute_input":"2025-10-01T17:50:21.256744Z","iopub.status.idle":"2025-10-01T18:10:38.984822Z","shell.execute_reply.started":"2025-10-01T17:50:21.256718Z","shell.execute_reply":"2025-10-01T18:10:38.984004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# load best\nmodel_cls.load_state_dict(torch.load(MODEL_DIR/\"resnet50_multilabel_best.pth\", map_location=device))\nmodel_cls.eval()\n\ny_true = []\ny_prob = []\n\nwith torch.no_grad():\n    for imgs, labels, _ in val_loader:\n        imgs = imgs.to(device)\n        out = model_cls(imgs)\n        probs = torch.sigmoid(out).cpu().numpy()\n        y_prob.append(probs)\n        y_true.append(labels.numpy())\n\ny_prob = np.vstack(y_prob)\ny_true = np.vstack(y_true)\n\n# threshold per class 0.5\ny_pred = (y_prob >= 0.5).astype(int)\n\n# metrics: per-class F1, precision, recall and macro/micro\nper_class_f1 = []\nper_class_prec = []\nper_class_rec = []\nfor i in range(NUM_CLASSES):\n    p = precision_score(y_true[:,i], y_pred[:,i], zero_division=0)\n    r = recall_score(y_true[:,i], y_pred[:,i], zero_division=0)\n    f = f1_score(y_true[:,i], y_pred[:,i], zero_division=0)\n    per_class_f1.append(f); per_class_prec.append(p); per_class_rec.append(r)\n    print(f\"{class_names[i]:20s}  Precision: {p:.3f}  Recall: {r:.3f}  F1: {f:.3f}\")\n\nmacro_f1 = f1_score(y_true, y_pred, average='macro', zero_division=0)\nmicro_f1 = f1_score(y_true, y_pred, average='micro', zero_division=0)\nprint(\"Macro F1:\", macro_f1, \"Micro F1:\", micro_f1)\n\n# show confusion-like stats for \"No_finding\" vs any finding (binary)\ntrue_any = (y_true[:, :14].sum(axis=1) > 0).astype(int)  # any of first 14 classes\npred_any = (y_pred[:, :14].sum(axis=1) > 0).astype(int)\nprint(\"Overall detection of any abnormality - Acc/F1/Rec:\", accuracy_score(true_any, pred_any),\n      f1_score(true_any, pred_any), recall_score(true_any, pred_any))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T18:12:37.094147Z","iopub.execute_input":"2025-10-01T18:12:37.094476Z","iopub.status.idle":"2025-10-01T18:12:42.364357Z","shell.execute_reply.started":"2025-10-01T18:12:37.094452Z","shell.execute_reply":"2025-10-01T18:12:42.363571Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================\n# Test / Confusion / Grad-CAM\n# =========================\n# Run this in the same environment where model_cls, val_loader, class_names, MODEL_DIR, device exist.\n\nimport numpy as np\nimport torch\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import precision_score, recall_score, f1_score, accuracy_score, roc_auc_score, confusion_matrix, ConfusionMatrixDisplay\nimport os\nfrom pathlib import Path\n\n# For Grad-CAM\nfrom pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\nfrom pytorch_grad_cam.utils.image import show_cam_on_image, preprocess_image\n\n# Output folder for visuals\nOUT_VIS = Path(\"/kaggle/working/vis\")\nOUT_VIS.mkdir(parents=True, exist_ok=True)\n\n# ------------- 1) Load the best model weights ----------------\nbest_path = MODEL_DIR / \"resnet50_multilabel_best.pth\"\nprint(\"Loading model from:\", best_path)\nstate = torch.load(best_path, map_location=device)\n# state is a state_dict (you saved model.state_dict())\ntry:\n    model_cls.load_state_dict(state)\nexcept RuntimeError:\n    # maybe saved as dict with 'model_state_dict'\n    if isinstance(state, dict) and 'model_state_dict' in state:\n        model_cls.load_state_dict(state['model_state_dict'])\n    else:\n        raise\nmodel_cls.to(device)\nmodel_cls.eval()\nprint(\"Model loaded.\")\n\n# ------------- 2) Run inference on validation set --------------\ny_true_list = []\ny_prob_list = []\nimg_ids_list = []\n\nwith torch.no_grad():\n    for imgs, labels, img_ids in val_loader:\n        imgs = imgs.to(device)\n        logits = model_cls(imgs)                     # shape (B, C)\n        probs = torch.sigmoid(logits).cpu().numpy() # multilabel probs\n        y_prob_list.append(probs)\n        y_true_list.append(labels.numpy())\n        img_ids_list.extend(img_ids)\n\ny_prob = np.vstack(y_prob_list)   # (N, C)\ny_true = np.vstack(y_true_list).astype(int)  # (N, C)\n\n# Choose threshold (0.5 default)\nTH = 0.5\ny_pred = (y_prob >= TH).astype(int)\n\nprint(\"Shapes: y_true\", y_true.shape, \"y_prob\", y_prob.shape, \"y_pred\", y_pred.shape)\n\n# ------------- 3) Per-class metrics ----------------\nper_class_prec = []\nper_class_rec = []\nper_class_f1 = []\nper_class_support = y_true.sum(axis=0).astype(int)\n\nfor i, cname in enumerate(class_names):\n    p = precision_score(y_true[:, i], y_pred[:, i], zero_division=0)\n    r = recall_score(y_true[:, i], y_pred[:, i], zero_division=0)\n    f = f1_score(y_true[:, i], y_pred[:, i], zero_division=0)\n    per_class_prec.append(p)\n    per_class_rec.append(r)\n    per_class_f1.append(f)\n    print(f\"{i:02d} {cname:20s} | Precision: {p:.3f}  Recall: {r:.3f}  F1: {f:.3f}  Support: {per_class_support[i]}\")\n\nmacro_f1 = f1_score(y_true, y_pred, average='macro', zero_division=0)\nmicro_f1 = f1_score(y_true, y_pred, average='micro', zero_division=0)\nprint(\"\\nMacro F1:\", macro_f1, \"Micro F1:\", micro_f1)\n\n# ------------- 4) Plot per-class F1 bar chart ---------------\nplt.figure(figsize=(12,4))\nplt.bar(range(len(class_names)), per_class_f1, tick_label=class_names)\nplt.xticks(rotation=45, ha='right')\nplt.ylabel(\"F1 score\")\nplt.title(\"Per-class F1 on Validation\")\nplt.tight_layout()\nplt.savefig(OUT_VIS / \"per_class_f1.png\", dpi=150)\nplt.show()\n\n# ------------- 5) Binary confusion matrix: No_finding vs Any finding -------------\n# In your class list \"No_finding\" is last (index 14). Adjust if different.\nNO_FIND_IDX = class_names.index(\"No_finding\")\ntrue_any = (y_true[:, :NO_FIND_IDX].sum(axis=1) > 0).astype(int)  # any abnormality in first 14 classes\npred_any = (y_pred[:, :NO_FIND_IDX].sum(axis=1) > 0).astype(int)\n\ncm = confusion_matrix(true_any, pred_any)\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=[\"No finding (true/neg)\", \"Any finding (true/pos)\"])\nfig, ax = plt.subplots(figsize=(5,4))\ndisp.plot(ax=ax)\nplt.title(\"Binary confusion matrix: any abnormality vs no_finding\")\nplt.savefig(OUT_VIS / \"binary_confusion_any_vs_none.png\", dpi=150)\nplt.show()\n\n# ------------- 6) Per-class confusion-like numbers (TP/FP/FN/TN) -------------\ntp = np.logical_and(y_true == 1, y_pred == 1).sum(axis=0)\nfp = np.logical_and(y_true == 0, y_pred == 1).sum(axis=0)\nfn = np.logical_and(y_true == 1, y_pred == 0).sum(axis=0)\ntn = np.logical_and(y_true == 0, y_pred == 0).sum(axis=0)\n\n# Save a CSV of per-class stats\nimport pandas as pd\nper_class_df = pd.DataFrame({\n    \"class\": class_names,\n    \"support\": per_class_support,\n    \"tp\": tp,\n    \"fp\": fp,\n    \"fn\": fn,\n    \"tn\": tn,\n    \"precision\": per_class_prec,\n    \"recall\": per_class_rec,\n    \"f1\": per_class_f1\n})\nper_class_df.to_csv(OUT_VIS / \"per_class_stats.csv\", index=False)\nprint(\"Saved per-class stats to\", OUT_VIS / \"per_class_stats.csv\")\nper_class_df.head(15)\n\n# ------------- 7) (Optional) AUC per class -------------\n# Only valid if ground-truth has both positive and negative examples for the class\nfrom sklearn.metrics import roc_auc_score\nauc_per_class = []\nfor i in range(len(class_names)):\n    try:\n        auc = roc_auc_score(y_true[:, i], y_prob[:, i])\n    except Exception:\n        auc = np.nan\n    auc_per_class.append(auc)\nprint(\"\\nPer-class AUCs (NaN means not computable):\")\nfor i, a in enumerate(auc_per_class):\n    print(f\"{i:02d} {class_names[i]:20s} AUC: {a}\")\n\n# Save AUCs\npd.DataFrame({\"class\": class_names, \"auc\": auc_per_class}).to_csv(OUT_VIS / \"per_class_auc.csv\", index=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T18:13:17.61276Z","iopub.execute_input":"2025-10-01T18:13:17.613538Z","iopub.status.idle":"2025-10-01T18:13:23.679218Z","shell.execute_reply.started":"2025-10-01T18:13:17.613508Z","shell.execute_reply":"2025-10-01T18:13:23.678314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------ Corrected Grad-CAM block ------------------\nfrom pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\n\n# make sure model is on the desired device (you already did this earlier)\n# model_cls.to(device)\nmodel_cls.eval()\n\n# choose target layers for ResNet50: layer4 is the last conv block\ntarget_layers = [model_cls.layer4]\n\n# helper to convert PIL to float RGB array [0,1]\ndef pil_to_float_rgb(img_pil, size=(224,224)):\n    img = img_pil.resize(size)\n    arr = np.array(img).astype(np.float32) / 255.0\n    if arr.ndim == 2:\n        arr = np.stack([arr]*3, axis=-1)\n    return arr\n\n# Build list of samples to visualize (re-using y_true, img_ids_list from earlier inference)\nsamples_to_visualize = []\nfor cls_idx in range(len(class_names)):\n    inds = np.where(y_true[:, cls_idx] == 1)[0]\n    if len(inds) == 0:\n        continue\n    idx = inds[0]\n    samples_to_visualize.append((idx, cls_idx))\n    if len(samples_to_visualize) >= 40:\n        break\n\nprint(\"Will generate Grad-CAM for\", len(samples_to_visualize), \"samples.\")\n\n# Create GradCAM object WITHOUT use_cuda argument (works with current version)\ncam = GradCAM(model=model_cls, target_layers=target_layers)  # no use_cuda\n\n# Run Grad-CAM for each sample\nfrom PIL import Image as PILImage\nfor sample_idx, target_cls in samples_to_visualize:\n    img_id = img_ids_list[sample_idx]\n    img_path = val_img_dir / f\"{img_id}.jpg\"\n    pil = PILImage.open(img_path).convert(\"RGB\")\n\n    # preprocess exactly as for validation (resize + normalize)\n    preprocess = transforms.Compose([\n        transforms.Resize((224,224)),\n        transforms.ToTensor(),\n        transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n    ])\n    input_tensor = preprocess(pil).unsqueeze(0).to(device)\n\n    # target is the class index we want visualization for\n    targets = [ClassifierOutputTarget(target_cls)]\n    # compute cam (returns HxW numpy)\n    grayscale_cam = cam(input_tensor=input_tensor, targets=targets)[0]\n\n    # overlay on the original (float RGB 0..1)\n    rgb_float = pil_to_float_rgb(pil, size=(grayscale_cam.shape[1], grayscale_cam.shape[0]))\n    cam_image = show_cam_on_image(rgb_float, grayscale_cam, use_rgb=True)\n\n    out_file = OUT_VIS / f\"gradcam_img_{img_id}_class_{class_names[target_cls]}.png\"\n    PILImage.fromarray(cam_image).save(out_file)\n    print(\"Saved grad-cam to\", out_file)\n\n# cleanup\ndel cam\nprint(\"Grad-CAM generation done. Visuals are in:\", OUT_VIS)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T15:39:56.457641Z","iopub.execute_input":"2025-10-01T15:39:56.458555Z","iopub.status.idle":"2025-10-01T15:39:57.378603Z","shell.execute_reply.started":"2025-10-01T15:39:56.458507Z","shell.execute_reply":"2025-10-01T15:39:57.377913Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Grad-CAM visualization cell (adapted for your code)\n# Assumes: model_cls (or model), device, val_loader, val_img_dir, class_names exist.\n\n!pip install -q grad-cam\n\nfrom pytorch_grad_cam import GradCAM, GradCAMPlusPlus, ScoreCAM, AblationCAM\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\nimport torchvision.transforms as T\nimport numpy as np\nimport torch\nimport random\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nfrom PIL import Image\n\nOUT_VIS = Path(\"/kaggle/working/vis\")\nOUT_VIS.mkdir(parents=True, exist_ok=True)\n\n# ---------- Config ----------\n# Choose sample index: set SAMPLE_INDEX to an int (0..N-1) to visualize that sample from val_loader\n# or set SAMPLE_INDEX = None to pick a random sample.\nSAMPLE_INDEX = None   # e.g. 0 or None for random\n# Choose target class index to visualize. If None -> uses model's top predicted class for that image.\nTARGET_CLASS = None   # e.g. 0..14 or None\n# Whether to use GradCAMPlusPlus (sometimes sharper) fallback to GradCAM if not available\nUSE_CAM_METHOD = \"gradcam\"  # options: \"gradcam\", \"gradcampp\"\n# ------------------------------------------------\n\n# helper: inverse normalization (to get image for plotting)\nmean = np.array([0.485, 0.456, 0.406])\nstd  = np.array([0.229, 0.224, 0.225])\ndef denormalize_tensor(tensor):   # tensor: C,H,W in torch\n    arr = tensor.cpu().numpy()\n    arr = np.transpose(arr, (1,2,0))  # HWC\n    arr = (arr * std[None,None,:]) + mean[None,None,:]\n    arr = np.clip(arr, 0.0, 1.0)\n    return arr\n\n# pick one sample from validation set\nall_imgs = []\nall_labels = []\nall_ids = []\nfor imgs, labels, ids in val_loader:\n    # store a batch at a time (not memory heavy for single sample selection)\n    all_imgs.append(imgs)       # tensor B,C,H,W normalized\n    all_labels.append(labels)   # tensor B,C\n    all_ids.extend(ids)\n# flatten batches to list\nimgs_tensor = torch.cat(all_imgs, dim=0)        # N,C,H,W\nlabels_tensor = torch.cat(all_labels, dim=0)    # N,C\nN = imgs_tensor.shape[0]\n\nif N == 0:\n    raise RuntimeError(\"Validation loader is empty or not accessible.\")\n\nif SAMPLE_INDEX is None:\n    idx = random.randrange(N)\nelse:\n    idx = int(SAMPLE_INDEX) % N\n\ninput_tensor = imgs_tensor[idx:idx+1].to(device)   # 1,C,H,W\ngt_vector = labels_tensor[idx].cpu().numpy()\nimg_id = all_ids[idx]\n\n# if TARGET_CLASS None -> choose top predicted class index for this sample\nwith torch.no_grad():\n    logits = model_cls(input_tensor.to(device))             # shape 1,C\n    probs = torch.sigmoid(logits).cpu().numpy()[0]         # multilabel probabilities\n\nif TARGET_CLASS is None:\n    # pick class with highest probability (argmax)\n    target_cls = int(np.argmax(probs))\nelse:\n    target_cls = int(TARGET_CLASS)\n\nprint(f\"Visualizing sample idx={idx} img_id={img_id} target_class={target_cls} ({class_names[target_cls]})\")\nprint(\"Top predicted probs (first 6):\", probs[:6])\n\n# prepare RGB image (float 0..1) to overlay CAM onto.\n# We prefer to load original JPG for better quality (val_img_dir exists) if available.\nimg_path = Path(val_img_dir) / f\"{img_id}.jpg\"\nif img_path.exists():\n    pil = Image.open(img_path).convert(\"RGB\")\n    rgb_for_overlay = np.array(pil.resize((input_tensor.shape[3], input_tensor.shape[2]))).astype(np.float32) / 255.0\nelse:\n    # fallback: use the denormalized tensor from loader\n    rgb_for_overlay = denormalize_tensor(input_tensor[0]).astype(np.float32)\n\n# choose target layers for ResNet50: layer4 (last conv block)\n# For ResNet50 the last conv block is model_cls.layer4; for ResNet18 it's similar.\ntry:\n    target_layers = [model_cls.layer4]\nexcept Exception:\n    # fallback: if your model variable is named differently\n    try:\n        target_layers = [model.layer4]\n    except Exception:\n        raise RuntimeError(\"Could not find layer4 on the model. Inspect your model to select appropriate target layer.\")\n\n# Instantiate CAM object robustly across grad-cam versions\ncam = None\nif USE_CAM_METHOD.lower() == \"gradcampp\":\n    try:\n        cam = GradCAMPlusPlus(model=model_cls, target_layers=target_layers)\n    except Exception:\n        cam = GradCAM(model=model_cls, target_layers=target_layers)\nelse:\n    try:\n        cam = GradCAM(model=model_cls, target_layers=target_layers)\n    except TypeError:\n        # some versions accept device arg, try passing device string\n        try:\n            cam = GradCAM(model=model_cls, target_layers=target_layers, device=str(device))\n        except Exception:\n            cam = GradCAMPlusPlus(model=model_cls, target_layers=target_layers)\n\n# Build target (ClassifierOutputTarget expects the class index for classification)\ntargets = [ClassifierOutputTarget(target_cls)]\n\n# compute cam (returns numpy HxW for each image)\ngrayscale_cam = cam(input_tensor=input_tensor, targets=targets)[0]  # HxW\n\n# overlay\ncam_image = show_cam_on_image(rgb_for_overlay, grayscale_cam, use_rgb=True)\n\n# Plot side-by-side original and cam overlay\nfig, (ax1, ax2) = plt.subplots(1,2, figsize=(12,6))\nax1.imshow(rgb_for_overlay)\nax1.set_title(f\"Original - id:{img_id}\\nGT positive classes: {[class_names[i] for i,v in enumerate(gt_vector) if v==1]}\")\nax1.axis('off')\n\nax2.imshow(cam_image)\nax2.set_title(f\"Grad-CAM -> {class_names[target_cls]} (pred prob {probs[target_cls]:.3f})\")\nax2.axis('off')\n\nplt.tight_layout()\nout_file = OUT_VIS / f\"gradcam_sample_{img_id}_class_{class_names[target_cls].replace(' ','_')}.png\"\nplt.savefig(out_file, dpi=150)\nplt.show()\n\nprint(\"Saved visualization to:\", out_file)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T18:13:58.022087Z","iopub.execute_input":"2025-10-01T18:13:58.022757Z","iopub.status.idle":"2025-10-01T18:14:06.237471Z","shell.execute_reply.started":"2025-10-01T18:13:58.02273Z","shell.execute_reply":"2025-10-01T18:14:06.236642Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# LAB_DIR structure: labels/{train,val,test}/{image_id}.txt contains lines \"class_id cx cy w h\"\n# We will create a single-class labels folder for YOLO training: labels_yolo_single/{train,val,test}\nLABEL_SINGLE_DIR = WORK_DIR / \"labels_single\"\nLABEL_SINGLE_DIR.mkdir(parents=True, exist_ok=True)\n\nfor split in [\"train\",\"val\",\"test\"]:\n    (LABEL_SINGLE_DIR / split).mkdir(parents=True, exist_ok=True)\n    src_dir = LAB_DIR / split\n    dst_dir = LABEL_SINGLE_DIR / split\n    for txt in src_dir.glob(\"*.txt\"):\n        dst_txt = dst_dir / txt.name\n        with open(txt, 'r') as f:\n            lines = [l.strip() for l in f if l.strip()]\n        out_lines = []\n        for l in lines:\n            # l = \"class_id cx cy w h\" (class_id from 0..14)\n            parts = l.split()\n            cls = int(parts[0])\n            if cls == 14:\n                # No finding -> skip (no boxes)\n                continue\n            # else map any class -> 0 (abnormal)\n            cx, cy, w, h = parts[1], parts[2], parts[3], parts[4]\n            out_lines.append(f\"0 {cx} {cy} {w} {h}\")\n        # write out (possibly empty)\n        with open(dst_txt, 'w') as fo:\n            fo.write(\"\\n\".join(out_lines))\nprint(\"Converted labels to single-class in\", LABEL_SINGLE_DIR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T18:26:09.783455Z","iopub.execute_input":"2025-10-01T18:26:09.784057Z","iopub.status.idle":"2025-10-01T18:26:10.900614Z","shell.execute_reply.started":"2025-10-01T18:26:09.784034Z","shell.execute_reply":"2025-10-01T18:26:10.899999Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import yaml\nyolo_yaml = {\n    \"path\": str(WORK_DIR),   # base path\n    \"train\": \"images/train\",\n    \"val\":   \"images/val\",\n    \"test\":  \"images/test\",\n    \"nc\": 1,\n    \"names\": [\"abnormal\"]\n}\nYAML_PATH = WORK_DIR / \"yolov8_abnormal.yaml\"\nwith open(YAML_PATH, 'w') as f:\n    yaml.dump(yolo_yaml, f)\nprint(\"Saved YAML:\", YAML_PATH)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T18:31:41.310795Z","iopub.execute_input":"2025-10-01T18:31:41.311166Z","iopub.status.idle":"2025-10-01T18:31:41.319322Z","shell.execute_reply.started":"2025-10-01T18:31:41.311135Z","shell.execute_reply":"2025-10-01T18:31:41.318183Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -U ultralytics\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T17:09:49.459693Z","iopub.execute_input":"2025-10-01T17:09:49.459973Z","iopub.status.idle":"2025-10-01T17:11:02.973444Z","shell.execute_reply.started":"2025-10-01T17:09:49.45995Z","shell.execute_reply":"2025-10-01T17:11:02.972688Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# choose a YOLO model - yolov8n (nano) for quick test, switch to yolov8m or yolov8l for better accuracy\nyolo_model = YOLO(\"yolov8n.pt\")  # or path to a trained weights\n# Train: set epochs small to test, increase for final runs\nresults = yolo_model.train(data=str(YAML_PATH), epochs=20, imgsz=640, batch=8, project=str(WORK_DIR/\"yolov8_runs\"), name=\"abnormal_singleclass\")\nprint(\"YOLO training finished. Check outputs in\", WORK_DIR/\"yolov8_runs\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-01T18:34:06.416453Z","iopub.execute_input":"2025-10-01T18:34:06.41681Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}