{"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":"gpu","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":"# =================================================================================\n# KAGGLE NOTEBOOK: GRAND X-RAY SLAM - CHIẾN LƯỢC \"TITAN REGULATOR\" v2.1\n# Tác giả: Kaggle Grandmaster (Persona)\n# Phiên bản: 2.1 - Sửa lỗi TypeError, Albumentations Warnings, và GradScaler Deprecation\n# Mục tiêu: Đạt hiệu suất SOTA (>0.96) với siêu kiến trúc EVA-02\n# =================================================================================\n\n# --- 1. IMPORT CÁC THƯ VIỆN CẦN THIẾT ---\nimport os\nimport gc\nimport random\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.metrics import roc_auc_score\nimport timm\nfrom tqdm.notebook import tqdm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport cv2\n\nprint(\"Tất cả thư viện đã được import thành công.\")\n\n# --- 2. CẤU HÌNH TOÀN CỤC (CONFIGURATION) ---\n\nclass Config:\n    DEBUG = False\n    MODEL_NAME = \"eva02_large_patch14_448.mim_in22k_ft_in1k\"\n    IMAGE_SIZE = 448\n    EPOCHS_FROZEN = 2\n    LR_FROZEN = 1e-3\n    EPOCHS_FINETUNE = 4\n    LR_FINETUNE = 1e-5\n    WEIGHT_DECAY_FROZEN = 1e-6\n    WEIGHT_DECAY_FINETUNE = 1e-4\n    LABEL_SMOOTHING = 0.05\n    EARLY_STOPPING_PATIENCE = 2\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    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    NUM_CLASSES = len(TARGET_COLS)\n    BATCH_SIZE = 2\n    N_SPLITS = 5\n    VALIDATION_FOLD = 0\n    USE_AMP = True\n    NUM_WORKERS = 2\n    SEED = 42\n    ACCUMULATION_STEPS = 8 # Mô phỏng batch size hiệu quả là 4 * 4 = 16\n    USE_CHECKPOINTING = True\n\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nprint(f\"Sử dụng thiết bị: {Config.DEVICE}\")\nprint(f\"Sử dụng mô hình: {Config.MODEL_NAME} với kích thước ảnh {Config.IMAGE_SIZE}\")\nprint(f\"Chế độ DEBUG: {'BẬT' if Config.DEBUG else 'TẮT'}\")\n\n# --- 3. HÀM TIỆN ÍCH & PIPELINE DỮ LIỆU ---\n\ndef set_seed(seed=Config.SEED):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(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()\n\ndef get_transforms(size, is_train=True):\n    \"\"\"\n    Pipeline tăng cường dữ liệu đã được cập nhật để sửa lỗi warnings.\n    \"\"\"\n    if is_train:\n        return A.Compose([\n            A.Resize(size, size, interpolation=cv2.INTER_AREA),\n            A.HorizontalFlip(p=0.5),\n            # Sửa lỗi warning: Sử dụng các tham số đúng cho Affine\n            A.Affine(scale=(0.9, 1.1), translate_percent=(-0.1, 0.1), rotate=(-20, 20), p=0.75, \n                     cval=0, interpolation=cv2.INTER_AREA),\n            A.OneOf([\n                A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2),\n                A.FancyPCA(),\n                A.HueSaturationValue(),\n            ], p=0.7),\n            A.OneOf([\n                A.MotionBlur(blur_limit=5),\n                A.MedianBlur(blur_limit=5),\n                A.GaussianBlur(blur_limit=5),\n            ], p=0.5),\n            A.GaussNoise(p=0.5),\n            # # Sửa lỗi warning: Sử dụng Cutout, là tên mới của CoarseDropout\n            # A.Cutout(num_holes=12, max_h_size=int(size*0.12), max_w_size=int(size*0.12), \n            #          fill_value=0, p=0.75),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ])\n    else:\n        return A.Compose([\n            A.Resize(size, size, interpolation=cv2.INTER_AREA),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ])\n\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        # Lấy trước nhãn để tối ưu hóa\n        if not is_test:\n            self.labels = self.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        row = self.df.iloc[idx]\n        img_path = os.path.join(self.image_dir, row['Image_name'])\n        try:\n            image = cv2.imread(img_path)\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        except Exception as e:\n            print(f\"Lỗi khi đọc ảnh: {img_path}. Lỗi: {e}\")\n            image = np.zeros((Config.IMAGE_SIZE, Config.IMAGE_SIZE, 3), dtype=np.uint8)\n        \n        if self.transform:\n            augmented = self.transform(image=image)\n            image = augmented['image']\n        \n        if self.is_test:\n            return image\n        else:\n            # SỬA LỖI TYPEERROR: Lấy trực tiếp từ self.labels đã được ép kiểu\n            labels = self.labels[idx]\n            return image, torch.from_numpy(labels)\n\n# --- 4. MÔ HÌNH & CÁC LỚP PHÒNG THỦ ---\n\nclass TitanModel(nn.Module):\n    def __init__(self, model_name, num_classes, pretrained=True, checkpointing=False):\n        super().__init__()\n        # Tạo model mà không ép tham số checkpointing\n        self.model = timm.create_model(\n            model_name, \n            pretrained=pretrained, \n            num_classes=num_classes\n        )\n\n        # Nếu model có hỗ trợ checkpointing thì bật qua method\n        if checkpointing and hasattr(self.model, \"set_grad_checkpointing\"):\n            self.model.set_grad_checkpointing(enable=True)\n    def forward(self, x):\n        return self.model(x)\n        \n    def freeze_backbone(self):\n        print(\"Đóng băng các lớp backbone...\")\n        for name, param in self.model.named_parameters():\n            if not name.startswith('head.'):\n                param.requires_grad = False\n                \n    def unfreeze_backbone(self):\n        print(\"Mở băng toàn bộ mô hình để fine-tune...\")\n        for param in self.model.parameters():\n            param.requires_grad = True\n\nclass SmoothedBCEWithLogitsLoss(nn.Module):\n    def __init__(self, pos_weight=None, smoothing=0.1):\n        super(SmoothedBCEWithLogitsLoss, self).__init__()\n        self.smoothing = smoothing\n        self.bce = nn.BCEWithLogitsLoss(pos_weight=pos_weight, reduction='none')\n    def forward(self, outputs, labels):\n        labels_smoothed = torch.where(labels > 0.5, 1.0 - self.smoothing, self.smoothing)\n        loss = self.bce(outputs, labels_smoothed)\n        return loss.mean()\n\nclass EarlyStopping:\n    def __init__(self, patience=2, delta=0.0001):\n        self.patience = patience; self.counter = 0; self.best_score = None\n        self.early_stop = False; self.delta = delta\n    def __call__(self, val_score, model, path):\n        if self.best_score is None:\n            self.best_score = val_score; self.save_checkpoint(model, path)\n        elif val_score < self.best_score + self.delta:\n            self.counter += 1\n            print(f'EarlyStopping counter: {self.counter} out of {self.patience}')\n            if self.counter >= self.patience: self.early_stop = True\n        else:\n            self.best_score = val_score; self.save_checkpoint(model, path); self.counter = 0\n    def save_checkpoint(self, model, path):\n        print(f'Validation score improved ({self.best_score:.4f}). Saving model to {path} ...')\n        torch.save(model.state_dict(), path)\n\n# --- 5. HÀM HUẤN LUYỆN & ĐÁNH GIÁ ---\n\ndef train_one_epoch(model, dataloader, optimizer, scheduler, criterion, device, scaler):\n    model.train()\n    running_loss = 0.0\n    pbar = tqdm(enumerate(dataloader), total=len(dataloader), desc=\"Training\", leave=False)\n    \n    # <<< THAY ĐỔI: Logic tích lũy gradient\n    for step, (images, labels) in pbar:\n        images, labels = images.to(device), labels.to(device)\n        \n        with torch.amp.autocast(device_type=device.type, enabled=Config.USE_AMP):\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            # Scale loss theo số bước tích lũy\n            loss = loss / Config.ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        # Chỉ cập nhật trọng số sau ACCUMULATION_STEPS\n        if (step + 1) % Config.ACCUMULATION_STEPS == 0:\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            if scheduler:\n                scheduler.step()\n        \n        running_loss += loss.item() * Config.ACCUMULATION_STEPS # Nhân lại để log loss đúng\n        pbar.set_postfix(loss=f'{running_loss / (step + 1):.4f}')\n        \n    return running_loss / len(dataloader)\n\ndef validate_one_epoch(model, dataloader, device):\n    model.eval(); all_preds, all_labels = [], []\n    with torch.no_grad():\n        for images, labels in tqdm(dataloader, desc=\"Validation\", leave=False):\n            images, labels = images.to(device), labels.to(device)\n            with torch.amp.autocast(device_type=device.type, enabled=Config.USE_AMP):\n                outputs = model(images)\n            all_preds.append(torch.sigmoid(outputs).cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n    all_preds = np.concatenate(all_preds); all_labels = np.concatenate(all_labels)\n    return roc_auc_score(all_labels, all_preds, average='macro')\n\n# --- 6. PIPELINE HUẤN LUYỆN CHÍNH ---\n\nprint(\"===== BẮT ĐẦU CHUẨN BỊ DỮ LIỆU =====\")\ntrain_df = pd.read_csv(Config.TRAIN_CSV)\n# if Config.DEBUG:\nprint(\"!!! CHẾ ĐỘ DEBUG ĐANG BẬT, SỬ DỤNG 1000 MẪU !!!\")\n# train_df = train_df.sample(n=10000, random_state=Config.SEED).reset_index(drop=True)\n\ntrain_df['Patient_ID'] = train_df['Image_name'].apply(lambda x: x.split('_')[0])\n\nsgkf = StratifiedGroupKFold(n_splits=Config.N_SPLITS, shuffle=True, random_state=Config.SEED)\ny_stratify = train_df[Config.TARGET_COLS].sum(axis=1)\ntrain_df['fold'] = -1\nfor fold, (train_idx, val_idx) in enumerate(sgkf.split(train_df, y_stratify, groups=train_df['Patient_ID'])):\n    train_df.loc[val_idx, 'fold'] = fold\n\ntrain_fold_df = train_df[train_df['fold'] != Config.VALIDATION_FOLD].reset_index(drop=True)\nval_fold_df = train_df[train_df['fold'] == Config.VALIDATION_FOLD].reset_index(drop=True)\nprint(f\"Huấn luyện trên {len(train_fold_df)} mẫu, kiểm định trên {len(val_fold_df)} mẫu.\")\n\ntrain_dataset = ChestXRayDataset(train_fold_df, Config.TRAIN_IMAGE_DIR, get_transforms(Config.IMAGE_SIZE, is_train=True))\nval_dataset = ChestXRayDataset(val_fold_df, Config.TRAIN_IMAGE_DIR, get_transforms(Config.IMAGE_SIZE, is_train=False))\ntrain_loader = DataLoader(train_dataset, batch_size=Config.BATCH_SIZE, shuffle=True, num_workers=Config.NUM_WORKERS, pin_memory=True)\nval_loader = DataLoader(val_dataset, batch_size=Config.BATCH_SIZE*2, shuffle=False, num_workers=Config.NUM_WORKERS, pin_memory=True)\n\nmodel = TitanModel(\n    Config.MODEL_NAME, \n    Config.NUM_CLASSES,\n    checkpointing=Config.USE_CHECKPOINTING\n).to(Config.DEVICE)\n\npos_counts = train_fold_df[Config.TARGET_COLS].sum()\nneg_counts = len(train_fold_df) - pos_counts\npos_weight = (neg_counts / pos_counts).values\ncriterion = SmoothedBCEWithLogitsLoss(pos_weight=torch.tensor(pos_weight, dtype=torch.float32).to(Config.DEVICE), \n                                     smoothing=Config.LABEL_SMOOTHING)\n# SỬA LỖI GRADSCALER\nscaler = torch.amp.GradScaler(device=Config.DEVICE.type, enabled=Config.USE_AMP)\n\n# === GIAI ĐOẠN 1: HUẤN LUYỆN CLASSIFIER (FROZEN) ===\nprint(\"\\n===== BẮT ĐẦU GIAI ĐOẠN 1: FROZEN BACKBONE =====\")\nmodel.freeze_backbone()\n# Quan trọng: đảm bảo optimizer chỉ nhận các tham số có thể huấn luyện\noptimizer = torch.optim.AdamW(\n    filter(lambda p: p.requires_grad, model.parameters()), \n    lr=Config.LR_FROZEN, \n    weight_decay=Config.WEIGHT_DECAY_FROZEN\n)\nearly_stopper = EarlyStopping(patience=Config.EARLY_STOPPING_PATIENCE)\n\nfor epoch in range(Config.EPOCHS_FROZEN):\n    print(f\"\\n--- Epoch Frozen {epoch + 1}/{Config.EPOCHS_FROZEN} ---\")\n    train_loss = train_one_epoch(model, train_loader, optimizer, None, criterion, Config.DEVICE, scaler)\n    val_auc = validate_one_epoch(model, val_loader, Config.DEVICE)\n    print(f\"Epoch {epoch + 1} | Train Loss: {train_loss:.4f} | Val AUC: {val_auc:.4f}\")\n    early_stopper(val_auc, model, \"best_model_frozen.pth\")\n    if early_stopper.early_stop:\n        print(\"Dừng sớm trong giai đoạn frozen.\")\n        break\n\nprint(\"\\n===== BẮT ĐẦU GIAI ĐOẠN 2: FINE-TUNING =====\")\nmodel.load_state_dict(torch.load(\"best_model_frozen.pth\"))\nmodel.unfreeze_backbone()\noptimizer = torch.optim.AdamW(model.parameters(), lr=Config.LR_FINETUNE, weight_decay=Config.WEIGHT_DECAY_FINETUNE)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=len(train_loader) * Config.EPOCHS_FINETUNE)\nearly_stopper = EarlyStopping(patience=Config.EARLY_STOPPING_PATIENCE, delta=0.0001)\n\nfor epoch in range(Config.EPOCHS_FINETUNE):\n    print(f\"\\n--- Epoch Finetune {epoch + 1}/{Config.EPOCHS_FINETUNE} ---\")\n    train_loss = train_one_epoch(model, train_loader, optimizer, scheduler, criterion, Config.DEVICE, scaler)\n    val_auc = validate_one_epoch(model, val_loader, Config.DEVICE)\n    print(f\"Epoch {epoch + 1} | Train Loss: {train_loss:.4f} | Val AUC: {val_auc:.4f}\")\n    early_stopper(val_auc, model, \"best_model_finetuned.pth\")\n    if early_stopper.early_stop:\n        print(\"Dừng sớm trong giai đoạn fine-tuning.\")\n        break\n        \ngc.collect()\ntorch.cuda.empty_cache()\n\nprint(\"\\n===== BẮT ĐẦU SUY LUẬN TRÊN TẬP TEST =====\")\ntest_df = pd.read_csv(Config.TEST_CSV)\ntest_dataset = ChestXRayDataset(test_df, Config.TEST_IMAGE_DIR, get_transforms(Config.IMAGE_SIZE, is_train=False), is_test=True)\ntest_loader = DataLoader(test_dataset, batch_size=Config.BATCH_SIZE*2, shuffle=False, num_workers=Config.NUM_WORKERS)\n\nmodel.load_state_dict(torch.load(\"best_model_finetuned.pth\"))\nmodel.eval()\nall_preds = []\n\nwith torch.no_grad():\n    for images in tqdm(test_loader, desc=\"Inference\"):\n        images = images.to(Config.DEVICE)\n        with torch.amp.autocast(device_type=Config.DEVICE.type, enabled=Config.USE_AMP):\n            preds_orig = torch.sigmoid(model(images))\n            images_flipped = torch.flip(images, dims=[3])\n            preds_flipped = torch.sigmoid(model(images_flipped))\n            avg_preds = (preds_orig + preds_flipped) / 2.0\n            all_preds.append(avg_preds.cpu().numpy())\n\npredictions = np.concatenate(all_preds)\n\nprint(\"\\n===== TẠO FILE SUBMISSION =====\")\nsubmission_df = pd.DataFrame(predictions, columns=Config.TARGET_COLS)\nsubmission_df.insert(0, 'Image_name', test_df['Image_name'])\nsubmission_df.to_csv(\"submission.csv\", index=False)\n\nprint(\"\\nĐã tạo file 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-20T19:26:15.259484Z","iopub.execute_input":"2025-09-20T19:26:15.259763Z"}},"outputs":[],"execution_count":null}]}