{"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":"\"\"\"\nHistopathologic Cancer Detection - Train ResNet50 (transfer learning)\nDataset: /kaggle/input/histopathologic-cancer-detection/\nChạy trực tiếp trong 1 cell của Kaggle Notebook (GPU T4/P100 bật ở Settings > Accelerator).\n\"\"\"\n\nimport os\nimport csv\nimport numpy as np\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\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\nIMG_SIZE = 96          # ảnh gốc đã là 96x96\nBATCH_SIZE = 128\nNUM_EPOCHS = 25\nLEARNING_RATE = 1e-4\nVAL_SPLIT = 0.1\nNUM_WORKERS = 0\nSEED = 42\nEARLY_STOP_PATIENCE = 7   # dừng sớm nếu val_auc không cải thiện sau N epoch liên tiếp\nHISTORY_CSV = \"training_history.csv\"\nBEST_MODEL_PATH = \"resnet50_pcam_best.pth\"\nFINAL_MODEL_PATH = \"resnet50_pcam.pth\"\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Đang dùng device: {device}\")\n\n# ---------------------------------------------------------------------------\n# 2. Load train_labels.csv\n# ---------------------------------------------------------------------------\ndf = pd.read_csv(LABELS_CSV)\nprint(f\"Tổng số ảnh: {len(df)}\")\nprint(df[\"label\"].value_counts(normalize=True))\n\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\n# ---------------------------------------------------------------------------\n# 3. Dataset & Transforms\n# ---------------------------------------------------------------------------\ntrain_transform = transforms.Compose([\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(20),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],  # chuẩn ImageNet\n                          std=[0.229, 0.224, 0.225]),\n])\n\nval_transform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                          std=[0.229, 0.224, 0.225]),\n])\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        img_path = os.path.join(self.img_dir, f\"{row['id']}.tif\")\n        image = Image.open(img_path).convert(\"RGB\")\n        label = torch.tensor(row[\"label\"], dtype=torch.float32)\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, label\n\n\ntrain_dataset = PCamDataset(train_df, TRAIN_IMG_DIR, transform=train_transform)\nval_dataset = PCamDataset(val_df, TRAIN_IMG_DIR, transform=val_transform)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,\n                           num_workers=NUM_WORKERS, pin_memory=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False,\n                         num_workers=NUM_WORKERS, pin_memory=True)\n\n# ---------------------------------------------------------------------------\n# 4. Model: ResNet50 pretrained, thay lớp cuối = 1 output cho binary classification\n# ---------------------------------------------------------------------------\nprint(\"Đang tải model ResNet50 pretrained...\")\nmodel = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)\nprint(\"Đã tải xong model.\")\n\n# Đóng băng phần backbone, chỉ fine-tune vài block cuối + lớp fc (tuỳ chọn, giúp train nhanh hơn)\nfor name, param in model.named_parameters():\n    if \"layer4\" not in name and \"fc\" not in name:\n        param.requires_grad = False\n\nnum_features = model.fc.in_features\nmodel.fc = nn.Linear(num_features, 1)  # 1 output logit\nmodel = model.to(device)\n\n# Lưu ý: dùng BCEWithLogitsLoss thay vì Sigmoid + BCELoss riêng lẻ\n# vì nó ổn định số học hơn (kết hợp sigmoid + binary cross-entropy trong 1 bước).\n# Nếu bạn thực sự cần .sigmoid() tường minh, xem hàm predict_proba() bên dưới.\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.Adam(\n    filter(lambda p: p.requires_grad, model.parameters()), lr=LEARNING_RATE\n)\n# Tự động giảm learning rate khi val_loss không cải thiện, giúp train ổn định hơn khi nhiều epoch\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode=\"min\", factor=0.5, patience=2\n)\n\n# ---------------------------------------------------------------------------\n# 5. Vòng lặp Train / Validate\n# ---------------------------------------------------------------------------\ndef run_epoch(loader, model, criterion, optimizer=None):\n    is_train = optimizer is not None\n    model.train() if is_train else model.eval()\n\n    total_loss, correct, total = 0.0, 0, 0\n    all_probs, all_labels = [], []\n    torch.set_grad_enabled(is_train)\n\n    phase = \"train\" if is_train else \"val\"\n    pbar = tqdm(loader, desc=phase, leave=False)\n\n    for images, labels in pbar:\n        images, labels = images.to(device), labels.to(device).unsqueeze(1)\n\n        if is_train:\n            optimizer.zero_grad()\n\n        logits = model(images)\n        loss = criterion(logits, labels)\n\n        if is_train:\n            loss.backward()\n            optimizer.step()\n\n        probs = torch.sigmoid(logits)\n        preds = (probs > 0.5).float()\n        correct += (preds == labels).sum().item()\n        total += labels.size(0)\n        total_loss += loss.item() * images.size(0)\n\n        all_probs.extend(probs.detach().cpu().numpy().ravel())\n        all_labels.extend(labels.detach().cpu().numpy().ravel())\n\n        pbar.set_postfix(loss=f\"{total_loss/total:.4f}\", acc=f\"{correct/total:.4f}\")\n\n    auc = roc_auc_score(all_labels, all_probs)\n    return total_loss / total, correct / total, auc\n\n\nprint(\"Bắt đầu training...\")\n\n# Tạo file CSV log ngay từ đầu (ghi từng epoch một, để dù bị ngắt giữa chừng\n# vẫn có dữ liệu các epoch đã chạy xong để bạn xem/vẽ biểu đồ)\nwith open(HISTORY_CSV, \"w\", newline=\"\") as f:\n    writer = csv.writer(f)\n    writer.writerow([\"epoch\", \"train_loss\", \"train_acc\", \"train_auc\",\n                      \"val_loss\", \"val_acc\", \"val_auc\", \"lr\"])\n\nbest_val_auc = 0.0\nepochs_no_improve = 0\n\nfor epoch in range(1, NUM_EPOCHS + 1):\n    train_loss, train_acc, train_auc = run_epoch(train_loader, model, criterion, optimizer)\n    val_loss, val_acc, val_auc = run_epoch(val_loader, model, criterion, optimizer=None)\n\n    scheduler.step(val_loss)\n    current_lr = optimizer.param_groups[0][\"lr\"]\n\n    print(\n        f\"Epoch {epoch}/{NUM_EPOCHS} | \"\n        f\"Train loss: {train_loss:.4f}, acc: {train_acc:.4f}, auc: {train_auc:.4f} | \"\n        f\"Val loss: {val_loss:.4f}, acc: {val_acc:.4f}, auc: {val_auc:.4f} | \"\n        f\"lr: {current_lr:.2e}\"\n    )\n\n    # Ghi thêm 1 dòng vào file log ngay sau mỗi epoch\n    with open(HISTORY_CSV, \"a\", newline=\"\") as f:\n        writer = csv.writer(f)\n        writer.writerow([epoch, train_loss, train_acc, train_auc,\n                          val_loss, val_acc, val_auc, current_lr])\n\n    # Lưu lại model tốt nhất theo val_auc (không phải model của epoch cuối)\n    if val_auc > best_val_auc:\n        best_val_auc = val_auc\n        epochs_no_improve = 0\n        torch.save(model.state_dict(), BEST_MODEL_PATH)\n        print(f\"  → val_auc cải thiện ({best_val_auc:.4f}), đã lưu {BEST_MODEL_PATH}\")\n    else:\n        epochs_no_improve += 1\n\n    # Early stopping: dừng sớm nếu không cải thiện sau EARLY_STOP_PATIENCE epoch\n    if epochs_no_improve >= EARLY_STOP_PATIENCE:\n        print(f\"Dừng sớm ở epoch {epoch} vì val_auc không cải thiện \"\n              f\"sau {EARLY_STOP_PATIENCE} epoch liên tiếp.\")\n        break\n\nprint(f\"Training kết thúc. Best val_auc = {best_val_auc:.4f}\")\n\n# ---------------------------------------------------------------------------\n# 6. Lưu model cuối cùng (bên cạnh resnet50_pcam_best.pth đã lưu tự động ở trên)\n# ---------------------------------------------------------------------------\ntorch.save(model.state_dict(), FINAL_MODEL_PATH)\nprint(f\"Đã lưu model epoch cuối vào {FINAL_MODEL_PATH}\")\nprint(f\"Model tốt nhất (theo val_auc) nằm ở {BEST_MODEL_PATH} — nên dùng file này để predict/submit.\")\n\n\n# ---------------------------------------------------------------------------\n# 7. (Tuỳ chọn) Hàm dự đoán xác suất, có gọi sigmoid tường minh\n# ---------------------------------------------------------------------------\ndef predict_proba(model, images):\n    model.eval()\n    with torch.no_grad():\n        logits = model(images.to(device))\n        probs = torch.sigmoid(logits)  # sigmoid tường minh ở bước inference\n    return probs.cpu().numpy()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null}]}