{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.14","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,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":187731,"sourceType":"datasetVersion","datasetId":80814}],"dockerImageVersionId":30804,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# 导入 numpy 库，用于进行线性代数运算，如数组操作、矩阵运算等\nimport numpy as np \n# 导入 pandas 库，用于数据处理和 CSV 文件的读写操作\nimport pandas as pd  \n# 导入 TensorFlow 深度学习框架\nimport tensorflow as tf\nfrom tensorflow.keras import backend as K  # 导入 TensorFlow Keras 的后端模块，用于在自定义加权损失函数中将类别权重转换为张量等操作\n# 从 matplotlib 库中导入 pyplot 模块，用于绘制图表和可视化数据\nfrom matplotlib import pyplot as plt\n# 从 sklearn 库的 metrics 模块中导入 cohen_kappa_score 函数，用于计算 Cohen's kappa 系数，评估分类模型性能\nfrom sklearn.metrics import cohen_kappa_score\n# 从 TensorFlow 的 keras 模块的 preprocessing.image 子模块导入 ImageDataGenerator 类，用于图像数据增强\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n# 从 TensorFlow 的 keras 模块的 applications.densenet 子模块导入 DenseNet121 预训练模型\nfrom tensorflow.keras.applications.densenet import DenseNet121\n# 导入 OpenCV 库，用于计算机视觉任务，如读取、处理和操作图像\nimport cv2\n# 导入 os 库，用于与操作系统进行交互，如文件和目录操作\nimport os\n# 从 TensorFlow 的 keras 模块的 callbacks 子模块导入 Callback 基类，用于在模型训练过程中添加回调函数\nfrom tensorflow.keras.callbacks import Callback\n# 从 sklearn 库的 model_selection 模块导入 train_test_split 函数，用于将数据集划分为训练集和测试集\nfrom sklearn.model_selection import train_test_split\n# 从 sklearn 库的 metrics 模块导入 confusion_matrix 函数，用于计算混淆矩阵，评估分类模型性能\nfrom sklearn.metrics import confusion_matrix\n# 从 sklearn 库的 utils.multiclass 模块导入 unique_labels 函数，用于获取数据集中的唯一标签\nfrom sklearn.utils.multiclass import unique_labels\n# 从 sklearn 库的 utils 模块导入 class_weight 函数，用于计算每个类别的权重，处理类别不平衡问题\nfrom sklearn.utils.class_weight import compute_class_weight\n# 导入 Keras，用于构建和训练深度学习模型\nfrom tensorflow import keras\n# 导入随机梯度下降（SGD）优化器，用于更新神经网络权重\nfrom tensorflow.keras.optimizers import SGD  \n# 导入 ModelCheckpoint，用于在训练过程中自动保存模型的最佳权重\nfrom tensorflow.keras.callbacks import ModelCheckpoint\n# 导入TensorFlow Keras模块中的Model类，用于构建和操作深度学习模型，例如定义模型结构、训练模型、评估模型性能等\nfrom tensorflow.keras.models import Model\n# 导入TensorFlow Keras的图像预处理模块，该模块提供了一系列用于处理图像数据的工具和函数\nfrom tensorflow.keras.preprocessing import image  \n# 打印指定目录（../input/）下的所有文件和目录的名称\nprint(os.listdir(\"../input\"))\n# 导入用于计算精确率、召回率和F1分数的函数\nfrom sklearn.metrics import precision_score, recall_score, f1_score\n# 导入用于计算ROC曲线和AUC值的函数\nfrom sklearn.metrics import roc_curve, auc\n# 导入label_binarize函数，用于将多类别标签编码为二进制形式\nfrom sklearn.preprocessing import label_binarize\n# 导入Sequence类，用于创建可迭代的数据生成器，该生成器能够提供批量数据给模型进行训练或预测\nfrom tensorflow.keras.utils import Sequence","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:01:46.330465Z","iopub.execute_input":"2025-05-04T02:01:46.330735Z","iopub.status.idle":"2025-05-04T02:01:50.024498Z","shell.execute_reply.started":"2025-05-04T02:01:46.330702Z","shell.execute_reply":"2025-05-04T02:01:50.023570Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 定义一个自定义回调类 QWKCallback，继承自 Callback 类\n# 用于在每个训练 epoch 结束时计算并打印 QWK 分数，并在分数提高时保存模型\n# QWK分数是一种常用于评估分类模型性能的指标，特别适用于评估类别之间存在顺序关系的情况。QWK的取值范围通常在0到1之间，分数越高表示一致性越高。\nclass QWKCallback(Callback):\n    def __init__(self, validation_data):\n        # 调用父类的构造函数\n        super(Callback, self).__init__()\n        # 保存验证集的输入数据\n        self.X = validation_data[0]\n        # 保存验证集的标签数据\n        self.Y = validation_data[1]\n        # 初始化一个空列表，用于存储每个 epoch 的 QWK 分数\n        self.history = []\n\n    def on_epoch_end(self, epoch, logs={}):\n        # 使用当前模型对验证集进行预测\n        pred = self.model.predict(self.X)\n        # 计算 QWK 分数\n        score = cohen_kappa_score(np.argmax(self.Y, axis=1), # 获取真实标签的类别索引\n                                  np.argmax(pred, axis=1),  # 获取模型预测的类别索引\n                                  labels=[0, 1, 2, 3, 4],   # 指定可能的类别标签，确保 kappa 计算时覆盖所有类别\n                                  weights='quadratic')      # 采用二次加权方式，对远离真实类别的错误给予更大惩罚\n\n        # 打印当前 epoch 的 QWK 分数\n        print(\"Epoch {} : QWK: {}\".format(epoch+1, score))\n        # 将当前 epoch 的 QWK 分数添加到历史记录中\n        self.history.append(score)\n        # 如果当前分数是历史最高分，则保存模型\n        if score >= max(self.history):\n            print('saving checkpoint: ', score)\n            self.model.save('../working/Resnet50_bestqwk.h5')","metadata":{"execution":{"iopub.status.busy":"2025-05-04T02:01:50.029648Z","iopub.execute_input":"2025-05-04T02:01:50.029907Z","iopub.status.idle":"2025-05-04T02:01:50.036973Z","shell.execute_reply.started":"2025-05-04T02:01:50.029880Z","shell.execute_reply":"2025-05-04T02:01:50.036049Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 创建一个数据生成器类，用于在模型训练过程中生成混合数据\nclass MixupGenerator(Sequence):\n    def __init__(self, X, y, batch_size=32, alpha=0.2, shuffle=True, datagen=None, **kwargs):\n        # 初始化MixupGenerator，继承自Sequence类，用于生成Mixup数据\n        super().__init__(**kwargs)  # 调用父类Sequence的构造函数\n        self.X = X  # 输入数据\n        self.y = y  # 标签数据\n        self.batch_size = batch_size  # 每个批次的样本数量\n        self.alpha = alpha  # Mixup参数，用于Beta分布，控制混合权重\n        self.shuffle = shuffle  # 是否在每个epoch结束时打乱数据\n        self.datagen = datagen  # 数据增强生成器\n        self.sample_num = len(self.X)  # 样本总数\n        self.on_epoch_end()  # 初始化索引\n\n    def __len__(self):\n        # 返回生成器中批次的总数，每个batch使用2 * batch_size个样本\n        return self.sample_num // (self.batch_size * 2)\n\n    def __getitem__(self, index):\n        # 根据索引获取批次数据\n        start = index * self.batch_size * 2\n        end = start + self.batch_size * 2\n\n        if end > len(self.indexes):\n            raise IndexError(f\"索引超出范围：index={index}, 可用长度={len(self.indexes)}\")\n\n        batch_ids = self.indexes[start:end]  # 获取批次索引\n\n        return self.__data_generation(batch_ids)  # 生成Mixup数据\n\n    def on_epoch_end(self):\n        # 每个epoch结束时调用，重置索引并打乱数据\n        self.indexes = np.arange(self.sample_num)\n        if self.shuffle:\n            np.random.shuffle(self.indexes)\n\n    def __data_generation(self, batch_ids):\n        # 根据给定的索引生成Mixup数据\n        l = np.random.beta(self.alpha, self.alpha, self.batch_size)  # 从Beta分布中采样混合权重\n        X_l = l.reshape(self.batch_size, 1, 1, 1)  # 调整形状以用于元素级乘法\n        y_l = l.reshape(self.batch_size, 1)  # 调整形状以用于标签混合\n\n        X1 = self.X[batch_ids[:self.batch_size]]  # 获取第一个样本\n        X2 = self.X[batch_ids[self.batch_size:]]  # 获取第二个样本\n        X = X1 * X_l + X2 * (1 - X_l)  # 混合图像数据\n\n        if self.datagen:  # 如果提供了数据增强生成器\n            for i in range(self.batch_size):\n                X[i] = self.datagen.random_transform(X[i])  # 随机变换图像\n                X[i] = self.datagen.standardize(X[i])  # 标准化图像\n\n        if isinstance(self.y, list):  # 如果标签是列表形式\n            y = []\n            for y_ in self.y:\n                y1 = y_[batch_ids[:self.batch_size]]\n                y2 = y_[batch_ids[self.batch_size:]]\n                y.append(y1 * y_l + y2 * (1 - y_l))  # 混合标签数据\n        else:\n            y1 = self.y[batch_ids[:self.batch_size]]\n            y2 = self.y[batch_ids[self.batch_size:]]\n            y = y1 * y_l + y2 * (1 - y_l)  # 混合标签数据\n\n        return X, y  # 返回混合后的数据和标签","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:01:50.038513Z","iopub.execute_input":"2025-05-04T02:01:50.039340Z","iopub.status.idle":"2025-05-04T02:01:50.059911Z","shell.execute_reply.started":"2025-05-04T02:01:50.039272Z","shell.execute_reply":"2025-05-04T02:01:50.059071Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 定义一个函数，用于绘制混淆矩阵，评估分类模型性能。\ndef plot_confusion_matrix(y_true, y_pred, classes,\n                          normalize=False,\n                          title=None,\n                          cmap=plt.cm.Blues):\n    if not title:\n        if normalize:\n            title = 'Normalized confusion matrix'\n        else:\n            title = 'Confusion matrix, without normalization'\n\n    # 计算混淆矩阵\n    cm = confusion_matrix(y_true, y_pred)\n    # 仅保留数据集中真实出现过的类别，避免使用多余的类别标签。\n    classes = classes[unique_labels(y_true, y_pred)]\n    if normalize:\n        # 如果需要归一化，则将混淆矩阵转换为百分比形式\n        cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]\n        print(\"Normalized confusion matrix\")\n    else:\n        print('Confusion matrix, without normalization')\n\n    print(cm)\n\n    # 创建一个图形和坐标轴对象\n    fig, ax = plt.subplots()\n    # 绘制混淆矩阵的热力图\n    im = ax.imshow(cm, interpolation='nearest', cmap=cmap) # 像素间不进行插值，使颜色块更清晰。\n    # 添加颜色条\n    ax.figure.colorbar(im, ax=ax)\n    # 设置坐标轴的刻度和标签\n    ax.set(xticks=np.arange(cm.shape[1]),  # x 轴刻度\n           yticks=np.arange(cm.shape[0]),  # y 轴刻度\n           xticklabels=classes, yticklabels=classes,  # 设置刻度标签\n           title=title,\n           ylabel='True label',  # y 轴标签（真实类别）\n           xlabel='Predicted label')  # x 轴标签（预测类别）\n\n    # 让 x 轴的标签 旋转 45°，避免重叠，提高可读性\n    plt.setp(ax.get_xticklabels(), rotation=45, ha=\"right\",\n             rotation_mode=\"anchor\")\n\n    # 根据混淆矩阵的值添加文本注释\n    fmt = '.2f' if normalize else 'd'   # 选择格式：归一化时保留两位小数，否则整数\n    thresh = cm.max() / 2.              # 设定颜色阈值\n    for i in range(cm.shape[0]):\n        for j in range(cm.shape[1]):\n            ax.text(j, i, format(cm[i, j], fmt),# 在每个方格内写入数值\n                    ha=\"center\", va=\"center\",\n                    color=\"white\" if cm[i, j] > thresh else \"black\") # 颜色对比提高可读性\n    # 调整图形布局\n    fig.tight_layout()\n    return ax","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:01:50.062706Z","iopub.execute_input":"2025-05-04T02:01:50.062958Z","iopub.status.idle":"2025-05-04T02:01:50.074252Z","shell.execute_reply.started":"2025-05-04T02:01:50.062933Z","shell.execute_reply":"2025-05-04T02:01:50.073288Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 创建一个回调函数，用于在每个epoch结束时计算精确率、召回率和F1分数\nclass MetricsCallback(Callback):\n    def __init__(self, validation_data):\n        super(Callback, self).__init__()\n        self.X_val = validation_data[0]  # 验证集的输入数据\n        self.Y_val = validation_data[1]  # 验证集的标签数据\n        self.precision_history = []  # 存储每个epoch的精确率\n        self.recall_history = []  # 存储每个epoch的召回率\n        self.f1_history = []  # 存储每个epoch的F1分数\n\n    def on_epoch_end(self, epoch, logs={}):\n        # 使用模型对验证集进行预测\n        Y_pred = self.model.predict(self.X_val)\n        # 将预测结果和验证集标签转换为类别索引\n        Y_pred_hot = np.argmax(Y_pred, axis=1)\n        Y_val_hot = np.argmax(self.Y_val, axis=1)\n\n        # 计算精确率、召回率和F1分数\n        precision = precision_score(Y_val_hot, Y_pred_hot, average='weighted', zero_division=1)\n        recall = recall_score(Y_val_hot, Y_pred_hot, average='weighted', zero_division=1)\n        f1 = f1_score(Y_val_hot, Y_pred_hot, average='weighted', zero_division=1)\n\n        # 将计算结果添加到历史记录中\n        self.precision_history.append(precision)\n        self.recall_history.append(recall)\n        self.f1_history.append(f1)\n\n        # 打印当前epoch的精确率、召回率和F1分数\n        print(f\"Epoch {epoch+1}, Precision: {precision:.4f}, Recall: {recall:.4f}, F1 Score: {f1:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:01:50.075093Z","iopub.execute_input":"2025-05-04T02:01:50.075378Z","iopub.status.idle":"2025-05-04T02:01:50.090462Z","shell.execute_reply.started":"2025-05-04T02:01:50.075336Z","shell.execute_reply":"2025-05-04T02:01:50.089674Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 定义一个函数，用于加载原始图像数据并进行预处理，包括调整图像大小和归一化。\ndef load_raw_images_df(data_frame, filenamecol, labelcol, img_size, n_classes):\n    n_images = len(data_frame)    # 获取数据集中的图像数量\n    X = np.empty((n_images, img_size, img_size, 3))    # 获取数据集中的图像数量\n    Y = np.zeros((n_images, n_classes))    # 初始化标签数据数组\n    for index, entry in data_frame.iterrows():\n        Y[index, entry[labelcol]] = 1        # 对标签进行 one-hot 编码\n        img = cv2.imread(entry[filenamecol])        # 读取图像并调整大小\n        if img is not None:\n            try:\n                X[index, :] = cv2.resize(img, (img_size, img_size))\n                # 对图像进行归一化处理\n                X[index, :] = X[index, :] / 255.0\n            except Exception as e:\n                print(f\"缩放图像 {entry[filenamecol]} 时出错: {e}\")\n        else:\n            print(f\"无法读取图像: {entry[filenamecol]}\")\n    return X, Y","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:01:50.091487Z","iopub.execute_input":"2025-05-04T02:01:50.091717Z","iopub.status.idle":"2025-05-04T02:01:50.104345Z","shell.execute_reply.started":"2025-05-04T02:01:50.091693Z","shell.execute_reply":"2025-05-04T02:01:50.103608Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"batch_size = 32  # 设定每个 mini-batch 训练 32 张图片\nimg_size = 224   # 设定所有输入图像大小为 224x224 像素","metadata":{"execution":{"iopub.status.busy":"2025-05-04T02:01:50.105200Z","iopub.execute_input":"2025-05-04T02:01:50.105448Z","iopub.status.idle":"2025-05-04T02:01:50.119935Z","shell.execute_reply.started":"2025-05-04T02:01:50.105424Z","shell.execute_reply":"2025-05-04T02:01:50.119150Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 读取训练集的 CSV 文件\ntrain_raw_data = pd.read_csv(\"../input/aptos2019-blindness-detection/train.csv\")\n# 生成图像文件的完整路径\ntrain_raw_data[\"filename\"] = train_raw_data[\"id_code\"].map(lambda x: os.path.join(\"../input/aptos2019-blindness-detection/train_images\", x + \".png\"))\n# 绘制训练集标签的直方图，查看类别分布\ntrain_raw_data.diagnosis.hist()\n\n# 查看 train_raw_data 各列的数据类型，检查是否有错误的类型\nprint(train_raw_data.dtypes)\n\n# 查看训练数据的前几行\nprint(train_raw_data.head())\n\n# 查看训练数据中 'diagnosis' 列的唯一值\nprint(train_raw_data.diagnosis.unique())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:01:50.120897Z","iopub.execute_input":"2025-05-04T02:01:50.121223Z","iopub.status.idle":"2025-05-04T02:01:50.412422Z","shell.execute_reply.started":"2025-05-04T02:01:50.121198Z","shell.execute_reply":"2025-05-04T02:01:50.411581Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 定义标签的标题映射\nlabel_title = {\"0\": \"No DR\", \"1\": \"Mild\", \"2\": \"Moderate\", \"3\": \"Severe\", \"4\": \"Proliferative DR\"}\n# 定义类别标签列表\nclass_labels = [\"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:01:50.413723Z","iopub.execute_input":"2025-05-04T02:01:50.413983Z","iopub.status.idle":"2025-05-04T02:01:50.418218Z","shell.execute_reply.started":"2025-05-04T02:01:50.413957Z","shell.execute_reply":"2025-05-04T02:01:50.417348Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 显示训练数据集中前 10 张图像，并在每张图像上标注其对应的疾病标签\nfigure, ax = plt.subplots(5, 2, figsize=(10, 12))\nax = ax.flatten()\nfor i, row in train_raw_data.iloc[0:10, :].iterrows():\n    # 读取图像并转换为 RGB 颜色格式\n    img = cv2.imread(os.path.join(\"../input/aptos2019-blindness-detection/train_images\", row[\"id_code\"] + \".png\"))\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 解决颜色异常问题\n    \n    ax[i].imshow(img)\n    ax[i].set_title(label_title[str(row[\"diagnosis\"])], fontsize=12, pad=10)  # 调整标题间距\n    ax[i].axis(\"off\")  # 隐藏坐标轴\n\n    plt.tight_layout()  # 自动调整子图间距","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:01:50.419514Z","iopub.execute_input":"2025-05-04T02:01:50.420484Z","iopub.status.idle":"2025-05-04T02:02:01.406559Z","shell.execute_reply.started":"2025-05-04T02:01:50.420444Z","shell.execute_reply":"2025-05-04T02:02:01.405664Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 将训练集划分为训练集和验证集\ntrain_df, val_df = train_test_split(train_raw_data, random_state=42, shuffle=True, test_size=0.333)\n# 重置训练集的索引\ntrain_df.reset_index(drop=True, inplace=True)\n# 重置验证集的索引\nval_df.reset_index(drop=True, inplace=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:02:01.407448Z","iopub.execute_input":"2025-05-04T02:02:01.407694Z","iopub.status.idle":"2025-05-04T02:02:01.415677Z","shell.execute_reply.started":"2025-05-04T02:02:01.407670Z","shell.execute_reply":"2025-05-04T02:02:01.414642Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 加载训练集的图像数据和标签\nX_train, Y_train = load_raw_images_df(train_df, \"filename\", \"diagnosis\", img_size, 5)\n# 加载验证集的图像数据和标签\nX_val, Y_val = load_raw_images_df(val_df, \"filename\", \"diagnosis\", img_size, 5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:02:01.416710Z","iopub.execute_input":"2025-05-04T02:02:01.416981Z","iopub.status.idle":"2025-05-04T02:07:44.315258Z","shell.execute_reply.started":"2025-05-04T02:02:01.416955Z","shell.execute_reply":"2025-05-04T02:07:44.314203Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 计算每个类别的权重，以处理类别不平衡问题。\n# 获取训练集标签的索引\nY_train_labels = np.argmax(Y_train, axis=1)\n\n# 计算每个类别的权重，处理类别不平衡问题\nclass_weights = compute_class_weight(class_weight='balanced', \n                                     classes=np.unique(Y_train_labels), \n                                     y=Y_train_labels)\n\n# 将类别权重转换为字典形式\ncls_wt_dict = dict(enumerate(class_weights))\nprint(cls_wt_dict)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:07:44.318731Z","iopub.execute_input":"2025-05-04T02:07:44.319095Z","iopub.status.idle":"2025-05-04T02:07:44.327709Z","shell.execute_reply.started":"2025-05-04T02:07:44.319066Z","shell.execute_reply":"2025-05-04T02:07:44.326937Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 创建一个图像数据增强器（ImageDataGenerator），用于对训练图像进行增强，包括随机缩放、填充、水平翻转和垂直翻转。\ndatagen = ImageDataGenerator(\n    zoom_range=0.15,  # 设置随机缩放的范围，图像会在 [1 - zoom_range, 1 + zoom_range] 的范围内随机缩放\n    fill_mode='constant',  # 设置填充新创建像素的方法，这里使用常数值填充\n    cval=0.,  # 当 fill_mode='constant' 时，用于填充的常数值\n    horizontal_flip=True,  # 是否随机水平翻转图像\n    vertical_flip=True,  # 是否随机垂直翻转图像\n)\n\n# 创建一个 MixupGenerator 实例，用于生成混合训练数据\n# Mixup 是一种数据增强方法，通过线性插值混合两个图像及其标签来生成新的训练样本\ntraining_generator = MixupGenerator(\n    X_train,  # 训练图像数据\n    Y_train,  # 训练标签数据\n    batch_size=batch_size,  # 每个批次的大小\n    alpha=0.2,  # Mixup 的超参数，用于控制混合权重的分布（从 Beta 分布中采样）\n    datagen=datagen  # 使用上述定义的 ImageDataGenerator 进行图像增强\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:07:44.328701Z","iopub.execute_input":"2025-05-04T02:07:44.328940Z","iopub.status.idle":"2025-05-04T02:07:44.340492Z","shell.execute_reply.started":"2025-05-04T02:07:44.328916Z","shell.execute_reply":"2025-05-04T02:07:44.339532Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 创建一个加权的分类交叉熵损失函数。在多分类问题里，不同类别的样本数量可能不均衡，使用加权损失函数能让模型更关注样本数量少的类别。\ndef weighted_categorical_crossentropy(weights):\n    \"\"\"\n    加权的分类交叉熵损失函数\n    :param weights: 类别权重，字典形式 {类别索引: 权重}\n    :return: 加权损失函数\n    \"\"\"\n    def loss(y_true, y_pred):\n        # 将权重转换为张量\n        weights_tensor = tf.constant([weights[i] for i in range(len(weights))], dtype=tf.float32)\n        # 计算标准分类交叉熵\n        loss = tf.keras.losses.categorical_crossentropy(y_true, y_pred)\n        # 应用权重\n        weighted_loss = tf.reduce_mean(loss * tf.reduce_sum(weights_tensor * y_true, axis=-1))\n        return weighted_loss\n    return loss","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:07:44.341494Z","iopub.execute_input":"2025-05-04T02:07:44.341788Z","iopub.status.idle":"2025-05-04T02:07:44.357165Z","shell.execute_reply.started":"2025-05-04T02:07:44.341761Z","shell.execute_reply":"2025-05-04T02:07:44.356340Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 构建模型：使用预训练的 DenseNet121 模型作为特征提取器，并在其顶部添加自定义的全连接层和输出层。\ndef buildModel():\n# include_top=False：不使用原始 DenseNet121 的全连接分类层，只保留卷积层部分（用于特征提取）。\n# weights=None：不使用 Keras 自带的预训练权重，而是手动加载 DenseNet-BC-121-32-no-top.h5。\n# input_tensor=keras.layers.Input(shape=(img_size, img_size, 3))：输入图像大小为 (img_size, img_size, 3)（RGB彩色图像）。\n    DenseNet121_model = DenseNet121(include_top=False,weights=None,input_tensor=keras.layers.Input(shape=(img_size,img_size,3)))\n    DenseNet121_model.load_weights('../input/densenet-keras/DenseNet-BC-121-32-no-top.h5')   \n    \n    # 添加一个全局平均池化层，减少模型参数，保留特征信息，提高泛化能力\n    p  = keras.layers.GlobalAveragePooling2D()(DenseNet121_model.output)\n\n    # 添加256个神经元的全连接层，ReLU激活函数。L2 正则化，防止过拟合。\n    d11 = keras.layers.Dense(units = 256, activation = 'relu',kernel_regularizer= keras.regularizers.l2(0.0001))(p)\n\n    # 添加Dropout层，丢弃50%的节点\n    dropout = keras.layers.Dropout(0.5)(d11)  \n\n    # 输出层，5个神经元，对应 5 种糖尿病视网膜病变的分类，使用 Softmax 计算概率。\n    o1 = keras.layers.Dense(units = 5, activation = 'softmax')(d11)\n\n    # 创建模型\n    model = keras.models.Model(inputs = DenseNet121_model.input,outputs = o1,name=\"DenseNet121Model\")\n    \n    # 定义 SGD 优化器通，过不断调整模型的参数，让模型的预测结果更接近真实值，从而最小化损失函数\n    sgd = SGD(learning_rate=0.01, momentum=0.9, nesterov=True)\n    # learning_rate=0.01\t学习率，控制每次梯度更新的步长\n    # momentum=0.9\t动量参数，提高稳定性，减少振荡\n    # nesterov=True\t使用 Nesterov 动量，加速收敛，避免局部最优\n\n    # 编译模型、由于是 多分类问题（5 类），使用 交叉熵损失。衡量模型性能，选择 准确率（accuracy）\n    model.compile(optimizer=sgd,\n                  loss=weighted_categorical_crossentropy(cls_wt_dict), \n                  metrics = ['accuracy'])\n    print(model.summary())# 输出模型的层结构\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:07:44.358133Z","iopub.execute_input":"2025-05-04T02:07:44.358411Z","iopub.status.idle":"2025-05-04T02:07:44.369489Z","shell.execute_reply.started":"2025-05-04T02:07:44.358380Z","shell.execute_reply":"2025-05-04T02:07:44.368608Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 调用 buildModel 函数构建模型\n# buildModel 函数会创建一个基于预训练的 DenseNet121 模型，并在顶部添加自定义的全连接层和输出层\n# 返回的 mymodel 是一个 Keras 模型实例，可以用于后续的训练和评估\nmymodel = buildModel()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:07:44.370611Z","iopub.execute_input":"2025-05-04T02:07:44.370941Z","iopub.status.idle":"2025-05-04T02:07:48.047024Z","shell.execute_reply.started":"2025-05-04T02:07:44.370903Z","shell.execute_reply":"2025-05-04T02:07:48.046204Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 设置训练参数和回调函数：\n\n# 设置训练的总轮数（epochs）\nEPOCHS = 50\n\n# 创建一个 EarlyStopping 回调，用于在验证集性能不再提升时提前停止训练\n# 参数 patience=10 表示如果验证集的性能在连续 10 个 epochs 内没有提升，则停止训练\nearlystop = keras.callbacks.EarlyStopping(patience=10)\n\n# 创建一个 ReduceLROnPlateau 回调，用于在验证集性能不再提升时降低学习率\n# 参数 monitor='val_acc' 表示监控验证集的准确率\n# 参数 patience=2 表示如果验证集的准确率在连续 2 个 epochs 内没有提升，则降低学习率\n# 参数 factor=0.5 表示每次降低学习率时，将其乘以 0.5\n# 参数 min_lr=0.00001 表示学习率的最小值，防止学习率过低\nlearning_rate_reduction = keras.callbacks.ReduceLROnPlateau(\n    monitor='val_accuracy', \n    patience=2, \n    verbose=1, \n    factor=0.5, \n    min_lr=0.00001\n)\n\n# 创建一个 ModelCheckpoint回调，用于在每个 epoch 结束时保存模型权重\n# 参数 monitor='val_loss' 表示监控验证集的损失\n# 参数verbose=1 表示在训练过程中输出相关信息\n# 参数 save_best_only=True 表示只保存验证集损失最低的模型权重\n# 参数 mode='min' 表示监控的指标是越小越好（对于损失函数）\n# 参数 save_weights_only=True 表示只保存模型的权重，不保存整个模型结构\ncheckpoint = ModelCheckpoint(\n    '../working/DenseNet121.weights.h5',  # 文件路径必须以 .weights.h5 结尾\n    monitor='val_loss',\n    verbose=1,\n    save_best_only=True,\n    mode='min',\n    save_weights_only=True\n)\n\n# 创建一个自定义的 QWKCallback 回调，用于在每个 epoch 结束时计算验证集的二次加权 kappa（QWK）分数\n# 参数 (X_val, Y_val) 表示验证集的输入和标签\nqwk = QWKCallback((X_val, Y_val))\n\n# 实例化MetricsCallback对象，传入验证集数据\nmetrics_callback = MetricsCallback((X_val, Y_val))\n\n# 将所有回调函数存储在一个列表中，以便在训练过程中使用\nmycallbacks = [earlystop, learning_rate_reduction, checkpoint, qwk,metrics_callback]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:07:48.048262Z","iopub.execute_input":"2025-05-04T02:07:48.048584Z","iopub.status.idle":"2025-05-04T02:07:48.054915Z","shell.execute_reply.started":"2025-05-04T02:07:48.048551Z","shell.execute_reply":"2025-05-04T02:07:48.054035Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#查看 QWKCallback 对象的属性和状态，方便调试和确认回调函数是否正确初始化batch\nprint(qwk)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:07:48.055981Z","iopub.execute_input":"2025-05-04T02:07:48.056226Z","iopub.status.idle":"2025-05-04T02:07:48.071999Z","shell.execute_reply.started":"2025-05-04T02:07:48.056194Z","shell.execute_reply":"2025-05-04T02:07:48.070946Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 预热模型，预热阶段的目的是让模型在训练初期快速适应数据，特别是当类别不平衡时，类别权重可以帮助模型更好地学习\nEPOCHS = 10  # 设置预热阶段的训练轮数为 10\n\n# 使用模型的 fit 方法进行训练\nhistory = mymodel.fit(\n    training_generator,  # 训练数据生成器，用于在每个 epoch 中生成训练数据批次\n    epochs=EPOCHS,  # 训练的总轮数\n    validation_data=(X_val, Y_val),  # 验证集数据，用于在每个 epoch 结束时评估模型性能\n    # validation_steps=len(X_val) // batch_size,  # 每个 epoch 的验证步数\n    verbose=2,  # 设置日志输出的详细程度，2 表示每个 epoch 输出一行日志\n    callbacks=mycallbacks  # 使用的回调函数列表，例如早停、学习率调整、模型保存等\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:07:48.072968Z","iopub.execute_input":"2025-05-04T02:07:48.073176Z","iopub.status.idle":"2025-05-04T02:14:13.096094Z","shell.execute_reply.started":"2025-05-04T02:07:48.073154Z","shell.execute_reply":"2025-05-04T02:14:13.095362Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 设置正式训练阶段的总轮数为 50 个 epochs\nEPOCHS = 50\n\n# 调用模型的 fit 方法进行正式训练\n# fit 是 Keras 中用于训练模型的方法，适用于使用生成器（如 MixupGenerator）生成数据的情况\nhistory = mymodel.fit(\n    training_generator,  # 使用 MixupGenerator 生成训练数据，该生成器会在每个批次中生成混合的训练样本\n    epochs=EPOCHS,  # 训练的总轮数\n    validation_data=(X_val, Y_val),  # 验证集数据，用于在每个 epoch 结束时评估模型性能\n    verbose=2,  # 设置日志输出的详细程度，2 表示每个 epoch 输出一行日志\n    callbacks=mycallbacks  # 使用之前定义的回调函数列表，包括早停、学习率调整、模型保存和 QWK 计算   \n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:14:13.098102Z","iopub.execute_input":"2025-05-04T02:14:13.098498Z","iopub.status.idle":"2025-05-04T02:24:26.715951Z","shell.execute_reply.started":"2025-05-04T02:14:13.098456Z","shell.execute_reply":"2025-05-04T02:24:26.715230Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 保存模型的权重到指定的文件中\n# 使用 Keras 的 save_weights 方法将模型的权重保存为 HDF5 文件\n# 这样可以在后续的训练或预测中重新加载这些权重，而无需重新训练整个模型\n# 文件名 \"model.h5\" 表示保存的权重文件将存储在当前工作目录下\nmymodel.save_weights(\"model.weights.h5\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:24:26.717581Z","iopub.execute_input":"2025-05-04T02:24:26.717967Z","iopub.status.idle":"2025-05-04T02:24:27.526215Z","shell.execute_reply.started":"2025-05-04T02:24:26.717925Z","shell.execute_reply":"2025-05-04T02:24:27.525371Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 保存模型\nmymodel.save(\"mymodel.keras\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:24:27.527680Z","iopub.execute_input":"2025-05-04T02:24:27.528024Z","iopub.status.idle":"2025-05-04T02:24:29.130080Z","shell.execute_reply.started":"2025-05-04T02:24:27.527983Z","shell.execute_reply":"2025-05-04T02:24:29.129182Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 使用训练好的模型对验证集数据进行预测\n# predict_on_batch 方法用于对一个批次的数据进行预测\n# X_val 是验证集的输入数据，模型将对这些数据进行前向传播，生成预测结果\n# 预测结果通常是一个概率分布，表示每个样本属于每个类别的概率\n# 这些预测结果将存储在 Y_val_pred 变量中，后续可以用于评估模型性能或进一步分析\nY_val_pred = mymodel.predict_on_batch(X_val)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:24:29.131376Z","iopub.execute_input":"2025-05-04T02:24:29.131743Z","iopub.status.idle":"2025-05-04T02:25:56.361919Z","shell.execute_reply.started":"2025-05-04T02:24:29.131704Z","shell.execute_reply":"2025-05-04T02:25:56.361145Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 将预测结果（概率分布）转换为预测标签\n# np.argmax 函数用于获取每个样本预测概率最高的类别索引\n# axis=1 表示沿着每个样本的类别维度进行操作\n# Y_val_pred 是模型对验证集的预测结果，形状为 (num_samples, num_classes)\n# Y_val_pred_hot 是预测的类别标签，形状为 (num_samples,)\nY_val_pred_hot = np.argmax(Y_val_pred, axis=1)\n\n# 将验证集的真实标签（One-Hot 编码）转换为类别标签\n# Y_val 是验证集的真实标签，形状为 (num_samples, num_classes)\n# Y_val_actual_hot 是真实类别标签，形状为 (num_samples,)\nY_val_actual_hot = np.argmax(Y_val, axis=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:25:56.363120Z","iopub.execute_input":"2025-05-04T02:25:56.363822Z","iopub.status.idle":"2025-05-04T02:25:56.368773Z","shell.execute_reply.started":"2025-05-04T02:25:56.363779Z","shell.execute_reply":"2025-05-04T02:25:56.367942Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 使用自定义的 plot_confusion_matrix 函数绘制验证集的真实标签和预测标签之间的混淆矩阵\nplot_confusion_matrix(Y_val_actual_hot, Y_val_pred_hot, np.array(class_labels))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:25:56.370036Z","iopub.execute_input":"2025-05-04T02:25:56.370667Z","iopub.status.idle":"2025-05-04T02:25:56.660747Z","shell.execute_reply.started":"2025-05-04T02:25:56.370627Z","shell.execute_reply":"2025-05-04T02:25:56.659676Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 创建一个包含两个子图的图形，用于绘制训练和验证的损失和准确率曲线\nfig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 12))\n\n# 在第一个子图（ax1）中绘制训练和验证的损失曲线\nax1.plot(history.history['loss'], color='b', label=\"Training loss\")  # 训练损失\nax1.plot(history.history['val_loss'], color='r', label=\"Validation loss\")  # 验证损失\nax1.set_title(\"Loss Curves\")  # 添加子图标题\nax1.set_xlabel(\"Epochs\")  # 添加x轴标签\nax1.set_ylabel(\"Loss\")  # 添加y轴标签\n# ax1.set_ylim([0.5, 1])  # 设置y轴的范围为0.5到1\nax1.legend(loc='best', shadow=True)  # 添加图例\n\n# 在第二个子图（ax2）中绘制训练和验证的准确率曲线\nax2.plot(history.history['accuracy'], color='b', label=\"Training accuracy\")  # 训练准确率\nax2.plot(history.history['val_accuracy'], color='r', label=\"Validation accuracy\")  # 验证准确率\nax2.set_title(\"Accuracy Curves\")  # 添加子图标题\nax2.set_xlabel(\"Epochs\")  # 添加x轴标签\nax2.set_ylabel(\"Accuracy\")  # 添加y轴标签\n# ax2.set_ylim([0.5, 1])  # 设置y轴的范围为0.5到1\nax2.legend(loc='best', shadow=True)  # 添加图例\n\n# 调整布局\nplt.tight_layout()\n\n# 显示图形\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:29:12.155766Z","iopub.execute_input":"2025-05-04T02:29:12.156554Z","iopub.status.idle":"2025-05-04T02:29:12.596870Z","shell.execute_reply.started":"2025-05-04T02:29:12.156512Z","shell.execute_reply":"2025-05-04T02:29:12.596008Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 绘制 QWK 分数的折线图\nplt.figure(figsize=(10, 5))\nplt.plot(qwk.history, marker='o', linestyle='-', color='b')\nplt.title('QWK Score over Epochs')\nplt.xlabel('Epochs')\nplt.ylabel('QWK Score')\nplt.grid(True)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:25:57.082642Z","iopub.execute_input":"2025-05-04T02:25:57.082889Z","iopub.status.idle":"2025-05-04T02:25:57.323656Z","shell.execute_reply.started":"2025-05-04T02:25:57.082864Z","shell.execute_reply":"2025-05-04T02:25:57.322784Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 绘制精确率、召回率和F1分数的折线图\nplt.figure(figsize=(15, 5))\n\n# 绘制精确率折线图\nplt.subplot(1, 3, 1)\nplt.plot(metrics_callback.precision_history, marker='o', linestyle='-', color='b')\nplt.title('Precision over Epochs')  # 图表标题\nplt.xlabel('Epochs')  # x轴标签\nplt.ylabel('Precision')  # y轴标签\nplt.grid(True)  # 显示网格\n\n# 绘制召回率折线图\nplt.subplot(1, 3, 2)\nplt.plot(metrics_callback.recall_history, marker='o', linestyle='-', color='r')\nplt.title('Recall over Epochs')  # 图表标题\nplt.xlabel('Epochs')  # x轴标签\nplt.ylabel('Recall')  # y轴标签\nplt.grid(True)  # 显示网格\n\n# 绘制F1分数折线图\nplt.subplot(1, 3, 3)\nplt.plot(metrics_callback.f1_history, marker='o', linestyle='-', color='g')\nplt.title('F1 Score over Epochs')  # 图表标题\nplt.xlabel('Epochs')  # x轴标签\nplt.ylabel('F1 Score')  # y轴标签\nplt.grid(True)  # 显示网格\n\n# 调整布局并显示图表\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:25:57.324823Z","iopub.execute_input":"2025-05-04T02:25:57.325142Z","iopub.status.idle":"2025-05-04T02:25:57.965531Z","shell.execute_reply.started":"2025-05-04T02:25:57.325113Z","shell.execute_reply":"2025-05-04T02:25:57.964617Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 将标签二值化\nY_val_binarized = label_binarize(Y_val_actual_hot, classes=[0, 1, 2, 3, 4])\nn_classes = Y_val_binarized.shape[1]\n\n# 计算每个类别的ROC曲线和AUC值\nfpr = dict()\ntpr = dict()\nroc_auc = dict()\nfor i in range(n_classes):\n    fpr[i], tpr[i], _ = roc_curve(Y_val_binarized[:, i], Y_val_pred[:, i])\n    roc_auc[i] = auc(fpr[i], tpr[i])\n\n# 绘制ROC曲线\nplt.figure()\nfor i in range(n_classes):\n    plt.plot(fpr[i], tpr[i], label=f'Class {i} (AUC = {roc_auc[i]:.2f})')\nplt.plot([0, 1], [0, 1], 'k--')\nplt.xlim([0.0, 1.0])\nplt.ylim([0.0, 1.05])\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.title('Receiver Operating Characteristic (ROC) Curve')\nplt.legend(loc=\"lower right\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:25:57.966643Z","iopub.execute_input":"2025-05-04T02:25:57.966919Z","iopub.status.idle":"2025-05-04T02:25:58.159548Z","shell.execute_reply.started":"2025-05-04T02:25:57.966892Z","shell.execute_reply":"2025-05-04T02:25:58.158746Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 载入模型\nmodel = tf.keras.models.load_model(\"mymodel.keras\",custom_objects={'loss': weighted_categorical_crossentropy(cls_wt_dict)})  \n\n# 选择最后一个卷积层的名称（Grad-CAM 需要用到）\nlast_conv_layer_name = \"conv5_block16_concat\"  # DenseNet121 的最后一个卷积层\n\n# 读取图像并进行预处理\ndef preprocess_image(img_path, img_size=224):\n    img = image.load_img(img_path, target_size=(img_size, img_size))\n    img_array = image.img_to_array(img)\n    img_array = np.expand_dims(img_array, axis=0)\n    img_array /= 255.0  # 归一化\n    return img_array, img\n\n# 计算 Grad-CAM 热力图\ndef make_gradcam_heatmap(img_array, model, last_conv_layer_name):\n    grad_model = Model(inputs=model.input, \n                       outputs=[model.get_layer(last_conv_layer_name).output, model.output])\n\n    with tf.GradientTape() as tape:\n        conv_outputs, predictions = grad_model(img_array)\n        class_index = np.argmax(predictions[0])  # 获取模型预测的类别索引\n        loss = predictions[:, class_index]  # 关注预测类别的损失\n\n    # 计算梯度\n    grads = tape.gradient(loss, conv_outputs)\n\n    # 计算权重均值\n    pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))\n\n    # 生成 Grad-CAM 结果\n    conv_outputs = conv_outputs[0]\n    heatmap = np.dot(conv_outputs, pooled_grads.numpy().T)\n\n    # 归一化热力图\n    heatmap = np.maximum(heatmap, 0)\n    heatmap /= np.max(heatmap)\n    \n    return heatmap\n\n# 在原始图像上叠加 Grad-CAM\ndef overlay_heatmap(img, heatmap, alpha=0.4):\n    heatmap = cv2.resize(heatmap, (img.size[0], img.size[1]))\n    heatmap = np.uint8(255 * heatmap)\n\n    heatmap_colored = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)  # 伪彩色映射\n    heatmap_colored = cv2.cvtColor(heatmap_colored, cv2.COLOR_BGR2RGB)\n\n    superimposed_img = np.array(img) * (1 - alpha) + heatmap_colored * alpha\n    superimposed_img = np.clip(superimposed_img, 0, 255).astype(\"uint8\")\n    \n    return superimposed_img\n\n# 处理多张图片\ndef process_images(img_paths, num_images=10):\n    for i, img_path in enumerate(img_paths[:num_images]):\n        img_array, img = preprocess_image(img_path)\n        heatmap = make_gradcam_heatmap(img_array, model, last_conv_layer_name)\n        result_img = overlay_heatmap(img, heatmap)\n\n        # 显示结果\n        plt.figure(figsize=(10, 5))\n        plt.subplot(1, 2, 1)\n        plt.imshow(img)\n        plt.title(\"Original Image\")\n        plt.axis(\"off\")\n\n        plt.subplot(1, 2, 2)\n        plt.imshow(result_img)\n        plt.title(\"Grad-CAM Heatmap\")\n        plt.axis(\"off\")\n\n        plt.show()\n\nimage_dir = \"/kaggle/input/aptos2019-blindness-detection/test_images\"\nimage_paths = [os.path.join(image_dir, img_name) for img_name in os.listdir(image_dir)]\nprocess_images(image_paths, num_images=10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:25:58.160805Z","iopub.execute_input":"2025-05-04T02:25:58.161153Z","iopub.status.idle":"2025-05-04T02:26:17.899851Z","shell.execute_reply.started":"2025-05-04T02:25:58.161113Z","shell.execute_reply":"2025-05-04T02:26:17.899031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 加载测试数据集\ntest_data = pd.read_csv(\"../input/aptos2019-blindness-detection/test.csv\")\n\n# 为测试数据添加文件名列\ntest_data[\"filename\"] = test_data[\"id_code\"].map(lambda x: x + \".png\")\n\n# 查看测试数据的前几行\ntest_data.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:26:17.901184Z","iopub.execute_input":"2025-05-04T02:26:17.901555Z","iopub.status.idle":"2025-05-04T02:26:17.920892Z","shell.execute_reply.started":"2025-05-04T02:26:17.901516Z","shell.execute_reply":"2025-05-04T02:26:17.920169Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 创建一个测试数据生成器\ntest_gen = ImageDataGenerator(rescale=1./255)  # 对图像进行归一化处理，将像素值缩放到 [0, 1] 范围\n\n# 使用 flow_from_dataframe 方法从 Pandas DataFrame 中加载测试数据\ntest_generator = test_gen.flow_from_dataframe(\n    dataframe=test_data,  # 指定包含测试数据信息的 Pandas DataFrame\n    directory=\"../input/aptos2019-blindness-detection/test_images\",  # 指定包含测试图像的文件夹路径\n    x_col=\"filename\",  # 指定 DataFrame 中包含图像文件名的列名\n    y_col=None,  # 测试数据没有标签，因此 y_col 设置为 None\n    target_size=(img_size, img_size),  # 指定图像的大小，所有图像将被调整到这个大小\n    batch_size=1,  # 指定每个批次的大小为 1，即每次处理一张图像\n    shuffle=False,  # 测试时不需要打乱数据\n    class_mode=None  # 测试数据没有标签，因此 class_mode 设置为 None\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:26:17.921751Z","iopub.execute_input":"2025-05-04T02:26:17.922007Z","iopub.status.idle":"2025-05-04T02:26:22.630213Z","shell.execute_reply.started":"2025-05-04T02:26:17.921982Z","shell.execute_reply":"2025-05-04T02:26:22.629379Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 使用模型对测试数据进行预测\npredictions = mymodel.predict(\n    test_generator,  # 测试数据生成器\n    steps=len(test_generator.filenames) // test_generator.batch_size  # 每个 epoch 的预测步数\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:26:22.631434Z","iopub.execute_input":"2025-05-04T02:26:22.632143Z","iopub.status.idle":"2025-05-04T02:27:55.897191Z","shell.execute_reply.started":"2025-05-04T02:26:22.632095Z","shell.execute_reply":"2025-05-04T02:27:55.896438Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 获取测试生成器中的文件名列表\nfilenames = test_generator.filenames\n# 将预测结果（概率分布）转换为预测标签\nY_pred_hot = np.argmax(predictions, axis=1)\n\n# 创建一个 DataFrame，包含文件名和预测结果\nresults = pd.DataFrame({\n    \"id_code\": filenames,  # 文件名\n    \"diagnosis\": Y_pred_hot # 预测结果的类别索引\n})\n\n# 定义标签的标题映射\nlabel_title = {\"0\": \"No DR\", \"1\": \"Mild\", \"2\": \"Moderate\", \"3\": \"Severe\", \"4\": \"Proliferative DR\"}\n\n# 将预测结果的数值转换为对应的病变程度\nresults['diagnosis'] = results['diagnosis'].map(lambda x: label_title[str(x)])\n\n# 去掉文件名的后缀（.png）\nresults['id_code'] = results['id_code'].map(lambda x: str(x)[:-4])\n\n# 将结果保存到 CSV 文件中\nresults.to_csv(\"预测结果.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:27:55.898572Z","iopub.execute_input":"2025-05-04T02:27:55.898932Z","iopub.status.idle":"2025-05-04T02:27:55.912431Z","shell.execute_reply.started":"2025-05-04T02:27:55.898892Z","shell.execute_reply":"2025-05-04T02:27:55.911602Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 绘制预测类别的直方图\nresults.diagnosis.hist()\n\n# 列出所有唯一的预测类别\nresults.diagnosis.unique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-04T02:27:55.913550Z","iopub.execute_input":"2025-05-04T02:27:55.914361Z","iopub.status.idle":"2025-05-04T02:27:56.046731Z","shell.execute_reply.started":"2025-05-04T02:27:55.914318Z","shell.execute_reply":"2025-05-04T02:27:56.045721Z"}},"outputs":[],"execution_count":null}]}