{"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# ============================================================\nimport os\nimport time\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom pathlib import Path\nfrom PIL import Image\nfrom collections import OrderedDict, defaultdict\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom sklearn.model_selection import train_test_split\n\n# пути\nWORK_DIR = Path('/kaggle/working/')                        # рабочая папка\nTRAIN_IMG_DIR  = WORK_DIR / 'images_training' / 'images_training_rev1'\nTEST_IMG_DIR   = WORK_DIR / 'images_test' / 'images_test_rev1'\nSOLUTIONS_PATH = WORK_DIR / 'training_solutions_rev1.csv'\n\nprint(f\"torch: {torch.__version__}\")\nprint(f\"cuda: {torch.cuda.is_available()}\")\nprint(f\"TRAIN_IMG_DIR: {TRAIN_IMG_DIR}  exists={TRAIN_IMG_DIR.exists()}\")\nprint(f\"SOLUTIONS_PATH: {SOLUTIONS_PATH}  exists={SOLUTIONS_PATH.exists()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:06:03.912651Z","iopub.execute_input":"2026-09-19T06:06:03.913396Z","iopub.status.idle":"2026-09-19T06:06:13.406133Z","shell.execute_reply.started":"2026-09-19T06:06:03.913366Z","shell.execute_reply":"2026-09-19T06:06:13.405365Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Распаковка\n# ============================================================\nimport zipfile\nfrom pathlib import Path\n\nINPUT_DIR = Path('/kaggle/input/competitions/galaxy-zoo-the-galaxy-challenge')\n\nprint(\"Содержимое input:\")\nfor f in sorted(INPUT_DIR.iterdir()):\n    size_mb = f.stat().st_size / 1e6\n    print(f\"  {f.name:40s} {size_mb:8.1f} MB\")\n\n\ndef unzip(zip_path, target_dir, marker=None):\n    \"\"\"\n    Распаковывает zip в target_dir.\n    marker — имя файла/папки для проверки, что уже распаковано.\n    \"\"\"\n    zip_path = Path(zip_path)\n    target_dir = Path(target_dir)\n\n    if marker is not None and (target_dir / marker).exists():\n        print(f\"✅ {target_dir.name}/ уже распакован\")\n        return\n\n    target_dir.mkdir(parents=True, exist_ok=True)\n    print(f\"📦 {zip_path.name} → {target_dir}\")\n    with zipfile.ZipFile(zip_path, 'r') as z:\n        z.extractall(target_dir)\n    print(\"   готово\")\n\n\n# solutions — маленький zip\nunzip(INPUT_DIR / 'training_solutions_rev1.zip',\n      WORK_DIR,\n      marker='training_solutions_rev1.csv')\n\n# train-картинки — несколько ГБ, займёт минуты\nunzip(INPUT_DIR / 'images_training_rev1.zip',\n      WORK_DIR / 'images_training')\n\n# test-картинки\nunzip(INPUT_DIR / 'images_test_rev1.zip',\n      WORK_DIR / 'images_test')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:06:13.407631Z","iopub.execute_input":"2026-09-19T06:06:13.408045Z","iopub.status.idle":"2026-09-19T06:06:47.068845Z","shell.execute_reply.started":"2026-09-19T06:06:13.408013Z","shell.execute_reply":"2026-09-19T06:06:47.068166Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# проверка\nfor name, p in [('TRAIN_IMG_DIR', TRAIN_IMG_DIR),\n                ('TEST_IMG_DIR', TEST_IMG_DIR),\n                ('SOLUTIONS_PATH', SOLUTIONS_PATH)]:\n    exists = p.exists()\n    if p.is_dir():\n        n = sum(1 for f in p.iterdir()\n                if f.is_file() and f.suffix.lower() in ('.jpg', '.jpeg', '.png'))\n        print(f\"{'✅' if exists else '❌'} {name:18s} {p}  ({n} JPG)\")\n    else:\n        print(f\"{'✅' if exists else '❌'} {name:18s} {p}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:06:47.069843Z","iopub.execute_input":"2026-09-19T06:06:47.070206Z","iopub.status.idle":"2026-09-19T06:06:47.997625Z","shell.execute_reply.started":"2026-09-19T06:06:47.070180Z","shell.execute_reply":"2026-09-19T06:06:47.997028Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Ячейка 2. Загрузка solutions\n# ============================================================\nsolutions = pd.read_csv(SOLUTIONS_PATH)\nprint(f\"solutions: {solutions.shape}\")\nprint(f\"столбцы:  {list(solutions.columns[:5])} ... {list(solutions.columns[-3:])}\")\nsolutions.head(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:06:47.999385Z","iopub.execute_input":"2026-09-19T06:06:47.999606Z","iopub.status.idle":"2026-09-19T06:06:48.286231Z","shell.execute_reply.started":"2026-09-19T06:06:47.999586Z","shell.execute_reply":"2026-09-19T06:06:48.285519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Строим TASK_MAP из solutions\n# ============================================================\nfrom collections import OrderedDict\n\n\ndef build_task_map(solutions):\n    \"\"\"\n    Из столбцов 'Class1.1', ..., 'Class11.6' собирает OrderedDict:\n        {'Class1': ['Class1.1','Class1.2','Class1.3'], ..., 'Class11': [...]}\n    Порядок задач и ответов — числовой.\n    \"\"\"\n    task_to_cols = {}\n    for col in solutions.columns:\n        if col == 'GalaxyID':\n            continue\n        task = col.split('.', 1)[0]\n        task_to_cols.setdefault(task, []).append(col)\n\n    task_key = lambda t: int(t.replace('Class', ''))\n    answer_key = lambda c: int(c.split('.', 1)[1])\n\n    return OrderedDict(\n        (task, sorted(task_to_cols[task], key=answer_key))\n        for task in sorted(task_to_cols, key=task_key)\n    )\n\n\nTASK_MAP = build_task_map(solutions)\n\n# проверка\nfor task, cols in TASK_MAP.items():\n    print(f\"{task:10s} {len(cols)} ответов: {cols}\")\n\nprint(f\"\\nвсего задач: {len(TASK_MAP)}\")\nprint(f\"всего ответов: {sum(len(v) for v in TASK_MAP.values())}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:06:48.287233Z","iopub.execute_input":"2026-09-19T06:06:48.287545Z","iopub.status.idle":"2026-09-19T06:06:48.295040Z","shell.execute_reply.started":"2026-09-19T06:06:48.287522Z","shell.execute_reply":"2026-09-19T06:06:48.294397Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Смотрим на сырые галактики с подписями\n# ============================================================\nimport os\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom pathlib import Path\nimport matplotlib.pyplot as plt\n\n# --- подписи на русском (по дереву решений Galaxy Zoo) ---\nCLASS1 = ['гладкая', 'структура/диск', 'звезда/артефакт']\nCLASS4 = ['есть спираль', 'нет спирали']\nCLASS11 = ['1 рукав', '2 рукава', '3 рукава', '4 рукава', '>4 рукавов', 'не видно']\n\n\ndef show_galaxies(img_dir, solutions, task_map, n=8, cols=4, seed=42):\n    rng = np.random.default_rng(seed)\n    files = sorted(f for f in os.listdir(img_dir)\n                   if f.lower().endswith(('.jpg', '.jpeg', '.png')))\n    picks = rng.choice(len(files), size=n, replace=False)\n\n    sol = solutions.set_index('GalaxyID')\n    rows = (n + cols - 1) // cols\n\n    fig, axes = plt.subplots(rows, cols, figsize=(4 * cols, 4.2 * rows))\n    axes = axes.flatten()\n\n    for i, idx in enumerate(picks):\n        fname = files[idx]\n        gid = int(fname.rsplit('.', 1)[0])\n        img = Image.open(os.path.join(img_dir, fname)).convert('RGB')\n\n        ax = axes[i]\n        ax.imshow(img)\n        ax.axis('off')\n\n        row = sol.loc[gid]\n        c1 = row[task_map['Class1']].values\n        c4 = row[task_map['Class4']].values\n        c11 = row[task_map['Class11']].values\n\n        title = (\n            f\"ID {gid}\\n\"\n            f\"Class1: {CLASS1[c1.argmax()]} ({c1.max():.2f})\\n\"\n            f\"Class4: {CLASS4[c4.argmax()]} ({c4.max():.2f})\\n\"\n            f\"Class11: {CLASS11[c11.argmax()]}\"\n        )\n        ax.set_title(title, fontsize=9)\n\n    for j in range(i + 1, len(axes)):\n        axes[j].axis('off')\n\n    plt.tight_layout()\n    plt.show()\n\n\nshow_galaxies(TRAIN_IMG_DIR, solutions, TASK_MAP, n=8, cols=4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:06:48.296945Z","iopub.execute_input":"2026-09-19T06:06:48.297649Z","iopub.status.idle":"2026-09-19T06:06:49.545748Z","shell.execute_reply.started":"2026-09-19T06:06:48.297619Z","shell.execute_reply":"2026-09-19T06:06:49.544571Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Базовый датасет — soft labels\n# ============================================================\nclass GalaxyZooDataset(Dataset):\n    \"\"\"\n    Читает JPG по имени файла '<GalaxyID>.jpg'.\n    Метки — вероятности (soft labels), сумма = 1 внутри каждой задачи.\n    Возвращает (image, dict[task -> tensor(n_answers,)]).\n    \"\"\"\n\n    IMG_EXTS = ('.jpg', '.jpeg', '.png')\n\n    def __init__(self, img_dir, solutions, transform=None):\n        self.img_dir = Path(img_dir)\n        self.transform = transform\n\n        # --- список файлов ---\n        self.filenames = sorted(\n            f for f in os.listdir(self.img_dir)\n            if f.lower().endswith(self.IMG_EXTS)\n        )\n        if not self.filenames:\n            raise FileNotFoundError(f\"Нет картинок в {self.img_dir}\")\n\n        # --- карта задач ---\n        self.task_cols = build_task_map(solutions)\n        self.tasks = list(self.task_cols.keys())\n        self.num_answers = {t: len(c) for t, c in self.task_cols.items()}\n\n        # --- метки (векторно) ---\n        gids = np.array(\n            [int(f.rsplit('.', 1)[0]) for f in self.filenames],\n            dtype=np.int64,\n        )\n        sol = solutions.set_index('GalaxyID').loc[gids]\n\n        self.labels = {}\n        for task, cols in self.task_cols.items():\n            arr = sol[cols].to_numpy(dtype=np.float32, copy=True)\n            row_sums = arr.sum(axis=1, keepdims=True)\n            np.divide(arr, row_sums, out=arr, where=row_sums > 0)\n            zero_mask = (row_sums == 0).ravel()\n            if zero_mask.any():\n                arr[zero_mask] = 1.0 / arr.shape[1]\n            self.labels[task] = torch.from_numpy(arr)\n\n    def __len__(self):\n        return len(self.filenames)\n\n    def _load_image(self, idx):\n        fname = self.filenames[idx]\n        img = Image.open(self.img_dir / fname).convert('RGB')\n        return self.transform(img) if self.transform else img\n\n    def __getitem__(self, idx):\n        img = self._load_image(idx)\n        labels = {task: arr[idx] for task, arr in self.labels.items()}\n        return img, labels\n\nclass TransformSubset(Dataset):\n    \"\"\"\n    Подмножество base_ds по индексам + свой transform.\n    Аналог torch.utils.data.Subset, но с поддержкой transform.\n    \"\"\"\n    def __init__(self, base, indices, transform):\n        self.base = base\n        self.indices = np.asarray(indices, dtype=np.int64)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.indices)\n\n    def __getitem__(self, i):\n        idx = int(self.indices[i])\n        fname = self.base.filenames[idx]\n        img = Image.open(self.base.img_dir / fname).convert('RGB')\n        img = self.transform(img)\n        labels = {task: arr[idx] for task, arr in self.base.labels.items()}\n        return img, labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:06:49.547337Z","iopub.execute_input":"2026-09-19T06:06:49.547993Z","iopub.status.idle":"2026-09-19T06:06:49.561620Z","shell.execute_reply.started":"2026-09-19T06:06:49.547925Z","shell.execute_reply":"2026-09-19T06:06:49.560781Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Трансформы\n# ============================================================\ntrain_tfms = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(15),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.5]*3, std=[0.5]*3),\n])\n\nval_tfms = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.5]*3, std=[0.5]*3),\n])\n\nprint(\"train_tfms:\", train_tfms)\nprint(\"val_tfms:  \", val_tfms)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:06:49.562561Z","iopub.execute_input":"2026-09-19T06:06:49.562868Z","iopub.status.idle":"2026-09-19T06:06:49.578811Z","shell.execute_reply.started":"2026-09-19T06:06:49.562836Z","shell.execute_reply":"2026-09-19T06:06:49.578047Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Ячейка 7. Базовый датасет\n# ============================================================\nbase_ds = GalaxyZooDataset(TRAIN_IMG_DIR, solutions, transform=None)\nprint(f\"✅ base_ds: {len(base_ds)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:06:49.580013Z","iopub.execute_input":"2026-09-19T06:06:49.580526Z","iopub.status.idle":"2026-09-19T06:06:49.721259Z","shell.execute_reply.started":"2026-09-19T06:06:49.580501Z","shell.execute_reply":"2026-09-19T06:06:49.720484Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\nN = len(base_ds)\nall_idx = np.arange(N)\n\ntrain_idx, temp_idx = train_test_split(all_idx, test_size=0.2, random_state=42)\nval_idx, test_idx   = train_test_split(temp_idx, test_size=0.5, random_state=42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:06:49.723337Z","iopub.execute_input":"2026-09-19T06:06:49.723668Z","iopub.status.idle":"2026-09-19T06:06:49.731913Z","shell.execute_reply.started":"2026-09-19T06:06:49.723642Z","shell.execute_reply":"2026-09-19T06:06:49.731217Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# DataLoader\n# ============================================================\nfrom torch.utils.data import DataLoader\n\nBATCH_SIZE = 64\n\ntrain_ds = TransformSubset(base_ds, train_idx, train_tfms)\nval_ds   = TransformSubset(base_ds, val_idx,   val_tfms)\ntest_ds  = TransformSubset(base_ds, test_idx,  val_tfms)\n\ntrain_loader = DataLoader(\n    train_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,        # было 0\n    pin_memory=True,      # было False\n    persistent_workers=True\n)\n\nval_loader = DataLoader(\n    val_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,        # было 0\n    pin_memory=True,      # было False\n    persistent_workers=True\n)\n\ntest_loader = DataLoader(\n    test_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,        # было 0\n    pin_memory=True,      # было False\n    persistent_workers=True\n)\n\nprint(f\"train_loader: {len(train_loader)} батчей\")\nprint(f\"val_loader:   {len(val_loader)} батчей\")\nprint(f\"test_loader:  {len(test_loader)} батчей\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:16:32.799829Z","iopub.execute_input":"2026-09-19T06:16:32.800814Z","iopub.status.idle":"2026-09-19T06:16:32.807793Z","shell.execute_reply.started":"2026-09-19T06:16:32.800776Z","shell.execute_reply":"2026-09-19T06:16:32.807126Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Soft cross-entropy\n# ============================================================\nimport torch.nn.functional as F\n\n\ndef soft_ce_loss(logits, soft_targets, eps=1e-7):\n    \"\"\"\n    logits:       (B, N) — сырые выходы модели\n    soft_targets: (B, N) — вероятности, сумма = 1\n    \"\"\"\n    log_probs = F.log_softmax(logits, dim=1)\n    soft_targets = soft_targets.clamp(min=eps)\n    return -(soft_targets * log_probs).sum(dim=1).mean()\n\n\ndef compute_loss(outputs, targets):\n    \"\"\"\n    outputs: dict {task: logits (B, n_answers)}\n    targets: dict {task: probs  (B, n_answers)}\n    \"\"\"\n    total = 0.0\n    for task in outputs:\n        total = total + soft_ce_loss(outputs[task], targets[task])\n    return total / len(outputs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:08:33.070584Z","iopub.execute_input":"2026-09-19T06:08:33.071316Z","iopub.status.idle":"2026-09-19T06:08:33.076470Z","shell.execute_reply.started":"2026-09-19T06:08:33.071287Z","shell.execute_reply":"2026-09-19T06:08:33.075530Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Backbone + GalaxyMultiHead\n# ============================================================\nimport torch\nimport torch.nn as nn\nimport torchvision.models as models\n\n\ndef build_backbone(name='resnet18', pretrained=True):\n    \"\"\"\n    Возвращает (backbone без классификационной головы, feat_dim).\n\n    Поддерживаемые имена:\n        CNN:  resnet18, resnet34, resnet50, resnet101,\n              efficientnet_b0, efficientnet_b3,\n              mobilenet_v3_small, mobilenet_v3_large,\n              densenet121, convnext_tiny\n        ViT:  vit_b_16, vit_b_32, vit_l_16\n        Swin: swin_t, swin_s\n    \"\"\"\n    weights = 'IMAGENET1K_V1' if pretrained else None\n\n    # ---------------- ResNet ----------------\n    if name == 'resnet18':\n        m = models.resnet18(weights=weights);           feat_dim = m.fc.in_features; m.fc = nn.Identity()\n    elif name == 'resnet34':\n        m = models.resnet34(weights=weights);           feat_dim = m.fc.in_features; m.fc = nn.Identity()\n    elif name == 'resnet50':\n        m = models.resnet50(weights='IMAGENET1K_V2' if pretrained else None)\n        feat_dim = m.fc.in_features; m.fc = nn.Identity()\n    elif name == 'resnet101':\n        m = models.resnet101(weights='IMAGENET1K_V2' if pretrained else None)\n        feat_dim = m.fc.in_features; m.fc = nn.Identity()\n\n    # ---------------- EfficientNet ----------------\n    elif name == 'efficientnet_b0':\n        m = models.efficientnet_b0(weights=weights)\n        feat_dim = m.classifier[1].in_features; m.classifier = nn.Identity()\n    elif name == 'efficientnet_b3':\n        m = models.efficientnet_b3(weights=weights)\n        feat_dim = m.classifier[1].in_features; m.classifier = nn.Identity()\n\n    # ---------------- MobileNet ----------------\n    elif name == 'mobilenet_v3_small':\n        m = models.mobilenet_v3_small(weights=weights)\n        feat_dim = m.classifier[3].in_features; m.classifier = nn.Identity()\n    elif name == 'mobilenet_v3_large':\n        m = models.mobilenet_v3_large(weights=weights)\n        feat_dim = m.classifier[3].in_features; m.classifier = nn.Identity()\n\n    # ---------------- DenseNet ----------------\n    elif name == 'densenet121':\n        m = models.densenet121(weights=weights)\n        feat_dim = m.classifier.in_features; m.classifier = nn.Identity()\n\n    # ---------------- ConvNeXt ----------------\n    elif name == 'convnext_tiny':\n        m = models.convnext_tiny(weights=weights)\n        feat_dim = m.classifier[2].in_features; m.classifier = nn.Identity()\n\n    # ---------------- ViT ----------------\n    elif name == 'vit_b_16':\n        m = models.vit_b_16(weights=weights)\n        # у ViT голова — m.heads (Sequential), последний слой — Linear\n        feat_dim = m.heads.head.in_features\n        m.heads = nn.Identity()\n\n    elif name == 'vit_b_32':\n        m = models.vit_b_32(weights=weights)\n        feat_dim = m.heads.head.in_features\n        m.heads = nn.Identity()\n\n    elif name == 'vit_l_16':\n        m = models.vit_l_16(weights='IMAGENET1K_SWAG_E2E_V1' if pretrained else None)\n        feat_dim = m.heads.head.in_features\n        m.heads = nn.Identity()\n\n    # ---------------- Swin ----------------\n    elif name == 'swin_t':\n        m = models.swin_t(weights=weights)\n        feat_dim = m.head.in_features\n        m.head = nn.Identity()\n\n    elif name == 'swin_s':\n        m = models.swin_s(weights=weights)\n        feat_dim = m.head.in_features\n        m.head = nn.Identity()\n\n    else:\n        raise ValueError(f'Неизвестный backbone: {name}. '\n                         f'Поддерживаются: resnet18/34/50/101, '\n                         f'efficientnet_b0/b3, mobilenet_v3_small/large, '\n                         f'densenet121, convnext_tiny, '\n                         f'vit_b_16, vit_b_32, vit_l_16, swin_t, swin_s')\n\n    return m, feat_dim\n\n\nclass GalaxyMultiHead(nn.Module):\n    def __init__(self, backbone, feat_dim, num_answers, dropout=0.2):\n        super().__init__()\n        self.backbone = backbone\n        self.num_answers = num_answers\n        self.dropout = nn.Dropout(dropout)\n        self.heads = nn.ModuleDict({\n            task: nn.Linear(feat_dim, n)\n            for task, n in num_answers.items()\n        })\n\n    def forward(self, x):\n        f = self.backbone(x)\n        if f.dim() > 2:\n            f = f.flatten(1)\n        f = self.dropout(f)\n        return {task: head(f) for task, head in self.heads.items()}\n\n\ndef create_model(backbone_name='resnet18', num_answers=None,\n                 dropout=0.2, pretrained=True):\n    backbone, feat_dim = build_backbone(backbone_name, pretrained=pretrained)\n    return GalaxyMultiHead(backbone, feat_dim, num_answers, dropout)\n\n\n# --- создаём ---\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"device: {device}\")\n\nmodel = create_model(\n    backbone_name='resnet18',\n    num_answers=base_ds.num_answers,\n    dropout=0.2,\n    pretrained=True,\n).to(device)\n\nn_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f\"обучаемых параметров: {n_params/1e6:.2f}M\")\n\n# --- проверка forward ---\nwith torch.no_grad():\n    imgs_b, _ = next(iter(val_loader))\n    imgs_b = imgs_b.to(device)\n    out = model(imgs_b)\n    print(\"\\nвыходы модели:\")\n    for task, logits in out.items():\n        print(f\"  {task:10s} {tuple(logits.shape)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:16:20.981076Z","iopub.execute_input":"2026-09-19T06:16:20.981361Z","iopub.status.idle":"2026-09-19T06:16:21.998428Z","shell.execute_reply.started":"2026-09-19T06:16:20.981336Z","shell.execute_reply":"2026-09-19T06:16:21.997664Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import seaborn as sns\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom tqdm.auto import tqdm\nimport torch\nimport time\n\n# --- Настройка стиля Seaborn ---\nsns.set_theme(style=\"whitegrid\", context=\"talk\")\n\nEPOCHS = 10\nLR = 1e-4\noptimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4)\n\n# --- Собираем историю в список словарей (как раньше) ---\nhistory = []\n\nfor epoch in range(EPOCHS):\n    # ---------- train ----------\n    model.train()\n    train_loss = 0.0\n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch} [train]\", leave=False)\n    t0 = time.time()\n    for imgs, targets in pbar:\n        imgs = imgs.to(device, non_blocking=True)\n        targets = {t: v.to(device, non_blocking=True) for t, v in targets.items()}\n        outputs = model(imgs)\n        loss = compute_loss(outputs, targets)\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n        train_loss += loss.item()\n        \n        pbar.set_postfix(loss=f\"{loss.item():.4f}\")\n\n    # ---------- val ----------\n    model.eval()\n    val_loss = 0.0\n    with torch.no_grad():\n        for imgs, targets in val_loader:\n            imgs = imgs.to(device, non_blocking=True)\n            targets = {t: v.to(device, non_blocking=True) for t, v in targets.items()}\n            outputs = model(imgs)\n            val_loss += compute_loss(outputs, targets).item()\n\n    tr = train_loss / len(train_loader)\n    va = val_loss / len(val_loader)\n    dt = time.time() - t0\n    print(f\"Epoch {epoch}: train={tr:.4f}  val={va:.4f}  time={dt:.1f}s\")\n    \n    # --- Записываем в историю ---\n    history.append({'epoch': epoch, 'train_loss': tr, 'val_loss': va, 'time': dt})\n\n# --- Сохраняем веса ---\ntorch.save(model.state_dict(), 'galaxy_resnet18_softce.pt')\nprint(\"\\n✅ веса сохранены\")\n\n# --- Красивая итоговая визуализация на Seaborn ---\nhist_df = pd.DataFrame(history)\n\nplt.figure(figsize=(10, 6))\nsns.lineplot(data=hist_df, x='epoch', y='train_loss', label='Обучение', marker='o', linewidth=2.5)\nsns.lineplot(data=hist_df, x='epoch', y='val_loss', label='Валидация', marker='o', linewidth=2.5)\n\nplt.title('Кривые обучения модели', fontsize=16, fontweight='bold')\nplt.xlabel('Эпоха', fontsize=12)\nplt.ylabel('Loss', fontsize=12)\nplt.xticks(hist_df['epoch'])\nplt.legend(fontsize=12)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:16:38.402760Z","iopub.execute_input":"2026-09-19T06:16:38.403542Z","iopub.status.idle":"2026-09-19T06:47:11.412338Z","shell.execute_reply.started":"2026-09-19T06:16:38.403511Z","shell.execute_reply":"2026-09-19T06:47:11.411421Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Ячейка 5. Submission — 37 столбцов в нужном порядке\n# ============================================================\nfrom torch.utils.data import Dataset\n\n\nclass GalaxyZooTest(Dataset):\n    \"\"\"Тестовый датасет — только картинки и GalaxyID.\"\"\"\n    IMG_EXTS = ('.jpg', '.jpeg', '.png')\n\n    def __init__(self, img_dir, transform=None):\n        self.img_dir = Path(img_dir)\n        self.transform = transform\n        self.filenames = sorted(\n            f for f in os.listdir(self.img_dir)\n            if f.lower().endswith(self.IMG_EXTS)\n        )\n\n    def __len__(self):\n        return len(self.filenames)\n\n    def __getitem__(self, i):\n        fname = self.filenames[i]\n        gid = int(fname.rsplit('.', 1)[0])\n        img = Image.open(self.img_dir / fname).convert('RGB')\n        if self.transform:\n            img = self.transform(img)\n        return img, gid\n\n\ntest_full = GalaxyZooTest(TEST_IMG_DIR, transform=val_tfms)\ntest_full_loader = DataLoader(\n    test_full, batch_size=64, shuffle=False, num_workers=2,\n)\nprint(f\"test_full: {len(test_full)} картинок, {len(test_full_loader)} батчей\")\n\n\n# --- предсказание ---\nmodel.eval()\nall_probs = {task: [] for task in base_ds.tasks}\nall_gids = []\n\nwith torch.no_grad():\n    for imgs, gids in tqdm(test_full_loader, desc='predict'):\n        imgs = imgs.to(device)\n        logits = model(imgs)\n        for task, l in logits.items():\n            p = torch.softmax(l, dim=1).cpu().numpy()\n            all_probs[task].append(p)\n        all_gids.extend(gids.numpy().tolist())\n\nfor task in all_probs:\n    all_probs[task] = np.concatenate(all_probs[task], axis=0)\n\n\n# --- сборка DataFrame в правильном порядке столбцов ---\nordered_cols = []\nfor task in base_ds.tasks:\n    ordered_cols.extend(base_ds.task_cols[task])\n\nsubmission = pd.DataFrame({'GalaxyID': all_gids})\nfor task in base_ds.tasks:\n    cols = base_ds.task_cols[task]\n    for i, col in enumerate(cols):\n        submission[col] = all_probs[task][:, i]\n\n# убеждаемся, что порядок столбцов совпадает с solutions_training\nexpected_cols = ['GalaxyID'] + [c for c in solutions.columns if c != 'GalaxyID']\nsubmission = submission[expected_cols]\n\n# сохраняем\nsubmission.to_csv('submission.csv', index=False)\nprint(f\"\\n✅ submission: {submission.shape}\")\nprint(submission.head(3))\nprint(f\"\\nстолбцы совпадают с solutions_training: \"\n      f\"{list(submission.columns) == expected_cols}\")\n\n# --- проверка: суммы внутри задач ≈ 1 ---\nprint(\"\\nпроверка сумм по задачам:\")\nfor task in base_ds.tasks:\n    cols = base_ds.task_cols[task]\n    s = submission[cols].values.sum(axis=1)\n    print(f\"  {task:10s}  min={s.min():.4f}  max={s.max():.4f}  \"\n          f\"mean={s.mean():.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:52:35.490093Z","iopub.execute_input":"2026-09-19T06:52:35.490419Z","iopub.status.idle":"2026-09-19T06:55:38.912729Z","shell.execute_reply.started":"2026-09-19T06:52:35.490380Z","shell.execute_reply":"2026-09-19T06:55:38.911795Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## MVP\n","metadata":{}},{"cell_type":"code","source":"import os\nimport torch\nimport numpy as np\nfrom PIL import Image\nimport torch.nn.functional as F\n\n# ---------- Настройки ----------\nCKPT_PATH = '/kaggle/working/galaxy_resnet18_softce.pt'\nIMG_SIZE  = 224\nDEVICE    = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nTASKS = ['Class1', 'Class4', 'Class11']\n\nCLASS_NAMES = {\n    'Class1':  ['гладкая', 'структура/диск', 'звезда/артефакт'],\n    'Class4':  ['есть спираль', 'нет спирали'],\n    'Class11': ['1 рукав', '2 рукава', '3 рукава',\n                '4 рукава', '>4 рукавов', 'не видно'],\n}\n\n\ndef load_trained_model(ckpt_path=CKPT_PATH, model=None, device=DEVICE):\n    \"\"\"Загружает веса из чекпоинта. Если model не передан — нужно создать архитектуру.\"\"\"\n    if not os.path.exists(ckpt_path):\n        raise FileNotFoundError(f\"Чекпоинт не найден: {ckpt_path}\")\n\n    ckpt = torch.load(ckpt_path, map_location=device)\n\n    if model is None:\n        raise ValueError(\"Передайте model — экземпляр вашей архитектуры (GalaxyModel)\")\n\n    model.load_state_dict(ckpt['model_state'])\n    model = model.to(device)\n    model.eval()\n\n    print(f\"✅ Модель загружена из эпохи {ckpt['epoch']}\")\n    if 'metrics' in ckpt:\n        m = ckpt['metrics']\n        print(f\"   mean_f1 = {m.get('mean_f1', '—'):.4f}\")\n        for t in TASKS:\n            print(f\"   {t}_f1 = {m.get(f'{t}_f1', '—'):.4f}\")\n\n    return model, ckpt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:56:06.520313Z","iopub.execute_input":"2026-09-19T06:56:06.520644Z","iopub.status.idle":"2026-09-19T06:56:06.531217Z","shell.execute_reply.started":"2026-09-19T06:56:06.520604Z","shell.execute_reply":"2026-09-19T06:56:06.530332Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def preprocess_image(image, img_size=224):\n    \"\"\"\n    Полностью соответствует val_tfms:\n        Resize((224,224)) → ToTensor → Normalize(mean=0.5, std=0.5)\n    \"\"\"\n    # 1) Приводим к PIL RGB\n    if isinstance(image, (str, os.PathLike)):\n        img = Image.open(image).convert('RGB')\n    elif isinstance(image, np.ndarray):\n        img = Image.fromarray(image).convert('RGB')\n    elif isinstance(image, Image.Image):\n        img = image.convert('RGB')\n    else:\n        raise TypeError(f\"Неизвестный тип: {type(image)}\")\n\n    # 2) Resize (аналог transforms.Resize)\n    img = img.resize((img_size, img_size), Image.BILINEAR)\n\n    # 3) ToTensor: HWC uint8 → CHW float [0..1]\n    arr = np.array(img, dtype=np.float32) / 255.0\n    tensor = torch.from_numpy(arr).permute(2, 0, 1)\n\n    # 4) Normalize (mean=0.5, std=0.5) → диапазон [-1, 1]\n    tensor = (tensor - 0.5) / 0.5\n\n    return tensor.unsqueeze(0)  # (1, 3, 224, 224)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:56:10.023242Z","iopub.execute_input":"2026-09-19T06:56:10.023643Z","iopub.status.idle":"2026-09-19T06:56:10.029873Z","shell.execute_reply.started":"2026-09-19T06:56:10.023617Z","shell.execute_reply":"2026-09-19T06:56:10.029149Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.no_grad()\ndef predict(model, image, device=DEVICE, top_k=3):\n    \"\"\"\n    Возвращает словарь:\n      {\n        'Class1':  {'label': '...', 'probs': {name: prob, ...}, 'top1': '...', 'top1_prob': 0.85},\n        'Class4':  {...},\n        'Class11': {...},\n      }\n    \"\"\"\n    model.eval()\n    x = preprocess_image(image).to(device)\n\n    outputs = model(x)\n\n    results = {}\n    for task in TASKS:\n        logits = outputs[task]                        # (1, N)\n        probs = F.softmax(logits, dim=1)[0].cpu().numpy()\n\n        names = CLASS_NAMES[task]\n        top_idx = np.argsort(probs)[::-1][:top_k]\n\n        results[task] = {\n            'label':      names[int(probs.argmax())],\n            'top1':       names[int(top_idx[0])],\n            'top1_prob':  float(probs[top_idx[0]]),\n            'probs':      {names[i]: float(probs[i]) for i in range(len(names))},\n            'top_k':      [(names[i], float(probs[i])) for i in top_idx],\n        }\n\n    return results","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:56:12.880706Z","iopub.execute_input":"2026-09-19T06:56:12.881018Z","iopub.status.idle":"2026-09-19T06:56:12.887655Z","shell.execute_reply.started":"2026-09-19T06:56:12.880942Z","shell.execute_reply":"2026-09-19T06:56:12.886878Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef show_prediction(model, image, device=DEVICE):\n    \"\"\"Показывает картинку + топ-3 по каждой задаче.\"\"\"\n    # Предсказание\n    res = predict(model, image, device)\n\n    # Картинка\n    if isinstance(image, (str, os.PathLike)):\n        img = Image.open(image).convert('RGB')\n        title = os.path.basename(image)\n    elif isinstance(image, Image.Image):\n        img = image\n        title = 'input'\n    else:\n        img = Image.fromarray(image)\n        title = 'input'\n\n    fig = plt.figure(figsize=(14, 5))\n    gs = fig.add_gridspec(1, 4, width_ratios=[1, 1, 1, 1])\n\n    # 1) Изображение\n    ax0 = fig.add_subplot(gs[0, 0])\n    ax0.imshow(img)\n    ax0.set_title(title, fontsize=10)\n    ax0.axis('off')\n\n    # 2–4) Бар-чарты по задачам\n    for i, task in enumerate(TASKS):\n        ax = fig.add_subplot(gs[0, i + 1])\n        names = [n for n, _ in res[task]['top_k']]\n        vals  = [p for _, p in res[task]['top_k']]\n\n        colors = ['#2ecc71' if n == res[task]['top1'] else '#95a5a6' for n in names]\n        ax.barh(names[::-1], vals[::-1], color=colors[::-1])\n        ax.set_xlim(0, 1)\n        ax.set_title(f\"{task}\\n{res[task]['top1']} ({res[task]['top1_prob']:.2%})\",\n                     fontsize=11, fontweight='bold')\n        ax.set_xlabel('Вероятность')\n        ax.grid(axis='x', alpha=0.3)\n\n    plt.tight_layout()\n    plt.savefig('/kaggle/working/prediction.png', dpi=120)\n    plt.show()\n\n    return res","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:56:15.452640Z","iopub.execute_input":"2026-09-19T06:56:15.452895Z","iopub.status.idle":"2026-09-19T06:56:15.461823Z","shell.execute_reply.started":"2026-09-19T06:56:15.452873Z","shell.execute_reply":"2026-09-19T06:56:15.460897Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import ipywidgets as widgets\nfrom IPython.display import display, clear_output\nimport io\n\nupload = widgets.FileUpload(accept='image/*', multiple=False,\n                            description='Загрузить галактику')\nbutton = widgets.Button(description='🔮 Предсказать', button_style='success')\noutput = widgets.Output()\n\ndef on_predict(b):\n    with output:\n        clear_output()\n        if not upload.value:\n            print(\"Сначала загрузите изображение\")\n            return\n\n        # Достаём байты\n        if isinstance(upload.value, dict):\n            _, info = next(iter(upload.value.items()))\n            content = info['content']\n        else:\n            content = upload.value[0]['content']\n\n        img = Image.open(io.BytesIO(content)).convert('RGB')\n        show_prediction(model, img)\n\nbutton.on_click(on_predict)\ndisplay(upload, button, output)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T06:56:18.457502Z","iopub.execute_input":"2026-09-19T06:56:18.457916Z","iopub.status.idle":"2026-09-19T06:56:18.475845Z","shell.execute_reply.started":"2026-09-19T06:56:18.457879Z","shell.execute_reply":"2026-09-19T06:56:18.474939Z"}},"outputs":[],"execution_count":null}]}