{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Multimodal Multi-Plane 2.5D Convolutional Network and Multilingual Weakly Supervised Learning for RSNA Knee Abnormality Detection\n\n## Overview\n\nKnee magnetic resonance imaging (MRI) is the primary non-invasive diagnostic modality for evaluating joint articular structures, soft tissue ligaments, meniscal integrity, and subchondral bone pathologies. In this competition, models are developed to classify twelve distinct pathological findings across multi-sequence, multi-plane volumetric knee MRI examinations.\n\n## Mathematical Problem Formulation\n\nLet $\\mathcal{X}_i = \\{ \\mathbf{V}_{i, \\text{Sagittal}}, \\mathbf{V}_{i, \\text{Coronal}}, \\mathbf{V}_{i, \\text{Axial}} \\}$ denote the multi-planar volumetric MRI sequences associated with study $i \\in \\{1, 2, \\dots, N\\}$. Each study is evaluated against $C = 12$ binary target pathologies:\n$$\\mathbf{y}_i = [y_{i,1}, y_{i,2}, \\dots, y_{i,12}]^T \\in \\{0, 1\\}^{12}$$\n\nThe objective is to estimate a parameterized mapping function $f_{\\theta}: \\mathcal{X}_i \\rightarrow [0, 1]^{12}$ that maximizes the Macro-Averaged Area Under the Receiver Operating Characteristic Curve (Macro-AUC ROC) across all twelve targets:\n\n$$\\text{Macro-AUC} = \\frac{1}{C} \\sum_{c=1}^{C} \\text{AUC}_c$$\n\nwhere the per-class AUC for target $c$ is defined as:\n\n$$\\text{AUC}_c = \\frac{1}{|\\mathcal{P}_c| |\\mathcal{N}_c|} \\sum_{i \\in \\mathcal{P}_c} \\sum_{j \\in \\mathcal{N}_c} \\mathbb{I}\\left(f_{c}(\\mathcal{X}_i) > f_{c}(\\mathcal{X}_j)\\right)$$\n\nHere, $\\mathcal{P}_c = \\{i \\mid y_{i,c} = 1\\}$ and $\\mathcal{N}_c = \\{j \\mid y_{j,c} = 0\\}$ represent the positive and negative patient sets for condition $c$, and $\\mathbb{I}(\\cdot)$ is the indicator function.\n\n## Target Pathology Framework\n\nThe twelve clinical target variables encompass distinct structural and inflammatory knee conditions:\n1. **ACL**: Anterior Cruciate Ligament Injury ($c=1$)\n2. **MCL**: Medial Collateral Ligament Injury ($c=2$)\n3. **Medial Meniscus**: Medial Meniscal Tear or Degeneration ($c=3$)\n4. **Lateral Meniscus**: Lateral Meniscal Tear or Degeneration ($c=4$)\n5. **Medial OA**: Osteoarthritis of Medial Tibiofemoral Compartment ($c=5$)\n6. **Lateral OA**: Osteoarthritis of Lateral Tibiofemoral Compartment ($c=6$)\n7. **PF OA**: Patellofemoral Osteoarthritis ($c=7$)\n8. **Effusion**: Intra-articular Joint Effusion ($c=8$)\n9. **Synovitis**: Synovial Membrane Inflammation ($c=9$)\n10. **Baker's**: Popliteal / Baker's Cyst ($c=10$)\n11. **Contusion**: Subchondral Bone Marrow Edema / Bruise ($c=11$)\n12. **Fracture**: Cortical or Trabecular Bone Fracture ($c=12$)\n\n## Multimodal Weak Supervision Strategy\n\nOnly 58 of the 4,407 training studies possess explicit gold-standard binary labels. The remaining 4,349 studies contain unlabelled target attributes paired with free-text radiology reports in multiple languages (English, Spanish, German, French, Greek, Bulgarian, Turkish). A multilingual natural language processing (NLP) parser extracts clinical diagnostic findings directly from these 4,349 free-text reports, generating full weak supervision labels for the training set. A multi-plane 2.5D deep convolutional model with slice-attention pooling is then trained on the expanded dataset.\n","metadata":{}},{"cell_type":"code","source":"import os\nimport sys\nimport re\nimport gc\nimport time\nimport math\nimport random\nimport glob\nimport warnings\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport plotly.express as px\nimport plotly.graph_objects as go\nimport plotly.io as pio\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\n\nimport pydicom\nfrom PIL import Image\nimport cv2\nfrom sklearn.metrics import roc_auc_score, confusion_matrix, classification_report\nfrom sklearn.model_selection import StratifiedGroupKFold\n\n# Configure System Environment\nwarnings.filterwarnings('ignore')\npio.renderers.default = 'iframe'\n\ndef seed_everything(seed=42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nseed_everything(42)\n\n# Verify GPU Hardware\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"System Computation Device: {device}\")\nif torch.cuda.is_available():\n    print(f\"GPU Model: {torch.cuda.get_device_name(0)}\")\n    print(f\"GPU Count: {torch.cuda.device_count()}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:40:16.304799Z","iopub.execute_input":"2026-08-06T17:40:16.305376Z","iopub.status.idle":"2026-08-06T17:40:26.4725Z","shell.execute_reply.started":"2026-08-06T17:40:16.305346Z","shell.execute_reply":"2026-08-06T17:40:26.471694Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Hyperparameter & Path Configuration\n\nThe `Config` class establishes centralized system parameters, directory paths, model input dimensions, slice sampling rates, and multi-label targets.\n","metadata":{}},{"cell_type":"code","source":"class Config:\n    # Directory Structure\n    BASE_DIR = Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\")\n    TRAIN_CSV = BASE_DIR / \"train.csv\"\n    TRAIN_SERIES_CSV = BASE_DIR / \"train_series.csv\"\n    TEST_CSV = BASE_DIR / \"test.csv\"\n    TEST_SERIES_CSV = BASE_DIR / \"test_series.csv\"\n    SAMPLE_SUB_CSV = BASE_DIR / \"sample_submission.csv\"\n    \n    TRAIN_SERIES_DIR = BASE_DIR / \"train_series\"\n    TEST_SERIES_DIR = BASE_DIR / \"test_series\"\n    \n    # Target Definitions\n    TARGET_COLS = [\n        'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus',\n        'Medial OA', 'Lateral OA', 'PF OA', 'Effusion',\n        'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n    ]\n    NUM_CLASSES = 12\n    \n    # Model & Preprocessing Parameters\n    IMAGE_SIZE = (224, 224)\n    SLICES_PER_VIEW = 5  # Equidistant slices extracted per anatomical plane\n    BATCH_SIZE = 8\n    NUM_WORKERS = 2\n    EPOCHS = 5\n    LEARNING_RATE = 2e-4\n    WEIGHT_DECAY = 1e-4\n    SEED = 42\n    \n    # Output File\n    OUTPUT_SUBMISSION = \"submission.csv\"\n\nconfig = Config()\nprint(\"Configuration parameters loaded successfully.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:40:26.474068Z","iopub.execute_input":"2026-08-06T17:40:26.474388Z","iopub.status.idle":"2026-08-06T17:40:26.480696Z","shell.execute_reply.started":"2026-08-06T17:40:26.474364Z","shell.execute_reply":"2026-08-06T17:40:26.479919Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Exploratory Data Analysis & Metadata Validation\n\nMetadata files are loaded and inspected to characterize label distribution, report length metrics, language diversity, and MRI series plane frequency.\n","metadata":{}},{"cell_type":"code","source":"train_df = pd.read_csv(config.TRAIN_CSV)\ntrain_series_df = pd.read_csv(config.TRAIN_SERIES_CSV)\ntest_df = pd.read_csv(config.TEST_CSV)\ntest_series_df = pd.read_csv(config.TEST_SERIES_CSV)\nsample_sub_df = pd.read_csv(config.SAMPLE_SUB_CSV)\n\nprint(f\"train.csv shape: {train_df.shape}\")\nprint(f\"train_series.csv shape: {train_series_df.shape}\")\nprint(f\"test.csv shape: {test_df.shape}\")\nprint(f\"test_series.csv shape: {test_series_df.shape}\")\nprint(f\"sample_submission.csv shape: {sample_sub_df.shape}\")\n\n# Quantitative missing value assessment in train.csv\nlabeled_count = train_df[config.TARGET_COLS[0]].notnull().sum()\nunlabeled_count = len(train_df) - labeled_count\nprint(f\"Gold-Standard Labeled Studies: {labeled_count} ({labeled_count/len(train_df)*100:.2f}%)\")\nprint(f\"Unlabeled Studies with Text Reports: {unlabeled_count} ({unlabeled_count/len(train_df)*100:.2f}%)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:40:26.481688Z","iopub.execute_input":"2026-08-06T17:40:26.481975Z","iopub.status.idle":"2026-08-06T17:40:26.735148Z","shell.execute_reply.started":"2026-08-06T17:40:26.481955Z","shell.execute_reply":"2026-08-06T17:40:26.734386Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualization 1: Target Prevalence on Gold-Standard 58 Labeled Studies\nlabeled_df = train_df.dropna(subset=[config.TARGET_COLS[0]])\npos_counts = labeled_df[config.TARGET_COLS].sum().sort_values(ascending=True)\npos_pct = (pos_counts / len(labeled_df)) * 100\n\nplt.figure(figsize=(10, 6), dpi=120)\nbars = plt.barh(pos_counts.index, pos_pct.values, color=plt.cm.cividis(np.linspace(0.2, 0.8, len(pos_counts))))\nplt.title(\"Target Pathology Positive Class Ratio (N = 58 Gold-Standard Studies)\", fontsize=13, fontweight='bold', pad=12)\nplt.xlabel(\"Positive Ratio (%)\", fontsize=11)\nplt.ylabel(\"Pathology Target\", fontsize=11)\nplt.xlim(0, 75)\nplt.grid(axis='x', linestyle='--', alpha=0.5)\n\nfor bar, pct, cnt in zip(bars, pos_pct.values, pos_counts.values):\n    plt.text(pct + 1, bar.get_y() + bar.get_height()/2, f\"{pct:.1f}% (n={int(cnt)})\", va='center', fontsize=9, fontweight='bold')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:40:26.736177Z","iopub.execute_input":"2026-08-06T17:40:26.736534Z","iopub.status.idle":"2026-08-06T17:40:27.044933Z","shell.execute_reply.started":"2026-08-06T17:40:26.736513Z","shell.execute_reply":"2026-08-06T17:40:27.044275Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualization 2: Target Co-Occurrence Correlation Matrix\ncorr_matrix = labeled_df[config.TARGET_COLS].corr()\n\nplt.figure(figsize=(11, 9), dpi=120)\nsns.heatmap(corr_matrix, annot=True, fmt=\".2f\", cmap=\"viridis\", cbar_kws={'label': 'Pearson Correlation Coefficient'},\n            linewidths=0.5, linecolor='white', square=True)\nplt.title(\"Target Pathology Inter-Correlation Matrix\", fontsize=14, fontweight='bold', pad=15)\nplt.xticks(rotation=45, ha='right', fontsize=10)\nplt.yticks(fontsize=10)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:40:27.046842Z","iopub.execute_input":"2026-08-06T17:40:27.047144Z","iopub.status.idle":"2026-08-06T17:40:27.571013Z","shell.execute_reply.started":"2026-08-06T17:40:27.04712Z","shell.execute_reply":"2026-08-06T17:40:27.570286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualization 3: Report Text Length & Linguistic Analysis\ntrain_df['report_char_len'] = train_df['Report'].astype(str).apply(len)\ntrain_df['report_word_len'] = train_df['Report'].astype(str).apply(lambda x: len(x.split()))\n\ndef classify_language(text):\n    text_lower = str(text).lower()\n    if any(k in text_lower for k in ['técnica:', 'impresión:', 'hallazgos:', 'rotura', 'derrame', 'menisco']):\n        return 'Spanish'\n    elif any(k in text_lower for k in ['befunde:', 'klinische', 'gewogen', 'beoordeling:']):\n        return 'German/Dutch'\n    elif any(k in text_lower for k in ['constatations:', 'épanchement', 'ménisque', 'aucun']):\n        return 'French'\n    elif any(k in text_lower for k in ['impression:', 'findings:', 'meniscus', 'effusion', 'intact', 'tear']):\n        return 'English'\n    else:\n        return 'Other Multilingual'\n\ntrain_df['language'] = train_df['Report'].apply(classify_language)\n\nplt.figure(figsize=(10, 5), dpi=120)\nlang_counts = train_df['language'].value_counts()\nplt.bar(lang_counts.index, lang_counts.values, color=plt.cm.plasma(np.linspace(0.2, 0.8, len(lang_counts))))\nplt.title(\"Radiology Report Language Composition (N = 4,407 Studies)\", fontsize=13, fontweight='bold', pad=12)\nplt.xlabel(\"Language Category\", fontsize=11)\nplt.ylabel(\"Number of Studies\", fontsize=11)\nplt.grid(axis='y', linestyle='--', alpha=0.5)\n\nfor i, v in enumerate(lang_counts.values):\n    plt.text(i, v + 50, f\"{v} ({v/len(train_df)*100:.1f}%)\", ha='center', fontsize=9, fontweight='bold')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:40:27.571978Z","iopub.execute_input":"2026-08-06T17:40:27.572328Z","iopub.status.idle":"2026-08-06T17:40:28.095205Z","shell.execute_reply.started":"2026-08-06T17:40:27.572304Z","shell.execute_reply":"2026-08-06T17:40:28.094566Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualization 4: Series Distribution and Anatomical Planes\nseries_per_study = train_series_df.groupby('StudyInstanceUID').size()\nplane_counts = train_series_df['Anatomical_Plane'].value_counts()\n\nfig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), dpi=120)\n\n# Top Subplot: Series count per study\nax1.hist(series_per_study.values, bins=range(1, 15), color='#2b5c8f', edgecolor='black', align='left')\nax1.set_title(\"Distribution of MRI Series Count per Study\", fontsize=12, fontweight='bold')\nax1.set_xlabel(\"Number of Series\", fontsize=10)\nax1.set_ylabel(\"Study Count\", fontsize=10)\nax1.set_xticks(range(1, 15))\nax1.grid(axis='y', linestyle='--', alpha=0.5)\n\n# Bottom Subplot: Anatomical Plane counts\nax2.bar(plane_counts.index, plane_counts.values, color=['#41b6c4', '#225ea8', '#081d58'])\nax2.set_title(\"Distribution of MRI Anatomical Acquisition Planes\", fontsize=12, fontweight='bold')\nax2.set_xlabel(\"Anatomical Plane\", fontsize=10)\nax2.set_ylabel(\"Series Count\", fontsize=10)\nax2.grid(axis='y', linestyle='--', alpha=0.5)\n\nfor i, v in enumerate(plane_counts.values):\n    ax2.text(i, v + 200, f\"{v} ({v/len(train_series_df)*100:.1f}%)\", ha='center', fontsize=9, fontweight='bold')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:56:33.041887Z","iopub.execute_input":"2026-08-06T17:56:33.042446Z","iopub.status.idle":"2026-08-06T17:56:33.418509Z","shell.execute_reply.started":"2026-08-06T17:56:33.042406Z","shell.execute_reply":"2026-08-06T17:56:33.417811Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Multilingual NLP Pseudo-Label Extraction Engine\n\nTo utilize the 4,349 unlabelled studies containing radiology text reports, a multilingual rule-based NLP extraction pipeline (`MultilingualReportParser`) is constructed.\n\n## Diagnostic Findings Parsing Logic\n\nFor each target pathology $c \\in \\{1, \\dots, 12\\}$, regular expression pattern matrices define positive clinical indicators and negative negation modifiers across English, Spanish, German, French, and Eastern European terms:\n\n$$y_{i, c}^{\\text{pseudo}} = \\begin{cases} \n1 & \\text{if } \\exists \\text{ positive match and } \\neg \\exists \\text{ local negation within window } \\Delta w \\\\\n0 & \\text{otherwise}\n\\end{cases}$$\n\nThe precision and recall of the extracted pseudo-labels are quantitatively validated against the 58 gold-standard labeled studies.\n","metadata":{}},{"cell_type":"code","source":"class MultilingualReportParser:\n    def __init__(self):\n        # Multilingual positive keywords and negation terms per target\n        self.rules = {\n            'ACL': {\n                'pos': [r'acl', r'anterior cruciate', r'cruzado anterior', r'vorderen kreuzband', r'croisé antérieur', r'руптура.*предната'],\n                'neg': [r'intact', r'normal', r'sin rotura', r'no tear', r'ohne riss', r'intacte', r'unremarkable', r'без.*руптура']\n            },\n            'MCL': {\n                'pos': [r'mcl', r'medial collateral', r'colateral medial', r'medialen kollateralband', r'collatéral médial'],\n                'neg': [r'intact', r'normal', r'sin rotura', r'no tear', r'ohne riss', r'intacte', r'unremarkable']\n            },\n            'Medial Meniscus': {\n                'pos': [r'medial meniscus', r'menisco interno', r'menisco medial', r'innenmeniscus', r'ménisque médial', r'медиалния кондил'],\n                'neg': [r'no tear', r'intact', r'sin rotura', r'ohne riss', r'pas de déchirure', r'normal']\n            },\n            'Lateral Meniscus': {\n                'pos': [r'lateral meniscus', r'menisco externo', r'menisco lateral', r'aussenmeniscus', r'ménisque latéral'],\n                'neg': [r'no tear', r'intact', r'sin rotura', r'ohne riss', r'pas de déchirure', r'normal']\n            },\n            'Medial OA': {\n                'pos': [r'medial.*osteoarthritis', r'artrosis.*medial', r'gonartroza.*medijalno', r'gonarthrose.*médiale', r'ostéoarthrite.*médiale'],\n                'neg': [r'no osteoarthritis', r'sin artrosis', r'keine gonarthrose', r\"pas d'arthrose\"]\n            },\n            'Lateral OA': {\n                'pos': [r'lateral.*osteoarthritis', r'artrosis.*lateral', r'gonartroza.*lateralno', r'gonarthrose.*latérale'],\n                'neg': [r'no osteoarthritis', r'sin artrosis', r'keine gonarthrose']\n            },\n            'PF OA': {\n                'pos': [r'patellofemoral.*osteoarthritis', r'artrosis.*rótula', r'condropatía.*rótula', r'patellofemoral.*arthrose', r'chondropathie.*patellaire'],\n                'neg': [r'no patellofemoral', r'sin condropatía', r'normal patella']\n            },\n            'Effusion': {\n                'pos': [r'effusion', r'derrame', r'stawen izliw', r'ergouss', r'gelenkerguss', r'épanchement', r'συλλογή υγρού', r'излив'],\n                'neg': [r'no effusion', r'sin derrame', r'kein erguss', r\"pas d'épanchement\", r'без.*излив', r'χωρίς.*υγρού']\n            },\n            'Synovitis': {\n                'pos': [r'synovitis', r'sinovitis', r'synovialis', r'inflammation.*synoviale'],\n                'neg': [r'no synovitis', r'sin sinovitis', r'keine synovitis']\n            },\n            \"Baker's\": {\n                'pos': [r'baker', r'popliteal cyst', r'quiste de baker', r'baker-zyste', r'kyste de baker', r'popliteal cyste'],\n                'neg': [r'no baker', r'sin quiste', r'keine zyste', r'pas de kyste']\n            },\n            'Contusion': {\n                'pos': [r'contusion', r'bone bruise', r'edema óseo', r'edema osseux', r'knochenmarködem', r'костномозъчен едем'],\n                'neg': [r'no contusion', r'sin edema', r'kein ödem', r\"pas d'édème\"]\n            },\n            'Fracture': {\n                'pos': [r'fracture', r'fractura', r'fraktur', r'fissure', r'fissuration'],\n                'neg': [r'no fracture', r'sin fractura', r'keine fraktur', r'pas de fracture', r'aucune']\n            }\n        }\n\n    def parse_report(self, text):\n        text_lower = str(text).lower()\n        results = {}\n        for target, conds in self.rules.items():\n            pos_found = any(re.search(pat, text_lower) for pat in conds['pos'])\n            neg_found = any(re.search(pat, text_lower) for pat in conds['neg'])\n            \n            if pos_found and not neg_found:\n                results[target] = 1.0\n            elif neg_found and not pos_found:\n                results[target] = 0.0\n            else:\n                results[target] = 0.0\n        return results\n\nparser = MultilingualReportParser()\n\n# Validate NLP parser on the 58 gold-standard labeled samples\nlabeled_indices = train_df.dropna(subset=[config.TARGET_COLS[0]]).index\ny_true_list = []\ny_pred_list = []\n\nfor idx in labeled_indices:\n    row = train_df.loc[idx]\n    report_text = row['Report']\n    parsed = parser.parse_report(report_text)\n    \n    true_vals = [row[c] for c in config.TARGET_COLS]\n    pred_vals = [parsed[c] for c in config.TARGET_COLS]\n    \n    y_true_list.append(true_vals)\n    y_pred_list.append(pred_vals)\n\ny_true_arr = np.array(y_true_list)\ny_pred_arr = np.array(y_pred_list)\n\n# Evaluate macro metrics on gold standard\noverall_f1s = []\nfor c_idx, col in enumerate(config.TARGET_COLS):\n    cm = confusion_matrix(y_true_arr[:, c_idx], y_pred_arr[:, c_idx], labels=[0, 1])\n    tn, fp, fn, tp = cm.ravel()\n    precision = tp / (tp + fp + 1e-6)\n    recall = tp / (tp + fn + 1e-6)\n    f1 = 2 * precision * recall / (precision + recall + 1e-6)\n    overall_f1s.append(f1)\n    print(f\"Target: {col:<18} | Precision: {precision:.3f} | Recall: {recall:.3f} | F1: {f1:.3f}\")\n\nprint(f\"\\nNLP Pseudo-Label Parser Mean F1-Score: {np.mean(overall_f1s):.4f}\")\n\n# Populate full training dataset pseudo-labels for unlabeled studies\nfull_target_df = train_df.copy()\nfor idx, row in full_target_df.iterrows():\n    if pd.isnull(row[config.TARGET_COLS[0]]):\n        parsed = parser.parse_report(row['Report'])\n        for col in config.TARGET_COLS:\n            full_target_df.at[idx, col] = parsed[col]\n\nprint(f\"Full Expanded Labeled Dataset Size: {len(full_target_df)} studies\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:40:28.406809Z","iopub.execute_input":"2026-08-06T17:40:28.407142Z","iopub.status.idle":"2026-08-06T17:40:30.519492Z","shell.execute_reply.started":"2026-08-06T17:40:28.407117Z","shell.execute_reply":"2026-08-06T17:40:30.518743Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualization 5: Comparison of Gold-Standard vs NLP-Parsed Pseudo-Label Distributions\nparsed_pos_counts = full_target_df[config.TARGET_COLS].sum().sort_values(ascending=True)\nparsed_pos_pct = (parsed_pos_counts / len(full_target_df)) * 100\n\nplt.figure(figsize=(12, 8), dpi=120)\nbars = plt.barh(parsed_pos_counts.index, parsed_pos_pct.values, color=plt.cm.magma(np.linspace(0.3, 0.85, len(parsed_pos_counts))))\nplt.title(\"Pathology Class Prevalence across Full Dataset (N = 4,407 Studies)\", fontsize=13, fontweight='bold', pad=12)\nplt.xlabel(\"Positive Class Percentage (%)\", fontsize=11)\nplt.ylabel(\"Pathology Target\", fontsize=11)\nplt.xlim(0, 75)\nplt.grid(axis='x', linestyle='--', alpha=0.5)\n\nfor bar, pct, cnt in zip(bars, parsed_pos_pct.values, parsed_pos_counts.values):\n    plt.text(pct + 1, bar.get_y() + bar.get_height()/2, f\"{pct:.1f}% (n={int(cnt)})\", va='center', fontsize=9, fontweight='bold')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:57:17.922986Z","iopub.execute_input":"2026-08-06T17:57:17.923465Z","iopub.status.idle":"2026-08-06T17:57:18.184158Z","shell.execute_reply.started":"2026-08-06T17:57:17.923436Z","shell.execute_reply":"2026-08-06T17:57:18.183519Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# DICOM Ingestion & Multi-Planar Volumetric Sampler\n\nDICOM image volumes are loaded from disk with windowing adjustments and spatial rescalings.\n\n## Voxel Rescaling & Intensity Normalization\n\nFor each raw DICOM slice $I_{\\text{raw}}$, Hounsfield units or signal intensity values are adjusted via DICOM rescale metadata tags:\n\n$$I_{\\text{HU}} = I_{\\text{raw}} \\times S + B$$\n\nwhere $S$ is `RescaleSlope` and $B$ is `RescaleIntercept`. Dynamic windowing applies intensity clipping to the $[p_{1}, p_{99}]$ percentile range followed by min-max mapping:\n\n$$I_{\\text{norm}} = \\frac{\\text{clip}(I_{\\text{HU}}, p_{1}, p_{99}) - p_{1}}{p_{99} - p_{1} + \\epsilon} \\in [0, 1]$$\n\n## Key Slice Extraction\n\nFrom a series containing $N_{\\text{slices}}$ images, $K = 5$ key slices centered at the median volumetric depth are extracted:\n\n$$k_i = \\text{round}\\left(\\frac{N_{\\text{slices}}}{K + 1} \\times i\\right), \\quad i \\in \\{1, 2, \\dots, K\\}$$\n","metadata":{}},{"cell_type":"code","source":"class DICOMVolumeSampler:\n    def __init__(self, target_size=(224, 224), num_slices=5):\n        self.target_size = target_size\n        self.num_slices = num_slices\n\n    def load_slice(self, dicom_path):\n        try:\n            dcm = pydicom.dcmread(dicom_path)\n            img = dcm.pixel_array.astype(np.float32)\n            \n            # Apply Rescale Slope & Intercept if present\n            slope = getattr(dcm, 'RescaleSlope', 1.0)\n            intercept = getattr(dcm, 'RescaleIntercept', 0.0)\n            img = img * slope + intercept\n            \n            # Min-Max Normalization with percentile clipping\n            p1, p99 = np.percentile(img, (1, 99))\n            if p99 > p1:\n                img = np.clip(img, p1, p99)\n                img = (img - p1) / (p99 - p1)\n            else:\n                img = np.zeros_like(img)\n                \n            img = (img * 255.0).astype(np.uint8)\n            img = cv2.resize(img, self.target_size, interpolation=cv2.INTER_AREA)\n            img = img.astype(np.float32) / 255.0\n            return img\n        except Exception:\n            # Fallback zero-filled array if file corrupt/missing\n            return np.zeros(self.target_size, dtype=np.float32)\n\n    def sample_series(self, dicom_paths):\n        if not dicom_paths:\n            return np.zeros((self.num_slices, 1, *self.target_size), dtype=np.float32)\n            \n        n_files = len(dicom_paths)\n        if n_files <= self.num_slices:\n            indices = np.linspace(0, n_files - 1, self.num_slices, dtype=int)\n        else:\n            indices = np.linspace(int(n_files * 0.15), int(n_files * 0.85), self.num_slices, dtype=int)\n            \n        selected_paths = [dicom_paths[i] for i in indices]\n        slices = [self.load_slice(p) for p in selected_paths]\n        \n        # Format as (K, 1, H, W)\n        tensor_slices = np.stack(slices, axis=0)[:, np.newaxis, :, :]\n        return tensor_slices\n\ndicom_sampler = DICOMVolumeSampler(target_size=config.IMAGE_SIZE, num_slices=config.SLICES_PER_VIEW)\nprint(\"DICOM Volume Sampler initialized.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:40:30.764516Z","iopub.execute_input":"2026-08-06T17:40:30.76478Z","iopub.status.idle":"2026-08-06T17:40:30.774112Z","shell.execute_reply.started":"2026-08-06T17:40:30.764757Z","shell.execute_reply":"2026-08-06T17:40:30.773529Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualization 6: Sample Slice Multi-Planar Rendering & Intensity Histograms\nsample_study = train_series_df['StudyInstanceUID'].iloc[0]\nsample_series_list = train_series_df[train_series_df['StudyInstanceUID'] == sample_study]\n\nplanes = ['Sagittal', 'Coronal', 'Axial']\nfig, axes = plt.subplots(3, 1, figsize=(8, 12), dpi=120)\n\nfor idx, plane in enumerate(planes):\n    plane_series = sample_series_list[sample_series_list['Anatomical_Plane'] == plane]\n    ax = axes[idx]\n    if not plane_series.empty:\n        s_uid = plane_series['SeriesInstanceUID'].iloc[0]\n        s_dir = config.TRAIN_SERIES_DIR / sample_study / s_uid\n        dcm_files = sorted(list(s_dir.glob(\"*.dcm\")))\n        if dcm_files:\n            img = dicom_sampler.load_slice(str(dcm_files[len(dcm_files)//2]))\n            im = ax.imshow(img, cmap='cividis')\n            ax.set_title(f\"Plane: {plane} | Slice Index: {len(dcm_files)//2}/{len(dcm_files)}\", fontsize=11, fontweight='bold')\n            ax.axis('off')\n            fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)\n        else:\n            ax.text(0.5, 0.5, f\"No DICOM Slices for {plane}\", ha='center', va='center')\n            ax.axis('off')\n    else:\n        ax.text(0.5, 0.5, f\"No {plane} Series\", ha='center', va='center')\n        ax.axis('off')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:40:30.775071Z","iopub.execute_input":"2026-08-06T17:40:30.775429Z","iopub.status.idle":"2026-08-06T17:40:31.491019Z","shell.execute_reply.started":"2026-08-06T17:40:30.775393Z","shell.execute_reply":"2026-08-06T17:40:31.490266Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Deep Multi-View Neural Network Architecture\n\nThe diagnostic network (`MultiViewKneeNet`) combines slice feature extraction, multi-head slice attention pooling, and joint multi-planar latent space fusion.\n\n## Slice Attention Pooling\n\nLet $\\mathbf{x}_{v, s} \\in \\mathbb{R}^{1 \\times H \\times W}$ denote key slice $s$ in plane $v \\in \\{\\text{Sagittal}, \\text{Coronal}, \\text{Axial}\\}$. A shared 2D convolutional encoder $f_{\\phi}$ extracts spatial feature vector $\\mathbf{h}_{v, s} = f_{\\phi}(\\mathbf{x}_{v, s}) \\in \\mathbb{R}^D$.\n\nAn attention pooling mechanism aggregates slice embeddings into volume representation $\\mathbf{z}_v \\in \\mathbb{R}^D$:\n\n$$\\alpha_{v, s} = \\frac{\\exp\\left(\\mathbf{w}^T \\tanh(\\mathbf{W}_a \\mathbf{h}_{v, s})\\right)}{\\sum_{j=1}^{K} \\exp\\left(\\mathbf{w}^T \\tanh(\\mathbf{W}_a \\mathbf{h}_{v, j})\\right)}$$\n\n$$\\mathbf{z}_v = \\sum_{s=1}^{K} \\alpha_{v, s} \\mathbf{h}_{v, s}$$\n\n## Multi-Plane Latent Concat & Multi-Label Classification\n\nVolume representations from all three planes are concatenated:\n$$\\mathbf{z}_{\\text{fused}} = [\\mathbf{z}_{\\text{Sagittal}} \\, \\Vert \\, \\mathbf{z}_{\\text{Coronal}} \\, \\Vert \\, \\mathbf{z}_{\\text{Axial}}] \\in \\mathbb{R}^{3D}$$\n\nThe joint vector passes through a multi-label classification head with sigmoid activation:\n$$\\hat{\\mathbf{y}}_i = \\sigma\\left(\\mathbf{W}_c \\mathbf{z}_{\\text{fused}} + \\mathbf{b}_c\\right) \\in [0, 1]^{12}$$\n","metadata":{}},{"cell_type":"code","source":"class KneeMRIDataset(Dataset):\n    def __init__(self, df, series_df, series_dir, config, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.series_df = series_df\n        self.series_dir = Path(series_dir)\n        self.config = config\n        self.is_train = is_train\n        self.sampler = DICOMVolumeSampler(target_size=config.IMAGE_SIZE, num_slices=config.SLICES_PER_VIEW)\n        \n        # Pre-group series by StudyInstanceUID\n        self.study_series_map = self.series_df.groupby('StudyInstanceUID')\n\n    def __len__(self):\n        return len(self.df)\n\n    def _get_plane_tensor(self, study_id, plane_name):\n        if study_id not in self.study_series_map.groups:\n            return np.zeros((self.config.SLICES_PER_VIEW, 1, *self.config.IMAGE_SIZE), dtype=np.float32)\n            \n        study_series = self.study_series_map.get_group(study_id)\n        plane_series = study_series[study_series['Anatomical_Plane'] == plane_name]\n        \n        if plane_series.empty:\n            return np.zeros((self.config.SLICES_PER_VIEW, 1, *self.config.IMAGE_SIZE), dtype=np.float32)\n            \n        series_uid = plane_series['SeriesInstanceUID'].iloc[0]\n        s_dir = self.series_dir / str(study_id) / str(series_uid)\n        dcm_files = sorted(list(s_dir.glob(\"*.dcm\")))\n        dcm_paths = [str(p) for p in dcm_files]\n        \n        return self.sampler.sample_series(dcm_paths)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_id = row['StudyInstanceUID']\n        \n        sagittal_tensor = self._get_plane_tensor(study_id, 'Sagittal')\n        coronal_tensor = self._get_plane_tensor(study_id, 'Coronal')\n        axial_tensor = self._get_plane_tensor(study_id, 'Axial')\n        \n        # Convert to torch Tensors\n        sagittal_t = torch.tensor(sagittal_tensor, dtype=torch.float32)\n        coronal_t = torch.tensor(coronal_tensor, dtype=torch.float32)\n        axial_t = torch.tensor(axial_tensor, dtype=torch.float32)\n        \n        if self.config.TARGET_COLS[0] in row:\n            labels = torch.tensor([row[c] for c in self.config.TARGET_COLS], dtype=torch.float32)\n        else:\n            labels = torch.zeros(self.config.NUM_CLASSES, dtype=torch.float32)\n            \n        return (sagittal_t, coronal_t, axial_t), labels, study_id\n\n\nclass SliceAttentionPool(nn.Module):\n    def __init__(self, in_features, hidden_dim=128):\n        super().__init__()\n        self.attention = nn.Sequential(\n            nn.Linear(in_features, hidden_dim),\n            nn.Tanh(),\n            nn.Linear(hidden_dim, 1)\n        )\n\n    def forward(self, x):\n        # x shape: (B, K, D)\n        weights = self.attention(x)  # (B, K, 1)\n        weights = F.softmax(weights, dim=1)\n        pooled = torch.sum(x * weights, dim=1)  # (B, D)\n        return pooled\n\n\nclass MultiViewKneeNet(nn.Module):\n    def __init__(self, num_classes=12, embed_dim=256):\n        super().__init__()\n        # Lightweight Convolutional Encoder\n        self.encoder = nn.Sequential(\n            nn.Conv2d(1, 32, kernel_size=3, stride=2, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2),\n            \n            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2),\n            \n            nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(),\n            nn.AdaptiveAvgPool2d((1, 1))\n        )\n        \n        self.fc_proj = nn.Linear(128, embed_dim)\n        self.pool_sagittal = SliceAttentionPool(embed_dim)\n        self.pool_coronal = SliceAttentionPool(embed_dim)\n        self.pool_axial = SliceAttentionPool(embed_dim)\n        \n        # Joint Classification Head\n        self.classifier = nn.Sequential(\n            nn.Linear(embed_dim * 3, 256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def _forward_plane(self, x, pool_layer):\n        # x shape: (B, K, 1, H, W)\n        B, K, C, H, W = x.shape\n        x_flat = x.view(B * K, C, H, W)\n        feats = self.encoder(x_flat).view(B * K, -1)\n        feats = F.relu(self.fc_proj(feats))\n        feats = feats.view(B, K, -1)\n        pooled_feat = pool_layer(feats)\n        return pooled_feat\n\n    def forward(self, sagittal, coronal, axial):\n        z_sag = self._forward_plane(sagittal, self.pool_sagittal)\n        z_cor = self._forward_plane(coronal, self.pool_coronal)\n        z_axi = self._forward_plane(axial, self.pool_axial)\n        \n        z_fused = torch.cat([z_sag, z_cor, z_axi], dim=1)\n        logits = self.classifier(z_fused)\n        return logits\n\nprint(\"Dataset and MultiViewKneeNet defined successfully.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:40:31.492108Z","iopub.execute_input":"2026-08-06T17:40:31.492426Z","iopub.status.idle":"2026-08-06T17:40:31.509811Z","shell.execute_reply.started":"2026-08-06T17:40:31.492404Z","shell.execute_reply":"2026-08-06T17:40:31.509277Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model Training Pipeline\n\nThe multi-view architecture is trained on a sampled dataset to maximize throughput while maintaining convergence stability across multi-label targets.\n\n## Loss Function & Mixed-Precision Optimization\n\nModels are optimized using Binary Cross-Entropy with Logits Loss ($\\text{BCEWithLogitsLoss}$):\n\n$$\\mathcal{L}_{\\text{BCE}}(\\mathbf{y}, \\hat{\\mathbf{y}}) = -\\frac{1}{C} \\sum_{c=1}^{C} \\left[ y_c \\log(\\sigma(\\hat{y}_c)) + (1 - y_c) \\log(1 - \\sigma(\\hat{y}_c)) \\right]$$\n\nwhere $\\sigma(\\cdot)$ is the element-wise sigmoid function.\n\n## System Acceleration & Training Strategy\n\n* **High-Throughput Subsampling:** A representative subset of $N = 800$ studies is sampled to accelerate dataset iteration.\n* **Gradient Accumulation & Asynchronous Operations:** Gradients are accumulated over $2$ steps to double the effective batch size to $32$ while preserving GPU memory overhead. Asynchronous data transfers (`non_blocking=True`) and automatic mixed precision (`GradScaler`) optimize dual T4 GPU utilization.","metadata":{}},{"cell_type":"code","source":"# System Acceleration Parameters\nFAST_BATCH_SIZE = 16\nACCUMULATION_STEPS = 2\nTRAIN_SAMPLES = 800\n\n# Sample a representative subset for rapid training iteration\ntrain_subset_df = full_target_df.sample(n=min(TRAIN_SAMPLES, len(full_target_df)), random_state=config.SEED).reset_index(drop=True)\n\n# Initialize Dataset & DataLoader\ntrain_dataset = KneeMRIDataset(train_subset_df, train_series_df, config.TRAIN_SERIES_DIR, config, is_train=True)\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=FAST_BATCH_SIZE,\n    shuffle=True,\n    num_workers=4,\n    pin_memory=True,\n    persistent_workers=True,\n    drop_last=True\n)\n\n# Initialize Model, Optimizer, Scheduler & Scaler\nmodel = MultiViewKneeNet(num_classes=config.NUM_CLASSES).to(device)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=config.LEARNING_RATE, weight_decay=config.WEIGHT_DECAY)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=config.EPOCHS)\nscaler = GradScaler()\n\nprint(f\"Starting Fast Accelerated Training ({len(train_loader)} steps/epoch across {config.EPOCHS} Epochs)...\")\n\nhistory = {'train_loss': [], 'val_auc': []}\n\nfor epoch in range(config.EPOCHS):\n    model.train()\n    running_loss = 0.0\n    start_time = time.time()\n    optimizer.zero_grad(set_to_none=True)\n    \n    for step, ((sag, cor, axi), labels, _) in enumerate(train_loader):\n        sag = sag.to(device, non_blocking=True)\n        cor = cor.to(device, non_blocking=True)\n        axi = axi.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True)\n        \n        with autocast():\n            logits = model(sag, cor, axi)\n            loss = criterion(logits, labels) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad(set_to_none=True)\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        \n        if (step + 1) % 10 == 0 or (step + 1) == len(train_loader):\n            print(f\"Epoch [{epoch+1}/{config.EPOCHS}] | Step [{step+1}/{len(train_loader)}] | Loss: {running_loss/(step+1):.4f}\")\n            \n    scheduler.step()\n    epoch_loss = running_loss / len(train_loader)\n    history['train_loss'].append(epoch_loss)\n    elapsed = time.time() - start_time\n    print(f\"Epoch {epoch+1} Completed in {elapsed:.2f}s | Average Train BCE Loss: {epoch_loss:.4f}\\n\")\n\n# Save Model Weights\ntorch.save(model.state_dict(), \"best_multiview_knee_model.pth\")\nprint(\"Model weights successfully saved to 'best_multiview_knee_model.pth'.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:46:52.194097Z","iopub.execute_input":"2026-08-06T17:46:52.194535Z","iopub.status.idle":"2026-08-06T17:50:39.969275Z","shell.execute_reply.started":"2026-08-06T17:46:52.194498Z","shell.execute_reply":"2026-08-06T17:50:39.968462Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualization 7: Training Loss Convergence Curve\nplt.figure(figsize=(9, 5), dpi=120)\nplt.plot(range(1, config.EPOCHS + 1), history['train_loss'], marker='o', color='#1f77b4', linewidth=2.5, label='Training Loss')\nplt.title(\"Multi-View Neural Network BCE Loss Convergence\", fontsize=13, fontweight='bold', pad=12)\nplt.xlabel(\"Training Epoch\", fontsize=11)\nplt.ylabel(\"Binary Cross-Entropy Loss\", fontsize=11)\nplt.xticks(range(1, config.EPOCHS + 1))\nplt.grid(True, linestyle='--', alpha=0.5)\nplt.legend(fontsize=10)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:50:48.891386Z","iopub.execute_input":"2026-08-06T17:50:48.892013Z","iopub.status.idle":"2026-08-06T17:50:49.068778Z","shell.execute_reply.started":"2026-08-06T17:50:48.891971Z","shell.execute_reply":"2026-08-06T17:50:49.068096Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Validation Performance & Out-of-Fold Receiver Operating Characteristic Analysis\n\nValidation probabilities are evaluated against the gold-standard ground truth labels to generate Macro-AUC metrics and ROC curves.\n","metadata":{}},{"cell_type":"code","source":"# Visualization 8: ROC Curves per Pathology Target\nmodel.eval()\nval_preds = []\nval_targets = []\n\n# Evaluate on the 58 gold-standard samples\ngold_val_df = train_df.dropna(subset=[config.TARGET_COLS[0]]).reset_index(drop=True)\nval_dataset = KneeMRIDataset(gold_val_df, train_series_df, config.TRAIN_SERIES_DIR, config, is_train=False)\nval_loader = DataLoader(val_dataset, batch_size=config.BATCH_SIZE, shuffle=False)\n\nwith torch.no_grad():\n    for (sag, cor, axi), labels, _ in val_loader:\n        sag = sag.to(device)\n        cor = cor.to(device)\n        axi = axi.to(device)\n        logits = model(sag, cor, axi)\n        probs = torch.sigmoid(logits).cpu().numpy()\n        val_preds.append(probs)\n        val_targets.append(labels.numpy())\n\nval_preds = np.vstack(val_preds)\nval_targets = np.vstack(val_targets)\n\n# Compute AUC ROC per target\nauc_scores = {}\nplt.figure(figsize=(10, 8), dpi=120)\n\ncolors = plt.cm.tab20(np.linspace(0, 1, config.NUM_CLASSES))\n\nfor c_idx, col in enumerate(config.TARGET_COLS):\n    score = roc_auc_score(val_targets[:, c_idx], val_preds[:, c_idx])\n    auc_scores[col] = score\n    \n    # Compute curve points\n    from sklearn.metrics import roc_curve\n    fpr, tpr, _ = roc_curve(val_targets[:, c_idx], val_preds[:, c_idx])\n    plt.plot(fpr, tpr, color=colors[c_idx], label=f\"{col:<16} (AUC = {score:.3f})\", linewidth=1.8)\n\nplt.plot([0, 1], [0, 1], 'k--', alpha=0.6, label='Random Baseline (AUC = 0.500)')\nplt.title(f\"ROC Curves on Gold-Standard Subset | Macro-AUC: {np.mean(list(auc_scores.values())):.4f}\", fontsize=13, fontweight='bold', pad=12)\nplt.xlabel(\"False Positive Rate (1 - Specificity)\", fontsize=11)\nplt.ylabel(\"True Positive Rate (Sensitivity)\", fontsize=11)\nplt.grid(True, linestyle='--', alpha=0.5)\nplt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=9)\nplt.tight_layout()\nplt.show()\n\nprint(\"Per-Target Validation AUC Scores:\")\nfor k, v in auc_scores.items():\n    print(f\"  {k:<18}: {v:.4f}\")\nprint(f\"\\nOverall Macro-Averaged AUC ROC: {np.mean(list(auc_scores.values())):.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:50:52.698045Z","iopub.execute_input":"2026-08-06T17:50:52.698749Z","iopub.status.idle":"2026-08-06T17:51:05.778042Z","shell.execute_reply.started":"2026-08-06T17:50:52.698718Z","shell.execute_reply":"2026-08-06T17:51:05.777301Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Inference Engine, Test-Time Augmentation & Leaderboard Submission\n\nThe trained model computes test set probability estimates. Test-Time Augmentation (TTA) evaluates original and horizontally flipped slice representations.\n","metadata":{}},{"cell_type":"code","source":"# Prepare Test Dataset & Dataloader\ntest_dataset = KneeMRIDataset(test_df, test_series_df, config.TEST_SERIES_DIR, config, is_train=False)\ntest_loader = DataLoader(test_dataset, batch_size=config.BATCH_SIZE, shuffle=False, num_workers=config.NUM_WORKERS)\n\nmodel.eval()\ntest_predictions = []\ntest_study_ids = []\n\nprint(\"Executing Accelerated Test Set Inference with Test-Time Augmentation...\")\n\nwith torch.no_grad():\n    for (sag, cor, axi), _, study_ids in test_loader:\n        sag = sag.to(device)\n        cor = cor.to(device)\n        axi = axi.to(device)\n        \n        # Pass 1: Standard Orientation\n        logits_orig = model(sag, cor, axi)\n        probs_orig = torch.sigmoid(logits_orig)\n        \n        # Pass 2: Horizontal Flip TTA\n        sag_flip = torch.flip(sag, dims=[-1])\n        cor_flip = torch.flip(cor, dims=[-1])\n        axi_flip = torch.flip(axi, dims=[-1])\n        \n        logits_flip = model(sag_flip, cor_flip, axi_flip)\n        probs_flip = torch.sigmoid(logits_flip)\n        \n        # Ensemble Average\n        probs = ((probs_orig + probs_flip) / 2.0).cpu().numpy()\n        \n        test_predictions.append(probs)\n        test_study_ids.extend(study_ids)\n\ntest_predictions = np.vstack(test_predictions)\n\n# Construct Submission DataFrame\nsub_df = pd.DataFrame({'StudyInstanceUID': test_study_ids})\nfor c_idx, col in enumerate(config.TARGET_COLS):\n    sub_df[col] = test_predictions[:, c_idx]\n\n# Ensure submission columns match sample_submission.csv format exactly\nsub_df = sub_df[sample_sub_df.columns]\nsub_df.to_csv(config.OUTPUT_SUBMISSION, index=False)\n\nprint(f\"\\nSubmission file saved successfully to '{config.OUTPUT_SUBMISSION}'.\")\nprint(f\"Submission Shape: {sub_df.shape}\")\nprint(\"Submission Preview:\")\ndisplay(sub_df)\n\n# Submission Integrity Verification\nassert not sub_df.isnull().values.any(), \"Error: Submission contains missing values.\"\nassert (sub_df[config.TARGET_COLS].values >= 0.0).all() and (sub_df[config.TARGET_COLS].values <= 1.0).all(), \"Error: Predicted probabilities out of range [0, 1].\"\nprint(\"Integrity Verification Passed: Submission format is fully valid.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-06T17:53:22.993968Z","iopub.execute_input":"2026-08-06T17:53:22.994765Z","iopub.status.idle":"2026-08-06T17:53:23.682933Z","shell.execute_reply.started":"2026-08-06T17:53:22.994734Z","shell.execute_reply":"2026-08-06T17:53:23.681946Z"}},"outputs":[],"execution_count":null}]}