{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":99005,"databundleVersionId":12261202,"sourceType":"competition"}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ✅ 导入必要库\nimport os\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torchvision.transforms as transforms\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models\n\nfrom sklearn.model_selection import train_test_split","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ✅ 检查 GPU\nprint(\"GPU 可用:\", torch.cuda.is_available())\nif torch.cuda.is_available():\n    print(\"GPU 名称:\", torch.cuda.get_device_name(0))\n\n# ✅ 加载标签文件，保留原始编号（0~881）\nlabel_df = pd.read_csv(\"train_labels.csv\")\nlabel_df[\"class\"] = label_df[\"ID\"].apply(lambda x: x.split(\"/\")[1])\nclass_to_idx = label_df.groupby(\"class\")[\"Label\"].min().to_dict()\nidx_to_class = {v: k for k, v in class_to_idx.items()}\nnum_classes = label_df[\"Label\"].max() + 1\nprint(f\"共有类别: {len(class_to_idx)}，输出维度: {num_classes}\")\n\n# ✅ 打印映射表\nprint(\"📋 类别编号 ↔ 中药名：\")\nfor idx, name in sorted(idx_to_class.items()):\n    print(f\"{idx:>3} ↔ {name}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ✅ 自定义 Dataset\nclass MedicineDataset(Dataset):\n    def __init__(self, samples, transform=None):\n        self.samples = samples\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        img_path, label = self.samples[idx]\n        try:\n            image = Image.open(img_path)\n            if image.mode == 'P':\n                image = image.convert(\"RGBA\").convert(\"RGB\")\n            else:\n                image = image.convert(\"RGB\")\n            if self.transform:\n                image = self.transform(image)\n            return image, label\n        except:\n            return self.__getitem__((idx + 1) % len(self.samples))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class TestDataset(Dataset):\n    def __init__(self, root_dir, transform=None):\n        self.img_paths = [os.path.join(root_dir, fname) for fname in os.listdir(root_dir) if fname.endswith('.png')]\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.img_paths)\n\n    def __getitem__(self, idx):\n        path = self.img_paths[idx]\n        img = Image.open(path).convert(\"RGB\")\n        if self.transform:\n            img = self.transform(img)\n        return img, os.path.basename(path)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class EarlyStopping:\n    def __init__(self, patience=5, mode=\"max\"):\n        self.patience = patience\n        self.counter = 0\n        self.best_score = None\n        self.early_stop = False\n        self.mode = mode\n        self.best_weights = None\n\n    def __call__(self, val_score, model):\n        score = val_score if self.mode == \"max\" else -val_score\n\n        if self.best_score is None or score > self.best_score:\n            self.best_score = score\n            self.best_weights = model.state_dict()\n            self.counter = 0\n        else:\n            self.counter += 1\n            if self.counter >= self.patience:\n                self.early_stop = True","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\ndef rand_bbox(size, lam):\n    W = size[2]\n    H = size[3]\n    cut_rat = np.sqrt(1. - lam)\n    cut_w = int(W * cut_rat)\n    cut_h = int(H * cut_rat)\n    cx = np.random.randint(W)\n    cy = np.random.randint(H)\n    bbx1 = np.clip(cx - cut_w // 2, 0, W)\n    bby1 = np.clip(cy - cut_h // 2, 0, H)\n    bbx2 = np.clip(cx + cut_w // 2, 0, W)\n    bby2 = np.clip(cy + cut_h // 2, 0, H)\n    return bbx1, bby1, bbx2, bby2\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ✅ 图像增强\ntransform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ColorJitter(0.1, 0.1, 0.1, 0.05),\n    transforms.ToTensor(),\n    transforms.Normalize([0.5]*3, [0.5]*3)\n])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ✅ 加载图像路径并划分训练验证集\ntrain_root = 'train'\nall_samples = []\nfor cls in os.listdir(train_root):\n    cls_path = os.path.join(train_root, cls)\n    if not os.path.isdir(cls_path) or cls not in class_to_idx:\n        continue\n    for fname in os.listdir(cls_path):\n        all_samples.append((os.path.join(cls_path, fname), class_to_idx[cls]))\n\ntrain_samples, val_samples = train_test_split(\n    all_samples, test_size=0.1, stratify=[s[1] for s in all_samples], random_state=42)\n\ntrain_dataset = MedicineDataset(train_samples, transform=transform)\nval_dataset = MedicineDataset(val_samples, transform=transform)\n\ntrain_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=4)\nval_loader = DataLoader(val_dataset, batch_size=64, shuffle=False, num_workers=4)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ✅ Voting 路线训练三个模型（多 GPU 支持）\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"使用设备: {device}，GPU数量: {torch.cuda.device_count()}\")\n\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.1)\n\n\n# ---------- ConvNeXt ----------\nconvnext = models.convnext_tiny(weights='DEFAULT')\nconvnext.classifier = nn.Sequential(\n    nn.Flatten(),\n    nn.LayerNorm((768,), eps=1e-6),\n    nn.Linear(768, num_classes)\n)\nif torch.cuda.device_count() > 1:\n    print(\"✅ 使用多 GPU 训练 ConvNeXt\")\n    convnext = nn.DataParallel(convnext)\nconvnext = convnext.to(device)\noptimizer = torch.optim.Adam(convnext.parameters(), lr=1e-4)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)\n\n\nearly_stopper = EarlyStopping(patience=3, mode=\"max\")\nfor epoch in range(50):\n    convnext.train()\n    total_loss = 0\n    for images, labels in tqdm(train_loader, desc=f\"ConvNeXt Epoch {epoch+1}\"):\n        images, labels = images.to(device), labels.to(device)\n         # ✅ CutMix：对一批图像应用 CutMix 增强\n        lam = np.random.beta(1.0, 1.0)\n        rand_index = torch.randperm(images.size(0)).to(device)\n        target_a = labels\n        target_b = labels[rand_index]\n\n        bbx1, bby1, bbx2, bby2 = rand_bbox(images.size(), lam)\n        images[:, :, bbx1:bbx2, bby1:bby2] = images[rand_index, :, bbx1:bbx2, bby1:bby2]\n        lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (images.size(-1) * images.size(-2)))\n       \n        optimizer.zero_grad()\n        outputs = convnext(images)\n        loss = criterion(outputs, target_a) * lam + criterion(outputs, target_b) * (1. - lam)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n    scheduler.step()\n    print(f\"[ConvNeXt] Epoch {epoch+1} Loss: {total_loss / len(train_loader):.4f}\")\n\n    convnext.eval()\n    correct = 0\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = convnext(images)\n            _, preds = torch.max(outputs, 1)\n            correct += (preds == labels).sum().item()\n    acc = correct / len(val_loader.dataset)\n    print(f\"[ConvNeXt] ✅ 验证准确率: {acc:.4f}\")\n    # 检查是否早停\n    early_stopper(acc, convnext)\n    if early_stopper.early_stop:\n        print(\"⏹️ 早停触发，停止训练\")\n        break\n\n# 恢复最优模型参数\nconvnext.load_state_dict(early_stopper.best_weights)\n\ntorch.save(convnext.state_dict(), \"convnext.pth\")\nprint(\"✅ ConvNeXt 已保存\")\n\n\n# ---------- ResNet ----------\nresnet = models.resnet50(weights='DEFAULT')\nresnet.fc = nn.Linear(2048, num_classes)\nif torch.cuda.device_count() > 1:\n    print(\"✅ 使用多 GPU 训练 ResNet\")\n    resnet = nn.DataParallel(resnet)\nresnet = resnet.to(device)\noptimizer = torch.optim.Adam(resnet.parameters(), lr=1e-4)\n\n\n\nearly_stopper = EarlyStopping(patience=3, mode=\"max\")\nfor epoch in range(50):\n    resnet.train()\n    total_loss = 0\n    for images, labels in tqdm(train_loader, desc=f\"ResNet Epoch {epoch+1}\"):\n        images, labels = images.to(device), labels.to(device)\n        # ✅ CutMix：对一批图像应用 CutMix 增强\n        lam = np.random.beta(1.0, 1.0)\n        rand_index = torch.randperm(images.size(0)).to(device)\n        target_a = labels\n        target_b = labels[rand_index]\n\n        bbx1, bby1, bbx2, bby2 = rand_bbox(images.size(), lam)\n        images[:, :, bbx1:bbx2, bby1:bby2] = images[rand_index, :, bbx1:bbx2, bby1:bby2]\n        lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (images.size(-1) * images.size(-2)))\n       \n        \n        optimizer.zero_grad()\n        outputs = resnet(images)\n        loss = criterion(outputs, target_a) * lam + criterion(outputs, target_b) * (1. - lam)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n    print(f\"[ResNet] Epoch {epoch+1} Loss: {total_loss / len(train_loader):.4f}\")\n\n    resnet.eval()\n    correct = 0\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = resnet(images)\n            _, preds = torch.max(outputs, 1)\n            correct += (preds == labels).sum().item()\n    acc = correct / len(val_loader.dataset)\n    print(f\"[ResNet] ✅ 验证准确率: {acc:.4f}\")\n\n      # 检查是否早停\n    early_stopper(acc, resnet)\n    if early_stopper.early_stop:\n        print(\"⏹️ 早停触发，停止训练\")\n        break\n\n# 恢复最优模型参数\nresnet.load_state_dict(early_stopper.best_weights)\ntorch.save(resnet.state_dict(), \"resnet.pth\")\nprint(\"✅ ResNet50 已保存\")\n\n# ---------- EfficientNet ----------\nefficientnet = models.efficientnet_b0(weights='DEFAULT')\nefficientnet.classifier = nn.Linear(1280, num_classes)\nif torch.cuda.device_count() > 1:\n    print(\"✅ 使用多 GPU 训练 EfficientNet\")\n    efficientnet = nn.DataParallel(efficientnet)\nefficientnet = efficientnet.to(device)\noptimizer = torch.optim.Adam(efficientnet.parameters(), lr=1e-4)\n\n\n\nearly_stopper = EarlyStopping(patience=3, mode=\"max\")\nfor epoch in range(50):\n    efficientnet.train()\n    total_loss = 0\n    for images, labels in tqdm(train_loader, desc=f\"EffNet Epoch {epoch+1}\"):\n        images, labels = images.to(device), labels.to(device)\n        # ✅ CutMix：对一批图像应用 CutMix 增强\n        lam = np.random.beta(1.0, 1.0)\n        rand_index = torch.randperm(images.size(0)).to(device)\n        target_a = labels\n        target_b = labels[rand_index]\n\n        bbx1, bby1, bbx2, bby2 = rand_bbox(images.size(), lam)\n        images[:, :, bbx1:bbx2, bby1:bby2] = images[rand_index, :, bbx1:bbx2, bby1:bby2]\n        lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (images.size(-1) * images.size(-2)))\n\n        \n        optimizer.zero_grad()\n        outputs = efficientnet(images)\n        loss = criterion(outputs, target_a) * lam + criterion(outputs, target_b) * (1. - lam)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n    print(f\"[EffNet] Epoch {epoch+1} Loss: {total_loss / len(train_loader):.4f}\")\n\n    efficientnet.eval()\n    correct = 0\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = efficientnet(images)\n            _, preds = torch.max(outputs, 1)\n            correct += (preds == labels).sum().item()\n    acc = correct / len(val_loader.dataset)\n    print(f\"[EffNet] ✅ 验证准确率: {acc:.4f}\")\n    # 检查是否早停\n    early_stopper(acc, efficientnet)\n    if early_stopper.early_stop:\n        print(\"⏹️ 早停触发，停止训练\")\n        break\n\n# 恢复最优模型参数\nefficientnet.load_state_dict(early_stopper.best_weights)\n\ntorch.save(efficientnet.state_dict(), \"efficientnet.pth\")\nprint(\"✅ EfficientNet 已保存\")\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 加载已训练好的三个模型\nconvnext.load_state_dict(torch.load(\"convnext.pth\"))\nresnet.load_state_dict(torch.load(\"resnet50.pth\"))\nefficientnet.load_state_dict(torch.load(\"efficientnet.pth\"))\n\nconvnext.eval()\nresnet.eval()\nefficientnet.eval()\n\ntest_dataset = TestDataset(\"test\", transform=transform)\ntest_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)\n\nresults = []\nwith torch.no_grad():\n    for images, filenames in tqdm(test_loader):\n        images = images.to(device)\n\n        # 每个模型预测\n        preds1 = convnext(images)\n        preds2 = resnet(images)\n        preds3 = efficientnet(images)\n\n        # 求平均概率，再取 argmax\n        probs = (F.softmax(preds1, dim=1) + F.softmax(preds2, dim=1) + F.softmax(preds3, dim=1)) / 3\n        final_preds = torch.argmax(probs, dim=1)\n\n        for fname, pred in zip(filenames, final_preds.cpu().numpy()):\n            results.append((fname, pred))\n\nsubmission = pd.DataFrame(results, columns=[\"ID\", \"Label\"])\nsubmission.to_csv(\"submission_voting.csv\", index=False)\nprint(\"📁 submission_voting.csv 已生成\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}