{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"LOAD VÀ LÀM SẠCH DỮ LIỆU","metadata":{}},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\n\nDATA_DIR = \"/kaggle/input/competitions/vinbigdata-chest-xray-abnormalities-detection\"\n\ntrain_csv = os.path.join(DATA_DIR, \"train.csv\")\ntrain_img_dir = os.path.join(DATA_DIR, \"train\")\n\ndf = pd.read_csv(train_csv)\n\nprint(\"Original shape:\", df.shape)\n\n# giữ class hợp lệ\ndf = df[df[\"class_id\"].between(0, 14)]\ndf = df.reset_index(drop=True)\n\nprint(\"After cleaning:\", df.shape)\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T02:00:27.733632Z","iopub.execute_input":"2026-03-29T02:00:27.734519Z","iopub.status.idle":"2026-03-29T02:00:28.175389Z","shell.execute_reply.started":"2026-03-29T02:00:27.734473Z","shell.execute_reply":"2026-03-29T02:00:28.174457Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"GOM NHÃN THEO ẢNH","metadata":{}},{"cell_type":"code","source":"grouped = df.groupby(\"image_id\")[\"class_id\"].apply(list).reset_index()\n\nprint(\"Total annotated images:\", len(grouped))\n\ngrouped.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T02:00:31.610361Z","iopub.execute_input":"2026-03-29T02:00:31.611132Z","iopub.status.idle":"2026-03-29T02:00:31.892123Z","shell.execute_reply.started":"2026-03-29T02:00:31.611098Z","shell.execute_reply":"2026-03-29T02:00:31.891311Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"CHUYỂN SANG MULTI-LABEL","metadata":{}},{"cell_type":"code","source":"NUM_CLASSES = 15 \n\ndef multi_hot(labels):\n    target = np.zeros(NUM_CLASSES)\n    for l in labels:\n        target[int(l)] = 1\n    return target\n\ngrouped[\"target\"] = grouped[\"class_id\"].apply(multi_hot)\n\nprint(\"Sample target:\", grouped.iloc[0][\"target\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T02:00:34.265935Z","iopub.execute_input":"2026-03-29T02:00:34.266371Z","iopub.status.idle":"2026-03-29T02:00:34.290618Z","shell.execute_reply.started":"2026-03-29T02:00:34.26634Z","shell.execute_reply":"2026-03-29T02:00:34.289783Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"KIỂM TRA IMBALANCE VÀ TÍNH CLASS WEIGHTS","metadata":{}},{"cell_type":"code","source":"import torch\n\ndef check_class_distribution(df, name=\"Dataset\"):\n    all_targets = np.vstack(df[\"target\"].values)\n    class_counts = all_targets.sum(axis=0)\n\n    print(f\"\\n{name} distribution:\")\n    print(class_counts)\n\n    print(\"Min:\", class_counts.min())\n    print(\"Max:\", class_counts.max())\n    print(\"Ratio:\", class_counts.max() / (class_counts.min() + 1e-6))\n\n    return class_counts\n\nclass_counts = check_class_distribution(grouped, \"Original\")\n\nclass_counts[class_counts == 0] = 1\n\nclass_weights = 1.0 / class_counts\nclass_weights = class_weights / class_weights.max()\n\nclass_weights = torch.tensor(class_weights, dtype=torch.float32)\n\nprint(\"\\nClass weights:\\n\", class_weights)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T02:00:37.28438Z","iopub.execute_input":"2026-03-29T02:00:37.284795Z","iopub.status.idle":"2026-03-29T02:00:41.634361Z","shell.execute_reply.started":"2026-03-29T02:00:37.284766Z","shell.execute_reply":"2026-03-29T02:00:41.633725Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"CHUẨN HOÁ ẢNH DICOM","metadata":{}},{"cell_type":"code","source":"import cv2\nimport pydicom\n\ndef read_dicom(path, img_size=224):\n    dicom = pydicom.dcmread(path)\n    img = dicom.pixel_array.astype(np.float32)\n\n    if dicom.PhotometricInterpretation == \"MONOCHROME1\":\n        img = np.max(img) - img\n\n    lower, upper = np.percentile(img, (1, 99))\n    img = np.clip(img, lower, upper)\n\n    img = (img - img.min()) / (img.max() - img.min() + 1e-6)\n\n    img = cv2.resize(img, (img_size, img_size))\n\n    img = np.stack([img]*3, axis=-1)\n\n    return img","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T02:00:46.168098Z","iopub.execute_input":"2026-03-29T02:00:46.168966Z","iopub.status.idle":"2026-03-29T02:00:47.035289Z","shell.execute_reply.started":"2026-03-29T02:00:46.168935Z","shell.execute_reply":"2026-03-29T02:00:47.034704Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"ÁP DỤNG DATA AUGMENTATION","metadata":{}},{"cell_type":"code","source":"from torchvision import transforms\n\nIMG_SIZE = 224\n\ntrain_tf = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(5),\n    transforms.ColorJitter(brightness=0.1, contrast=0.1),\n    transforms.ToTensor(),\n])\n\nval_tf = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T02:00:51.143697Z","iopub.execute_input":"2026-03-29T02:00:51.144035Z","iopub.status.idle":"2026-03-29T02:00:54.572861Z","shell.execute_reply.started":"2026-03-29T02:00:51.144007Z","shell.execute_reply":"2026-03-29T02:00:54.572104Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"CHIA TRAIN / VALIDATION","metadata":{}},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\ntrain_df, val_df = train_test_split(\n    grouped,\n    test_size=0.2,\n    random_state=42\n)\n\ntrain_df = train_df.reset_index(drop=True)\nval_df = val_df.reset_index(drop=True)\n\nprint(\"Train size:\", len(train_df))\nprint(\"Validation size:\", len(val_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T02:00:57.07638Z","iopub.execute_input":"2026-03-29T02:00:57.077568Z","iopub.status.idle":"2026-03-29T02:00:57.968786Z","shell.execute_reply.started":"2026-03-29T02:00:57.077535Z","shell.execute_reply":"2026-03-29T02:00:57.967903Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"TEST ĐỌC ẢNH","metadata":{}},{"cell_type":"code","source":"sample_path = os.path.join(train_img_dir, train_df.iloc[0][\"image_id\"] + \".dicom\")\n\nimg = read_dicom(sample_path)\n\nprint(\"Image shape:\", img.shape)\nprint(\"Min:\", img.min(), \"Max:\", img.max())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T02:01:01.076346Z","iopub.execute_input":"2026-03-29T02:01:01.07716Z","iopub.status.idle":"2026-03-29T02:01:01.577911Z","shell.execute_reply.started":"2026-03-29T02:01:01.077127Z","shell.execute_reply":"2026-03-29T02:01:01.577118Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"TẠO CACHE","metadata":{}},{"cell_type":"code","source":"from joblib import Parallel, delayed\n\ndef save_dicom_as_npy(row, cache_dir):\n    dicom_path = os.path.join(train_img_dir, row[\"image_id\"] + \".dicom\")\n    img = read_dicom(dicom_path)\n    np.save(os.path.join(cache_dir, row[\"image_id\"] + \".npy\"), img)\n    return row[\"image_id\"] \n\nos.makedirs(\"cache/train\", exist_ok=True)\nos.makedirs(\"cache/val\", exist_ok=True)\n\nresults = Parallel(n_jobs=8, backend=\"loky\", verbose=10)(\n    delayed(save_dicom_as_npy)(row, \"cache/train\") for _, row in train_df.iterrows()\n)\nprint(\"Train caching done:\", len(results))\n\nresults = Parallel(n_jobs=8, backend=\"loky\", verbose=10)(\n    delayed(save_dicom_as_npy)(row, \"cache/val\") for _, row in val_df.iterrows()\n)\nprint(\"Val caching done:\", len(results))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T02:01:05.468285Z","iopub.execute_input":"2026-03-29T02:01:05.469034Z","iopub.status.idle":"2026-03-29T03:15:28.83678Z","shell.execute_reply.started":"2026-03-29T02:01:05.469002Z","shell.execute_reply":"2026-03-29T03:15:28.835972Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"DATASET CLASS","metadata":{}},{"cell_type":"code","source":"from torch.utils.data import Dataset\n\nclass CXRDatasetCached(Dataset):\n    def __init__(self, df, cache_dir, transform=None):\n        self.df = df\n        self.cache_dir = cache_dir\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 = np.load(os.path.join(self.cache_dir, row[\"image_id\"]+\".npy\"))\n        if self.transform:\n            img = self.transform(img)\n        target = torch.tensor(row[\"target\"], dtype=torch.float32)\n        return img, target","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T03:15:46.421153Z","iopub.execute_input":"2026-03-29T03:15:46.421718Z","iopub.status.idle":"2026-03-29T03:15:46.426847Z","shell.execute_reply.started":"2026-03-29T03:15:46.421687Z","shell.execute_reply":"2026-03-29T03:15:46.426172Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"DATALOADER","metadata":{}},{"cell_type":"code","source":"from torch.utils.data import DataLoader\n\ntrain_dataset = CXRDatasetCached(train_df, \"cache/train\", transform=train_tf)\nval_dataset   = CXRDatasetCached(val_df, \"cache/val\", transform=val_tf)\n\ntrain_loader = DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=2)\nval_loader   = DataLoader(val_dataset, batch_size=4, shuffle=False, num_workers=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T03:15:51.124645Z","iopub.execute_input":"2026-03-29T03:15:51.125075Z","iopub.status.idle":"2026-03-29T03:15:51.131224Z","shell.execute_reply.started":"2026-03-29T03:15:51.125044Z","shell.execute_reply":"2026-03-29T03:15:51.130447Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"IMPORT + SETUP","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport timm\nfrom sklearn.metrics import roc_auc_score\nfrom torch.cuda.amp import autocast, GradScaler\n\n# Device\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device)\n\n# Tăng tốc GPU\ntorch.backends.cudnn.benchmark = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T03:16:00.968411Z","iopub.execute_input":"2026-03-29T03:16:00.969172Z","iopub.status.idle":"2026-03-29T03:16:05.546552Z","shell.execute_reply.started":"2026-03-29T03:16:00.969139Z","shell.execute_reply":"2026-03-29T03:16:05.54591Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"MODEL: ConvNeXtV2 + ViT","metadata":{}},{"cell_type":"code","source":"class HybridConvNeXtViT(nn.Module):\n    def __init__(self, num_classes=15):\n        super().__init__()\n\n        # CNN backbone\n        self.convnext = timm.create_model(\n            \"convnextv2_tiny\",\n            pretrained=True,\n            num_classes=0\n        )\n\n        # Transformer backbone\n        self.vit = timm.create_model(\n            \"vit_small_patch16_224\",\n            pretrained=True,\n            num_classes=0\n        )\n\n        # Feature dimension\n        conv_dim = self.convnext.num_features\n        vit_dim = self.vit.num_features\n\n        # Fusion head\n        self.fc = nn.Sequential(\n            nn.Linear(conv_dim + vit_dim, 512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, num_classes)\n        )\n\n    def forward(self, x):\n        f1 = self.convnext(x)\n        f2 = self.vit(x)\n\n        x = torch.cat([f1, f2], dim=1)\n        x = self.fc(x)\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T03:16:13.57213Z","iopub.execute_input":"2026-03-29T03:16:13.572556Z","iopub.status.idle":"2026-03-29T03:16:13.578789Z","shell.execute_reply.started":"2026-03-29T03:16:13.57253Z","shell.execute_reply":"2026-03-29T03:16:13.577939Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"LOAD MODEL + MULTI-GPU","metadata":{}},{"cell_type":"code","source":"model = HybridConvNeXtViT(num_classes=15)\n\nmodel = model.to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T03:16:18.579459Z","iopub.execute_input":"2026-03-29T03:16:18.580047Z","iopub.status.idle":"2026-03-29T03:16:25.137836Z","shell.execute_reply.started":"2026-03-29T03:16:18.580017Z","shell.execute_reply":"2026-03-29T03:16:25.136925Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"TEST FORWARD","metadata":{}},{"cell_type":"code","source":"imgs, targets = next(iter(train_loader))\nimgs = imgs.to(device)\n\nwith torch.no_grad():\n    outputs = model(imgs)\n\nprint(\"Output shape:\", outputs.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T03:16:30.531221Z","iopub.execute_input":"2026-03-29T03:16:30.53191Z","iopub.status.idle":"2026-03-29T03:16:34.579614Z","shell.execute_reply.started":"2026-03-29T03:16:30.531877Z","shell.execute_reply":"2026-03-29T03:16:34.578776Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"LOSS + OPIMIZER + SCHEDULER","metadata":{}},{"cell_type":"code","source":"criterion = nn.BCEWithLogitsLoss(\n    pos_weight=class_weights.to(device)\n)\n\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=1e-4,\n    weight_decay=1e-4\n)\n\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer,\n    mode='max',\n    factor=0.5,\n    patience=2\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T03:16:39.543571Z","iopub.execute_input":"2026-03-29T03:16:39.544708Z","iopub.status.idle":"2026-03-29T03:16:39.551247Z","shell.execute_reply.started":"2026-03-29T03:16:39.544669Z","shell.execute_reply":"2026-03-29T03:16:39.550611Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"MIXED PRECISION","metadata":{}},{"cell_type":"code","source":"from torch.amp import GradScaler\nscaler = GradScaler() ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T03:16:43.863543Z","iopub.execute_input":"2026-03-29T03:16:43.864205Z","iopub.status.idle":"2026-03-29T03:16:43.868179Z","shell.execute_reply.started":"2026-03-29T03:16:43.864173Z","shell.execute_reply":"2026-03-29T03:16:43.867364Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"EVALUATE","metadata":{}},{"cell_type":"code","source":"def evaluate(model, loader):\n    model.eval()\n    all_targets = []\n    all_outputs = []\n\n    with torch.no_grad():\n        for imgs, targets in loader:\n            imgs = imgs.to(device)\n            targets = targets.to(device).float()\n\n            outputs = model(imgs)\n            outputs = torch.sigmoid(outputs)\n\n            all_targets.append(targets.cpu())\n            all_outputs.append(outputs.cpu())\n\n    all_targets = torch.cat(all_targets).numpy()\n    all_outputs = torch.cat(all_outputs).numpy()\n\n    auc = roc_auc_score(all_targets, all_outputs, average=\"macro\")\n    return auc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T03:16:50.475554Z","iopub.execute_input":"2026-03-29T03:16:50.476113Z","iopub.status.idle":"2026-03-29T03:16:50.481379Z","shell.execute_reply.started":"2026-03-29T03:16:50.476082Z","shell.execute_reply":"2026-03-29T03:16:50.480653Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"TRAIN LOOP","metadata":{}},{"cell_type":"code","source":"from torch.amp import autocast\n\nEPOCHS = 20\n\nbest_auc = 0\npatience = 3\ncounter = 0\n\ntrain_losses = [] \nval_aucs = [] \n\nfor epoch in range(EPOCHS):\n    model.train()\n    train_loss = 0\n\n    for imgs, targets in train_loader:\n        imgs = imgs.to(device)\n        targets = targets.to(device).float()\n\n        optimizer.zero_grad()\n\n        with autocast(device_type=\"cuda\", enabled=(device.type == \"cuda\")):\n            outputs = model(imgs)\n            loss = criterion(outputs, targets)\n\n        scaler.scale(loss).backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n        scaler.step(optimizer)\n        scaler.update()\n\n        train_loss += loss.item()\n\n    train_loss /= len(train_loader)\n\n    val_auc = evaluate(model, val_loader)\n\n    train_losses.append(train_loss)\n    val_aucs.append(val_auc)\n\n    print(f\"Epoch {epoch+1}/{EPOCHS} - Loss: {train_loss:.4f} - Val AUC: {val_auc:.4f}\")\n\n    if val_auc > best_auc:\n        best_auc = val_auc\n        counter = 0\n        torch.save(model.state_dict(), \"best_model.pth\")\n        print(\"Saved best model!\")\n    else:\n        counter += 1\n\n    scheduler.step(val_auc)\n\n    if counter >= patience:\n        print(\"Early stopping triggered!\")\n        break\nprint(\"Best AUC:\", best_auc)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T03:16:55.312919Z","iopub.execute_input":"2026-03-29T03:16:55.313617Z","iopub.status.idle":"2026-03-29T04:22:17.631191Z","shell.execute_reply.started":"2026-03-29T03:16:55.313548Z","shell.execute_reply":"2026-03-29T04:22:17.629777Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"VẼ BIỂU ĐỒ","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport os\n\nsave_dir = \"/kaggle/working/\"\nos.makedirs(save_dir, exist_ok=True)\n\nepochs = range(1, len(train_losses) + 1)\n\nplt.figure(figsize=(6,5))\nplt.plot(epochs, train_losses, marker='o')\nplt.title(\"Training Loss\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.grid()\n\nplt.savefig(os.path.join(save_dir, \"loss_curve.png\"), dpi=300) \nplt.show()\n\nplt.figure(figsize=(6,5))\nplt.plot(epochs, val_aucs, marker='o')\nplt.title(\"Validation AUC\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"AUC\")\nplt.grid()\n\nplt.savefig(os.path.join(save_dir, \"auc_curve.png\"), dpi=300)  \nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-29T04:26:33.357425Z","iopub.execute_input":"2026-03-29T04:26:33.357944Z","iopub.status.idle":"2026-03-29T04:26:34.148159Z","shell.execute_reply.started":"2026-03-29T04:26:33.357908Z","shell.execute_reply":"2026-03-29T04:26:34.147445Z"}},"outputs":[],"execution_count":null}]}