{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":5441,"databundleVersionId":38425,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# --- CELL 1: 库导入与环境设置 ---","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.applications import ResNet50V2\nfrom tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\nimport os\nimport zipfile\n\n# 检查 GPU\nprint(\"GPU Available: \", tf.config.list_physical_devices('GPU'))\n\n# 定义关键参数 (Top 30% 的关键设置)\nIMAGE_SIZE = (224, 224)  # ResNet 喜欢这个尺寸\nBATCH_SIZE = 32\nEPOCHS_HEAD = 5          # 第一阶段轮数\nEPOCHS_FINE = 10         # 第二阶段微调轮数","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T08:48:02.441288Z","iopub.execute_input":"2025-12-03T08:48:02.441547Z","iopub.status.idle":"2025-12-03T08:48:20.266813Z","shell.execute_reply.started":"2025-12-03T08:48:02.441525Z","shell.execute_reply":"2025-12-03T08:48:20.266136Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 2: 解压数据  ---","metadata":{}},{"cell_type":"code","source":"print(\"📦 正在解压数据，请稍候...\")\n\nwith zipfile.ZipFile(\"/kaggle/input/dogs-vs-cats-redux-kernels-edition/train.zip\",\"r\") as z:\n    z.extractall(\".\")\n    \nwith zipfile.ZipFile(\"/kaggle/input/dogs-vs-cats-redux-kernels-edition/test.zip\",\"r\") as z:\n    z.extractall(\".\")\n\nprint(\"✅ 解压完成！\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T08:48:20.268210Z","iopub.execute_input":"2025-12-03T08:48:20.269071Z","iopub.status.idle":"2025-12-03T08:48:32.325291Z","shell.execute_reply.started":"2025-12-03T08:48:20.269036Z","shell.execute_reply":"2025-12-03T08:48:32.324500Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 3: 整理数据列表 ---","metadata":{}},{"cell_type":"code","source":"filenames = os.listdir(\"./train\")\ncategories = []\nfor filename in filenames:\n    category = filename.split('.')[0]\n    if category == 'dog':\n        categories.append(1) # 1 = Dog\n    else:\n        categories.append(0) # 0 = Cat\n\ndf = pd.DataFrame({\n    'filename': filenames,\n    'category': categories\n})\n#把类别转成字符串，因为 flow_from_dataframe 需要字符串\ndf['category'] = df['category'].replace({0: 'cat', 1: 'dog'}) \n\nprint(df.head())\nprint(f\"总图片数: {df.shape[0]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T08:48:32.326242Z","iopub.execute_input":"2025-12-03T08:48:32.327092Z","iopub.status.idle":"2025-12-03T08:48:32.371664Z","shell.execute_reply.started":"2025-12-03T08:48:32.327064Z","shell.execute_reply":"2025-12-03T08:48:32.370845Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 4: 数据增强生成器  ---","metadata":{}},{"cell_type":"code","source":"train_datagen = ImageDataGenerator(\n    rotation_range=15,\n    rescale=1./255,        # 归一化\n    shear_range=0.1,\n    zoom_range=0.2,\n    horizontal_flip=True,\n    width_shift_range=0.1,\n    height_shift_range=0.1,\n    validation_split=0.1   # 划分 10% 做验证\n)\n\n# 验证集不需要增强，只需要归一化\nvalidation_datagen = ImageDataGenerator(rescale=1./255, validation_split=0.1)\n\n# 构建训练集流\ntrain_generator = train_datagen.flow_from_dataframe(\n    df, \n    \"./train\", \n    x_col='filename',\n    y_col='category',\n    target_size=IMAGE_SIZE,\n    class_mode='categorical',\n    batch_size=BATCH_SIZE,\n    subset='training'\n)\n\n# 构建验证集流\nvalidation_generator = validation_datagen.flow_from_dataframe(\n    df, \n    \"./train\", \n    x_col='filename',\n    y_col='category',\n    target_size=IMAGE_SIZE,\n    class_mode='categorical',\n    batch_size=BATCH_SIZE,\n    subset='validation'\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T08:48:32.373438Z","iopub.execute_input":"2025-12-03T08:48:32.373919Z","iopub.status.idle":"2025-12-03T08:48:32.692008Z","shell.execute_reply.started":"2025-12-03T08:48:32.373894Z","shell.execute_reply":"2025-12-03T08:48:32.691398Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 5: 构建 ResNet50V2 模型 ---","metadata":{}},{"cell_type":"code","source":"print(\"🔹 构建模型中...\")\n\n# 1. 加载预训练的 ResNet50V2 \nbase_model = ResNet50V2(weights='imagenet', include_top=False, input_shape=(224, 224, 3))\n\n# 2. 冻结基础模型 (第一阶段不训练骨干)\nbase_model.trainable = False\n\n# 3. 添加自定义分类头\nx = base_model.output\nx = GlobalAveragePooling2D()(x)\nx = Dense(256, activation='relu')(x)\nx = Dropout(0.5)(x) # 防止过拟合\npredictions = Dense(2, activation='softmax')(x)\n\nmodel = Model(inputs=base_model.input, outputs=predictions)\n\n# 4. 编译模型\nmodel.compile(optimizer=Adam(learning_rate=1e-3), loss='categorical_crossentropy', metrics=['accuracy'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T08:48:32.692702Z","iopub.execute_input":"2025-12-03T08:48:32.693001Z","iopub.status.idle":"2025-12-03T08:48:36.089370Z","shell.execute_reply.started":"2025-12-03T08:48:32.692960Z","shell.execute_reply":"2025-12-03T08:48:36.088765Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 6: 第一阶段训练  ---","metadata":{}},{"cell_type":"code","source":"print(\"🚀 阶段一：训练分类头 (Warm-up)...\")\nhistory_1 = model.fit(\n    train_generator,\n    epochs=EPOCHS_HEAD,\n    validation_data=validation_generator,\n    validation_steps=len(validation_generator),\n    steps_per_epoch=len(train_generator)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T08:48:36.090157Z","iopub.execute_input":"2025-12-03T08:48:36.090420Z","iopub.status.idle":"2025-12-03T09:09:05.736235Z","shell.execute_reply.started":"2025-12-03T08:48:36.090395Z","shell.execute_reply":"2025-12-03T09:09:05.735631Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 7: 第二阶段训练 (微调) ---","metadata":{}},{"cell_type":"code","source":"print(\"阶段二：解冻顶层并微调 (Fine-tuning)...\")\n\n# 1. 设置解冻策略\nbase_model.trainable = True\n\n#先冻结所有层，然后只解冻最后 30 层\nfor layer in base_model.layers[:-30]: \n    layer.trainable = False\n\n# 2. 重新编译 (微调必须用很小的学习率)\nmodel.compile(optimizer=Adam(learning_rate=1e-5), loss='categorical_crossentropy', metrics=['accuracy'])\n\n# 3. 设置早停 \ncallbacks = [\n    EarlyStopping(patience=2, restore_best_weights=True, monitor='val_accuracy')\n]\n\n# 4. 训练 \nhistory_2 = model.fit(\n    train_generator,\n    epochs=5, \n    validation_data=validation_generator,\n    validation_steps=len(validation_generator),\n    steps_per_epoch=len(train_generator),\n    callbacks=callbacks\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T09:09:05.737266Z","iopub.execute_input":"2025-12-03T09:09:05.737497Z","iopub.status.idle":"2025-12-03T09:29:47.936487Z","shell.execute_reply.started":"2025-12-03T09:09:05.737477Z","shell.execute_reply":"2025-12-03T09:29:47.935879Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 8: 可视化训练曲线 ---","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# 主要看第二阶段（微调）的曲线，这才是模型真正变强的地方\nacc = history_2.history['accuracy']\nval_acc = history_2.history['val_accuracy']\nloss = history_2.history['loss']\nval_loss = history_2.history['val_loss']\n\nepochs_range = range(len(acc))\n\nplt.figure(figsize=(15, 5))\nplt.subplot(1, 2, 1)\nplt.plot(epochs_range, acc, label='Training Accuracy')\nplt.plot(epochs_range, val_acc, label='Validation Accuracy')\nplt.legend(loc='lower right')\nplt.title('Training and Validation Accuracy')\n\nplt.subplot(1, 2, 2)\nplt.plot(epochs_range, loss, label='Training Loss')\nplt.plot(epochs_range, val_loss, label='Validation Loss')\nplt.legend(loc='upper right')\nplt.title('Training and Validation Loss')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T09:29:47.937523Z","iopub.execute_input":"2025-12-03T09:29:47.937799Z","iopub.status.idle":"2025-12-03T09:29:48.297095Z","shell.execute_reply.started":"2025-12-03T09:29:47.937769Z","shell.execute_reply":"2025-12-03T09:29:48.296315Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 9: 补充可视化混淆矩阵 ---","metadata":{}},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix, classification_report\nimport seaborn as sns\n\n# 1. 让验证集生成器重置，并预测一遍\nvalidation_generator.reset()\n# 预测\nprint(\"正在对验证集进行评估...\")\nval_preds = model.predict(validation_generator, verbose=1)\nval_pred_classes = np.argmax(val_preds, axis=1)\n\n# 2. 获取真实标签\nval_true_classes = validation_generator.classes\n\n# 3. 画图\ncm = confusion_matrix(val_true_classes, val_pred_classes)\nplt.figure(figsize=(6, 5))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Cat', 'Dog'], yticklabels=['Cat', 'Dog'])\nplt.xlabel('Predicted')\nplt.ylabel('True')\nplt.title('Confusion Matrix (ResNet50V2)')\nplt.show()\n\n# 打印报告\nprint(classification_report(val_true_classes, val_pred_classes, target_names=['Cat', 'Dog']))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T09:29:48.297892Z","iopub.execute_input":"2025-12-03T09:29:48.298197Z","iopub.status.idle":"2025-12-03T09:30:01.408668Z","shell.execute_reply.started":"2025-12-03T09:29:48.298178Z","shell.execute_reply":"2025-12-03T09:30:01.407965Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 10: 可视化Grad-CAM 热力图 ---","metadata":{}},{"cell_type":"code","source":"import matplotlib.cm as mpl_cm\n\ndef make_gradcam_heatmap(img_array, model, last_conv_layer_name, pred_index=None):\n    # 构建专门用于Grad-CAM的子模型\n    grad_model = tf.keras.models.Model(\n        inputs=[model.inputs],\n        outputs=[model.get_layer(last_conv_layer_name).output, model.output]\n    )\n    with tf.GradientTape() as tape:\n        last_conv_layer_output, preds = grad_model(img_array)\n        if pred_index is None:\n            pred_index = tf.argmax(preds[0])\n        class_channel = preds[:, pred_index]\n    \n    grads = tape.gradient(class_channel, last_conv_layer_output)\n    pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))\n    last_conv_layer_output = last_conv_layer_output[0]\n    heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis]\n    heatmap = tf.squeeze(heatmap)\n    heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)\n    return heatmap.numpy()\n\ndef display_gradcam(img, heatmap, alpha=0.6):\n    jet = mpl_cm.get_cmap(\"jet\")\n    jet_colors = jet(np.arange(256))[:, :3]\n    heatmap_uint8 = np.uint8(255 * heatmap)\n    jet_heatmap = jet_colors[heatmap_uint8]\n    \n    # 调整大小\n    jet_heatmap = tf.image.resize(jet_heatmap, (img.shape[0], img.shape[1]))\n    jet_heatmap = tf.keras.preprocessing.image.img_to_array(jet_heatmap)\n    \n    # 叠加\n    # 注意：这里的img已经是0-1归一化的，所以要乘255\n    superimposed_img = jet_heatmap * 255 * alpha + img * 255\n    superimposed_img = tf.keras.preprocessing.image.array_to_img(superimposed_img)\n    return superimposed_img\n\n# --- 执行可视化 ---\n# 获取 ResNet50V2 的最后一个卷积层名字\nlast_conv_layer_name = \"post_relu\" \n\nprint(\"正在生成热力图...\")\nvalidation_generator.reset()\n# 取出一批图片\nx_batch, y_batch = next(validation_generator)\n\nplt.figure(figsize=(15, 6))\nnum_images = 3\n\nfor i in range(num_images):\n    img = x_batch[i] # 这是一个 (224, 224, 3) 的图\n    img_array = np.expand_dims(img, axis=0) # 增加 batch 维度\n    \n    # 预测\n    preds = model.predict(img_array, verbose=0)\n    pred_label = \"Dog\" if np.argmax(preds)==1 else \"Cat\"\n    true_label = \"Dog\" if np.argmax(y_batch[i])==1 else \"Cat\"\n    conf = np.max(preds)\n    \n    # 生成热力图\n    try:\n        heatmap = make_gradcam_heatmap(img_array, model, last_conv_layer_name)\n        result_img = display_gradcam(img, heatmap)\n        \n        plt.subplot(1, num_images, i+1)\n        plt.imshow(result_img)\n        plt.title(f\"True: {true_label}\\nPred: {pred_label} ({conf:.1%})\")\n        plt.axis('off')\n    except Exception as e:\n        print(f\"热力图生成失败: {e}\")\n        # 如果 post_relu 找不到，尝试 conv5_block3_out\n        print(\"请尝试将 last_conv_layer_name 改为 'conv5_block3_out'\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T09:30:01.410344Z","iopub.execute_input":"2025-12-03T09:30:01.410720Z","iopub.status.idle":"2025-12-03T09:30:09.277650Z","shell.execute_reply.started":"2025-12-03T09:30:01.410702Z","shell.execute_reply":"2025-12-03T09:30:09.276870Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 11: 准备测试集并预测 ---","metadata":{}},{"cell_type":"code","source":"print(\" 正在预测测试集...\")\n\n# 准备测试集文件名\ntest_filenames = os.listdir(\"./test\")\ntest_df = pd.DataFrame({'filename': test_filenames})\nnb_samples = test_df.shape[0]\n\n# 测试集生成器 \ntest_gen = ImageDataGenerator(rescale=1./255)\ntest_generator = test_gen.flow_from_dataframe(\n    test_df, \n   \"./test\", \n    x_col='filename',\n    y_col=None,\n    class_mode=None,\n    target_size=IMAGE_SIZE,\n    batch_size=BATCH_SIZE,\n    shuffle=False\n)\n\n# 预测\npredict = model.predict(test_generator, steps=int(np.ceil(nb_samples/BATCH_SIZE)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T09:39:35.773572Z","iopub.execute_input":"2025-12-03T09:39:35.773894Z","iopub.status.idle":"2025-12-03T09:40:17.397750Z","shell.execute_reply.started":"2025-12-03T09:39:35.773868Z","shell.execute_reply":"2025-12-03T09:40:17.397185Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# --- CELL 12: 生成提交文件 ---","metadata":{}},{"cell_type":"code","source":"# 1. 提取预测结果\n\ntest_df['label'] = predict[:, 1] \n\n# 2. 提取数字 ID\ntest_df['id'] = test_df['filename'].str.split('.').str[0]\n\n# 3. 整理最终格式 \nsubmission_df = test_df[['id', 'label']].copy()\n\n# 4. 按 ID 排序 \nsubmission_df.sort_values(by=['id'], key=lambda x: x.astype(int), inplace=True)\n\n# 5. 保存 CSV\nsubmission_df.to_csv('submission.csv', index=False)\n\nprint(\" submission.csv已生成！\")\nprint(submission_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T09:47:14.007202Z","iopub.execute_input":"2025-12-03T09:47:14.007792Z","iopub.status.idle":"2025-12-03T09:47:14.047161Z","shell.execute_reply.started":"2025-12-03T09:47:14.007767Z","shell.execute_reply":"2025-12-03T09:47:14.046399Z"}},"outputs":[],"execution_count":null}]}