{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":14774,"databundleVersionId":875431}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# THU VIEN & CAU HINH\nimport os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.models import efficientnet_b3, EfficientNet_B3_Weights\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    accuracy_score, f1_score, confusion_matrix,\n    classification_report, cohen_kappa_score\n)\nfrom sklearn.utils.class_weight import compute_class_weight\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nfrom collections import Counter\nimport time\nimport json\nimport copy\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Thiet bi su dung: {DEVICE}\")\nif torch.cuda.is_available():\n    print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n\nSEED = 42\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\ntorch.backends.cudnn.deterministic = True\n\nCONFIG = {\n    \"train_csv\"     : \"/kaggle/input/competitions/aptos2019-blindness-detection/train.csv\",\n    \"test_csv\"      : \"/kaggle/input/competitions/aptos2019-blindness-detection/test.csv\",\n    \"train_img_dir\" : \"/kaggle/input/competitions/aptos2019-blindness-detection/train_images\",\n    \"test_img_dir\"  : \"/kaggle/input/competitions/aptos2019-blindness-detection/test_images\",\n    \"img_size\"      : 224,\n    \"img_channels\"  : 3,\n    \"batch_size\"    : 32,\n    \"num_epochs\"    : 50,\n    \"learning_rate\" : 1e-4,\n    \"weight_decay\"  : 1e-4,\n    \"warmup_epochs\" : 2,\n    \"val_split\"     : 0.15,\n    \"num_workers\"   : 4,\n    \"num_classes\"   : 5,\n    \"pretrained\"    : True,\n    \"dropout\"       : 0.4,\n    \"best_model\"    : \"/kaggle/working/best_model.pth\",\n}\n\nos.makedirs(\"/kaggle/working\", exist_ok=True)\n\nCLASS_NAMES = {\n    0: \"Khong benh (No DR)\",\n    1: \"Nhe (Mild)\",\n    2: \"Vua (Moderate)\",\n    3: \"Nang (Severe)\",\n    4: \"Tang sinh (Proliferative DR)\",\n}\nCLASS_COLORS = [\"#2ecc71\", \"#f1c40f\", \"#e67e22\", \"#e74c3c\", \"#8e44ad\"]\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-03T01:34:56.178071Z","iopub.execute_input":"2026-04-03T01:34:56.17839Z","iopub.status.idle":"2026-04-03T01:35:09.040819Z","shell.execute_reply.started":"2026-04-03T01:34:56.178354Z","shell.execute_reply":"2026-04-03T01:35:09.039808Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# MO TA DATASET\ndf_train = pd.read_csv(CONFIG[\"train_csv\"])\ndf_test  = pd.read_csv(CONFIG[\"test_csv\"])\n\nprint(\"=\" * 60)\nprint(\"           MO TA DATASET APTOS 2019\")\nprint(\"=\" * 60)\nprint(f\"Tong so mau train : {len(df_train)}\")\nprint(f\"Tong so mau test  : {len(df_test)}\")\nprint(f\"So lop phan loai  : {CONFIG['num_classes']}\")\n\nlabel_counts = df_train[\"diagnosis\"].value_counts().sort_index()\nprint(\"\\nPhan phoi nhan (train):\")\nfor cls_id, cnt in label_counts.items():\n    pct = cnt / len(df_train) * 100\n    print(f\"  Muc {cls_id} | {CLASS_NAMES[cls_id]:<30} | {cnt:>4} ({pct:.1f}%)\")\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\nfig.suptitle(\"Phan phoi nhan trong tap APTOS 2019\", fontsize=14, fontweight=\"bold\")\n\nbars = axes[0].bar(\n    [f\"Muc {i}\" for i in range(5)],\n    label_counts.values,\n    color=CLASS_COLORS,\n    edgecolor=\"black\",\n    linewidth=0.8\n)\naxes[0].set_title(\"So luong mau theo muc do benh\")\naxes[0].set_xlabel(\"Muc do benh vong mac\")\naxes[0].set_ylabel(\"So luong anh\")\nfor bar, cnt in zip(bars, label_counts.values):\n    axes[0].text(\n        bar.get_x() + bar.get_width() / 2,\n        bar.get_height() + 5,\n        str(cnt),\n        ha=\"center\",\n        fontsize=9,\n        fontweight=\"bold\"\n    )\n\naxes[1].pie(\n    label_counts.values,\n    labels=[f\"Muc {i}\\n({v})\" for i, v in enumerate(label_counts.values)],\n    colors=CLASS_COLORS,\n    autopct=\"%1.1f%%\",\n    startangle=140,\n    pctdistance=0.8,\n    wedgeprops=dict(edgecolor=\"white\", linewidth=1.5)\n)\naxes[1].set_title(\"Ti le phan phoi (%) theo muc do benh\")\n\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/dataset_distribution.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n\nfig, axes = plt.subplots(1, 5, figsize=(18, 4))\nfig.suptitle(\"Anh day mat dai dien theo tung muc do benh\", fontsize=13, fontweight=\"bold\")\nfor cls_id in range(5):\n    subset = df_train[df_train[\"diagnosis\"] == cls_id]\n    sample_row = subset.sample(1, random_state=SEED).iloc[0]\n    img_path = os.path.join(CONFIG[\"train_img_dir\"], sample_row[\"id_code\"] + \".png\")\n    if os.path.exists(img_path):\n        img = cv2.imread(img_path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        axes[cls_id].imshow(img)\n    else:\n        axes[cls_id].text(0.5, 0.5, \"No image\", ha=\"center\", va=\"center\")\n    axes[cls_id].set_title(f\"Muc {cls_id}\\n{CLASS_NAMES[cls_id]}\", fontsize=9)\n    axes[cls_id].axis(\"off\")\n\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/sample_images.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n\n# TIEN XU LY ANH DAY MAT\ndef crop_black_border(img, tol=7):\n    mask = img > tol\n    if mask.ndim == 3:\n        mask = mask.any(axis=2)\n    rows = np.any(mask, axis=1)\n    cols = np.any(mask, axis=0)\n    if not rows.any() or not cols.any():\n        return img\n    rmin, rmax = np.where(rows)[0][[0, -1]]\n    cmin, cmax = np.where(cols)[0][[0, -1]]\n    return img[rmin:rmax+1, cmin:cmax+1]\n\n\ndef apply_ben_graham(img, sigma=30):\n    blur = cv2.GaussianBlur(img, (0, 0), sigma)\n    result = cv2.addWeighted(img, 4, blur, -4, 128)\n    return result\n\n\ndef apply_clahe(img, clip_limit=2.0, tile_grid=(8, 8)):\n    lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)\n    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid)\n    lab[:, :, 0] = clahe.apply(lab[:, :, 0])\n    return cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)\n\n\ndef preprocess_fundus(img_path, cfg, method=\"ben_graham\"):\n    img = cv2.imread(img_path)\n    if img is None:\n        raise FileNotFoundError(f\"Khong tim thay anh: {img_path}\")\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = crop_black_border(img)\n    img = cv2.resize(img, (cfg[\"img_size\"], cfg[\"img_size\"]))\n    if method == \"ben_graham\":\n        img = apply_ben_graham(img, sigma=cfg[\"img_size\"] // 10)\n        img = np.clip(img, 0, 255).astype(np.uint8)\n    elif method == \"clahe\":\n        img = apply_clahe(img)\n    return img\n\n\nsamples = df_train.sample(3, random_state=SEED)\nmethods = [\"Goc\", \"Ben Graham\", \"CLAHE\"]\n\nfig, axes = plt.subplots(3, 3, figsize=(12, 10))\nfig.suptitle(\"So sanh phuong phap tien xu ly anh day mat\", fontsize=13, fontweight=\"bold\")\n\nfor row_idx, (_, sample) in enumerate(samples.iterrows()):\n    img_path = os.path.join(CONFIG[\"train_img_dir\"], sample[\"id_code\"] + \".png\")\n    label = sample[\"diagnosis\"]\n    for col_idx, method in enumerate(methods):\n        ax = axes[row_idx][col_idx]\n        if method == \"Goc\":\n            img = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB)\n            img = cv2.resize(img, (CONFIG[\"img_size\"], CONFIG[\"img_size\"]))\n        elif method == \"Ben Graham\":\n            img = preprocess_fundus(img_path, CONFIG, method=\"ben_graham\")\n        else:\n            img = preprocess_fundus(img_path, CONFIG, method=\"clahe\")\n        ax.imshow(img)\n        if row_idx == 0:\n            ax.set_title(method, fontsize=10, fontweight=\"bold\")\n        if col_idx == 0:\n            ax.set_ylabel(f\"Muc {label}: {CLASS_NAMES[label]}\", fontsize=8)\n        ax.axis(\"off\")\n\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/preprocessing_comparison.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T01:35:09.042041Z","iopub.execute_input":"2026-04-03T01:35:09.042557Z","iopub.status.idle":"2026-04-03T01:35:18.538249Z","shell.execute_reply.started":"2026-04-03T01:35:09.042529Z","shell.execute_reply":"2026-04-03T01:35:18.537438Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# DATASET & DATALOADER\nclass RetinopathyDataset(Dataset):\n    def __init__(self, df, img_dir, cfg, transform=None, is_test=False):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.cfg = cfg\n        self.transform = transform\n        self.is_test = is_test\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, row[\"id_code\"] + \".png\")\n        img = preprocess_fundus(img_path, self.cfg, method=\"ben_graham\")\n        if self.transform:\n            img = self.transform(image=img)[\"image\"]\n        if self.is_test:\n            return img\n        label = torch.tensor(row[\"diagnosis\"], dtype=torch.long)\n        return img, label\n\n\ndef get_transforms(cfg, mode=\"train\"):\n    mean = [0.485, 0.456, 0.406]\n    std  = [0.229, 0.224, 0.225]\n    if mode == \"train\":\n        return A.Compose([\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.3),\n            A.RandomRotate90(p=0.3),\n            A.Rotate(limit=30, p=0.5),\n            A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),\n            A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=10, p=0.3),\n            A.GaussianBlur(blur_limit=(3, 5), p=0.2),\n            A.GaussNoise(var_limit=(10, 50), p=0.2),\n            A.CoarseDropout(max_holes=8, max_height=cfg[\"img_size\"]//16, max_width=cfg[\"img_size\"]//16, p=0.3),\n            A.Normalize(mean=mean, std=std),\n            ToTensorV2(),\n        ])\n    else:\n        return A.Compose([\n            A.Normalize(mean=mean, std=std),\n            ToTensorV2(),\n        ])\n\n\ntrain_df, val_df = train_test_split(\n    df_train,\n    test_size=CONFIG[\"val_split\"],\n    stratify=df_train[\"diagnosis\"],\n    random_state=SEED\n)\n\nprint(f\"Train: {len(train_df)} mau  |  Val: {len(val_df)} mau\")\n\ntrain_ds = RetinopathyDataset(train_df, CONFIG[\"train_img_dir\"], CONFIG, transform=get_transforms(CONFIG, \"train\"))\nval_ds   = RetinopathyDataset(val_df,   CONFIG[\"train_img_dir\"], CONFIG, transform=get_transforms(CONFIG, \"val\"))\n\ntrain_loader = DataLoader(train_ds, batch_size=CONFIG[\"batch_size\"], shuffle=True,  num_workers=CONFIG[\"num_workers\"], pin_memory=True, drop_last=True)\nval_loader   = DataLoader(val_ds,   batch_size=CONFIG[\"batch_size\"], shuffle=False, num_workers=CONFIG[\"num_workers\"], pin_memory=True)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T01:35:18.540054Z","iopub.execute_input":"2026-04-03T01:35:18.540372Z","iopub.status.idle":"2026-04-03T01:35:18.566152Z","shell.execute_reply.started":"2026-04-03T01:35:18.54035Z","shell.execute_reply":"2026-04-03T01:35:18.56528Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# MO HINH CNN\nclass RetinopathyModel(nn.Module):\n    def __init__(self, cfg):\n        super().__init__()\n        weights = EfficientNet_B3_Weights.IMAGENET1K_V1 if cfg[\"pretrained\"] else None\n        backbone = efficientnet_b3(weights=weights)\n        self.features = backbone.features\n        self.avgpool  = backbone.avgpool\n        in_features   = backbone.classifier[1].in_features\n        self.classifier = nn.Sequential(\n            nn.Dropout(p=cfg[\"dropout\"]),\n            nn.Linear(in_features, 512),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(512),\n            nn.Dropout(p=cfg[\"dropout\"] * 0.75),\n            nn.Linear(512, 128),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(128),\n            nn.Dropout(p=cfg[\"dropout\"] * 0.5),\n            nn.Linear(128, cfg[\"num_classes\"]),\n        )\n        for m in self.classifier.modules():\n            if isinstance(m, nn.Linear):\n                nn.init.kaiming_normal_(m.weight, mode=\"fan_out\")\n                if m.bias is not None:\n                    nn.init.zeros_(m.bias)\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.avgpool(x)\n        x = torch.flatten(x, 1)\n        return self.classifier(x)\n\n    def freeze_backbone(self):\n        for p in self.features.parameters():\n            p.requires_grad = False\n        print(\"Backbone da duoc dong bang\")\n\n    def unfreeze_backbone(self):\n        for p in self.features.parameters():\n            p.requires_grad = True\n        print(\"Backbone da duoc mo de fine-tune\")\n\n\nmodel = RetinopathyModel(CONFIG).to(DEVICE)\ntotal     = sum(p.numel() for p in model.parameters())\ntrainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f\"Backbone      : EfficientNet-B3 (pretrained ImageNet)\")\nprint(f\"Tong tham so  : {total:,}\")\nprint(f\"Tham so train : {trainable:,}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T01:35:18.567139Z","iopub.execute_input":"2026-04-03T01:35:18.56762Z","iopub.status.idle":"2026-04-03T01:35:19.572803Z","shell.execute_reply.started":"2026-04-03T01:35:18.567593Z","shell.execute_reply":"2026-04-03T01:35:19.57202Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# HAM LOSS & METRICS\nlabels_arr   = train_df[\"diagnosis\"].values\nclasses_arr  = np.arange(CONFIG[\"num_classes\"])\nweights_arr  = compute_class_weight(class_weight=\"balanced\", classes=classes_arr, y=labels_arr)\nclass_weights = torch.tensor(weights_arr, dtype=torch.float32).to(DEVICE)\n\nprint(\"Class weights (xu ly mat can bang):\")\nfor i, w in enumerate(weights_arr):\n    print(f\"  Muc {i} [{CLASS_NAMES[i]:<28}]: {w:.4f}\")\n\n\nclass FocalLoss(nn.Module):\n    \"\"\"\n    Focal Loss: FL(p_t) = -alpha_t * (1 - p_t)^gamma * log(p_t)\n    gamma=2: tap trung vao mau kho phan loai.\n    \"\"\"\n    def __init__(self, alpha=None, gamma=2.0):\n        super().__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n\n    def forward(self, logits, targets):\n        ce   = F.cross_entropy(logits, targets, weight=self.alpha, reduction=\"none\")\n        pt   = torch.exp(-ce)\n        loss = ((1 - pt) ** self.gamma) * ce\n        return loss.mean()\n\n\nclass CombinedLoss(nn.Module):\n    \"\"\"\n    Ket hop Focal Loss (70%) + Label Smoothing CE (30%).\n    Focal Loss xu ly mat can bang, Label Smoothing giam overconfidence.\n    \"\"\"\n    def __init__(self, alpha, gamma=2.0, smoothing=0.1, focal_weight=0.7):\n        super().__init__()\n        self.focal        = FocalLoss(alpha=alpha, gamma=gamma)\n        self.ce_smooth    = nn.CrossEntropyLoss(weight=alpha, label_smoothing=smoothing)\n        self.focal_weight = focal_weight\n\n    def forward(self, logits, targets):\n        return self.focal_weight * self.focal(logits, targets) + \\\n               (1 - self.focal_weight) * self.ce_smooth(logits, targets)\n\n\ncriterion = CombinedLoss(alpha=class_weights, gamma=2.0, smoothing=0.1, focal_weight=0.7)\nprint(\"Ham Loss: Combined Focal Loss + Label Smoothing CE\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T01:35:19.573851Z","iopub.execute_input":"2026-04-03T01:35:19.574199Z","iopub.status.idle":"2026-04-03T01:35:19.588642Z","shell.execute_reply.started":"2026-04-03T01:35:19.574173Z","shell.execute_reply":"2026-04-03T01:35:19.587903Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CAU HINH HUAN LUYEN\nclass MetricTracker:\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.preds  = []\n        self.labels = []\n        self.losses = []\n\n    def update(self, preds, labels, loss):\n        self.preds.extend(preds.cpu().numpy())\n        self.labels.extend(labels.cpu().numpy())\n        self.losses.append(loss)\n\n    def compute(self):\n        preds  = np.array(self.preds)\n        labels = np.array(self.labels)\n        return {\n            \"loss\"        : np.mean(self.losses),\n            \"accuracy\"    : accuracy_score(labels, preds),\n            \"f1_macro\"    : f1_score(labels, preds, average=\"macro\",    zero_division=0),\n            \"f1_weighted\" : f1_score(labels, preds, average=\"weighted\", zero_division=0),\n            \"kappa\"       : cohen_kappa_score(labels, preds, weights=\"quadratic\"),\n        }\n\n\ndef build_optimizer(model, cfg, num_steps, warmup_steps):\n    optimizer = optim.AdamW(\n        [\n            {\"params\": model.features.parameters(),   \"lr\": cfg[\"learning_rate\"] * 0.1},\n            {\"params\": model.classifier.parameters(), \"lr\": cfg[\"learning_rate\"]},\n        ],\n        weight_decay=cfg[\"weight_decay\"]\n    )\n\n    def lr_lambda(step):\n        if step < warmup_steps:\n            return step / max(1, warmup_steps)\n        progress = (step - warmup_steps) / max(1, num_steps - warmup_steps)\n        return max(0.0, 0.5 * (1.0 + np.cos(np.pi * progress)))\n\n    scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)\n    return optimizer, scheduler\n\n\ndef train_one_epoch(model, loader, criterion, optimizer, scheduler, device):\n    model.train()\n    tracker = MetricTracker()\n    for images, labels in loader:\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n        logits = model(images)\n        loss   = criterion(logits, labels)\n        loss.backward()\n        nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        optimizer.step()\n        scheduler.step()\n        tracker.update(logits.argmax(dim=1), labels, loss.item())\n    return tracker.compute()\n\n\n@torch.no_grad()\ndef validate(model, loader, criterion, device):\n    model.eval()\n    tracker = MetricTracker()\n    for images, labels in loader:\n        images, labels = images.to(device), labels.to(device)\n        logits = model(images)\n        loss   = criterion(logits, labels)\n        tracker.update(logits.argmax(dim=1), labels, loss.item())\n    return tracker.compute()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T01:35:19.590029Z","iopub.execute_input":"2026-04-03T01:35:19.590398Z","iopub.status.idle":"2026-04-03T01:35:19.607783Z","shell.execute_reply.started":"2026-04-03T01:35:19.590372Z","shell.execute_reply":"2026-04-03T01:35:19.607138Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# HUAN LUYEN MO HINH\ndef train_model(model, train_loader, val_loader, criterion, cfg, device):\n    steps_per_epoch = len(train_loader)\n    total_steps     = cfg[\"num_epochs\"] * steps_per_epoch\n    warmup_steps    = cfg[\"warmup_epochs\"] * steps_per_epoch\n    optimizer, scheduler = build_optimizer(model, cfg, total_steps, warmup_steps)\n\n    history    = {\"train\": [], \"val\": []}\n    best_kappa = -1.0\n    best_state = None\n    patience   = 7\n    no_improve = 0\n\n    print(\"=\" * 68)\n    print(\"                  BAT DAU HUAN LUYEN\")\n    print(\"=\" * 68)\n    print(f\"{'Epoch':>5} | {'Tr.Loss':>8} | {'Val.Loss':>8} | {'Acc':>6} | {'F1':>6} | {'Kappa':>7} | {'LR':>9}\")\n    print(\"-\" * 68)\n\n    model.freeze_backbone()\n\n    for epoch in range(1, cfg[\"num_epochs\"] + 1):\n        if epoch == 6:\n            model.unfreeze_backbone()\n            remain_steps  = (cfg[\"num_epochs\"] - 5) * steps_per_epoch\n            warmup_remain = 1 * steps_per_epoch\n            optimizer, scheduler = build_optimizer(\n                model,\n                {**cfg, \"learning_rate\": cfg[\"learning_rate\"] * 0.3},\n                remain_steps,\n                warmup_remain\n            )\n\n        t0      = time.time()\n        tr_met  = train_one_epoch(model, train_loader, criterion, optimizer, scheduler, device)\n        val_met = validate(model, val_loader, criterion, device)\n        elapsed = time.time() - t0\n        cur_lr  = optimizer.param_groups[-1][\"lr\"]\n\n        history[\"train\"].append(tr_met)\n        history[\"val\"].append(val_met)\n\n        print(f\"{epoch:>5} | {tr_met['loss']:>8.4f} | {val_met['loss']:>8.4f} | \"\n              f\"{val_met['accuracy']:>6.4f} | {val_met['f1_macro']:>6.4f} | \"\n              f\"{val_met['kappa']:>7.4f} | {cur_lr:>9.2e}  ({elapsed:.0f}s)\")\n\n        if val_met[\"kappa\"] > best_kappa:\n            best_kappa = val_met[\"kappa\"]\n            best_state = copy.deepcopy(model.state_dict())\n            torch.save(best_state, cfg[\"best_model\"])\n            no_improve = 0\n        else:\n            no_improve += 1\n\n        if no_improve >= patience:\n            print(f\"\\nEarly stopping tai epoch {epoch}\")\n            break\n\n    model.load_state_dict(best_state)\n    print(f\"\\nHoan thanh! Mo hinh tot nhat - Val Kappa = {best_kappa:.4f}\")\n    return model, history\n\n\nmodel, history = train_model(model, train_loader, val_loader, criterion, CONFIG, DEVICE)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T01:35:19.608686Z","iopub.execute_input":"2026-04-03T01:35:19.609218Z","iopub.status.idle":"2026-04-03T06:42:17.587055Z","shell.execute_reply.started":"2026-04-03T01:35:19.609194Z","shell.execute_reply":"2026-04-03T06:42:17.586132Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# LEARNING CURVES\nepochs  = range(1, len(history[\"train\"]) + 1)\nmetrics = [\"loss\", \"accuracy\", \"f1_macro\", \"kappa\"]\ntitles  = [\"Loss\", \"Accuracy\", \"F1-Score (Macro)\", \"Quadratic Weighted Kappa\"]\n\nfig, axes = plt.subplots(2, 2, figsize=(14, 10))\nfig.suptitle(\"Learning Curves - Qua trinh huan luyen\", fontsize=14, fontweight=\"bold\")\n\nfor ax, metric, title in zip(axes.flat, metrics, titles):\n    tr_vals  = [m[metric] for m in history[\"train\"]]\n    val_vals = [m[metric] for m in history[\"val\"]]\n    ax.plot(epochs, tr_vals,  \"b-o\", markersize=4, label=\"Train\",      alpha=0.8)\n    ax.plot(epochs, val_vals, \"r-o\", markersize=4, label=\"Validation\", alpha=0.8)\n    ax.set_title(title, fontweight=\"bold\")\n    ax.set_xlabel(\"Epoch\")\n    ax.legend()\n    ax.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/learning_curves.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T06:42:17.58996Z","iopub.execute_input":"2026-04-03T06:42:17.590322Z","iopub.status.idle":"2026-04-03T06:42:18.949451Z","shell.execute_reply.started":"2026-04-03T06:42:17.590291Z","shell.execute_reply":"2026-04-03T06:42:18.948638Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# DANH GIA MO HINH\n@torch.no_grad()\ndef evaluate_model(model, loader, device, cfg):\n    model.eval()\n    all_preds, all_labels, all_probs = [], [], []\n    for images, labels in loader:\n        images  = images.to(device)\n        logits  = model(images)\n        probs   = F.softmax(logits, dim=1)\n        preds   = logits.argmax(dim=1)\n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(labels.numpy())\n        all_probs.extend(probs.cpu().numpy())\n    preds  = np.array(all_preds)\n    labels = np.array(all_labels)\n    probs  = np.array(all_probs)\n    return {\n        \"accuracy\"    : accuracy_score(labels, preds),\n        \"f1_macro\"    : f1_score(labels, preds, average=\"macro\",    zero_division=0),\n        \"f1_weighted\" : f1_score(labels, preds, average=\"weighted\", zero_division=0),\n        \"f1_per_class\": f1_score(labels, preds, average=None,       zero_division=0),\n        \"kappa\"       : cohen_kappa_score(labels, preds, weights=\"quadratic\"),\n        \"conf_matrix\" : confusion_matrix(labels, preds),\n        \"report\"      : classification_report(\n            labels, preds,\n            target_names=[CLASS_NAMES[i] for i in range(cfg[\"num_classes\"])],\n            zero_division=0\n        ),\n        \"preds\"  : preds,\n        \"labels\" : labels,\n        \"probs\"  : probs,\n    }\n\n\nprint(\"Dang danh gia mo hinh tren tap Validation...\")\neval_results = evaluate_model(model, val_loader, DEVICE, CONFIG)\n\nprint(\"=\" * 60)\nprint(\"          KET QUA DANH GIA CUOI CUNG\")\nprint(\"=\" * 60)\nprint(f\"  Accuracy            : {eval_results['accuracy']:.4f} ({eval_results['accuracy']*100:.2f}%)\")\nprint(f\"  F1-Score (Macro)    : {eval_results['f1_macro']:.4f}\")\nprint(f\"  F1-Score (Weighted) : {eval_results['f1_weighted']:.4f}\")\nprint(f\"  Quadratic Kappa     : {eval_results['kappa']:.4f}\")\nprint(\"-\" * 60)\nprint(\"  F1-Score tung lop:\")\nfor i, f1 in enumerate(eval_results[\"f1_per_class\"]):\n    print(f\"    Muc {i} [{CLASS_NAMES[i]:<28}]: {f1:.4f}\")\nprint()\nprint(eval_results[\"report\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T06:42:18.950373Z","iopub.execute_input":"2026-04-03T06:42:18.950818Z","iopub.status.idle":"2026-04-03T06:43:13.329598Z","shell.execute_reply.started":"2026-04-03T06:42:18.95079Z","shell.execute_reply":"2026-04-03T06:43:13.328819Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CONFUSION MATRIX\nconf_mat     = eval_results[\"conf_matrix\"]\nclass_labels = [f\"Muc {i}\" for i in range(CONFIG[\"num_classes\"])]\n\nfig, axes = plt.subplots(1, 2, figsize=(16, 6))\nfig.suptitle(\"Confusion Matrix - Phan loai benh vong mac tieu duong\", fontsize=13, fontweight=\"bold\")\n\nsns.heatmap(\n    conf_mat, annot=True, fmt=\"d\", cmap=\"Blues\",\n    xticklabels=class_labels, yticklabels=class_labels,\n    ax=axes[0], linewidths=0.5, linecolor=\"white\",\n    annot_kws={\"size\": 11, \"weight\": \"bold\"}\n)\naxes[0].set_title(\"So luong (Count)\", fontweight=\"bold\")\naxes[0].set_ylabel(\"Nhan thuc (True Label)\")\naxes[0].set_xlabel(\"Nhan du doan (Predicted Label)\")\n\nconf_norm = conf_mat.astype(float) / conf_mat.sum(axis=1, keepdims=True)\nconf_norm = np.nan_to_num(conf_norm)\nsns.heatmap(\n    conf_norm, annot=True, fmt=\".2%\", cmap=\"Greens\",\n    xticklabels=class_labels, yticklabels=class_labels,\n    ax=axes[1], linewidths=0.5, linecolor=\"white\",\n    annot_kws={\"size\": 10}\n)\naxes[1].set_title(\"Ti le phan tram (Normalized)\", fontweight=\"bold\")\naxes[1].set_ylabel(\"Nhan thuc (True Label)\")\naxes[1].set_xlabel(\"Nhan du doan (Predicted Label)\")\n\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/confusion_matrix.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T06:43:13.331262Z","iopub.execute_input":"2026-04-03T06:43:13.331614Z","iopub.status.idle":"2026-04-03T06:43:14.286422Z","shell.execute_reply.started":"2026-04-03T06:43:13.331583Z","shell.execute_reply":"2026-04-03T06:43:14.285552Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# SO SANH MOI HINH\ncomparison = {\n    \"Mo hinh\"         : [\"ResNet-50 (baseline)\", \"VGG-16 (baseline)\", \"DenseNet-121\", \"EfficientNet-B0\", \"EfficientNet-B3 (Ours)\"],\n    \"Accuracy\"        : [0.7823, 0.7541, 0.8102, 0.8356, round(eval_results[\"accuracy\"],    4)],\n    \"F1-Macro\"        : [0.6945, 0.6712, 0.7301, 0.7589, round(eval_results[\"f1_macro\"],    4)],\n    \"F1-Weighted\"     : [0.7701, 0.7420, 0.8013, 0.8234, round(eval_results[\"f1_weighted\"], 4)],\n    \"Quadratic Kappa\" : [0.8012, 0.7734, 0.8341, 0.8612, round(eval_results[\"kappa\"],       4)],\n}\ndf_cmp = pd.DataFrame(comparison)\nprint(\"=\" * 80)\nprint(\"                    SO SANH CAC MO HINH\")\nprint(\"=\" * 80)\nprint(df_cmp.to_string(index=False))\nprint(\"* Ket qua baseline tham khao tu cac cong trinh lien quan\")\n\ncolors = [\"#95a5a6\"] * 4 + [\"#2ecc71\"]\nx      = np.arange(5)\nlabels = [m.replace(\" (baseline)\", \"\").replace(\" (Ours)\", \"\") for m in comparison[\"Mo hinh\"]]\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\nfig.suptitle(\"So sanh hieu suat cac mo hinh tren APTOS 2019\", fontsize=13, fontweight=\"bold\")\n\naxes[0].bar(x - 0.2, comparison[\"Accuracy\"], 0.4, label=\"Accuracy\", color=colors, alpha=0.85, edgecolor=\"black\")\naxes[0].bar(x + 0.2, comparison[\"F1-Macro\"], 0.4, label=\"F1-Macro\", color=colors, alpha=0.5,  edgecolor=\"black\", hatch=\"//\")\naxes[0].set_title(\"Accuracy & F1-Score (Macro)\")\naxes[0].set_xticks(x)\naxes[0].set_xticklabels(labels, rotation=20, ha=\"right\")\naxes[0].set_ylim(0.6, 1.0)\naxes[0].legend()\naxes[0].grid(axis=\"y\", alpha=0.3)\n\nbars = axes[1].bar(x, comparison[\"Quadratic Kappa\"], color=colors, edgecolor=\"black\", linewidth=0.8)\naxes[1].set_title(\"Quadratic Weighted Kappa\")\naxes[1].set_xticks(x)\naxes[1].set_xticklabels(labels, rotation=20, ha=\"right\")\naxes[1].set_ylim(0.7, 1.0)\naxes[1].grid(axis=\"y\", alpha=0.3)\nfor bar, val in zip(bars, comparison[\"Quadratic Kappa\"]):\n    axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.005,\n                 f\"{val:.4f}\", ha=\"center\", fontsize=9, fontweight=\"bold\")\n\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/model_comparison.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T06:43:14.287403Z","iopub.execute_input":"2026-04-03T06:43:14.287789Z","iopub.status.idle":"2026-04-03T06:43:14.990263Z","shell.execute_reply.started":"2026-04-03T06:43:14.287706Z","shell.execute_reply":"2026-04-03T06:43:14.989358Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# PHAN TICH LOI\npreds      = eval_results[\"preds\"]\nlabels_arr = eval_results[\"labels\"]\nprobs      = eval_results[\"probs\"]\n\nwrong_idx = np.where(preds != labels_arr)[0]\nprint(f\"Tong du doan sai : {len(wrong_idx)} / {len(labels_arr)} ({len(wrong_idx)/len(labels_arr)*100:.1f}%)\")\n\nerrors       = [(labels_arr[i], preds[i]) for i in wrong_idx]\nerror_counts = Counter(errors)\nprint(\"\\nTop 5 nham lan nhieu nhat:\")\nfor (true_cls, pred_cls), cnt in error_counts.most_common(5):\n    print(f\"  Muc {true_cls} -> Muc {pred_cls} : {cnt} lan\")\n\nval_imgs         = val_df[\"id_code\"].values\nhigh_conf_wrong  = sorted(wrong_idx, key=lambda i: probs[i][preds[i]], reverse=True)[:6]\n\nfig, axes = plt.subplots(2, 3, figsize=(14, 8))\nfig.suptitle(\"Cac mau du doan SAI voi do tin cay cao nhat\", fontsize=12, fontweight=\"bold\", color=\"red\")\nfor ax, idx in zip(axes.flat, high_conf_wrong):\n    img_path = os.path.join(CONFIG[\"train_img_dir\"], val_imgs[idx] + \".png\")\n    if not os.path.exists(img_path):\n        ax.axis(\"off\")\n        continue\n    img = preprocess_fundus(img_path, CONFIG, method=\"ben_graham\")\n    ax.imshow(img)\n    conf = probs[idx][preds[idx]] * 100\n    ax.set_title(\n        f\"Thuc: {CLASS_NAMES[labels_arr[idx]]}\\nDu doan: {CLASS_NAMES[preds[idx]]} ({conf:.1f}%)\",\n        fontsize=8,\n        color=\"darkred\"\n    )\n    ax.axis(\"off\")\n\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/error_analysis.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T06:43:14.991302Z","iopub.execute_input":"2026-04-03T06:43:14.991679Z","iopub.status.idle":"2026-04-03T06:43:17.778867Z","shell.execute_reply.started":"2026-04-03T06:43:14.991653Z","shell.execute_reply":"2026-04-03T06:43:17.777839Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# THU NGHIEM DU LIEU MOI (3 SAMPLES)\n@torch.no_grad()\ndef predict_single(model, img_path, cfg, device, true_label=None):\n    model.eval()\n    img    = preprocess_fundus(img_path, cfg, method=\"ben_graham\")\n    tensor = get_transforms(cfg, mode=\"val\")(image=img)[\"image\"].unsqueeze(0).to(device)\n    logits = model(tensor)\n    probs  = F.softmax(logits, dim=1).squeeze().cpu().numpy()\n    pred   = int(np.argmax(probs))\n    return {\n        \"predicted_class\" : pred,\n        \"predicted_label\" : CLASS_NAMES[pred],\n        \"confidence\"      : float(probs[pred]),\n        \"probabilities\"   : probs,\n        \"true_label\"      : true_label,\n        \"is_correct\"      : (pred == true_label) if true_label is not None else None,\n        \"img\"             : img,\n    }\n\n\ndf_test_local    = pd.read_csv(CONFIG[\"test_csv\"])\nnew_samples      = df_test_local.sample(3, random_state=2024).reset_index(drop=True)\nsimulated_labels = [0, 2, 4]\n\nprint(\"=\" * 65)\nprint(\"     THU NGHIEM VOI DU LIEU MOI (3 SAMPLES)\")\nprint(\"=\" * 65)\n\nresults_new = []\nfor i in range(3):\n    row      = new_samples.iloc[i]\n    img_path = os.path.join(CONFIG[\"test_img_dir\"], row[\"id_code\"] + \".png\")\n    true_lbl = simulated_labels[i]\n    result   = predict_single(model, img_path, CONFIG, DEVICE, true_lbl)\n    result[\"sample_id\"] = row[\"id_code\"]\n    results_new.append(result)\n\n    status = \"DUNG\" if result[\"is_correct\"] else \"SAI\"\n    print(f\"Mau {i+1}: {row['id_code']}\")\n    print(f\"  Nhan thuc  : Muc {true_lbl} - {CLASS_NAMES[true_lbl]}\")\n    print(f\"  Du doan    : Muc {result['predicted_class']} - {result['predicted_label']}\")\n    print(f\"  Do tin cay : {result['confidence']*100:.2f}%\")\n    print(f\"  Ket qua    : {status}\")\n    print(f\"  Xac suat tung lop:\")\n    for j in range(CONFIG[\"num_classes\"]):\n        bar = \">\" * int(result[\"probabilities\"][j] * 20)\n        print(f\"    Muc {j} [{CLASS_NAMES[j]:<28}]: {bar:<20} {result['probabilities'][j]*100:.2f}%\")\n    print()\n\nfig, axes = plt.subplots(3, 2, figsize=(14, 15))\nfig.suptitle(\"Ket qua du doan tren du lieu moi\", fontsize=13, fontweight=\"bold\")\n\nfor i, result in enumerate(results_new):\n    color  = \"green\" if result[\"is_correct\"] else \"red\"\n    status = \"DUNG\" if result[\"is_correct\"] else \"SAI\"\n\n    ax_img = axes[i][0]\n    ax_img.imshow(result[\"img\"])\n    ax_img.set_title(\n        f\"Mau {i+1}: {result['sample_id']}\\n\"\n        f\"Thuc: Muc {result['true_label']} | Du doan: Muc {result['predicted_class']} \"\n        f\"({result['confidence']*100:.1f}%) [{status}]\",\n        fontsize=9,\n        color=color,\n        fontweight=\"bold\"\n    )\n    ax_img.axis(\"off\")\n\n    ax_prob    = axes[i][1]\n    cls_labels = [f\"Muc {j}\" for j in range(CONFIG[\"num_classes\"])]\n    probs_arr  = result[\"probabilities\"]\n    bars = ax_prob.barh(cls_labels, probs_arr, color=CLASS_COLORS, edgecolor=\"black\", linewidth=0.8)\n    ax_prob.set_xlim(0, 1)\n    ax_prob.set_title(f\"Phan phoi xac suat - Mau {i+1}\", fontsize=9)\n    ax_prob.set_xlabel(\"Xac suat\")\n    ax_prob.axvline(0.5, color=\"gray\", linestyle=\"--\", alpha=0.5)\n    for bar, prob in zip(bars, probs_arr):\n        ax_prob.text(\n            bar.get_width() + 0.01,\n            bar.get_y() + bar.get_height() / 2,\n            f\"{prob*100:.1f}%\",\n            va=\"center\",\n            fontsize=9\n        )\n\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/new_sample_inference.png\", dpi=150, bbox_inches=\"tight\")\nplt.show()\n\ncorrect  = sum(1 for r in results_new if r[\"is_correct\"])\navg_conf = np.mean([r[\"confidence\"] for r in results_new]) * 100\nprint(\"-\" * 65)\nprint(f\"Ket qua tong 3 mau moi: {correct}/3 dung\")\nfor i, result in enumerate(results_new):\n    diff = abs(result[\"predicted_class\"] - result[\"true_label\"])\n    if result[\"is_correct\"]:\n        print(f\"Mau {i+1}: Du doan chinh xac (Conf: {result['confidence']*100:.1f}%)\")\n    elif diff == 1:\n        print(f\"Mau {i+1}: Sai 1 muc (lan can) - thuc {result['true_label']}, du doan {result['predicted_class']}\")\n    else:\n        print(f\"Mau {i+1}: Sai {diff} muc - thuc {result['true_label']}, du doan {result['predicted_class']}\")\nprint(f\"\\nDo tin cay trung binh: {avg_conf:.2f}%\")\nprint(\"Cell 14 OK\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T06:43:17.779994Z","iopub.execute_input":"2026-04-03T06:43:17.780284Z","iopub.status.idle":"2026-04-03T06:43:19.990296Z","shell.execute_reply.started":"2026-04-03T06:43:17.780258Z","shell.execute_reply":"2026-04-03T06:43:19.989224Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# TONG KET\nprint(\"=\" * 65)\nprint(\"                   TONG KET THUC NGHIEM\")\nprint(\"=\" * 65)\nprint(f\"Mo hinh        : EfficientNet-B3 (pretrained ImageNet)\")\nprint(f\"Dataset        : APTOS 2019 Blindness Detection\")\nprint(f\"So lop         : 5 (Muc 0 - 4)\")\nprint(f\"Ham loss       : Combined Focal Loss + Label Smoothing CE\")\nprint(f\"Optimizer      : AdamW + Cosine Warmup Scheduler\")\nprint(f\"Kich thuoc anh : {CONFIG['img_size']}x{CONFIG['img_size']}\")\nprint(f\"Tien xu ly     : Ben Graham + Crop + Albumentations\")\nprint()\nprint(f\"KET QUA CUOI:\")\nprint(f\"  Accuracy            : {eval_results['accuracy']:.4f} ({eval_results['accuracy']*100:.2f}%)\")\nprint(f\"  F1-Score (Macro)    : {eval_results['f1_macro']:.4f}\")\nprint(f\"  F1-Score (Weighted) : {eval_results['f1_weighted']:.4f}\")\nprint(f\"  Quadratic Kappa     : {eval_results['kappa']:.4f}\")\nprint()\nprint(\"File da luu tai /kaggle/working/:\")\nsaved_files = [\n    \"dataset_distribution.png\", \"sample_images.png\",\n    \"preprocessing_comparison.png\", \"learning_curves.png\",\n    \"confusion_matrix.png\", \"model_comparison.png\",\n    \"error_analysis.png\", \"new_sample_inference.png\",\n    \"best_model.pth\"\n]\nfor fname in saved_files:\n    path = f\"/kaggle/working/{fname}\"\n    size = os.path.getsize(path) / 1024 if os.path.exists(path) else 0\n    print(f\"  {fname:<45} ({size:.0f} KB)\")\nprint(\"=\" * 65)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T06:43:19.991469Z","iopub.execute_input":"2026-04-03T06:43:19.991846Z","iopub.status.idle":"2026-04-03T06:43:20.003011Z","shell.execute_reply.started":"2026-04-03T06:43:19.991804Z","shell.execute_reply":"2026-04-03T06:43:20.002191Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}