{"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":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":14774,"databundleVersionId":875431}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install grad-cam timm -q","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-03-20T02:42:37.841625Z","iopub.execute_input":"2026-03-20T02:42:37.841901Z","iopub.status.idle":"2026-03-20T02:42:49.157217Z","shell.execute_reply.started":"2026-03-20T02:42:37.841878Z","shell.execute_reply":"2026-03-20T02:42:49.156330Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score, confusion_matrix, ConfusionMatrixDisplay\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as transforms\nimport timm\n\n# 確認 GPU 是否可用\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"使用裝置：{device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-20T05:33:02.589297Z","iopub.execute_input":"2026-03-20T05:33:02.589874Z","iopub.status.idle":"2026-03-20T05:33:12.794622Z","shell.execute_reply.started":"2026-03-20T05:33:02.589845Z","shell.execute_reply":"2026-03-20T05:33:12.793696Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Kaggle 上 APTOS 2019 資料集的路徑\nDATA_DIR = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\n\ndf = pd.read_csv(f\"{DATA_DIR}/train.csv\")\nprint(f\"總共 {len(df)} 筆資料\")\nprint(df['diagnosis'].value_counts().sort_index())\n\n# 設定中文字體 (例如：微軟正黑體)\nplt.rcParams['font.sans-serif'] = ['Microsoft JhengHei'] \n\n# 畫出類別分布\nfig, axes = plt.subplots(1, 2, figsize=(12, 4))\n\n# 長條圖\ndf['diagnosis'].value_counts().sort_index().plot(kind='bar', ax=axes[0], color='steelblue')\naxes[0].set_title(\"Class distribution (0=Normal, 4=Severe)\")\naxes[0].set_xlabel(\"Diagnosis grade\")\naxes[0].set_ylabel(\"Count\")\nplt.suptitle(\"Sample images per grade\")\n\n# 顯示各類別樣本影像\naxes[1].axis('off')\nplt.tight_layout()\nplt.show()\n\n# 顯示 5 個類別各一張範例影像\nfig, axes = plt.subplots(1, 5, figsize=(15, 3))\nfor grade in range(5):\n    sample = df[df['diagnosis'] == grade].iloc[0]\n    img_path = f\"{DATA_DIR}/train_images/{sample['id_code']}.png\"\n    img = Image.open(img_path)\n    axes[grade].imshow(img)\n    axes[grade].set_title(f\"Grade {grade}\")\n    axes[grade].axis('off')\nplt.suptitle(\"各嚴重程度範例影像\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-20T05:33:16.103629Z","iopub.execute_input":"2026-03-20T05:33:16.104742Z","iopub.status.idle":"2026-03-20T05:33:18.596695Z","shell.execute_reply.started":"2026-03-20T05:33:16.104712Z","shell.execute_reply":"2026-03-20T05:33:18.595854Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\n\ndef load_and_preprocess(img_path, image_size=380):\n    \"\"\"\n    讀取 Fundus 影像並做前處理：\n    1. 去除黑色邊框（裁切出圓形視網膜區域）\n    2. CLAHE 強化血管對比\n    3. 縮放到固定尺寸\n    \"\"\"\n    img = cv2.imread(img_path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    \n    # 去黑邊：找到非黑色像素的範圍並裁切\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n    _, thresh = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)\n    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    if contours:\n        x, y, w, h = cv2.boundingRect(max(contours, key=cv2.contourArea))\n        img = img[y:y+h, x:x+w]\n    \n    # CLAHE：在 LAB 色彩空間對亮度通道做對比增強\n    lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)\n    l, a, b = cv2.split(lab)\n    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n    l = clahe.apply(l)\n    img = cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2RGB)\n    \n    img = cv2.resize(img, (image_size, image_size))\n    return img\n\n# 確認前處理效果\nsample_path = f\"{DATA_DIR}/train_images/{df.iloc[0]['id_code']}.png\"\noriginal = Image.open(sample_path)\nprocessed = load_and_preprocess(sample_path)\n\nfig, axes = plt.subplots(1, 2, figsize=(8, 4))\naxes[0].imshow(original)\naxes[0].set_title(\"原始影像\")\naxes[0].axis('off')\naxes[1].imshow(processed)\naxes[1].set_title(\"前處理後\")\naxes[1].axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-20T05:33:30.801450Z","iopub.execute_input":"2026-03-20T05:33:30.802167Z","iopub.status.idle":"2026-03-20T05:33:32.234903Z","shell.execute_reply.started":"2026-03-20T05:33:30.802135Z","shell.execute_reply":"2026-03-20T05:33:32.234298Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 訓練用的資料增強：隨機翻轉、旋轉等，讓模型更有泛化能力\ntrain_transforms = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(30),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                         std=[0.229, 0.224, 0.225])  # ImageNet 標準化\n])\n\nval_transforms = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                         std=[0.229, 0.224, 0.225])\n])\n\nclass FundusDataset(Dataset):\n    def __init__(self, df, data_dir, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.data_dir = data_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 = f\"{self.data_dir}/train_images/{row['id_code']}.png\"\n        img = load_and_preprocess(img_path)\n        label = row['diagnosis']\n        if self.transform:\n            img = self.transform(img)\n        return img, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-20T05:33:51.045770Z","iopub.execute_input":"2026-03-20T05:33:51.046086Z","iopub.status.idle":"2026-03-20T05:33:51.053299Z","shell.execute_reply.started":"2026-03-20T05:33:51.046063Z","shell.execute_reply":"2026-03-20T05:33:51.052401Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 用 StratifiedKFold 確保每個 fold 的類別比例一致\nskf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\ntrain_idx, val_idx = next(iter(skf.split(df, df['diagnosis'])))\n\ntrain_df = df.iloc[train_idx]\nval_df   = df.iloc[val_idx]\n\ntrain_dataset = FundusDataset(train_df, DATA_DIR, transform=train_transforms)\nval_dataset   = FundusDataset(val_df,   DATA_DIR, transform=val_transforms)\n\ntrain_loader = DataLoader(train_dataset, batch_size=16, shuffle=True,  num_workers=2)\nval_loader   = DataLoader(val_dataset,   batch_size=16, shuffle=False, num_workers=2)\n\n# 載入預訓練的 EfficientNet-B3\nmodel = timm.create_model('efficientnet_b3', pretrained=True, num_classes=5)\nmodel = model.to(device)\n\n# 類別不均衡處理：計算每個類別的權重（少的類別給高權重）\nclass_counts = df['diagnosis'].value_counts().sort_index().values\nclass_weights = torch.tensor(1.0 / class_counts, dtype=torch.float).to(device)\nclass_weights = class_weights / class_weights.sum() * 5  # 正規化\n\ncriterion = nn.CrossEntropyLoss(weight=class_weights)\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)\n\ndef train_one_epoch(model, loader, optimizer, criterion):\n    model.train()\n    total_loss, correct, total = 0, 0, 0\n    for imgs, labels in loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n        correct += (outputs.argmax(1) == labels).sum().item()\n        total += len(labels)\n    return total_loss / len(loader), correct / total\n\ndef validate(model, loader, criterion):\n    model.eval()\n    total_loss, correct, total = 0, 0, 0\n    all_probs, all_labels = [], []\n    with torch.no_grad():\n        for imgs, labels in loader:\n            imgs, labels = imgs.to(device), labels.to(device)\n            outputs = model(imgs)\n            loss = criterion(outputs, labels)\n            total_loss += loss.item()\n            correct += (outputs.argmax(1) == labels).sum().item()\n            total += len(labels)\n            all_probs.append(torch.softmax(outputs, dim=1).cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n    all_probs  = np.vstack(all_probs)\n    all_labels = np.concatenate(all_labels)\n    auc = roc_auc_score(all_labels, all_probs, multi_class='ovr')\n    return total_loss / len(loader), correct / total, auc\n\n# 訓練 10 個 epoch\nEPOCHS = 10\nbest_auc = 0\n\nfor epoch in range(EPOCHS):\n    train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, criterion)\n    val_loss, val_acc, val_auc = validate(model, val_loader, criterion)\n    scheduler.step()\n    \n    print(f\"Epoch {epoch+1:02d}/{EPOCHS} | \"\n          f\"Train Loss: {train_loss:.4f} Acc: {train_acc:.3f} | \"\n          f\"Val Loss: {val_loss:.4f} Acc: {val_acc:.3f} AUC: {val_auc:.4f}\")\n    \n    if val_auc > best_auc:\n        best_auc = val_auc\n        torch.save(model.state_dict(), \"best_model.pth\")\n        print(f\"  -> 儲存最佳模型 (AUC: {best_auc:.4f})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-20T02:55:24.811828Z","iopub.execute_input":"2026-03-20T02:55:24.812222Z","iopub.status.idle":"2026-03-20T04:01:12.795457Z","shell.execute_reply.started":"2026-03-20T02:55:24.812193Z","shell.execute_reply":"2026-03-20T04:01:12.794460Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\n\n# 載入最佳模型\nmodel.load_state_dict(torch.load(\"best_model.pth\"))\nmodel.eval()\n\n# --- Confusion Matrix ---\nall_preds, all_labels = [], []\nwith torch.no_grad():\n    for imgs, labels in val_loader:\n        imgs = imgs.to(device)\n        preds = model(imgs).argmax(1).cpu().numpy()\n        all_preds.extend(preds)\n        all_labels.extend(labels.numpy())\n\ncm = confusion_matrix(all_labels, all_preds)\ndisp = ConfusionMatrixDisplay(cm, display_labels=[f\"Grade {i}\" for i in range(5)])\nfig, ax = plt.subplots(figsize=(7, 6))\ndisp.plot(ax=ax, colorbar=False, cmap='Blues')\nplt.title(\"Confusion Matrix (Validation Set)\")\nplt.suptitle(\"Grad-CAM: Model attention visualization\", fontsize=13)\nplt.tight_layout()\nplt.savefig(\"confusion_matrix.png\", dpi=150)\nplt.show()\n\n# --- Grad-CAM ---\n# 取 EfficientNet 最後一個卷積層作為目標層\ntarget_layer = [model.conv_head]\ncam = GradCAM(model=model, target_layers=target_layer)\n\nfig, axes = plt.subplots(2, 5, figsize=(16, 7))\nfor grade in range(5):\n    sample = val_df[val_df['diagnosis'] == grade].iloc[0]\n    img_path = f\"{DATA_DIR}/train_images/{sample['id_code']}.png\"\n    \n    raw_img = load_and_preprocess(img_path).astype(np.float32) / 255.0\n    input_tensor = val_transforms(\n        (raw_img * 255).astype(np.uint8)\n    ).unsqueeze(0).to(device)\n    \n    targets = [ClassifierOutputTarget(grade)]\n    grayscale_cam = cam(input_tensor=input_tensor, targets=targets)[0]\n    cam_image = show_cam_on_image(raw_img, grayscale_cam, use_rgb=True)\n    \n    axes[0][grade].imshow(raw_img)\n    axes[0][grade].set_title(f\"Grade {grade} 原圖\")\n    axes[0][grade].axis('off')\n    \n    axes[1][grade].imshow(cam_image)\n    axes[1][grade].set_title(f\"Grade {grade} Grad-CAM\")\n    axes[1][grade].axis('off')\n\nplt.suptitle(\"Grad-CAM：模型關注區域視覺化\", fontsize=13)\nplt.tight_layout()\nplt.savefig(\"gradcam_results.png\", dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-20T04:47:14.952025Z","iopub.execute_input":"2026-03-20T04:47:14.952334Z","iopub.status.idle":"2026-03-20T04:48:35.025547Z","shell.execute_reply.started":"2026-03-20T04:47:14.952309Z","shell.execute_reply":"2026-03-20T04:48:35.024397Z"}},"outputs":[],"execution_count":null}]}