{"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":"\"\"\"\nRSNA Knee Abnormality Detection — полный baseline-пайплайн.\n\nDINOv2 (изображения) + multilingual BERT/MiniLM (текст отчёта) -> MLP -> 12 меток.\n\nПЕРЕД ЗАПУСКОМ:\n1. Settings -> Accelerator -> GPU T4 x2\n2. Input должен содержать: датасет соревнования + модель DINOv2 + мультиязычную\n   текстовую модель (все три добавлены через Add Input, как уже сделано)\n3. Полный прогон на всём train/test может занять много времени -- код сначала\n   печатает статистику, чтобы можно было оценить масштаб до полного запуска\n\"\"\"\n\nimport glob\nimport os\n\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader, Dataset\nfrom transformers import AutoImageProcessor, AutoModel, AutoTokenizer\n\n# =============================================================================\n# 1. Автопоиск путей -- ничего не вбиваем руками, находим сами\n# =============================================================================\ndef find_data_dir() -> str:\n    candidates = glob.glob(\"/kaggle/input/**/train.csv\", recursive=True)\n    if not candidates:\n        raise FileNotFoundError(\"train.csv не найден в /kaggle/input -- проверь Input\")\n    return os.path.dirname(candidates[0])\n\n\ndef find_model_dir(marker_file: str, exclude_substr: str = \"\") -> str:\n    \"\"\"Ищет папку, где лежит характерный файл модели (например config.json\n    рядом с pytorch_model.bin), пропуская папки, похожие на чужие ноутбуки.\"\"\"\n    candidates = glob.glob(f\"/kaggle/input/**/{marker_file}\", recursive=True)\n    for c in candidates:\n        d = os.path.dirname(c)\n        if exclude_substr and exclude_substr in d:\n            continue\n        # модель должна содержать config.json рядом\n        if os.path.exists(os.path.join(d, \"config.json\")):\n            return d\n    raise FileNotFoundError(f\"Не нашли модель с файлом {marker_file}\")\n\n\nDATA_DIR = find_data_dir()\nTRAIN_CSV = os.path.join(DATA_DIR, \"train.csv\")\nTRAIN_SERIES_CSV = os.path.join(DATA_DIR, \"train_series.csv\")\nTRAIN_SERIES_DIR = os.path.join(DATA_DIR, \"train_series\")\nTEST_CSV = os.path.join(DATA_DIR, \"test.csv\")\nTEST_SERIES_CSV = os.path.join(DATA_DIR, \"test_series.csv\")\nTEST_SERIES_DIR = os.path.join(DATA_DIR, \"test_series\")\nSAMPLE_SUB = os.path.join(DATA_DIR, \"sample_submission.csv\")\n\ndef find_model_dir_by_keyword(keyword: str) -> str:\n    \"\"\"Ищет папку модели, в пути которой встречается характерное слово\n    (например 'dinov2' или 'minilm'), чтобы не перепутать две разные модели.\"\"\"\n    candidates = glob.glob(\"/kaggle/input/**/config.json\", recursive=True)\n    for c in candidates:\n        d = os.path.dirname(c)\n        if keyword.lower() in d.lower():\n            return d\n    raise FileNotFoundError(f\"Не нашли модель с ключевым словом '{keyword}'\")\n\n\nIMAGE_MODEL_PATH = find_model_dir_by_keyword(\"dinov2\")\nTEXT_MODEL_PATH = find_model_dir_by_keyword(\"minilm\")\n\nprint(\"DATA_DIR:\", DATA_DIR)\nprint(\"IMAGE_MODEL_PATH:\", IMAGE_MODEL_PATH)\nprint(\"TEXT_MODEL_PATH:\", TEXT_MODEL_PATH)\n\nLABEL_COLS = [\n    \"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\", \"Medial OA\",\n    \"Lateral OA\", \"PF OA\", \"Effusion\", \"Synovitis\", \"Baker's\",\n    \"Contusion\", \"Fracture\",\n]\n\nSLICES_PER_SERIES = 5\nMAX_SERIES_PER_STUDY = 4\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device)\n\n\n# =============================================================================\n# 2. Таблицы\n# =============================================================================\ntrain_df = pd.read_csv(TRAIN_CSV)\nseries_df = pd.read_csv(TRAIN_SERIES_CSV)\ntest_df = pd.read_csv(TEST_CSV)\ntest_series_df = pd.read_csv(TEST_SERIES_CSV)\n\nlabeled_df = train_df.dropna(subset=LABEL_COLS).reset_index(drop=True)\nprint(f\"Study в train.csv: {len(train_df)} | с полными метками: {len(labeled_df)}\")\nprint(f\"Study в test.csv: {len(test_df)}\")\n\n\n# =============================================================================\n# 3. DICOM-загрузка\n# =============================================================================\ndef load_dicom_slice(path: str) -> np.ndarray:\n    dcm = pydicom.dcmread(path)\n    arr = dcm.pixel_array.astype(np.float32)\n    arr -= arr.min()\n    if arr.max() > 0:\n        arr = arr / arr.max()\n    return (arr * 255).astype(np.uint8)\n\n\ndef get_study_slices(study_id: str, series_table: pd.DataFrame, series_root: str) -> list[np.ndarray]:\n    study_series = series_table[series_table[\"StudyInstanceUID\"] == study_id]\n    slices = []\n    for _, row in study_series.head(MAX_SERIES_PER_STUDY).iterrows():\n        series_dir = os.path.join(series_root, study_id, row[\"SeriesInstanceUID\"])\n        if not os.path.isdir(series_dir):\n            continue\n        files = sorted(os.listdir(series_dir))\n        if not files:\n            continue\n        idxs = np.linspace(0, len(files) - 1, min(SLICES_PER_SERIES, len(files))).astype(int)\n        for i in idxs:\n            try:\n                slices.append(load_dicom_slice(os.path.join(series_dir, files[i])))\n            except Exception as e:\n                print(f\"Skip slice in {study_id}: {e}\")\n    return slices\n\n\n# =============================================================================\n# 4. Энкодеры\n# =============================================================================\nimage_processor = AutoImageProcessor.from_pretrained(IMAGE_MODEL_PATH)\nimage_model = AutoModel.from_pretrained(IMAGE_MODEL_PATH).to(device).eval()\n\ntokenizer = AutoTokenizer.from_pretrained(TEXT_MODEL_PATH)\ntext_model = AutoModel.from_pretrained(TEXT_MODEL_PATH).to(device).eval()\n\n\n@torch.no_grad()\ndef encode_images(slices: list[np.ndarray]) -> np.ndarray:\n    if not slices:\n        return np.zeros(image_model.config.hidden_size, dtype=np.float32)\n    rgb_slices = [np.stack([s, s, s], axis=-1) for s in slices]\n    inputs = image_processor(images=rgb_slices, return_tensors=\"pt\").to(device)\n    outputs = image_model(**inputs)\n    embeddings = outputs.last_hidden_state[:, 0, :].cpu().numpy()\n    return embeddings.mean(axis=0)\n\n\n@torch.no_grad()\ndef encode_text(report) -> np.ndarray:\n    if not isinstance(report, str) or not report.strip():\n        return np.zeros(text_model.config.hidden_size, dtype=np.float32)\n    inputs = tokenizer(\n        report, return_tensors=\"pt\", truncation=True, max_length=512, padding=True\n    ).to(device)\n    outputs = text_model(**inputs)\n    return outputs.last_hidden_state.mean(dim=1).squeeze(0).cpu().numpy()\n\n\ndef build_features(df: pd.DataFrame, series_table: pd.DataFrame, series_root: str, has_labels: bool):\n    image_feats, text_feats, labels, ids = [], [], [], []\n    for i, row in df.iterrows():\n        study_id = row[\"StudyInstanceUID\"]\n        slices = get_study_slices(study_id, series_table, series_root)\n        image_feats.append(encode_images(slices))\n        text_feats.append(encode_text(row.get(\"Report\", \"\")))\n        ids.append(study_id)\n        if has_labels:\n            labels.append(row[LABEL_COLS].values.astype(np.float32))\n        if i % 20 == 0:\n            print(f\"  Обработано {i}/{len(df)}\")\n    image_feats = np.stack(image_feats)\n    text_feats = np.stack(text_feats)\n    labels = np.stack(labels) if has_labels else None\n    return image_feats, text_feats, labels, ids\n\n\n# =============================================================================\n# 5. Признаки на train (только размеченные study)\n# =============================================================================\nprint(\"\\nИзвлекаем признаки TRAIN...\")\ntrain_img_feats, train_txt_feats, train_labels, _ = build_features(\n    labeled_df, series_df, TRAIN_SERIES_DIR, has_labels=True\n)\nprint(\"Формы train:\", train_img_feats.shape, train_txt_feats.shape, train_labels.shape)\n\n\n# =============================================================================\n# 6. Модель и обучение\n# =============================================================================\nclass FusionDataset(Dataset):\n    def __init__(self, img_feats, txt_feats, labels):\n        self.x = np.concatenate([img_feats, txt_feats], axis=1)\n        self.y = labels\n\n    def __len__(self):\n        return len(self.y)\n\n    def __getitem__(self, idx):\n        return torch.tensor(self.x[idx], dtype=torch.float32), torch.tensor(self.y[idx], dtype=torch.float32)\n\n\nclass MLPHead(nn.Module):\n    def __init__(self, input_dim: int, num_labels: int = 12):\n        super().__init__()\n        self.net = nn.Sequential(\n            nn.Linear(input_dim, 256), nn.ReLU(), nn.Dropout(0.3),\n            nn.Linear(256, num_labels),\n        )\n\n    def forward(self, x):\n        return self.net(x)\n\n\ndataset = FusionDataset(train_img_feats, train_txt_feats, train_labels)\nloader = DataLoader(dataset, batch_size=16, shuffle=True)\n\ninput_dim = train_img_feats.shape[1] + train_txt_feats.shape[1]\nmodel = MLPHead(input_dim).to(device)\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-3)\ncriterion = nn.BCEWithLogitsLoss()\n\nprint(\"\\nОбучение...\")\nEPOCHS = 20\nfor epoch in range(EPOCHS):\n    model.train()\n    total_loss = 0.0\n    for x, y in loader:\n        x, y = x.to(device), y.to(device)\n        optimizer.zero_grad()\n        loss = criterion(model(x), y)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n    print(f\"Epoch {epoch+1}/{EPOCHS}, loss={total_loss/len(loader):.4f}\")\n\ntorch.save(model.state_dict(), \"/kaggle/working/mlp_head.pt\")\n\n\n# =============================================================================\n# 7. Признаки на test + предсказание + submission.csv\n# =============================================================================\nprint(\"\\nИзвлекаем признаки TEST...\")\ntest_img_feats, test_txt_feats, _, test_ids = build_features(\n    test_df, test_series_df, TEST_SERIES_DIR, has_labels=False\n)\n\nmodel.eval()\nwith torch.no_grad():\n    x_test = torch.tensor(\n        np.concatenate([test_img_feats, test_txt_feats], axis=1), dtype=torch.float32\n    ).to(device)\n    logits = model(x_test)\n    probs = torch.sigmoid(logits).cpu().numpy()\n\nsubmission = pd.DataFrame(probs, columns=LABEL_COLS)\nsubmission.insert(0, \"StudyInstanceUID\", test_ids)\nsubmission.to_csv(\"/kaggle/working/submission.csv\", index=False)\nprint(\"\\nГотово! submission.csv сохранён:\")\nprint(submission.head())","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-08-07T13:42:41.633406Z","iopub.execute_input":"2026-08-07T13:42:41.633619Z","iopub.status.idle":"2026-08-07T13:47:44.362481Z","shell.execute_reply.started":"2026-08-07T13:42:41.633595Z","shell.execute_reply":"2026-08-07T13:47:44.361735Z"}},"outputs":[],"execution_count":null}]}