{"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":"none","dataSources":[{"sourceId":3362,"databundleVersionId":31148,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-28T17:18:22.018383Z","iopub.execute_input":"2025-11-28T17:18:22.018738Z","iopub.status.idle":"2025-11-28T17:18:22.026732Z","shell.execute_reply.started":"2025-11-28T17:18:22.018715Z","shell.execute_reply":"2025-11-28T17:18:22.025917Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport zipfile\nimport random\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\n# ==========================================\n# 1. 环境配置与数据解压\n# ==========================================\n\n# 设置随机种子，保证结果可复现\ndef set_seed(seed_value=42):\n    random.seed(seed_value)\n    np.random.seed(seed_value)\n    torch.manual_seed(seed_value)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed_all(seed_value)\n\nset_seed(42)\n\n# 检查设备\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"当前使用的计算设备: {device}\")\n\n# 定义路径\n# Kaggle 输入数据通常在 /kaggle/input 下，是只读的\n# 我们需要将其解压到 /kaggle/working/ 下\ninput_zip_path = '/kaggle/input/dogs-vs-cats/train.zip'\nworking_dir = '/kaggle/working/train_extracted'\ntrain_dir = os.path.join(working_dir, 'train')\n\n# 解压数据\nif not os.path.exists(train_dir):\n    print(\"正在解压数据，请稍候...\")\n    with zipfile.ZipFile(input_zip_path, 'r') as zip_ref:\n        zip_ref.extractall(working_dir)\n    print(\"数据解压完成！\")\nelse:\n    print(\"检测到数据已解压，跳过解压步骤。\")\n\n# 获取所有图片文件列表\nall_files = os.listdir(train_dir)\nimage_files = [f for f in all_files if f.endswith('.jpg')]\nprint(f\"图片总数: {len(image_files)}\")\n\n# ==========================================\n# 2. 数据预处理与加载 (Dataset & DataLoader)\n# ==========================================\n\n# 定义图像转换：调整大小 -> 转张量 -> 归一化\ntransform = transforms.Compose([\n    transforms.Resize((128, 128)),  # 统一调整为 128x128\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# 自定义数据集类\nclass CatDogDataset(Dataset):\n    def __init__(self, file_list, root_dir, transform=None):\n        self.file_list = file_list\n        self.root_dir = root_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.file_list)\n\n    def __getitem__(self, idx):\n        img_name = self.file_list[idx]\n        img_path = os.path.join(self.root_dir, img_name)\n        \n        # 打开图片\n        image = Image.open(img_path)\n        \n        # 根据文件名解析标签: dog.x.jpg -> 1, cat.x.jpg -> 0\n        if 'dog' in img_name:\n            label = 1\n        else:\n            label = 0\n        \n        # 应用预处理\n        if self.transform:\n            image = self.transform(image)\n            \n        return image, label\n\n# 划分训练集 (80%) 和验证集 (20%)\nrandom.shuffle(image_files)\nsplit_idx = int(len(image_files) * 0.8)\ntrain_files = image_files[:split_idx]\nval_files = image_files[split_idx:]\n\n# 创建数据集实例\ntrain_dataset = CatDogDataset(train_files, train_dir, transform=transform)\nval_dataset = CatDogDataset(val_files, train_dir, transform=transform)\n\n# 创建数据加载器\nbatch_size = 64\ntrain_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\nval_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)\n\nprint(f\"训练集数量: {len(train_dataset)}, 验证集数量: {len(val_dataset)}\")\n\n# ==========================================\n# 3. 建立分类模型 (CNN)\n# ==========================================\n\nclass SimpleCNN(nn.Module):\n    def __init__(self):\n        super(SimpleCNN, self).__init__()\n        \n        # 特征提取层\n        self.features = nn.Sequential(\n            # Block 1\n            nn.Conv2d(3, 32, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(kernel_size=2, stride=2), # 128 -> 64\n            \n            # Block 2\n            nn.Conv2d(32, 64, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(kernel_size=2, stride=2), # 64 -> 32\n            \n            # Block 3\n            nn.Conv2d(64, 128, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(kernel_size=2, stride=2), # 32 -> 16\n        )\n        \n        # 分类器层\n        self.classifier = nn.Sequential(\n            nn.Flatten(),\n            nn.Linear(128 * 16 * 16, 512),\n            nn.ReLU(),\n            nn.Dropout(0.5), # 防止过拟合\n            nn.Linear(512, 2) # 输出2类：猫, 狗\n        )\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.classifier(x)\n        return x\n\nmodel = SimpleCNN().to(device)\n# print(model) # 如果需要查看模型结构可取消注释\n\n# ==========================================\n# 4. 模型训练与参数调优\n# ==========================================\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(model.parameters(), lr=0.001)\n\n# 初始化 history 字典，防止 NameError\nhistory = {\n    'train_loss': [], 'train_acc': [],\n    'val_loss': [], 'val_acc': []\n}\n\ndef train_process(num_epochs=10):\n    print(f\"开始训练，共 {num_epochs} 轮...\")\n    \n    for epoch in range(num_epochs):\n        # --- 训练阶段 ---\n        model.train()\n        running_loss = 0.0\n        correct_train = 0\n        total_train = 0\n        \n        for i, (images, labels) in enumerate(train_loader):\n            images, labels = images.to(device), labels.to(device)\n            \n            optimizer.zero_grad()       # 清空梯度\n            outputs = model(images)     # 前向传播\n            loss = criterion(outputs, labels) # 计算损失\n            loss.backward()             # 反向传播\n            optimizer.step()            # 更新参数\n            \n            running_loss += loss.item()\n            _, predicted = torch.max(outputs.data, 1)\n            total_train += labels.size(0)\n            correct_train += (predicted == labels).sum().item()\n            \n        avg_train_loss = running_loss / len(train_loader)\n        train_acc = 100 * correct_train / total_train\n        \n        # --- 验证阶段 ---\n        model.eval()\n        val_loss = 0.0\n        correct_val = 0\n        total_val = 0\n        \n        with torch.no_grad():\n            for images, labels in val_loader:\n                images, labels = images.to(device), labels.to(device)\n                outputs = model(images)\n                loss = criterion(outputs, labels)\n                \n                val_loss += loss.item()\n                _, predicted = torch.max(outputs.data, 1)\n                total_val += labels.size(0)\n                correct_val += (predicted == labels).sum().item()\n        \n        avg_val_loss = val_loss / len(val_loader)\n        val_acc = 100 * correct_val / total_val\n        \n        # 记录数据\n        history['train_loss'].append(avg_train_loss)\n        history['train_acc'].append(train_acc)\n        history['val_loss'].append(avg_val_loss)\n        history['val_acc'].append(val_acc)\n        \n        print(f\"Epoch [{epoch+1}/{num_epochs}] | \"\n              f\"Train Loss: {avg_train_loss:.4f} Acc: {train_acc:.2f}% | \"\n              f\"Val Loss: {avg_val_loss:.4f} Acc: {val_acc:.2f}%\")\n\n# 执行训练\n# Kaggle GPU 环境下建议设为 10-15\ntrain_process(num_epochs=10)\n\n# ==========================================\n# 5. 结果分析与可视化\n# ==========================================\n\n# 绘制 Loss 和 Accuracy 曲线\ndef plot_results(hist):\n    epochs_range = range(1, len(hist['train_loss']) + 1)\n    \n    plt.figure(figsize=(14, 5))\n    \n    # Loss 曲线\n    plt.subplot(1, 2, 1)\n    plt.plot(epochs_range, hist['train_loss'], label='Train Loss')\n    plt.plot(epochs_range, hist['val_loss'], label='Validation Loss')\n    plt.title('Loss Curve')\n    plt.xlabel('Epochs')\n    plt.ylabel('Loss')\n    plt.legend()\n    plt.grid(True)\n    \n    # Accuracy 曲线\n    plt.subplot(1, 2, 2)\n    plt.plot(epochs_range, hist['train_acc'], label='Train Acc')\n    plt.plot(epochs_range, hist['val_acc'], label='Validation Acc')\n    plt.title('Accuracy Curve')\n    plt.xlabel('Epochs')\n    plt.ylabel('Accuracy (%)')\n    plt.legend()\n    plt.grid(True)\n    \n    plt.show()\n\n# 预测展示函数\ndef show_predictions(loader, count=5):\n    model.eval()\n    images_shown = 0\n    \n    plt.figure(figsize=(15, 4))\n    \n    # 获取一个 batch 的数据\n    data_iter = iter(loader)\n    images, labels = next(data_iter)\n    images, labels = images.to(device), labels.to(device)\n    \n    with torch.no_grad():\n        outputs = model(images)\n        _, preds = torch.max(outputs, 1)\n    \n    # 反归一化参数用于显示\n    mean = torch.tensor([0.485, 0.456, 0.406]).to(device).view(3, 1, 1)\n    std = torch.tensor([0.229, 0.224, 0.225]).to(device).view(3, 1, 1)\n    \n    for i in range(count):\n        img = images[i]\n        # 反归一化\n        img = img * std + mean\n        img = torch.clamp(img, 0, 1)\n        # 转换为 numpy (H, W, C)\n        img_np = img.cpu().permute(1, 2, 0).numpy()\n        \n        true_label = \"Dog\" if labels[i].item() == 1 else \"Cat\"\n        pred_label = \"Dog\" if preds[i].item() == 1 else \"Cat\"\n        color = 'green' if true_label == pred_label else 'red'\n        \n        plt.subplot(1, count, i+1)\n        plt.imshow(img_np)\n        plt.title(f\"True: {true_label}\\nPred: {pred_label}\", color=color)\n        plt.axis('off')\n        \n    plt.show()\n\nprint(\"\\n正在绘制训练曲线...\")\nplot_results(history)\n\nprint(\"\\n正在展示验证集预测结果...\")\nshow_predictions(val_loader, count=6)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-28T17:18:22.102948Z","iopub.execute_input":"2025-11-28T17:18:22.103258Z","iopub.status.idle":"2025-11-28T19:08:50.736039Z","shell.execute_reply.started":"2025-11-28T17:18:22.103234Z","shell.execute_reply":"2025-11-28T19:08:50.735146Z"}},"outputs":[],"execution_count":null}]}