{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112899,"databundleVersionId":13449579,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.metrics import roc_auc_score\nfrom PIL import Image\nimport os\nimport timm\nfrom timm.loss import AsymmetricLossMultiLabel\nfrom tqdm import tqdm\nimport warnings\n\nwarnings.filterwarnings('ignore')\n\n# --- 0. Cấu hình TỐI ƯU ---\nclass Config:\n    # PATHS\n    DATA_ROOT = \"/kaggle/input/grand-xray-slam-division-a\"\n    TRAIN_CSV = os.path.join(DATA_ROOT, \"train1.csv\")\n    TEST_CSV = os.path.join(DATA_ROOT, \"sample_submission_1.csv\")\n    TRAIN_IMAGE_DIR = os.path.join(DATA_ROOT, \"train1\")\n    TEST_IMAGE_DIR = os.path.join(DATA_ROOT, \"test1\")\n    \n    # MODEL - NÂNG CẤP\n    MODEL_NAME = \"tf_efficientnet_b4_ns\"  # Hoặc \"tf_efficientnetv2_s_in21ft1k\"\n    IMAGE_SIZE = 380  # Tận dụng kiến trúc B4\n    NUM_CLASSES = 14\n\n    # TRAINING - TỐI ƯU HIỆU SUẤT\n    BATCH_SIZE = 16   # Giảm để fit GPU với ảnh 380x380\n    NUM_EPOCHS = 10   # Train sâu 1 fold thay vì nông 3 fold\n    LEARNING_RATE = 3e-4\n    WEIGHT_DECAY = 1e-5\n    NUM_WORKERS = 8\n    SEED = 42\n    \n    # CROSS-VALIDATION - CHỈ DÙNG 1 FOLD ĐỂ TẬN DỤNG THỜI GIAN\n    N_SPLITS = 2\n    TARGET_COLS = ['Atelectasis', 'Cardiomegaly', 'Consolidation', 'Edema', \n                   'Enlarged Cardiomediastinum', 'Fracture', 'Lung Lesion', \n                   'Lung Opacity', 'No Finding', 'Pleural Effusion', \n                   'Pleural Other', 'Pneumonia', 'Pneumothorax', 'Support Devices']\n\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# Set random seed\ndef set_seed(seed):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed(seed)\n        torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\nset_seed(Config.SEED)\n\n# --- 1. Dataset với Albumentations ---\nclass ChestXRayDataset(Dataset):\n    def __init__(self, df, image_dir, transform=None, is_test=False):\n        self.df = df\n        self.image_dir = image_dir\n        self.transform = transform\n        self.is_test = is_test\n        self.image_names = df['Image_name'].tolist()\n        if not is_test:\n            self.labels = df[Config.TARGET_COLS].values.astype(np.float32)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_name = self.image_names[idx]\n        img_path = os.path.join(self.image_dir, img_name)\n        image = Image.open(img_path).convert(\"RGB\")\n        image = np.array(image)  # Albumentations cần numpy array\n\n        if self.transform:\n            augmented = self.transform(image=image)\n            image = augmented['image']\n        \n        if self.is_test:\n            return image, img_name\n        else:\n            labels = torch.tensor(self.labels[idx], dtype=torch.float32)\n            return image, labels\n\n# --- 2. Augmentation mạnh với Albumentations ---\ntrain_transform = A.Compose([\n    A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE),\n    A.HorizontalFlip(p=0.5),\n    A.Rotate(limit=15, p=0.5),\n    A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.3),\n    A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=0.5),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\ntest_transform = A.Compose([\n    A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\n# Load data\ntrain_df = pd.read_csv(Config.TRAIN_CSV)\ntest_df = pd.read_csv(Config.TEST_CSV)\nprint(f\"Train DF shape: {train_df.shape}\")\nprint(f\"Test DF shape: {test_df.shape}\")\n\n# --- 3. Mô hình ---\nclass MultiLabelEfficientNet(nn.Module):\n    def __init__(self, model_name=Config.MODEL_NAME, num_classes=Config.NUM_CLASSES, pretrained=True):\n        super().__init__()\n        self.model = timm.create_model(model_name, pretrained=pretrained, num_classes=num_classes)\n        # EfficientNet B4/V2 đã tự động thay classifier nếu num_classes != 1000\n\n    def forward(self, x):\n        return self.model(x)\n\n# --- 4. Hàm Train/Validate với AMP ---\ndef train_one_epoch(model, dataloader, optimizer, criterion, scheduler, device, scaler):\n    model.train()\n    running_loss = 0.0\n    for images, labels in tqdm(dataloader, desc=\"Training\"):\n        images, labels = images.to(device), labels.to(device)\n\n        optimizer.zero_grad()\n        \n        with torch.cuda.amp.autocast():\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n        \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        scheduler.step()\n\n        running_loss += loss.item() * images.size(0)\n    return running_loss / len(dataloader.dataset)\n\ndef validate_one_epoch(model, dataloader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    all_preds = []\n    all_labels = []\n    with torch.no_grad():\n        for images, labels in tqdm(dataloader, desc=\"Validation\"):\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            running_loss += loss.item() * images.size(0)\n            all_preds.append(torch.sigmoid(outputs).cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n\n    avg_loss = running_loss / len(dataloader.dataset)\n    all_preds = np.concatenate(all_preds)\n    all_labels = np.concatenate(all_labels)\n    \n    roc_auc_per_class = []\n    for i in range(Config.NUM_CLASSES):\n        try:\n            if len(np.unique(all_labels[:, i])) > 1:\n                roc_auc_per_class.append(roc_auc_score(all_labels[:, i], all_preds[:, i]))\n            else:\n                roc_auc_per_class.append(np.nan)\n        except:\n            roc_auc_per_class.append(np.nan)\n\n    mean_auc = np.nanmean(roc_auc_per_class)\n    return avg_loss, mean_auc\n\n# --- 5. TTA Predict Function ---\ndef tta_predict(model, image, device, n_tta=4):\n    model.eval()\n    preds = []\n    \n    # Define TTA transforms\n    tta_transforms = [\n        A.Compose([A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE), \n                   A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2()]),\n        A.Compose([A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE), A.HorizontalFlip(p=1),\n                   A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2()]),\n        A.Compose([A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE), A.Rotate(limit=10, p=1),\n                   A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2()]),\n        A.Compose([A.Resize(Config.IMAGE_SIZE, Config.IMAGE_SIZE), A.RandomBrightnessContrast(p=1),\n                   A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2()])\n    ]\n    \n    for i in range(n_tta):\n        tf = tta_transforms[i]\n        img_tta = tf(image=np.array(image))['image'].unsqueeze(0).to(device)\n        with torch.no_grad(), torch.cuda.amp.autocast():\n            pred = torch.sigmoid(model(img_tta)).cpu().numpy()\n        preds.append(pred)\n    \n    return np.mean(preds, axis=0)\n\n# --- 6. Pipeline Huấn luyện (1 FOLD, 12 EPOCH, MODEL SOUP) ---\nprint(f\"\\n--- Bắt đầu huấn luyện mô hình {Config.MODEL_NAME} ---\")\n\ngkf = GroupKFold(n_splits=Config.N_SPLITS)\n\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(train_df, train_df[Config.TARGET_COLS], groups=train_df['Patient_ID'])):\n    print(f\"\\n--- Fold {fold+1}/{Config.N_SPLITS} ---\")\n    \n    train_fold_df = train_df.iloc[train_idx].reset_index(drop=True)\n    val_fold_df = train_df.iloc[val_idx].reset_index(drop=True)\n\n    train_dataset = ChestXRayDataset(train_fold_df, Config.TRAIN_IMAGE_DIR, train_transform)\n    val_dataset = ChestXRayDataset(val_fold_df, Config.TRAIN_IMAGE_DIR, test_transform)\n\n    train_loader = DataLoader(train_dataset, batch_size=Config.BATCH_SIZE, shuffle=True, \n                              num_workers=Config.NUM_WORKERS, pin_memory=True)\n    val_loader = DataLoader(val_dataset, batch_size=Config.BATCH_SIZE, shuffle=False, \n                            num_workers=Config.NUM_WORKERS, pin_memory=True)\n\n    model = MultiLabelEfficientNet(pretrained=True).to(Config.DEVICE)\n    optimizer = torch.optim.AdamW(model.parameters(), lr=Config.LEARNING_RATE, weight_decay=Config.WEIGHT_DECAY)\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=5, T_mult=1, eta_min=1e-6)\n    criterion = AsymmetricLossMultiLabel(gamma_neg=4, gamma_pos=1, clip=0.05)  # Xử lý imbalance tốt hơn\n    scaler = torch.cuda.amp.GradScaler()\n\n    best_val_auc = -1.0\n    checkpoint_paths = []\n\n    for epoch in range(Config.NUM_EPOCHS):\n        print(f\"\\n>>> Epoch {epoch+1}/{Config.NUM_EPOCHS} <<<\")\n        train_loss = train_one_epoch(model, train_loader, optimizer, criterion, scheduler, Config.DEVICE, scaler)\n        val_loss, val_auc = validate_one_epoch(model, val_loader, criterion, Config.DEVICE)\n        \n        print(f\"Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, Val Mean AUC: {val_auc:.4f}\")\n\n        # Lưu checkpoint mỗi epoch để làm model soup\n        checkpoint_path = f\"model_epoch_{epoch+1}_fold_{fold}.pth\"\n        torch.save(model.state_dict(), checkpoint_path)\n        checkpoint_paths.append(checkpoint_path)\n\n        if val_auc > best_val_auc:\n            best_val_auc = val_auc\n            torch.save(model.state_dict(), f\"best_model_fold_{fold}.pth\")\n            print(f\"🌟 Lưu mô hình tốt nhất với Val Mean AUC: {best_val_auc:.4f}\")\n\n    # --- MODEL SOUP: Average last 5 checkpoints ---\n    print(\"\\n--- Tạo Model Soup từ last 5 checkpoints ---\")\n    model_soup = MultiLabelEfficientNet(pretrained=False).to(Config.DEVICE)\n    soup_state = model_soup.state_dict()\n    \n    # Lấy 5 epoch cuối\n    last_5_paths = checkpoint_paths[-5:]\n    for key in soup_state:\n        soup_state[key] = torch.stack([\n            torch.load(path, map_location=Config.DEVICE)[key] for path in last_5_paths\n        ]).mean(dim=0)\n    \n    model_soup.load_state_dict(soup_state)\n    torch.save(model_soup.state_dict(), f\"model_soup_fold_{fold}.pth\")\n    print(\"✅ Đã lưu Model Soup.\")\n\n# --- 7. Dự đoán trên tập Kiểm tra với TTA ---\nprint(\"\\n--- Dự đoán trên tập kiểm tra với TTA ---\")\n\n# Load model soup\nmodel = MultiLabelEfficientNet(pretrained=False).to(Config.DEVICE)\nmodel.load_state_dict(torch.load(f\"model_soup_fold_{fold}.pth\"))\nmodel.eval()\n\ntest_dataset = ChestXRayDataset(test_df, Config.TEST_IMAGE_DIR, test_transform, is_test=True)\ntest_image_names = test_dataset.image_names\n\ntest_preds = []\n\nfor idx in tqdm(range(len(test_dataset)), desc=\"TTA Predicting\"):\n    image, img_name = test_dataset[idx]\n    pred = tta_predict(model, image, Config.DEVICE, n_tta=4)  # TTA x4\n    test_preds.append(pred)\n\nraw_predictions = np.vstack(test_preds)\nprint(f\"Shape của dự đoán thô: {raw_predictions.shape}\")\n\n# --- 8. Hậu xử lý NÂNG CAO: Label Correlation Adjustment ---\nprint(\"\\n--- Áp dụng Hậu xử lý NÂNG CAO ---\")\n\n# Tính ma trận correlation từ tập train\ncorr_matrix = train_df[Config.TARGET_COLS].corr().values\nnp.fill_diagonal(corr_matrix, 0)  # Loại bỏ self-correlation\n\nfinal_predictions = raw_predictions.copy()\n\n# Điều chỉnh dựa trên correlation\nfor i in range(final_predictions.shape[0]):\n    adjusted = np.zeros_like(final_predictions[i])\n    for j in range(Config.NUM_CLASSES):\n        # Weighted sum by correlation\n        corr_weights = corr_matrix[j]\n        weighted_sum = np.dot(corr_weights, final_predictions[i])\n        total_corr = np.sum(np.abs(corr_weights)) + 1e-8\n        adjustment = weighted_sum / total_corr\n        adjusted[j] = final_predictions[i][j] + 0.1 * adjustment  # Hệ số 0.1 điều chỉnh nhẹ\n    \n    # Chuẩn hoá về [0,1]\n    adjusted = np.clip(adjusted, 0, 1)\n    final_predictions[i] = adjusted\n\n# Thêm rule-based cũ để đảm bảo \"No Finding\" hợp lý\nNF_idx = Config.TARGET_COLS.index('No Finding')\nother_indices = [i for i in range(Config.NUM_CLASSES) if i != NF_idx]\n\nfor i in range(final_predictions.shape[0]):\n    p_nf = final_predictions[i, NF_idx]\n    p_others = final_predictions[i, other_indices]\n    max_p_disease = np.max(p_others) if len(p_others) > 0 else 0.0\n\n    if p_nf > max_p_disease:\n        final_predictions[i, other_indices] = final_predictions[i, other_indices] * (1 - p_nf)\n    else:\n        final_predictions[i, NF_idx] = final_predictions[i, NF_idx] * (1 - max_p_disease)\n    \n    final_predictions[i] = np.clip(final_predictions[i], 1e-6, 1 - 1e-6)\n\nprint(f\"Shape của dự đoán sau hậu xử lý: {final_predictions.shape}\")\n\n# --- 9. Tạo file Submission ---\nprint(\"\\n--- Tạo file submission.csv ---\")\n\nsubmission_df = pd.DataFrame(final_predictions, columns=Config.TARGET_COLS)\nsubmission_df.insert(0, 'Image_name', test_image_names)\nsubmission_df.to_csv(\"submission.csv\", index=False)\n\nprint(\"🎉 Đã tạo submission.csv thành công!\")\nprint(\"Một vài hàng đầu của submission.csv:\")\nprint(submission_df.head())","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-19T20:39:49.225168Z","iopub.execute_input":"2025-09-19T20:39:49.225529Z","iopub.status.idle":"2025-09-19T20:40:07.501797Z","shell.execute_reply.started":"2025-09-19T20:39:49.225495Z","shell.execute_reply":"2025-09-19T20:40:07.50067Z"}},"outputs":[],"execution_count":null}]}