{"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 shutil\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. Cau hinh\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 = 224\nBATCH_SIZE = 64\nNUM_EPOCHS = 40              # train tiep den epoch 40\nSTART_EPOCH_PHASE2 = 21      # epoch bat dau cua Giai doan 2 (sau khi da co epoch 1-20)\nLEARNING_RATE = 5e-5         # giam so voi Giai doan 1 (1e-4) vi la fine-tune tiep tren model da hoi tu\nVAL_SPLIT = 0.1\nNUM_WORKERS = 2\nSEED = 42\nEARLY_STOP_PATIENCE = 6\nHISTORY_CSV = \"/kaggle/working/convnext_training_history.csv\"\nBEST_MODEL_PATH = \"/kaggle/working/convnext_base_pcam_best.pth\"\nFINAL_MODEL_PATH = \"/kaggle/working/convnext_base_pcam.pth\"\nCHECKPOINT_PATH = \"/kaggle/working/convnext_base_pcam_checkpoint.pth\"\n\n# So block CUOI CUNG cua Stage3 (tong 27 block) duoc MO de train; cac block con lai cua Stage3 bi dong bang\n# -> chinh so nay de dieu chinh muc do \"nang do\": tang len = train nhieu hon, giam = dong bang nhieu hon\n\nSTAGE3_TRAINABLE_BLOCKS = 9 #Khanh\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Dang dung device: {device}\")\n\n# ---------------------------------------------------------------------------\n# 1b. Tim va ke thua ket qua Giai doan 1 (epoch 1-20) tu /kaggle/input\n#     CHI ke thua TRONG SO model (best checkpoint) va file log cu.\n#     KHONG ke thua optimizer/scheduler cu, vi tap tham so duoc train da\n#     thay doi (dong bang chi tiet hon theo yeu cau cua thay).\n# ---------------------------------------------------------------------------\nOLD_BEST_MODEL_NAME = \"convnext_base_pcam_best.pth\"\nOLD_HISTORY_NAME = \"convnext_training_history.csv\"\n\nold_best_path = None\nold_history_path = None\nfor root, dirs, files in os.walk(\"/kaggle/input\"):\n    if old_best_path is None and OLD_BEST_MODEL_NAME in files:\n        old_best_path = os.path.join(root, OLD_BEST_MODEL_NAME)\n    if old_history_path is None and OLD_HISTORY_NAME in files:\n        old_history_path = os.path.join(root, OLD_HISTORY_NAME)\n\nif old_best_path is None:\n    raise FileNotFoundError(\n        \"Khong tim thay convnext_base_pcam_best.pth trong /kaggle/input. \"\n        \"Hay chac chan da Add Input chinh notebook ConvNeXt-Base (ban cu, co Version 1) vao notebook nay.\"\n    )\nprint(f\"Se nap trong so Giai doan 1 tu: {old_best_path}\")\n\nif old_history_path and not os.path.exists(HISTORY_CSV):\n    shutil.copy(old_history_path, HISTORY_CSV)\n    print(f\"Da copy log Giai doan 1 tu {old_history_path} -> {HISTORY_CSV} (se noi tiep tu epoch 21)\")\nelif not os.path.exists(HISTORY_CSV):\n    with 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\n# ---------------------------------------------------------------------------\n# 2. Load train_labels.csv, chia Train/Val (giong het Giai doan 1 va ResNet50)\n# ---------------------------------------------------------------------------\ndf = pd.read_csv(LABELS_CSV)\ndf[\"image_path\"] = df[\"id\"].apply(lambda x: os.path.join(TRAIN_IMG_DIR, f\"{x}.tif\"))\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.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(20),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                          std=[0.229, 0.224, 0.225]),\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\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, transform=None):\n        self.df = dataframe\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(row[\"image_path\"]).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(\n    PCamDataset(train_df, transform=train_transform),\n    batch_size=BATCH_SIZE, shuffle=True,\n    num_workers=NUM_WORKERS, pin_memory=True,\n)\nval_loader = DataLoader(\n    PCamDataset(val_df, transform=val_transform),\n    batch_size=BATCH_SIZE, shuffle=False,\n    num_workers=NUM_WORKERS, pin_memory=True,\n)\n\n# ---------------------------------------------------------------------------\n# 4. Model: nap lai kien truc + trong so da train o Giai doan 1,\n#    roi DONG BANG THEO STAGE VA THEO TUNG BLOCK TRONG STAGE (yeu cau cua thay)\n# ---------------------------------------------------------------------------\nmodel = models.convnext_base(weights=None)   # khong tai pretrained ImageNet nua, se nap trong so da fine-tune\nin_features = model.classifier[2].in_features\nmodel.classifier[2] = nn.Linear(in_features, 1)\n\nstate_dict = torch.load(old_best_path, map_location=device)\nmodel.load_state_dict(state_dict)\nprint(\"Da nap trong so tu Giai doan 1 (epoch 1-20, best val_auc=0.9985).\")\n\nmodel = model.to(device)\n\n# model.features gom 8 phan tu:\n#   0: stem (4x4 conv, stride4)        | 1: Stage1 (3 block, dim=128)\n#   2: downsample1                      | 3: Stage2 (3 block, dim=256)\n#   4: downsample2                      | 5: Stage3 (27 block, dim=512)\n#   6: downsample3                      | 7: Stage4 (3 block, dim=1024)\n\n# (a) Dong bang TOAN BO stem + Stage1 + downsample1 + Stage2 + downsample2\nfor i in [0, 1, 2, 3, 4]:\n    for p in model.features[i].parameters():\n        p.requires_grad = False\n\n# (b) Stage3 (27 block): dong bang THEO TUNG BLOCK -- chi mo N block CUOI de train\nstage3_blocks = list(model.features[5].children())\nstage3_total = len(stage3_blocks)   # = 27\nfreeze_count = stage3_total - STAGE3_TRAINABLE_BLOCKS\nfor b_idx, block in enumerate(stage3_blocks):\n    train_this_block = b_idx >= freeze_count\n    for p in block.parameters():\n        p.requires_grad = train_this_block\n\n# (c) downsample3 + Stage4: train toan bo (gan output nhat, dac thu bai toan nhat)\nfor i in [6, 7]:\n    for p in model.features[i].parameters():\n        p.requires_grad = True\n\n# (d) classifier: luon train\nfor p in model.classifier.parameters():\n    p.requires_grad = True\n\ntotal_params = sum(p.numel() for p in model.parameters())\ntrainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\nfrozen_params = total_params - trainable_params\nprint(f\"\\nTong tham so: {total_params:,}\")\nprint(f\"Tham so train duoc: {trainable_params:,} ({trainable_params/total_params*100:.2f}%)\")\nprint(f\"Tham so dong bang: {frozen_params:,} ({frozen_params/total_params*100:.2f}%)\")\n\nprint(\"\\nChi tiet dong bang theo stage (dung de dua vao bao cao):\")\nstage_names = [\"Stem\", \"Stage1 (3 block)\", \"Downsample1\", \"Stage2 (3 block)\",\n               \"Downsample2\", f\"Stage3 (27 block, {STAGE3_TRAINABLE_BLOCKS} block cuoi duoc train)\",\n               \"Downsample3\", \"Stage4 (3 block)\"]\nfor i, name in enumerate(stage_names):\n    p_total = sum(p.numel() for p in model.features[i].parameters())\n    p_train = sum(p.numel() for p in model.features[i].parameters() if p.requires_grad)\n    pct = (p_train / p_total * 100) if p_total > 0 else 0\n    print(f\"  {name:<55}: {p_train:>11,} / {p_total:>11,} train ({pct:5.1f}%)\")\n\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(\n    filter(lambda p: p.requires_grad, model.parameters()),\n    lr=LEARNING_RATE, weight_decay=1e-4,\n)\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode=\"min\", factor=0.5, patience=2\n)\nscaler = torch.amp.GradScaler(\"cuda\", enabled=(device.type == \"cuda\"))\n\n# ---------------------------------------------------------------------------\n# 5. Giai doan 2: optimizer MOI (khong ke thua momentum cu vi tap tham so\n#    train da thay doi). Epoch van danh so tiep 21 -> 40 de log/bieu do lien tuc.\n#    best_val_auc duoc reset vi day la cau hinh train khac voi Giai doan 1.\n# ---------------------------------------------------------------------------\nstart_epoch = START_EPOCH_PHASE2\nbest_val_auc = 0.0\nepochs_no_improve = 0\n\n# ---------------------------------------------------------------------------\n# 6. Vong lap Train / Validate (co AMP) -- giong het Giai doan 1\n# ---------------------------------------------------------------------------\ndef run_epoch(loader, model, criterion, optimizer=None, scaler=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        with torch.autocast(device_type=device.type, enabled=(device.type == \"cuda\")):\n            logits = model(images)\n            loss = criterion(logits, labels)\n\n        if is_train:\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n\n        probs = torch.sigmoid(logits.float())\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(f\"\\nBat dau Giai doan 2: train tu epoch {start_epoch} den epoch {NUM_EPOCHS}...\")\n\nfor epoch in range(start_epoch, NUM_EPOCHS + 1):\n    train_loss, train_acc, train_auc = run_epoch(\n        train_loader, model, criterion, optimizer, scaler\n    )\n    val_loss, val_acc, val_auc = run_epoch(\n        val_loader, model, criterion, optimizer=None\n    )\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    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    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 cai thien ({best_val_auc:.4f}), da luu {BEST_MODEL_PATH}\")\n    else:\n        epochs_no_improve += 1\n\n    torch.save({\n        \"epoch\": epoch,\n        \"model_state_dict\": model.state_dict(),\n        \"optimizer_state_dict\": optimizer.state_dict(),\n        \"scheduler_state_dict\": scheduler.state_dict(),\n        \"best_val_auc\": best_val_auc,\n        \"epochs_no_improve\": epochs_no_improve,\n        \"stage3_trainable_blocks\": STAGE3_TRAINABLE_BLOCKS,\n    }, CHECKPOINT_PATH)\n\n    if epochs_no_improve >= EARLY_STOP_PATIENCE:\n        print(f\"Dung som o epoch {epoch} vi val_auc khong cai thien \"\n              f\"sau {EARLY_STOP_PATIENCE} epoch lien tiep.\")\n        break\n\nprint(f\"\\nGiai doan 2 ket thuc. Best val_auc (Giai doan 2) = {best_val_auc:.4f}\")\n\ntorch.save(model.state_dict(), FINAL_MODEL_PATH)\nprint(f\"Da luu model epoch cuoi vao {FINAL_MODEL_PATH}\")\nprint(f\"Model tot nhat Giai doan 2 (theo val_auc) nam o {BEST_MODEL_PATH}\")\nprint(\"LUU Y: neu val_auc Giai doan 2 KHONG vuot qua 0.9985 (best cua Giai doan 1),\")\nprint(\"      hay so sanh ca 2 file best de chon phien ban tot nhat cho bao cao/submit.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null}]}