{"cells":[{"cell_type":"markdown","metadata":{},"source":"<div style=\"padding:22px 26px;border:1px solid #dbe4ee;border-left:6px solid #176b87;border-radius:12px;background:linear-gradient(120deg,#f8fbfd,#edf7f6)\">\n<div style=\"font-size:12px;font-weight:700;letter-spacing:.08em;color:#d1495b\">VERSION 5</div>\n<h1 style=\"margin:3px 0 0;color:#102a43\">RSNA Knee Baseline</h1>\n<p style=\"margin:8px 0 0;color:#486581\"><b>weak-label 2.5D student</b> — Use all 4,407 studies: reports supervise training, while inference remains image- and protocol-only.</p>\n</div>\n","id":"cell-00"},{"cell_type":"markdown","metadata":{},"source":"## Version map\n\n| Version | Focus | Role |\n|---|---|---|\n| **V1** | audit | dataset structure and failure modes |\n| **V2** | metadata | protocol-only sanity baseline |\n| **V3** | teacher | report pseudo-labels without text leakage |\n| **V4** | images | gold-only 2.5D model |\n| **V5** ← current | student | weak-label 2.5D model + TTA |","id":"cell-01"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"import os, re, gc, random, hashlib, warnings\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom IPython.display import display, HTML\n\nwarnings.filterwarnings('ignore')\n\nclass CFG:\n    seed = 42\n    targets = ['ACL','MCL','Medial Meniscus','Lateral Meniscus',\n               'Medial OA','Lateral OA','PF OA','Effusion',\n               'Synovitis',\"Baker's\",'Contusion','Fracture']\n    work = Path('/kaggle/working') if Path('/kaggle').exists() else Path('work/notebook-output')\n\ndef seed_everything(seed=42):\n    random.seed(seed); np.random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n\ndef find_root():\n    candidates = [\n        Path('/kaggle/input/competitions/rsna-knee-abnormality-detection'),\n        Path('/kaggle/input/rsna-knee-abnormality-detection'),\n        Path('work/competition-csv'),\n    ]\n    candidates += list(Path('/kaggle/input').glob('*knee*')) if Path('/kaggle/input').exists() else []\n    for p in candidates:\n        if (p / 'train.csv').exists():\n            return p\n    raise FileNotFoundError('Competition data not found')\n\nseed_everything(CFG.seed)\nROOT = find_root()\nCFG.work.mkdir(parents=True, exist_ok=True)\n\nPALETTE = ['#176b87','#64ccc5','#dafffb','#f5b971','#d1495b']\nplt.rcParams.update({\n    'figure.dpi': 120, 'axes.spines.top': False, 'axes.spines.right': False,\n    'axes.titleweight': 'bold', 'axes.edgecolor': '#bcccdc',\n    'grid.color': '#e6eef4', 'figure.facecolor': 'white',\n    'axes.grid': True, 'axes.axisbelow': True\n})\nprint('data:', ROOT)","id":"cell-02"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"TARGETS = CFG.targets\ntrain = pd.read_csv(ROOT / 'train.csv')\nseries = pd.read_csv(ROOT / 'train_series.csv')\ntest = pd.read_csv(ROOT / 'test.csv')\ntest_series = pd.read_csv(ROOT / 'test_series.csv')\nsample = pd.read_csv(ROOT / 'sample_submission.csv')\n\nhas_gold = train[TARGETS].notna().all(axis=1)\ngold = train.loc[has_gold].reset_index(drop=True)\n\nfacts = [\n    ('train studies', f'{len(train):,}'),\n    ('gold studies', f'{has_gold.sum():,}'),\n    ('report-only', f'{(~has_gold).sum():,}'),\n    ('series', f'{len(series):,}'),\n    ('targets', len(TARGETS)),\n    ('visible test', len(test)),\n]\ncards = ''.join(\n    f'<div style=\"padding:12px 15px;border:1px solid #dbe4ee;border-radius:10px;min-width:105px\">'\n    f'<div style=\"font-size:20px;font-weight:700;color:#176b87\">{v}</div>'\n    f'<div style=\"font-size:11px;color:#627d98\">{k}</div></div>' for k,v in facts\n)\ndisplay(HTML(f'<div style=\"display:flex;gap:8px;flex-wrap:wrap\">{cards}</div>'))\n\nassert list(sample.columns) == ['StudyInstanceUID'] + TARGETS\nassert series['StudyInstanceUID'].nunique() == len(train)\nprint('reports at test:', 'Report' in test.columns)\nprint('PatientSex in train.csv:', 'PatientSex' in train.columns)","id":"cell-03"},{"cell_type":"markdown","metadata":{},"source":"## Data decisions\n\nThe final version keeps the audit close to the model: sparse gold labels, train-only reports, duplicated protocol flags and report fingerprints all change either supervision or validation.","id":"cell-04"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"fig, ax = plt.subplots(1, 2, figsize=(12, 3.8))\n\npos = gold[TARGETS].sum().sort_values()\nax[0].barh(pos.index, pos.values, color=PALETTE[0])\nax[0].set(title='Gold positives', xlabel=f'count among {len(gold)}')\nfor i, v in enumerate(pos.values):\n    ax[0].text(v + .35, i, str(int(v)), va='center', fontsize=8)\n\nn_series = series.groupby('StudyInstanceUID').size()\nbins = np.arange(n_series.min() - .5, n_series.max() + 1.5)\nax[1].hist(n_series, bins=bins, color=PALETTE[1], edgecolor='white')\nax[1].axvline(n_series.median(), color=PALETTE[4], ls='--', label=f'median {n_series.median():.0f}')\nax[1].set(title='Series per study', xlabel='series', ylabel='studies')\nax[1].legend(frameon=False)\nplt.tight_layout(); plt.show()\n\nprint(f'gold coverage: {has_gold.mean():.2%}')\nprint('gold studies with zero positives:', int((gold[TARGETS].sum(axis=1) == 0).sum()))\nprint('all studies contain planes:', series.groupby('StudyInstanceUID').Anatomical_Plane.nunique().value_counts().to_dict())\nprint('Fluid_Sensitive == Fat_Suppression:', bool(series.Fluid_Sensitive.equals(series.Fat_Suppression)))","id":"cell-05"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"def normalize_report(text):\n    text = re.sub(r'\\s+', ' ', str(text).lower()).strip()\n    return re.sub(r'\\b\\d+(?:[.,]\\d+)?\\b', '<n>', text)\n\nreport_key = train.Report.map(normalize_report)\ngroups = pd.DataFrame({'key': report_key, 'gold': has_gold}).groupby('key').gold.agg(['size','any','all'])\ncross = groups[(groups['size'] > 1) & groups['any'] & ~groups['all']]\n\nslot = series.assign(slot=series.Anatomical_Plane.str[0] + '_' + series.Fluid_Sensitive.astype(str))\nslot = pd.crosstab(slot.StudyInstanceUID, slot.slot).reindex(\n    columns=['A_0','A_1','C_0','C_1','S_0','S_1'], fill_value=0)\ncoverage = (slot > 0).mean().sort_values()\n\nfig, ax = plt.subplots(figsize=(7.6, 3.2))\nax.barh(coverage.index, coverage.values, color=PALETTE[0])\nax.set(xlim=(0,1.03), xlabel='share of studies', title='Protocol slot coverage')\nfor i, v in enumerate(coverage.values):\n    ax.text(v + .012, i, f'{v:.0%}', va='center', fontsize=8)\nplt.tight_layout(); plt.show()\n\nprint('normalized duplicate-report studies:', int(groups.loc[groups['size'] > 1, 'size'].sum()))\nprint('gold-to-unlabeled report groups:', len(cross))\nprint('visible test is only the scoring placeholder:', len(test) == 3)","id":"cell-06"},{"cell_type":"markdown","metadata":{},"source":"### Findings that change the pipeline\n\n- **58 / 4,407 studies are labeled**, and every gold study has at least one positive finding. Gold prevalence is selection-biased.\n- Reports cover the full training set but disappear at inference. Use them as a **teacher**, never as a test feature.\n- Every study contains sagittal, coronal and axial series. Fixed plane slots are a strong first design.\n- `Fluid_Sensitive` and `Fat_Suppression` are identical across 24,371 rows. Keep one.\n- Normalized report duplicates connect labeled and unlabeled studies. Group report fingerprints before validation.\n- `test.csv` contains three placeholder studies. Train/test shift estimates from this sample are inconclusive.\n","id":"cell-07"},{"cell_type":"markdown","metadata":{},"source":"## Report teacher\n\nMultilingual rules anchor common findings and negations; character and word TF-IDF add local phrasing. Teacher probabilities supervise the unlabeled image studies, while all 58 gold rows retain their original targets and higher weight.","id":"cell-08"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"def protocol_features(df, study_ids=None):\n    x = df.copy()\n    x['slot'] = x.Anatomical_Plane.str[0] + '_' + x.Fluid_Sensitive.astype(str)\n    f = pd.crosstab(x.StudyInstanceUID, x.slot)\n    slots = ['A_0','A_1','C_0','C_1','S_0','S_1']\n    f = f.reindex(columns=slots, fill_value=0)\n    agg = x.groupby('StudyInstanceUID').agg(\n        n_series=('SeriesInstanceUID','size'),\n        n_planes=('Anatomical_Plane','nunique'),\n        n_fluid=('Fluid_Sensitive','sum'))\n    f = f.join(agg).astype(np.float32)\n    if study_ids is not None:\n        f = f.reindex(study_ids, fill_value=0)\n    return f\n\ntrain_meta = protocol_features(series, train.StudyInstanceUID)\ntest_meta = protocol_features(test_series, test.StudyInstanceUID)\nFEATURES = train_meta.columns.tolist()\ndisplay(train_meta.describe().T.round(2))","id":"cell-09"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"def save_submission(pred, name='submission.csv'):\n    pred = np.asarray(pred, dtype=np.float64)\n    assert pred.shape == (len(test), len(TARGETS))\n    assert np.isfinite(pred).all()\n    out = sample[['StudyInstanceUID']].copy()\n    out[TARGETS] = np.clip(pred, 1e-5, 1 - 1e-5)\n    path = CFG.work / name\n    out.to_csv(path, index=False)\n    assert list(out.columns) == ['StudyInstanceUID'] + TARGETS\n    print(path, out.shape, f'[{out[TARGETS].min().min():.4f}, {out[TARGETS].max().max():.4f}]')\n    return out\n","id":"cell-10"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"from sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.linear_model import LogisticRegression, Ridge\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score\nfrom scipy import sparse\n\nTERMS = {\n    'ACL': r'\\bacl\\b|anterior cruciate|ligament croise anterieur|\\blca\\b|vorder.{0,8}kreuzband',\n    'MCL': r'\\bmcl\\b|medial collateral|ligament collateral medial|innenband',\n    'Medial Meniscus': r'medial menisc|menisco medial|menisque interne|innenmeniskus',\n    'Lateral Meniscus': r'lateral menisc|menisco lateral|menisque externe|aussenmeniskus|außenmeniskus',\n    'Medial OA': r'medial.{0,35}(osteoarth|arthros|chondr|degenerat|joint space)',\n    'Lateral OA': r'lateral.{0,35}(osteoarth|arthros|chondr|degenerat|joint space)',\n    'PF OA': r'patellofemoral|femoropatel|retropatell|chondromalacia',\n    'Effusion': r'effusion|joint fluid|epanchement|erguss|derrame articular',\n    'Synovitis': r'synovit|sinovit|synovial thick|synovial hypertroph',\n    \"Baker's\": r'baker.{0,8}cyst|popliteal cyst|kyste poplite|baker.?zyste',\n    'Contusion': r'contusion|bone bruise|bone marrow (edema|oedema)|knochenmark',\n    'Fracture': r'fractur|fraktur|fractura|avulsion|segond',\n}\nNEG = re.compile(r'\\b(no|not|without|absence|negative|normal|intact|kein|keine|geen|sans|aucun|sin|yok)\\b', re.I)\n\ndef rule_scores(text):\n    text = re.sub(r'\\s+', ' ', str(text).lower())\n    out = []\n    for target in TARGETS:\n        hits = list(re.finditer(TERMS[target], text, re.I))\n        if not hits:\n            out.append(.08); continue\n        asserted = False\n        for h in hits:\n            window = text[max(0, h.start()-55):min(len(text), h.end()+55)]\n            if not NEG.search(window):\n                asserted = True; break\n        out.append(.88 if asserted else .12)\n    return out\n\ntexts = train.Report.fillna('').str.lower().values\nrules = np.asarray([rule_scores(t) for t in texts], dtype=np.float32)\nchar = TfidfVectorizer(analyzer='char_wb', ngram_range=(3,5), min_df=3, max_features=120_000, sublinear_tf=True)\nword = TfidfVectorizer(ngram_range=(1,2), min_df=2, max_features=70_000, sublinear_tf=True)\nX_text = sparse.hstack([char.fit_transform(texts), word.fit_transform(texts), sparse.csr_matrix(rules)]).tocsr()\ngold_idx = np.flatnonzero(has_gold.values)\nteacher = np.zeros((len(train), len(TARGETS)), dtype=np.float32)\nteacher_oof = np.zeros((len(gold), len(TARGETS)), dtype=np.float32)\nteacher_rows = []\n\nfor j, target in enumerate(TARGETS):\n    y = gold[target].astype(int).values\n    cv = StratifiedKFold(4, shuffle=True, random_state=CFG.seed)\n    for tr_idx, va_idx in cv.split(gold_idx, y):\n        m = LogisticRegression(C=2, class_weight='balanced', solver='liblinear', max_iter=700)\n        m.fit(X_text[gold_idx[tr_idx]], y[tr_idx])\n        teacher_oof[va_idx, j] = m.predict_proba(X_text[gold_idx[va_idx]])[:,1]\n    m = LogisticRegression(C=2, class_weight='balanced', solver='liblinear', max_iter=700)\n    m.fit(X_text[gold_idx], y)\n    teacher[:, j] = .75 * m.predict_proba(X_text)[:,1] + .25 * rules[:,j]\n    teacher_rows.append((target, roc_auc_score(y, teacher_oof[:,j])))\n\nteacher[gold_idx] = gold[TARGETS].values\nteacher_score = pd.DataFrame(teacher_rows, columns=['target','text_oof_auc'])\nprint('teacher OOF macro:', round(teacher_score.text_oof_auc.mean(), 3))\ndisplay(teacher_score.round(3))\n\nview = teacher_score.sort_values('text_oof_auc')\nfig, ax = plt.subplots(figsize=(7.2,3.6))\nax.barh(view.target, view.text_oof_auc, color=[PALETTE[4] if v < .5 else PALETTE[0] for v in view.text_oof_auc])\nax.axvline(.5, color='#627d98', ls='--')\nax.set(xlim=(.3,1), xlabel='OOF AUC on 58 gold studies', title='Report teacher: useful, uneven, noisy')\nplt.tight_layout(); plt.show()\n\npseudo = train[['StudyInstanceUID']].copy()\npseudo[TARGETS] = teacher\npseudo['is_hard'] = has_gold.astype(int)\npseudo.to_csv(CFG.work / 'pseudo_labels.csv', index=False)","id":"cell-11"},{"cell_type":"markdown","metadata":{},"source":"## Image student\n\nThe student reads two 2.5D triplets from each anatomical plane. Gold studies are repeated, soft-label rows are down-weighted, and horizontal TTA stabilizes inference. A 12% metadata blend adds cheap model diversity without introducing train-only text.","id":"cell-12"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"import pydicom\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport timm\nfrom functools import lru_cache\n\nclass IMG:\n    size = 192\n    triplets = 3\n    batch = 3\n    workers = 2\n    epochs = 8\n    lr = 3e-4\n    wd = 1e-3\n    backbone = 'resnet18'\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    amp = torch.cuda.is_available()\n\ntorch.manual_seed(CFG.seed)\ntorch.cuda.manual_seed_all(CFG.seed)\nprint('device:', IMG.device)","id":"cell-13"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"PLANES = ['Sagittal','Coronal','Axial']\n\n@lru_cache(maxsize=18000)\ndef ordered_paths(folder_text):\n    folder = Path(folder_text)\n    rows = []\n    for i, path in enumerate(folder.glob('*.dcm')):\n        try:\n            ds = pydicom.dcmread(path, stop_before_pixels=True, force=True)\n            if hasattr(ds, 'ImageOrientationPatient') and hasattr(ds, 'ImagePositionPatient'):\n                orient = np.asarray(ds.ImageOrientationPatient, dtype=float)\n                normal = np.cross(orient[:3], orient[3:])\n                key = float(np.dot(np.asarray(ds.ImagePositionPatient, dtype=float), normal))\n            else:\n                key = float(getattr(ds, 'InstanceNumber', i))\n            rows.append((key, str(path)))\n        except Exception:\n            rows.append((float(i), str(path)))\n    return tuple(p for _, p in sorted(rows))\n\ndef decode_triplets(folder, count=3, size=192):\n    paths = ordered_paths(str(folder))\n    if not paths:\n        raise FileNotFoundError(folder)\n    centers = np.linspace(0, len(paths)-1, count+2)[1:-1].round().astype(int)\n    images = []\n    for c in centers:\n        ids = [max(0,c-1), c, min(len(paths)-1,c+1)]\n        stack = []\n        for i in ids:\n            ds = pydicom.dcmread(paths[i], force=True)\n            x = ds.pixel_array.astype(np.float32)\n            x = x * float(getattr(ds, 'RescaleSlope', 1.0)) + float(getattr(ds, 'RescaleIntercept', 0.0))\n            stack.append(x)\n        x = np.stack(stack)\n        lo, hi = np.percentile(x, [.5, 99.5])\n        x = np.clip((x-lo)/(hi-lo+1e-6), 0, 1).astype(np.float32, copy=False)\n        x = torch.from_numpy(x).unsqueeze(0)\n        x = F.interpolate(x, (size,size), mode='bilinear', align_corners=False).squeeze(0)\n        images.append(x)\n    return torch.stack(images)\n\ndef choose_series(frame):\n    if frame.empty:\n        return [None] * len(PLANES)\n    chosen = []\n    for plane in PLANES:\n        q = frame[frame.Anatomical_Plane.eq(plane)].sort_values(\n            ['Fluid_Sensitive','SeriesInstanceUID'], ascending=[False,True])\n        chosen.append(None if q.empty else q.iloc[0])\n    return chosen\n\nclass KneeDataset(Dataset):\n    def __init__(self, frame, series_frame, image_root, targets=None, sample_weight=None, augment=False):\n        self.frame = frame.reset_index(drop=True)\n        self.groups = {k:v for k,v in series_frame.groupby('StudyInstanceUID')}\n        self.image_root = Path(image_root)\n        self.targets = targets\n        self.sample_weight = sample_weight\n        self.augment = augment\n\n    def __len__(self): return len(self.frame)\n\n    def __getitem__(self, idx):\n        row = self.frame.iloc[idx]\n        uid = row.StudyInstanceUID\n        items, plane_ids, masks = [], [], []\n        for plane_id, item in enumerate(choose_series(self.groups.get(uid, pd.DataFrame()))):\n            try:\n                folder = self.image_root / uid / item.SeriesInstanceUID\n                x = decode_triplets(folder, IMG.triplets, IMG.size)\n                valid = True\n            except Exception:\n                x = torch.zeros(IMG.triplets,3,IMG.size,IMG.size)\n                valid = False\n            if self.augment and random.random() < .5:\n                x = torch.flip(x, dims=[-1])\n            items.append(x)\n            plane_ids.extend([plane_id] * IMG.triplets)\n            masks.extend([valid] * IMG.triplets)\n        x = torch.cat(items, dim=0)\n        y = torch.zeros(len(TARGETS)) if self.targets is None else torch.tensor(self.targets[idx], dtype=torch.float32)\n        w = 1.0 if self.sample_weight is None else float(self.sample_weight[idx])\n        return x, torch.tensor(plane_ids), torch.tensor(masks), y, torch.tensor(w), uid\n\nclass KneeNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.encoder = timm.create_model(IMG.backbone, pretrained=False, num_classes=0, global_pool='avg')\n        dim = self.encoder.num_features\n        self.plane = nn.Embedding(3, dim)\n        self.attn = nn.Sequential(nn.Linear(dim,128), nn.Tanh(), nn.Linear(128,1))\n        self.head = nn.Sequential(nn.Dropout(.3), nn.Linear(dim,len(TARGETS)))\n\n    def forward(self, x, plane, mask):\n        b,k,c,h,w = x.shape\n        f = self.encoder(x.reshape(b*k,c,h,w)).reshape(b,k,-1)\n        f = f + self.plane(plane)\n        a = self.attn(f).squeeze(-1).masked_fill(~mask, -1e4)\n        z = (f * a.softmax(-1).unsqueeze(-1)).sum(1)\n        return self.head(z)\n\nprint('DICOM loader: geometric ordering -> percentile scaling -> 2.5D triplets')","id":"cell-14"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"def fit_image_model(train_frame, targets, weights, epochs=None):\n    epochs = IMG.epochs if epochs is None else epochs\n    ds = KneeDataset(train_frame, series, ROOT/'train_series', targets, weights, augment=True)\n    loader = DataLoader(ds, batch_size=IMG.batch, shuffle=True, num_workers=IMG.workers,\n                        pin_memory=True, persistent_workers=IMG.workers > 0)\n    model = KneeNet().to(IMG.device)\n    opt = torch.optim.AdamW(model.parameters(), lr=IMG.lr, weight_decay=IMG.wd)\n    pos = np.clip(np.asarray(targets).sum(0), 1, None)\n    neg = len(targets) - pos\n    pos_weight = torch.tensor(np.clip(neg/pos,1,8), dtype=torch.float32, device=IMG.device)\n    scaler = torch.cuda.amp.GradScaler(enabled=IMG.amp)\n\n    for epoch in range(epochs):\n        model.train(); losses=[]\n        for x, plane, mask, y, w, _ in loader:\n            x,plane,mask,y,w = x.to(IMG.device),plane.to(IMG.device),mask.to(IMG.device),y.to(IMG.device),w.to(IMG.device)\n            opt.zero_grad(set_to_none=True)\n            with torch.cuda.amp.autocast(enabled=IMG.amp):\n                logits = model(x,plane,mask)\n                raw = F.binary_cross_entropy_with_logits(logits,y,pos_weight=pos_weight,reduction='none').mean(1)\n                loss = (raw*w).sum()/w.sum().clamp_min(1)\n            scaler.scale(loss).backward()\n            scaler.unscale_(opt); torch.nn.utils.clip_grad_norm_(model.parameters(),1.0)\n            scaler.step(opt); scaler.update(); losses.append(loss.item())\n        print(f'epoch {epoch+1:02d}/{epochs} loss={np.mean(losses):.4f}')\n    return model\n\n@torch.no_grad()\ndef predict_image(model, flip=False):\n    ds = KneeDataset(test, test_series, ROOT/'test_series')\n    loader = DataLoader(ds, batch_size=IMG.batch, shuffle=False, num_workers=IMG.workers,\n                        pin_memory=True, persistent_workers=IMG.workers > 0)\n    model.eval(); pred=[]; ids=[]\n    for x,plane,mask,_,_,uid in loader:\n        x,plane,mask = x.to(IMG.device),plane.to(IMG.device),mask.to(IMG.device)\n        with torch.cuda.amp.autocast(enabled=IMG.amp):\n            p = torch.sigmoid(model(x,plane,mask))\n            if flip:\n                p = .5 * (p + torch.sigmoid(model(torch.flip(x,[-1]),plane,mask)))\n        pred.append(p.float().cpu().numpy()); ids.extend(uid)\n    return np.concatenate(pred), ids","id":"cell-15"},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":"# hard rows are repeated so 58 audited targets remain visible to the optimizer\ntarget_frame = train[['StudyInstanceUID']].copy()\nsoft_y = teacher.astype(np.float32)\nsoft_w = np.full(len(train), .35, dtype=np.float32)\nsoft_w[has_gold.values] = 1.0\n\nhard_repeat = 8\nhard_frame = pd.concat([train.loc[has_gold, ['StudyInstanceUID']]] * hard_repeat, ignore_index=True)\nhard_targets = np.tile(gold[TARGETS].values.astype(np.float32), (hard_repeat,1))\n\nmix_frame = pd.concat([target_frame, hard_frame], ignore_index=True)\nmix_targets = np.concatenate([soft_y, hard_targets])\nmix_weights = np.concatenate([soft_w, np.ones(len(hard_frame), dtype=np.float32)])\n\nIMG.epochs = 2\nIMG.triplets = 2\nmodel = fit_image_model(mix_frame, mix_targets, mix_weights, epochs=IMG.epochs)\nimage_pred, ids = predict_image(model, flip=True)\nassert ids == test.StudyInstanceUID.tolist()\n\n# low-weight metadata blend: cheap diversity, not calibration\nX_all = train_meta.values; Xt = test_meta.values\nmeta_pred = np.zeros_like(image_pred)\nfor j in range(len(TARGETS)):\n    m = make_pipeline(StandardScaler(), Ridge(alpha=8.0))\n    m.fit(X_all, teacher[:,j], ridge__sample_weight=np.where(has_gold.values,8.0,1.0))\n    meta_pred[:,j] = m.predict(Xt)\n\nfinal_pred = .88*image_pred + .12*np.clip(meta_pred,.02,.98)\ntorch.save(model.state_dict(), CFG.work/'knee_v5.pt')\nsubmission = save_submission(final_pred)\nsubmission.head()","id":"cell-16"}],"metadata":{"kaggle":{"accelerator":"gpu","dataSources":[],"dockerImageVersionId":null},"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3"}},"nbformat":4,"nbformat_minor":5}