{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":10338,"databundleVersionId":862042},{"sourceType":"kernelVersion","sourceId":316980196}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# CELL 1: IMPORT THƯ VIỆN VÀ CẤU HÌNH CHUNG\n# ============================================================\n\nimport os\nimport gc\nimport json\nimport random\nimport shutil\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nimport torchvision.transforms as T\nimport torchvision.models as models\n\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nfrom sklearn.metrics import (\n    roc_auc_score,\n    accuracy_score,\n    precision_score,\n    recall_score,\n    f1_score,\n    confusion_matrix,\n    classification_report\n)\n\nimport matplotlib.pyplot as plt\n\n# ------------------------------------------------------------\n# Cấu hình seed để kết quả có thể tái lập\n# ------------------------------------------------------------\nSEED = 42\n\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    \n    # Giúp kết quả ổn định hơn, nhưng có thể làm train hơi chậm\n    torch.backends.cudnn.deterministic = False\n    torch.backends.cudnn.benchmark = True\n\nset_seed(SEED)\n\n# ------------------------------------------------------------\n# Cấu hình thiết bị\n# ------------------------------------------------------------\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nprint(\"Device:\", DEVICE)\n\nif torch.cuda.is_available():\n    print(\"GPU:\", torch.cuda.get_device_name(0))\n    print(\"CUDA version:\", torch.version.cuda)\n\n# ------------------------------------------------------------\n# Thư mục output\n# ------------------------------------------------------------\nOUTPUT_DIR = Path(\"/kaggle/working/notebook02_baseline\")\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\nCKPT_DIR = OUTPUT_DIR / \"checkpoints\"\nCKPT_DIR.mkdir(parents=True, exist_ok=True)\n\nPRED_DIR = OUTPUT_DIR / \"predictions\"\nPRED_DIR.mkdir(parents=True, exist_ok=True)\n\nFIG_DIR = OUTPUT_DIR / \"figures\"\nFIG_DIR.mkdir(parents=True, exist_ok=True)\n\nprint(\"OUTPUT_DIR:\", OUTPUT_DIR)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 2: TÌM METADATA TỪ NOTEBOOK 01\n# ============================================================\n\nINPUT_ROOT = Path(\"/kaggle/input\")\nWORKING_ROOT = Path(\"/kaggle/working\")\n\ndef find_file_everywhere(filename):\n    \"\"\"\n    Tìm file trong /kaggle/working trước, sau đó trong /kaggle/input.\n    \"\"\"\n    search_roots = [WORKING_ROOT, INPUT_ROOT]\n    \n    for root in search_roots:\n        matches = list(root.rglob(filename))\n        if len(matches) > 0:\n            return matches[0]\n    \n    return None\n\nRSNA_TRAIN_CSV = find_file_everywhere(\"rsna_train.csv\")\nRSNA_VAL_CSV = find_file_everywhere(\"rsna_val.csv\")\nRSNA_TEST_CSV = find_file_everywhere(\"rsna_test.csv\")\n\nprint(\"RSNA_TRAIN_CSV:\", RSNA_TRAIN_CSV)\nprint(\"RSNA_VAL_CSV  :\", RSNA_VAL_CSV)\nprint(\"RSNA_TEST_CSV :\", RSNA_TEST_CSV)\n\nassert RSNA_TRAIN_CSV is not None, \"Không tìm thấy rsna_train.csv. Hãy add output Notebook 01.\"\nassert RSNA_VAL_CSV is not None, \"Không tìm thấy rsna_val.csv. Hãy add output Notebook 01.\"\nassert RSNA_TEST_CSV is not None, \"Không tìm thấy rsna_test.csv. Hãy add output Notebook 01.\"\n\nprint(\"Đã tìm thấy đủ metadata RSNA.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 3: ĐỌC METADATA RSNA\n# ============================================================\n\ntrain_df = pd.read_csv(RSNA_TRAIN_CSV)\nval_df = pd.read_csv(RSNA_VAL_CSV)\ntest_df = pd.read_csv(RSNA_TEST_CSV)\n\nprint(\"Train shape:\", train_df.shape)\nprint(\"Val shape  :\", val_df.shape)\nprint(\"Test shape :\", test_df.shape)\n\nprint(\"\\nTrain label distribution:\")\nprint(train_df[\"label\"].value_counts())\n\nprint(\"\\nVal label distribution:\")\nprint(val_df[\"label\"].value_counts())\n\nprint(\"\\nTest label distribution:\")\nprint(test_df[\"label\"].value_counts())\n\ndisplay(train_df.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 4: KIỂM TRA ĐƯỜNG DẪN ẢNH RSNA\n# ============================================================\n\ndef count_missing_images(df):\n    return (~df[\"image_path\"].apply(lambda x: Path(x).exists())).sum()\n\nmissing_train = count_missing_images(train_df)\nmissing_val = count_missing_images(val_df)\nmissing_test = count_missing_images(test_df)\n\nprint(\"Missing train images:\", missing_train)\nprint(\"Missing val images  :\", missing_val)\nprint(\"Missing test images :\", missing_test)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 5: SỬA ĐƯỜNG DẪN ẢNH RSNA NẾU CẦN\n# ============================================================\n\ndef find_dir(root, dirname):\n    matches = [p for p in Path(root).rglob(dirname) if p.is_dir()]\n    return matches[0] if matches else None\n\ntotal_missing = missing_train + missing_val + missing_test\n\nif total_missing > 0:\n    print(\"Có ảnh bị missing. Đang tìm lại thư mục stage_2_train_images...\")\n    \n    RSNA_TRAIN_IMG_DIR = find_dir(INPUT_ROOT, \"stage_2_train_images\")\n    \n    print(\"RSNA_TRAIN_IMG_DIR:\", RSNA_TRAIN_IMG_DIR)\n    assert RSNA_TRAIN_IMG_DIR is not None, \"Không tìm thấy stage_2_train_images. Hãy add RSNA dataset.\"\n    \n    def fix_rsna_path(patient_id):\n        return str(RSNA_TRAIN_IMG_DIR / f\"{patient_id}.dcm\")\n    \n    train_df[\"image_path\"] = train_df[\"patientId\"].apply(fix_rsna_path)\n    val_df[\"image_path\"] = val_df[\"patientId\"].apply(fix_rsna_path)\n    test_df[\"image_path\"] = test_df[\"patientId\"].apply(fix_rsna_path)\n\nprint(\"Missing train images after fix:\", count_missing_images(train_df))\nprint(\"Missing val images after fix  :\", count_missing_images(val_df))\nprint(\"Missing test images after fix :\", count_missing_images(test_df))\n\nassert count_missing_images(train_df) == 0, \"Train vẫn còn ảnh missing.\"\nassert count_missing_images(val_df) == 0, \"Val vẫn còn ảnh missing.\"\nassert count_missing_images(test_df) == 0, \"Test vẫn còn ảnh missing.\"\n\nprint(\"Đường dẫn ảnh RSNA đã OK.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 6: IMPORT PYDICOM ĐỂ ĐỌC ẢNH DICOM\n# ============================================================\n\ntry:\n    import pydicom\n    print(\"pydicom imported successfully.\")\nexcept ImportError:\n    print(\"pydicom chưa có. Đang cài đặt...\")\n    !pip install -q pydicom\n    import pydicom\n    print(\"pydicom installed and imported successfully.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 7: HÀM ĐỌC DICOM RSNA\n# ============================================================\n\ndef read_dicom_as_pil(path):\n    \"\"\"\n    Đọc ảnh DICOM và chuyển thành PIL Image RGB.\n    \n    Vì ResNet50/ViT pretrained ImageNet nhận ảnh 3 kênh,\n    ta chuyển grayscale X-quang thành RGB bằng cách lặp lại 3 kênh.\n    \"\"\"\n    dicom = pydicom.dcmread(path)\n    img = dicom.pixel_array.astype(np.float32)\n    \n    # Normalize intensity về [0, 255]\n    img = img - np.min(img)\n    max_val = np.max(img)\n    if max_val > 0:\n        img = img / max_val\n    img = (img * 255).astype(np.uint8)\n    \n    # Chuyển grayscale sang PIL RGB\n    pil_img = Image.fromarray(img).convert(\"RGB\")\n    return pil_img","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 8: TEST ĐỌC THỬ ẢNH RSNA\n# ============================================================\n\nsample_paths = train_df[\"image_path\"].sample(n=3, random_state=SEED).tolist()\n\nfor p in sample_paths:\n    img = read_dicom_as_pil(p)\n    print(Path(p).name, \"size:\", img.size, \"mode:\", img.mode)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 9: TRANSFORM CHO TRAIN / VAL / TEST\n# ============================================================\n\nIMG_SIZE = 224\n\n# Normalize theo ImageNet pretrained\nIMAGENET_MEAN = [0.485, 0.456, 0.406]\nIMAGENET_STD = [0.229, 0.224, 0.225]\n\ntrain_transform = T.Compose([\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.RandomRotation(degrees=7),\n    T.RandomHorizontalFlip(p=0.5),\n    T.ColorJitter(brightness=0.10, contrast=0.10),\n    T.ToTensor(),\n    T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n])\n\neval_transform = T.Compose([\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.ToTensor(),\n    T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n])\n\nprint(\"Transforms ready.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 10: DATASET CLASS CHO RSNA\n# ============================================================\n\nclass RSNADataset(Dataset):\n    \"\"\"\n    Dataset cho bài toán binary classification:\n    Pneumonia / Non-pneumonia.\n    \"\"\"\n    \n    def __init__(self, dataframe, transform=None):\n        self.df = dataframe.reset_index(drop=True).copy()\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        \n        image_path = row[\"image_path\"]\n        label = float(row[\"label\"])\n        \n        image = read_dicom_as_pil(image_path)\n        \n        if self.transform is not None:\n            image = self.transform(image)\n        \n        label = torch.tensor(label, dtype=torch.float32)\n        \n        return {\n            \"image\": image,\n            \"label\": label,\n            \"patientId\": row[\"patientId\"]\n        }","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 11: TẠO DATALOADER\n# ============================================================\n\nBATCH_SIZE = 16\nNUM_WORKERS = 2\n\ntrain_dataset = RSNADataset(train_df, transform=train_transform)\nval_dataset = RSNADataset(val_df, transform=eval_transform)\ntest_dataset = RSNADataset(test_df, transform=eval_transform)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=NUM_WORKERS,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=NUM_WORKERS,\n    pin_memory=True\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=NUM_WORKERS,\n    pin_memory=True\n)\n\nprint(\"Train batches:\", len(train_loader))\nprint(\"Val batches  :\", len(val_loader))\nprint(\"Test batches :\", len(test_loader))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 12: TEST MỘT BATCH DỮ LIỆU\n# ============================================================\n\nbatch = next(iter(train_loader))\n\nprint(\"Image batch shape:\", batch[\"image\"].shape)\nprint(\"Label batch shape:\", batch[\"label\"].shape)\nprint(\"Patient IDs example:\", batch[\"patientId\"][:3])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 13: HÀM TÍNH METRIC BINARY CLASSIFICATION\n# ============================================================\n\ndef compute_binary_metrics(y_true, y_prob, threshold=0.5):\n    \"\"\"\n    y_true: ground truth 0/1\n    y_prob: xác suất dự đoán lớp pneumonia\n    threshold: ngưỡng phân loại\n    \"\"\"\n    y_true = np.array(y_true).astype(int)\n    y_prob = np.array(y_prob)\n    y_pred = (y_prob >= threshold).astype(int)\n    \n    # AUC có thể lỗi nếu chỉ có một class trong y_true\n    try:\n        auc = roc_auc_score(y_true, y_prob)\n    except ValueError:\n        auc = np.nan\n    \n    acc = accuracy_score(y_true, y_pred)\n    precision = precision_score(y_true, y_pred, zero_division=0)\n    recall = recall_score(y_true, y_pred, zero_division=0)\n    f1 = f1_score(y_true, y_pred, zero_division=0)\n    \n    cm = confusion_matrix(y_true, y_pred, labels=[0, 1])\n    tn, fp, fn, tp = cm.ravel()\n    \n    specificity = tn / (tn + fp + 1e-8)\n    \n    return {\n        \"auc\": auc,\n        \"accuracy\": acc,\n        \"precision\": precision,\n        \"recall_pneumonia\": recall,\n        \"specificity\": specificity,\n        \"f1\": f1,\n        \"tn\": int(tn),\n        \"fp\": int(fp),\n        \"fn\": int(fn),\n        \"tp\": int(tp),\n        \"threshold\": threshold\n    }","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 14: HÀM EVALUATE MODEL\n# ============================================================\n\n@torch.no_grad()\ndef evaluate_model(model, data_loader, criterion=None):\n    model.eval()\n    \n    all_labels = []\n    all_probs = []\n    all_patient_ids = []\n    total_loss = 0.0\n    total_samples = 0\n    \n    for batch in tqdm(data_loader, desc=\"Evaluating\", leave=False):\n        images = batch[\"image\"].to(DEVICE, non_blocking=True)\n        labels = batch[\"label\"].to(DEVICE, non_blocking=True)\n        \n        logits = model(images).squeeze(1)\n        \n        if criterion is not None:\n            loss = criterion(logits, labels)\n            total_loss += loss.item() * images.size(0)\n        \n        probs = torch.sigmoid(logits)\n        \n        all_labels.extend(labels.detach().cpu().numpy().tolist())\n        all_probs.extend(probs.detach().cpu().numpy().tolist())\n        all_patient_ids.extend(batch[\"patientId\"])\n        \n        total_samples += images.size(0)\n    \n    avg_loss = total_loss / total_samples if criterion is not None else None\n    metrics = compute_binary_metrics(all_labels, all_probs, threshold=0.5)\n    \n    pred_df = pd.DataFrame({\n        \"patientId\": all_patient_ids,\n        \"label\": all_labels,\n        \"prob_pneumonia\": all_probs\n    })\n    pred_df[\"pred\"] = (pred_df[\"prob_pneumonia\"] >= 0.5).astype(int)\n    \n    return avg_loss, metrics, pred_df","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 15: HÀM TRAIN MỘT EPOCH\n# ============================================================\n\ndef train_one_epoch(\n    model,\n    train_loader,\n    optimizer,\n    criterion,\n    scaler,\n    accumulation_steps=1,\n    max_grad_norm=1.0\n):\n    model.train()\n    \n    running_loss = 0.0\n    total_samples = 0\n    \n    optimizer.zero_grad(set_to_none=True)\n    \n    for step, batch in enumerate(tqdm(train_loader, desc=\"Training\", leave=False)):\n        images = batch[\"image\"].to(DEVICE, non_blocking=True)\n        labels = batch[\"label\"].to(DEVICE, non_blocking=True)\n        \n        with torch.cuda.amp.autocast(enabled=torch.cuda.is_available()):\n            logits = model(images).squeeze(1)\n            loss = criterion(logits, labels)\n            loss = loss / accumulation_steps\n        \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % accumulation_steps == 0:\n            scaler.unscale_(optimizer)\n            \n            if max_grad_norm is not None:\n                torch.nn.utils.clip_grad_norm_(model.parameters(), max_grad_norm)\n            \n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad(set_to_none=True)\n        \n        running_loss += loss.item() * accumulation_steps * images.size(0)\n        total_samples += images.size(0)\n    \n    avg_loss = running_loss / total_samples\n    return avg_loss","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 16: HÀM TRAIN MODEL HOÀN CHỈNH\n# ============================================================\n\ndef train_model(\n    model,\n    model_name,\n    train_loader,\n    val_loader,\n    num_epochs=5,\n    lr=1e-4,\n    weight_decay=1e-4,\n    accumulation_steps=2,\n    patience=3\n):\n    model = model.to(DEVICE)\n    \n    criterion = nn.BCEWithLogitsLoss()\n    \n    optimizer = torch.optim.AdamW(\n        model.parameters(),\n        lr=lr,\n        weight_decay=weight_decay\n    )\n    \n    scaler = torch.cuda.amp.GradScaler(enabled=torch.cuda.is_available())\n    \n    best_auc = -1\n    best_epoch = -1\n    patience_counter = 0\n    \n    history = []\n    \n    best_ckpt_path = CKPT_DIR / f\"{model_name}_best.pth\"\n    \n    for epoch in range(1, num_epochs + 1):\n        print(f\"\\n===== {model_name} | Epoch {epoch}/{num_epochs} =====\")\n        \n        train_loss = train_one_epoch(\n            model=model,\n            train_loader=train_loader,\n            optimizer=optimizer,\n            criterion=criterion,\n            scaler=scaler,\n            accumulation_steps=accumulation_steps\n        )\n        \n        val_loss, val_metrics, _ = evaluate_model(\n            model=model,\n            data_loader=val_loader,\n            criterion=criterion\n        )\n        \n        row = {\n            \"model_name\": model_name,\n            \"epoch\": epoch,\n            \"train_loss\": train_loss,\n            \"val_loss\": val_loss,\n            **{f\"val_{k}\": v for k, v in val_metrics.items()}\n        }\n        history.append(row)\n        \n        print(f\"Train loss: {train_loss:.4f}\")\n        print(f\"Val loss  : {val_loss:.4f}\")\n        print(f\"Val AUC   : {val_metrics['auc']:.4f}\")\n        print(f\"Val F1    : {val_metrics['f1']:.4f}\")\n        print(f\"Val Recall: {val_metrics['recall_pneumonia']:.4f}\")\n        print(f\"Val Spec  : {val_metrics['specificity']:.4f}\")\n        \n        current_auc = val_metrics[\"auc\"]\n        \n        if current_auc > best_auc:\n            best_auc = current_auc\n            best_epoch = epoch\n            patience_counter = 0\n            \n            torch.save({\n                \"model_state_dict\": model.state_dict(),\n                \"model_name\": model_name,\n                \"epoch\": epoch,\n                \"best_auc\": best_auc,\n                \"config\": {\n                    \"lr\": lr,\n                    \"weight_decay\": weight_decay,\n                    \"num_epochs\": num_epochs,\n                    \"accumulation_steps\": accumulation_steps,\n                    \"seed\": SEED,\n                    \"img_size\": IMG_SIZE\n                }\n            }, best_ckpt_path)\n            \n            print(f\"Saved best checkpoint: {best_ckpt_path}\")\n        else:\n            patience_counter += 1\n            print(f\"No improvement. Patience: {patience_counter}/{patience}\")\n            \n            if patience_counter >= patience:\n                print(\"Early stopping triggered.\")\n                break\n    \n    history_df = pd.DataFrame(history)\n    history_path = OUTPUT_DIR / f\"{model_name}_train_history.csv\"\n    history_df.to_csv(history_path, index=False)\n    \n    print(f\"\\nBest {model_name} AUC: {best_auc:.4f} at epoch {best_epoch}\")\n    print(\"History saved:\", history_path)\n    \n    return best_ckpt_path, history_df","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 17: TẠO MODEL RESNET50 IMAGENET BASELINE\n# ============================================================\n\ndef build_resnet50_imagenet():\n    \"\"\"\n    ResNet50 pretrained ImageNet.\n    Thay classifier cuối thành 1 output logit.\n    \"\"\"\n    try:\n        weights = models.ResNet50_Weights.IMAGENET1K_V2\n        model = models.resnet50(weights=weights)\n        print(\"Loaded ResNet50 ImageNet weights: IMAGENET1K_V2\")\n    except Exception as e:\n        print(\"Không tải được pretrained weights. Lỗi:\", e)\n        print(\"Fallback: ResNet50 không pretrained. Kết quả này không còn là ImageNet baseline.\")\n        model = models.resnet50(weights=None)\n    \n    in_features = model.fc.in_features\n    model.fc = nn.Linear(in_features, 1)\n    \n    return model\n\nresnet50_model = build_resnet50_imagenet()\nresnet50_model = resnet50_model.to(DEVICE)\n\nprint(\"ResNet50 model ready.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 18: TRAIN RESNET50 BASELINE\n# ============================================================\n\nRESNET_EPOCHS = 5\nRESNET_LR = 1e-4\nRESNET_ACCUMULATION = 2\n\nresnet50_ckpt_path, resnet50_history = train_model(\n    model=resnet50_model,\n    model_name=\"resnet50_imagenet\",\n    train_loader=train_loader,\n    val_loader=val_loader,\n    num_epochs=RESNET_EPOCHS,\n    lr=RESNET_LR,\n    weight_decay=1e-4,\n    accumulation_steps=RESNET_ACCUMULATION,\n    patience=3\n)\n\ndisplay(resnet50_history)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 19 - FIXED: ĐÁNH GIÁ RESNET50 TRÊN TEST SET\n# Lý do sửa:\n# PyTorch 2.6 đổi mặc định torch.load(weights_only=True),\n# trong khi checkpoint của ta lưu cả dictionary gồm model_state_dict,\n# epoch, best_auc, config... nên cần set weights_only=False.\n# ============================================================\n\n# Tạo lại đúng kiến trúc ResNet50\nresnet50_best = build_resnet50_imagenet()\n\n# Load checkpoint an toàn trong trường hợp PyTorch mới/cũ\ntry:\n    checkpoint = torch.load(\n        resnet50_ckpt_path,\n        map_location=DEVICE,\n        weights_only=False\n    )\nexcept TypeError:\n    # Dành cho PyTorch version cũ chưa hỗ trợ weights_only\n    checkpoint = torch.load(\n        resnet50_ckpt_path,\n        map_location=DEVICE\n    )\n\n# Checkpoint của Cell 16 là dictionary có key \"model_state_dict\"\nif isinstance(checkpoint, dict) and \"model_state_dict\" in checkpoint:\n    resnet50_best.load_state_dict(checkpoint[\"model_state_dict\"])\nelse:\n    # Trường hợp checkpoint chỉ chứa state_dict thuần\n    resnet50_best.load_state_dict(checkpoint)\n\nresnet50_best = resnet50_best.to(DEVICE)\nresnet50_best.eval()\n\ncriterion = nn.BCEWithLogitsLoss()\n\n# Đánh giá trên test set\nresnet_test_loss, resnet_test_metrics, resnet_pred_df = evaluate_model(\n    model=resnet50_best,\n    data_loader=test_loader,\n    criterion=criterion\n)\n\nprint(\"ResNet50 Test Loss:\", resnet_test_loss)\nprint(\"ResNet50 Test Metrics:\")\nfor k, v in resnet_test_metrics.items():\n    print(f\"{k}: {v}\")\n\n# Lưu prediction để dùng cho Notebook 05\nresnet_pred_path = PRED_DIR / \"resnet50_predictions.csv\"\nresnet_pred_df.to_csv(resnet_pred_path, index=False)\n\nprint(\"Saved predictions:\", resnet_pred_path)\n\nprint(\"\\nClassification report:\")\nprint(classification_report(\n    resnet_pred_df[\"label\"].astype(int),\n    resnet_pred_df[\"pred\"].astype(int),\n    target_names=[\"Non-pneumonia\", \"Pneumonia\"],\n    zero_division=0\n))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 20: DỌN BỘ NHỚ SAU RESNET50\n# ============================================================\n\ndel resnet50_model\ndel resnet50_best\n\ngc.collect()\n\nif torch.cuda.is_available():\n    torch.cuda.empty_cache()\n\nprint(\"GPU memory cleared.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 21: IMPORT HOẶC CÀI TIMM\n# ============================================================\n\ntry:\n    import timm\n    print(\"timm imported successfully.\")\nexcept ImportError:\n    print(\"timm chưa có. Đang cài đặt...\")\n    !pip install -q timm\n    import timm\n    print(\"timm installed and imported successfully.\")\n\nprint(\"timm version:\", timm.__version__)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 22: TẠO MODEL VIT-SMALL/16 IMAGENET BASELINE\n# ============================================================\n\ndef build_vit_imagenet():\n    \"\"\"\n    ViT-Small/16 pretrained ImageNet bằng timm.\n    num_classes=1 để dùng BCEWithLogitsLoss.\n    \"\"\"\n    model_name = \"vit_small_patch16_224\"\n    \n    try:\n        model = timm.create_model(\n            model_name,\n            pretrained=True,\n            num_classes=1\n        )\n        print(f\"Loaded {model_name} pretrained ImageNet.\")\n    except Exception as e:\n        print(\"Không tải được pretrained ViT weights. Lỗi:\", e)\n        print(\"Fallback: ViT không pretrained. Kết quả này không còn là ImageNet baseline.\")\n        model = timm.create_model(\n            model_name,\n            pretrained=False,\n            num_classes=1\n        )\n    \n    return model\n\nvit_model = build_vit_imagenet()\nvit_model = vit_model.to(DEVICE)\n\nprint(\"ViT model ready.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 23: TRAIN VIT IMAGENET BASELINE\n# ============================================================\n\nVIT_EPOCHS = 5\nVIT_LR = 5e-5\nVIT_ACCUMULATION = 2\n\nvit_ckpt_path, vit_history = train_model(\n    model=vit_model,\n    model_name=\"vit_imagenet\",\n    train_loader=train_loader,\n    val_loader=val_loader,\n    num_epochs=VIT_EPOCHS,\n    lr=VIT_LR,\n    weight_decay=1e-4,\n    accumulation_steps=VIT_ACCUMULATION,\n    patience=3\n)\n\ndisplay(vit_history)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 24 - FIXED: ĐÁNH GIÁ VIT TRÊN TEST SET\n# Lý do sửa:\n# PyTorch 2.6 đổi mặc định torch.load(weights_only=True).\n# Checkpoint của ta là dictionary nên cần weights_only=False.\n# ============================================================\n\nvit_best = build_vit_imagenet()\n\ntry:\n    checkpoint = torch.load(\n        vit_ckpt_path,\n        map_location=DEVICE,\n        weights_only=False\n    )\nexcept TypeError:\n    checkpoint = torch.load(\n        vit_ckpt_path,\n        map_location=DEVICE\n    )\n\nif isinstance(checkpoint, dict) and \"model_state_dict\" in checkpoint:\n    vit_best.load_state_dict(checkpoint[\"model_state_dict\"])\nelse:\n    vit_best.load_state_dict(checkpoint)\n\nvit_best = vit_best.to(DEVICE)\nvit_best.eval()\n\ncriterion = nn.BCEWithLogitsLoss()\n\nvit_test_loss, vit_test_metrics, vit_pred_df = evaluate_model(\n    model=vit_best,\n    data_loader=test_loader,\n    criterion=criterion\n)\n\nprint(\"ViT Test Loss:\", vit_test_loss)\nprint(\"ViT Test Metrics:\")\nfor k, v in vit_test_metrics.items():\n    print(f\"{k}: {v}\")\n\nvit_pred_path = PRED_DIR / \"vit_predictions.csv\"\nvit_pred_df.to_csv(vit_pred_path, index=False)\n\nprint(\"Saved predictions:\", vit_pred_path)\n\nprint(\"\\nClassification report:\")\nprint(classification_report(\n    vit_pred_df[\"label\"].astype(int),\n    vit_pred_df[\"pred\"].astype(int),\n    target_names=[\"Non-pneumonia\", \"Pneumonia\"],\n    zero_division=0\n))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 25: TẠO BẢNG BASELINE METRICS\n# ============================================================\n\nbaseline_rows = []\n\nbaseline_rows.append({\n    \"model\": \"ResNet50 ImageNet\",\n    \"test_loss\": resnet_test_loss,\n    **resnet_test_metrics\n})\n\nbaseline_rows.append({\n    \"model\": \"ViT-Small/16 ImageNet\",\n    \"test_loss\": vit_test_loss,\n    **vit_test_metrics\n})\n\nbaseline_metrics_df = pd.DataFrame(baseline_rows)\n\nbaseline_metrics_path = OUTPUT_DIR / \"baseline_metrics.csv\"\nbaseline_metrics_df.to_csv(baseline_metrics_path, index=False)\n\ndisplay(baseline_metrics_df)\n\nprint(\"Saved:\", baseline_metrics_path)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 26: VẼ CONFUSION MATRIX\n# ============================================================\n\ndef plot_confusion_matrix_from_df(pred_df, title, save_path):\n    y_true = pred_df[\"label\"].astype(int).values\n    y_pred = pred_df[\"pred\"].astype(int).values\n    \n    cm = confusion_matrix(y_true, y_pred, labels=[0, 1])\n    \n    plt.figure(figsize=(5, 4))\n    plt.imshow(cm)\n    plt.title(title)\n    plt.xlabel(\"Predicted\")\n    plt.ylabel(\"True\")\n    plt.xticks([0, 1], [\"Non-pneumonia\", \"Pneumonia\"], rotation=20)\n    plt.yticks([0, 1], [\"Non-pneumonia\", \"Pneumonia\"])\n    \n    for i in range(2):\n        for j in range(2):\n            plt.text(j, i, cm[i, j], ha=\"center\", va=\"center\")\n    \n    plt.tight_layout()\n    plt.savefig(save_path, dpi=200)\n    plt.show()\n    \n    print(\"Saved:\", save_path)\n\nplot_confusion_matrix_from_df(\n    resnet_pred_df,\n    title=\"ResNet50 ImageNet - Confusion Matrix\",\n    save_path=FIG_DIR / \"resnet50_confusion_matrix.png\"\n)\n\nplot_confusion_matrix_from_df(\n    vit_pred_df,\n    title=\"ViT ImageNet - Confusion Matrix\",\n    save_path=FIG_DIR / \"vit_confusion_matrix.png\"\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 27: VẼ ROC CURVE SO SÁNH BASELINE\n# ============================================================\n\nfrom sklearn.metrics import roc_curve\n\ndef get_roc_data(pred_df):\n    y_true = pred_df[\"label\"].astype(int).values\n    y_prob = pred_df[\"prob_pneumonia\"].values\n    fpr, tpr, thresholds = roc_curve(y_true, y_prob)\n    auc = roc_auc_score(y_true, y_prob)\n    return fpr, tpr, auc\n\nresnet_fpr, resnet_tpr, resnet_auc = get_roc_data(resnet_pred_df)\nvit_fpr, vit_tpr, vit_auc = get_roc_data(vit_pred_df)\n\nplt.figure(figsize=(6, 5))\nplt.plot(resnet_fpr, resnet_tpr, label=f\"ResNet50 AUC={resnet_auc:.4f}\")\nplt.plot(vit_fpr, vit_tpr, label=f\"ViT AUC={vit_auc:.4f}\")\nplt.plot([0, 1], [0, 1], linestyle=\"--\", label=\"Random\")\n\nplt.xlabel(\"False Positive Rate\")\nplt.ylabel(\"True Positive Rate\")\nplt.title(\"Baseline ROC Curve on RSNA Test Set\")\nplt.legend()\nplt.tight_layout()\n\nroc_path = FIG_DIR / \"baseline_roc_curve.png\"\nplt.savefig(roc_path, dpi=200)\nplt.show()\n\nprint(\"Saved:\", roc_path)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 28: NÉN OUTPUT NOTEBOOK 02\n# ============================================================\n\nzip_base = \"/kaggle/working/notebook02_baseline\"\nshutil.make_archive(zip_base, \"zip\", OUTPUT_DIR)\n\nprint(\"Created:\", zip_base + \".zip\")\n\nprint(\"\\nDanh sách file output:\")\nfor p in sorted(OUTPUT_DIR.rglob(\"*\")):\n    if p.is_file():\n        print(\"-\", p.relative_to(OUTPUT_DIR))","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}