{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","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":1810938,"datasetId":1075803,"databundleVersionId":1848422},{"sourceType":"datasetVersion","sourceId":1810939,"datasetId":1075804,"databundleVersionId":1848423}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"ca1a6d6b-5ebf-4af8-bb6c-b572fdea4ca0","cell_type":"markdown","source":"# 🫁 YOLOv11 – VinBigData Chest X-Ray Abnormalities Detection\n\n> **Dataset:** [VinBigData Chest X-ray Resized PNG 512x512](https://www.kaggle.com/datasets/xhlulu/vinbigdata)  \n> **Model:** YOLOv11m (medium – cân bằng tốt giữa accuracy và VRAM)  \n> **GPU:** Kaggle T4 x2 (16GB x2) hoặc P100 (16GB)  \n> **14 Classes:** Aortic enlargement, Atelectasis, Calcification, Cardiomegaly, Consolidation, ILD, Infiltration, Lung Opacity, Nodule/Mass, Other lesion, Pleural effusion, Pleural thickening, Pneumothorax, Pulmonary fibrosis\n\n---\n### ⚙️ Pipeline Overview\n1. Install & Import\n2. EDA – phân tích phân phối nhãn\n3. Chuyển đổi annotation sang YOLO format\n4. Cấu trúc thư mục & data.yaml\n5. Augmentation strategy\n6. Train YOLOv11 với AdamW + Cosine LR\n7. Evaluate & visualize\n8. Export model","metadata":{}},{"id":"2c18c7c7-639d-4b5a-82e4-ce338b123f1b","cell_type":"markdown","source":"## 📦 Cell 1 – Cài đặt & Kiểm tra GPU","metadata":{}},{"id":"2396ecea-886c-45ce-934b-163cc2fb67b3","cell_type":"code","source":"# ── Cài đặt Ultralytics (YOLOv11) ─────────────────────────────────────────\n!pip install -q ultralytics albumentations opencv-python-headless\n\nimport os, gc, cv2, json, shutil, random, warnings\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nfrom sklearn.model_selection import StratifiedGroupKFold\nimport torch\nimport ultralytics\nfrom ultralytics import YOLO\n\nwarnings.filterwarnings('ignore')\n\n# ── Kiểm tra GPU ──────────────────────────────────────────────────────────\nprint(f'Ultralytics version : {ultralytics.__version__}')\nprint(f'PyTorch version     : {torch.__version__}')\nprint(f'CUDA available      : {torch.cuda.is_available()}')\nif torch.cuda.is_available():\n    for i in range(torch.cuda.device_count()):\n        props = torch.cuda.get_device_properties(i)\n        print(f'  GPU {i}: {props.name}  |  VRAM: {props.total_memory/1e9:.1f} GB')\n\nDEVICE = '0' if torch.cuda.device_count() == 1 else '0,1'  # T4x2 → dùng cả 2 GPU\nprint(f'\\nDevice config       : {DEVICE}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-11T11:02:15.718847Z","iopub.execute_input":"2026-05-11T11:02:15.719087Z","iopub.status.idle":"2026-05-11T11:02:38.650046Z","shell.execute_reply.started":"2026-05-11T11:02:15.719062Z","shell.execute_reply":"2026-05-11T11:02:38.649085Z"}},"outputs":[],"execution_count":null},{"id":"11a2bb45-2d1a-4fb2-8d94-20016c9c5797","cell_type":"markdown","source":"## 📂 Cell 2 – Đường dẫn & Cấu hình","metadata":{}},{"id":"2463c881-186f-4906-a86f-a99d39850163","cell_type":"code","source":"# ── Dataset ảnh PNG (đã có) ────────────────────────────────────────────────\nROOT      = Path('/kaggle/input/datasets/xhlulu/vinbigdata-chest-xray-resized-png-1024x1024')\nTRAIN_IMG = ROOT / 'train'\nTEST_IMG  = ROOT / 'test'\n\n# ── Annotation: cần thêm dataset vinbigdata-chest-xray-abnormalities-detection\n# Vào Kaggle → Add Data → tìm \"vinbigdata-chest-xray-abnormalities-detection\"\nANNOT_DIR = Path('/kaggle/input/competitions/vinbigdata-chest-xray-abnormalities-detection')\nTRAIN_CSV = ANNOT_DIR / 'train.csv'\n\nWORK_DIR  = Path('/kaggle/working')\nYOLO_DIR  = WORK_DIR / 'yolo_dataset'\nMODEL_DIR = WORK_DIR / 'runs'\n\n# Verify\nprint('Ảnh train tồn tại :', TRAIN_IMG.exists())\nprint('Annotation tồn tại:', TRAIN_CSV.exists())\n\n# ── 14 class labels (class 14 = \"No finding\" → bỏ qua khi detect) ─────────\nCLASS_NAMES = [\n    'Aortic enlargement',   # 0\n    'Atelectasis',          # 1\n    'Calcification',        # 2\n    'Cardiomegaly',         # 3\n    'Consolidation',        # 4\n    'ILD',                  # 5\n    'Infiltration',         # 6\n    'Lung Opacity',         # 7\n    'Nodule/Mass',          # 8\n    'Other lesion',         # 9\n    'Pleural effusion',     # 10\n    'Pleural thickening',   # 11\n    'Pneumothorax',         # 12\n    'Pulmonary fibrosis',   # 13\n]\nNUM_CLASSES = len(CLASS_NAMES)\nprint(f'Số lớp bệnh: {NUM_CLASSES}')\nfor i, c in enumerate(CLASS_NAMES):\n    print(f'  {i:2d}  {c}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-11T11:02:38.675204Z","iopub.execute_input":"2026-05-11T11:02:38.675496Z","iopub.status.idle":"2026-05-11T11:02:38.694827Z","shell.execute_reply.started":"2026-05-11T11:02:38.67546Z","shell.execute_reply":"2026-05-11T11:02:38.694053Z"}},"outputs":[],"execution_count":null},{"id":"03b2df99-4efe-46ed-9b1c-24fbd380f05a","cell_type":"markdown","source":"## 📊 Cell 3 – EDA – Phân tích dữ liệu","metadata":{}},{"id":"dea483fd-01dd-4352-9a65-02a2b0084c57","cell_type":"code","source":"df = pd.read_csv(TRAIN_CSV)\nprint(f'Tổng số dòng annotation: {len(df):,}')\nprint(f'Số ảnh unique          : {df[\"image_id\"].nunique():,}')\nprint(df.head())\n\n# ── Lọc bỏ \"No finding\" (class_id = 14) ───────────────────────────────────\ndf_lesion = df[df['class_id'] != 14].copy()\ndf_normal = df[df['class_id'] == 14].copy()\nprint(f'\\nAnnotation có tổn thương: {len(df_lesion):,}')\nprint(f'Ảnh \"No finding\"        : {len(df_normal[\"image_id\"].unique()):,}')\n\n# ── Merge nhãn trùng (consensus từ 3 bác sĩ) ──────────────────────────────\n# Giữ bbox theo WBF hoặc đơn giản nhất: mean của các bbox cùng class/image\ndf_consensus = (\n    df_lesion\n    .groupby(['image_id', 'class_id'], as_index=False)\n    .agg({'x_min': 'mean', 'y_min': 'mean', 'x_max': 'mean', 'y_max': 'mean',\n          'class_name': 'first', 'rad_id': 'count'})\n    .rename(columns={'rad_id': 'vote_count'})\n)\n# Chỉ giữ bbox được ít nhất 2/3 bác sĩ đồng ý → giảm nhiễu nhãn\ndf_consensus = df_consensus[df_consensus['vote_count'] >= 2].reset_index(drop=True)\nprint(f'\\nAnnotation sau consensus (≥2 votes): {len(df_consensus):,}')\n\n# ── Phân phối class ────────────────────────────────────────────────────────\nclass_counts = df_consensus['class_name'].value_counts()\nfig, ax = plt.subplots(figsize=(12, 5))\nclass_counts.plot(kind='barh', ax=ax, color='steelblue')\nax.set_title('Phân phối annotation theo class (sau consensus ≥2 votes)', fontsize=13)\nax.set_xlabel('Số lượng bbox')\nplt.tight_layout()\nplt.savefig(WORK_DIR / 'class_distribution.png', dpi=100)\nplt.show()\nprint(class_counts)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-11T11:02:38.696287Z","iopub.execute_input":"2026-05-11T11:02:38.696483Z","iopub.status.idle":"2026-05-11T11:02:39.398165Z","shell.execute_reply.started":"2026-05-11T11:02:38.696465Z","shell.execute_reply":"2026-05-11T11:02:39.397431Z"}},"outputs":[],"execution_count":null},{"id":"33120ef6-7f9c-47ac-a16f-3048eb2f1113","cell_type":"markdown","source":"## 🗂️ Cell 4 – Tạo YOLO Dataset (train / val split)","metadata":{}},{"id":"9a405e94-c7fd-485d-ae24-d0f7df3c1058","cell_type":"code","source":"# ── Load metadata kích thước ảnh DICOM gốc ────────────────────────────────\nMETA_CSV = Path('/kaggle/input/datasets/xhlulu/vinbigdata-chest-xray-resized-png-1024x1024/train_meta.csv')\ndf_meta = pd.read_csv(META_CSV)\nmeta_dict = {\n    row['image_id']: (row['dim1'], row['dim0'])  # (W_orig, H_orig)\n    for _, row in df_meta.iterrows()\n}\n\nPNG_SIZE = 1024\n\ndef xywh_normalize(row, image_id):\n    x_min, y_min = row['x_min'], row['y_min']\n    x_max, y_max = row['x_max'], row['y_max']\n    W_orig, H_orig = meta_dict.get(image_id, (PNG_SIZE, PNG_SIZE))\n    scale_x = PNG_SIZE / W_orig\n    scale_y = PNG_SIZE / H_orig\n    cx = ((x_min * scale_x + x_max * scale_x) / 2) / PNG_SIZE\n    cy = ((y_min * scale_y + y_max * scale_y) / 2) / PNG_SIZE\n    w  = (x_max - x_min) * scale_x / PNG_SIZE\n    h  = (y_max - y_min) * scale_y / PNG_SIZE\n    return cx, cy, w, h\n\n# ── Tạo thư mục YOLO ──────────────────────────────────────────────────────\ndef create_yolo_dirs(base: Path):\n    for split in ['train', 'val', 'test']:\n        (base / 'images' / split).mkdir(parents=True, exist_ok=True)\n        (base / 'labels' / split).mkdir(parents=True, exist_ok=True)\n\ncreate_yolo_dirs(YOLO_DIR)\n\nVALID_CLASS_IDS = set(range(14))\n\n# ── Stratified split 80/10/10 ─────────────────────────────────────────────\nimg_dominant = (\n    df_consensus\n    .groupby('image_id')['class_id']\n    .agg(lambda x: x.value_counts().index[0])\n    .reset_index()\n    .rename(columns={'class_id': 'dominant_class'})\n)\nnormal_ids = df_normal['image_id'].unique()\nnormal_df  = pd.DataFrame({'image_id': normal_ids, 'dominant_class': 14})\nimg_dominant = pd.concat([img_dominant, normal_df], ignore_index=True)\n\nfrom sklearn.model_selection import train_test_split\n\n# Bước 1: tách 80% train / 20% temp\ntrain_ids, temp_ids = train_test_split(\n    img_dominant['image_id'].values,\n    test_size=0.20,\n    random_state=42,\n    stratify=img_dominant['dominant_class'].values\n)\n\n# Bước 2: tách 20% temp → 10% val / 10% test\ntemp_dominant = img_dominant[img_dominant['image_id'].isin(temp_ids)]\nval_ids, test_ids = train_test_split(\n    temp_dominant['image_id'].values,\n    test_size=0.50,\n    random_state=42,\n    stratify=temp_dominant['dominant_class'].values\n)\n\nprint(f'Train : {len(train_ids):,} images')\nprint(f'Val   : {len(val_ids):,} images')\nprint(f'Test  : {len(test_ids):,} images')\nprint(f'Total : {len(train_ids)+len(val_ids)+len(test_ids):,} images')\n\n# ── Tạo label files & symlink ảnh ─────────────────────────────────────────\ndef write_labels_and_link(image_ids, split):\n    img_dir = YOLO_DIR / 'images' / split\n    lbl_dir = YOLO_DIR / 'labels' / split\n    skipped = 0\n\n    for img_id in tqdm(image_ids, desc=f'Processing {split}'):\n        src_img = TRAIN_IMG / f'{img_id}.png'\n        dst_img = img_dir / f'{img_id}.png'\n\n        try:\n            if not dst_img.exists():\n                os.symlink(src_img, dst_img)\n        except FileExistsError:\n            pass\n\n        if not src_img.exists():\n            skipped += 1\n            continue\n\n        rows = df_consensus[df_consensus['image_id'] == img_id]\n        lbl_path = lbl_dir / f'{img_id}.txt'\n\n        if len(rows) == 0:\n            lbl_path.touch()\n            continue\n\n        lines = []\n        for _, row in rows.iterrows():\n            cid = int(row['class_id'])\n            if cid not in VALID_CLASS_IDS:\n                continue\n            cx, cy, w, h = xywh_normalize(row, img_id)\n            cx = max(0.0, min(1.0, cx))\n            cy = max(0.0, min(1.0, cy))\n            w  = max(0.0, min(1.0, w))\n            h  = max(0.0, min(1.0, h))\n            if w > 0.001 and h > 0.001:\n                lines.append(f'{cid} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}')\n\n        with open(lbl_path, 'w') as f:\n            f.write('\\n'.join(lines))\n\n    print(f'  ⚠️  Skipped {skipped} missing images')\n\nwrite_labels_and_link(train_ids, 'train')\nwrite_labels_and_link(val_ids,   'val')\nwrite_labels_and_link(test_ids,  'test')\nprint('✅ Tạo YOLO dataset hoàn tất!')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-11T11:10:29.191112Z","iopub.execute_input":"2026-05-11T11:10:29.191459Z","iopub.status.idle":"2026-05-11T11:11:31.448808Z","shell.execute_reply.started":"2026-05-11T11:10:29.191434Z","shell.execute_reply":"2026-05-11T11:11:31.447836Z"}},"outputs":[],"execution_count":null},{"id":"014c13e0-4f79-4fd1-b57d-ca9adbe614e2","cell_type":"markdown","source":"## 📝 Cell 5 – Tạo data.yaml","metadata":{}},{"id":"1b1887e4-81b2-40ff-9e9d-329f3a870988","cell_type":"code","source":"yaml_content = f\"\"\"# VinBigData Chest X-Ray – YOLOv11\npath: {str(YOLO_DIR)}\ntrain: images/train\nval:   images/val\ntest:  images/test\n\nnc: {NUM_CLASSES}\nnames:\n  0:  Aortic enlargement\n  1:  Atelectasis\n  2:  Calcification\n  3:  Cardiomegaly\n  4:  Consolidation\n  5:  ILD\n  6:  Infiltration\n  7:  Lung Opacity\n  8:  Nodule/Mass\n  9:  Other lesion\n  10: Pleural effusion\n  11: Pleural thickening\n  12: Pneumothorax\n  13: Pulmonary fibrosis\n\"\"\"\n\nYAML_PATH = WORK_DIR / 'vinbigdata.yaml'\nwith open(YAML_PATH, 'w') as f:\n    f.write(yaml_content)\n\nprint(f'✅ Đã tạo: {YAML_PATH}')\nprint(yaml_content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-11T11:12:08.648234Z","iopub.execute_input":"2026-05-11T11:12:08.648542Z","iopub.status.idle":"2026-05-11T11:12:08.654652Z","shell.execute_reply.started":"2026-05-11T11:12:08.648519Z","shell.execute_reply":"2026-05-11T11:12:08.653774Z"}},"outputs":[],"execution_count":null},{"id":"c3de017a-1d03-4eb1-906a-05be9e6c5ab1","cell_type":"markdown","source":"## 🔬 Cell 6 – Kiểm tra annotation (sanity check)","metadata":{}},{"id":"7e3f02c9-06c9-4ff0-b220-77fd2d6836b6","cell_type":"code","source":"def visualize_sample(split='train', n=4):\n    \"\"\"Hiển thị n ảnh ngẫu nhiên với bbox.\"\"\"\n    img_dir = YOLO_DIR / 'images' / split\n    lbl_dir = YOLO_DIR / 'labels' / split\n    img_files = list(img_dir.glob('*.png'))\n    samples = random.sample(img_files, min(n, len(img_files)))\n\n    fig, axes = plt.subplots(1, len(samples), figsize=(5*len(samples), 5))\n    if len(samples) == 1:\n        axes = [axes]\n\n    COLORS = plt.cm.tab20(np.linspace(0, 1, NUM_CLASSES))\n\n    for ax, img_path in zip(axes, samples):\n        img = cv2.imread(str(img_path))\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        H, W = img.shape[:2]\n        ax.imshow(img, cmap='gray')\n\n        lbl_path = lbl_dir / (img_path.stem + '.txt')\n        if lbl_path.exists() and lbl_path.stat().st_size > 0:\n            with open(lbl_path) as f:\n                for line in f:\n                    parts = line.strip().split()\n                    cid, cx, cy, bw, bh = int(parts[0]), *map(float, parts[1:])\n                    x1 = (cx - bw/2) * W\n                    y1 = (cy - bh/2) * H\n                    bw_px = bw * W\n                    bh_px = bh * H\n                    rect = patches.Rectangle((x1, y1), bw_px, bh_px,\n                                             linewidth=2, edgecolor=COLORS[cid], facecolor='none')\n                    ax.add_patch(rect)\n                    ax.text(x1, y1-5, CLASS_NAMES[cid], color=COLORS[cid], fontsize=7,\n                            bbox=dict(facecolor='black', alpha=0.4, pad=1))\n\n        ax.set_title(img_path.stem[:20], fontsize=8)\n        ax.axis('off')\n\n    plt.suptitle(f'Sanity Check – {split} set', fontsize=13)\n    plt.tight_layout()\n    plt.show()\n\nvisualize_sample('train', n=5)\nvisualize_sample('val',   n=5)\nvisualize_sample('test',  n=5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-11T11:13:05.305011Z","iopub.execute_input":"2026-05-11T11:13:05.305754Z","iopub.status.idle":"2026-05-11T11:13:08.370255Z","shell.execute_reply.started":"2026-05-11T11:13:05.305723Z","shell.execute_reply":"2026-05-11T11:13:08.369467Z"}},"outputs":[],"execution_count":null},{"id":"229c15f7-7802-4302-a185-232b14295e85","cell_type":"code","source":"# # Chỉ lấy 10% data để test nhanh\n# from sklearn.model_selection import train_test_split\n\n# train_ids_small, _ = train_test_split(\n#     train_ids, test_size=0.90, random_state=42\n# )\n# QUICK_CONFIG = {**TRAIN_CONFIG, 'epochs': 5, 'name': 'yolo11s_quicktest'}\n\n# model = YOLO('yolo11s.pt')\n# results = model.train(**QUICK_CONFIG)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-11T11:18:09.572329Z","iopub.execute_input":"2026-05-11T11:18:09.572931Z","iopub.status.idle":"2026-05-11T11:41:15.489788Z","shell.execute_reply.started":"2026-05-11T11:18:09.572899Z","shell.execute_reply":"2026-05-11T11:41:15.48913Z"}},"outputs":[],"execution_count":null},{"id":"a7ffa6f3-465c-482d-86a3-40a54b3b819c","cell_type":"code","source":"# # ── Quick evaluation sau train thử ───────────────────────────────────────\n# quick_best = MODEL_DIR / 'yolo11s_quicktest' / 'weights' / 'best.pt'\n# print(f'Loading: {quick_best}')\n# print(f'Exists : {quick_best.exists()}')\n\n# model_quick = YOLO(str(quick_best))\n\n# for split in ['train', 'val', 'test']:\n#     print(f'\\n{\"─\"*45}')\n#     print(f'📊 Evaluating on {split} set...')\n#     res = model_quick.val(\n#         data    = str(YAML_PATH),\n#         split   = split,\n#         imgsz   = 640,\n#         batch   = 16,\n#         device  = DEVICE,\n#         conf    = 0.25,\n#         iou     = 0.5,\n#         verbose = False,\n#     )\n#     print(f'  mAP@0.5      : {res.box.map50:.4f}')\n#     print(f'  mAP@0.5:0.95 : {res.box.map:.4f}')\n#     print(f'  Precision    : {res.box.mp:.4f}')\n#     print(f'  Recall       : {res.box.mr:.4f}')\n\n# # ── Per-class mAP nhanh (chỉ trên val) ───────────────────────────────────\n# print(f'\\n{\"─\"*45}')\n# print('📋 Per-class mAP@0.5 (val set):')\n# res_val = model_quick.val(\n#     data    = str(YAML_PATH),\n#     split   = 'val',\n#     imgsz   = 640,\n#     batch   = 16,\n#     device  = DEVICE,\n#     conf    = 0.25,\n#     iou     = 0.5,\n#     verbose = False,\n# )\n# report = pd.DataFrame({\n#     'Class'   : CLASS_NAMES,\n#     'AP@0.5'  : res_val.box.ap50,\n# }).sort_values('AP@0.5', ascending=False)\n# print(report.to_string(index=False, float_format='{:.4f}'.format))\n# print(f'{\"─\"*45}')\n# print(f'Mean AP@0.5 : {report[\"AP@0.5\"].mean():.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-11T11:41:45.36152Z","iopub.execute_input":"2026-05-11T11:41:45.362134Z","iopub.status.idle":"2026-05-11T11:46:28.127888Z","shell.execute_reply.started":"2026-05-11T11:41:45.362106Z","shell.execute_reply":"2026-05-11T11:46:28.127145Z"}},"outputs":[],"execution_count":null},{"id":"79be51eb-3ceb-4029-bf97-2a569c85cbe3","cell_type":"markdown","source":"## 🚀 Cell 7 – Train YOLOv11\n\n### 🎯 Chiến lược tối ưu\n\n| Thành phần | Lựa chọn | Lý do |\n|---|---|---|\n| Model | **YOLOv11m** | Cân bằng accuracy/VRAM – P100/T4 16GB chạy tốt |\n| Optimizer | **AdamW** | Hội tụ nhanh hơn SGD, weight decay tránh overfitting |\n| Scheduler | **Cosine annealing** (`cos_lr=True`) | LR giảm mượt mà, tránh stuck local minima |\n| Loss box | **CIoU** (mặc định YOLO11) | Xử lý tốt bbox y tế (overlap lớn) |\n| Image size | **640** | Phù hợp T4/P100, ảnh 512px upscale nhẹ |\n| Batch | **16** | Đủ VRAM, gradient ổn định |\n| Augmentation | Mosaic + MixUp + Copy-paste | Mô phỏng đa dạng lesion |\n| Warmup | 3 epochs | Tránh diverge đầu training |\n| Pretrained | COCO weights | Transfer learning cải thiện đáng kể |\n| Close-mosaic | 10 epochs | Tắt mosaic cuối để fine-tune clean images |","metadata":{}},{"id":"e6b87e8a-a736-4196-9ae9-a259962366e9","cell_type":"code","source":"# ── Load YOLOv11m pretrained (COCO) ───────────────────────────────────────\nmodel = YOLO('yolo11m.pt')  # tự download ~40MB\n\n# ── Hyperparameters tối ưu cho medical imaging ────────────────────────────\nTRAIN_CONFIG = dict(\n    # ── Paths ────────────────────────────────────────────────────────────\n    data        = str(YAML_PATH),\n    project     = str(MODEL_DIR),\n    name        = 'yolo11m_vinbigdata',\n    exist_ok    = True,\n\n    # ── Hardware ─────────────────────────────────────────────────────────\n    device      = DEVICE,           # '0' hoặc '0,1'\n    workers     = 4,                # Kaggle: 4 workers phù hợp\n    amp         = True,             # Mixed precision FP16 – tiết kiệm VRAM\n\n    # ── Training schedule ────────────────────────────────────────────────\n    epochs      = 80,               # 80 epochs đủ với LR decay tốt\n    imgsz       = 640,              # Resolution\n    batch       = 16,               # Per GPU; DDP x2 GPU = effective 32\n\n    # ── Optimizer: AdamW ─────────────────────────────────────────────────\n    optimizer   = 'AdamW',\n    lr0         = 1e-3,             # LR khởi đầu\n    lrf         = 0.01,             # Final LR = lr0 * lrf = 1e-5\n    momentum    = 0.9,              # Beta1 của AdamW\n    weight_decay= 5e-4,             # L2 regularization\n    warmup_epochs   = 3,            # 3 epoch warmup\n    warmup_momentum = 0.8,\n    warmup_bias_lr  = 0.1,\n    cos_lr      = True,             # Cosine annealing scheduler\n\n    # ── Loss weights – tăng box để localize tốt hơn ─────────────────────\n    box         = 7.5,              # Box regression loss weight\n    cls         = 0.5,              # Classification loss weight\n    dfl         = 1.5,              # DFL loss weight\n\n    # ── Augmentation ────────────────────────────────────────────────────\n    # Mosaic: ghép 4 ảnh → mô phỏng nhiều lesion đồng thời\n    mosaic      = 1.0,\n    close_mosaic= 10,               # Tắt mosaic 10 epoch cuối → clean fine-tune\n\n    # MixUp: blend ảnh → regularize\n    mixup       = 0.1,\n\n    # Copy-paste: copy lesion sang ảnh khác → tăng rare class\n    copy_paste  = 0.1,\n\n    # Geometric\n    degrees     = 5.0,              # Nhỏ thôi – X-ray không xoay nhiều\n    translate   = 0.1,\n    scale       = 0.5,\n    shear       = 2.0,\n    perspective = 0.0,              # Không dùng cho X-ray\n    flipud      = 0.0,              # X-ray không lật dọc\n    fliplr      = 0.5,              # Lật ngang: hợp lệ với X-ray\n\n    # Color/Intensity – X-ray chỉ cần nhỏ\n    hsv_h       = 0.0,              # X-ray grayscale, không cần hue\n    hsv_s       = 0.0,              # Không cần saturation\n    hsv_v       = 0.3,              # Value (brightness) biến đổi nhẹ\n\n    # ── EarlyStopping & Saving ───────────────────────────────────────────\n    patience    = 20,               # Dừng nếu 20 epoch không cải thiện\n    save        = True,\n    save_period = 10,               # Lưu checkpoint mỗi 10 epoch\n\n    # ── Eval ────────────────────────────────────────────────────────────\n    val         = True,\n    plots       = True,\n    verbose     = True,\n)\n\nprint('🚀 Bắt đầu training...')\nprint(f'   Model    : YOLOv11m')\nprint(f'   Epochs   : {TRAIN_CONFIG[\"epochs\"]}')\nprint(f'   Batch    : {TRAIN_CONFIG[\"batch\"]}')\nprint(f'   Device   : {TRAIN_CONFIG[\"device\"]}')\nprint(f'   Img size : {TRAIN_CONFIG[\"imgsz\"]}')\nprint('─'*50)\n\nresults = model.train(**TRAIN_CONFIG)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-11T11:16:42.028957Z","iopub.execute_input":"2026-05-11T11:16:42.029746Z","iopub.status.idle":"2026-05-11T11:16:42.138652Z","shell.execute_reply.started":"2026-05-11T11:16:42.029714Z","shell.execute_reply":"2026-05-11T11:16:42.137558Z"}},"outputs":[],"execution_count":null},{"id":"c187f9ee-5037-4688-b00e-ef9aa44cc651","cell_type":"markdown","source":"## 📈 Cell 8 – Fine-tune (Phase 2) với learning rate nhỏ hơn\n\n> Resume từ best checkpoint, train thêm 20 epoch với LR thấp và tắt augmentation mạnh để fine-tune.","metadata":{}},{"id":"a0256bd6-ef83-434e-9235-3ac641b3276a","cell_type":"code","source":"# ── Tìm best.pt từ phase 1 ────────────────────────────────────────────────\nbest_ckpt = MODEL_DIR / 'yolo11m_vinbigdata' / 'weights' / 'best.pt'\nprint(f'Best checkpoint: {best_ckpt}')\nprint(f'Exists: {best_ckpt.exists()}')\n\n# ── Fine-tune Phase 2: lr thấp, ít augmentation ──────────────────────────\nmodel_ft = YOLO(str(best_ckpt))\n\nresults_ft = model_ft.train(\n    data        = str(YAML_PATH),\n    project     = str(MODEL_DIR),\n    name        = 'yolo11m_vinbigdata_finetune',\n    exist_ok    = True,\n\n    device      = DEVICE,\n    workers     = 4,\n    amp         = True,\n\n    # ── Schedule phase 2 ─────────────────────────────────────────────────\n    epochs      = 20,\n    imgsz       = 640,\n    batch       = 16,\n\n    optimizer   = 'AdamW',\n    lr0         = 1e-4,             # LR nhỏ hơn 10x\n    lrf         = 0.01,\n    weight_decay= 1e-4,\n    cos_lr      = True,\n    warmup_epochs = 1,\n\n    # ── Giảm augmentation để fine-tune sạch ─────────────────────────────\n    mosaic      = 0.5,              # Giảm mosaic\n    mixup       = 0.0,\n    copy_paste  = 0.0,\n    degrees     = 3.0,\n    scale       = 0.3,\n    fliplr      = 0.5,\n    hsv_v       = 0.2,\n\n    patience    = 10,\n    save        = True,\n    plots       = True,\n    verbose     = True,\n)\n\nprint('✅ Fine-tune hoàn tất!')","metadata":{},"outputs":[],"execution_count":null},{"id":"fab765c7-fc78-48d4-a88a-6d3a959e557e","cell_type":"markdown","source":"## 📊 Cell 9 – Evaluate & Visualize kết quả","metadata":{}},{"id":"f8c19341-823c-4dfb-a4f7-3032ae7ec0d5","cell_type":"code","source":"# ── Load best model từ fine-tune ──────────────────────────────────────────\nfinal_best = MODEL_DIR / 'yolo11m_vinbigdata_finetune' / 'weights' / 'best.pt'\nif not final_best.exists():\n    final_best = best_ckpt  # Fallback về phase 1\n\nprint(f'Evaluating: {final_best}')\nmodel_eval = YOLO(str(final_best))\n\n# ── Validation metrics ────────────────────────────────────────────────────\nval_results = model_eval.val(\n    data    = str(YAML_PATH),\n    imgsz   = 640,\n    batch   = 16,\n    device  = DEVICE,\n    verbose = True,\n    plots   = True,\n    conf    = 0.25,\n    iou     = 0.5,\n    save_json = True,\n)\n\nprint('\\n📊 Kết quả Validation:')\nprint(f'  mAP@0.5      : {val_results.box.map50:.4f}')\nprint(f'  mAP@0.5:0.95 : {val_results.box.map:.4f}')\nprint(f'  Precision    : {val_results.box.mp:.4f}')\nprint(f'  Recall       : {val_results.box.mr:.4f}')","metadata":{},"outputs":[],"execution_count":null},{"id":"b6405d43-fc27-47a8-90ba-97a511df744b","cell_type":"code","source":"# ── Hiển thị training curves ─────────────────────────────────────────────\nresults_csv = MODEL_DIR / 'yolo11m_vinbigdata' / 'results.csv'\nif results_csv.exists():\n    res_df = pd.read_csv(results_csv)\n    res_df.columns = res_df.columns.str.strip()\n\n    fig, axes = plt.subplots(2, 3, figsize=(15, 8))\n    metrics = [\n        ('train/box_loss',    'Train Box Loss'),\n        ('train/cls_loss',    'Train Cls Loss'),\n        ('train/dfl_loss',    'Train DFL Loss'),\n        ('metrics/mAP50(B)',  'mAP@0.5'),\n        ('metrics/mAP50-95(B)','mAP@0.5:0.95'),\n        ('val/box_loss',      'Val Box Loss'),\n    ]\n    for ax, (col, title) in zip(axes.flat, metrics):\n        if col in res_df.columns:\n            ax.plot(res_df['epoch'], res_df[col], linewidth=2)\n            ax.set_title(title)\n            ax.set_xlabel('Epoch')\n            ax.grid(True, alpha=0.3)\n    plt.suptitle('YOLOv11m – Training Curves (VinBigData)', fontsize=14)\n    plt.tight_layout()\n    plt.savefig(WORK_DIR / 'training_curves.png', dpi=120)\n    plt.show()","metadata":{},"outputs":[],"execution_count":null},{"id":"4709f9bf-e3fe-4b7b-8dd0-5a32a9f639d7","cell_type":"code","source":"# ── Inference trên ảnh val và hiển thị kết quả ───────────────────────────\nval_imgs = list((YOLO_DIR / 'images' / 'val').glob('*.png'))\nsample_imgs = random.sample(val_imgs, min(6, len(val_imgs)))\n\nfig, axes = plt.subplots(2, 3, figsize=(15, 10))\nfor ax, img_path in zip(axes.flat, sample_imgs):\n    preds = model_eval.predict(\n        source  = str(img_path),\n        imgsz   = 640,\n        conf    = 0.25,\n        iou     = 0.45,\n        verbose = False\n    )\n    res_img = preds[0].plot(font_size=8, line_width=2)\n    res_img = cv2.cvtColor(res_img, cv2.COLOR_BGR2RGB)\n    ax.imshow(res_img)\n    ax.set_title(img_path.stem[:20], fontsize=8)\n    ax.axis('off')\n\nplt.suptitle('Predictions trên validation set (conf≥0.25)', fontsize=13)\nplt.tight_layout()\nplt.savefig(WORK_DIR / 'val_predictions.png', dpi=120)\nplt.show()","metadata":{},"outputs":[],"execution_count":null},{"id":"7edc202a-9eb1-474c-8dbc-6bc23b743e9b","cell_type":"code","source":"# Evaluate trên test set (chạy sau khi có best model)\ntest_results = model_eval.val(\n    data    = str(YAML_PATH),\n    split   = 'test',          # ← chỉ định test split\n    imgsz   = 640,\n    batch   = 16,\n    device  = DEVICE,\n    verbose = True,\n    conf    = 0.25,\n    iou     = 0.5,\n)\nprint('\\n📊 Kết quả Test set (unseen):')\nprint(f'  mAP@0.5      : {test_results.box.map50:.4f}')\nprint(f'  mAP@0.5:0.95 : {test_results.box.map:.4f}')\nprint(f'  Precision    : {test_results.box.mp:.4f}')\nprint(f'  Recall       : {test_results.box.mr:.4f}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"9800484e-bf75-458f-bef8-448dceac17fd","cell_type":"markdown","source":"## 💾 Cell 10 – Export & Lưu Model","metadata":{}},{"id":"ce8608aa-0fab-46d8-90c4-9096b00c10ca","cell_type":"code","source":"# ── Export sang ONNX để deploy (optional) ─────────────────────────────────\nmodel_eval.export(\n    format  = 'onnx',\n    imgsz   = 640,\n    dynamic = True,\n    opset   = 17,\n    simplify= True,\n)\n\n# ── Copy best weights ra thư mục working ─────────────────────────────────\nout_pt   = WORK_DIR / 'yolo11m_vinbigdata_best.pt'\nout_onnx = WORK_DIR / 'yolo11m_vinbigdata_best.onnx'\n\nshutil.copy(final_best, out_pt)\nonnx_path = str(final_best).replace('.pt', '.onnx')\nif Path(onnx_path).exists():\n    shutil.copy(onnx_path, out_onnx)\n\nprint(f'✅ Saved PyTorch weights : {out_pt}')\nprint(f'✅ Saved ONNX model     : {out_onnx}')\nprint(f'\\nFile size (PT)  : {out_pt.stat().st_size/1e6:.1f} MB')","metadata":{},"outputs":[],"execution_count":null},{"id":"a9cedaec-dea1-4a37-9a9f-e2d2e06d8758","cell_type":"markdown","source":"## 🏆 Cell 11 – Per-class mAP Report","metadata":{}},{"id":"e516da59-56bc-4e56-9317-5ab5056298e7","cell_type":"code","source":"# ── Per-class metrics ─────────────────────────────────────────────────────\ntry:\n    ap50_per_class = val_results.box.ap50          # shape: (nc,)\n    ap_per_class   = val_results.box.ap            # mAP@0.5:0.95\n\n    report = pd.DataFrame({\n        'Class'        : CLASS_NAMES,\n        'AP@0.5'       : ap50_per_class,\n        'AP@0.5:0.95'  : ap_per_class,\n    }).sort_values('AP@0.5', ascending=False)\n\n    print('\\n📊 Per-class mAP Report')\n    print('─' * 45)\n    print(report.to_string(index=False, float_format='{:.4f}'.format))\n    print('─' * 45)\n    print(f'Mean AP@0.5      : {report[\"AP@0.5\"].mean():.4f}')\n    print(f'Mean AP@0.5:0.95 : {report[\"AP@0.5:0.95\"].mean():.4f}')\n\n    # Bar chart\n    fig, ax = plt.subplots(figsize=(10, 6))\n    report.plot(x='Class', y=['AP@0.5', 'AP@0.5:0.95'], kind='barh', ax=ax)\n    ax.set_title('Per-class mAP – YOLOv11m VinBigData', fontsize=13)\n    ax.set_xlabel('Average Precision')\n    plt.tight_layout()\n    plt.savefig(WORK_DIR / 'per_class_map.png', dpi=120)\n    plt.show()\n    report.to_csv(WORK_DIR / 'per_class_map.csv', index=False)\n\nexcept Exception as e:\n    print(f'Per-class report error: {e}')","metadata":{},"outputs":[],"execution_count":null},{"id":"522a862c-07d9-49a3-a4d3-96c08da38cfd","cell_type":"markdown","source":"## 🔧 Cell 12 – Tips & Troubleshooting\n\n```\n❓ OOM (Out of Memory)?\n   → Giảm batch=8, hoặc dùng yolo11s.pt (small)\n   → Bật amp=True (đã bật)\n   → Giảm imgsz=512\n\n❓ mAP thấp?\n   → Tăng epochs lên 120\n   → Thử WBF (Weighted Box Fusion) thay vì NMS\n   → Dùng TTA (Test Time Augmentation) khi predict\n   → Tăng weight_decay nếu overfit\n\n❓ Class imbalance?\n   → Thêm copy_paste=0.3 để augment rare classes\n   → Dùng focal loss (cls_pw khác 1.0)\n\n💡 Cải tiến thêm:\n   → SAHI (Sliced Inference) cho bbox nhỏ\n   → Ensemble nhiều model (yolo11s + yolo11m + yolo11l)\n   → CLAHE preprocessing trước khi đưa vào model\n```","metadata":{}},{"id":"14abcf0f-fbf6-4d67-9de6-b99eba325602","cell_type":"code","source":"# ── BONUS: Test Time Augmentation (TTA) ──────────────────────────────────\n# TTA thường cải thiện mAP thêm ~1-2%\nprint('🔬 Evaluating với TTA...')\nval_tta = model_eval.val(\n    data    = str(YAML_PATH),\n    imgsz   = 640,\n    batch   = 8,           # TTA tốn 2x VRAM\n    device  = DEVICE,\n    augment = True,        # TTA\n    conf    = 0.25,\n    iou     = 0.5,\n    verbose = False,\n)\nprint(f'TTA mAP@0.5      : {val_tta.box.map50:.4f}')\nprint(f'TTA mAP@0.5:0.95 : {val_tta.box.map:.4f}')","metadata":{},"outputs":[],"execution_count":null},{"id":"a512baa7-a8ea-4e6c-9ec6-f671ba9d8a2f","cell_type":"code","source":"# ── BONUS: CLAHE Preprocessing (tùy chọn) ────────────────────────────────\n# CLAHE (Contrast Limited Adaptive Histogram Equalization)\n# Cải thiện độ tương phản X-ray, giúp model detect lesion tốt hơn\n# Áp dụng khi preprocess dataset nếu muốn thử nghiệm\n\ndef apply_clahe(img_bgr, clip_limit=2.0, tile_grid=(8, 8)):\n    \"\"\"Áp dụng CLAHE lên ảnh grayscale X-ray.\"\"\"\n    gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)\n    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid)\n    enhanced = clahe.apply(gray)\n    return cv2.cvtColor(enhanced, cv2.COLOR_GRAY2BGR)\n\n# Demo CLAHE trên 1 ảnh\nsample = list((YOLO_DIR / 'images' / 'val').glob('*.png'))[0]\nimg_orig = cv2.imread(str(sample))\nimg_clahe = apply_clahe(img_orig)\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\nax1.imshow(cv2.cvtColor(img_orig,  cv2.COLOR_BGR2RGB), cmap='gray')\nax1.set_title('Original'); ax1.axis('off')\nax2.imshow(cv2.cvtColor(img_clahe, cv2.COLOR_BGR2RGB), cmap='gray')\nax2.set_title('CLAHE Enhanced'); ax2.axis('off')\nplt.suptitle('CLAHE Preprocessing Demo')\nplt.tight_layout()\nplt.savefig(WORK_DIR / 'clahe_demo.png', dpi=100)\nplt.show()\n\nprint('\\n✅ Notebook hoàn tất!')\nprint(f'📁 Outputs tại: {WORK_DIR}')\nprint(f'   best.pt      : {out_pt}')\nprint(f'   best.onnx    : {out_onnx}')\nprint(f'   per_class_map: {WORK_DIR}/per_class_map.csv')","metadata":{},"outputs":[],"execution_count":null}]}