{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":101538,"databundleVersionId":12239568,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":11824059,"sourceType":"datasetVersion","datasetId":7427556}],"dockerImageVersionId":31041,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport glob\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import f1_score\nimport random\nfrom sklearn.metrics import f1_score\n\n\nseed = 42\nrandom.seed(seed)\nnp.random.seed(seed)\ntorch.manual_seed(seed)\ntorch.cuda.manual_seed_all(seed)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\ndata_dir = r\"/kaggle/input/multioutputclassificationdatatrain/multi-output-classification data\"\n\n\n# 构造标签映射\n\nclasses = sorted([d for d in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, d))])\ncolors = sorted({name.split('_')[0] for name in classes})\nitems  = sorted({name.split('_')[1] for name in classes})\ncolor_to_idx = {c: i for i, c in enumerate(colors)}\nitem_to_idx  = {g: i for i, g in enumerate(items)}\n\n\n# 读取图片\n\nimage_paths, color_labels, item_labels = [], [], []\nfor cls in classes:\n    c, it = cls.split('_')\n    for ext in (\"*.jpg\",\"*.jpeg\",\"*.png\",\"*.bmp\",\"*.tiff\",\"*.gif\"):\n        for p in glob.glob(os.path.join(data_dir, cls, ext)):\n            image_paths.append(p)\n            color_labels.append(color_to_idx[c])\n            item_labels.append(item_to_idx[it])\n\ncolor_labels = np.array(color_labels, dtype=np.int64)\nitem_labels  = np.array(item_labels,  dtype=np.int64)\n\n\n\ntrain_imgs, val_imgs, train_c_lbls, val_c_lbls, train_i_lbls, val_i_lbls = train_test_split(\n    image_paths, color_labels, item_labels,\n    test_size=0.2, random_state=seed, stratify=color_labels\n)\n\n\nclass MultiLabelDataset(Dataset):\n    def __init__(self, img_paths, c_lbls, i_lbls, transform=None):\n        self.img_paths = img_paths\n        self.c_lbls = c_lbls\n        self.i_lbls = i_lbls\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.img_paths)\n\n    def __getitem__(self, idx):\n        img = Image.open(self.img_paths[idx]).convert('RGB')\n        if self.transform:\n            img = self.transform(img)\n        return img, self.c_lbls[idx], self.i_lbls[idx]\n\n\ntrain_tf = transforms.Compose([\n    transforms.RandomResizedCrop(224, scale=(0.8, 1.0), ratio=(0.75, 1.33)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(15),\n    transforms.ColorJitter(0.1, 0.1, 0.1, 0.1),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                         [0.229, 0.224, 0.225]),\n])\n\nval_tf = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]),\n])\n\ntrain_ds = MultiLabelDataset(train_imgs, train_c_lbls, train_i_lbls, transform=train_tf)\nval_ds   = MultiLabelDataset(val_imgs,   val_c_lbls,   val_i_lbls,   transform=val_tf)\ntrain_loader = DataLoader(train_ds, batch_size=32, shuffle=True,  num_workers=4)\nval_loader   = DataLoader(val_ds,   batch_size=32, shuffle=False, num_workers=4)\n\n\n# 模型\n\nbackbone = models.resnet50(pretrained=True)\nin_feats = backbone.fc.in_features\nbackbone.fc = nn.Identity()\n\ncolor_head = nn.Linear(in_feats, len(colors))\nitem_head  = nn.Linear(in_feats, len(items))\nmodel = nn.Sequential(backbone).to(device)\ncolor_head = color_head.to(device)\nitem_head  = item_head.to(device)\n\n\ncriterion_c = nn.CrossEntropyLoss()\ncriterion_i = nn.CrossEntropyLoss()\noptimizer = optim.Adam(list(backbone.parameters()) + \n                       list(color_head.parameters()) + \n                       list(item_head.parameters()), lr=1e-4)\n\n\n# 训练\n\ndef train_one_epoch():\n    backbone.train(); color_head.train(); item_head.train()\n    running_loss = 0\n    all_c_preds, all_c_tgts = [], []\n    all_i_preds, all_i_tgts = [], []\n    for imgs, c_lbl, i_lbl in train_loader:\n        imgs, c_lbl, i_lbl = imgs.to(device), c_lbl.to(device), i_lbl.to(device)\n        optimizer.zero_grad()\n        feats = backbone(imgs)\n        c_logits = color_head(feats)\n        i_logits = item_head(feats)\n        loss = criterion_c(c_logits, c_lbl) + criterion_i(i_logits, i_lbl)\n        loss.backward(); optimizer.step()\n        running_loss += loss.item() * imgs.size(0)\n\n        # 预测\n        cp = torch.argmax(c_logits, dim=1).cpu().numpy()\n        ip = torch.argmax(i_logits, dim=1).cpu().numpy()\n        all_c_preds.append(cp); all_c_tgts.append(c_lbl.cpu().numpy())\n        all_i_preds.append(ip); all_i_tgts.append(i_lbl.cpu().numpy())\n\n    # 平均损失\n    epoch_loss = running_loss / len(train_ds)\n    # 展平\n    all_c_preds = np.hstack(all_c_preds)\n    all_c_tgts  = np.hstack(all_c_tgts)\n    all_i_preds = np.hstack(all_i_preds)\n    all_i_tgts  = np.hstack(all_i_tgts)\n    # 分别计算 F1，平均\n    f1_c = f1_score(all_c_tgts, all_c_preds, average='micro')\n    f1_i = f1_score(all_i_tgts, all_i_preds, average='micro')\n    micro_f1 = 0.5 * (f1_c + f1_i)\n    return epoch_loss, micro_f1\n\ndef validate_one_epoch():\n    backbone.eval(); color_head.eval(); item_head.eval()\n    running_loss = 0\n    all_c_preds, all_c_tgts = [], []\n    all_i_preds, all_i_tgts = [], []\n    with torch.no_grad():\n        for imgs, c_lbl, i_lbl in val_loader:\n            imgs, c_lbl, i_lbl = imgs.to(device), c_lbl.to(device), i_lbl.to(device)\n            feats = backbone(imgs)\n            c_logits = color_head(feats)\n            i_logits = item_head(feats)\n            loss = criterion_c(c_logits, c_lbl) + criterion_i(i_logits, i_lbl)\n            running_loss += loss.item() * imgs.size(0)\n\n            cp = torch.argmax(c_logits, dim=1).cpu().numpy()\n            ip = torch.argmax(i_logits, dim=1).cpu().numpy()\n            all_c_preds.append(cp); all_c_tgts.append(c_lbl.cpu().numpy())\n            all_i_preds.append(ip); all_i_tgts.append(i_lbl.cpu().numpy())\n\n    epoch_loss = running_loss / len(val_ds)\n    all_c_preds = np.hstack(all_c_preds)\n    all_c_tgts  = np.hstack(all_c_tgts)\n    all_i_preds = np.hstack(all_i_preds)\n    all_i_tgts  = np.hstack(all_i_tgts)\n    f1_c = f1_score(all_c_tgts, all_c_preds, average='micro')\n    f1_i = f1_score(all_i_tgts, all_i_preds, average='micro')\n    micro_f1 = 0.5 * (f1_c + f1_i)\n    return epoch_loss, micro_f1\n\n\nnum_epochs = 25\nbest_f1 = 0\nfor epoch in range(1, num_epochs+1):\n    tr_loss, tr_f1 = train_one_epoch()\n    val_loss, val_f1 = validate_one_epoch()\n    print(f\"Epoch {epoch}/{num_epochs}\"\n          f\" | Train Loss: {tr_loss:.4f}, Train F1: {tr_f1:.4f}\"\n          f\" | Val   Loss: {val_loss:.4f}, Val   F1: {val_f1:.4f}\")\n    if val_f1 > best_f1:\n        best_f1 = val_f1\n        torch.save({\n            'backbone': backbone.state_dict(),\n            'color_head': color_head.state_dict(),\n            'item_head': item_head.state_dict()\n        }, \"best_model.pth\")\n\nprint(\"训练结束，最佳验证 F1:\", best_f1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-18T12:46:51.282969Z","iopub.execute_input":"2025-05-18T12:46:51.283298Z","iopub.status.idle":"2025-05-18T12:56:14.851157Z","shell.execute_reply.started":"2025-05-18T12:46:51.283266Z","shell.execute_reply":"2025-05-18T12:56:14.850203Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# 10. 推理\n\n\nckpt = torch.load(\"best_model.pth\", map_location=device)\nbackbone.load_state_dict(ckpt['backbone'])\ncolor_head.load_state_dict(ckpt['color_head'])\nitem_head.load_state_dict(ckpt['item_head'])\nbackbone.eval(); color_head.eval(); item_head.eval()\n\n\ntest_data_dir = r\"/kaggle/input/buct-multi-classification/npy_val\"\ntest_images = [p for p in glob.glob(os.path.join(test_data_dir, \"*.npy\")) if os.path.isfile(p)]\nclass TestDataset(Dataset):\n    def __init__(self, paths, transform=None):\n        self.paths = paths\n        self.transform = transform\n        # ImageNet 归一化参数\n        self.mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)\n        self.std  = np.array([0.229, 0.224, 0.225], dtype=np.float32)\n\n    def __len__(self):\n        return len(self.paths)\n\n    def __getitem__(self, idx):\n        # 1. 加载 numpy 数组 (H, W, 3)，值在归一化后的近似 [-2, +2.6] 范围\n        arr = np.load(self.paths[idx]).astype(np.float32)\n        # 2. 反归一化：从 (x - mean)/std 还原到 [0,1]\n        img_np = arr * self.std + self.mean\n        # 限制范围在 [0,1]\n        img_np = np.clip(img_np, 0.0, 1.0)\n        # 3. 扩展到 [0,255] 并转为 uint8\n        img_uint8 = (img_np * 255).astype(np.uint8)\n        # 4. PIL 转换\n        img = Image.fromarray(img_uint8)\n        # 5. 再做 Resize/ToTensor/Normalize\n        if self.transform:\n            img = self.transform(img)\n\n        # 返回图像张量和 ID（去掉扩展名）\n        img_id = os.path.splitext(os.path.basename(self.paths[idx]))[0]\n        return img, img_id\n\n\ntest_tf = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],\n                         [0.229,0.224,0.225]),\n])\ntest_ds = TestDataset(test_images, transform=test_tf)\ntest_loader = DataLoader(test_ds, batch_size=32, shuffle=False, num_workers=4)\n\n# 推理\nresults = []\nwith torch.no_grad():\n    for imgs, paths in test_loader:\n        imgs = imgs.to(device)\n        feats = backbone(imgs)\n        c_logits = color_head(feats)\n        i_logits = item_head(feats)\n        c_probs = torch.softmax(c_logits, dim=1).cpu().numpy()\n        i_probs = torch.softmax(i_logits, dim=1).cpu().numpy()\n        c_idx = np.argmax(c_probs, axis=1)\n        i_idx = np.argmax(i_probs, axis=1)\n        for p, ci, ii in zip(paths, c_idx, i_idx):\n            fid = os.path.splitext(os.path.basename(p))[0]\n            label = f\"{colors[ci]}_{items[ii]}\"\n            results.append((fid, label))\n\ndf = pd.DataFrame(results, columns=[\"ID\",\"Label\"])\ndf.to_csv(\"submission.csv\", index=False)\nprint(\"已生成 submission.csv，前5行示例：\")\nprint(df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-18T12:56:14.852831Z","iopub.execute_input":"2025-05-18T12:56:14.853401Z","iopub.status.idle":"2025-05-18T12:57:03.58407Z","shell.execute_reply.started":"2025-05-18T12:56:14.853375Z","shell.execute_reply":"2025-05-18T12:57:03.583375Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\n# 重新定义 TestDataset\nclass TestDataset(Dataset):\n    def __init__(self, paths, transform=None):\n        self.paths = paths\n        self.transform = transform\n        # ImageNet 归一化参数\n        self.mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)\n        self.std  = np.array([0.229, 0.224, 0.225], dtype=np.float32)\n\n    def __len__(self):\n        return len(self.paths)\n\n    def __getitem__(self, idx):\n        # 1. 加载 numpy 数组 (H, W, 3)，值在归一化后的近似 [-2, +2.6] 范围\n        arr = np.load(self.paths[idx]).astype(np.float32)\n        # 2. 反归一化：从 (x - mean)/std 还原到 [0,1]\n        img_np = arr * self.std + self.mean\n        # 限制范围在 [0,1]\n        img_np = np.clip(img_np, 0.0, 1.0)\n        # 3. 扩展到 [0,255] 并转为 uint8\n        img_uint8 = (img_np * 255).astype(np.uint8)\n        # 4. PIL 转换\n        img = Image.fromarray(img_uint8)\n        # 5. 再做 Resize/ToTensor/Normalize\n        if self.transform:\n            img = self.transform(img)\n\n        # 返回图像张量和 ID（去掉扩展名）\n        img_id = os.path.splitext(os.path.basename(self.paths[idx]))[0]\n        return img, img_id\n\n# 获取前 4 个样本路径\ntest_data_dir = \"/kaggle/input/buct-multi-classification/npy_val\"\ntest_images = [p for p in glob.glob(os.path.join(test_data_dir, \"*.npy\")) if os.path.isfile(p)]\nsample_paths = test_images[:5]\n\n# 可视化原始图像（不做任何 transform）\nraw_ds = TestDataset(sample_paths, transform=None)\n\nplt.figure(figsize=(12, 6))\nfor i in range(len(raw_ds)):\n    img, path = raw_ds[i]\n    plt.subplot(1, 5, i+1)\n    plt.imshow(np.array(img))\n    plt.axis('off')\n    plt.title(os.path.basename(path))\nplt.show()\n\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-18T12:57:03.585079Z","iopub.execute_input":"2025-05-18T12:57:03.585744Z","iopub.status.idle":"2025-05-18T12:57:06.715552Z","shell.execute_reply.started":"2025-05-18T12:57:03.585714Z","shell.execute_reply":"2025-05-18T12:57:06.714855Z"}},"outputs":[],"execution_count":null}]}