{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================\n# ИМПОРТЫ\n# ============================================\n\nimport torch\nimport torch.nn as nn\nfrom torchvision import transforms, models, datasets\nfrom torch.utils.data import DataLoader, Dataset\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.model_selection import train_test_split\nfrom PIL import Image\nimport numpy as np\nfrom tqdm import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ============================================\n# ТРАНСФОРМАЦИИ\n# ============================================\n\ntrain_transform = transforms.Compose([\n    transforms.RandomResizedCrop(224, scale=(0.7, 1.0)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomRotation(15),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize(256),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# ============================================\n# КЛАССЫ ДЛЯ РАБОТЫ С ДАННЫМИ\n# ============================================\n\nclass ButterflyDataset(Dataset):\n    def __init__(self, paths, labels, transform=None):\n        self.paths = paths\n        self.labels = labels\n        self.transform = transform\n    \n    def __len__(self):\n        return len(self.paths)\n    \n    def __getitem__(self, idx):\n        img = Image.open(self.paths[idx]).convert('RGB')\n        label = self.labels[idx]\n        if self.transform:\n            img = self.transform(img)\n        return img, label\n\nclass TestDataset(Dataset):\n    def __init__(self, path, transform=None):\n        self.paths = sorted(\n            [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.jpg')],\n            key=lambda x: int(os.path.splitext(os.path.basename(x))[0])\n        )\n        self.transform = transform\n    \n    def __len__(self):\n        return len(self.paths)\n    \n    def __getitem__(self, idx):\n        img = Image.open(self.paths[idx]).convert('RGB')\n        if self.transform:\n            img = self.transform(img)\n        return img, self.paths[idx]\n\n# ============================================\n# ЗАГРУЗКА ДАННЫХ\n# ============================================\n\nprint(\"\\n📂 Загрузка данных...\")\n\nif os.path.exists(TRAIN_DIR):\n    # Загружаем данные через ImageFolder\n    base_ds = datasets.ImageFolder(TRAIN_DIR)\n    samples = base_ds.samples\n    paths, labels = zip(*samples)\n    \n    # Разделяем на train/val\n    train_paths, val_paths, train_labels, val_labels = train_test_split(\n        paths, labels, test_size=0.2, stratify=labels, random_state=42\n    )\n    \n    train_dataset = ButterflyDataset(train_paths, train_labels, transform=train_transform)\n    val_dataset = ButterflyDataset(val_paths, val_labels, transform=val_transform)\n    \n    train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)\n    val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)\n    \n    print(f\"✅ Тренировочных: {len(train_dataset)}\")\n    print(f\"✅ Валидационных: {len(val_dataset)}\")\nelse:\n    print(\"❌ Данные не загружены!\")\n\n# ============================================\n# МОДЕЛЬ\n# ============================================\n\nclass ButterflyClassifier(nn.Module):\n    def __init__(self, backbone_name='resnet18', num_classes=NUM_CLASSES):\n        super().__init__()\n        if backbone_name == 'resnet18':\n            self.backbone = models.resnet18(pretrained=True)\n            in_features = self.backbone.fc.in_features\n            self.backbone.fc = nn.Linear(in_features, num_classes)\n        elif backbone_name == 'resnet50':\n            self.backbone = models.resnet50(pretrained=True)\n            in_features = self.backbone.fc.in_features\n            self.backbone.fc = nn.Linear(in_features, num_classes)\n        else:\n            raise ValueError(f\"Unknown backbone: {backbone_name}\")\n    \n    def forward(self, x):\n        return self.backbone(x)\n\n# ============================================\n# ФУНКЦИИ ОБУЧЕНИЯ\n# ============================================\n\ndef train_one_epoch(model, dataloader, optimizer, loss_fn, epoch, device):\n    model.train()\n    running_loss, correct, total = 0.0, 0, 0\n    \n    pbar = tqdm(dataloader, desc=f'Train Epoch {epoch}')\n    for inputs, labels in pbar:\n        inputs, labels = inputs.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(inputs)\n        loss = loss_fn(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item() * inputs.size(0)\n        _, preds = torch.max(outputs, 1)\n        correct += (preds == labels).sum().item()\n        total += inputs.size(0)\n        \n        pbar.set_postfix({\n            'loss': f'{running_loss/total:.4f}',\n            'acc': f'{correct/total:.4f}'\n        })\n    \n    return running_loss / total, correct / total\n\n@torch.no_grad()\ndef validate_one_epoch(model, dataloader, loss_fn, epoch, device):\n    model.eval()\n    running_loss, correct, total = 0.0, 0, 0\n    \n    pbar = tqdm(dataloader, desc=f'Val Epoch {epoch}')\n    for inputs, labels in pbar:\n        inputs, labels = inputs.to(device), labels.to(device)\n        \n        outputs = model(inputs)\n        loss = loss_fn(outputs, labels)\n        \n        running_loss += loss.item() * inputs.size(0)\n        _, preds = torch.max(outputs, 1)\n        correct += (preds == labels).sum().item()\n        total += inputs.size(0)\n        \n        pbar.set_postfix({\n            'loss': f'{running_loss/total:.4f}',\n            'acc': f'{correct/total:.4f}'\n        })\n    \n    return running_loss / total, correct / total\n\n# ============================================\n# МОДЕЛЬ (БЕЗ ПРЕДОБУЧЕНИЯ)\n# ============================================\n\nclass ButterflyClassifier(nn.Module):\n    def __init__(self, backbone_name='resnet18', num_classes=NUM_CLASSES):\n        super().__init__()\n        if backbone_name == 'resnet18':\n            # pretrained=False - не скачиваем веса\n            self.backbone = models.resnet18(pretrained=False)\n            in_features = self.backbone.fc.in_features\n            self.backbone.fc = nn.Linear(in_features, num_classes)\n        elif backbone_name == 'resnet50':\n            self.backbone = models.resnet50(pretrained=False)\n            in_features = self.backbone.fc.in_features\n            self.backbone.fc = nn.Linear(in_features, num_classes)\n        else:\n            raise ValueError(f\"Unknown backbone: {backbone_name}\")\n    \n    def forward(self, x):\n        return self.backbone(x)\n\n# ============================================\n# ОБУЧЕНИЕ\n# ============================================\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"🔄 ОБУЧЕНИЕ МОДЕЛИ (С НУЛЯ)\")\nprint(\"=\"*50)\n\nmodel = ButterflyClassifier(backbone_name='resnet18').to(DEVICE)\nloss_fn = nn.CrossEntropyLoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)\n\nhistory = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': []}\nbest_val_acc = 0.0\n\nfor epoch in range(1, EPOCHS + 1):\n    train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, loss_fn, epoch, DEVICE)\n    val_loss, val_acc = validate_one_epoch(model, val_loader, loss_fn, epoch, DEVICE)\n    \n    history['train_loss'].append(train_loss)\n    history['train_acc'].append(train_acc)\n    history['val_loss'].append(val_loss)\n    history['val_acc'].append(val_acc)\n    \n    print(f\"\\nEpoch {epoch:02d}: Train Loss={train_loss:.4f}, Train Acc={train_acc:.4f}, \"\n          f\"Val Loss={val_loss:.4f}, Val Acc={val_acc:.4f}\\n\")\n    \n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        torch.save(model.state_dict(), 'best_model.pth')\n        print(f\"💾 Модель сохранена (Val Acc: {best_val_acc:.4f})\")\n\nprint(f\"\\n✅ Лучшая валидационная точность: {best_val_acc:.4f}\")\n\n# ============================================\n# ПРЕДСКАЗАНИЕ НА ТЕСТЕ\n# ============================================\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"🔄 ПРЕДСКАЗАНИЕ ТЕСТОВЫХ ДАННЫХ\")\nprint(\"=\"*50)\n\nmodel.load_state_dict(torch.load('best_model.pth'))\nmodel.eval()\n\ntest_dataset = TestDataset(TEST_DIR, transform=val_transform)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)\n\nprint(f\"✅ Загружено {len(test_dataset)} тестовых изображений\")\n\nall_preds = []\nall_paths = []\n\nwith torch.no_grad():\n    for images, paths in tqdm(test_loader, desc='Предсказание'):\n        images = images.to(DEVICE)\n        outputs = model(images)\n        _, preds = torch.max(outputs, 1)\n        all_preds.extend(preds.cpu().numpy())\n        all_paths.extend(paths)\n\n# ============================================\n# СОЗДАНИЕ САБМИТА\n# ============================================\n\nindices = [int(os.path.splitext(os.path.basename(p))[0]) for p in all_paths]\n\nsubmission = pd.DataFrame({\n    'index': indices,\n    'label': all_preds\n}).sort_values('index').reset_index(drop=True)\n\nsubmission.to_csv('submission.csv', index=False)\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"✅ САБМИТ СОЗДАН: submission.csv\")\nprint(\"=\"*50)\nprint(\"\\n📊 Первые 10 строк:\")\nprint(submission.head(10))\nprint(f\"\\n📊 Количество строк: {len(submission)}\")\n\n# ============================================\n# СКАЧИВАНИЕ ФАЙЛА (для Kaggle)\n# ============================================\n\nfrom IPython.display import FileLink, display\ndisplay(FileLink('submission.csv'))\nprint(\"\\n📥 Нажмите на ссылку выше, чтобы скачать submission.csv\")\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"🎉 ПРОЕКТ УСПЕШНО ЗАВЕРШЕН!\")\nprint(\"=\"*50)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-07-09T06:40:26.909677Z","iopub.execute_input":"2026-07-09T06:40:26.910058Z","iopub.status.idle":"2026-07-09T07:22:07.630025Z","shell.execute_reply.started":"2026-07-09T06:40:26.910031Z","shell.execute_reply":"2026-07-09T07:22:07.629196Z"}},"outputs":[],"execution_count":null}]}