{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":14774,"databundleVersionId":875431,"sourceType":"competition"},{"sourceId":12722239,"sourceType":"datasetVersion","datasetId":8041124},{"sourceId":12725723,"sourceType":"datasetVersion","datasetId":8043467},{"sourceId":12749005,"sourceType":"datasetVersion","datasetId":8059130},{"sourceId":12779032,"sourceType":"datasetVersion","datasetId":8078999},{"sourceId":12786005,"sourceType":"datasetVersion","datasetId":8083628},{"sourceId":12786116,"sourceType":"datasetVersion","datasetId":8083695},{"sourceId":12796699,"sourceType":"datasetVersion","datasetId":8090653},{"sourceId":526233,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":412338,"modelId":430113}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport os\nimport pandas as pd\nimport random\nimport timm\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom albumentations import Compose, Normalize, HorizontalFlip, Rotate, ColorJitter, Resize\nfrom albumentations.pytorch import ToTensorV2\nfrom sklearn.metrics import cohen_kappa_score\nfrom sklearn.model_selection import StratifiedKFold\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom torch.utils.data import DataLoader, Dataset\nfrom tqdm import tqdm\nfrom scipy.optimize import minimize\n\n# ==============================================================================\n# 0. 全局设置\n# ==============================================================================\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed_all(seed)\n        torch.backends.cudnn.deterministic = True\n        torch.backends.cudnn.benchmark = False\n\nset_seed(42)\n\n# ==============================================================================\n# 1. 预处理函数\n# ==============================================================================\ndef circle_crop(image):\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    _, thresh = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)\n    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    if not contours: return image\n    cnt = max(contours, key=cv2.contourArea)\n    x, y, w, h = cv2.boundingRect(cnt)\n    return image[y:y + h, x:x + w]\n\ndef apply_ben_graham_preprocessing(image, sigmaX=30):\n    blurred_image = cv2.GaussianBlur(image, (0, 0), sigmaX)\n    return cv2.addWeighted(image, 4, blurred_image, -4, 128)\n\ndef preprocess_directory(input_dir, output_dir, image_size):\n    if not os.path.exists(output_dir):\n        os.makedirs(output_dir); print(f\"创建目录: {output_dir}\")\n    image_files = [f for f in os.listdir(input_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]\n    for filename in tqdm(image_files, desc=f\"处理中 {os.path.basename(input_dir)}\"):\n        input_path, output_path = os.path.join(input_dir, filename), os.path.join(output_dir, filename)\n        image = cv2.imread(input_path)\n        if image is None: continue\n        processed_image = circle_crop(image)\n        processed_image = cv2.resize(processed_image, (image_size, image_size))\n        processed_image = apply_ben_graham_preprocessing(processed_image)\n        cv2.imwrite(output_path, processed_image)\n    print(f\"处理完成！处理后的图片已保存至: {output_dir}\")\n\n# ==============================================================================\n# 2. PyTorch 模型与数据类\n# ==============================================================================\ndef replace_batchnorm_with_groupnorm(module, num_groups=32):\n    for name, child in module.named_children():\n        if isinstance(child, nn.BatchNorm2d):\n            num_channels = child.num_features\n            if num_channels % num_groups == 0:\n                setattr(module, name, nn.GroupNorm(num_groups=num_groups, num_channels=num_channels))\n        else:\n            replace_batchnorm_with_groupnorm(child, num_groups)\n\nclass EfficientNetModel(nn.Module):\n    def __init__(self, model_name='efficientnet_b6', pretrained_model_path=None):\n        super().__init__()\n        self.model = timm.create_model(model_name, pretrained=False)\n        replace_batchnorm_with_groupnorm(self.model)\n        in_features = self.model.classifier.in_features\n        self.model.classifier = nn.Sequential(\n            nn.Dropout(p=0.5), nn.Linear(in_features, 512), nn.Mish(),\n            nn.Dropout(p=0.5), nn.Linear(512, 1)\n        )\n        if pretrained_model_path and os.path.exists(pretrained_model_path):\n            print(f\"加载自定义预训练权重: {pretrained_model_path}\")\n            self.model.load_state_dict(torch.load(pretrained_model_path), strict=False)\n        elif pretrained_model_path:\n             print(f\"警告: 预训练模型路径未找到: {pretrained_model_path}\")\n    def forward(self, x): return self.model(x)\n\nclass CustomImageDataset(Dataset):\n    def __init__(self, img_dir, labels_df, transform=None, is_test=False):\n        self.img_dir, self.labels_df, self.transform, self.is_test = img_dir, labels_df, transform, is_test\n    def __len__(self): return len(self.labels_df)\n    def __getitem__(self, idx):\n        row = self.labels_df.iloc[idx]\n        img_id = row['id_code']\n        img_path = os.path.join(self.img_dir, img_id + '.png')\n        try:\n            img = cv2.imread(img_path)\n            if img is None: return self.__getitem__((idx + 1) % len(self))\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            if self.transform: img = self.transform(image=img)[\"image\"]\n            if self.is_test: return img\n            else:\n                label = float(row['diagnosis'])\n                return img, label\n        except Exception: return self.__getitem__((idx + 1) % len(self))\n\ndef get_transforms(data_type='train', image_size=512):\n    if data_type == 'train':\n        return Compose([Resize(image_size, image_size), Rotate(limit=40, p=0.5), ColorJitter(p=0.3), HorizontalFlip(p=0.5), Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2()])\n    else:\n        return Compose([Resize(image_size, image_size), Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2()])\n\n# ==============================================================================\n# 3. 训练与预测函数\n# ==============================================================================\ndef train_fold(model, train_loader, valid_loader, device, num_epochs, lr, fold):\n    criterion, optimizer = nn.MSELoss(), optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-5)\n    scheduler = CosineAnnealingLR(optimizer, T_max=num_epochs, eta_min=1e-6)\n    best_valid_kappa, patience, epochs_no_improve = 0.0, 5, 0\n    save_path = os.path.join(os.getcwd(), f'best_model_fold_{fold}.pth')\n    for epoch in range(num_epochs):\n        model.train(); running_loss = 0.0\n        for images, labels in tqdm(train_loader, desc=f\"Fold {fold} - Epoch {epoch+1} 训练\"):\n            images, labels = images.to(device), labels.to(device).float().view(-1, 1)\n            optimizer.zero_grad(); outputs = model(images); loss = criterion(outputs, labels)\n            loss.backward(); optimizer.step(); running_loss += loss.item()\n        train_loss = running_loss / len(train_loader)\n        \n        model.eval(); all_valid_labels, all_valid_preds = [], []\n        with torch.no_grad():\n            for images, labels in tqdm(valid_loader, desc=f\"Fold {fold} - Epoch {epoch+1} 验证\"):\n                \n                # <--- 关键修正 ---\n                # 必须先将数据移动到device，再送入模型\n                images = images.to(device)\n                outputs = model(images).squeeze()\n                # --- 修正结束 ---\n\n                all_valid_labels.extend(labels.cpu().numpy()); all_valid_preds.extend(np.atleast_1d(outputs.cpu().numpy()))\n        \n        valid_kappa = cohen_kappa_score(all_valid_labels, np.round(all_valid_preds).astype(int), weights='quadratic')\n        print(f\"\\nFold {fold} - Epoch {epoch+1}\\n  训练损失: {train_loss:.4f}\\n  验证 Kappa: {valid_kappa:.4f}\")\n        scheduler.step()\n        if valid_kappa > best_valid_kappa:\n            best_valid_kappa, epochs_no_improve = valid_kappa, 0\n            torch.save(model.state_dict(), save_path); print(f\"  -> 新最佳模型已保存! Kappa: {best_valid_kappa:.4f}\")\n        else: epochs_no_improve += 1\n        if epochs_no_improve >= patience: print(f\"触发早停。\"); break\n    \n    print(f\"\\nFold {fold} 训练结束。最佳模型保存在: {save_path}\")\n    return save_path, best_valid_kappa\n\nclass OptimizedRounder:\n    def __init__(self): self.coef_ = 0\n    def _kappa_loss(self, coef, X, y): return -cohen_kappa_score(y, self.predict(X, coef), weights='quadratic')\n    def fit(self, X, y):\n        loss_partial = lambda coef: self._kappa_loss(coef, X, y)\n        self.coef_ = minimize(loss_partial, [0.5, 1.5, 2.5, 3.5], method='nelder-mead')['x']\n        print(f\"优化后的阈值: {self.coef_}\")\n    def predict(self, X, coef):\n        X_p = np.copy(X)\n        for i, pred in enumerate(X_p):\n            if pred < coef[0]: X_p[i] = 0\n            elif pred < coef[1]: X_p[i] = 1\n            elif pred < coef[2]: X_p[i] = 2\n            elif pred < coef[3]: X_p[i] = 3\n            else: X_p[i] = 4\n        return X_p.astype(int)\n\ndef get_predictions(loader, model, device):\n    model.eval(); all_preds = []\n    with torch.no_grad():\n        for images in tqdm(loader, desc=\"获取预测结果\"):\n            images = images.to(device); outputs = model(images).squeeze()\n            all_preds.extend(np.atleast_1d(outputs.cpu().numpy()))\n    return np.array(all_preds)\n\n# ==============================================================================\n# 4. 主执行逻辑\n# ==============================================================================\nif __name__ == \"__main__\":\n    # --- 步骤 1: 定义路径和参数 ---\n    IMAGE_SIZE = 512; BATCH_SIZE = 4; NUM_WORKERS = 2\n    FINETUNE_EPOCHS = 25; FINETUNE_LR = 3e-4; N_SPLITS = 5\n    PRETRAINED_MODEL_PATH = '/kaggle/input/pretrained_2015_efficientnet_b6_256/pytorch/default/1/pretrained_2015_efficientnet_b6_256.pth'\n    \n    RAW_DATA_DIR = '/kaggle/input/aptos2019-blindness-detection'\n    source_train_dir = os.path.join(RAW_DATA_DIR, 'train_images')\n    source_test_dir = os.path.join(RAW_DATA_DIR, 'test_images')\n    train_label_file = os.path.join(RAW_DATA_DIR, 'train.csv')\n    test_label_file = os.path.join(RAW_DATA_DIR, 'test.csv')\n\n    preprocessed_input_dir = '/kaggle/input/images-preprocessed-512'\n    working_dir = '/kaggle/working'\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    print(f\"使用的设备: {device}\")\n\n    # --- 步骤 2: 智能检查并确定数据路径 ---\n    final_train_dir = os.path.join(preprocessed_input_dir, 'train_images_preprocessed_512')\n    final_test_dir = os.path.join(preprocessed_input_dir, 'test_images_preprocessed_512')\n    if not (os.path.exists(final_train_dir) and os.path.exists(final_test_dir)):\n        print(f\"未在 {preprocessed_input_dir} 找到预处理数据，将执行实时预处理。\")\n        final_train_dir, final_test_dir = os.path.join(working_dir, f'train_images_preprocessed_{IMAGE_SIZE}'), os.path.join(working_dir, f'test_images_preprocessed_{IMAGE_SIZE}')\n        preprocess_directory(source_train_dir, final_train_dir, IMAGE_SIZE)\n        preprocess_directory(source_test_dir, final_test_dir, IMAGE_SIZE)\n    else: print(f\"成功找到已有的预处理数据: {preprocessed_input_dir}\")\n\n    # --- 步骤 3: 5折交叉验证训练 ---\n    df = pd.read_csv(train_label_file)\n    skf = StratifiedKFold(n_splits=N_SPLITS, shuffle=True, random_state=42)\n    all_folds_exist = all([os.path.exists(f'best_model_fold_{f}.pth') for f in range(N_SPLITS)])\n    if not all_folds_exist:\n        for fold, (train_idx, val_idx) in enumerate(skf.split(df['id_code'], df['diagnosis'])):\n            if os.path.exists(f'best_model_fold_{fold}.pth'): print(f\"Fold {fold} 模型已存在，跳过。\"); continue\n            print(f\"\\n--- 开始训练 Fold {fold} ---\")\n            train_df, valid_df = df.iloc[train_idx], df.iloc[val_idx]\n            train_dataset = CustomImageDataset(final_train_dir, train_df, transform=get_transforms('train', IMAGE_SIZE))\n            valid_dataset = CustomImageDataset(final_train_dir, valid_df, transform=get_transforms('valid', IMAGE_SIZE))\n            train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS)\n            valid_loader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n            model = EfficientNetModel(pretrained_model_path=PRETRAINED_MODEL_PATH).to(device)\n            train_fold(model, train_loader, valid_loader, device, FINETUNE_EPOCHS, FINETUNE_LR, fold)\n    else: print(\"\\n--- 所有5折模型文件均已存在，跳过训练阶段 ---\")\n\n    # --- 步骤 4: OOF预测与阈值优化 ---\n    print(\"\\n--- 步骤 4: 生成OOF预测并优化阈值 ---\")\n    df = pd.read_csv(train_label_file)\n    oof_preds = np.zeros((len(df), 1))\n    for fold, (train_idx, val_idx) in enumerate(skf.split(df['id_code'], df['diagnosis'])):\n        valid_df = df.iloc[val_idx]\n        valid_dataset = CustomImageDataset(final_train_dir, valid_df, transform=get_transforms('valid', IMAGE_SIZE))\n        valid_loader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n        model = EfficientNetModel(pretrained_model_path=None).to(device)\n        model.load_state_dict(torch.load(f'best_model_fold_{fold}.pth'))\n        oof_preds[val_idx] = get_predictions(valid_loader, model, device).reshape(-1, 1)\n\n    true_labels = df['diagnosis'].values\n    rounder = OptimizedRounder(); rounder.fit(oof_preds.flatten(), true_labels)\n    optimized_coefficients = rounder.coef_\n    final_oof_preds = rounder.predict(oof_preds.flatten(), optimized_coefficients)\n    oof_kappa = cohen_kappa_score(true_labels, final_oof_preds, weights='quadratic')\n    print(f\"\\n优化后的 OOF Kappa 分数: {oof_kappa:.4f}\")\n\n    # --- 步骤 5: 集成预测测试集并生成提交文件 ---\n    print(\"\\n--- 步骤 5: 集成预测测试集并提交 ---\")\n    test_df = pd.read_csv(test_label_file)\n    test_dataset = CustomImageDataset(final_test_dir, test_df, transform=get_transforms('test', IMAGE_SIZE), is_test=True)\n    test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n    all_test_preds = []\n    for fold in range(N_SPLITS):\n        print(f\"--- 使用 Fold {fold} 模型进行测试集预测 ---\")\n        model = EfficientNetModel(pretrained_model_path=None).to(device)\n        model.load_state_dict(torch.load(f'best_model_fold_{fold}.pth'))\n        all_test_preds.append(get_predictions(test_loader, model, device))\n    avg_test_preds = np.mean(all_test_preds, axis=0)\n    final_test_labels = rounder.predict(avg_test_preds, optimized_coefficients)\n    submission_df = pd.read_csv(test_label_file)\n    submission_df['diagnosis'] = final_test_labels\n    submission_df.to_csv('submission.csv', index=False)\n    print(\"\\n--- 提交文件 'submission.csv' 已生成 ---\")\n    print(\"--- 脚本执行结束 ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-10T15:22:44.252016Z","iopub.execute_input":"2025-08-10T15:22:44.252338Z"}},"outputs":[],"execution_count":null}]}