{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport csv\nimport time\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models, transforms\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score\nfrom tqdm.auto import tqdm\n\n# ---------------------------------------------------------------------------\n# 1. Cấu hình  --> CHỈ CẦN ĐỔI VERSION (1, 2 hoặc 3) rồi chạy\n# ---------------------------------------------------------------------------\nVERSION = 1\n\n# Đường dẫn checkpoint đã lưu từ lần chạy trước (epoch 43, val_auc 0.9957).\n# Nếu add output của notebook cũ làm Input thì đường dẫn sẽ dạng /kaggle/input/...\n# Hãy mở panel Input bên phải, bấm vào file .pth để copy đúng đường dẫn.\nimport glob\n_found = glob.glob(\"/kaggle/input/**/resnet50_pcam_best.pth\", recursive=True)\nprint(\"Tìm thấy checkpoint:\", _found)\n_prefer = \"/kaggle/input/notebooks/nguyenphucduyy/histopathologic-cancer-detection/resnet50_pcam_best.pth\"\nassert os.path.exists(_prefer) or _found, (\"Chưa thấy resnet50_pcam_best.pth trong /kaggle/input. \"\n                \"Hãy Add Input -> output của notebook cũ (hoặc upload file .pth làm Dataset).\")\nINIT_CKPT = _prefer if os.path.exists(_prefer) else _found[0]\n\nCONFIGS = {\n    # V1: chỉ train lớp fc (linear probe) - nhanh nhất, ổn định nhất\n    1: dict(name=\"v1_fc_only\",      trainable=[\"fc\"],                          lr=1e-4, epochs=20),\n    # V2: train block cuối của layer4 (layer4.2) + fc\n    2: dict(name=\"v2_layer4.2_fc\",  trainable=[\"layer4.2\", \"fc\"],              lr=5e-5, epochs=20),\n    # V3: train 2 block cuối của layer4 (layer4.1 + layer4.2) + fc\n    3: dict(name=\"v3_layer4.1-2_fc\", trainable=[\"layer4.1\", \"layer4.2\", \"fc\"], lr=3e-5, epochs=20),\n}\ncfg = CONFIGS[VERSION]\nTRAINABLE_PREFIXES = cfg[\"trainable\"]\nLEARNING_RATE = cfg[\"lr\"]\nNUM_EPOCHS = cfg[\"epochs\"]\n\nDATA_DIR = \"/kaggle/input/competitions/histopathologic-cancer-detection\"\nTRAIN_IMG_DIR = os.path.join(DATA_DIR, \"train\")\nLABELS_CSV = os.path.join(DATA_DIR, \"train_labels.csv\")\n\nBATCH_SIZE = 128\nVAL_SPLIT = 0.1\nNUM_WORKERS = 2          # đổi từ 0 -> 2 để load ảnh nhanh hơn (nguyên nhân chính khiến epoch chậm)\nSEED = 42                # PHẢI giữ nguyên SEED/VAL_SPLIT để tập val trùng với lần train trước\nEARLY_STOP_PATIENCE = 5\nTIME_LIMIT_SEC = 11 * 3600   # tự dừng trước khi Kaggle timeout 12h\n\nHISTORY_CSV = f\"history_{cfg['name']}.csv\"\nBEST_PATH = f\"resnet50_pcam_{cfg['name']}_best.pth\"\nLAST_PATH = f\"resnet50_pcam_{cfg['name']}_last.pth\"\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Device: {device} | Version {VERSION}: {cfg['name']}\")\nassert device.type == \"cuda\", \"Đang chạy bằng CPU! Bật GPU: Settings -> Accelerator -> GPU rồi chạy lại.\"\n\n# ---------------------------------------------------------------------------\n# 2. Dữ liệu (giữ nguyên split như cũ)\n# ---------------------------------------------------------------------------\ndf = pd.read_csv(LABELS_CSV)\ntrain_df, val_df = train_test_split(\n    df, test_size=VAL_SPLIT, stratify=df[\"label\"], random_state=SEED\n)\ntrain_df = train_df.reset_index(drop=True)\nval_df = val_df.reset_index(drop=True)\nprint(f\"Train: {len(train_df)} | Val: {len(val_df)}\")\n\nnorm = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\ntrain_transform = transforms.Compose([\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(20),\n    transforms.ToTensor(),\n    norm,\n])\nval_transform = transforms.Compose([transforms.ToTensor(), norm])\n\n\nclass PCamDataset(Dataset):\n    def __init__(self, dataframe, img_dir, transform=None):\n        self.df = dataframe\n        self.img_dir = img_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        image = Image.open(os.path.join(self.img_dir, f\"{row['id']}.tif\")).convert(\"RGB\")\n        label = torch.tensor(row[\"label\"], dtype=torch.float32)\n        if self.transform:\n            image = self.transform(image)\n        return image, label\n\n\ntrain_loader = DataLoader(PCamDataset(train_df, TRAIN_IMG_DIR, train_transform),\n                          batch_size=BATCH_SIZE, shuffle=True,\n                          num_workers=NUM_WORKERS, pin_memory=True)\nval_loader = DataLoader(PCamDataset(val_df, TRAIN_IMG_DIR, val_transform),\n                        batch_size=BATCH_SIZE, shuffle=False,\n                        num_workers=NUM_WORKERS, pin_memory=True)\n\n# ---------------------------------------------------------------------------\n# 3. Model: tạo lại kiến trúc -> load checkpoint -> đóng băng theo VERSION\n# ---------------------------------------------------------------------------\nmodel = models.resnet50(weights=None)          # không cần tải lại weight ImageNet\nmodel.fc = nn.Linear(model.fc.in_features, 1)  # phải giống kiến trúc lúc lưu\nmodel.load_state_dict(torch.load(INIT_CKPT, map_location=\"cpu\"))\nmodel = model.to(device)\nprint(f\"Đã load checkpoint: {INIT_CKPT}\")\n\n# Đóng băng tất cả, chỉ mở các phần nằm trong TRAINABLE_PREFIXES\nfor name, param in model.named_parameters():\n    param.requires_grad = any(name.startswith(p) for p in TRAINABLE_PREFIXES)\n\nn_train = sum(p.numel() for p in model.parameters() if p.requires_grad)\nn_total = sum(p.numel() for p in model.parameters())\nprint(f\"Tham số được train: {n_train:,} / {n_total:,} ({100*n_train/n_total:.2f}%)\")\n\n\ndef set_train_mode(model):\n    \"\"\"model.train() nhưng giữ BatchNorm của phần bị đóng băng ở eval(),\n    để running mean/var của phần đóng băng không bị thay đổi.\"\"\"\n    model.train()\n    for name, m in model.named_modules():\n        if isinstance(m, nn.BatchNorm2d) and not any(name.startswith(p) for p in TRAINABLE_PREFIXES):\n            m.eval()\n\n\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.Adam(\n    filter(lambda p: p.requires_grad, model.parameters()), lr=LEARNING_RATE\n)\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode=\"min\", factor=0.5, patience=1\n)\n\n# ---------------------------------------------------------------------------\n# 4. Train / Validate\n# ---------------------------------------------------------------------------\ndef run_epoch(loader, train=False):\n    set_train_mode(model) if train else model.eval()\n    total_loss, correct, total = 0.0, 0, 0\n    all_probs, all_labels = [], []\n\n    with torch.set_grad_enabled(train):\n        for images, labels in tqdm(loader, desc=\"train\" if train else \"val\", leave=False):\n            images, labels = images.to(device), labels.to(device).unsqueeze(1)\n            if train:\n                optimizer.zero_grad()\n            logits = model(images)\n            loss = criterion(logits, labels)\n            if train:\n                loss.backward()\n                optimizer.step()\n\n            probs = torch.sigmoid(logits)\n            correct += ((probs > 0.5).float() == labels).sum().item()\n            total += labels.size(0)\n            total_loss += loss.item() * images.size(0)\n            all_probs.extend(probs.detach().cpu().numpy().ravel())\n            all_labels.extend(labels.detach().cpu().numpy().ravel())\n\n    return total_loss / total, correct / total, roc_auc_score(all_labels, all_probs)\n\n\n# Đánh giá checkpoint ban đầu để làm mốc (chỉ lưu best nếu thật sự tốt hơn)\nbase_loss, base_acc, base_auc = run_epoch(val_loader, train=False)\nprint(f\"Checkpoint ban đầu | Val loss: {base_loss:.4f}, acc: {base_acc:.4f}, auc: {base_auc:.4f}\")\nbest_val_auc = base_auc\n\nwith open(HISTORY_CSV, \"w\", newline=\"\") as f:\n    csv.writer(f).writerow([\"epoch\", \"train_loss\", \"train_acc\", \"train_auc\",\n                            \"val_loss\", \"val_acc\", \"val_auc\", \"lr\"])\n\nstart_time = time.time()\nepochs_no_improve = 0\n\nfor epoch in range(1, NUM_EPOCHS + 1):\n    tr_loss, tr_acc, tr_auc = run_epoch(train_loader, train=True)\n    va_loss, va_acc, va_auc = run_epoch(val_loader, train=False)\n    scheduler.step(va_loss)\n    lr = optimizer.param_groups[0][\"lr\"]\n\n    print(f\"[{cfg['name']}] Epoch {epoch}/{NUM_EPOCHS} | \"\n          f\"Train loss: {tr_loss:.4f}, acc: {tr_acc:.4f}, auc: {tr_auc:.4f} | \"\n          f\"Val loss: {va_loss:.4f}, acc: {va_acc:.4f}, auc: {va_auc:.4f} | lr: {lr:.2e}\")\n\n    with open(HISTORY_CSV, \"a\", newline=\"\") as f:\n        csv.writer(f).writerow([epoch, tr_loss, tr_acc, tr_auc, va_loss, va_acc, va_auc, lr])\n\n    torch.save(model.state_dict(), LAST_PATH)   # luôn lưu epoch mới nhất\n\n    if va_auc > best_val_auc:\n        best_val_auc = va_auc\n        epochs_no_improve = 0\n        torch.save(model.state_dict(), BEST_PATH)\n        print(f\"  → val_auc cải thiện ({best_val_auc:.4f}), đã lưu {BEST_PATH}\")\n    else:\n        epochs_no_improve += 1\n\n    if epochs_no_improve >= EARLY_STOP_PATIENCE:\n        print(f\"Dừng sớm ở epoch {epoch}.\")\n        break\n    if time.time() - start_time > TIME_LIMIT_SEC:\n        print(\"Gần hết thời gian session, dừng để không bị timeout.\")\n        break\n\nprint(f\"Xong version {VERSION}. Best val_auc = {best_val_auc:.4f} (mốc ban đầu: {base_auc:.4f})\")\nif not os.path.exists(BEST_PATH):\n    print(\"Không có epoch nào vượt checkpoint ban đầu -> giữ checkpoint cũ là tốt nhất.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-23T13:20:46.693894Z","iopub.execute_input":"2026-09-23T13:20:46.694199Z","iopub.status.idle":"2026-09-23T13:20:53.281215Z","shell.execute_reply.started":"2026-09-23T13:20:46.694177Z","shell.execute_reply":"2026-09-23T13:20:53.280027Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision import models, transforms\nfrom PIL import Image\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# Khởi tạo đúng kiến trúc đã train\nmodel_infer = models.resnet50(weights=None)\nnum_features = model_infer.fc.in_features\nmodel_infer.fc = nn.Linear(num_features, 1)\n\n# Đường dẫn đúng (chú ý có 2 chữ y: nguyenphucduyy)\nMODEL_PATH = \"/kaggle/input/notebooks/nguyenphucduyy/histopathologic-cancer-detection/resnet50_pcam_best.pth\"\n\nmodel_infer.load_state_dict(torch.load(MODEL_PATH, map_location=device))\nmodel_infer = model_infer.to(device)\nmodel_infer.eval()\n\n# Transform\ninfer_transform = transforms.Compose([\n    transforms.Resize((96, 96)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\ndef predict_single_image(image_path):\n    image = Image.open(image_path).convert(\"RGB\")\n    input_tensor = infer_transform(image).unsqueeze(0).to(device)\n    \n    with torch.no_grad():\n        logit = model_infer(input_tensor)\n        prob = torch.sigmoid(logit).item()\n    \n    label = \"POSITIVE (có mô u)\" if prob >= 0.5 else \"NEGATIVE (không có mô u)\"\n    \n    print(f\"Xác suất có mô u: {prob*100:.2f}%\")\n    print(f\"Kết luận: {label}\")\n    return prob\n\n# Thay đường dẫn ảnh test vào đây\npredict_single_image(\"/kaggle/input/competitions/histopathologic-cancer-detection/test/00006537328c33e284c973d7b39d340809f7271b.tif\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-23T07:37:28.357241Z","iopub.execute_input":"2026-09-23T07:37:28.357719Z","iopub.status.idle":"2026-09-23T07:37:28.887608Z","shell.execute_reply.started":"2026-09-23T07:37:28.357673Z","shell.execute_reply":"2026-09-23T07:37:28.886696Z"}},"outputs":[],"execution_count":null}]}