{"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":"import os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames[:5]:  # just first 5 per folder so it doesn't flood\n        print(os.path.join(dirname, filename))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:36:54.14769Z","iopub.execute_input":"2026-08-30T07:36:54.14816Z","iopub.status.idle":"2026-08-30T07:37:09.362329Z","shell.execute_reply.started":"2026-08-30T07:36:54.148119Z","shell.execute_reply":"2026-08-30T07:37:09.355683Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    print(dirname, '->', len(filenames), 'files')\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.363081Z","iopub.status.idle":"2026-08-30T07:37:09.363401Z","shell.execute_reply.started":"2026-08-30T07:37:09.363263Z","shell.execute_reply":"2026-08-30T07:37:09.36328Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfor item in os.listdir('/kaggle/input/competitions/rsna-knee-abnormality-detection'):\n    print(item)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.364458Z","iopub.status.idle":"2026-08-30T07:37:09.364836Z","shell.execute_reply.started":"2026-08-30T07:37:09.36463Z","shell.execute_reply":"2026-08-30T07:37:09.364656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\nprint(train.shape)\nprint(train.columns.tolist())\ntrain.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.36641Z","iopub.status.idle":"2026-08-30T07:37:09.366814Z","shell.execute_reply.started":"2026-08-30T07:37:09.366616Z","shell.execute_reply":"2026-08-30T07:37:09.366642Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(train.shape)\nprint(train[['ACL','MCL','Medial Meniscus','Lateral Meniscus','Medial OA',\n             'Lateral OA','PF OA','Effusion','Synovitis',\"Baker's\",'Contusion','Fracture']].isna().sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.368104Z","iopub.status.idle":"2026-08-30T07:37:09.368516Z","shell.execute_reply.started":"2026-08-30T07:37:09.368309Z","shell.execute_reply":"2026-08-30T07:37:09.368333Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled = train.dropna(subset=['ACL'])  # keep only rows where ACL isn't NaN\nprint(labeled.shape)\nlabeled.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.36997Z","iopub.status.idle":"2026-08-30T07:37:09.370375Z","shell.execute_reply.started":"2026-08-30T07:37:09.370172Z","shell.execute_reply":"2026-08-30T07:37:09.370198Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(labeled.shape[0], \"labeled rows out of\", train.shape[0])\nprint(labeled[['ACL','MCL','Medial Meniscus','Lateral Meniscus','Medial OA',\n               'Lateral OA','PF OA','Effusion','Synovitis',\"Baker's\",'Contusion','Fracture']].mean())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.371423Z","iopub.status.idle":"2026-08-30T07:37:09.371841Z","shell.execute_reply.started":"2026-08-30T07:37:09.371613Z","shell.execute_reply":"2026-08-30T07:37:09.371638Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for i in range(3):\n    row = labeled.iloc[i]\n    print(\"---- STUDY\", row['StudyInstanceUID'][:20], \"----\")\n    print(row['Report'])\n    print(\"Labels:\", row[['ACL','MCL','Medial Meniscus','Lateral Meniscus','Medial OA',\n                           'Lateral OA','PF OA','Effusion','Synovitis',\"Baker's\",'Contusion','Fracture']].to_dict())\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.373991Z","iopub.status.idle":"2026-08-30T07:37:09.374351Z","shell.execute_reply.started":"2026-08-30T07:37:09.374191Z","shell.execute_reply":"2026-08-30T07:37:09.374217Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def guess_fracture(report_text):\n    text = report_text.lower()\n    if \"no fracture\" in text or \"no acute fracture\" in text:\n        return 0\n    elif \"fracture\" in text:\n        return 1\n    else:\n        return 0  # default guess if not mentioned\n\nlabeled['fracture_guess'] = labeled['Report'].apply(guess_fracture)\n\n# compare guesses to real labels\naccuracy = (labeled['fracture_guess'] == labeled['Fracture']).mean()\nprint(\"Accuracy on Fracture:\", accuracy)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.376953Z","iopub.status.idle":"2026-08-30T07:37:09.377346Z","shell.execute_reply.started":"2026-08-30T07:37:09.377204Z","shell.execute_reply":"2026-08-30T07:37:09.377222Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"wrong = labeled[labeled['fracture_guess'] != labeled['Fracture']]\nprint(wrong.shape[0], \"wrong out of\", labeled.shape[0])\nfor i in range(len(wrong)):\n    row = wrong.iloc[i]\n    print(\"---- Actual:\", row['Fracture'], \" Guessed:\", row['fracture_guess'], \"----\")\n    print(row['Report'][:500])  # first 500 chars only\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.378629Z","iopub.status.idle":"2026-08-30T07:37:09.379017Z","shell.execute_reply.started":"2026-08-30T07:37:09.378873Z","shell.execute_reply":"2026-08-30T07:37:09.378891Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install deep-translator","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.380309Z","iopub.status.idle":"2026-08-30T07:37:09.380729Z","shell.execute_reply.started":"2026-08-30T07:37:09.380495Z","shell.execute_reply":"2026-08-30T07:37:09.38052Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from deep_translator import GoogleTranslator\n\nsample_text = \"Rotura del LCA. Rotura parcial del LCM. Derrame.\"\ntranslated = GoogleTranslator(source='auto', target='en').translate(sample_text)\nprint(translated)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.382167Z","iopub.status.idle":"2026-08-30T07:37:09.382563Z","shell.execute_reply.started":"2026-08-30T07:37:09.382343Z","shell.execute_reply":"2026-08-30T07:37:09.382387Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fracture_words = {\n    'en': ['fracture'],\n    'es': ['fractura'],\n    'tr': ['kırık', 'kirik'],\n    'de': ['fraktur'],\n    'bg': ['фрактура'],\n    'el': ['κάταγμα'],\n    'hr': ['fraktura', 'frakture']\n}\n\nnegation_words = ['no ', 'not ', 'without ', 'sin ', 'no hay', 'yok', 'kein', 'без', 'όχι', 'nema']\n\ndef guess_fracture_multilang(text):\n    text_lower = text.lower()\n    all_words = [w for words in fracture_words.values() for w in words]\n    for word in all_words:\n        if word in text_lower:\n            idx = text_lower.find(word)\n            window = text_lower[max(0, idx-20):idx]  # 20 chars before the word\n            if any(neg in window for neg in negation_words):\n                continue  # negated mention, skip\n            return 1\n    return 0\n\nlabeled['fracture_guess_v2'] = labeled['Report'].apply(guess_fracture_multilang)\naccuracy_v2 = (labeled['fracture_guess_v2'] == labeled['Fracture']).mean()\nprint(\"New accuracy:\", accuracy_v2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.383806Z","iopub.status.idle":"2026-08-30T07:37:09.384074Z","shell.execute_reply.started":"2026-08-30T07:37:09.383942Z","shell.execute_reply":"2026-08-30T07:37:09.383958Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\nlabeled = train.dropna(subset=['ACL']).copy()\nprint(labeled.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.385149Z","iopub.status.idle":"2026-08-30T07:37:09.385417Z","shell.execute_reply.started":"2026-08-30T07:37:09.385289Z","shell.execute_reply":"2026-08-30T07:37:09.385305Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fracture_words = {\n    'en': ['fracture'],\n    'es': ['fractura'],\n    'tr': ['kırık', 'kirik'],\n    'de': ['fraktur'],\n    'bg': ['фрактура'],\n    'el': ['κάταγμα'],\n    'hr': ['fraktura', 'frakture']\n}\n\nnegation_words = ['no ', 'not ', 'without ', 'sin ', 'no hay', 'yok', 'kein', 'без', 'όχι', 'nema']\n\ndef guess_fracture_multilang(text):\n    text_lower = text.lower()\n    all_words = [w for words in fracture_words.values() for w in words]\n    for word in all_words:\n        if word in text_lower:\n            idx = text_lower.find(word)\n            window = text_lower[max(0, idx-20):idx]  # 20 chars before the word\n            if any(neg in window for neg in negation_words):\n                continue  # negated mention, skip\n            return 1\n    return 0\n\nlabeled['fracture_guess_v2'] = labeled['Report'].apply(guess_fracture_multilang)\naccuracy_v2 = (labeled['fracture_guess_v2'] == labeled['Fracture']).mean()\nprint(\"New accuracy:\", accuracy_v2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.386797Z","iopub.status.idle":"2026-08-30T07:37:09.387201Z","shell.execute_reply.started":"2026-08-30T07:37:09.386996Z","shell.execute_reply":"2026-08-30T07:37:09.387022Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"wrong_v2 = labeled[labeled['fracture_guess_v2'] != labeled['Fracture']]\nprint(wrong_v2.shape[0], \"wrong out of\", labeled.shape[0])\nfor i in range(len(wrong_v2)):\n    row = wrong_v2.iloc[i]\n    print(\"---- Actual:\", row['Fracture'], \" Guessed:\", row['fracture_guess_v2'], \"----\")\n    print(row['Report'][:300])\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.388158Z","iopub.status.idle":"2026-08-30T07:37:09.388414Z","shell.execute_reply.started":"2026-08-30T07:37:09.388289Z","shell.execute_reply":"2026-08-30T07:37:09.388305Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install google-generativeai\n\nfrom kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"GEMINI_API_KEY\")\n\nimport google.generativeai as genai\ngenai.configure(api_key=api_key)\n\nmodel = genai.GenerativeModel('gemini-2.0-flash')\nresponse = model.generate_content(\"Say hello in one word.\")\nprint(response.text)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.390383Z","iopub.status.idle":"2026-08-30T07:37:09.390804Z","shell.execute_reply.started":"2026-08-30T07:37:09.39058Z","shell.execute_reply":"2026-08-30T07:37:09.390609Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = genai.GenerativeModel('gemini-3.6-flash')\nresponse = model.generate_content(\"Say hello in one word.\")\nprint(response.text)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.391948Z","iopub.status.idle":"2026-08-30T07:37:09.392589Z","shell.execute_reply.started":"2026-08-30T07:37:09.392389Z","shell.execute_reply":"2026-08-30T07:37:09.392413Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\n\nlabel_cols = ['ACL','MCL','Medial Meniscus','Lateral Meniscus','Medial OA',\n              'Lateral OA','PF OA','Effusion','Synovitis',\"Baker's\",'Contusion','Fracture']\n\ndef extract_labels(report_text):\n    prompt = f\"\"\"You are a radiology expert. Read this knee MRI report (it may be in any language) and determine whether each of the following 12 conditions is present (1) or absent (0).\n\nReport:\n{report_text}\n\nConditions to assess:\n- ACL: anterior cruciate ligament tear\n- MCL: medial collateral ligament tear\n- Medial Meniscus: medial meniscus tear\n- Lateral Meniscus: lateral meniscus tear\n- Medial OA: medial compartment osteoarthritis\n- Lateral OA: lateral compartment osteoarthritis\n- PF OA: patellofemoral osteoarthritis\n- Effusion: joint effusion\n- Synovitis: synovitis/synovial inflammation\n- Baker's: Baker's cyst\n- Contusion: bone contusion/bone bruise\n- Fracture: fracture (traumatic bone fracture, NOT osteochondral/cartilage-only lesions)\n\nRespond with ONLY a JSON object, no other text, in this exact format:\n{{\"ACL\": 0, \"MCL\": 0, \"Medial Meniscus\": 0, \"Lateral Meniscus\": 0, \"Medial OA\": 0, \"Lateral OA\": 0, \"PF OA\": 0, \"Effusion\": 0, \"Synovitis\": 0, \"Baker's\": 0, \"Contusion\": 0, \"Fracture\": 0}}\"\"\"\n\n    response = model.generate_content(prompt)\n    text = response.text.strip()\n    text = text.replace('```json', '').replace('```', '').strip()\n    return json.loads(text)\n\n# test on one report first\ntest_result = extract_labels(labeled.iloc[0]['Report'])\nprint(test_result)\nprint(\"Actual:\", labeled.iloc[0][label_cols].to_dict())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.393888Z","iopub.status.idle":"2026-08-30T07:37:09.394215Z","shell.execute_reply.started":"2026-08-30T07:37:09.394059Z","shell.execute_reply":"2026-08-30T07:37:09.394076Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\n\nresults = []\nfor i in range(len(labeled)):\n    report = labeled.iloc[i]['Report']\n    try:\n        pred = extract_labels(report)\n        results.append(pred)\n    except Exception as e:\n        print(f\"Error on row {i}: {e}\")\n        results.append({col: None for col in label_cols})\n    time.sleep(1)  # avoid hitting rate limits\n\npred_df = pd.DataFrame(results)\npred_df.index = labeled.index\n\n# accuracy per label\nfor col in label_cols:\n    acc = (pred_df[col] == labeled[col]).mean()\n    print(f\"{col}: {acc:.3f}\")\n\noverall_acc = (pred_df[label_cols].values == labeled[label_cols].values).mean()\nprint(f\"\\nOverall accuracy across all labels: {overall_acc:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.397445Z","iopub.status.idle":"2026-08-30T07:37:09.397867Z","shell.execute_reply.started":"2026-08-30T07:37:09.397676Z","shell.execute_reply":"2026-08-30T07:37:09.397693Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\n\ndef extract_labels_safe(report_text, max_retries=5):\n    for attempt in range(max_retries):\n        try:\n            return extract_labels(report_text)\n        except Exception as e:\n            if '429' in str(e):\n                wait = 15  # wait 15s and try again\n                print(f\"Rate limited, waiting {wait}s...\")\n                time.sleep(wait)\n            else:\n                print(f\"Other error: {e}\")\n                return {col: None for col in label_cols}\n    return {col: None for col in label_cols}  # gave up after retries\n\nresults = []\nfor i in range(len(labeled)):\n    report = labeled.iloc[i]['Report']\n    pred = extract_labels_safe(report)\n    results.append(pred)\n    time.sleep(4)  # ~15 requests/minute, safely under free tier limits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.399177Z","iopub.status.idle":"2026-08-30T07:37:09.399604Z","shell.execute_reply.started":"2026-08-30T07:37:09.399399Z","shell.execute_reply":"2026-08-30T07:37:09.399423Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install openai\n\nfrom kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"DEEPSEEK_API_KEY\")\n\nfrom openai import OpenAI\nclient = OpenAI(api_key=api_key, base_url=\"https://api.deepseek.com\")\n\nresponse = client.chat.completions.create(\n    model=\"deepseek-chat\",\n    messages=[{\"role\": \"user\", \"content\": \"Say hello in one word.\"}]\n)\nprint(response.choices[0].message.content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.401397Z","iopub.status.idle":"2026-08-30T07:37:09.40181Z","shell.execute_reply.started":"2026-08-30T07:37:09.401596Z","shell.execute_reply":"2026-08-30T07:37:09.401619Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install openai\n\nfrom kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"DEEPSEEK_API_KEY\")\n\nfrom openai import OpenAI\nclient = OpenAI(api_key=api_key, base_url=\"https://api.deepseek.com\")\n\nresponse = client.chat.completions.create(\n    model=\"deepseek-chat\",\n    messages=[{\"role\": \"user\", \"content\": \"Say hello in one word.\"}]\n)\nprint(response.choices[0].message.content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.403095Z","iopub.status.idle":"2026-08-30T07:37:09.403507Z","shell.execute_reply.started":"2026-08-30T07:37:09.403298Z","shell.execute_reply":"2026-08-30T07:37:09.403322Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install openai\n\nfrom kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"DEEPSEEK_API_KEY\")\n\nfrom openai import OpenAI\nclient = OpenAI(api_key=api_key, base_url=\"https://api.deepseek.com\")\n\nresponse = client.chat.completions.create(\n    model=\"deepseek-chat\",\n    messages=[{\"role\": \"user\", \"content\": \"Say hello in one word.\"}]\n)\nprint(response.choices[0].message.content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.405494Z","iopub.status.idle":"2026-08-30T07:37:09.406295Z","shell.execute_reply.started":"2026-08-30T07:37:09.406124Z","shell.execute_reply":"2026-08-30T07:37:09.406144Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"ZENMUX_API_KEY\")\n\nfrom openai import OpenAI\nclient = OpenAI(api_key=api_key, base_url=\"https://zenmux.ai/api/v1\")\n\nresponse = client.chat.completions.create(\n    model=\"deepseek/deepseek-chat\",  # ZenMux model naming may need a provider prefix\n    messages=[{\"role\": \"user\", \"content\": \"Say hello in one word.\"}]\n)\nprint(response.choices[0].message.content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.407401Z","iopub.status.idle":"2026-08-30T07:37:09.40775Z","shell.execute_reply.started":"2026-08-30T07:37:09.407568Z","shell.execute_reply":"2026-08-30T07:37:09.407595Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"ZENMUX_API_KEY\")\n\nfrom openai import OpenAI\nclient = OpenAI(api_key=api_key, base_url=\"https://zenmux.ai/api/v1\")\n\nresponse = client.chat.completions.create(\n    model=\"deepseek/deepseek-chat\",\n    messages=[{\"role\": \"user\", \"content\": \"Say hello in one word.\"}]\n)\nprint(response.choices[0].message.content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.40927Z","iopub.status.idle":"2026-08-30T07:37:09.409526Z","shell.execute_reply.started":"2026-08-30T07:37:09.409404Z","shell.execute_reply":"2026-08-30T07:37:09.409419Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"response = client.chat.completions.create(\n    model=\"deepseek/deepseek-chat-v3.1\",  # check exact slug on zenmux.ai/models if this fails\n    messages=[{\"role\": \"user\", \"content\": \"Say hello in one word.\"}]\n)\nprint(response.choices[0].message.content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.411144Z","iopub.status.idle":"2026-08-30T07:37:09.411436Z","shell.execute_reply.started":"2026-08-30T07:37:09.41131Z","shell.execute_reply":"2026-08-30T07:37:09.411326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"OPENROUTER_API_KEY\")\n\nfrom openai import OpenAI\nclient = OpenAI(api_key=api_key, base_url=\"https://openrouter.ai/api/v1\")\n\nresponse = client.chat.completions.create(\n    model=\"meta-llama/llama-3.3-70b-instruct:free\",\n    messages=[{\"role\": \"user\", \"content\": \"Say hello in one word.\"}]\n)\nprint(response.choices[0].message.content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.412856Z","iopub.status.idle":"2026-08-30T07:37:09.413241Z","shell.execute_reply.started":"2026-08-30T07:37:09.413102Z","shell.execute_reply":"2026-08-30T07:37:09.413124Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"OPENAI_API_KEY\")\n\nfrom openai import OpenAI\nclient = OpenAI(api_key=api_key)\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"Say hello in one word.\"}]\n)\nprint(response.choices[0].message.content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.414219Z","iopub.status.idle":"2026-08-30T07:37:09.414625Z","shell.execute_reply.started":"2026-08-30T07:37:09.414387Z","shell.execute_reply":"2026-08-30T07:37:09.414414Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"GEMINI_API_KEY\")\n\nimport google.generativeai as genai\ngenai.configure(api_key=api_key)\n\nmodel = genai.GenerativeModel('gemini-2.5-flash-lite')\nresponse = model.generate_content(\"Say hello in one word.\")\nprint(response.text)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.415646Z","iopub.status.idle":"2026-08-30T07:37:09.415967Z","shell.execute_reply.started":"2026-08-30T07:37:09.415832Z","shell.execute_reply":"2026-08-30T07:37:09.415848Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = genai.GenerativeModel('gemini-3.5-flash-lite')\nresponse = model.generate_content(\"Say hello in one word.\")\nprint(response.text)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.418168Z","iopub.status.idle":"2026-08-30T07:37:09.418566Z","shell.execute_reply.started":"2026-08-30T07:37:09.418363Z","shell.execute_reply":"2026-08-30T07:37:09.418387Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\n\nlabel_cols = ['ACL','MCL','Medial Meniscus','Lateral Meniscus','Medial OA',\n              'Lateral OA','PF OA','Effusion','Synovitis',\"Baker's\",'Contusion','Fracture']\n\nmodel = genai.GenerativeModel('gemini-3.5-flash-lite')\n\ndef extract_labels(report_text):\n    prompt = f\"\"\"You are a radiology expert. Read this knee MRI report (it may be in any language) and determine whether each of the following 12 conditions is present (1) or absent (0).\n\nReport:\n{report_text}\n\nConditions to assess:\n- ACL: anterior cruciate ligament tear\n- MCL: medial collateral ligament tear\n- Medial Meniscus: medial meniscus tear\n- Lateral Meniscus: lateral meniscus tear\n- Medial OA: medial compartment osteoarthritis\n- Lateral OA: lateral compartment osteoarthritis\n- PF OA: patellofemoral osteoarthritis\n- Effusion: joint effusion\n- Synovitis: synovitis/synovial inflammation\n- Baker's: Baker's cyst\n- Contusion: bone contusion/bone bruise\n- Fracture: fracture (traumatic bone fracture, NOT osteochondral/cartilage-only lesions)\n\nRespond with ONLY a JSON object, no other text, in this exact format:\n{{\"ACL\": 0, \"MCL\": 0, \"Medial Meniscus\": 0, \"Lateral Meniscus\": 0, \"Medial OA\": 0, \"Lateral OA\": 0, \"PF OA\": 0, \"Effusion\":","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.419433Z","iopub.status.idle":"2026-08-30T07:37:09.419845Z","shell.execute_reply.started":"2026-08-30T07:37:09.419618Z","shell.execute_reply":"2026-08-30T07:37:09.419644Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\n\nlabel_cols = ['ACL','MCL','Medial Meniscus','Lateral Meniscus','Medial OA',\n              'Lateral OA','PF OA','Effusion','Synovitis',\"Baker's\",'Contusion','Fracture']\n\nmodel = genai.GenerativeModel('gemini-3.5-flash-lite')\n\ndef extract_labels(report_text):\n    prompt = (\n        \"You are a radiology expert. Read this knee MRI report (it may be in any language) \"\n        \"and determine whether each of the following 12 conditions is present (1) or absent (0).\\n\\n\"\n        \"Report:\\n\" + report_text + \"\\n\\n\"\n        \"Conditions to assess:\\n\"\n        \"- ACL: anterior cruciate ligament tear\\n\"\n        \"- MCL: medial collateral ligament tear\\n\"\n        \"- Medial Meniscus: medial meniscus tear\\n\"\n        \"- Lateral Meniscus: lateral meniscus tear\\n\"\n        \"- Medial OA: medial compartment osteoarthritis\\n\"\n        \"- Lateral OA: lateral compartment osteoarthritis\\n\"\n        \"- PF OA: patellofemoral osteoarthritis\\n\"\n        \"- Effusion: joint effusion\\n\"\n        \"- Synovitis: synovitis/synovial inflammation\\n\"\n        \"- Baker's: Baker's cyst\\n\"\n        \"- Contusion: bone contusion/bone bruise\\n\"\n        \"- Fracture: fracture (traumatic bone fracture, NOT osteochondral/cartilage-only lesions)\\n\\n\"\n        \"Respond with ONLY a JSON object, no other text, in this exact format:\\n\"\n        '{\"ACL\": 0, \"MCL\": 0, \"Medial Meniscus\": 0, \"Lateral Meniscus\": 0, \"Medial OA\": 0, '\n        '\"Lateral OA\": 0, \"PF OA\": 0, \"Effusion\": 0, \"Synovitis\": 0,","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.420992Z","iopub.status.idle":"2026-08-30T07:37:09.421294Z","shell.execute_reply.started":"2026-08-30T07:37:09.421167Z","shell.execute_reply":"2026-08-30T07:37:09.421183Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\n\nlabel_cols = ['ACL','MCL','Medial Meniscus','Lateral Meniscus','Medial OA',\n              'Lateral OA','PF OA','Effusion','Synovitis',\"Baker's\",'Contusion','Fracture']\n\nmodel = genai.GenerativeModel('gemini-3.5-flash-lite')\n\nexample_json = json.dumps({col: 0 for col in label_cols})\n\ninstructions = [\n    \"You are a radiology expert.\",\n    \"Read this knee MRI report (it may be in any language) and determine whether each of the following 12 conditions is present (1) or absent (0).\",\n    \"\",\n    \"Conditions to assess:\",\n    \"ACL: anterior cruciate ligament tear\",\n    \"MCL: medial collateral ligament tear\",\n    \"Medial Meniscus: medial meniscus tear\",","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.423069Z","iopub.status.idle":"2026-08-30T07:37:09.423326Z","shell.execute_reply.started":"2026-08-30T07:37:09.4232Z","shell.execute_reply":"2026-08-30T07:37:09.423218Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile extractor.py\nimport json\n\nlabel_cols = ['ACL','MCL','Medial Meniscus','Lateral Meniscus','Medial OA','Lateral OA','PF OA','Effusion','Synovitis',\"Baker's\",'Contusion','Fracture']\n\ndef build_prompt(report_text, model_name='gemini-3.5-flash-lite'):\n    example_json = json.dumps({col: 0 for col in label_cols})\n    parts = []\n    parts.append(\"You are a radiology expert.\")\n    parts.append(\"Read this knee MRI report (it may be in any language) and determine whether each of the following 12 conditions is present (1) or absent (0).\")\n    parts.append(\"Conditions: ACL tear, MCL tear, Medial Meniscus tear, Lateral Meniscus tear, Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Bakers cyst, Contusion, Fracture (traumatic only, not osteochondral).\")\n    parts.append(\"Respond with ONLY a JSON object in this exact format, no other text:\")\n    parts.append(example_json)\n    parts.append(\"Report:\")\n    parts.append(report_text)\n    return \"\\n\".join(parts)\n\ndef extract_labels(model, report_text):\n    prompt = build_prompt(report_text)\n    response = model.generate_content(prompt)\n    text = response.text.strip()\n    text = text.replace('```json', '').replace('```', '').strip()\n    return json.loads(text)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.424422Z","iopub.status.idle":"2026-08-30T07:37:09.424674Z","shell.execute_reply.started":"2026-08-30T07:37:09.424555Z","shell.execute_reply":"2026-08-30T07:37:09.42457Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from extractor import extract_labels, label_cols\n\ntest_result = extract_labels(model, labeled.iloc[0]['Report'])\nprint(test_result)\nprint(\"Actual:\", labeled.iloc[0][label_cols].to_dict())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.425909Z","iopub.status.idle":"2026-08-30T07:37:09.426261Z","shell.execute_reply.started":"2026-08-30T07:37:09.42611Z","shell.execute_reply":"2026-08-30T07:37:09.426133Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\n\ntest_batch = labeled.iloc[:10]\nresults = []\nfor i in range(len(test_batch)):\n    report = test_batch.iloc[i]['Report']\n    try:\n        pred = extract_labels(model, report)\n        results.append(pred)\n        print(f\"Row {i}: OK\")\n    except Exception as e:\n        print(f\"Row {i}: Error - {e}\")\n        results.append({col: None for col in label_cols})\n    time.sleep(3)\n\npred_df = pd.DataFrame(results)\npred_df.index = test_batch.index\n\nvalid_mask = pred_df['ACL'].notna()\nprint(f\"\\nValid: {valid_mask.sum()} out of {len(test_batch)}\")\n\noverall_acc = (pred_df.loc[valid_mask, label_cols].values == test_batch.loc[valid_mask, label_cols].values).mean()\nprint(f\"Accuracy on this batch: {overall_acc:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.427804Z","iopub.status.idle":"2026-08-30T07:37:09.428057Z","shell.execute_reply.started":"2026-08-30T07:37:09.427932Z","shell.execute_reply":"2026-08-30T07:37:09.427947Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"remaining_batch = labeled.iloc[10:]\nresults2 = []\nfor i in range(len(remaining_batch)):\n    report = remaining_batch.iloc[i]['Report']\n    try:\n        pred = extract_labels(model, report)\n        results2.append(pred)\n        print(f\"Row {i+10}: OK\")\n    except Exception as e:\n        print(f\"Row {i+10}: Error - {e}\")\n        results2.append({col: None for col in label_cols})\n    time.sleep(3)\n\nall_results = results + results2\npred_df_full = pd.DataFrame(all_results)\npred_df_full.index = labeled.index\n\nvalid_mask_full = pred_df_full['ACL'].notna()\nprint(f\"\\nValid: {valid_mask_full.sum()} out of {len(labeled)}\")\n\nfor col in label_cols:\n    acc = (pred_df_full.loc[valid_mask_full, col] == labeled.loc[valid_mask_full, col]).mean()\n    print(f\"{col}: {acc:.3f}\")\n\noverall_acc_full = (pred_df_full.loc[valid_mask_full, label_cols].values == labeled.loc[valid_mask_full, label_cols].values).mean()\nprint(f\"\\nOverall accuracy across all 58 rows: {overall_acc_full:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.428935Z","iopub.status.idle":"2026-08-30T07:37:09.429264Z","shell.execute_reply.started":"2026-08-30T07:37:09.429066Z","shell.execute_reply":"2026-08-30T07:37:09.429081Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for col in ['Synovitis', 'Effusion', 'Contusion']:\n    print(f\"\\n{'='*20} {col} {'='*20}\")\n    wrong = labeled.loc[valid_mask_full][pred_df_full.loc[valid_mask_full, col] != labeled.loc[valid_mask_full, col]]\n    wrong_preds = pred_df_full.loc[wrong.index, col]\n    for idx in wrong.index:\n        print(f\"\\n--- Actual: {labeled.loc[idx, col]}  Predicted: {wrong_preds[idx]} ---\")\n        print(labeled.loc[idx, 'Report'][:400])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.431381Z","iopub.status.idle":"2026-08-30T07:37:09.431813Z","shell.execute_reply.started":"2026-08-30T07:37:09.43157Z","shell.execute_reply":"2026-08-30T07:37:09.431594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"col = 'Effusion'  # change this to 'Synovitis' or 'Contusion' to check those too\nwrong = labeled.loc[valid_mask_full][pred_df_full.loc[valid_mask_full, col] != labeled.loc[valid_mask_full, col]]\nwrong_preds = pred_df_full.loc[wrong.index, col]\n\nfor idx in wrong.index:\n    print(f\"\\n--- Actual: {labeled.loc[idx, col]}  Predicted: {wrong_preds[idx]} ---\")\n    print(labeled.loc[idx, 'Report'])  # full report, no truncation\n    print(\"\\n\" + \"=\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.432865Z","iopub.status.idle":"2026-08-30T07:37:09.433229Z","shell.execute_reply.started":"2026-08-30T07:37:09.43305Z","shell.execute_reply":"2026-08-30T07:37:09.433077Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"col = 'Effusion'\nwrong = labeled.loc[valid_mask_full][pred_df_full.loc[valid_mask_full, col] != labeled.loc[valid_mask_full, col]]\nwrong_preds = pred_df_full.loc[wrong.index, col]\n\nfor idx in wrong.index:\n    print(f\"\\n--- Actual: {labeled.loc[idx, col]}  Predicted: {wrong_preds[idx]} ---\")\n    print(labeled.loc[idx, 'Report'])\n    print(\"\\n\" + \"=\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.434345Z","iopub.status.idle":"2026-08-30T07:37:09.434641Z","shell.execute_reply.started":"2026-08-30T07:37:09.43451Z","shell.execute_reply":"2026-08-30T07:37:09.434527Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile extractor.py\nimport json\n\nlabel_cols = ['ACL','MCL','Medial Meniscus','Lateral Meniscus','Medial OA','Lateral OA','PF OA','Effusion','Synovitis',\"Baker's\",'Contusion','Fracture']\n\ndef build_prompt(report_text, model_name='gemini-3.5-flash-lite'):\n    example_json = json.dumps({col: 0 for col in label_cols})\n    parts = []\n    parts.append(\"You are a radiology expert.\")\n    parts.append(\"Read this knee MRI report (it may be in any language) and determine whether each of the following 12 conditions is present (1) or absent (0).\")\n    parts.append(\"IMPORTANT: Only count a finding as present (1) if it is clinically significant. Trace, minimal, or trivial findings (e.g. 'minor effusion', 'trace fluid', 'mild/minimal') should generally be scored as 0 unless the report emphasizes it as a notable finding. Only osteochondral or cartilage-only fractures should be excluded from Fracture; traumatic bone fractures (including bony avulsions) count as Fracture=1.\")\n    parts.append(\"Conditions: ACL tear, MCL tear, Medial Meniscus tear, Lateral Meniscus tear, Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Bakers cyst, Contusion, Fracture (traumatic, including avulsion fractures; not osteochondral/cartilage-only lesions).\")\n    parts.append(\"Respond with ONLY a JSON object in this exact format, no other text:\")\n    parts.append(example_json)\n    parts.append(\"Report:\")\n    parts.append(report_text)\n    return \"\\n\".join(parts)\n\ndef extract_labels(model, report_text):\n    prompt = build_prompt(report_text)\n    response = model.generate_content(prompt)\n    text = response.text.strip()\n    text = text.replace('```json', '').replace('```', '').strip()\n    return json.loads(text)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.436204Z","iopub.status.idle":"2026-08-30T07:37:09.436514Z","shell.execute_reply.started":"2026-08-30T07:37:09.436385Z","shell.execute_reply":"2026-08-30T07:37:09.436403Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from extractor import extract_labels, label_cols\nimport time\n\nresults_v2 = []\nfor i in range(len(labeled)):\n    report = labeled.iloc[i]['Report']\n    try:\n        pred = extract_labels(model, report)\n        results_v2.append(pred)\n    except Exception as e:\n        print(f\"Row {i}: Error - {e}\")\n        results_v2.append({col: None for col in label_cols})\n    time.sleep(3)\n\npred_df_v2 = pd.DataFrame(results_v2)\npred_df_v2.index = labeled.index\nvalid_v2 = pred_df_v2['ACL'].notna()\n\nfor col in label_cols:\n    acc = (pred_df_v2.loc[valid_v2, col] == labeled.loc[valid_v2, col]).mean()\n    print(f\"{col}: {acc:.3f}\")\n\noverall_v2 = (pred_df_v2.loc[valid_v2, label_cols].values == labeled.loc[valid_v2, label_cols].values).mean()\nprint(f\"\\nOverall accuracy: {overall_v2:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.437544Z","iopub.status.idle":"2026-08-30T07:37:09.437847Z","shell.execute_reply.started":"2026-08-30T07:37:09.437664Z","shell.execute_reply":"2026-08-30T07:37:09.437679Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from extractor import extract_labels, label_cols\nimport time\n\nresults_v2 = []\nfor i in range(len(labeled)):\n    report = labeled.iloc[i]['Report']\n    try:\n        pred = extract_labels(model, report)\n        results_v2.append(pred)\n        print(f\"Row {i}: OK\")\n    except Exception as e:\n        print(f\"Row {i}: Error - {e}\")\n        results_v2.append({col: None for col in label_cols})\n    time.sleep(3)\n\npred_df_v2 = pd.DataFrame(results_v2)\npred_df_v2.index = labeled.index\nvalid_v2 = pred_df_v2['ACL'].notna()\n\nfor col in label_cols:\n    acc = (pred_df_v2.loc[valid_v2, col] == labeled.loc[valid_v2, col]).mean()\n    print(f\"{col}: {acc:.3f}\")\n\noverall_v2 = (pred_df_v2.loc[valid_v2, label_cols].values == labeled.loc[valid_v2, label_cols].values).mean()\nprint(f\"\\nOverall accuracy: {overall_v2:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.439069Z","iopub.status.idle":"2026-08-30T07:37:09.439475Z","shell.execute_reply.started":"2026-08-30T07:37:09.439244Z","shell.execute_reply":"2026-08-30T07:37:09.439272Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport time\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\n\n# Resume from checkpoint if it exists, otherwise start fresh\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming: {len(done_ids)} already done\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh\")\n\nremaining = train[~train['StudyInstanceUID'].isin(done_ids)]\nprint(f\"{len(remaining)} reports left to process\")\n\nresults_all = []\nsave_every = 25  # save progress every 25 rows\n\nfor i, (idx, row) in enumerate(remaining.iterrows()):\n    try:\n        pred = extract_labels(model, row['Report'])\n        pred['StudyInstanceUID'] = row['StudyInstanceUID']\n        results_all.append(pred)\n    except Exception as e:\n        print(f\"Row {i} ({row['StudyInstanceUID'][:20]}): Error - {e}\")\n        time.sleep(20)  # back off longer on error, then continue\n        continue\n\n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        combined = pd.concat([done_df, batch_df], ignore_index=True)\n        combined.to_csv(CHECKPOINT_FILE, index=False)\n        print(f\"Checkpoint saved: {len(combined)} total rows done ({i+1}/{len(remaining)} this session)\")\n\n    time.sleep(4)  # ~15/min pace to respect free tier limit\n\n# final save\nbatch_df = pd.DataFrame(results_all)\ncombined = pd.concat([done_df, batch_df], ignore_index=True)\ncombined.to_csv(CHECKPOINT_FILE, index=False)\nprint(f\"\\nDone! Total processed: {len(combined)} out of {len(train)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.440969Z","iopub.status.idle":"2026-08-30T07:37:09.441329Z","shell.execute_reply.started":"2026-08-30T07:37:09.44114Z","shell.execute_reply":"2026-08-30T07:37:09.441164Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile groq_test.py\nfrom groq import Groq\n\ndef test_groq(api_key):\n    client = Groq(api_key=api_key)\n    response = client.chat.completions.create(\n        model=\"llama-3.3-70b-versatile\",\n        messages=[{\"role\": \"user\", \"content\": \"Say hello in one word.\"}]\n    )\n    return response.choices[0].message.content","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.442588Z","iopub.status.idle":"2026-08-30T07:37:09.443299Z","shell.execute_reply.started":"2026-08-30T07:37:09.442921Z","shell.execute_reply":"2026-08-30T07:37:09.44294Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install groq --quiet\n\nfrom kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"GROQ_API_KEY\")\n\nfrom groq_test import test_groq\nprint(test_groq(api_key))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.444644Z","iopub.status.idle":"2026-08-30T07:37:09.445043Z","shell.execute_reply.started":"2026-08-30T07:37:09.444902Z","shell.execute_reply":"2026-08-30T07:37:09.44492Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile groq_test.py\nfrom groq import Groq\n\ndef test_groq(api_key):\n    client = Groq(api_key=api_key)\n    response = client.chat.completions.create(\n        model=\"openai/gpt-oss-120b\",\n        messages=[{\"role\": \"user\", \"content\": \"Say hello in one word.\"}]\n    )\n    return response.choices[0].message.content","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.446665Z","iopub.status.idle":"2026-08-30T07:37:09.447015Z","shell.execute_reply.started":"2026-08-30T07:37:09.446874Z","shell.execute_reply":"2026-08-30T07:37:09.446892Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from groq_test import test_groq\nprint(test_groq(api_key))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.448116Z","iopub.status.idle":"2026-08-30T07:37:09.448527Z","shell.execute_reply.started":"2026-08-30T07:37:09.448363Z","shell.execute_reply":"2026-08-30T07:37:09.448392Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import importlib\nimport groq_test\nimportlib.reload(groq_test)\nfrom groq_test import test_groq\n\nprint(test_groq(api_key))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.449823Z","iopub.status.idle":"2026-08-30T07:37:09.450224Z","shell.execute_reply.started":"2026-08-30T07:37:09.450023Z","shell.execute_reply":"2026-08-30T07:37:09.450047Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile extractor_groq.py\nimport json\n\nlabel_cols = ['ACL','MCL','Medial Meniscus','Lateral Meniscus','Medial OA','Lateral OA','PF OA','Effusion','Synovitis',\"Baker's\",'Contusion','Fracture']\n\ndef build_prompt(report_text):\n    example_json = json.dumps({col: 0 for col in label_cols})\n    parts = []\n    parts.append(\"You are a radiology expert.\")\n    parts.append(\"Read this knee MRI report (it may be in any language) and determine whether each of the following 12 conditions is present (1) or absent (0).\")\n    parts.append(\"IMPORTANT: Only count a finding as present (1) if it is clinically significant. Trace, minimal, or trivial findings should generally be scored as 0 unless the report emphasizes it as a notable finding. Only osteochondral or cartilage-only fractures should be excluded from Fracture; traumatic bone fractures (including bony avulsions) count as Fracture=1.\")\n    parts.append(\"Conditions: ACL tear, MCL tear, Medial Meniscus tear, Lateral Meniscus tear, Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Bakers cyst, Contusion, Fracture (traumatic, including avulsion fractures; not osteochondral/cartilage-only lesions).\")\n    parts.append(\"Respond with ONLY a JSON object in this exact format, no other text:\")\n    parts.append(example_json)\n    parts.append(\"Report:\")\n    parts.append(report_text)\n    return \"\\n\".join(parts)\n\ndef extract_labels_groq(client, report_text):\n    prompt = build_prompt(report_text)\n    response = client.chat.completions.create(\n        model=\"openai/gpt-oss-120b\",\n        messages=[{\"role\": \"user\", \"content\": prompt}]\n    )\n    text = response.choices[0].message.content.strip()\n    text = text.replace('```json', '').replace('```', '').strip()\n    return json.loads(text)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.451968Z","iopub.status.idle":"2026-08-30T07:37:09.452388Z","shell.execute_reply.started":"2026-08-30T07:37:09.452162Z","shell.execute_reply":"2026-08-30T07:37:09.452187Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from groq import Groq\nfrom extractor_groq import extract_labels_groq, label_cols\n\ngroq_client = Groq(api_key=api_key)\n\ntest_result = extract_labels_groq(groq_client, labeled.iloc[0]['Report'])\nprint(test_result)\nprint(\"Actual:\", labeled.iloc[0][label_cols].to_dict())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.453319Z","iopub.status.idle":"2026-08-30T07:37:09.453724Z","shell.execute_reply.started":"2026-08-30T07:37:09.453499Z","shell.execute_reply":"2026-08-30T07:37:09.453525Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\nlabeled = train.dropna(subset=['ACL']).copy()\nprint(labeled.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.455029Z","iopub.status.idle":"2026-08-30T07:37:09.455289Z","shell.execute_reply.started":"2026-08-30T07:37:09.455159Z","shell.execute_reply":"2026-08-30T07:37:09.455174Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_result = extract_labels_groq(groq_client, labeled.iloc[0]['Report'])\nprint(test_result)\nprint(\"Actual:\", labeled.iloc[0][label_cols].to_dict())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.457521Z","iopub.status.idle":"2026-08-30T07:37:09.458621Z","shell.execute_reply.started":"2026-08-30T07:37:09.458398Z","shell.execute_reply":"2026-08-30T07:37:09.458444Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport time\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\n\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming: {len(done_ids)} already done\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh\")\n\nremaining = train[~train['StudyInstanceUID'].isin(done_ids)]\nprint(f\"{len(remaining)} reports left to process\")\n\nresults_all = []\nsave_every = 25\n\nfor i, (idx, row) in enumerate(remaining.iterrows()):\n    try:\n        pred = extract_labels_groq(groq_client, row['Report'])\n        pred['StudyInstanceUID'] = row['StudyInstanceUID']\n        results_all.append(pred)\n        print(f\"Row {i}: OK\")\n    except Exception as e:\n        print(f\"Row {i} ({row['StudyInstanceUID'][:20]}): Error - {e}\")\n        time.sleep(10)\n        continue\n\n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        combined = pd.concat([done_df, batch_df], ignore_index=True)\n        combined.to_csv(CHECKPOINT_FILE, index=False)\n        print(f\"Checkpoint saved: {len(combined)} total rows done ({i+1}/{len(remaining)} this session)\")\n\n    time.sleep(1)  # Groq's free tier is much faster/more generous than Gemini's\n\nbatch_df = pd.DataFrame(results_all)\ncombined = pd.concat([done_df, batch_df], ignore_index=True)\ncombined.to_csv(CHECKPOINT_FILE, index=False)\nprint(f\"\\nDone! Total processed: {len(combined)} out of {len(train)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.459534Z","iopub.status.idle":"2026-08-30T07:37:09.459895Z","shell.execute_reply.started":"2026-08-30T07:37:09.459759Z","shell.execute_reply":"2026-08-30T07:37:09.459778Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install transformers accelerate torch --quiet\n\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\nimport torch\n\nmodel_name = \"Qwen/Qwen2.5-7B-Instruct\"\n\ntokenizer = AutoTokenizer.from_pretrained(model_name)\nlocal_model = AutoModelForCausalLM.from_pretrained(\n    model_name, torch_dtype=torch.float16, device_map=\"auto\"\n)\nprint(\"Model loaded successfully\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.460912Z","iopub.status.idle":"2026-08-30T07:37:09.461205Z","shell.execute_reply.started":"2026-08-30T07:37:09.461075Z","shell.execute_reply":"2026-08-30T07:37:09.461092Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig\n\nmodel_name = \"Qwen/Qwen2.5-7B-Instruct\"\n\n# Configure 4-bit quantization for VRAM efficiency\nbnb_config = BitsAndBytesConfig(\n    load_in_4bit=True,\n    bnb_4bit_compute_dtype=torch.float16,\n    bnb_4bit_use_double_quant=True\n)\n\ntokenizer = AutoTokenizer.from_pretrained(model_name)\ntokenizer.padding_side = \"left\" # Critical for batched generation\nif tokenizer.pad_token is None:\n    tokenizer.pad_token = tokenizer.eos_token\n\nlocal_model = AutoModelForCausalLM.from_pretrained(\n    model_name,\n    quantization_config=bnb_config,\n    device_map=\"auto\"\n)\nlocal_model.eval()\nprint(\"Model loaded successfully in 4-bit precision!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.462889Z","iopub.status.idle":"2026-08-30T07:37:09.463193Z","shell.execute_reply.started":"2026-08-30T07:37:09.463061Z","shell.execute_reply":"2026-08-30T07:37:09.463078Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -q bitsandbytes accelerate","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.464643Z","iopub.status.idle":"2026-08-30T07:37:09.464979Z","shell.execute_reply.started":"2026-08-30T07:37:09.464839Z","shell.execute_reply":"2026-08-30T07:37:09.464865Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig\n\nmodel_name = \"Qwen/Qwen2.5-7B-Instruct\"\n\n# Configure 4-bit quantization\nbnb_config = BitsAndBytesConfig(\n    load_in_4bit=True,\n    bnb_4bit_compute_dtype=torch.float16,\n    bnb_4bit_use_double_quant=True\n)\n\ntokenizer = AutoTokenizer.from_pretrained(model_name)\ntokenizer.padding_side = \"left\"\nif tokenizer.pad_token is None:\n    tokenizer.pad_token = tokenizer.eos_token\n\nlocal_model = AutoModelForCausalLM.from_pretrained(\n    model_name,\n    quantization_config=bnb_config,\n    device_map=\"auto\"\n)\nlocal_model.eval()\nprint(\"Model loaded successfully in 4-bit!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.466786Z","iopub.status.idle":"2026-08-30T07:37:09.467086Z","shell.execute_reply.started":"2026-08-30T07:37:09.46695Z","shell.execute_reply":"2026-08-30T07:37:09.466967Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -U bitsandbytes>=0.46.1 accelerate --quiet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.468455Z","iopub.status.idle":"2026-08-30T07:37:09.468806Z","shell.execute_reply.started":"2026-08-30T07:37:09.468622Z","shell.execute_reply":"2026-08-30T07:37:09.468639Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -U bitsandbytes>=0.46.1 accelerate --quiet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.470289Z","iopub.status.idle":"2026-08-30T07:37:09.470552Z","shell.execute_reply.started":"2026-08-30T07:37:09.47042Z","shell.execute_reply":"2026-08-30T07:37:09.470435Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -U bitsandbytes>=0.46.1 accelerate --quiet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.471565Z","iopub.status.idle":"2026-08-30T07:37:09.472327Z","shell.execute_reply.started":"2026-08-30T07:37:09.472153Z","shell.execute_reply":"2026-08-30T07:37:09.472172Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nmodel_name = \"Qwen/Qwen2.5-7B-Instruct\"\n\ntokenizer = AutoTokenizer.from_pretrained(model_name)\ntokenizer.padding_side = \"left\"\nif tokenizer.pad_token is None:\n    tokenizer.pad_token = tokenizer.eos_token\n\n# Load directly in float16 without bitsandbytes quantization\nlocal_model = AutoModelForCausalLM.from_pretrained(\n    model_name,\n    torch_dtype=torch.float16,\n    device_map=\"auto\"\n)\nlocal_model.eval()\nprint(\"Model loaded successfully in float16!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.473938Z","iopub.status.idle":"2026-08-30T07:37:09.474189Z","shell.execute_reply.started":"2026-08-30T07:37:09.474066Z","shell.execute_reply":"2026-08-30T07:37:09.474082Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport torch\nimport pandas as pd\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\nlabel_cols = ['ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', 'Medial OA', 'Lateral OA',\n              'PF OA', 'Effusion', 'Synovitis', \"Baker's\", 'Contusion', 'Fracture']\n\n# 1. Resume from checkpoint if it exists\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming: {len(done_ids)} studies already processed.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh pseudo-labeling run...\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Reports remaining to process: {len(remaining_df)}\")\n\nresults_all = []\nsave_every = 25  # Save checkpoint every 25 rows\n\n# 2. Processing Loop\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    # Construct prompt with our calibrated clinical rules\n    chat = [\n        {\"role\": \"system\", \"content\": \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"},\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=1024).to(local_model.device)\n    \n    with torch.no_grad():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            temperature=0.0,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    generated_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)\n    \n    try:\n        clean_text = generated_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n        start, end = clean_text.find(\"{\"), clean_text.rfind(\"}\") + 1\n        pred = json.loads(clean_text[start:end])\n    except Exception as e:\n        # Fallback dictionary if parsing fails\n        pred = {col: 0 for col in label_cols}\n        \n    pred['StudyInstanceUID'] = row['StudyInstanceUID']\n    results_all.append(pred)\n    \n    # 3. Iterative checkpoint saving\n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        combined = pd.concat([done_df, batch_df], ignore_index=True)\n        combined.to_csv(CHECKPOINT_FILE, index=False)\n        results_all = []\n        print(f\"Checkpoint saved: Processed {i + 1} / {len(remaining_df)} remaining reports.\")\n\n# Final save for any leftover rows\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    combined = pd.concat([done_df, batch_df], ignore_index=True)\n    combined.to_csv(CHECKPOINT_FILE, index=False)\n\nprint(\"Finished generating pseudo-labels for all studies!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.475566Z","iopub.status.idle":"2026-08-30T07:37:09.475889Z","shell.execute_reply.started":"2026-08-30T07:37:09.475747Z","shell.execute_reply":"2026-08-30T07:37:09.475774Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\n# Load the main train dataframe\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\nprint(\"Train shape loaded successfully:\", train.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.477277Z","iopub.status.idle":"2026-08-30T07:37:09.477591Z","shell.execute_reply.started":"2026-08-30T07:37:09.477467Z","shell.execute_reply":"2026-08-30T07:37:09.477483Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport torch\nimport pandas as pd\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\nlabel_cols = ['ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', 'Medial OA', 'Lateral OA',\n              'PF OA', 'Effusion', 'Synovitis', \"Baker's\", 'Contusion', 'Fracture']\n\n# 1. Resume from checkpoint if it exists\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming: {len(done_ids)} studies already processed.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh pseudo-labeling run...\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Reports remaining to process: {len(remaining_df)}\")\n\nresults_all = []\nsave_every = 25  # Save checkpoint every 25 rows\n\n# 2. Processing Loop\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    # Construct prompt with our calibrated clinical rules\n    chat = [\n        {\"role\": \"system\", \"content\": \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"},\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=It looks like you might have hit send by accident while working on your Kaggle notebook! \n\nYou were in the middle of setting up your Groq client (`openai/gpt-oss-120b`) to process the remaining knee MRI reports for the **RSNA Knee Abnormality Detection** competition after hitting the Gemini API free tier rate limits. \n\nWould you like help putting together the batch processing and checkpoint loop for your remaining data using Groq? Let me know how you'd like to proceed!","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.478446Z","iopub.status.idle":"2026-08-30T07:37:09.478789Z","shell.execute_reply.started":"2026-08-30T07:37:09.4786Z","shell.execute_reply":"2026-08-30T07:37:09.478621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport torch\nimport pandas as pd\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\nlabel_cols = ['ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', 'Medial OA', 'Lateral OA',\n              'PF OA', 'Effusion', 'Synovitis', \"Baker's\", 'Contusion', 'Fracture']\n\n# 1. Resume from checkpoint if it exists\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming: {len(done_ids)} studies already processed.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh pseudo-labeling run...\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Reports remaining to process: {len(remaining_df)}\")\n\nresults_all = []\nsave_every = 25  # Save checkpoint every 25 rows\n\n# 2. Processing Loop\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    # Construct prompt with our calibrated clinical rules\n    chat = [\n        {\"role\": \"system\", \"content\": \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"},\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenizeHere is the complete batch processing and checkpoint loop using your Groq client (`openai/gpt-oss-120b`) to finish labeling the remaining knee MRI reports for the **RSNA Knee Abnormality Detection** competition without hitting rate limits:\n\n```python\nimport os\nimport json\nimport time\nimport pandas as pd\nfrom groq import Groq\nfrom extractor_groq import extract_labels_groq, label_cols\n\n# Initialize Groq client\ngroq_client = Groq(api_key=api_key)\n\nCHECKPOINT_FILE = 'pseudo_labels_groq_checkpoint.csv'\nsave_every = 25\n\n# 1. Load train dataset & identify already processed rows\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\nlabeled = train.dropna(subset=['ACL']).copy()\n\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming: {len(done_ids)} already done\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh checkpoint\")\n\nremaining = labeled[~labeled['StudyInstanceUID'].isin(done_ids)]\nprint(f\"{len(remaining)} reports left to process\")\n\nresults_all = []\n\n# 2. Main processing loop\nfor i, (idx, row) in enumerate(remaining.iterrows()):\n    try:\n        pred = extract_labels_groq(groq_client, row['Report'])\n        pred['StudyInstanceUID'] = row['StudyInstanceUID']\n        results_all.append(pred)\n        print(f\"Row {i+1}/{len(remaining)}: OK\")\n    except Exception as e:\n        print(f\"Row {i+1} ({row['StudyInstanceUID'][:20]}): Error - {e}\")\n        time.sleep(10) # back off on error\n        continue\n    \n    # Save periodically\n    if (i + 1) % save_every == 0:\n        if results_all:\n            batch_df = pd.DataFrame(results_all)\n            combined = pd.concat([done_df, batch_df], ignore_index=True)\n            combined.to_csv(CHECKPOINT_FILE, index=False)\n            done_df = combined\n            results_all = []\n            print(f\"--- Checkpoint saved at row {i+1} ---\")\n    \n    # Small delay to stay safe on rate limits\n    time.sleep(1)\n\n# Save any remaining results\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    combined = pd.concat([done_df, batch_df], ignore_index=True)\n    combined.to_csv(CHECKPOINT_FILE, index=False)\n    print(\"--- Final checkpoint saved ---\")\n\nprint(\"Processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.480812Z","iopub.status.idle":"2026-08-30T07:37:09.481202Z","shell.execute_reply.started":"2026-08-30T07:37:09.480998Z","shell.execute_reply":"2026-08-30T07:37:09.481025Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport torch\nimport pandas as pd\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\nlabel_cols = ['ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', 'Medial OA', 'Lateral OA',\n              'PF OA', 'Effusion', 'Synovitis', \"Baker's\", 'Contusion', 'Fracture']\n\nif 'train' not in globals():\n    train = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming: {len(done_ids)} studies already processed.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh pseudo-labeling run...\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Reports remaining to process: {len(remaining_df)}\")\n\nresults_all = []\nsave_every = 25\n\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\"role\": \"system\", \"content\": \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"},\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=1024).to(local_model.device)\n    \n    with torch.no_grad():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            temperature=0.0,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )Looks like some explanatory text accidentally slipped right into your Python code cell instead of a Markdown cell, causing Python to throw a `SyntaxError`. \n\nHere is the clean, complete batch processing and checkpoint loop using your Groq client (`openai/gpt-oss-120b`) to finish labeling the remaining knee MRI reports. Just paste this into a fresh **Code** cell:\n\n```python\nimport os\nimport time\nimport pandas as pd\nfrom extractor_groq import extract_labels_groq, label_cols\nfrom groq import Groq\n\nCHECKPOINT_FILE = \"pseudo_labels_checkpoint_groq.csv\"\n\n# Resume from checkpoint if it exists, otherwise start fresh\nif os.path.exists(CHECKPOINT_FILE):\n  done_df = pd.read_csv(CHECKPOINT_FILE)\n  done_ids = set(done_df[\"StudyInstanceUID\"])\n  print(f\"Resuming: {len(done_ids)} already done\")\nelse:\n  done_df = pd.DataFrame(columns=[\"StudyInstanceUID\"] + label_cols)\n  done_ids = set()\n  print(\"Starting fresh\")\n\nremaining = train[~train[\"StudyInstanceUID\"].isin(done_ids)]\nprint(f\"{len(remaining)} reports left to process\")\n\nresults_all = []\nsave_every = 25  # save progress every 25 rows\n\nfor i, (idx, row) in enumerate(remaining.iterrows()):\n  try:\n    pred = extract_labels_groq(groq_client, row[\"Report\"])\n    pred[\"StudyInstanceUID\"] = row[\"StudyInstanceUID\"]\n    results_all.append(pred)\n    print(f\"Row {i}: OK\")\n  except Exception as e:\n    print(f\"Row {i} ({row['StudyInstanceUID'][:20]}): Error - {e}\")\n    time.sleep(5)  # brief back off on error\n    continue\n\n  if (i + 1) % save_every == 0:\n    batch_df = pd.DataFrame(results_all)\n    combined = pd.concat([done_df, batch_df], ignore_index=True)\n    combined.to_csv(CHECKPOINT_FILE, index=False)\n    done_df = combined  # update done_df reference\n    results_all = []  # clear buffer\n    print(\"--- Checkpoint saved ---\")\n\n# Save any remaining results\nif results_all:\n  batch_df = pd.DataFrame(results_all)\n  combined = pd.concat([done_df, batch_df], ignore_index=True)\n  combined.to_csv(CHECKPOINT_FILE, index=False)\n  print(\"--- Final checkpoint saved ---\")\n\nprint(\"Processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.483333Z","iopub.status.idle":"2026-08-30T07:37:09.483593Z","shell.execute_reply.started":"2026-08-30T07:37:09.483462Z","shell.execute_reply":"2026-08-30T07:37:09.483476Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport time\nimport pandas as pd\nfrom groq import Groq\nfrom extractor_groq import extract_labels_groq, label_cols\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint_groq.csv'\n\n# Resume from checkpoint if it exists, otherwise start fresh\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming: {len(done_ids)} already done\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh\")\n\nremaining = train[~train['StudyInstanceUID'].isin(done_ids)]\nprint(f\"{len(remaining)} reports left to process\")\n\nresults_all = []\nsave_every = 25  # save progress every 25 rows\n\nfor i, (idx, row) in enumerate(remaining.iterrows()):\n    try:\n        pred = extract_labels_groq(groq_client, row['Report'])\n        pred['StudyInstanceUID'] = row['StudyInstanceUID']\n        results_all.append(pred)\n        print(f\"Row {i}: OK\")\n    except Exception as e:\n        print(f\"Row {i} ({row['StudyInstanceUID'][:20]}): Error - {e}\")\n        time.sleep(5)  # brief back off on error\n        continue\n\n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        combined = pd.concat([done_df, batch_df], ignore_index=True)\n        combined.to_csv(CHECKPOINT_FILE, index=False)\n        done_df = combined  # update done_df reference\n        results_all = []    # clear buffer\n        print(\"--- Checkpoint saved ---\")\n\n# Save any remaining results\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    combined = pd.concat([done_df, batch_df], ignore_index=True)\n    combined.to_csv(CHECKPOINT_FILE, index=False)\n    print(\"--- Final checkpoint saved ---\")\n\nprint(\"Processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.484399Z","iopub.status.idle":"2026-08-30T07:37:09.484794Z","shell.execute_reply.started":"2026-08-30T07:37:09.484578Z","shell.execute_reply":"2026-08-30T07:37:09.484603Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install groq --quiet\n\nimport os\nimport time\nimport pandas as pd\nfrom groq import Groq\nfrom extractor_groq import extract_labels_groq, label_cols\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint_groq.csv'\n\n# Resume from checkpoint if it exists, otherwise start fresh\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming: {len(done_ids)} already done\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh\")\n\nremaining = train[~train['StudyInstanceUID'].isin(done_ids)]\nprint(f\"{len(remaining)} reports left to process\")\n\nresults_all = []\nsave_every = 25  # save progress every 25 rows\n\nfor i, (idx, row) in enumerate(remaining.iterrows()):\n    try:\n        pred = extract_labels_groq(groq_client, row['Report'])\n        pred['StudyInstanceUID'] = row['StudyInstanceUID']\n        results_all.append(pred)\n        print(f\"Row {i}: OK\")\n    except Exception as e:\n        print(f\"Row {i} ({row['StudyInstanceUID'][:20]}): Error - {e}\")\n        time.sleep(5)  # brief back off on error\n        continue\n\n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        combined = pd.concat([done_df, batch_df], ignore_index=True)\n        combined.to_csv(CHECKPOINT_FILE, index=False)\n        done_df = combined  # update done_df reference\n        results_all = []    # clear buffer\n        print(\"--- Checkpoint saved ---\")\n\n# Save any remaining results\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    combined = pd.concat([done_df, batch_df], ignore_index=True)\n    combined.to_csv(CHECKPOINT_FILE, index=False)\n    print(\"--- Final checkpoint saved ---\")\n\nprint(\"Processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.486067Z","iopub.status.idle":"2026-08-30T07:37:09.486482Z","shell.execute_reply.started":"2026-08-30T07:37:09.486298Z","shell.execute_reply":"2026-08-30T07:37:09.486322Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install groq --quiet\n\nimport os\nimport time\nimport json\nimport pandas as pd\nfrom groq import Groq\nfrom kaggle_secrets import UserSecretsClient\n\n# 1. Initialize Groq client securely from Kaggle secrets\nuser_secrets = UserSecretsClient()\napi_key = user_secrets.get_secret(\"GROQ_API_KEY\")\ngroq_client = Groq(api_key=api_key)\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint_groq.csv'\n\nlabel_cols = ['ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', 'Medial OA', 'Lateral OA',\n              'PF OA', 'Effusion', 'Synovitis', \"Baker's\", 'Contusion', 'Fracture']\n\ndef build_prompt(report_text):\n    example_json = json.dumps({col: 0 for col in label_cols})\n    parts = []\n    parts.append(\"You are a radiology expert.\")\n    parts.append(\"Read this knee MRI report (it may be in any language) and determine whether each of the following 12 conditions is present (1) or absent (0).\")\n    parts.append(\"IMPORTANT: Only count a finding as present (1) if it is clinically significant. Trace, minimal, or trivial findings should generally be scored as 0 unless the report emphasizes it as a notable finding. Only osteochondral or cartilage-only fractures should be excluded from Fracture; traumatic bone fractures (including bony avulsions) count as Fracture=1.\")\n    parts.append(\"Conditions: ACL tear, MCL tear, Medial Meniscus tear, Lateral Meniscus tear, Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Bakers cyst, Contusion, Fracture (traumatic, including avulsion fractures; not osteochondral/cartilage-only lesions).\")\n    parts.append(\"Respond with ONLY a JSON object in this exact format, no other text:\")\n    parts.append(example_json)\n    parts.append(\"Report:\")\n    parts.append(report_text)\n    return \"\\n\".join(parts)\n\ndef extract_labels_groq(client, report_text):\n    prompt = build_prompt(report_text)\n    response = client.chat.completions.create(\n        model=\"openai/gpt-oss-120b\",\n        messages=[{\"role\": \"user\", \"content\": prompt}]\n    )\n    text = response.choices[0].message.content.strip()\n    text = text.replace('```json', '').replace('```', '').strip()\n    return json.loads(text)\n\n# 2. Load train dataset if not already loaded\nif 'train' not in globals():\n    train = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\n# 3. Resume from checkpoint if it exists, otherwise start fresh\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming: {len(done_ids)} already done\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh\")\n\nremaining = train[~train['StudyInstanceUID'].isin(done_ids)]\nprint(f\"{len(remaining)} reports left to process\")\n\nresults_all = []\nsave_every = 25  # save progress every 25 rows\n\n# 4. Main Processing Loop\nfor i, (idx, row) in enumerate(remaining.iterrows()):\n    try:\n        pred = extract_labels_groq(groq_client, row['Report'])\n        pred['StudyInstanceUID'] = row['StudyInstanceUID']\n        results_all.append(pred)\n        print(f\"Row {i}: OK\")\n    except Exception as e:\n        print(f\"Row {i} ({row['StudyInstanceUID'][:20]}): Error - {e}\")\n        time.sleep(5)  # brief back off on error\n        continue\n\n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        combined = pd.concat([done_df, batch_df], ignore_index=True)\n        combined.to_csv(CHECKPOINT_FILE, index=False)\n        done_df = combined  # update done_df reference\n        results_all = []    # clear buffer\n        print(\"--- Checkpoint saved ---\")\n\n# Save any remaining results\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    combined = pd.concat([done_df, batch_df], ignore_index=True)\n    combined.to_csv(CHECKPOINT_FILE, index=False)\n    print(\"--- Final checkpoint saved ---\")\n\nprint(\"Processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.487944Z","iopub.status.idle":"2026-08-30T07:37:09.488327Z","shell.execute_reply.started":"2026-08-30T07:37:09.488125Z","shell.execute_reply":"2026-08-30T07:37:09.488149Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport torch\nimport pandas as pd\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint_local.csv'\nlabel_cols = ['ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', 'Medial OA', 'Lateral OA',\n              'PF OA', 'Effusion', 'Synovitis', \"Baker's\", 'Contusion', 'Fracture']\n\n# 1. Ensure train dataframe is loaded\nif 'train' not in globals():\n    train = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\n# 2. Resume from local checkpoint if it exists\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'])\n    print(f\"Resuming locally: {len(done_ids)} studies already processed.\")\nelse:\n    # If you want to inherit what Groq already finished, you can copy rows over, \n    # but starting fresh or combining CSVs is easiest. Let's start a clean local run:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting local pseudo-labeling run with Qwen2.5-7B...\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Reports remaining to process locally: {len(remaining_df)}\")\n\nresults_all = []\nsave_every = 25\n\n# 3. Local Inference Loop (Zero API limits!)\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\"role\": \"system\", \"content\": \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"},\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=1024).to(local_model.device)\n    \n    with torch.no_grad():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            temperature=0.0,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    generated_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)\n    \n    try:\n        clean_text = generated_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n        start, end = clean_text.find(\"{\"), clean_text.rfind(\"}\") + 1\n        pred = json.loads(clean_text[start:end])\n    except Exception as e:\n        pred = {col: 0 for col in label_cols}\n        \n    pred['StudyInstanceUID'] = row['StudyInstanceUID']\n    results_all.append(pred)\n    print(f\"Local Row {i}: OK\")\n    \n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        combined = pd.concat([done_df, batch_df], ignore_index=True)\n        combined.to_csv(CHECKPOINT_FILE, index=False)\n        done_df = combined\n        results_all = []\n        print(f\"--- Local Checkpoint Saved ({i+1}) ---\")\n\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    combined = pd.concat([done_df, batch_df], ignore_index=True)\n    combined.to_csv(CHECKPOINT_FILE, index=False)\n    print(\"--- Final Local Checkpoint Saved ---\")\n\nprint(\"Local processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.489184Z","iopub.status.idle":"2026-08-30T07:37:09.4896Z","shell.execute_reply.started":"2026-08-30T07:37:09.489402Z","shell.execute_reply":"2026-08-30T07:37:09.489428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport torch\nimport pandas as pd\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\n# 1. Load Tokenizer & Model into GPU Memory\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\nprint(\"Loading Qwen2.5-7B into memory...\")\n\ntokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\nmodel = AutoModelForCausalLM.from_pretrained(\n    MODEL_ID,\n    torch_dtype=torch.float16,\n    device_map=\"auto\"\n)\nlocal_model = model\n\n# 2. Checkpoint & Data Setup\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\nlabel_cols = ['ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', 'Medial OA', 'Lateral OA',\n              'PF OA', 'Effusion', 'Synovitis', \"Baker's\", 'Contusion', 'Fracture']\n\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"Resuming: {len(done_ids)} studies already processed and preserved.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh checkpoint.\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Reports remaining to process: {len(remaining_df)}\")\n\n# 3. Local Generation Loop\nresults_all = []\nsave_every = 25\n\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\"role\": \"system\", \"content\": \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"},\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=1024).to(local_model.device)\n    \n    with torch.no_grad():\n        outputs = local_model.generateYour Kaggle session restarted or reset its memory, which cleared the in-memory variables and triggered the error:\n\n```text\nNameError: name 'tokenizer' is not defined....","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.491181Z","iopub.status.idle":"2026-08-30T07:37:09.491595Z","shell.execute_reply.started":"2026-08-30T07:37:09.491357Z","shell.execute_reply":"2026-08-30T07:37:09.49141Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport torch\nimport pandas as pd\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint_local.csv'\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\n# Check for existing checkpoint\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"Resuming: {len(done_ids)} studies already processed.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh local checkpoint.\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Reports remaining to process: {len(remaining_df)}\")\n\nresults_all = []\nsave_every = 25\n\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\n            \"role\": \"system\",\n            \"content\": (\n                \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly \"\n                \"a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, \"\n                \"Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. \"\n                \"Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"\n            )\n        },\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\nNormally I can help with things like this, but I don't seem to have access to that content. You can try again or ask me for something else.","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.493319Z","iopub.status.idle":"2026-08-30T07:37:09.493745Z","shell.execute_reply.started":"2026-08-30T07:37:09.493579Z","shell.execute_reply":"2026-08-30T07:37:09.493596Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport torch\nimport pandas as pd\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\n# 1. Load Model & Tokenizer\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\n\nif 'local_model' not in globals() or 'tokenizer' not in globals():\n    print(\"Loading Qwen2.5-7B into memory...\")\n    tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\n    local_model = AutoModelForCausalLM.from_pretrained(\n        MODEL_ID,\n        torch_dtype=torch.float16,\n        device_map=\"auto\"\n    )\n    print(\"Model loaded successfully!\")\nelse:\n    print(\"Using existing model in memory.\")\n\n# 2. Checkpoint Setup\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint_local.csv'\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"Resuming: {len(done_ids)} studies already processed.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh local checkpoint.\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Reports remaining to process: {len(remaining_df)}\")\n\n# 3. Processing Loop\nresults_all = []\nsave_every = 25\n\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\n            \"role\": \"system\",\n            \"content\": (\n                \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly \"\n                \"a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, \"\n                \"Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. \"\n                \"Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"\n            )\n        },\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=1024).to(local_model.device)\n    \n    with torch.no_grad():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            temperature=0.0,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    generated_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)\n    \n    try:\n        clean_text = generated_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n        start, end = clean_text.find(\"{\"), clean_text.rfind(\"}\") + 1\n        pred = json.loads(clean_text[start:end])\n    except Exception as e:\n        pred = {col: 0 for col in label_cols}\n        \n    pred['StudyInstanceUID'] = row['StudyInstanceUID']\n    results_all.append(pred)\n    print(f\"Local Row {i}: OK\")\n    \n    # Save periodically to prevent progress loss\n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        combined = pd.concat([done_df, batch_df], ignore_index=True)\n        combined.to_csv(CHECKPOINT_FILE, index=False)\n        done_df = combined\n        results_all = []\n        print(f\"--- Local Checkpoint Saved ({len(done_df)} total done) ---\")\n\n# Save remaining rows\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    combined = pd.concat([done_df, batch_df], ignore_index=True)\n    combined.to_csv(CHECKPOINT_FILE, index=False)\n    print(\"--- Final Checkpoint Saved ---\")\n\nprint(\"Local processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.495096Z","iopub.status.idle":"2026-08-30T07:37:09.49568Z","shell.execute_reply.started":"2026-08-30T07:37:09.495515Z","shell.execute_reply":"2026-08-30T07:37:09.495547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport json\nimport torch\nimport pandas as pd\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\n# 1. Model & Tokenizer Verification\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\n\nif 'local_model' not in globals() or 'tokenizer' not in globals():\n    print(\"Loading Qwen2.5-7B into memory...\")\n    tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\n    local_model = AutoModelForCausalLM.from_pretrained(\n        MODEL_ID,\n        torch_dtype=torch.float16,\n        device_map=\"auto\"\n    )\n    print(\"Model loaded successfully!\")\nelse:\n    print(\"Using existing model in memory.\")\n\n# 2. Automatically Find & Merge All Checkpoint CSVs\nPRIMARY_CHECKPOINT = 'pseudo_labels_checkpoint.csv'\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\n# Search for any checkpoint files generated across all runs\ncheckpoint_files = glob.glob('*checkpoint*.csv') + glob.glob('*pseudo*.csv')\ncheckpoint_files = list(set(checkpoint_files))\n\nexisting_frames = []\nfor f in checkpoint_files:\n    try:\n        df_temp = pd.read_csv(f)\n        if 'StudyInstanceUID' in df_temp.columns and len(df_temp) > 0:\n            print(f\"Found existing progress in '{f}': {len(df_temp)} rows\")\n            existing_frames.append(df_temp)\n    except Exception:\n        continue\n\nif existing_frames:\n    done_df = pd.concat(existing_frames, ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    # Save unified base immediately\n    done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"--> Total Recovered & Preserved: {len(done_ids)} studies.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"No existing files found. Starting fresh.\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Actual remaining reports to process: {len(remaining_df)}\")\n\n# 3. Resume Local Generation Loop\nresults_all = []\nsave_every = 25\n\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\n            \"role\": \"system\",\n            \"content\": (\n                \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly \"\n                \"a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, \"\n                \"Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. \"\n                \"Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"\n            )\n        },\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=1024).to(local_model.device)\n    \n    with torch.no_grad():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            temperature=0.0,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    generated_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)\n    \n    try:\n        clean_text = generated_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n        start, end = clean_text.find(\"{\"), clean_text.rfind(\"}\") + 1\n        pred = json.loads(clean_text[start:end])\n    except Exception:\n        pred = {col: 0 for col in label_cols}\n        \n    pred['StudyInstanceUID'] = row['StudyInstanceUID']\n    results_all.append(pred)\n    print(f\"Remaining Row {i} (Done total: {len(done_df) + len(results_all)}): OK\")\n    \n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n        done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n        results_all = []\n        print(f\"--- Checkpoint Updated: {len(done_df)}/{len(train)} complete ---\")\n\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n    print(\"--- Final Checkpoint Saved ---\")\n\nprint(\"Processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.496836Z","iopub.status.idle":"2026-08-30T07:37:09.497068Z","shell.execute_reply.started":"2026-08-30T07:37:09.496959Z","shell.execute_reply":"2026-08-30T07:37:09.496973Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport json\nimport torch\nimport pandas as pd\nfrom transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig\n\n# 1. Clean up lingering VRAM allocations from the crashed process\nif 'local_model' in globals():\n    del local_model\nif 'tokenizer' in globals():\n    del tokenizer\ngc.collect()\ntorch.cuda.empty_cache()\n\n# 2. Configure 4-bit Quantization (Shrinks 7B model to ~4.5 GB VRAM)\nbnb_config = BitsAndBytesConfig(\n    load_in_4bit=True,\n    bnb_4bit_quant_type=\"nf4\",\n    bnb_4bit_compute_dtype=torch.float16,\n    bnb_4bit_use_double_quant=True\n)\n\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\nprint(\"Loading Qwen2.5-7B in memory-safe 4-bit...\")\n\ntokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\nlocal_model = AutoModelForCausalLM.from_pretrained(\n    MODEL_ID,\n    quantization_config=bnb_config,\n    device_map=\"auto\"\n)\nprint(\"Model loaded securely with ~10 GB VRAM headroom!\")\n\n# 3. Recover and unify all existing checkpoint files\nPRIMARY_CHECKPOINT = 'pseudo_labels_checkpoint.csv'\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\ncheckpoint_files = list(set(glob.glob('*checkpoint*.csv') + glob.glob('*pseudo*.csv')))\nexisting_frames = []\n\nfor f in checkpoint_files:\n    try:\n        df_temp = pd.read_csv(f)\n        if 'StudyInstanceUID' in df_temp.columns and len(df_temp) > 0:\n            existing_frames.append(df_temp)\n    except Exception:\n        continue\n\nif existing_frames:\n    done_df = pd.concat(existing_frames, ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"--> Successfully recovered: {len(done_ids)} studies.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"No previous checkpoints found. Starting fresh.\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Reports left to process: {len(remaining_df)}\")\n\n# 4. Memory-Safe Inference Loop\nresults_all = []\nsave_every = 25\n\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\n            \"role\": \"system\",\n            \"content\": (\n                \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly \"\n                \"a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, \"\n                \"Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. \"\n                \"Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"\n            )\n        },\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=768).to(local_model.device)\n    \n    with torch.inference_mode():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    generated_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)\n    \n    try:\n        clean_text = generated_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n        start, end = clean_text.find(\"{\"), clean_text.rfind(\"}\") + 1\n        pred = json.loads(clean_text[start:end])\n    except Exception:\n        pred = {col: 0 for col in label_cols}\n        \n    pred['StudyInstanceUID'] = row['StudyInstanceUID']\n    results_all.append(pred)\n    print(f\"Row {i} (Total Done: {len(done_df) + len(results_all)}): OK\")\n    \n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n        done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n        results_all = []\n        gc.collect()\n        torch.cuda.empty_cache()\n        print(f\"--- Saved Checkpoint: {len(done_df)}/{len(train)} complete ---\")\n\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n    print(\"--- Final Checkpoint Saved ---\")\n\nprint(\"Processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.498542Z","iopub.status.idle":"2026-08-30T07:37:09.498882Z","shell.execute_reply.started":"2026-08-30T07:37:09.498741Z","shell.execute_reply":"2026-08-30T07:37:09.49877Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport json\nimport torch\nimport pandas as pd\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\n# 1. Force flush GPU VRAM\nif 'local_model' in globals():\n    del local_model\nif 'tokenizer' in globals():\n    del tokenizer\ngc.collect()\ntorch.cuda.empty_cache()\n\n# 2. Distribute Model Across Both T4 GPUs (16-bit Precision)\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\nprint(\"Loading Qwen2.5-7B balanced across dual T4 GPUs...\")\n\ntokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\n\n# Balanced split prevents one GPU from overflowing\nmax_mem = {0: \"12GiB\", 1: \"12GiB\"} if torch.cuda.device_count() > 1 else {0: \"14GiB\"}\n\nlocal_model = AutoModelForCausalLM.from_pretrained(\n    MODEL_ID,\n    torch_dtype=torch.float16,\n    device_map=\"auto\",\n    max_memory=max_mem\n)\nprint(f\"Model loaded successfully across {torch.cuda.device_count()} GPU(s)!\")\n\n# 3. Recover and unify all existing checkpoint files\nPRIMARY_CHECKPOINT = 'pseudo_labels_checkpoint.csv'\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\ncheckpoint_files = list(set(glob.glob('*checkpoint*.csv') + glob.glob('*pseudo*.csv')))\nexisting_frames = []\n\nfor f in checkpoint_files:\n    try:\n        df_temp = pd.read_csv(f)\n        if 'StudyInstanceUID' in df_temp.columns and len(df_temp) > 0:\n            existing_frames.append(df_temp)\n    except Exception:\n        continue\n\nif existing_frames:\n    done_df = pd.concat(existing_frames, ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"--> Preserved and unified {len(done_ids)} completed studies.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh checkpoint.\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Remaining reports to process: {len(remaining_df)}\")\n\n# 4. Resumed Generation Loop\nresults_all = []\nsave_every = 25\n\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\n            \"role\": \"system\",\n            \"content\": (\n                \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly \"\n                \"a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, \"\n                \"Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. \"\n                \"Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"\n            )\n        },\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=1024).to(local_model.device)\n    \n    with torch.inference_mode():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    generated_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)\n    \n    try:\n        clean_text = generated_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n        start, end = clean_text.find(\"{\"), clean_text.rfind(\"}\") + 1\n        pred = json.loads(clean_text[start:end])\n    except Exception:\n        pred = {col: 0 for col in label_cols}\n        \n    pred['StudyInstanceUID'] = row['StudyInstanceUID']\n    results_all.append(pred)\n    print(f\"Row {i} (Total Done: {len(done_df) + len(results_all)}): OK\")\n    \n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n        done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n        results_all = []\n        gc.collect()\n        torch.cuda.empty_cache()\n        print(f\"--- Saved Checkpoint: {len(done_df)}/{len(train)} complete ---\")\n\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n    print(\"--- Final Checkpoint Saved ---\")\n\nprint(\"Processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.500046Z","iopub.status.idle":"2026-08-30T07:37:09.500625Z","shell.execute_reply.started":"2026-08-30T07:37:09.500488Z","shell.execute_reply":"2026-08-30T07:37:09.500507Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nos._exit(0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.502511Z","iopub.status.idle":"2026-08-30T07:37:09.502905Z","shell.execute_reply.started":"2026-08-30T07:37:09.502751Z","shell.execute_reply":"2026-08-30T07:37:09.502775Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport json\nimport torch\nimport pandas as pd\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\n# 1. Recover and unify all existing checkpoint files FIRST\nPRIMARY_CHECKPOINT = 'pseudo_labels_checkpoint.csv'\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\ncheckpoint_files = list(set(glob.glob('*checkpoint*.csv') + glob.glob('*pseudo*.csv')))\nexisting_frames = []\n\nfor f in checkpoint_files:\n    try:\n        df_temp = pd.read_csv(f)\n        if 'StudyInstanceUID' in df_temp.columns and len(df_temp) > 0:\n            print(f\"Found existing progress in '{f}': {len(df_temp)} rows\")\n            existing_frames.append(df_temp)\n    except Exception:\n        continue\n\nif existing_frames:\n    done_df = pd.concat(existing_frames, ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"--> Total Recovered & Preserved: {len(done_ids)} studies.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"No previous checkpoints found. Starting fresh.\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Remaining reports to process: {len(remaining_df)}\")\n\n# 2. Cleanly Load Qwen2.5-7B with Split Memory Limits\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\nprint(\"Loading model across GPUs...\")\n\ntokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\n\n# Allocate max 7 GiB per GPU so 7+ GiB remains free for token generation\nmax_memory_map = {0: \"7.5GiB\", 1: \"7.5GiB\"} if torch.cuda.device_count() > 1 else {0: \"13GiB\"}\n\nlocal_model = AutoModelForCausalLM.from_pretrained(\n    MODEL_ID,\n    torch_dtype=torch.float16,\n    device_map=\"auto\",\n    max_memory=max_memory_map,\n    low_cpu_mem_usage=True\n)\nprint(\"Model loaded with balanced VRAM allocation!\")\n\n# 3. Processing Loop\nresults_all = []\nsave_every = 25\n\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\n            \"role\": \"system\",\n            \"content\": (\n                \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly \"\n                \"a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, \"\n                \"Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. \"\n                \"Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"\n            )\n        },\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=768).to(local_model.device)\n    \n    with torch.inference_mode():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    generated_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)\n    \n    try:\n        clean_text = generated_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n        start, end = clean_text.find(\"{\"), clean_text.rfind(\"}\") + 1\n        pred = json.loads(clean_text[start:end])\n    except Exception:\n        pred = {col: 0 for col in label_cols}\n        \n    pred['StudyInstanceUID'] = row['StudyInstanceUID']\n    results_all.append(pred)\n    print(f\"Row {i} (Done total: {len(done_df) + len(results_all)}): OK\")\n    \n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n        done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n        results_all = []\n        gc.collect()\n        torch.cuda.empty_cache()\n        print(f\"--- Saved Checkpoint: {len(done_df)}/{len(train)} complete ---\")\n\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n    print(\"--- Final Checkpoint Saved ---\")\n\nprint(\"Processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.504186Z","iopub.status.idle":"2026-08-30T07:37:09.504505Z","shell.execute_reply.started":"2026-08-30T07:37:09.504368Z","shell.execute_reply":"2026-08-30T07:37:09.504395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport signal\nimport psutil\nimport torch\n\ncurrent_pid = os.getpid()\nkilled = 0\n\nfor proc in psutil.process_iter(['pid', 'name']):\n    try:\n        pid = proc.info['pid']\n        name = proc.info['name'] or ''\n        if pid != current_pid and 'python' in name.lower():\n            os.kill(pid, signal.SIGKILL)\n            print(f\"Terminated zombie process: PID {pid}\")\n            killed += 1\n    except Exception:\n        pass\n\ntorch.cuda.empty_cache()\nprint(f\"Cleaned up {killed} orphaned processes.\")\n\n# Verify GPU memory is completely free\n!nvidia-smi --query-gpu=index,name,memory.used,memory.total --format=csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.505513Z","iopub.status.idle":"2026-08-30T07:37:09.505888Z","shell.execute_reply.started":"2026-08-30T07:37:09.505663Z","shell.execute_reply":"2026-08-30T07:37:09.505689Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport json\nimport torch\nimport pandas as pd\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\n# 1. Recover and unify all existing checkpoint files\nPRIMARY_CHECKPOINT = 'pseudo_labels_checkpoint.csv'\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\ncheckpoint_files = list(set(glob.glob('*checkpoint*.csv') + glob.glob('*pseudo*.csv')))\nexisting_frames = []\n\nfor f in checkpoint_files:\n    try:\n        df_temp = pd.read_csv(f)\n        if 'StudyInstanceUID' in df_temp.columns and len(df_temp) > 0:\n            print(f\"Found existing progress in '{f}': {len(df_temp)} rows\")\n            existing_frames.append(df_temp)\n    except Exception:\n        continue\n\nif existing_frames:\n    done_df = pd.concat(existing_frames, ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"--> Total Recovered & Preserved: {len(done_ids)} studies.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"No previous checkpoints found. Starting fresh.\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Remaining reports to process: {len(remaining_df)}\")\n\n# 2. Load Qwen2.5-7B evenly across both clean GPUs\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\nprint(\"Loading model across dual GPUs...\")\n\ntokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\nlocal_model = AutoModelForCausalLM.from_pretrained(\n    MODEL_ID,\n    torch_dtype=torch.float16,\n    device_map=\"balanced\",\n    low_cpu_mem_usage=True\n)\nprint(\"Model loaded successfully across both GPUs!\")\n\n# 3. Resume Processing Loop\nresults_all = []\nsave_every = 25\n\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\n            \"role\": \"system\",\n            \"content\": (\n                \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly \"\n                \"a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, \"\n                \"Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. \"\n                \"Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"\n            )\n        },\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=768).to(local_model.device)\n    \n    with torch.inference_mode():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    generated_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)\n    \n    try:\n        clean_text = generated_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n        start, end = clean_text.find(\"{\"), clean_text.rfind(\"}\") + 1\n        pred = json.loads(clean_text[start:end])\n    except Exception:\n        pred = {col: 0 for col in label_cols}\n        \n    pred['StudyInstanceUID'] = row['StudyInstanceUID']\n    results_all.append(pred)\n    print(f\"Row {i} (Total Done: {len(done_df) + len(results_all)}): OK\")\n    \n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(results_all)\n        done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n        done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n        results_all = []\n        gc.collect()\n        torch.cuda.empty_cache()\n        print(f\"--- Saved Checkpoint: {len(done_df)}/{len(train)} complete ---\")\n\nif results_all:\n    batch_df = pd.DataFrame(results_all)\n    done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(PRIMARY_CHECKPOINT, index=False)\n    print(\"--- Final Checkpoint Saved ---\")\n\nprint(\"Processing complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.507252Z","iopub.status.idle":"2026-08-30T07:37:09.5075Z","shell.execute_reply.started":"2026-08-30T07:37:09.507366Z","shell.execute_reply":"2026-08-30T07:37:09.507399Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom sklearn.metrics import roc_auc_score, f1_score, precision_score, recall_score\n\n# 1. Load Extracted Pseudo-Labels and Master Metadata\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\ntrain_df = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\npseudo_df = pd.read_csv(CHECKPOINT_FILE).drop_duplicates(subset=['StudyInstanceUID'])\n\nprint(f\"Total Studies in Dataset: {len(train_df)}\")\nprint(f\"Total Pseudo-Labeled Studies: {len(pseudo_df)}\")\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# Ensure no missing values or corrupted rows\nfor col in label_cols:\n    pseudo_df[col] = pd.to_numeric(pseudo_df[col], errors='coerce').fillna(0).astype(int)\n\n# 2. Isolate the 58 Gold-Standard Oracle Studies\n# (Studies where human annotator ground truth is provided in the official competition metadata)\ngold_mask = train_df[label_cols].notnull().all(axis=1) & (train_df[label_cols].isin([0, 1]).all(axis=1))\ngold_df = train_df[gold_mask].copy()\n\nif len(gold_df) > 0:\n    print(f\"\\nEvaluating against {len(gold_df)} Gold-Standard Studies...\")\n    merged_oracle = pd.merge(gold_df[['StudyInstanceUID'] + label_cols], \n                             pseudo_df[['StudyInstanceUID'] + label_cols], \n                             on='StudyInstanceUID', \n                             suffixes=('_gold', '_pred'))\n    \n    metrics = []\n    for col in label_cols:\n        y_true = merged_oracle[f'{col}_gold'].values\n        y_pred = merged_oracle[f'{col}_pred'].values\n        \n        # Guard against zero-variance classes in small evaluation slices\n        auc = roc_auc_score(y_true, y_pred) if len(np.unique(y_true)) > 1 else 1.0\n        f1 = f1_score(y_true, y_pred, zero_division=0)\n        prec = precision_score(y_true, y_pred, zero_division=0)\n        rec = recall_score(y_true, y_pred, zero_division=0)\n        \n        metrics.append({\n            'Target': col,\n            'ROC-AUC': round(auc, 4),\n            'F1': round(f1, 4),\n            'Precision': round(prec, 4),\n            'Recall': round(rec, 4),\n            'Gold Positives': int(y_true.sum()),\n            'Pred Positives': int(y_pred.sum())\n        })\n    \n    oracle_summary = pd.DataFrame(metrics)\n    display(oracle_summary)\n    print(f\"--> Macro-Averaged Oracle AUC: {oracle_summary['ROC-AUC'].mean():.4f}\")\nelse:\n    print(\"\\nGold-standard annotations isolated. Merging full pseudo-labels for training...\")\n\n# 3. Export Clean Training Master Table\nfinal_train_df = pd.merge(train_df[['StudyInstanceUID']], pseudo_df, on='StudyInstanceUID', how='left')\nfinal_train_df[label_cols] = final_train_df[label_cols].fillna(0).astype(int)\nfinal_train_df.to_csv('final_calibrated_train_labels.csv', index=False)\nprint(\"Saved 'final_calibrated_train_labels.csv' ready for visual training.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.508795Z","iopub.status.idle":"2026-08-30T07:37:09.509072Z","shell.execute_reply.started":"2026-08-30T07:37:09.508953Z","shell.execute_reply":"2026-08-30T07:37:09.508969Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport pandas as pd\nimport numpy as np\nfrom sklearn.metrics import roc_auc_score, f1_score, precision_score, recall_score\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 1. Scan and detect all CSV files in the working directory\nall_csvs = glob.glob('*.csv') + glob.glob('/kaggle/working/*.csv')\nall_csvs = list(set(all_csvs))\n\nprint(f\"Discovered CSV files on disk: {all_csvs}\")\n\ncandidate_frames = []\nfor f in all_csvs:\n    try:\n        # Avoid re-reading master competition metadata as pseudo-labels\n        if 'rsna-knee-abnormality-detection' in f:\n            continue\n        temp_df = pd.read_csv(f)\n        if 'StudyInstanceUID' in temp_df.columns and len(temp_df) > 0:\n            # Check if it has any predicted target columns\n            matched_cols = [c for c in label_cols if c in temp_df.columns]\n            if len(matched_cols) > 0:\n                print(f\"Loading '{f}' -> {len(temp_df)} records (found {len(matched_cols)} target columns)\")\n                candidate_frames.append(temp_df)\n    except Exception as e:\n        continue\n\nif not candidate_frames:\n    raise FileNotFoundError(\"No checkpoint CSVs found. Ensure your extraction script has executed and saved to disk.\")\n\npseudo_df = pd.concat(candidate_frames, ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\nprint(f\"\\n--> Successfully consolidated {len(pseudo_df)} unique pseudo-labeled studies.\")\n\n# Ensure numeric types\nfor col in label_cols:\n    if col in pseudo_df.columns:\n        pseudo_df[col] = pd.to_numeric(pseudo_df[col], errors='coerce').fillna(0).astype(int)\n    else:\n        pseudo_df[col] = 0\n\n# 2. Benchmark against the 58 Gold-Standard Oracle Studies\ntrain_path = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nif os.path.exists(train_path):\n    train_df = pd.read_csv(train_path)\n    \n    # Identify gold standard rows (labeled directly in train.csv)\n    has_labels = [c for c in label_cols if c in train_df.columns]\n    if len(has_labels) == 12:\n        gold_df = train_df[train_df[label_cols].notnull().all(axis=1)].copy()\n        \n        if len(gold_df) > 0:\n            merged_oracle = pd.merge(\n                gold_df[['StudyInstanceUID'] + label_cols],\n                pseudo_df[['StudyInstanceUID'] + label_cols],\n                on='StudyInstanceUID',\n                suffixes=('_gold', '_pred')\n            )\n            \n            if len(merged_oracle) > 0:\n                print(f\"\\n--- Oracle Benchmark ({len(merged_oracle)} Studies Matched) ---\")\n                metrics = []\n                for col in label_cols:\n                    y_true = merged_oracle[f'{col}_gold'].values\n                    y_pred = merged_oracle[f'{col}_pred'].values\n                    \n                    auc = roc_auc_score(y_true, y_pred) if len(np.unique(y_true)) > 1 else 1.0\n                    f1 = f1_score(y_true, y_pred, zero_division=0)\n                    \n                    metrics.append({\n                        'Target': col,\n                        'ROC-AUC': round(auc, 4),\n                        'F1': round(f1, 4),\n                        'Gold Positives': int(y_true.sum()),\n                        'Pred Positives': int(y_pred.sum())\n                    })\n                \n                oracle_summary = pd.DataFrame(metrics)\n                display(oracle_summary)\n                print(f\"--> Macro-Averaged Oracle AUC: {oracle_summary['ROC-AUC'].mean():.4f}\")\n\n# 3. Export Unified Master Dataset for Visual Modeling\npseudo_df.to_csv('final_calibrated_train_labels.csv', index=False)\nprint(\"\\nExported clean 'final_calibrated_train_labels.csv' to /kaggle/working/.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.511319Z","iopub.status.idle":"2026-08-30T07:37:09.511736Z","shell.execute_reply.started":"2026-08-30T07:37:09.511492Z","shell.execute_reply":"2026-08-30T07:37:09.511516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Files in current directory:\", os.listdir('.'))\nprint(\"Files in /kaggle/working:\", os.listdir('/kaggle/working'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.512868Z","iopub.status.idle":"2026-08-30T07:37:09.513232Z","shell.execute_reply.started":"2026-08-30T07:37:09.513049Z","shell.execute_reply":"2026-08-30T07:37:09.513071Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport json\nimport torch\nimport pandas as pd\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 1. Load Data & Existing Checkpoints\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"Resuming: {len(done_ids)} studies already processed on disk.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Remaining reports to batch-process: {len(remaining_df)}\")\n\n# 2. Configure Tokenizer for Batched Left-Padding\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\nif 'local_model' not in globals():\n    print(\"Loading model across GPUs...\")\n    tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\n    local_model = AutoModelForCausalLM.from_pretrained(\n        MODEL_ID,\n        torch_dtype=torch.float16,\n        device_map=\"balanced\",\n        low_cpu_mem_usage=True\n    )\n    print(\"Model ready!\")\n\ntokenizer.padding_side = \"left\"\nif tokenizer.pad_token_id is None:\n    tokenizer.pad_token_id = tokenizer.eos_token_id\n\n# 3. High-Speed Batched Generation Loop (Batch Size = 8)\nBATCH_SIZE = 8\nsave_interval = 40  # Writes to disk every 5 batches\n\nsystem_prompt = (\n    \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly \"\n    \"a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, \"\n    \"Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. \"\n    \"Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"\n)\n\nbuffer = []\ntotal_remaining = len(remaining_df)\n\nfor start_idx in range(0, total_remaining, BATCH_SIZE):\n    batch_rows = remaining_df.iloc[start_idx : start_idx + BATCH_SIZE]\n    \n    batch_prompts = []\n    for _, row in batch_rows.iterrows():\n        chat = [\n            {\"role\": \"system\", \"content\": system_prompt},\n            {\"role\": \"user\", \"content\": f\"Report:\\n{row['Report']}\"}\n        ]\n        prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n        batch_prompts.append(prompt)\n    \n    # Tokenize full batch together\n    inputs = tokenizer(batch_prompts, return_tensors=\"pt\", padding=True, truncation=True, max_length=768).to(local_model.device)\n    input_len = inputs.input_ids.shape[1]\n    \n    with torch.inference_mode():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    # Parse generated outputs in batch\n    for r_idx, (_, row) in enumerate(batch_rows.iterrows()):\n        gen_tokens = outputs[r_idx][input_len:]\n        gen_text = tokenizer.decode(gen_tokens, skip_special_tokens=True)\n        \n        try:\n            clean = gen_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n            s, e = clean.find(\"{\"), clean.rfind(\"}\") + 1\n            pred = json.loads(clean[s:e])\n        except Exception:\n            pred = {col: 0 for col in label_cols}\n            \n        pred['StudyInstanceUID'] = row['StudyInstanceUID']\n        buffer.append(pred)\n\n    processed_count = min(start_idx + BATCH_SIZE, total_remaining)\n    print(f\"Progress: [{processed_count}/{total_remaining}] studies | Total on Disk: {len(done_df) + len(buffer)}\")\n\n    # Flush batches periodically\n    if len(buffer) >= save_interval:\n        batch_df = pd.DataFrame(buffer)\n        done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n        done_df.to_csv(CHECKPOINT_FILE, index=False)\n        buffer = []\n        gc.collect()\n        torch.cuda.empty_cache()\n\n# Final save\nif buffer:\n    batch_df = pd.DataFrame(buffer)\n    done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(CHECKPOINT_FILE, index=False)\n\nprint(f\"\\nAll pseudo-labels generated and saved! Total: {len(done_df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.514846Z","iopub.status.idle":"2026-08-30T07:37:09.515247Z","shell.execute_reply.started":"2026-08-30T07:37:09.515052Z","shell.execute_reply":"2026-08-30T07:37:09.515078Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport json\nimport torch\nimport pandas as pd\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 1. Load Data & Existing Checkpoint\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"Resuming: {len(done_ids)} studies already on disk.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n    print(\"Starting fresh checkpoint.\")\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Total reports left to process: {len(remaining_df)}\")\n\n# 2. Load Model across GPUs\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\nif 'local_model' not in globals():\n    print(\"Loading model across GPUs...\")\n    tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\n    local_model = AutoModelForCausalLM.from_pretrained(\n        MODEL_ID,\n        torch_dtype=torch.float16,\n        device_map=\"balanced\",\n        low_cpu_mem_usage=True\n    )\n    print(\"Model ready!\")\n\n# 3. Process with Immediate Disk Commits (Every 5 rows)\nsave_every = 5\nbuffer = []\n\nfor i, (idx, row) in enumerate(remaining_df.iterrows()):\n    report_text = row['Report']\n    \n    chat = [\n        {\n            \"role\": \"system\",\n            \"content\": (\n                \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly \"\n                \"a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, \"\n                \"Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. \"\n                \"Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"\n            )\n        },\n        {\"role\": \"user\", \"content\": f\"Report:\\n{report_text}\"}\n    ]\n    \n    prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\", truncation=True, max_length=768).to(local_model.device)\n    \n    with torch.inference_mode():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    generated_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)\n    \n    try:\n        clean_text = generated_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n        start, end = clean_text.find(\"{\"), clean_text.rfind(\"}\") + 1\n        pred = json.loads(clean_text[start:end])\n    except Exception:\n        pred = {col: 0 for col in label_cols}\n        \n    pred['StudyInstanceUID'] = row['StudyInstanceUID']\n    buffer.append(pred)\n    print(f\"Row {i} (Saved: {len(done_df) + len(buffer)}): OK\")\n    \n    # Save every 5 records to disk\n    if (i + 1) % save_every == 0:\n        batch_df = pd.DataFrame(buffer)\n        done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n        done_df.to_csv(CHECKPOINT_FILE, index=False)\n        buffer = []\n        gc.collect()\n        torch.cuda.empty_cache()\n\n# Flush any trailing rows\nif buffer:\n    batch_df = pd.DataFrame(buffer)\n    done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(CHECKPOINT_FILE, index=False)\n\nprint(f\"Extraction complete! Total saved: {len(done_df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.516036Z","iopub.status.idle":"2026-08-30T07:37:09.516415Z","shell.execute_reply.started":"2026-08-30T07:37:09.516228Z","shell.execute_reply":"2026-08-30T07:37:09.516251Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport json\nimport torch\nimport pandas as pd\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 1. Load Data & Existing Checkpoints\ntrain = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\nif os.path.exists(CHECKPOINT_FILE):\n    done_df = pd.read_csv(CHECKPOINT_FILE)\n    done_ids = set(done_df['StudyInstanceUID'].dropna())\n    print(f\"Resuming: {len(done_ids)} studies already processed on disk.\")\nelse:\n    done_df = pd.DataFrame(columns=['StudyInstanceUID'] + label_cols)\n    done_ids = set()\n\nremaining_df = train[~train['StudyInstanceUID'].isin(done_ids)].reset_index(drop=True)\nprint(f\"Remaining reports to batch-process: {len(remaining_df)}\")\n\n# 2. Configure Tokenizer for Batched Left-Padding\nMODEL_ID = \"Qwen/Qwen2.5-7B-Instruct\"\nif 'local_model' not in globals():\n    print(\"Loading model across GPUs...\")\n    tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\n    local_model = AutoModelForCausalLM.from_pretrained(\n        MODEL_ID,\n        torch_dtype=torch.float16,\n        device_map=\"balanced\",\n        low_cpu_mem_usage=True\n    )\n    print(\"Model ready!\")\n\ntokenizer.padding_side = \"left\"\nif tokenizer.pad_token_id is None:\n    tokenizer.pad_token_id = tokenizer.eos_token_id\n\n# 3. High-Speed Batched Generation Loop (Batch Size = 8)\nBATCH_SIZE = 8\nsave_interval = 40  # Writes to disk every 5 batches\n\nsystem_prompt = (\n    \"You are an expert musculoskeletal radiologist. Read the knee MRI report and output strictly \"\n    \"a JSON object with these 12 keys (0 or 1): ACL, MCL, Medial Meniscus, Lateral Meniscus, \"\n    \"Medial OA, Lateral OA, PF OA, Effusion, Synovitis, Baker's, Contusion, Fracture. \"\n    \"Only score 1 for clinically significant findings. Exclude trace/minimal fluid and non-traumatic/osteochondral fractures.\"\n)\n\nbuffer = []\ntotal_remaining = len(remaining_df)\n\nfor start_idx in range(0, total_remaining, BATCH_SIZE):\n    batch_rows = remaining_df.iloc[start_idx : start_idx + BATCH_SIZE]\n    \n    batch_prompts = []\n    for _, row in batch_rows.iterrows():\n        chat = [\n            {\"role\": \"system\", \"content\": system_prompt},\n            {\"role\": \"user\", \"content\": f\"Report:\\n{row['Report']}\"}\n        ]\n        prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n        batch_prompts.append(prompt)\n    \n    # Tokenize full batch together\n    inputs = tokenizer(batch_prompts, return_tensors=\"pt\", padding=True, truncation=True, max_length=768).to(local_model.device)\n    input_len = inputs.input_ids.shape[1]\n    \n    with torch.inference_mode():\n        outputs = local_model.generate(\n            **inputs,\n            max_new_tokens=128,\n            do_sample=False,\n            pad_token_id=tokenizer.pad_token_id\n        )\n    \n    # Parse generated outputs in batch\n    for r_idx, (_, row) in enumerate(batch_rows.iterrows()):\n        gen_tokens = outputs[r_idx][input_len:]\n        gen_text = tokenizer.decode(gen_tokens, skip_special_tokens=True)\n        \n        try:\n            clean = gen_text.replace(\"```json\", \"\").replace(\"```\", \"\").strip()\n            s, e = clean.find(\"{\"), clean.rfind(\"}\") + 1\n            pred = json.loads(clean[s:e])\n        except Exception:\n            pred = {col: 0 for col in label_cols}\n            \n        pred['StudyInstanceUID'] = row['StudyInstanceUID']\n        buffer.append(pred)\n\n    processed_count = min(start_idx + BATCH_SIZE, total_remaining)\n    print(f\"Progress: [{processed_count}/{total_remaining}] studies | Total on Disk: {len(done_df) + len(buffer)}\")\n\n    # Flush batches periodically\n    if len(buffer) >= save_interval:\n        batch_df = pd.DataFrame(buffer)\n        done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n        done_df.to_csv(CHECKPOINT_FILE, index=False)\n        buffer = []\n        gc.collect()\n        torch.cuda.empty_cache()\n\n# Final save\nif buffer:\n    batch_df = pd.DataFrame(buffer)\n    done_df = pd.concat([done_df, batch_df], ignore_index=True).drop_duplicates(subset=['StudyInstanceUID'])\n    done_df.to_csv(CHECKPOINT_FILE, index=False)\n\nprint(f\"\\nAll pseudo-labels generated and saved! Total: {len(done_df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.517537Z","iopub.status.idle":"2026-08-30T07:37:09.517933Z","shell.execute_reply.started":"2026-08-30T07:37:09.517737Z","shell.execute_reply":"2026-08-30T07:37:09.517764Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nfrom sklearn.metrics import roc_auc_score, f1_score\nfrom iterstrat.ml_stratifiers import MultilabelStratifiedKFold\n\n# 1. Load Generated Pseudo-Labels\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\ntrain_df = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\npseudo_df = pd.read_csv(CHECKPOINT_FILE).drop_duplicates(subset=['StudyInstanceUID'])\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\nfor col in label_cols:\n    pseudo_df[col] = pd.to_numeric(pseudo_df[col], errors='coerce').fillna(0).astype(int)\n\n# 2. Benchmark Against 58 Gold-Standard Oracle Studies\nhas_gold = train_df[label_cols].notnull().all(axis=1) & (train_df[label_cols].isin([0, 1]).all(axis=1))\ngold_df = train_df[has_gold].copy()\n\nif len(gold_df) > 0:\n    merged_oracle = pd.merge(gold_df[['StudyInstanceUID'] + label_cols], \n                             pseudo_df[['StudyInstanceUID'] + label_cols], \n                             on='StudyInstanceUID', \n                             suffixes=('_gold', '_pred'))\n    \n    metrics = []\n    for col in label_cols:\n        y_t, y_p = merged_oracle[f'{col}_gold'].values, merged_oracle[f'{col}_pred'].values\n        auc = roc_auc_score(y_t, y_p) if len(np.unique(y_t)) > 1 else 1.0\n        f1 = f1_score(y_t, y_p, zero_division=0)\n        metrics.append({'Pathology': col, 'ROC-AUC': round(auc, 4), 'F1': round(f1, 4), 'Gold Pos': int(y_t.sum()), 'Pred Pos': int(y_p.sum())})\n    \n    display(pd.DataFrame(metrics))\n    print(f\"--> Macro Oracle ROC-AUC: {pd.DataFrame(metrics)['ROC-AUC'].mean():.4f}\")\n\n# 3. Create 5-Fold Multilabel Stratified Split\nmerged_master = pd.merge(train_df[['StudyInstanceUID']], pseudo_df, on='StudyInstanceUID', how='left')\nmerged_master[label_cols] = merged_master[label_cols].fillna(0).astype(int)\n\nmskf = MultilabelStratifiedKFold(n_splits=5, shuffle=True, random_state=42)\nmerged_master['fold'] = -1\nfor fold, (train_idx, val_idx) in enumerate(mskf.split(merged_master, merged_master[label_cols])):\n    merged_master.loc[val_idx, 'fold'] = fold\n\nmerged_master.to_csv('master_train_folds.csv', index=False)\nprint(\"Saved 'master_train_folds.csv' with 5 balanced cross-validation folds.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.519363Z","iopub.status.idle":"2026-08-30T07:37:09.519614Z","shell.execute_reply.started":"2026-08-30T07:37:09.519492Z","shell.execute_reply":"2026-08-30T07:37:09.519508Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Install iterative-stratification for balanced multi-label splitting\n!pip install -q iterative-stratification\n\nimport os\nimport pandas as pd\nimport numpy as np\nfrom sklearn.metrics import roc_auc_score, f1_score\nfrom iterstrat.ml_stratifiers import MultilabelStratifiedKFold\n\n# 1. Load Generated Pseudo-Labels & Metadata\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\ntrain_df = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\npseudo_df = pd.read_csv(CHECKPOINT_FILE).drop_duplicates(subset=['StudyInstanceUID'])\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\nfor col in label_cols:\n    pseudo_df[col] = pd.to_numeric(pseudo_df[col], errors='coerce').fillna(0).astype(int)\n\n# 2. Benchmark Against 58 Gold-Standard Oracle Studies\nhas_gold = [c for c in label_cols if c in train_df.columns]\nif len(has_gold) == 12:\n    gold_mask = train_df[label_cols].notnull().all(axis=1) & (train_df[label_cols].isin([0, 1]).all(axis=1))\n    gold_df = train_df[gold_mask].copy()\nelse:\n    gold_df = pd.DataFrame()\n\nif len(gold_df) > 0:\n    merged_oracle = pd.merge(\n        gold_df[['StudyInstanceUID'] + label_cols], \n        pseudo_df[['StudyInstanceUID'] + label_cols], \n        on='StudyInstanceUID', \n        suffixes=('_gold', '_pred')\n    )\n    \n    metrics = []\n    for col in label_cols:\n        y_t = merged_oracle[f'{col}_gold'].values\n        y_p = merged_oracle[f'{col}_pred'].values\n        auc = roc_auc_score(y_t, y_p) if len(np.unique(y_t)) > 1 else 1.0\n        f1 = f1_score(y_t, y_p, zero_division=0)\n        metrics.append({\n            'Pathology': col,\n            'ROC-AUC': round(auc, 4),\n            'F1': round(f1, 4),\n            'Gold Positives': int(y_t.sum()),\n            'Pred Positives': int(y_p.sum())\n        })\n    \n    oracle_summary = pd.DataFrame(metrics)\n    display(oracle_summary)\n    print(f\"--> Macro Oracle ROC-AUC: {oracle_summary['ROC-AUC'].mean():.4f}\")\nelse:\n    print(\"Gold standard annotations processed.\")\n\n# 3. Create 5-Fold Multilabel Stratified Split\nmerged_master = pd.merge(train_df[['StudyInstanceUID']], pseudo_df, on='StudyInstanceUID', how='left')\nmerged_master[label_cols] = merged_master[label_cols].fillna(0).astype(int)\n\nmskf = MultilabelStratifiedKFold(n_splits=5, shuffle=True, random_state=42)\nmerged_master['fold'] = -1\nfor fold, (train_idx, val_idx) in enumerate(mskf.split(merged_master, merged_master[label_cols])):\n    merged_master.loc[val_idx, 'fold'] = fold\n\nmerged_master.to_csv('master_train_folds.csv', index=False)\nprint(f\"Saved 'master_train_folds.csv' with {len(merged_master)} studies across 5 balanced folds.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.520554Z","iopub.status.idle":"2026-08-30T07:37:09.520944Z","shell.execute_reply.started":"2026-08-30T07:37:09.520666Z","shell.execute_reply":"2026-08-30T07:37:09.52068Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport math\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.cuda.amp import GradScaler, autocast\nimport timm\n\n# 1. Geometry-Aware 2.5D Volumetric Dataset\nclass GeometryAwareKneeDataset(Dataset):\n    def __init__(self, df, dicom_dir, target_size=(256, 256), max_slices=24, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.dicom_dir = dicom_dir\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = [\n            'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n            'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n            'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n        ]\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_study_slices(self, study_path):\n        dcm_files = glob.glob(os.path.join(study_path, '**', '*.dcm'), recursive=True)\n        if not dcm_files:\n            return None\n        slice_data = []\n        for f in dcm_files:\n            try:\n                ds = pydicom.dcmread(f, stop_before_pixels=False)\n                pos_z = getattr(ds, 'ImagePositionPatient', [0, 0, 0])[2] if hasattr(ds, 'ImagePositionPatient') else getattr(ds, 'InstanceNumber', 0)\n                slice_data.append((pos_z, ds))\n            except Exception:\n                continue\n        if not slice_data:\n            return None\n        slice_data.sort(key=lambda x: float(x[0]))\n        return [item[1] for item in slice_data]\n\n    def _process_slice(self, ds):\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = arr * getattr(ds, 'RescaleSlope', 1.0) + getattr(ds, 'RescaleIntercept', 0.0)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        if p99 - p1 > 0:\n            arr = np.clip(arr, p1, p99)\n            arr = (arr - p1) / (p99 - p1)\n        else:\n            arr = np.zeros_like(arr)\n        return arr\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_path = os.path.join(self.dicom_dir, str(row['StudyInstanceUID']))\n        slices = self._load_study_slices(study_path)\n        \n        if slices is None or len(slices) == 0:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            processed = [self._process_slice(s) for s in slices]\n            num_raw = len(processed)\n            indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int) if num_raw >= self.max_slices else np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            sampled = [processed[i] for i in indices]\n            \n            stacked = []\n            for i in range(len(sampled)):\n                p = sampled[max(0, i - 1)]\n                c = sampled[i]\n                n = sampled[min(len(sampled) - 1, i + 1)]\n                s3 = torch.tensor(np.stack([p, c, n], axis=0), dtype=torch.float32).unsqueeze(0)\n                resized = F.interpolate(s3, size=self.target_size, mode='bilinear', align_corners=False)\n                stacked.append(resized.squeeze(0))\n            vol = torch.stack(stacked, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        raw_labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        # Apply label smoothing (0.05) to absorb LLM pseudo-label noise\n        if self.is_train:\n            labels = raw_labels * 0.90 + 0.05\n        else:\n            labels = raw_labels\n            \n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 2. Loss & Architecture\nclass AsymmetricLoss(nn.Module):\n    def __init__(self, gamma_neg=4, gamma_pos=1, clip=0.05, eps=1e-8):\n        super().__init__()\n        self.gamma_neg, self.gamma_pos, self.clip, self.eps = gamma_neg, gamma_pos, clip, eps\n\n    def forward(self, x, y):\n        xs_pos = torch.sigmoid(x)\n        xs_neg = (1.0 - xs_pos + self.clip).clamp(max=1.0) if self.clip > 0 else 1.0 - xs_pos\n        los_pos = y * torch.log(xs_pos.clamp(min=self.eps)) * ((1 - xs_pos) ** self.gamma_pos)\n        los_neg = (1 - y) * torch.log(xs_neg.clamp(min=self.eps)) * (xs_pos ** self.gamma_neg)\n        return -(los_pos + los_neg).sum(dim=-1).mean()\n\nclass GaussianSpatialGuidance(nn.Module):\n    def __init__(self, num_classes=12):\n        super().__init__()\n        self.mu = nn.Parameter(torch.linspace(0.25, 0.75, num_classes).unsqueeze(0))\n        self.log_sigma = nn.Parameter(torch.ones(1, num_classes) * math.log(0.18))\n\n    def forward(self, S, device):\n        z = torch.linspace(0.0, 1.0, S, device=device).unsqueeze(1)\n        sigma = torch.exp(self.log_sigma).to(device)\n        return torch.exp(-((z - self.mu.to(device)) ** 2) / (2 * (sigma ** 2) + 1e-6))\n\nclass TargetSpecificSpatialMIL(nn.Module):\n    def __init__(self, backbone='convnext_small', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        embed_dim = self.encoder.num_features\n        self.spatial_guidance = GaussianSpatialGuidance(num_classes)\n        self.attention_nets = nn.ModuleList([\n            nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh(), nn.Linear(128, 1)) for _ in range(num_classes)\n        ])\n        self.classifiers = nn.ModuleList([\n            nn.Sequential(nn.Linear(embed_dim, 128), nn.SiLU(), nn.Dropout(0.3), nn.Linear(128, 1)) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        spatial_prior = self.spatial_guidance(S, x.device)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            raw_attn = self.attention_nets[c](feats).squeeze(-1)\n            guided_attn = raw_attn + torch.log(spatial_prior[:, c].unsqueeze(0) + 1e-6)\n            attn_weights = F.softmax(guided_attn, dim=-1).unsqueeze(-1)\n            bag_feat = torch.sum(feats * attn_weights, dim=1)\n            logits_list.append(self.classifiers[c](bag_feat))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 3. Execution Pipeline\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train'\ntrain_df = pd.read_csv('master_train_folds.csv')\nFOLD = 0\n\ntrain_set = GeometryAwareKneeDataset(train_df[train_df['fold'] != FOLD], DICOM_ROOT, max_slices=24, is_train=True)\nval_set = GeometryAwareKneeDataset(train_df[train_df['fold'] == FOLD], DICOM_ROOT, max_slices=24, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=2, pin_memory=True)\n\nmodel = TargetSpecificSpatialMIL(backbone='convnext_small', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = AsymmetricLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler()\n\nbest_auc = 0.0\nprint(f\"Starting Training: Fold {FOLD} | Device: {DEVICE}\")\n\nfor epoch in range(8):\n    model.train()\n    total_loss = 0.0\n    for step, (images, targets) in enumerate(train_loader):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        optimizer.zero_grad()\n        with autocast():\n            logits = model(images)\n            loss = criterion(logits, targets)\n        scaler.scale(loss).backward()\n        scaler.unscale_(optimizer)\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n        scaler.step(optimizer)\n        scaler.update()\n        total_loss += loss.item()\n    \n    scheduler.step()\n    \n    # Validation\n    model.eval()\n    val_preds, val_targets = [], []\n    with torch.no_grad():\n        for images, targets in val_loader:\n            images = images.to(DEVICE)\n            with autocast():\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"Epoch [{epoch+1}/8] - Train Loss: {total_loss/len(train_loader):.4f} - Val AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_spatial_mil_fold{FOLD}.pth')\n        print(f\"--> Saved Model Checkpoint (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Training Complete. Peak AUC: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.52233Z","iopub.status.idle":"2026-08-30T07:37:09.522681Z","shell.execute_reply.started":"2026-08-30T07:37:09.522491Z","shell.execute_reply":"2026-08-30T07:37:09.522508Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport math\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.cuda.amp import GradScaler, autocast\nimport timm\n\n# 1. Clean lingering VRAM allocations\ngc.collect()\ntorch.cuda.empty_cache()\n\n# 2. Geometry-Aware 2.5D Volumetric Dataset\nclass GeometryAwareKneeDataset(Dataset):\n    def __init__(self, df, dicom_dir, target_size=(256, 256), max_slices=20, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.dicom_dir = dicom_dir\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = [\n            'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n            'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n            'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n        ]\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_study_slices(self, study_path):\n        dcm_files = glob.glob(os.path.join(study_path, '**', '*.dcm'), recursive=True)\n        if not dcm_files:\n            return None\n        slice_data = []\n        for f in dcm_files:\n            try:\n                ds = pydicom.dcmread(f, stop_before_pixels=False)\n                pos_z = getattr(ds, 'ImagePositionPatient', [0, 0, 0])[2] if hasattr(ds, 'ImagePositionPatient') else getattr(ds, 'InstanceNumber', 0)\n                slice_data.append((pos_z, ds))\n            except Exception:\n                continue\n        if not slice_data:\n            return None\n        slice_data.sort(key=lambda x: float(x[0]))\n        return [item[1] for item in slice_data]\n\n    def _process_slice(self, ds):\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = arr * getattr(ds, 'RescaleSlope', 1.0) + getattr(ds, 'RescaleIntercept', 0.0)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        if p99 - p1 > 0:\n            arr = np.clip(arr, p1, p99)\n            arr = (arr - p1) / (p99 - p1)\n        else:\n            arr = np.zeros_like(arr)\n        return arr\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_path = os.path.join(self.dicom_dir, str(row['StudyInstanceUID']))\n        slices = self._load_study_slices(study_path)\n        \n        if slices is None or len(slices) == 0:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            processed = [self._process_slice(s) for s in slices]\n            num_raw = len(processed)\n            indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int) if num_raw >= self.max_slices else np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            sampled = [processed[i] for i in indices]\n            \n            stacked = []\n            for i in range(len(sampled)):\n                p = sampled[max(0, i - 1)]\n                c = sampled[i]\n                n = sampled[min(len(sampled) - 1, i + 1)]\n                s3 = torch.tensor(np.stack([p, c, n], axis=0), dtype=torch.float32).unsqueeze(0)\n                resized = F.interpolate(s3, size=self.target_size, mode='bilinear', align_corners=False)\n                stacked.append(resized.squeeze(0))\n            vol = torch.stack(stacked, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        raw_labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        labels = raw_labels * 0.90 + 0.05 if self.is_train else raw_labels\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 3. Loss & Memory-Efficient Architecture\nclass AsymmetricLoss(nn.Module):\n    def __init__(self, gamma_neg=4, gamma_pos=1, clip=0.05, eps=1e-8):\n        super().__init__()\n        self.gamma_neg, self.gamma_pos, self.clip, self.eps = gamma_neg, gamma_pos, clip, eps\n\n    def forward(self, x, y):\n        xs_pos = torch.sigmoid(x)\n        xs_neg = (1.0 - xs_pos + self.clip).clamp(max=1.0) if self.clip > 0 else 1.0 - xs_pos\n        los_pos = y * torch.log(xs_pos.clamp(min=self.eps)) * ((1 - xs_pos) ** self.gamma_pos)\n        los_neg = (1 - y) * torch.log(xs_neg.clamp(min=self.eps)) * (xs_pos ** self.gamma_neg)\n        return -(los_pos + los_neg).sum(dim=-1).mean()\n\nclass GaussianSpatialGuidance(nn.Module):\n    def __init__(self, num_classes=12):\n        super().__init__()\n        self.mu = nn.Parameter(torch.linspace(0.25, 0.75, num_classes).unsqueeze(0))\n        self.log_sigma = nn.Parameter(torch.ones(1, num_classes) * math.log(0.18))\n\n    def forward(self, S, device):\n        z = torch.linspace(0.0, 1.0, S, device=device).unsqueeze(1)\n        sigma = torch.exp(self.log_sigma).to(device)\n        return torch.exp(-((z - self.mu.to(device)) ** 2) / (2 * (sigma ** 2) + 1e-6))\n\nclass TargetSpecificSpatialMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(\n            backbone, \n            pretrained=pretrained, \n            num_classes=0, \n            in_chans=3, \n            drop_rate=0.2,\n            grad_checkpointing=True  # Slashes backbone memory by ~60%\n        )\n        embed_dim = self.encoder.num_features\n        self.spatial_guidance = GaussianSpatialGuidance(num_classes)\n        self.attention_nets = nn.ModuleList([\n            nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh(), nn.Linear(128, 1)) for _ in range(num_classes)\n        ])\n        self.classifiers = nn.ModuleList([\n            nn.Sequential(nn.Linear(embed_dim, 128), nn.SiLU(), nn.Dropout(0.3), nn.Linear(128, 1)) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        spatial_prior = self.spatial_guidance(S, x.device)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            raw_attn = self.attention_nets[c](feats).squeeze(-1)\n            guided_attn = raw_attn + torch.log(spatial_prior[:, c].unsqueeze(0) + 1e-6)\n            attn_weights = F.softmax(guided_attn, dim=-1).unsqueeze(-1)\n            bag_feat = torch.sum(feats * attn_weights, dim=1)\n            logits_list.append(self.classifiers[c](bag_feat))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 4. Training Pipeline\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train'\ntrain_df = pd.read_csv('master_train_folds.csv')\nFOLD = 0\n\ntrain_set = GeometryAwareKneeDataset(train_df[train_df['fold'] != FOLD], DICOM_ROOT, max_slices=20, is_train=True)\nval_set = GeometryAwareKneeDataset(train_df[train_df['fold'] == FOLD], DICOM_ROOT, max_slices=20, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=2, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_set, batch_size=2, shuffle=False, num_workers=2, pin_memory=True)\n\nmodel = TargetSpecificSpatialMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = AsymmetricLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler()\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\nprint(f\"Starting Training: Fold {FOLD} | Peak VRAM Footprint < 6 GB\")\n\nfor epoch in range(8):\n    model.train()\n    total_loss = 0.0\n    optimizer.zero_grad()\n    \n    for step, (images, targets) in enumerate(train_loader):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast():\n            logits = model(images)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        total_loss += loss.item() * ACCUMULATION_STEPS\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    with torch.no_grad():\n        for images, targets in val_loader:\n            images = images.to(DEVICE)\n            with autocast():\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"Epoch [{epoch+1}/8] - Train Loss: {total_loss/len(train_loader):.4f} - Val AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_spatial_mil_fold{FOLD}.pth')\n        print(f\"--> Saved Best Model Checkpoint (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Finished. Best Val AUC: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.524507Z","iopub.status.idle":"2026-08-30T07:37:09.524942Z","shell.execute_reply.started":"2026-08-30T07:37:09.524697Z","shell.execute_reply":"2026-08-30T07:37:09.524743Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Modern PyTorch Amp Imports & Initialization\nfrom torch.amp import GradScaler, autocast\n\nscaler = GradScaler('cuda')\n\n# Inside the training/validation steps:\nwith autocast(device_type='cuda', dtype=torch.float16):\n    logits = model(images)\n    loss = criterion(logits, targets)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.525709Z","iopub.status.idle":"2026-08-30T07:37:09.526071Z","shell.execute_reply.started":"2026-08-30T07:37:09.525886Z","shell.execute_reply":"2026-08-30T07:37:09.525909Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport math\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.cuda.amp import GradScaler, autocast\nimport timm\n\n# 1. Clean lingering VRAM allocations\ngc.collect()\ntorch.cuda.empty_cache()\n\n# 2. Geometry-Aware 2.5D Volumetric Dataset\nclass GeometryAwareKneeDataset(Dataset):\n    def __init__(self, df, dicom_dir, target_size=(256, 256), max_slices=20, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.dicom_dir = dicom_dir\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = [\n            'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n            'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n            'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n        ]\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_study_slices(self, study_path):\n        dcm_files = glob.glob(os.path.join(study_path, '**', '*.dcm'), recursive=True)\n        if not dcm_files:\n            return None\n        slice_data = []\n        for f in dcm_files:\n            try:\n                ds = pydicom.dcmread(f, stop_before_pixels=False)\n                pos_z = getattr(ds, 'ImagePositionPatient', [0, 0, 0])[2] if hasattr(ds, 'ImagePositionPatient') else getattr(ds, 'InstanceNumber', 0)\n                slice_data.append((pos_z, ds))\n            except Exception:\n                continue\n        if not slice_data:\n            return None\n        slice_data.sort(key=lambda x: float(x[0]))\n        return [item[1] for item in slice_data]\n\n    def _process_slice(self, ds):\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = arr * getattr(ds, 'RescaleSlope', 1.0) + getattr(ds, 'RescaleIntercept', 0.0)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        if p99 - p1 > 0:\n            arr = np.clip(arr, p1, p99)\n            arr = (arr - p1) / (p99 - p1)\n        else:\n            arr = np.zeros_like(arr)\n        return arr\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_path = os.path.join(self.dicom_dir, str(row['StudyInstanceUID']))\n        slices = self._load_study_slices(study_path)\n        \n        if slices is None or len(slices) == 0:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            processed = [self._process_slice(s) for s in slices]\n            num_raw = len(processed)\n            indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int) if num_raw >= self.max_slices else np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            sampled = [processed[i] for i in indices]\n            \n            stacked = []\n            for i in range(len(sampled)):\n                p = sampled[max(0, i - 1)]\n                c = sampled[i]\n                n = sampled[min(len(sampled) - 1, i + 1)]\n                s3 = torch.tensor(np.stack([p, c, n], axis=0), dtype=torch.float32).unsqueeze(0)\n                resized = F.interpolate(s3, size=self.target_size, mode='bilinear', align_corners=False)\n                stacked.append(resized.squeeze(0))\n            vol = torch.stack(stacked, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        raw_labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        labels = raw_labels * 0.90 + 0.05 if self.is_train else raw_labels\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 3. Loss & Architecture\nclass AsymmetricLoss(nn.Module):\n    def __init__(self, gamma_neg=4, gamma_pos=1, clip=0.05, eps=1e-8):\n        super().__init__()\n        self.gamma_neg, self.gamma_pos, self.clip, self.eps = gamma_neg, gamma_pos, clip, eps\n\n    def forward(self, x, y):\n        xs_pos = torch.sigmoid(x)\n        xs_neg = (1.0 - xs_pos + self.clip).clamp(max=1.0) if self.clip > 0 else 1.0 - xs_pos\n        los_pos = y * torch.log(xs_pos.clamp(min=self.eps)) * ((1 - xs_pos) ** self.gamma_pos)\n        los_neg = (1 - y) * torch.log(xs_neg.clamp(min=self.eps)) * (xs_pos ** self.gamma_neg)\n        return -(los_pos + los_neg).sum(dim=-1).mean()\n\nclass GaussianSpatialGuidance(nn.Module):\n    def __init__(self, num_classes=12):\n        super().__init__()\n        self.mu = nn.Parameter(torch.linspace(0.25, 0.75, num_classes).unsqueeze(0))\n        self.log_sigma = nn.Parameter(torch.ones(1, num_classes) * math.log(0.18))\n\n    def forward(self, S, device):\n        z = torch.linspace(0.0, 1.0, S, device=device).unsqueeze(1)\n        sigma = torch.exp(self.log_sigma).to(device)\n        return torch.exp(-((z - self.mu.to(device)) ** 2) / (2 * (sigma ** 2) + 1e-6))\n\nclass TargetSpecificSpatialMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(\n            backbone, \n            pretrained=pretrained, \n            num_classes=0, \n            in_chans=3, \n            drop_rate=0.2\n        )\n        # Enable gradient checkpointing on the instantiated backbone\n        if hasattr(self.encoder, 'set_grad_checkpointing'):\n            self.encoder.set_grad_checkpointing(True)\n            \n        embed_dim = self.encoder.num_features\n        self.spatial_guidance = GaussianSpatialGuidance(num_classes)\n        self.attention_nets = nn.ModuleList([\n            nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh(), nn.Linear(128, 1)) for _ in range(num_classes)\n        ])\n        self.classifiers = nn.ModuleList([\n            nn.Sequential(nn.Linear(embed_dim, 128), nn.SiLU(), nn.Dropout(0.3), nn.Linear(128, 1)) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        spatial_prior = self.spatial_guidance(S, x.device)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            raw_attn = self.attention_nets[c](feats).squeeze(-1)\n            guided_attn = raw_attn + torch.log(spatial_prior[:, c].unsqueeze(0) + 1e-6)\n            attn_weights = F.softmax(guided_attn, dim=-1).unsqueeze(-1)\n            bag_feat = torch.sum(feats * attn_weights, dim=1)\n            logits_list.append(self.classifiers[c](bag_feat))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 4. Training Pipeline\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train'\ntrain_df = pd.read_csv('master_train_folds.csv')\nFOLD = 0\n\ntrain_set = GeometryAwareKneeDataset(train_df[train_df['fold'] != FOLD], DICOM_ROOT, max_slices=20, is_train=True)\nval_set = GeometryAwareKneeDataset(train_df[train_df['fold'] == FOLD], DICOM_ROOT, max_slices=20, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=2, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_set, batch_size=2, shuffle=False, num_workers=2, pin_memory=True)\n\nmodel = TargetSpecificSpatialMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = AsymmetricLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler()\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\nprint(f\"Starting Training: Fold {FOLD} | Peak VRAM Footprint < 6 GB\")\n\nfor epoch in range(8):\n    model.train()\n    total_loss = 0.0\n    optimizer.zero_grad()\n    \n    for step, (images, targets) in enumerate(train_loader):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast():\n            logits = model(images)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        total_loss += loss.item() * ACCUMULATION_STEPS\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    with torch.no_grad():\n        for images, targets in val_loader:\n            images = images.to(DEVICE)\n            with autocast():\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"Epoch [{epoch+1}/8] - Train Loss: {total_loss/len(train_loader):.4f} - Val AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_spatial_mil_fold{FOLD}.pth')\n        print(f\"--> Saved Best Model Checkpoint (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Finished. Best Val AUC: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.527195Z","iopub.status.idle":"2026-08-30T07:37:09.52754Z","shell.execute_reply.started":"2026-08-30T07:37:09.527361Z","shell.execute_reply":"2026-08-30T07:37:09.527383Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Modern PyTorch Amp Imports & Initialization\nfrom torch.amp import GradScaler, autocast\n\nscaler = GradScaler('cuda')\n\n# Inside the training/validation steps:\nwith autocast(device_type='cuda', dtype=torch.float16):\n    logits = model(images)\n    loss = criterion(logits, targets)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.52935Z","iopub.status.idle":"2026-08-30T07:37:09.529635Z","shell.execute_reply.started":"2026-08-30T07:37:09.529512Z","shell.execute_reply":"2026-08-30T07:37:09.529529Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport math\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Clear GPU Cache\ngc.collect()\ntorch.cuda.empty_cache()\n\n# 2. Geometry-Aware 2.5D Volumetric Dataset\nclass GeometryAwareKneeDataset(Dataset):\n    def __init__(self, df, dicom_dir, target_size=(256, 256), max_slices=16, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.dicom_dir = dicom_dir\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = [\n            'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n            'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n            'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n        ]\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_study_slices(self, study_path):\n        dcm_files = glob.glob(os.path.join(study_path, '**', '*.dcm'), recursive=True)\n        if not dcm_files:\n            return None\n        slice_data = []\n        for f in dcm_files:\n            try:\n                ds = pydicom.dcmread(f, stop_before_pixels=False)\n                pos_z = getattr(ds, 'ImagePositionPatient', [0, 0, 0])[2] if hasattr(ds, 'ImagePositionPatient') else getattr(ds, 'InstanceNumber', 0)\n                slice_data.append((pos_z, ds))\n            except Exception:\n                continue\n        if not slice_data:\n            return None\n        slice_data.sort(key=lambda x: float(x[0]))\n        return [item[1] for item in slice_data]\n\n    def _process_slice(self, ds):\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = arr * getattr(ds, 'RescaleSlope', 1.0) + getattr(ds, 'RescaleIntercept', 0.0)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        if p99 - p1 > 0:\n            arr = np.clip(arr, p1, p99)\n            arr = (arr - p1) / (p99 - p1)\n        else:\n            arr = np.zeros_like(arr)\n        return arr\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_path = os.path.join(self.dicom_dir, str(row['StudyInstanceUID']))\n        slices = self._load_study_slices(study_path)\n        \n        if slices is None or len(slices) == 0:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            processed = [self._process_slice(s) for s in slices]\n            num_raw = len(processed)\n            indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int) if num_raw >= self.max_slices else np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            sampled = [processed[i] for i in indices]\n            \n            stacked = []\n            for i in range(len(sampled)):\n                p = sampled[max(0, i - 1)]\n                c = sampled[i]\n                n = sampled[min(len(sampled) - 1, i + 1)]\n                s3 = torch.tensor(np.stack([p, c, n], axis=0), dtype=torch.float32).unsqueeze(0)\n                resized = F.interpolate(s3, size=self.target_size, mode='bilinear', align_corners=False)\n                stacked.append(resized.squeeze(0))\n            vol = torch.stack(stacked, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        raw_labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        labels = raw_labels * 0.90 + 0.05 if self.is_train else raw_labels\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 3. Model Architecture & Loss\nclass AsymmetricLoss(nn.Module):\n    def __init__(self, gamma_neg=4, gamma_pos=1, clip=0.05, eps=1e-8):\n        super().__init__()\n        self.gamma_neg, self.gamma_pos, self.clip, self.eps = gamma_neg, gamma_pos, clip, eps\n\n    def forward(self, x, y):\n        xs_pos = torch.sigmoid(x)\n        xs_neg = (1.0 - xs_pos + self.clip).clamp(max=1.0) if self.clip > 0 else 1.0 - xs_pos\n        los_pos = y * torch.log(xs_pos.clamp(min=self.eps)) * ((1 - xs_pos) ** self.gamma_pos)\n        los_neg = (1 - y) * torch.log(xs_neg.clamp(min=self.eps)) * (xs_pos ** self.gamma_neg)\n        return -(los_pos + los_neg).sum(dim=-1).mean()\n\nclass GaussianSpatialGuidance(nn.Module):\n    def __init__(self, num_classes=12):\n        super().__init__()\n        self.mu = nn.Parameter(torch.linspace(0.25, 0.75, num_classes).unsqueeze(0))\n        self.log_sigma = nn.Parameter(torch.ones(1, num_classes) * math.log(0.18))\n\n    def forward(self, S, device):\n        z = torch.linspace(0.0, 1.0, S, device=device).unsqueeze(1)\n        sigma = torch.exp(self.log_sigma).to(device)\n        return torch.exp(-((z - self.mu.to(device)) ** 2) / (2 * (sigma ** 2) + 1e-6))\n\nclass TargetSpecificSpatialMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        if hasattr(self.encoder, 'set_grad_checkpointing'):\n            self.encoder.set_grad_checkpointing(True)\n            \n        embed_dim = self.encoder.num_features\n        self.spatial_guidance = GaussianSpatialGuidance(num_classes)\n        self.attention_nets = nn.ModuleList([\n            nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh(), nn.Linear(128, 1)) for _ in range(num_classes)\n        ])\n        self.classifiers = nn.ModuleList([\n            nn.Sequential(nn.Linear(embed_dim, 128), nn.SiLU(), nn.Dropout(0.3), nn.Linear(128, 1)) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        spatial_prior = self.spatial_guidance(S, x.device)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            raw_attn = self.attention_nets[c](feats).squeeze(-1)\n            guided_attn = raw_attn + torch.log(spatial_prior[:, c].unsqueeze(0) + 1e-6)\n            attn_weights = F.softmax(guided_attn, dim=-1).unsqueeze(-1)\n            bag_feat = torch.sum(feats * attn_weights, dim=1)\n            logits_list.append(self.classifiers[c](bag_feat))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 4. Training Loop with Live TQDM Progress\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train'\ntrain_df = pd.read_csv('master_train_folds.csv')\nFOLD = 0\n\ntrain_set = GeometryAwareKneeDataset(train_df[train_df['fold'] != FOLD], DICOM_ROOT, max_slices=16, is_train=True)\nval_set = GeometryAwareKneeDataset(train_df[train_df['fold'] == FOLD], DICOM_ROOT, max_slices=16, is_train=False)\n\n# Use num_workers=0 to eliminate IPC multiprocessing deadlocks\ntrain_loader = DataLoader(train_set, batch_size=2, shuffle=True, num_workers=0, pin_memory=True)\nval_loader = DataLoader(val_set, batch_size=2, shuffle=False, num_workers=0, pin_memory=True)\n\nmodel = TargetSpecificSpatialMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = AsymmetricLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"--- Training Fold {FOLD} | Train Samples: {len(train_set)} | Val Samples: {len(val_set)} ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, targets) in enumerate(pbar):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\")\n    \n    with torch.no_grad():\n        for images, targets in val_pbar:\n            images = images.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} Complete | Train Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_spatial_mil_fold{FOLD}.pth')\n        print(f\"  * Saved New Best Weights (Val AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Complete! Best Score: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.530667Z","iopub.status.idle":"2026-08-30T07:37:09.53107Z","shell.execute_reply.started":"2026-08-30T07:37:09.530868Z","shell.execute_reply":"2026-08-30T07:37:09.530884Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport pydicom\nimport pandas as pd\n\ntrain_df = pd.read_csv('master_train_folds.csv')\nsample_uid = str(train_df['StudyInstanceUID'].iloc[0])\n\n# Test possible root directories\npotential_paths = [\n    f'/kaggle/input/rsna-knee-abnormality-detection/train/{sample_uid}',\n    f'/kaggle/input/competitions/rsna-knee-abnormality-detection/train/{sample_uid}',\n    f'/kaggle/input/rsna-knee-abnormality-detection/{sample_uid}'\n]\n\nvalid_path = None\nfor p in potential_paths:\n    if os.path.exists(p):\n        valid_path = p\n        break\n\nprint(f\"Target Study Folder: {valid_path}\")\nif valid_path:\n    # RSNA files often lack .dcm extensions\n    all_files = [os.path.join(dp, f) for dp, _, fn in os.walk(valid_path) for f in fn if not f.endswith('.csv')]\n    print(f\"Files found in study directory: {len(all_files)}\")\n    if all_files:\n        try:\n            dcm = pydicom.dcmread(all_files[0], stop_before_pixels=True)\n            print(f\"Successfully read DICOM header. Modality: {getattr(dcm, 'Modality', 'Unknown')}\")\n        except Exception as e:\n            print(f\"Error reading DICOM: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.532373Z","iopub.status.idle":"2026-08-30T07:37:09.532671Z","shell.execute_reply.started":"2026-08-30T07:37:09.532517Z","shell.execute_reply":"2026-08-30T07:37:09.532531Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport math\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom tqdm.auto import tqdm\nimport timm\n\ngc.collect()\ntorch.cuda.empty_cache()\n\n# 1. Resilient Volumetric Dataset Loader\nclass RobustKneeDataset(Dataset):\n    def __init__(self, df, dicom_root, target_size=(224, 224), max_slices=16, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.dicom_root = dicom_root\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = [\n            'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n            'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n            'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n        ]\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_study(self, study_uid):\n        study_path = os.path.join(self.dicom_root, str(study_uid))\n        if not os.path.exists(study_path):\n            return None\n        \n        # Grab all slice files recursively (with or without .dcm)\n        file_paths = [os.path.join(dp, f) for dp, _, fn in os.walk(study_path) for f in fn if not f.endswith(('.csv', '.txt'))]\n        if not file_paths:\n            return None\n            \n        slices = []\n        for fp in file_paths:\n            try:\n                ds = pydicom.dcmread(fp)\n                z_pos = float(getattr(ds, 'ImagePositionPatient', [0, 0, 0])[2]) if hasattr(ds, 'ImagePositionPatient') else float(getattr(ds, 'InstanceNumber', 0))\n                arr = ds.pixel_array.astype(np.float32)\n                \n                # Invert MONOCHROME1\n                if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                    arr = np.amax(arr) - arr\n                    \n                # Min-Max Normalization\n                p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1 + 1e-6)\n                \n                slices.append((z_pos, arr))\n            except Exception:\n                continue\n                \n        if not slices:\n            return None\n            \n        slices.sort(key=lambda x: x[0])\n        return [s[1] for s in slices]\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        slices_2d = self._load_study(row['StudyInstanceUID'])\n        \n        if slices_2d is None or len(slices_2d) == 0:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            num_raw = len(slices_2d)\n            indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int) if num_raw >= self.max_slices else np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            sampled = [slices_2d[i] for i in indices]\n            \n            stacked_slabs = []\n            for i in range(len(sampled)):\n                p = sampled[max(0, i - 1)]\n                c = sampled[i]\n                n = sampled[min(len(sampled) - 1, i + 1)]\n                slab = torch.tensor(np.stack([p, c, n], axis=0), dtype=torch.float32).unsqueeze(0)\n                resized = F.interpolate(slab, size=self.target_size, mode='bilinear', align_corners=False)\n                stacked_slabs.append(resized.squeeze(0))\n                \n            vol = torch.stack(stacked_slabs, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 2. Stable Gated Attention MIL Network\nclass GatedAttentionMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        embed_dim = self.encoder.num_features\n        \n        # Gated Attention Pooling\n        self.attention_V = nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh())\n        self.attention_U = nn.Sequential(nn.Linear(embed_dim, 128), nn.Sigmoid())\n        self.attention_weights = nn.Linear(128, num_classes)\n        \n        # Classifier Heads\n        self.classifiers = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(embed_dim, 128),\n                nn.SiLU(),\n                nn.Dropout(0.3),\n                nn.Linear(128, 1)\n            ) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1) # (B, S, embed_dim)\n        \n        # Gated Attention Mechanism\n        v = self.attention_V(feats) # (B, S, 128)\n        u = self.attention_U(feats) # (B, S, 128)\n        attn_scores = self.attention_weights(v * u) # (B, S, 12)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            attn_c = F.softmax(attn_scores[:, :, c], dim=1).unsqueeze(-1) # (B, S, 1)\n            bag_c = torch.sum(feats * attn_c, dim=1) # (B, embed_dim)\n            logits_list.append(self.classifiers[c](bag_c))\n            \n        return torch.cat(logits_list, dim=-1) # (B, 12)\n\n# 3. Training Loop\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nDICOM_ROOT = '/kaggle/input/rsna-knee-abnormality-detection/train' if os.path.exists('/kaggle/input/rsna-knee-abnormality-detection/train') else '/kaggle/input/competitions/rsna-knee-abnormality-detection/train'\n\ntrain_df = pd.read_csv('master_train_folds.csv')\nFOLD = 0\n\ntrain_set = RobustKneeDataset(train_df[train_df['fold'] != FOLD], DICOM_ROOT, max_slices=16, is_train=True)\nval_set = RobustKneeDataset(train_df[train_df['fold'] == FOLD], DICOM_ROOT, max_slices=16, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=2, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_set, batch_size=2, shuffle=False, num_workers=2, pin_memory=True)\n\nmodel = GatedAttentionMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"--- Starting Retraining Fold {FOLD} ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, targets) in enumerate(pbar):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\")\n    \n    with torch.no_grad():\n        for images, targets in val_pbar:\n            images = images.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} Complete | Train Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_gated_mil_fold{FOLD}.pth')\n        print(f\"  * Saved Best Model (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Training Complete. Best Score: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.5338Z","iopub.status.idle":"2026-08-30T07:37:09.534034Z","shell.execute_reply.started":"2026-08-30T07:37:09.533918Z","shell.execute_reply":"2026-08-30T07:37:09.533932Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\n\nprint(\"--- Inspecting /kaggle/input Structure ---\")\nfor root, dirs, files in os.walk('/kaggle/input'):\n    depth = root.replace('/kaggle/input', '').count(os.sep)\n    if depth <= 3:\n        print(f\"{'  ' * depth}📁 {root}\")\n        if files:\n            sample_files = [f for f in files if not f.startswith('.')] [:5]\n            print(f\"{'  ' * (depth + 1)}📄 Samples: {sample_files}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.53561Z","iopub.status.idle":"2026-08-30T07:37:09.535921Z","shell.execute_reply.started":"2026-08-30T07:37:09.535791Z","shell.execute_reply":"2026-08-30T07:37:09.535809Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Clean VRAM\ngc.collect()\ntorch.cuda.empty_cache()\n\n# 2. Geometry-Aware 2.5D Dataset Loader\nclass RobustKneeDataset(Dataset):\n    def __init__(self, df, dicom_root, target_size=(224, 224), max_slices=16, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.dicom_root = dicom_root\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = [\n            'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n            'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n            'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n        ]\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_study(self, study_uid):\n        study_path = os.path.join(self.dicom_root, str(study_uid))\n        if not os.path.exists(study_path):\n            return None\n        \n        # Grab all slice files recursively\n        file_paths = [os.path.join(dp, f) for dp, _, fn in os.walk(study_path) for f in fn if not f.endswith(('.csv', '.txt'))]\n        if not file_paths:\n            return None\n            \n        slices = []\n        for fp in file_paths:\n            try:\n                ds = pydicom.dcmread(fp, stop_before_pixels=False)\n                z_pos = float(getattr(ds, 'ImagePositionPatient', [0, 0, 0])[2]) if hasattr(ds, 'ImagePositionPatient') else float(getattr(ds, 'InstanceNumber', 0))\n                arr = ds.pixel_array.astype(np.float32)\n                \n                # MONOCHROME1 Photometric Inversion\n                if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                    arr = np.amax(arr) - arr\n                    \n                # Min-Max Normalization\n                p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1 + 1e-6)\n                \n                slices.append((z_pos, arr))\n            except Exception:\n                continue\n                \n        if not slices:\n            return None\n            \n        # Sort along volumetric Z-axis\n        slices.sort(key=lambda x: x[0])\n        return [s[1] for s in slices]\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        slices_2d = self._load_study(row['StudyInstanceUID'])\n        \n        if slices_2d is None or len(slices_2d) == 0:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            num_raw = len(slices_2d)\n            if num_raw >= self.max_slices:\n                indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int)\n            else:\n                indices = np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n                \n            sampled = [slices_2d[i] for i in indices]\n            \n            stacked_slabs = []\n            for i in range(len(sampled)):\n                p = sampled[max(0, i - 1)]\n                c = sampled[i]\n                n = sampled[min(len(sampled) - 1, i + 1)]\n                slab = torch.tensor(np.stack([p, c, n], axis=0), dtype=torch.float32).unsqueeze(0)\n                resized = F.interpolate(slab, size=self.target_size, mode='bilinear', align_corners=False)\n                stacked_slabs.append(resized.squeeze(0))\n                \n            vol = torch.stack(stacked_slabs, dim=0)\n\n        # Slice flip augmentation\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 3. Gated Attention MIL Model\nclass GatedAttentionMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        embed_dim = self.encoder.num_features\n        \n        # Gated Attention Module\n        self.attention_V = nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh())\n        self.attention_U = nn.Sequential(nn.Linear(embed_dim, 128), nn.Sigmoid())\n        self.attention_weights = nn.Linear(128, num_classes)\n        \n        # Pathology Classifiers\n        self.classifiers = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(embed_dim, 128),\n                nn.SiLU(),\n                nn.Dropout(0.3),\n                nn.Linear(128, 1)\n            ) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1) # (B, S, embed_dim)\n        \n        v = self.attention_V(feats)\n        u = self.attention_U(feats)\n        attn_scores = self.attention_weights(v * u) # (B, S, 12)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            attn_c = F.softmax(attn_scores[:, :, c], dim=1).unsqueeze(-1)\n            bag_c = torch.sum(feats * attn_c, dim=1)\n            logits_list.append(self.classifiers[c](bag_c))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 4. Training Loop\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\ntrain_df = pd.read_csv('master_train_folds.csv')\nFOLD = 0\n\ntrain_set = RobustKneeDataset(train_df[train_df['fold'] != FOLD], DICOM_ROOT, max_slices=16, is_train=True)\nval_set = RobustKneeDataset(train_df[train_df['fold'] == FOLD], DICOM_ROOT, max_slices=16, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=2, shuffle=True, num_workers=0, pin_memory=True)\nval_loader = DataLoader(val_set, batch_size=2, shuffle=False, num_workers=0, pin_memory=True)\n\nmodel = GatedAttentionMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"--- Training Fold {FOLD} | Train: {len(train_set)} | Val: {len(val_set)} ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, targets) in enumerate(pbar):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\")\n    \n    with torch.no_grad():\n        for images, targets in val_pbar:\n            images = images.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} Complete | Train Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_gated_mil_fold{FOLD}.pth')\n        print(f\"  * Saved Best Model (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Training Complete. Best Score: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.536661Z","iopub.status.idle":"2026-08-30T07:37:09.536958Z","shell.execute_reply.started":"2026-08-30T07:37:09.53684Z","shell.execute_reply":"2026-08-30T07:37:09.536855Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. High-Speed Geometry-Aware Dataset\nclass FastKneeDataset(Dataset):\n    def __init__(self, df, dicom_root, target_size=(224, 224), max_slices=16, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.dicom_root = dicom_root\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = [\n            'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n            'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n            'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n        ]\n        \n        # Pre-index study paths once at initialization\n        print(\"Pre-indexing study file paths...\")\n        self.study_file_map = {}\n        for uid in self.df['StudyInstanceUID']:\n            s_path = os.path.join(self.dicom_root, str(uid))\n            if os.path.exists(s_path):\n                fps = [os.path.join(dp, f) for dp, _, fn in os.walk(s_path) for f in fn if not f.endswith(('.csv', '.txt'))]\n                self.study_file_map[uid] = sorted(fps)\n            else:\n                self.study_file_map[uid] = []\n\n    def __len__(self):\n        return len(self.df)\n\n    def _read_and_process_file(self, fp):\n        try:\n            ds = pydicom.dcmread(fp, stop_before_pixels=False)\n            arr = ds.pixel_array.astype(np.float32)\n            if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                arr = np.amax(arr) - arr\n            arr = cv2.resize(arr, self.target_size, interpolation=cv2.INTER_LINEAR)\n            p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n            if p99 > p1:\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1)\n            else:\n                arr = np.zeros_like(arr)\n            return arr\n        except Exception:\n            return np.zeros(self.target_size, dtype=np.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        file_paths = self.study_file_map.get(uid, [])\n        \n        if not file_paths:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            num_raw = len(file_paths)\n            if num_raw >= self.max_slices:\n                indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int)\n            else:\n                indices = np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            \n            # Read ONLY the required 16 slices\n            sampled_fps = [file_paths[i] for i in indices]\n            sampled_2d = [self._read_and_process_file(fp) for fp in sampled_fps]\n            \n            # Stack 2.5D slabs\n            stacked_slabs = []\n            for i in range(len(sampled_2d)):\n                p = sampled_2d[max(0, i - 1)]\n                c = sampled_2d[i]\n                n = sampled_2d[min(len(sampled_2d) - 1, i + 1)]\n                slab = np.stack([p, c, n], axis=0)\n                stacked_slabs.append(torch.tensor(slab, dtype=torch.float32))\n                \n            vol = torch.stack(stacked_slabs, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 2. Optimized DataLoaders with Multi-Worker Prefetching\ntrain_set = FastKneeDataset(train_df[train_df['fold'] != FOLD], DICOM_ROOT, max_slices=16, is_train=True)\nval_set = FastKneeDataset(train_df[train_df['fold'] == FOLD], DICOM_ROOT, max_slices=16, is_train=False)\n\ntrain_loader = DataLoader(\n    train_set, \n    batch_size=4, \n    shuffle=True, \n    num_workers=4, \n    pin_memory=True, \n    prefetch_factor=2, \n    persistent_workers=True\n)\nval_loader = DataLoader(\n    val_set, \n    batch_size=4, \n    shuffle=False, \n    num_workers=4, \n    pin_memory=True, \n    persistent_workers=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.53815Z","iopub.status.idle":"2026-08-30T07:37:09.538406Z","shell.execute_reply.started":"2026-08-30T07:37:09.53828Z","shell.execute_reply":"2026-08-30T07:37:09.538295Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Clean VRAM\ngc.collect()\ntorch.cuda.empty_cache()\n\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\ntrain_df = pd.read_csv('master_train_folds.csv')\nFOLD = 0\n\n# 2. Parallel Fast File-Index Builder (Takes ~8 seconds total)\nprint(\"Building global study file index in parallel...\")\ndef index_study(uid):\n    s_path = os.path.join(DICOM_ROOT, str(uid))\n    if not os.path.exists(s_path):\n        return uid, []\n    fps = [os.path.join(dp, f) for dp, _, fn in os.walk(s_path) for f in fn if not f.endswith(('.csv', '.txt'))]\n    return uid, sorted(fps)\n\nall_uids = train_df['StudyInstanceUID'].unique()\nwith ThreadPoolExecutor(max_workers=16) as executor:\n    results = list(tqdm(executor.map(index_study, all_uids), total=len(all_uids), desc=\"Indexing DICOMs\"))\n\nGLOBAL_FILE_MAP = dict(results)\nprint(f\"Indexed {len(GLOBAL_FILE_MAP)} studies successfully.\")\n\n# 3. High-Speed Dataset Loader (Zero Init Overhead)\nclass ParallelKneeDataset(Dataset):\n    def __init__(self, df, file_map, target_size=(224, 224), max_slices=16, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.file_map = file_map\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = [\n            'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n            'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n            'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n        ]\n\n    def __len__(self):\n        return len(self.df)\n\n    def _read_slice(self, fp):\n        try:\n            ds = pydicom.dcmread(fp, stop_before_pixels=False)\n            arr = ds.pixel_array.astype(np.float32)\n            if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                arr = np.amax(arr) - arr\n            arr = cv2.resize(arr, self.target_size, interpolation=cv2.INTER_LINEAR)\n            p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n            if p99 > p1:\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1)\n            else:\n                arr = np.zeros_like(arr)\n            return arr\n        except Exception:\n            return np.zeros(self.target_size, dtype=np.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        file_paths = self.file_map.get(uid, [])\n        \n        if not file_paths:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            num_raw = len(file_paths)\n            if num_raw >= self.max_slices:\n                indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int)\n            else:\n                indices = np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            \n            # Read only the selected 16 slices\n            sampled_fps = [file_paths[i] for i in indices]\n            sampled_2d = [self._read_slice(fp) for fp in sampled_fps]\n            \n            # Stack 2.5D slabs\n            stacked = []\n            for i in range(len(sampled_2d)):\n                p = sampled_2d[max(0, i - 1)]\n                c = sampled_2d[i]\n                n = sampled_2d[min(len(sampled_2d) - 1, i + 1)]\n                slab = np.stack([p, c, n], axis=0)\n                stacked.append(torch.tensor(slab, dtype=torch.float32))\n                \n            vol = torch.stack(stacked, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 4. Gated Attention MIL Network\nclass GatedAttentionMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        embed_dim = self.encoder.num_features\n        \n        self.attention_V = nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh())\n        self.attention_U = nn.Sequential(nn.Linear(embed_dim, 128), nn.Sigmoid())\n        self.attention_weights = nn.Linear(128, num_classes)\n        \n        self.classifiers = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(embed_dim, 128),\n                nn.SiLU(),\n                nn.Dropout(0.3),\n                nn.Linear(128, 1)\n            ) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        \n        v = self.attention_V(feats)\n        u = self.attention_U(feats)\n        attn_scores = self.attention_weights(v * u)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            attn_c = F.softmax(attn_scores[:, :, c], dim=1).unsqueeze(-1)\n            bag_c = torch.sum(feats * attn_c, dim=1)\n            logits_list.append(self.classifiers[c](bag_c))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 5. Fast Multi-Worker Training\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ntrain_set = ParallelKneeDataset(train_df[train_df['fold'] != FOLD], GLOBAL_FILE_MAP, max_slices=16, is_train=True)\nval_set = ParallelKneeDataset(train_df[train_df['fold'] == FOLD], GLOBAL_FILE_MAP, max_slices=16, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\nval_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=4, pin_memory=True)\n\nmodel = GatedAttentionMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"\\n--- Firing Fast Training: Fold {FOLD} ({len(train_set)} train, {len(val_set)} val) ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, targets) in enumerate(pbar):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\")\n    \n    with torch.no_grad():\n        for images, targets in val_pbar:\n            images = images.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} Complete | Train Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_gated_mil_fold{FOLD}.pth')\n        print(f\"  * Saved Best Model (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Complete! Best Val AUC: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.539311Z","iopub.status.idle":"2026-08-30T07:37:09.539747Z","shell.execute_reply.started":"2026-08-30T07:37:09.539474Z","shell.execute_reply":"2026-08-30T07:37:09.53951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Clean VRAM\ngc.collect()\ntorch.cuda.empty_cache()\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Re-create Stratified Folds from Saved Checkpoint\nCHECKPOINT_FILE = 'pseudo_labels_checkpoint.csv'\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\n\nif not os.path.exists(CHECKPOINT_FILE):\n    raise FileNotFoundError(f\"'{CHECKPOINT_FILE}' not found. Verify your checkpoint filename in /kaggle/working.\")\n\ntrain_meta = pd.read_csv(TRAIN_CSV)\npseudo_df = pd.read_csv(CHECKPOINT_FILE).drop_duplicates(subset=['StudyInstanceUID'])\n\nfor col in label_cols:\n    pseudo_df[col] = pd.to_numeric(pseudo_df[col], errors='coerce').fillna(0).astype(int)\n\nmerged_master = pd.merge(train_meta[['StudyInstanceUID']], pseudo_df, on='StudyInstanceUID', how='left')\nmerged_master[label_cols] = merged_master[label_cols].fillna(0).astype(int)\n\n# 5-Fold Stratification\ntry:\n    from iterstrat.ml_stratifiers import MultilabelStratifiedKFold\n    mskf = MultilabelStratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n    merged_master['fold'] = -1\n    for fold, (t_idx, v_idx) in enumerate(mskf.split(merged_master, merged_master[label_cols])):\n        merged_master.loc[v_idx, 'fold'] = fold\nexcept Exception:\n    from sklearn.model_selection import KFold\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    merged_master['fold'] = -1\n    for fold, (t_idx, v_idx) in enumerate(kf.split(merged_master)):\n        merged_master.loc[v_idx, 'fold'] = fold\n\nmerged_master.to_csv('master_train_folds.csv', index=False)\ntrain_df = merged_master\nprint(f\"Generated 'master_train_folds.csv' with {len(train_df)} studies across 5 folds.\")\n\n# 3. Parallel File Indexing (Takes ~8s)\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\nFOLD = 0\n\nprint(\"Building global study file index in parallel...\")\ndef index_study(uid):\n    s_path = os.path.join(DICOM_ROOT, str(uid))\n    if not os.path.exists(s_path):\n        return uid, []\n    fps = [os.path.join(dp, f) for dp, _, fn in os.walk(s_path) for f in fn if not f.endswith(('.csv', '.txt'))]\n    return uid, sorted(fps)\n\nall_uids = train_df['StudyInstanceUID'].unique()\nwith ThreadPoolExecutor(max_workers=16) as executor:\n    results = list(tqdm(executor.map(index_study, all_uids), total=len(all_uids), desc=\"Indexing DICOMs\"))\n\nGLOBAL_FILE_MAP = dict(results)\nprint(f\"Indexed {len(GLOBAL_FILE_MAP)} studies.\")\n\n# 4. Fast Dataset Loader\nclass ParallelKneeDataset(Dataset):\n    def __init__(self, df, file_map, target_size=(224, 224), max_slices=16, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.file_map = file_map\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = label_cols\n\n    def __len__(self):\n        return len(self.df)\n\n    def _read_slice(self, fp):\n        try:\n            ds = pydicom.dcmread(fp, stop_before_pixels=False)\n            arr = ds.pixel_array.astype(np.float32)\n            if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                arr = np.amax(arr) - arr\n            arr = cv2.resize(arr, self.target_size, interpolation=cv2.INTER_LINEAR)\n            p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n            if p99 > p1:\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1)\n            else:\n                arr = np.zeros_like(arr)\n            return arr\n        except Exception:\n            return np.zeros(self.target_size, dtype=np.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        file_paths = self.file_map.get(uid, [])\n        \n        if not file_paths:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            num_raw = len(file_paths)\n            if num_raw >= self.max_slices:\n                indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int)\n            else:\n                indices = np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            \n            sampled_fps = [file_paths[i] for i in indices]\n            sampled_2d = [self._read_slice(fp) for fp in sampled_fps]\n            \n            stacked = []\n            for i in range(len(sampled_2d)):\n                p = sampled_2d[max(0, i - 1)]\n                c = sampled_2d[i]\n                n = sampled_2d[min(len(sampled_2d) - 1, i + 1)]\n                slab = np.stack([p, c, n], axis=0)\n                stacked.append(torch.tensor(slab, dtype=torch.float32))\n                \n            vol = torch.stack(stacked, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 5. Gated Attention MIL Architecture\nclass GatedAttentionMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        embed_dim = self.encoder.num_features\n        \n        self.attention_V = nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh())\n        self.attention_U = nn.Sequential(nn.Linear(embed_dim, 128), nn.Sigmoid())\n        self.attention_weights = nn.Linear(128, num_classes)\n        \n        self.classifiers = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(embed_dim, 128),\n                nn.SiLU(),\n                nn.Dropout(0.3),\n                nn.Linear(128, 1)\n            ) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        \n        v = self.attention_V(feats)\n        u = self.attention_U(feats)\n        attn_scores = self.attention_weights(v * u)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            attn_c = F.softmax(attn_scores[:, :, c], dim=1).unsqueeze(-1)\n            bag_c = torch.sum(feats * attn_c, dim=1)\n            logits_list.append(self.classifiers[c](bag_c))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 6. Training Pipeline\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ntrain_set = ParallelKneeDataset(train_df[train_df['fold'] != FOLD], GLOBAL_FILE_MAP, max_slices=16, is_train=True)\nval_set = ParallelKneeDataset(train_df[train_df['fold'] == FOLD], GLOBAL_FILE_MAP, max_slices=16, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\nval_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=4, pin_memory=True)\n\nmodel = GatedAttentionMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"\\n--- Training Fold {FOLD} ({len(train_set)} train, {len(val_set)} val) ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, targets) in enumerate(pbar):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\")\n    \n    with torch.no_grad():\n        for images, targets in val_pbar:\n            images = images.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} Complete | Train Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_gated_mil_fold{FOLD}.pth')\n        print(f\"  * Saved Best Model (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Complete! Best Val AUC: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.540932Z","iopub.status.idle":"2026-08-30T07:37:09.541241Z","shell.execute_reply.started":"2026-08-30T07:37:09.541089Z","shell.execute_reply":"2026-08-30T07:37:09.541104Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\n\nprint(\"Files in current working directory:\")\nfor f in glob.glob('/kaggle/working/*') + glob.glob('./*'):\n    print(\" -\", f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.542376Z","iopub.status.idle":"2026-08-30T07:37:09.542679Z","shell.execute_reply.started":"2026-08-30T07:37:09.542547Z","shell.execute_reply":"2026-08-30T07:37:09.542564Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport re\nimport json\nimport pandas as pd\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm.auto import tqdm\n\n# Setup\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nCHECKPOINT_FILE = '/kaggle/working/pseudo_labels_checkpoint.csv'\ndf = pd.read_csv(TRAIN_CSV)\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# Clinical regex / rule-based fast-parsing fallback engine\ndef parse_report_fast(report_text):\n    text = str(report_text).lower()\n    findings = {col: 0 for col in label_cols}\n    \n    patterns = {\n        'ACL': [r'\\bacl\\b.*(tear|rupture|sprain|deficient|reconstruct)', r'anterior cruciate ligament.*(tear|rupture|sprain)'],\n        'MCL': [r'\\bmcl\\b.*(tear|sprain|injury)', r'medial collateral ligament.*(tear|sprain)'],\n        'Medial Meniscus': [r'medial meniscus.*(tear|macerat|frayed|defect)', r'\\bmm\\b.*tear'],\n        'Lateral Meniscus': [r'lateral meniscus.*(tear|macerat|frayed|defect)', r'\\blm\\b.*tear'],\n        'Medial OA': [r'medial.*(osteoarthritis|arthrosis|joint space narrowing|cartilage loss|chondromalacia)'],\n        'Lateral OA': [r'lateral.*(osteoarthritis|arthrosis|joint space narrowing|cartilage loss)'],\n        'PF OA': [r'patellofemoral.*(osteoarthritis|arthrosis|chondromalacia|cartilage thinning)'],\n        'Effusion': [r'(joint effusion|fluid collection|joint fluid|moderate effusion|large effusion)'],\n        'Synovitis': [r'(synovitis|synovial thickening|synovial enhancement)'],\n        \"Baker's\": [r\"(baker's cyst|popliteal cyst|gastrocnemius-semimembranosus)\"],\n        'Contusion': [r'(bone contusion|bone bruise|marrow edema|marrow contusion)'],\n        'Fracture': [r'(fracture|cortical disruption|trabecular fracture|avulsion)']\n    }\n    \n    # Negation filter\n    negation = r'(no |negative for |without |intact |unremarkable |free of |no evidence of )'\n    \n    for label, pat_list in patterns.items():\n        for pat in pat_list:\n            matches = list(re.finditer(pat, text))\n            for m in matches:\n                # Check preceding window for negation\n                start_window = max(0, m.start() - 30)\n                preceding = text[start_window:m.start()]\n                if not re.search(negation, preceding):\n                    findings[label] = 1\n                    break\n    return findings\n\nprint(f\"Parsing {len(df)} reports across CPU cores...\")\n\ndef process_row(idx_row):\n    idx, row = idx_row\n    labels = parse_report_fast(row.get('clean_text', row.get('description', '')))\n    labels['StudyInstanceUID'] = row['StudyInstanceUID']\n    return labels\n\nwith ThreadPoolExecutor(max_workers=8) as executor:\n    results = list(tqdm(executor.map(process_row, df.iterrows()), total=len(df), desc=\"Extracting Targets\"))\n\npseudo_df = pd.DataFrame(results)\npseudo_df.to_csv(CHECKPOINT_FILE, index=False)\nprint(f\"Saved {len(pseudo_df)} pseudo-labels to '{CHECKPOINT_FILE}'!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.544325Z","iopub.status.idle":"2026-08-30T07:37:09.544632Z","shell.execute_reply.started":"2026-08-30T07:37:09.54448Z","shell.execute_reply":"2026-08-30T07:37:09.544495Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport re\nimport json\nimport pandas as pd\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm.auto import tqdm\n\n# Setup\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nCHECKPOINT_FILE = '/kaggle/working/pseudo_labels_checkpoint.csv'\ndf = pd.read_csv(TRAIN_CSV)\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# Clinical regex / rule-based fast-parsing fallback engine\ndef parse_report_fast(report_text):\n    text = str(report_text).lower()\n    findings = {col: 0 for col in label_cols}\n    \n    patterns = {\n        'ACL': [r'\\bacl\\b.*(tear|rupture|sprain|deficient|reconstruct)', r'anterior cruciate ligament.*(tear|rupture|sprain)'],\n        'MCL': [r'\\bmcl\\b.*(tear|sprain|injury)', r'medial collateral ligament.*(tear|sprain)'],\n        'Medial Meniscus': [r'medial meniscus.*(tear|macerat|frayed|defect)', r'\\bmm\\b.*tear'],\n        'Lateral Meniscus': [r'lateral meniscus.*(tear|macerat|frayed|defect)', r'\\blm\\b.*tear'],\n        'Medial OA': [r'medial.*(osteoarthritis|arthrosis|joint space narrowing|cartilage loss|chondromalacia)'],\n        'Lateral OA': [r'lateral.*(osteoarthritis|arthrosis|joint space narrowing|cartilage loss)'],\n        'PF OA': [r'patellofemoral.*(osteoarthritis|arthrosis|chondromalacia|cartilage thinning)'],\n        'Effusion': [r'(joint effusion|fluid collection|joint fluid|moderate effusion|large effusion)'],\n        'Synovitis': [r'(synovitis|synovial thickening|synovial enhancement)'],\n        \"Baker's\": [r\"(baker's cyst|popliteal cyst|gastrocnemius-semimembranosus)\"],\n        'Contusion': [r'(bone contusion|bone bruise|marrow edema|marrow contusion)'],\n        'Fracture': [r'(fracture|cortical disruption|trabecular fracture|avulsion)']\n    }\n    \n    # Negation filter\n    negation = r'(no |negative for |without |intact |unremarkable |free of |no evidence of )'\n    \n    for label, pat_list in patterns.items():\n        for pat in pat_list:\n            matches = list(re.finditer(pat, text))\n            for m in matches:\n                # Check preceding window for negation\n                start_window = max(0, m.start() - 30)\n                preceding = text[start_window:m.start()]\n                if not re.search(negation, preceding):\n                    findings[label] = 1\n                    break\n    return findings\n\nprint(f\"Parsing {len(df)} reports across CPU cores...\")\n\ndef process_row(idx_row):\n    idx, row = idx_row\n    labels = parse_report_fast(row.get('clean_text', row.get('description', '')))\n    labels['StudyInstanceUID'] = row['StudyInstanceUID']\n    return labels\n\nwith ThreadPoolExecutor(max_workers=8) as executor:\n    results = list(tqdm(executor.map(process_row, df.iterrows()), total=len(df), desc=\"Extracting Targets\"))\n\npseudo_df = pd.DataFrame(results)\npseudo_df.to_csv(CHECKPOINT_FILE, index=False)\nprint(f\"Saved {len(pseudo_df)} pseudo-labels to '{CHECKPOINT_FILE}'!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.546181Z","iopub.status.idle":"2026-08-30T07:37:09.546526Z","shell.execute_reply.started":"2026-08-30T07:37:09.546336Z","shell.execute_reply":"2026-08-30T07:37:09.546363Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Clean VRAM & Set Seed\ngc.collect()\ntorch.cuda.empty_cache()\ntorch.manual_seed(42)\nnp.random.seed(42)\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Build 5-Fold Stratified Split\nCHECKPOINT_FILE = '/kaggle/working/pseudo_labels_checkpoint.csv'\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\nFOLD = 0\n\ntrain_meta = pd.read_csv(TRAIN_CSV)\npseudo_df = pd.read_csv(CHECKPOINT_FILE).drop_duplicates(subset=['StudyInstanceUID'])\n\nfor col in label_cols:\n    pseudo_df[col] = pd.to_numeric(pseudo_df[col], errors='coerce').fillna(0).astype(int)\n\nmerged_master = pd.merge(train_meta[['StudyInstanceUID']], pseudo_df, on='StudyInstanceUID', how='left')\nmerged_master[label_cols] = merged_master[label_cols].fillna(0).astype(int)\n\ntry:\n    from iterstrat.ml_stratifiers import MultilabelStratifiedKFold\n    mskf = MultilabelStratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n    merged_master['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(mskf.split(merged_master, merged_master[label_cols])):\n        merged_master.loc[v_idx, 'fold'] = f\nexcept Exception:\n    from sklearn.model_selection import KFold\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    merged_master['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(kf.split(merged_master)):\n        merged_master.loc[v_idx, 'fold'] = f\n\nmerged_master.to_csv('master_train_folds.csv', index=False)\ntrain_df = merged_master\nprint(f\"Generated 'master_train_folds.csv' with {len(train_df)} studies across 5 folds.\")\n\n# 3. Parallel Global DICOM File Indexing (~8 seconds)\nprint(\"Building study file index in parallel...\")\ndef index_study(uid):\n    s_path = os.path.join(DICOM_ROOT, str(uid))\n    if not os.path.exists(s_path):\n        return uid, []\n    fps = [os.path.join(dp, f) for dp, _, fn in os.walk(s_path) for f in fn if not f.endswith(('.csv', '.txt'))]\n    return uid, sorted(fps)\n\nall_uids = train_df['StudyInstanceUID'].unique()\nwith ThreadPoolExecutor(max_workers=16) as executor:\n    results = list(tqdm(executor.map(index_study, all_uids), total=len(all_uids), desc=\"Indexing DICOMs\"))\n\nGLOBAL_FILE_MAP = dict(results)\nprint(f\"Successfully indexed {len(GLOBAL_FILE_MAP)} study folders.\")\n\n# 4. Fast Geometry-Aware Dataset Loader\nclass ParallelKneeDataset(Dataset):\n    def __init__(self, df, file_map, target_size=(224, 224), max_slices=16, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.file_map = file_map\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = label_cols\n\n    def __len__(self):\n        return len(self.df)\n\n    def _read_slice(self, fp):\n        try:\n            ds = pydicom.dcmread(fp, stop_before_pixels=False)\n            arr = ds.pixel_array.astype(np.float32)\n            if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                arr = np.amax(arr) - arr\n            arr = cv2.resize(arr, self.target_size, interpolation=cv2.INTER_LINEAR)\n            p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n            if p99 > p1:\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1)\n            else:\n                arr = np.zeros_like(arr)\n            return arr\n        except Exception:\n            return np.zeros(self.target_size, dtype=np.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        file_paths = self.file_map.get(uid, [])\n        \n        if not file_paths:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            num_raw = len(file_paths)\n            if num_raw >= self.max_slices:\n                indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int)\n            else:\n                indices = np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            \n            sampled_fps = [file_paths[i] for i in indices]\n            sampled_2d = [self._read_slice(fp) for fp in sampled_fps]\n            \n            stacked = []\n            for i in range(len(sampled_2d)):\n                p = sampled_2d[max(0, i - 1)]\n                c = sampled_2d[i]\n                n = sampled_2d[min(len(sampled_2d) - 1, i + 1)]\n                slab = np.stack([p, c, n], axis=0)\n                stacked.append(torch.tensor(slab, dtype=torch.float32))\n                \n            vol = torch.stack(stacked, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 5. Gated Attention MIL Network\nclass GatedAttentionMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        embed_dim = self.encoder.num_features\n        \n        self.attention_V = nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh())\n        self.attention_U = nn.Sequential(nn.Linear(embed_dim, 128), nn.Sigmoid())\n        self.attention_weights = nn.Linear(128, num_classes)\n        \n        self.classifiers = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(embed_dim, 128),\n                nn.SiLU(),\n                nn.Dropout(0.3),\n                nn.Linear(128, 1)\n            ) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        \n        v = self.attention_V(feats)\n        u = self.attention_U(feats)\n        attn_scores = self.attention_weights(v * u)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            attn_c = F.softmax(attn_scores[:, :, c], dim=1).unsqueeze(-1)\n            bag_c = torch.sum(feats * attn_c, dim=1)\n            logits_list.append(self.classifiers[c](bag_c))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 6. Training Pipeline\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ntrain_set = ParallelKneeDataset(train_df[train_df['fold'] != FOLD], GLOBAL_FILE_MAP, max_slices=16, is_train=True)\nval_set = ParallelKneeDataset(train_df[train_df['fold'] == FOLD], GLOBAL_FILE_MAP, max_slices=16, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\nval_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=4, pin_memory=True)\n\nmodel = GatedAttentionMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"\\n--- Training Fold {FOLD} ({len(train_set)} train, {len(val_set)} val) ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, targets) in enumerate(pbar):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\")\n    \n    with torch.no_grad():\n        for images, targets in val_pbar:\n            images = images.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} Complete | Train Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_gated_mil_fold{FOLD}.pth')\n        print(f\"  * Saved Best Model (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Complete! Best Val AUC: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.547678Z","iopub.status.idle":"2026-08-30T07:37:09.548039Z","shell.execute_reply.started":"2026-08-30T07:37:09.547887Z","shell.execute_reply":"2026-08-30T07:37:09.547903Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\n\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nCHECKPOINT_FILE = '/kaggle/working/pseudo_labels_checkpoint.csv'\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\n\n# 1. Check raw train.csv schema\nraw_train = pd.read_csv(TRAIN_CSV)\nprint(\"=== train.csv Columns ===\")\nprint(raw_train.columns.tolist())\nprint(\"\\n=== Sample First Row ===\")\nprint(raw_train.iloc[0].to_dict())\n\n# 2. Check generated pseudo-label sums\nif os.path.exists(CHECKPOINT_FILE):\n    pseudo_df = pd.read_csv(CHECKPOINT_FILE)\n    print(\"\\n=== Pseudo-Label Class Positive Counts ===\")\n    label_cols = [c for c in pseudo_df.columns if c != 'StudyInstanceUID']\n    print(pseudo_df[label_cols].sum())\n\n# 3. Check train_series folder sample\nif os.path.exists(DICOM_ROOT):\n    sample_dirs = os.listdir(DICOM_ROOT)[:5]\n    print(f\"\\n=== Sample Folders in train_series ({len(os.listdir(DICOM_ROOT))} total) ===\")\n    print(sample_dirs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.550546Z","iopub.status.idle":"2026-08-30T07:37:09.550925Z","shell.execute_reply.started":"2026-08-30T07:37:09.550737Z","shell.execute_reply":"2026-08-30T07:37:09.550761Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport re\nimport pandas as pd\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm.auto import tqdm\n\n# 1. Setup\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nCHECKPOINT_FILE = '/kaggle/working/pseudo_labels_checkpoint.csv'\ndf = pd.read_csv(TRAIN_CSV)\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Spanish/Bilingual Clinical Parsing Engine\ndef parse_report_spanish(report_text):\n    text = str(report_text).lower()\n    findings = {col: 0 for col in label_cols}\n    \n    # Spanish Radiological Terminology Matches\n    patterns = {\n        'ACL': [r'(cruzado anterior|lca).*?(rotura|desgarro|esguince|lesión|plast)', r'acl.*?(tear|sprain|rupture)'],\n        'MCL': [r'(colateral medial|colateral interno|lcm).*?(rotura|desgarro|esguince|lesión)', r'mcl.*?(tear|sprain)'],\n        'Medial Meniscus': [r'menisco (interno|medial).*?(rotura|desgarro|lesión|macerado|franco)', r'medial meniscus.*tear'],\n        'Lateral Meniscus': [r'menisco (externo|lateral).*?(rotura|desgarro|lesión|macerado)', r'lateral meniscus.*tear'],\n        'Medial OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(medial|interno)'],\n        'Lateral OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(lateral|externo)'],\n        'PF OA': [r'(artrosis|condromalacia|desgaste).*?(patelofemoral|femoropatelar|rótula|rotulian|patelar)'],\n        'Effusion': [r'(derrame|líquido articular|aumento de líquido)'],\n        'Synovitis': [r'(sinovitis|engrosamiento sinovial|proliferación sinovial)'],\n        \"Baker's\": [r'(quiste de baker|quiste poplíteo)'],\n        'Contusion': [r'(contusión|edema óseo|edema medular|edema subcondral|contusión ósea)'],\n        'Fracture': [r'(fractura|avulsión|arrancamiento)']\n    }\n    \n    # Spanish Negation Window\n    negation = r'(no hay |sin |ausencia de |negativo para |íntegr|conservad|normal )'\n    \n    for label, pat_list in patterns.items():\n        for pat in pat_list:\n            matches = list(re.finditer(pat, text))\n            for m in matches:\n                start_window = max(0, m.start() - 35)\n                preceding = text[start_window:m.start()]\n                # If no negation word is found right before the match, mark positive\n                if not re.search(negation, preceding):\n                    findings[label] = 1\n                    break\n    return findings\n\nprint(f\"Parsing {len(df)} Spanish/English reports across CPU cores...\")\n\ndef process_row(idx_row):\n    idx, row = idx_row\n    # Target the 'Report' column found in your diagnostic output\n    labels = parse_report_spanish(row.get('Report', ''))\n    labels['StudyInstanceUID'] = row['StudyInstanceUID']\n    return labels\n\n# 3. Parallel Execution\nwith ThreadPoolExecutor(max_workers=8) as executor:\n    results = list(tqdm(executor.map(process_row, df.iterrows()), total=len(df), desc=\"Extracting Labels\"))\n\npseudo_df = pd.DataFrame(results)\npseudo_df.to_csv(CHECKPOINT_FILE, index=False)\n\nprint(f\"Saved {len(pseudo_df)} pseudo-labels to '{CHECKPOINT_FILE}'\")\nprint(\"\\n=== Validated Positive Class Counts ===\")\nprint(pseudo_df[label_cols].sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.5521Z","iopub.status.idle":"2026-08-30T07:37:09.552507Z","shell.execute_reply.started":"2026-08-30T07:37:09.552305Z","shell.execute_reply":"2026-08-30T07:37:09.552329Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Clean VRAM & Set Seed\ngc.collect()\ntorch.cuda.empty_cache()\ntorch.manual_seed(42)\nnp.random.seed(42)\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Re-create Balanced 5-Fold Stratification\nCHECKPOINT_FILE = '/kaggle/working/pseudo_labels_checkpoint.csv'\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\nFOLD = 0\n\ntrain_meta = pd.read_csv(TRAIN_CSV)\npseudo_df = pd.read_csv(CHECKPOINT_FILE).drop_duplicates(subset=['StudyInstanceUID'])\n\nfor col in label_cols:\n    pseudo_df[col] = pd.to_numeric(pseudo_df[col], errors='coerce').fillna(0).astype(int)\n\nmerged_master = pd.merge(train_meta[['StudyInstanceUID']], pseudo_df, on='StudyInstanceUID', how='left')\nmerged_master[label_cols] = merged_master[label_cols].fillna(0).astype(int)\n\ntry:\n    from iterstrat.ml_stratifiers import MultilabelStratifiedKFold\n    mskf = MultilabelStratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n    merged_master['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(mskf.split(merged_master, merged_master[label_cols])):\n        merged_master.loc[v_idx, 'fold'] = f\nexcept Exception:\n    from sklearn.model_selection import KFold\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    merged_master['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(kf.split(merged_master)):\n        merged_master.loc[v_idx, 'fold'] = f\n\nmerged_master.to_csv('master_train_folds.csv', index=False)\ntrain_df = merged_master\nprint(f\"Stratified {len(train_df)} studies across 5 folds.\")\n\n# 3. High-Speed Global Parallel File Indexing (~8 seconds)\nprint(\"Indexing study file paths in parallel...\")\ndef index_study(uid):\n    s_path = os.path.join(DICOM_ROOT, str(uid))\n    if not os.path.exists(s_path):\n        return uid, []\n    fps = [os.path.join(dp, f) for dp, _, fn in os.walk(s_path) for f in fn if not f.endswith(('.csv', '.txt'))]\n    return uid, sorted(fps)\n\nall_uids = train_df['StudyInstanceUID'].unique()\nwith ThreadPoolExecutor(max_workers=16) as executor:\n    results = list(tqdm(executor.map(index_study, all_uids), total=len(all_uids), desc=\"Indexing DICOMs\"))\n\nGLOBAL_FILE_MAP = dict(results)\nprint(f\"Indexed {len(GLOBAL_FILE_MAP)} study folders.\")\n\n# 4. Geometry-Aware Fast Volumetric Dataset\nclass FastKneeDataset(Dataset):\n    def __init__(self, df, file_map, target_size=(224, 224), max_slices=16, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.file_map = file_map\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = label_cols\n\n    def __len__(self):\n        return len(self.df)\n\n    def _read_slice(self, fp):\n        try:\n            ds = pydicom.dcmread(fp, stop_before_pixels=False)\n            arr = ds.pixel_array.astype(np.float32)\n            if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                arr = np.amax(arr) - arr\n            arr = cv2.resize(arr, self.target_size, interpolation=cv2.INTER_LINEAR)\n            p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n            if p99 > p1:\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1)\n            else:\n                arr = np.zeros_like(arr)\n            return arr\n        except Exception:\n            return np.zeros(self.target_size, dtype=np.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        file_paths = self.file_map.get(uid, [])\n        \n        if not file_paths:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            num_raw = len(file_paths)\n            if num_raw >= self.max_slices:\n                indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int)\n            else:\n                indices = np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            \n            sampled_fps = [file_paths[i] for i in indices]\n            sampled_2d = [self._read_slice(fp) for fp in sampled_fps]\n            \n            # Construct 2.5D 3-channel slice slabs\n            stacked = []\n            for i in range(len(sampled_2d)):\n                p = sampled_2d[max(0, i - 1)]\n                c = sampled_2d[i]\n                n = sampled_2d[min(len(sampled_2d) - 1, i + 1)]\n                slab = np.stack([p, c, n], axis=0)\n                stacked.append(torch.tensor(slab, dtype=torch.float32))\n                \n            vol = torch.stack(stacked, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 5. Gated Attention Multiple Instance Learning Model\nclass GatedAttentionMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        embed_dim = self.encoder.num_features\n        \n        self.attention_V = nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh())\n        self.attention_U = nn.Sequential(nn.Linear(embed_dim, 128), nn.Sigmoid())\n        self.attention_weights = nn.Linear(128, num_classes)\n        \n        self.classifiers = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(embed_dim, 128),\n                nn.SiLU(),\n                nn.Dropout(0.3),\n                nn.Linear(128, 1)\n            ) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        \n        v = self.attention_V(feats)\n        u = self.attention_U(feats)\n        attn_scores = self.attention_weights(v * u)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            attn_c = F.softmax(attn_scores[:, :, c], dim=1).unsqueeze(-1)\n            bag_c = torch.sum(feats * attn_c, dim=1)\n            logits_list.append(self.classifiers[c](bag_c))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 6. Training Pipeline\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ntrain_set = FastKneeDataset(train_df[train_df['fold'] != FOLD], GLOBAL_FILE_MAP, max_slices=16, is_train=True)\nval_set = FastKneeDataset(train_df[train_df['fold'] == FOLD], GLOBAL_FILE_MAP, max_slices=16, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\nval_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=4, pin_memory=True)\n\nmodel = GatedAttentionMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"\\n--- Training Fold {FOLD} ({len(train_set)} train, {len(val_set)} val) ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, targets) in enumerate(pbar):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\")\n    \n    with torch.no_grad():\n        for images, targets in val_pbar:\n            images = images.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} Complete | Train Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_gated_mil_fold{FOLD}.pth')\n        print(f\"  * Saved Best Model (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Complete! Best Val AUC: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.553854Z","iopub.status.idle":"2026-08-30T07:37:09.554152Z","shell.execute_reply.started":"2026-08-30T07:37:09.554012Z","shell.execute_reply":"2026-08-30T07:37:09.554029Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport re\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Environment Setup & Hardware Configuration\ngc.collect()\ntorch.cuda.empty_cache()\ntorch.manual_seed(42)\nnp.random.seed(42)\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\nFOLD = 0\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. In-Memory Spanish Clinical Rule Engine\ndef parse_report_spanish(text):\n    text = str(text).lower()\n    findings = {col: 0 for col in label_cols}\n    patterns = {\n        'ACL': [r'(cruzado anterior|lca).*?(rotura|desgarro|esguince|lesión|plast)', r'acl.*?(tear|sprain|rupture)'],\n        'MCL': [r'(colateral medial|colateral interno|lcm).*?(rotura|desgarro|esguince|lesión)', r'mcl.*?(tear|sprain)'],\n        'Medial Meniscus': [r'menisco (interno|medial).*?(rotura|desgarro|lesión|macerado|franco)', r'medial meniscus.*tear'],\n        'Lateral Meniscus': [r'menisco (externo|lateral).*?(rotura|desgarro|lesión|macerado)', r'lateral meniscus.*tear'],\n        'Medial OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(medial|interno)'],\n        'Lateral OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(lateral|externo)'],\n        'PF OA': [r'(artrosis|condromalacia|desgaste).*?(patelofemoral|femoropatelar|rótula|rotulian|patelar)'],\n        'Effusion': [r'(derrame|líquido articular|aumento de líquido)'],\n        'Synovitis': [r'(sinovitis|engrosamiento sinovial|proliferación sinovial)'],\n        \"Baker's\": [r'(quiste de baker|quiste poplíteo)'],\n        'Contusion': [r'(contusión|edema óseo|edema medular|edema subcondral|contusión ósea)'],\n        'Fracture': [r'(fractura|avulsión|arrancamiento)']\n    }\n    negation = r'(no hay |sin |ausencia de |negativo para |íntegr|conservad|normal )'\n    for label, pat_list in patterns.items():\n        for pat in pat_list:\n            matches = list(re.finditer(pat, text))\n            for m in matches:\n                start_window = max(0, m.start() - 35)\n                if not re.search(negation, text[start_window:m.start()]):\n                    findings[label] = 1\n                    break\n    return findings\n\nprint(\"Extracting targets from clinical text...\")\ntrain_df = pd.read_csv(TRAIN_CSV)\nparsed_records = [parse_report_spanish(r) for r in train_df['Report']]\nparsed_df = pd.DataFrame(parsed_records)\n\nfor col in label_cols:\n    train_df[col] = parsed_df[col].values\n\n# Save local backup\ntrain_df.to_csv('/kaggle/working/master_train_folds.csv', index=False)\nprint(\"Target extraction complete. Class distributions:\")\nprint(train_df[label_cols].sum())\n\n# 3. Stratified 5-Fold Splitting\ntry:\n    from iterstrat.ml_stratifiers import MultilabelStratifiedKFold\n    mskf = MultilabelStratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n    train_df['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(mskf.split(train_df, train_df[label_cols])):\n        train_df.loc[v_idx, 'fold'] = f\nexcept Exception:\n    from sklearn.model_selection import KFold\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    train_df['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(kf.split(train_df)):\n        train_df.loc[v_idx, 'fold'] = f\n\n# 4. Multi-Threaded Parallel DICOM Directory Scan\nprint(\"\\nIndexing DICOM studies across threads...\")\ndef index_study(uid):\n    s_path = os.path.join(DICOM_ROOT, str(uid))\n    if not os.path.exists(s_path):\n        return uid, []\n    fps = [os.path.join(dp, f) for dp, _, fn in os.walk(s_path) for f in fn if not f.endswith(('.csv', '.txt'))]\n    return uid, sorted(fps)\n\nall_uids = train_df['StudyInstanceUID'].unique()\nwith ThreadPoolExecutor(max_workers=16) as executor:\n    results = list(tqdm(executor.map(index_study, all_uids), total=len(all_uids), desc=\"Indexing DICOMs\"))\nGLOBAL_FILE_MAP = dict(results)\n\n# 5. Volumetric 2.5D Dataset Loader\nclass FastKneeDataset(Dataset):\n    def __init__(self, df, file_map, target_size=(224, 224), max_slices=16, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.file_map = file_map\n        self.target_size = target_size\n        self.max_slices = max_slices\n        self.is_train = is_train\n        self.label_cols = label_cols\n\n    def __len__(self):\n        return len(self.df)\n\n    def _read_slice(self, fp):\n        try:\n            ds = pydicom.dcmread(fp, stop_before_pixels=False)\n            arr = ds.pixel_array.astype(np.float32)\n            if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                arr = np.amax(arr) - arr\n            arr = cv2.resize(arr, self.target_size, interpolation=cv2.INTER_LINEAR)\n            p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n            if p99 > p1:\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1)\n            else:\n                arr = np.zeros_like(arr)\n            return arr\n        except Exception:\n            return np.zeros(self.target_size, dtype=np.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        file_paths = self.file_map.get(uid, [])\n        \n        if not file_paths:\n            vol = torch.zeros((self.max_slices, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n        else:\n            num_raw = len(file_paths)\n            if num_raw >= self.max_slices:\n                indices = np.linspace(0, num_raw - 1, self.max_slices, dtype=int)\n            else:\n                indices = np.pad(np.arange(num_raw), (0, self.max_slices - num_raw), mode='edge')\n            \n            sampled_fps = [file_paths[i] for i in indices]\n            sampled_2d = [self._read_slice(fp) for fp in sampled_fps]\n            \n            stacked = []\n            for i in range(len(sampled_2d)):\n                p = sampled_2d[max(0, i - 1)]\n                c = sampled_2d[i]\n                n = sampled_2d[min(len(sampled_2d) - 1, i + 1)]\n                slab = np.stack([p, c, n], axis=0)\n                stacked.append(torch.tensor(slab, dtype=torch.float32))\n                \n            vol = torch.stack(stacked, dim=0)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[0])\n\n        labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        return vol, torch.tensor(labels, dtype=torch.float32)\n\n# 6. Gated Attention MIL Network\nclass GatedAttentionMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        embed_dim = self.encoder.num_features\n        \n        self.attention_V = nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh())\n        self.attention_U = nn.Sequential(nn.Linear(embed_dim, 128), nn.Sigmoid())\n        self.attention_weights = nn.Linear(128, num_classes)\n        \n        self.classifiers = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(embed_dim, 128),\n                nn.SiLU(),\n                nn.Dropout(0.3),\n                nn.Linear(128, 1)\n            ) for _ in range(num_classes)\n        ])\n\n    def forward(self, x):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        \n        v = self.attention_V(feats)\n        u = self.attention_U(feats)\n        attn_scores = self.attention_weights(v * u)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            attn_c = F.softmax(attn_scores[:, :, c], dim=1).unsqueeze(-1)\n            bag_c = torch.sum(feats * attn_c, dim=1)\n            logits_list.append(self.classifiers[c](bag_c))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 7. Training Pipeline\ntrain_set = FastKneeDataset(train_df[train_df['fold'] != FOLD], GLOBAL_FILE_MAP, max_slices=16, is_train=True)\nval_set = FastKneeDataset(train_df[train_df['fold'] == FOLD], GLOBAL_FILE_MAP, max_slices=16, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\nval_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=4, pin_memory=True)\n\nmodel = GatedAttentionMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"\\n--- Training Fold {FOLD} ({len(train_set)} train, {len(val_set)} val) ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, targets) in enumerate(pbar):\n        images, targets = images.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\")\n    \n    with torch.no_grad():\n        for images, targets in val_pbar:\n            images = images.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} Complete | Train Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_gated_mil_fold{FOLD}.pth')\n        print(f\"  * Saved Best Model (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Training Complete. Best Score: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.564804Z","iopub.status.idle":"2026-08-30T07:37:09.565391Z","shell.execute_reply.started":"2026-08-30T07:37:09.565217Z","shell.execute_reply":"2026-08-30T07:37:09.565242Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nseries_df = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series.csv')\nprint(\"=== train_series.csv Columns ===\")\nprint(series_df.columns.tolist())\nprint(\"\\n=== Sample Rows ===\")\ndisplay(series_df.head(6))\nprint(\"\\n=== Series Descriptions / Views ===\")\nif 'SeriesDescription' in series_df.columns:\n    print(series_df['SeriesDescription'].value_counts().head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.566808Z","iopub.status.idle":"2026-08-30T07:37:09.567224Z","shell.execute_reply.started":"2026-08-30T07:37:09.567023Z","shell.execute_reply":"2026-08-30T07:37:09.567047Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pydicom\nimport pandas as pd\nimport numpy as np\n\nSERIES_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series.csv'\nseries_df = pd.read_csv(SERIES_CSV)\n\nprint(\"=== train_series.csv Schema ===\")\nprint(\"Columns:\", series_df.columns.tolist())\ndisplay(series_df.head(5))\n\n# Check DICOM orientation for a sample study to automatically classify planes\ndef determine_plane(iop):\n    # ImageOrientationPatient: [cos_x_row, cos_y_row, cos_z_row, cos_x_col, cos_y_col, cos_z_col]\n    if len(iop) < 6:\n        return 'Unknown'\n    row_vec = np.array(iop[:3])\n    col_vec = np.array(iop[3:6])\n    normal_vec = np.cross(row_vec, col_vec)\n    \n    # Largest absolute component determines normal vector orientation\n    plane_idx = np.argmax(np.abs(normal_vec))\n    if plane_idx == 0:\n        return 'Sagittal'   # X-axis normal\n    elif plane_idx == 1:\n        return 'Coronal'    # Y-axis normal\n    elif plane_idx == 2:\n        return 'Axial'      # Z-axis normal\n    return 'Unknown'\n\n# Test on the first study's DICOMs\nsample_study_uid = str(series_df['StudyInstanceUID'].iloc[0])\nsample_dir = f\"/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series/{sample_study_uid}\"\n\nprint(f\"\\n=== Testing Plane Detection on Study: {sample_study_uid} ===\")\nif os.path.exists(sample_dir):\n    found_series = {}\n    for dp, _, fn in os.walk(sample_dir):\n        for f in fn:\n            if not f.endswith(('.csv', '.txt')):\n                try:\n                    ds = pydicom.dcmread(os.path.join(dp, f), stop_before_pixels=True)\n                    s_uid = getattr(ds, 'SeriesInstanceUID', 'unknown_series')\n                    if s_uid not in found_series:\n                        iop = getattr(ds, 'ImageOrientationPatient', [0, 0, 0, 0, 0, 0])\n                        iop = [float(v) for v in iop] if len(iop) == 6 else []\n                        plane = determine_plane(iop) if iop else 'Unknown'\n                        desc = getattr(ds, 'SeriesDescription', 'No Description')\n                        found_series[s_uid] = {'Plane': plane, 'Description': desc}\n                except Exception:\n                    continue\n    \n    for s_uid, info in found_series.items():\n        print(f\"Series: {s_uid} | Detected Plane: {info['Plane']} | Description: {info['Description']}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.568759Z","iopub.status.idle":"2026-08-30T07:37:09.569773Z","shell.execute_reply.started":"2026-08-30T07:37:09.569489Z","shell.execute_reply":"2026-08-30T07:37:09.56952Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Clean Memory & Set Seed\ngc.collect()\ntorch.cuda.empty_cache()\ntorch.manual_seed(42)\nnp.random.seed(42)\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\nFOLD = 0\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Extract Spanish/Bilingual Ground-Truth Labels\ndef parse_report_spanish(text):\n    text = str(text).lower()\n    findings = {col: 0 for col in label_cols}\n    patterns = {\n        'ACL': [r'(cruzado anterior|lca).*?(rotura|desgarro|esguince|lesión|plast)', r'acl.*?(tear|sprain|rupture)'],\n        'MCL': [r'(colateral medial|colateral interno|lcm).*?(rotura|desgarro|esguince|lesión)', r'mcl.*?(tear|sprain)'],\n        'Medial Meniscus': [r'menisco (interno|medial).*?(rotura|desgarro|lesión|macerado|franco)', r'medial meniscus.*tear'],\n        'Lateral Meniscus': [r'menisco (externo|lateral).*?(rotura|desgarro|lesión|macerado)', r'lateral meniscus.*tear'],\n        'Medial OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(medial|interno)'],\n        'Lateral OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(lateral|externo)'],\n        'PF OA': [r'(artrosis|condromalacia|desgaste).*?(patelofemoral|femoropatelar|rótula|rotulian|patelar)'],\n        'Effusion': [r'(derrame|líquido articular|aumento de líquido)'],\n        'Synovitis': [r'(sinovitis|engrosamiento sinovial|proliferación sinovial)'],\n        \"Baker's\": [r'(quiste de baker|quiste poplíteo)'],\n        'Contusion': [r'(contusión|edema óseo|edema medular|edema subcondral|contusión ósea)'],\n        'Fracture': [r'(fractura|avulsión|arrancamiento)']\n    }\n    negation = r'(no hay |sin |ausencia de |negativo para |íntegr|conservad|normal )'\n    for label, pat_list in patterns.items():\n        for pat in pat_list:\n            matches = list(re.finditer(pat, text))\n            for m in matches:\n                start_window = max(0, m.start() - 35)\n                if not re.search(negation, text[start_window:m.start()]):\n                    findings[label] = 1\n                    break\n    return findings\n\nimport re\ntrain_df = pd.read_csv(TRAIN_CSV)\nparsed_records = [parse_report_spanish(r) for r in train_df['Report']]\nparsed_df = pd.DataFrame(parsed_records)\nfor col in label_cols:\n    train_df[col] = parsed_df[col].values\n\n# Stratified 5-Fold Split\ntry:\n    from iterstrat.ml_stratifiers import MultilabelStratifiedKFold\n    mskf = MultilabelStratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n    train_df['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(mskf.split(train_df, train_df[label_cols])):\n        train_df.loc[v_idx, 'fold'] = f\nexcept Exception:\n    from sklearn.model_selection import KFold\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    train_df['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(kf.split(train_df)):\n        train_df.loc[v_idx, 'fold'] = f\n\ntrain_df.to_csv('master_train_folds.csv', index=False)\n\n# 3. Parallel Tri-Planar Study Parser\ndef parse_study_planes(uid):\n    study_path = os.path.join(DICOM_ROOT, str(uid))\n    if not os.path.exists(study_path):\n        return uid, {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    \n    series_dict = {}\n    for dp, _, fn in os.walk(study_path):\n        for f in fn:\n            if not f.endswith(('.csv', '.txt')):\n                fp = os.path.join(dp, f)\n                try:\n                    ds = pydicom.dcmread(fp, stop_before_pixels=True)\n                    s_uid = getattr(ds, 'SeriesInstanceUID', 'default')\n                    iop = getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])\n                    iop = [float(v) for v in iop] if len(iop) == 6 else [1, 0, 0, 0, 1, 0]\n                    normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n                    p_idx = np.argmax(np.abs(normal))\n                    plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n                    \n                    ipp = getattr(ds, 'ImagePositionPatient', [0, 0, 0])\n                    pos = float(np.dot(np.array(ipp, dtype=float), normal)) if len(ipp) == 3 else float(getattr(ds, 'InstanceNumber', 0))\n                    \n                    if s_uid not in series_dict:\n                        series_dict[s_uid] = {'plane': plane, 'slices': []}\n                    series_dict[s_uid]['slices'].append((pos, fp))\n                except Exception:\n                    continue\n                    \n    # Select the series with the maximum number of diagnostic slices for each plane\n    tri_plane_files = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    for plane in ['Sagittal', 'Coronal', 'Axial']:\n        matching_series = [s for s in series_dict.values() if s['plane'] == plane]\n        if matching_series:\n            best_s = max(matching_series, key=lambda x: len(x['slices']))\n            best_s['slices'].sort(key=lambda x: x[0])\n            tri_plane_files[plane] = [item[1] for item in best_s['slices']]\n            \n    return uid, tri_plane_files\n\nprint(\"Indexing Tri-Planar series in parallel...\")\nall_uids = train_df['StudyInstanceUID'].unique()\nwith ThreadPoolExecutor(max_workers=16) as executor:\n    tri_plane_results = list(tqdm(executor.map(parse_study_planes, all_uids), total=len(all_uids), desc=\"Parsing Planes\"))\nGLOBAL_PLANE_MAP = dict(tri_plane_results)\n\n# 4. Tri-Planar Dataset Loader\nclass TriPlanarKneeDataset(Dataset):\n    def __init__(self, df, plane_map, target_size=(224, 224), slices_per_plane=8, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.plane_map = plane_map\n        self.target_size = target_size\n        self.slices_per_plane = slices_per_plane\n        self.is_train = is_train\n        self.label_cols = label_cols\n\n    def __len__(self):\n        return len(self.df)\n\n    def _read_slice(self, fp):\n        try:\n            ds = pydicom.dcmread(fp, stop_before_pixels=False)\n            arr = ds.pixel_array.astype(np.float32)\n            if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                arr = np.amax(arr) - arr\n            arr = cv2.resize(arr, self.target_size, interpolation=cv2.INTER_LINEAR)\n            p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n            if p99 > p1:\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1)\n            else:\n                arr = np.zeros_like(arr)\n            return arr\n        except Exception:\n            return np.zeros(self.target_size, dtype=np.float32)\n\n    def _get_plane_slabs(self, file_paths):\n        if not file_paths:\n            return torch.zeros((self.slices_per_plane, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n            \n        num_raw = len(file_paths)\n        indices = np.linspace(0, num_raw - 1, self.slices_per_plane, dtype=int) if num_raw >= self.slices_per_plane else np.pad(np.arange(num_raw), (0, self.slices_per_plane - num_raw), mode='edge')\n        sampled_fps = [file_paths[i] for i in indices]\n        sampled_2d = [self._read_slice(fp) for fp in sampled_fps]\n        \n        slabs = []\n        for i in range(len(sampled_2d)):\n            p = sampled_2d[max(0, i - 1)]\n            c = sampled_2d[i]\n            n = sampled_2d[min(len(sampled_2d) - 1, i + 1)]\n            slabs.append(torch.tensor(np.stack([p, c, n], axis=0), dtype=torch.float32))\n        return torch.stack(slabs, dim=0)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        planes = self.plane_map.get(uid, {'Sagittal': [], 'Coronal': [], 'Axial': []})\n        \n        sag_vol = self._get_plane_slabs(planes['Sagittal'])  # (8, 3, H, W)\n        cor_vol = self._get_plane_slabs(planes['Coronal'])   # (8, 3, H, W)\n        ax_vol  = self._get_plane_slabs(planes['Axial'])     # (8, 3, H, W)\n        \n        # Combined volume of 24 contiguous slices (8 Sag + 8 Cor + 8 Ax)\n        full_vol = torch.cat([sag_vol, cor_vol, ax_vol], dim=0) # (24, 3, H, W)\n        plane_ids = torch.tensor([0]*8 + [1]*8 + [2]*8, dtype=torch.long)\n        \n        labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        return full_vol, plane_ids, torch.tensor(labels, dtype=torch.float32)\n\n# 5. Tri-Planar Gated Attention MIL Network\nclass TriPlanarMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        if hasattr(self.encoder, 'set_grad_checkpointing'):\n            self.encoder.set_grad_checkpointing(True)\n            \n        embed_dim = self.encoder.num_features\n        # Learnable plane embeddings (Sagittal=0, Coronal=1, Axial=2)\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        \n        self.attention_V = nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh())\n        self.attention_U = nn.Sequential(nn.Linear(embed_dim, 128), nn.Sigmoid())\n        self.attention_weights = nn.Linear(128, num_classes)\n        \n        self.classifiers = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(embed_dim, 128),\n                nn.SiLU(),\n                nn.Dropout(0.3),\n                nn.Linear(128, 1)\n            ) for _ in range(num_classes)\n        ])\n\n    def forward(self, x, plane_ids):\n        # x: (B, 24, 3, H, W), plane_ids: (B, 24)\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        \n        # Inject plane-specific geometric context\n        p_emb = self.plane_emb(plane_ids) # (B, S, embed_dim)\n        feats = feats + p_emb\n        \n        v = self.attention_V(feats)\n        u = self.attention_U(feats)\n        attn_scores = self.attention_weights(v * u) # (B, S, 12)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            attn_c = F.softmax(attn_scores[:, :, c], dim=1).unsqueeze(-1)\n            bag_c = torch.sum(feats * attn_c, dim=1)\n            logits_list.append(self.classifiers[c](bag_c))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 6. Training Pipeline\ntrain_set = TriPlanarKneeDataset(train_df[train_df['fold'] != FOLD], GLOBAL_PLANE_MAP, slices_per_plane=8, is_train=True)\nval_set = TriPlanarKneeDataset(train_df[train_df['fold'] == FOLD], GLOBAL_PLANE_MAP, slices_per_plane=8, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=2, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\nval_loader = DataLoader(val_set, batch_size=2, shuffle=False, num_workers=4, pin_memory=True)\n\nmodel = TriPlanarMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"\\n--- Training Tri-Planar MIL: Fold {FOLD} ({len(train_set)} train, {len(val_set)} val) ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, plane_ids, targets) in enumerate(pbar):\n        images, plane_ids, targets = images.to(DEVICE), plane_ids.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images, plane_ids)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\")\n    \n    with torch.no_grad():\n        for images, plane_ids, targets in val_pbar:\n            images, plane_ids = images.to(DEVICE), plane_ids.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images, plane_ids))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} Complete | Train Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_triplanar_mil_fold{FOLD}.pth')\n        print(f\"  * Saved Best Tri-Planar Model (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Training Complete. Best Score: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.571089Z","iopub.status.idle":"2026-08-30T07:37:09.571383Z","shell.execute_reply.started":"2026-08-30T07:37:09.57126Z","shell.execute_reply":"2026-08-30T07:37:09.571276Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport re\nimport cv2\nimport json\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Environment Setup\ngc.collect()\ntorch.cuda.empty_cache()\ntorch.manual_seed(42)\nnp.random.seed(42)\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\nPLANE_CACHE = '/kaggle/working/tri_plane_map.json'\nFOLD = 0\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Extract Labels & Stratify Folds\ndef parse_report_spanish(text):\n    text = str(text).lower()\n    findings = {col: 0 for col in label_cols}\n    patterns = {\n        'ACL': [r'(cruzado anterior|lca).*?(rotura|desgarro|esguince|lesión|plast)', r'acl.*?(tear|sprain|rupture)'],\n        'MCL': [r'(colateral medial|colateral interno|lcm).*?(rotura|desgarro|esguince|lesión)', r'mcl.*?(tear|sprain)'],\n        'Medial Meniscus': [r'menisco (interno|medial).*?(rotura|desgarro|lesión|macerado|franco)', r'medial meniscus.*tear'],\n        'Lateral Meniscus': [r'menisco (externo|lateral).*?(rotura|desgarro|lesión|macerado)', r'lateral meniscus.*tear'],\n        'Medial OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(medial|interno)'],\n        'Lateral OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(lateral|externo)'],\n        'PF OA': [r'(artrosis|condromalacia|desgaste).*?(patelofemoral|femoropatelar|rótula|rotulian|patelar)'],\n        'Effusion': [r'(derrame|líquido articular|aumento de líquido)'],\n        'Synovitis': [r'(sinovitis|engrosamiento sinovial|proliferación sinovial)'],\n        \"Baker's\": [r'(quiste de baker|quiste poplíteo)'],\n        'Contusion': [r'(contusión|edema óseo|edema medular|edema subcondral|contusión ósea)'],\n        'Fracture': [r'(fractura|avulsión|arrancamiento)']\n    }\n    negation = r'(no hay |sin |ausencia de |negativo para |íntegr|conservad|normal )'\n    for label, pat_list in patterns.items():\n        for pat in pat_list:\n            matches = list(re.finditer(pat, text))\n            for m in matches:\n                start_window = max(0, m.start() - 35)\n                if not re.search(negation, text[start_window:m.start()]):\n                    findings[label] = 1\n                    break\n    return findings\n\ntrain_df = pd.read_csv(TRAIN_CSV)\nparsed_records = [parse_report_spanish(r) for r in train_df['Report']]\nparsed_df = pd.DataFrame(parsed_records)\nfor col in label_cols:\n    train_df[col] = parsed_df[col].values\n\ntry:\n    from iterstrat.ml_stratifiers import MultilabelStratifiedKFold\n    mskf = MultilabelStratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n    train_df['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(mskf.split(train_df, train_df[label_cols])):\n        train_df.loc[v_idx, 'fold'] = f\nexcept Exception:\n    from sklearn.model_selection import KFold\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    train_df['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(kf.split(train_df)):\n        train_df.loc[v_idx, 'fold'] = f\n\n# 3. High-Speed 1-Slice-Per-Series Parallel Plane Indexing\ndef fast_parse_study(uid):\n    study_path = os.path.join(DICOM_ROOT, str(uid))\n    if not os.path.exists(study_path):\n        return uid, {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    \n    tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    \n    # Iterate through series folders directly\n    for root, _, files in os.walk(study_path):\n        dcm_files = [f for f in files if not f.endswith(('.csv', '.txt'))]\n        if not dcm_files:\n            continue\n            \n        dcm_files.sort()\n        first_fp = os.path.join(root, dcm_files[0])\n        try:\n            ds = pydicom.dcmread(first_fp, stop_before_pixels=True)\n            iop = getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])\n            iop = [float(v) for v in iop] if len(iop) == 6 else [1, 0, 0, 0, 1, 0]\n            normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n            p_idx = np.argmax(np.abs(normal))\n            plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n            \n            full_paths = [os.path.join(root, f) for f in dcm_files]\n            if len(full_paths) > len(tri_plane[plane]):\n                tri_plane[plane] = full_paths\n        except Exception:\n            continue\n            \n    return uid, tri_plane\n\nif os.path.exists(PLANE_CACHE):\n    print(\"Loading cached plane map...\")\n    with open(PLANE_CACHE, 'r') as f:\n        GLOBAL_PLANE_MAP = json.load(f)\nelse:\n    print(\"Fast indexing study planes in parallel (~45s)...\")\n    all_uids = train_df['StudyInstanceUID'].unique()\n    with ThreadPoolExecutor(max_workers=32) as executor:\n        results = list(tqdm(executor.map(fast_parse_study, all_uids), total=len(all_uids), desc=\"Indexing Planes\"))\n    GLOBAL_PLANE_MAP = dict(results)\n    with open(PLANE_CACHE, 'w') as f:\n        json.dump(GLOBAL_PLANE_MAP, f)\n\nprint(f\"Indexed {len(GLOBAL_PLANE_MAP)} studies.\")\n\n# 4. Tri-Planar Dataset Loader\nclass TriPlanarKneeDataset(Dataset):\n    def __init__(self, df, plane_map, target_size=(224, 224), slices_per_plane=8, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.plane_map = plane_map\n        self.target_size = target_size\n        self.slices_per_plane = slices_per_plane\n        self.is_train = is_train\n        self.label_cols = label_cols\n\n    def __len__(self):\n        return len(self.df)\n\n    def _read_slice(self, fp):\n        try:\n            ds = pydicom.dcmread(fp, stop_before_pixels=False)\n            arr = ds.pixel_array.astype(np.float32)\n            if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                arr = np.amax(arr) - arr\n            arr = cv2.resize(arr, self.target_size, interpolation=cv2.INTER_LINEAR)\n            p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n            if p99 > p1:\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1)\n            else:\n                arr = np.zeros_like(arr)\n            return arr\n        except Exception:\n            return np.zeros(self.target_size, dtype=np.float32)\n\n    def _get_plane_slabs(self, file_paths):\n        if not file_paths:\n            return torch.zeros((self.slices_per_plane, 3, self.target_size[0], self.target_size[1]), dtype=torch.float32)\n            \n        num_raw = len(file_paths)\n        indices = np.linspace(0, num_raw - 1, self.slices_per_plane, dtype=int) if num_raw >= self.slices_per_plane else np.pad(np.arange(num_raw), (0, self.slices_per_plane - num_raw), mode='edge')\n        sampled_fps = [file_paths[i] for i in indices]\n        sampled_2d = [self._read_slice(fp) for fp in sampled_fps]\n        \n        slabs = []\n        for i in range(len(sampled_2d)):\n            p = sampled_2d[max(0, i - 1)]\n            c = sampled_2d[i]\n            n = sampled_2d[min(len(sampled_2d) - 1, i + 1)]\n            slabs.append(torch.tensor(np.stack([p, c, n], axis=0), dtype=torch.float32))\n        return torch.stack(slabs, dim=0)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        planes = self.plane_map.get(uid, {'Sagittal': [], 'Coronal': [], 'Axial': []})\n        \n        sag_vol = self._get_plane_slabs(planes.get('Sagittal', []))\n        cor_vol = self._get_plane_slabs(planes.get('Coronal', []))\n        ax_vol  = self._get_plane_slabs(planes.get('Axial', []))\n        \n        full_vol = torch.cat([sag_vol, cor_vol, ax_vol], dim=0) # (24, 3, H, W)\n        plane_ids = torch.tensor([0]*8 + [1]*8 + [2]*8, dtype=torch.long)\n        \n        labels = np.array([float(row.get(col, 0.0)) for col in self.label_cols], dtype=np.float32)\n        return full_vol, plane_ids, torch.tensor(labels, dtype=torch.float32)\n\n# 5. Tri-Planar Gated Attention MIL Network\nclass TriPlanarMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.num_classes = num_classes\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        if hasattr(self.encoder, 'set_grad_checkpointing'):\n            self.encoder.set_grad_checkpointing(True)\n            \n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        \n        self.attention_V = nn.Sequential(nn.Linear(embed_dim, 128), nn.Tanh())\n        self.attention_U = nn.Sequential(nn.Linear(embed_dim, 128), nn.Sigmoid())\n        self.attention_weights = nn.Linear(128, num_classes)\n        \n        self.classifiers = nn.ModuleList([\n            nn.Sequential(\n                nn.Linear(embed_dim, 128),\n                nn.SiLU(),\n                nn.Dropout(0.3),\n                nn.Linear(128, 1)\n            ) for _ in range(num_classes)\n        ])\n\n    def forward(self, x, plane_ids):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        \n        p_emb = self.plane_emb(plane_ids)\n        feats = feats + p_emb\n        \n        v = self.attention_V(feats)\n        u = self.attention_U(feats)\n        attn_scores = self.attention_weights(v * u)\n        \n        logits_list = []\n        for c in range(self.num_classes):\n            attn_c = F.softmax(attn_scores[:, :, c], dim=1).unsqueeze(-1)\n            bag_c = torch.sum(feats * attn_c, dim=1)\n            logits_list.append(self.classifiers[c](bag_c))\n            \n        return torch.cat(logits_list, dim=-1)\n\n# 6. Training Pipeline\ntrain_set = TriPlanarKneeDataset(train_df[train_df['fold'] != FOLD], GLOBAL_PLANE_MAP, slices_per_plane=8, is_train=True)\nval_set = TriPlanarKneeDataset(train_df[train_df['fold'] == FOLD], GLOBAL_PLANE_MAP, slices_per_plane=8, is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=2, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\nval_loader = DataLoader(val_set, batch_size=2, shuffle=False, num_workers=4, pin_memory=True)\n\nmodel = TriPlanarMIL(backbone='convnext_tiny', num_classes=12, pretrained=True).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"\\n--- Training Tri-Planar MIL: Fold {FOLD} ({len(train_set)} train, {len(val_set)} val) ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, plane_ids, targets) in enumerate(pbar):\n        images, plane_ids, targets = images.to(DEVICE), plane_ids.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images, plane_ids)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    val_pbar = tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\")\n    \n    with torch.no_grad():\n        for images, plane_ids, targets in val_pbar:\n            images, plane_ids = images.to(DEVICE), plane_ids.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images, plane_ids))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} Complete | Train Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_triplanar_mil_fold{FOLD}.pth')\n        print(f\"  * Saved Best Tri-Planar Model (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Training Complete. Best Score: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.572381Z","iopub.status.idle":"2026-08-30T07:37:09.572655Z","shell.execute_reply.started":"2026-08-30T07:37:09.572534Z","shell.execute_reply":"2026-08-30T07:37:09.57255Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\nfrom concurrent.futures import ProcessPoolExecutor\n\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\nSAVE_DIR = '/kaggle/working/cached_npy_volumes'\nos.makedirs(SAVE_DIR, exist_ok=True)\n\ndf = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\nTARGET_SIZE = (224, 224)\nSLICES_PER_PLANE = 8\n\ndef read_slice(fp):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=False)\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = cv2.resize(arr, TARGET_SIZE, interpolation=cv2.INTER_LINEAR)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        if p99 > p1:\n            arr = np.clip(arr, p1, p99)\n            arr = (arr - p1) / (p99 - p1)\n        else:\n            arr = np.zeros_like(arr)\n        return arr\n    except Exception:\n        return np.zeros(TARGET_SIZE, dtype=np.float32)\n\ndef process_and_save_study(uid):\n    out_file = os.path.join(SAVE_DIR, f\"{uid}.npy\")\n    if os.path.exists(out_file):\n        return\n        \n    study_path = os.path.join(DICOM_ROOT, str(uid))\n    if not os.path.exists(study_path):\n        np.save(out_file, np.zeros((24, 3, TARGET_SIZE[0], TARGET_SIZE[1]), dtype=np.float16))\n        return\n\n    tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    for root, _, files in os.walk(study_path):\n        dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n        if not dcms:\n            continue\n        try:\n            ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n            iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n            normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n            p_idx = np.argmax(np.abs(normal))\n            plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n            fps = [os.path.join(root, f) for f in dcms]\n            if len(fps) > len(tri_plane[plane]):\n                tri_plane[plane] = fps\n        except Exception:\n            continue\n\n    def build_slabs(file_list):\n        if not file_list:\n            return np.zeros((SLICES_PER_PLANE, 3, TARGET_SIZE[0], TARGET_SIZE[1]), dtype=np.float16)\n        n = len(file_list)\n        idx = np.linspace(0, n - 1, SLICES_PER_PLANE, dtype=int) if n >= SLICES_PER_PLANE else np.pad(np.arange(n), (0, SLICES_PER_PLANE - n), mode='edge')\n        slices = [read_slice(file_list[i]) for i in idx]\n        slabs = [np.stack([slices[max(0, i-1)], slices[i], slices[min(len(slices)-1, i+1)]], axis=0) for i in range(len(slices))]\n        return np.stack(slabs, axis=0).astype(np.float16)\n\n    full_vol = np.concatenate([\n        build_slabs(tri_plane['Sagittal']),\n        build_slabs(tri_plane['Coronal']),\n        build_slabs(tri_plane['Axial'])\n    ], axis=0)\n    \n    np.save(out_file, full_vol)\n\nprint(\"Pre-caching all 4,407 studies to fast float16 binary tensors...\")\nuids = df['StudyInstanceUID'].unique()\nwith ProcessPoolExecutor(max_workers=8) as executor:\n    list(tqdm(executor.map(process_and_save_study, uids), total=len(uids), desc=\"Writing Tensors\"))\n\nprint(\"Pre-caching complete! Tensors ready in memory-mapped format.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.574197Z","iopub.status.idle":"2026-08-30T07:37:09.574454Z","shell.execute_reply.started":"2026-08-30T07:37:09.574334Z","shell.execute_reply":"2026-08-30T07:37:09.574349Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport shutil\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\nfrom concurrent.futures import ProcessPoolExecutor\n\n# 1. Clean up incomplete files to free disk space immediately\nSAVE_DIR = '/kaggle/working/cached_uint8_volumes'\nif os.path.exists('/kaggle/working/cached_npy_volumes'):\n    shutil.rmtree('/kaggle/working/cached_npy_volumes', ignore_errors=True)\nos.makedirs(SAVE_DIR, exist_ok=True)\n\nDICOM_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\ndf = pd.read_csv('/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv')\n\nTARGET_SIZE = (224, 224)\nSLICES_PER_PLANE = 8\n\ndef read_slice_uint8(fp):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=False)\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = cv2.resize(arr, TARGET_SIZE, interpolation=cv2.INTER_LINEAR)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        if p99 > p1:\n            arr = np.clip(arr, p1, p99)\n            arr = (arr - p1) / (p99 - p1)\n        else:\n            arr = np.zeros_like(arr)\n        return (arr * 255.0).astype(np.uint8)\n    except Exception:\n        return np.zeros(TARGET_SIZE, dtype=np.uint8)\n\ndef process_and_save_study(uid):\n    out_file = os.path.join(SAVE_DIR, f\"{uid}.npy\")\n    if os.path.exists(out_file):\n        return\n        \n    study_path = os.path.join(DICOM_ROOT, str(uid))\n    if not os.path.exists(study_path):\n        np.save(out_file, np.zeros((24, TARGET_SIZE[0], TARGET_SIZE[1]), dtype=np.uint8))\n        return\n\n    tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    for root, _, files in os.walk(study_path):\n        dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n        if not dcms:\n            continue\n        try:\n            ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n            iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n            normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n            p_idx = np.argmax(np.abs(normal))\n            plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n            fps = [os.path.join(root, f) for f in dcms]\n            if len(fps) > len(tri_plane[plane]):\n                tri_plane[plane] = fps\n        except Exception:\n            continue\n\n    def sample_plane_slices(file_list):\n        if not file_list:\n            return np.zeros((SLICES_PER_PLANE, TARGET_SIZE[0], TARGET_SIZE[1]), dtype=np.uint8)\n        n = len(file_list)\n        idx = np.linspace(0, n - 1, SLICES_PER_PLANE, dtype=int) if n >= SLICES_PER_PLANE else np.pad(np.arange(n), (0, SLICES_PER_PLANE - n), mode='edge')\n        return np.stack([read_slice_uint8(file_list[i]) for i in idx], axis=0)\n\n    # Pack into (24, 224, 224) uint8 array = 1.15 MB per study\n    full_vol = np.concatenate([\n        sample_plane_slices(tri_plane['Sagittal']),\n        sample_plane_slices(tri_plane['Coronal']),\n        sample_plane_slices(tri_plane['Axial'])\n    ], axis=0)\n    \n    np.save(out_file, full_vol)\n\nprint(\"Pre-caching 4,407 studies to compact uint8 binaries (Total size ~5.1 GB)...\")\nuids = df['StudyInstanceUID'].unique()\nwith ProcessPoolExecutor(max_workers=8) as executor:\n    list(tqdm(executor.map(process_and_save_study, uids), total=len(uids), desc=\"Writing 5.1GB Cache\"))\n\nprint(\"Pre-caching complete! Disk space fully safe.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.575752Z","iopub.status.idle":"2026-08-30T07:37:09.576077Z","shell.execute_reply.started":"2026-08-30T07:37:09.575942Z","shell.execute_reply":"2026-08-30T07:37:09.57596Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport re\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom tqdm.auto import tqdm\nimport timm\n\ngc.collect()\ntorch.cuda.empty_cache()\ntorch.manual_seed(42)\nnp.random.seed(42)\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nNPY_DIR = '/kaggle/working/cached_uint8_volumes'\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nFOLD = 0\n\nlabel_cols = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 1. Parse Spanish Clinical Ground-Truth\ndef parse_report_spanish(text):\n    text = str(text).lower()\n    findings = {col: 0 for col in label_cols}\n    patterns = {\n        'ACL': [r'(cruzado anterior|lca).*?(rotura|desgarro|esguince|lesión|plast)', r'acl.*?(tear|sprain|rupture)'],\n        'MCL': [r'(colateral medial|colateral interno|lcm).*?(rotura|desgarro|esguince|lesión)', r'mcl.*?(tear|sprain)'],\n        'Medial Meniscus': [r'menisco (interno|medial).*?(rotura|desgarro|lesión|macerado|franco)', r'medial meniscus.*tear'],\n        'Lateral Meniscus': [r'menisco (externo|lateral).*?(rotura|desgarro|lesión|macerado)', r'lateral meniscus.*tear'],\n        'Medial OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(medial|interno)'],\n        'Lateral OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(lateral|externo)'],\n        'PF OA': [r'(artrosis|condromalacia|desgaste).*?(patelofemoral|femoropatelar|rótula|rotulian|patelar)'],\n        'Effusion': [r'(derrame|líquido articular|aumento de líquido)'],\n        'Synovitis': [r'(sinovitis|engrosamiento sinovial|proliferación sinovial)'],\n        \"Baker's\": [r'(quiste de baker|quiste poplíteo)'],\n        'Contusion': [r'(contusión|edema óseo|edema medular|edema subcondral|contusión ósea)'],\n        'Fracture': [r'(fractura|avulsión|arrancamiento)']\n    }\n    negation = r'(no hay |sin |ausencia de |negativo para |íntegr|conservad|normal )'\n    for label, pat_list in patterns.items():\n        for pat in pat_list:\n            matches = list(re.finditer(pat, text))\n            for m in matches:\n                start_window = max(0, m.start() - 35)\n                if not re.search(negation, text[start_window:m.start()]):\n                    findings[label] = 1\n                    break\n    return findings\n\ntrain_df = pd.read_csv(TRAIN_CSV)\nparsed_records = [parse_report_spanish(r) for r in train_df['Report']]\nparsed_df = pd.DataFrame(parsed_records)\nfor col in label_cols:\n    train_df[col] = parsed_df[col].values\n\ntry:\n    from iterstrat.ml_stratifiers import MultilabelStratifiedKFold\n    mskf = MultilabelStratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n    train_df['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(mskf.split(train_df, train_df[label_cols])):\n        train_df.loc[v_idx, 'fold'] = f\nexcept Exception:\n    from sklearn.model_selection import KFold\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    train_df['fold'] = -1\n    for f, (t_idx, v_idx) in enumerate(kf.split(train_df)):\n        train_df.loc[v_idx, 'fold'] = f\n\n# 2. Fast Dynamic Slab Dataset\nclass FastUint8Dataset(Dataset):\n    def __init__(self, df, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.is_train = is_train\n\n    def __len__(self):\n        return len(self.df)\n\n    def _build_plane_slabs(self, plane_slices):\n        # plane_slices: (8, 224, 224) float32\n        slabs = []\n        for i in range(8):\n            p = plane_slices[max(0, i - 1)]\n            c = plane_slices[i]\n            n = plane_slices[min(7, i + 1)]\n            slabs.append(np.stack([p, c, n], axis=0))\n        return np.stack(slabs, axis=0) # (8, 3, 224, 224)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        fp = os.path.join(NPY_DIR, f\"{uid}.npy\")\n        \n        if os.path.exists(fp):\n            raw = np.load(fp).astype(np.float32) / 255.0 # (24, 224, 224)\n        else:\n            raw = np.zeros((24, 224, 224), dtype=np.float32)\n\n        # Build 3-channel 2.5D slabs per plane in RAM\n        sag = self._build_plane_slabs(raw[0:8])\n        cor = self._build_plane_slabs(raw[8:16])\n        axi = self._build_plane_slabs(raw[16:24])\n        vol = torch.from_numpy(np.concatenate([sag, cor, axi], axis=0)) # (24, 3, 224, 224)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[-1])\n\n        plane_ids = torch.tensor([0]*8 + [1]*8 + [2]*8, dtype=torch.long)\n        targets = torch.tensor([float(row.get(col, 0.0)) for col in label_cols], dtype=torch.float32)\n        return vol, plane_ids, targets\n\n# 3. Model Architecture\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True, tau=1.0):\n        super().__init__()\n        self.num_classes = num_classes\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        if hasattr(self.encoder, 'set_grad_checkpointing'):\n            self.encoder.set_grad_checkpointing(True)\n            \n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        \n        self.slice_head = nn.Sequential(\n            nn.Linear(embed_dim, 256),\n            nn.SiLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x, plane_ids):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        feats = feats + self.plane_emb(plane_ids)\n        slice_logits = self.slice_head(feats)\n        study_logits = self.tau * torch.logsumexp(slice_logits / self.tau, dim=1)\n        return study_logits\n\n# 4. Positive Class Loss Weights\ntrain_targets = train_df[train_df['fold'] != FOLD][label_cols].values\npos_counts = train_targets.sum(axis=0)\nneg_counts = len(train_targets) - pos_counts\npos_weights = torch.tensor(np.clip(neg_counts / (pos_counts + 1e-5), 1.0, 15.0), dtype=torch.float32).to(DEVICE)\n\ntrain_set = FastUint8Dataset(train_df[train_df['fold'] != FOLD], is_train=True)\nval_set = FastUint8Dataset(train_df[train_df['fold'] == FOLD], is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\nval_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=4, pin_memory=True)\n\nmodel = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', num_classes=12, pretrained=True, tau=1.0).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)\n\noptimizer = torch.optim.AdamW([\n    {'params': model.encoder.parameters(), 'lr': 3e-5, 'weight_decay': 1e-2},\n    {'params': model.plane_emb.parameters(), 'lr': 5e-4, 'weight_decay': 1e-3},\n    {'params': model.slice_head.parameters(), 'lr': 5e-4, 'weight_decay': 1e-3}\n])\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nACCUMULATION_STEPS = 2\nbest_auc = 0.0\n\nprint(f\"\\n--- Ultra-Fast Training: Fold {FOLD} ---\")\n\nfor epoch in range(8):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/8 [Train]\")\n    for step, (images, plane_ids, targets) in enumerate(pbar):\n        images, plane_ids, targets = images.to(DEVICE), plane_ids.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images, plane_ids)\n            loss = criterion(logits, targets) / ACCUMULATION_STEPS\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * ACCUMULATION_STEPS\n        pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n    \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    with torch.no_grad():\n        for images, plane_ids, targets in tqdm(val_loader, desc=f\"Epoch {epoch+1}/8 [Val]\"):\n            images, plane_ids = images.to(DEVICE), plane_ids.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images, plane_ids))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    \n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1} | Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), f'best_smoothmax_mil_fold{FOLD}.pth')\n        print(f\"  * Saved Best Model (AUC: {best_auc:.4f})\")\n\nprint(f\"\\nFold {FOLD} Complete! Best Val AUC: {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.577069Z","iopub.status.idle":"2026-08-30T07:37:09.57736Z","shell.execute_reply.started":"2026-08-30T07:37:09.577232Z","shell.execute_reply":"2026-08-30T07:37:09.577249Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom scipy.stats import rankdata\nimport torch\nimport torch.nn as nn\nfrom torch.amp import autocast\nfrom tqdm.auto import tqdm\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nTEST_SERIES_DIR = '/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series'\nSAMPLE_SUB_PATH = '/kaggle/input/competitions/rsna-knee-abnormality-detection/sample_submission.csv'\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 1. Load Inference Data\nsub_df = pd.read_csv(SAMPLE_SUB_PATH)\ntest_uids = sub_df['StudyInstanceUID'].unique() if 'StudyInstanceUID' in sub_df.columns else [f for f in os.listdir(TEST_SERIES_DIR) if os.path.isdir(os.path.join(TEST_SERIES_DIR, f))]\n\ndef process_test_volume(uid):\n    spath = os.path.join(TEST_SERIES_DIR, str(uid))\n    if not os.path.exists(spath):\n        return np.zeros((24, 3, 224, 224), dtype=np.float32)\n        \n    tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    for root, _, files in os.walk(spath):\n        dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n        if not dcms:\n            continue\n        try:\n            ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n            iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n            normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n            p_idx = np.argmax(np.abs(normal))\n            plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n            fps = [os.path.join(root, f) for f in dcms]\n            if len(fps) > len(tri_plane[plane]):\n                tri_plane[plane] = fps\n        except Exception:\n            continue\n\n    def get_slabs(file_list):\n        if not file_list:\n            return np.zeros((8, 3, 224, 224), dtype=np.float32)\n        n = len(file_list)\n        idx = np.linspace(0, n - 1, 8, dtype=int) if n >= 8 else np.pad(np.arange(n), (0, 8 - n), mode='edge')\n        \n        slices_2d = []\n        for i in idx:\n            try:\n                ds = pydicom.dcmread(file_list[i], stop_before_pixels=False)\n                arr = ds.pixel_array.astype(np.float32)\n                if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n                    arr = np.amax(arr) - arr\n                arr = cv2.resize(arr, (224, 224), interpolation=cv2.INTER_LINEAR)\n                p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n                arr = np.clip(arr, p1, p99)\n                arr = (arr - p1) / (p99 - p1) if p99 > p1 else np.zeros_like(arr)\n                slices_2d.append(arr)\n            except Exception:\n                slices_2d.append(np.zeros((224, 224), dtype=np.float32))\n                \n        slabs = []\n        for i in range(8):\n            p = slices_2d[max(0, i-1)]\n            c = slices_2d[i]\n            n = slices_2d[min(7, i+1)]\n            slabs.append(np.stack([p, c, n], axis=0))\n        return np.stack(slabs, axis=0)\n\n    full = np.concatenate([\n        get_slabs(tri_plane['Sagittal']),\n        get_slabs(tri_plane['Coronal']),\n        get_slabs(tri_plane['Axial'])\n    ], axis=0)\n    return full\n\n# 2. Load Model Checkpoints\nmodels = []\n\n# Load ConvNeXt Folds\nfor f in range(5):\n    p = f'best_convnext_fold{f}.pth' if f > 0 else 'best_smoothmax_mil_fold0.pth'\n    if os.path.exists(p):\n        m = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', pretrained=False).to(DEVICE)\n        m.load_state_dict(torch.load(p, map_location=DEVICE))\n        m.eval()\n        models.append(m)\n\n# Load Swin Models\nif os.path.exists('best_swin_fold0.pth'):\n    sm = SwinKneeMIL(pretrained=False).to(DEVICE)\n    sm.load_state_dict(torch.load('best_swin_fold0.pth', map_location=DEVICE))\n    sm.eval()\n    models.append(sm)\n\nprint(f\"Loaded {len(models)} ensemble models for inference.\")\n\n# 3. Predict with 3D Test-Time Augmentation (TTA)\nall_preds = []\nplane_ids = torch.tensor([[0]*8 + [1]*8 + [2]*8], dtype=torch.long).to(DEVICE)\n\nwith torch.no_grad():\n    for uid in tqdm(test_uids, desc=\"Test Inference\"):\n        vol_np = process_test_volume(uid) # (24, 3, 224, 224)\n        vol_orig = torch.from_numpy(vol_np).unsqueeze(0).to(DEVICE)\n        vol_flip = torch.flip(vol_orig, dims=[-1]) # Horizontal TTA\n        \n        study_model_preds = []\n        for m in models:\n            with autocast(device_type='cuda', dtype=torch.float16):\n                p1 = torch.sigmoid(m(vol_orig, plane_ids))\n                p2 = torch.sigmoid(m(vol_flip, plane_ids))\n                pred = ((p1 + p2) / 2.0).cpu().numpy()[0]\n                study_model_preds.append(pred)\n                \n        # Average model probabilities for this study\n        avg_study_pred = np.mean(study_model_preds, axis=0)\n        all_preds.append(avg_study_pred)\n\nall_preds = np.array(all_preds)\n\n# 4. Percentile Rank Transformation (Optimized for Macro-AUC)\nrank_preds = np.zeros_like(all_preds)\nfor c in range(12):\n    rank_preds[:, c] = (rankdata(all_preds[:, c]) - 1.0) / (len(all_preds) - 1.0 + 1e-8)\n\n# 5. Format Submission\nsub = pd.DataFrame({'StudyInstanceUID': test_uids})\nfor i, col in enumerate(LABEL_COLS):\n    sub[col] = rank_preds[:, i]\n\nsub.to_csv('submission.csv', index=False)\nprint(\"Saved 'submission.csv' successfully with full rank-averaged TTA ensemble!\")\ndisplay(sub.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.579Z","iopub.status.idle":"2026-08-30T07:37:09.579403Z","shell.execute_reply.started":"2026-08-30T07:37:09.579244Z","shell.execute_reply":"2026-08-30T07:37:09.579266Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\n\n# Remove partially loaded pandas modules from memory\nfor mod in list(sys.modules.keys()):\n    if mod.startswith('pandas'):\n        del sys.modules[mod]\n\nimport pandas as pd\nprint(f\"Pandas successfully restored! Version: {pd.__version__}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.581033Z","iopub.status.idle":"2026-08-30T07:37:09.581378Z","shell.execute_reply.started":"2026-08-30T07:37:09.581239Z","shell.execute_reply":"2026-08-30T07:37:09.581257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Hardware & Environment Config\ngc.collect()\ntorch.cuda.empty_cache()\ntorch.manual_seed(42)\nnp.random.seed(42)\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nNPY_DIR = '/kaggle/working/cached_uint8_volumes'\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Dataset Definition\nclass FastUint8Dataset(Dataset):\n    def __init__(self, df, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.is_train = is_train\n\n    def __len__(self):\n        return len(self.df)\n\n    def _build_plane_slabs(self, plane_slices):\n        slabs = []\n        for i in range(8):\n            p = plane_slices[max(0, i - 1)]\n            c = plane_slices[i]\n            n = plane_slices[min(7, i + 1)]\n            slabs.append(np.stack([p, c, n], axis=0))\n        return np.stack(slabs, axis=0)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        fp = os.path.join(NPY_DIR, f\"{uid}.npy\")\n        \n        if os.path.exists(fp):\n            raw = np.load(fp).astype(np.float32) / 255.0\n        else:\n            raw = np.zeros((24, 224, 224), dtype=np.float32)\n\n        sag = self._build_plane_slabs(raw[0:8])\n        cor = self._build_plane_slabs(raw[8:16])\n        axi = self._build_plane_slabs(raw[16:24])\n        vol = torch.from_numpy(np.concatenate([sag, cor, axi], axis=0))\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[-1])\n\n        plane_ids = torch.tensor([0]*8 + [1]*8 + [2]*8, dtype=torch.long)\n        targets = torch.tensor([float(row.get(col, 0.0)) for col in LABEL_COLS], dtype=torch.float32)\n        return vol, plane_ids, targets\n\n# 3. Model Architecture\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True, tau=1.0):\n        super().__init__()\n        self.num_classes = num_classes\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        if hasattr(self.encoder, 'set_grad_checkpointing'):\n            self.encoder.set_grad_checkpointing(True)\n            \n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        \n        self.slice_head = nn.Sequential(\n            nn.Linear(embed_dim, 256),\n            nn.SiLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x, plane_ids):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        feats = feats + self.plane_emb(plane_ids)\n        slice_logits = self.slice_head(feats)\n        study_logits = self.tau * torch.logsumexp(slice_logits / self.tau, dim=1)\n        return study_logits\n\n# 4. Load Folds Metadata\nif os.path.exists('/kaggle/working/master_train_folds.csv'):\n    train_df = pd.read_csv('/kaggle/working/master_train_folds.csv')\nelse:\n    raise FileNotFoundError(\"master_train_folds.csv not found in /kaggle/working/\")\n\n# 5. Train Folds 1 through 4\nfor FOLD in range(1, 5):\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    print(f\"\\n{'='*20} TRAINING CONVNEXT-T: FOLD {FOLD} {'='*20}\")\n    \n    train_set = FastUint8Dataset(train_df[train_df['fold'] != FOLD], is_train=True)\n    val_set = FastUint8Dataset(train_df[train_df['fold'] == FOLD], is_train=False)\n    \n    train_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\n    val_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=4, pin_memory=True)\n    \n    targets_np = train_df[train_df['fold'] != FOLD][LABEL_COLS].values\n    pos_counts = targets_np.sum(axis=0)\n    neg_counts = len(targets_np) - pos_counts\n    pos_weights = torch.tensor(np.clip(neg_counts / (pos_counts + 1e-5), 1.0, 15.0), dtype=torch.float32).to(DEVICE)\n    \n    model = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', num_classes=12, pretrained=True, tau=1.0).to(DEVICE)\n    criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)\n    \n    optimizer = torch.optim.AdamW([\n        {'params': model.encoder.parameters(), 'lr': 3e-5, 'weight_decay': 1e-2},\n        {'params': model.plane_emb.parameters(), 'lr': 5e-4, 'weight_decay': 1e-3},\n        {'params': model.slice_head.parameters(), 'lr': 5e-4, 'weight_decay': 1e-3}\n    ])\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\n    scaler = GradScaler('cuda')\n    \n    best_auc = 0.0\n    ACCUMULATION_STEPS = 2\n    \n    for epoch in range(8):\n        model.train()\n        running_loss = 0.0\n        optimizer.zero_grad()\n        \n        pbar = tqdm(train_loader, desc=f\"Fold {FOLD} | Epoch {epoch+1}/8 [Train]\")\n        for step, (images, plane_ids, targets) in enumerate(pbar):\n            images, plane_ids, targets = images.to(DEVICE), plane_ids.to(DEVICE), targets.to(DEVICE)\n            \n            with autocast(device_type='cuda', dtype=torch.float16):\n                logits = model(images, plane_ids)\n                loss = criterion(logits, targets) / ACCUMULATION_STEPS\n                \n            scaler.scale(loss).backward()\n            \n            if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n                scaler.unscale_(optimizer)\n                torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n                scaler.step(optimizer)\n                scaler.update()\n                optimizer.zero_grad()\n                \n            running_loss += loss.item() * ACCUMULATION_STEPS\n            pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n        \n        scheduler.step()\n        \n        # Validation Loop\n        model.eval()\n        val_preds, val_targets = [], []\n        with torch.no_grad():\n            for images, plane_ids, targets in tqdm(val_loader, desc=f\"Fold {FOLD} | Epoch {epoch+1}/8 [Val]\"):\n                images, plane_ids = images.to(DEVICE), plane_ids.to(DEVICE)\n                with autocast(device_type='cuda', dtype=torch.float16):\n                    probs = torch.sigmoid(model(images, plane_ids))\n                val_preds.append(probs.cpu().numpy())\n                val_targets.append(targets.numpy())\n                \n        val_preds = np.concatenate(val_preds, axis=0)\n        val_targets = np.concatenate(val_targets, axis=0)\n        \n        aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n        epoch_auc = np.mean(aucs) if aucs else 0.0\n        print(f\"--> Fold {FOLD} Epoch {epoch+1}/8 | Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n        \n        if epoch_auc > best_auc:\n            best_auc = epoch_auc\n            torch.save(model.state_dict(), f'best_convnext_fold{FOLD}.pth')\n            print(f\"  * Saved Best Weights for Fold {FOLD} (AUC: {best_auc:.4f})\")\n            \n    print(f\"\\n=== Fold {FOLD} Complete! Best Score: {best_auc:.4f} ===\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.582583Z","iopub.status.idle":"2026-08-30T07:37:09.582946Z","shell.execute_reply.started":"2026-08-30T07:37:09.582778Z","shell.execute_reply":"2026-08-30T07:37:09.582795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport re\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Hardware & Environment Config\ngc.collect()\ntorch.cuda.empty_cache()\ntorch.manual_seed(42)\nnp.random.seed(42)\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nNPY_DIR = '/kaggle/working/cached_uint8_volumes'\nTRAIN_CSV = '/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\nFOLDS_CSV = '/kaggle/working/master_train_folds.csv'\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Generate or Load Master Folds\ndef parse_report_spanish(text):\n    text = str(text).lower()\n    findings = {col: 0 for col in LABEL_COLS}\n    patterns = {\n        'ACL': [r'(cruzado anterior|lca).*?(rotura|desgarro|esguince|lesión|plast)', r'acl.*?(tear|sprain|rupture)'],\n        'MCL': [r'(colateral medial|colateral interno|lcm).*?(rotura|desgarro|esguince|lesión)', r'mcl.*?(tear|sprain)'],\n        'Medial Meniscus': [r'menisco (interno|medial).*?(rotura|desgarro|lesión|macerado|franco)', r'medial meniscus.*tear'],\n        'Lateral Meniscus': [r'menisco (externo|lateral).*?(rotura|desgarro|lesión|macerado)', r'lateral meniscus.*tear'],\n        'Medial OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(medial|interno)'],\n        'Lateral OA': [r'(artrosis|condromalacia|pinzamiento|desgaste).*?(lateral|externo)'],\n        'PF OA': [r'(artrosis|condromalacia|desgaste).*?(patelofemoral|femoropatelar|rótula|rotulian|patelar)'],\n        'Effusion': [r'(derrame|líquido articular|aumento de líquido)'],\n        'Synovitis': [r'(sinovitis|engrosamiento sinovial|proliferación sinovial)'],\n        \"Baker's\": [r'(quiste de baker|quiste poplíteo)'],\n        'Contusion': [r'(contusión|edema óseo|edema medular|edema subcondral|contusión ósea)'],\n        'Fracture': [r'(fractura|avulsión|arrancamiento)']\n    }\n    negation = r'(no hay |sin |ausencia de |negativo para |íntegr|conservad|normal )'\n    for label, pat_list in patterns.items():\n        for pat in pat_list:\n            matches = list(re.finditer(pat, text))\n            for m in matches:\n                start_window = max(0, m.start() - 35)\n                if not re.search(negation, text[start_window:m.start()]):\n                    findings[label] = 1\n                    break\n    return findings\n\nif os.path.exists(FOLDS_CSV):\n    print(\"Loading existing master_train_folds.csv...\")\n    train_df = pd.read_csv(FOLDS_CSV)\nelse:\n    print(\"Building master_train_folds.csv from raw train.csv...\")\n    train_df = pd.read_csv(TRAIN_CSV)\n    parsed_records = [parse_report_spanish(r) for r in train_df['Report']]\n    parsed_df = pd.DataFrame(parsed_records)\n    for col in LABEL_COLS:\n        train_df[col] = parsed_df[col].values\n\n    try:\n        from iterstrat.ml_stratifiers import MultilabelStratifiedKFold\n        mskf = MultilabelStratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n        train_df['fold'] = -1\n        for f, (t_idx, v_idx) in enumerate(mskf.split(train_df, train_df[LABEL_COLS])):\n            train_df.loc[v_idx, 'fold'] = f\n    except Exception:\n        from sklearn.model_selection import KFold\n        kf = KFold(n_splits=5, shuffle=True, random_state=42)\n        train_df['fold'] = -1\n        for f, (t_idx, v_idx) in enumerate(kf.split(train_df)):\n            train_df.loc[v_idx, 'fold'] = f\n\n    train_df.to_csv(FOLDS_CSV, index=False)\n    print(\"Saved master_train_folds.csv successfully.\")\n\n# 3. Fast Uint8 Binary Dataset\nclass FastUint8Dataset(Dataset):\n    def __init__(self, df, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.is_train = is_train\n\n    def __len__(self):\n        return len(self.df)\n\n    def _build_plane_slabs(self, plane_slices):\n        slabs = []\n        for i in range(8):\n            p = plane_slices[max(0, i - 1)]\n            c = plane_slices[i]\n            n = plane_slices[min(7, i + 1)]\n            slabs.append(np.stack([p, c, n], axis=0))\n        return np.stack(slabs, axis=0)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        fp = os.path.join(NPY_DIR, f\"{uid}.npy\")\n        \n        if os.path.exists(fp):\n            raw = np.load(fp).astype(np.float32) / 255.0\n        else:\n            raw = np.zeros((24, 224, 224), dtype=np.float32)\n\n        sag = self._build_plane_slabs(raw[0:8])\n        cor = self._build_plane_slabs(raw[8:16])\n        axi = self._build_plane_slabs(raw[16:24])\n        vol = torch.from_numpy(np.concatenate([sag, cor, axi], axis=0))\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[-1])\n\n        plane_ids = torch.tensor([0]*8 + [1]*8 + [2]*8, dtype=torch.long)\n        targets = torch.tensor([float(row.get(col, 0.0)) for col in LABEL_COLS], dtype=torch.float32)\n        return vol, plane_ids, targets\n\n# 4. Model Architecture\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True, tau=1.0):\n        super().__init__()\n        self.num_classes = num_classes\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        if hasattr(self.encoder, 'set_grad_checkpointing'):\n            self.encoder.set_grad_checkpointing(True)\n            \n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        \n        self.slice_head = nn.Sequential(\n            nn.Linear(embed_dim, 256),\n            nn.SiLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x, plane_ids):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        feats = feats + self.plane_emb(plane_ids)\n        slice_logits = self.slice_head(feats)\n        study_logits = self.tau * torch.logsumexp(slice_logits / self.tau, dim=1)\n        return study_logits\n\n# 5. Train Folds 1 through 4\nfor FOLD in range(1, 5):\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    print(f\"\\n{'='*20} TRAINING CONVNEXT-T: FOLD {FOLD} {'='*20}\")\n    \n    train_set = FastUint8Dataset(train_df[train_df['fold'] != FOLD], is_train=True)\n    val_set = FastUint8Dataset(train_df[train_df['fold'] == FOLD], is_train=False)\n    \n    train_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\n    val_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=4, pin_memory=True)\n    \n    targets_np = train_df[train_df['fold'] != FOLD][LABEL_COLS].values\n    pos_counts = targets_np.sum(axis=0)\n    neg_counts = len(targets_np) - pos_counts\n    pos_weights = torch.tensor(np.clip(neg_counts / (pos_counts + 1e-5), 1.0, 15.0), dtype=torch.float32).to(DEVICE)\n    \n    model = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', num_classes=12, pretrained=True, tau=1.0).to(DEVICE)\n    criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)\n    \n    optimizer = torch.optim.AdamW([\n        {'params': model.encoder.parameters(), 'lr': 3e-5, 'weight_decay': 1e-2},\n        {'params': model.plane_emb.parameters(), 'lr': 5e-4, 'weight_decay': 1e-3},\n        {'params': model.slice_head.parameters(), 'lr': 5e-4, 'weight_decay': 1e-3}\n    ])\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\n    scaler = GradScaler('cuda')\n    \n    best_auc = 0.0\n    ACCUMULATION_STEPS = 2\n    \n    for epoch in range(8):\n        model.train()\n        running_loss = 0.0\n        optimizer.zero_grad()\n        \n        pbar = tqdm(train_loader, desc=f\"Fold {FOLD} | Epoch {epoch+1}/8 [Train]\")\n        for step, (images, plane_ids, targets) in enumerate(pbar):\n            images, plane_ids, targets = images.to(DEVICE), plane_ids.to(DEVICE), targets.to(DEVICE)\n            \n            with autocast(device_type='cuda', dtype=torch.float16):\n                logits = model(images, plane_ids)\n                loss = criterion(logits, targets) / ACCUMULATION_STEPS\n                \n            scaler.scale(loss).backward()\n            \n            if (step + 1) % ACCUMULATION_STEPS == 0 or (step + 1) == len(train_loader):\n                scaler.unscale_(optimizer)\n                torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n                scaler.step(optimizer)\n                scaler.update()\n                optimizer.zero_grad()\n                \n            running_loss += loss.item() * ACCUMULATION_STEPS\n            pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n        \n        scheduler.step()\n        \n        # Validation Loop\n        model.eval()\n        val_preds, val_targets = [], []\n        with torch.no_grad():\n            for images, plane_ids, targets in tqdm(val_loader, desc=f\"Fold {FOLD} | Epoch {epoch+1}/8 [Val]\"):\n                images, plane_ids = images.to(DEVICE), plane_ids.to(DEVICE)\n                with autocast(device_type='cuda', dtype=torch.float16):\n                    probs = torch.sigmoid(model(images, plane_ids))\n                val_preds.append(probs.cpu().numpy())\n                val_targets.append(targets.numpy())\n                \n        val_preds = np.concatenate(val_preds, axis=0)\n        val_targets = np.concatenate(val_targets, axis=0)\n        \n        aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n        epoch_auc = np.mean(aucs) if aucs else 0.0\n        print(f\"--> Fold {FOLD} Epoch {epoch+1}/8 | Loss: {running_loss/len(train_loader):.4f} | Val Macro-AUC: {epoch_auc:.4f}\")\n        \n        if epoch_auc > best_auc:\n            best_auc = epoch_auc\n            torch.save(model.state_dict(), f'best_convnext_fold{FOLD}.pth')\n            print(f\"  * Saved Best Weights for Fold {FOLD} (AUC: {best_auc:.4f})\")\n            \n    print(f\"\\n=== Fold {FOLD} Complete! Best Score: {best_auc:.4f} ===\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.5841Z","iopub.status.idle":"2026-08-30T07:37:09.584488Z","shell.execute_reply.started":"2026-08-30T07:37:09.584267Z","shell.execute_reply":"2026-08-30T07:37:09.584284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom scipy.stats import rankdata\nimport torch\nimport torch.nn as nn\nfrom torch.amp import autocast\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Environment & Path Setup\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# Auto-detect competition path\nCOMP_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\nif not os.path.exists(COMP_ROOT):\n    COMP_ROOT = '/kaggle/input/rsna-knee-abnormality-detection'\n\nSAMPLE_SUB_PATH = os.path.join(COMP_ROOT, 'sample_submission.csv')\nTEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test_series')\nif not os.path.exists(TEST_SERIES_DIR):\n    TEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test')\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\nTARGET_SIZE = (224, 224)\nSLICES_PER_PLANE = 8\nWEIGHTS_PATH = '/kaggle/working/best_smoothmax_mil_fold0.pth'\n\n# 2. Model Definition\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=False, tau=1.0):\n        super().__init__()\n        self.num_classes = num_classes\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3)\n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        \n        self.slice_head = nn.Sequential(\n            nn.Linear(embed_dim, 256),\n            nn.SiLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x, plane_ids):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        feats = feats + self.plane_emb(plane_ids)\n        slice_logits = self.slice_head(feats)\n        study_logits = self.tau * torch.logsumexp(slice_logits / self.tau, dim=1)\n        return study_logits\n\n# 3. Load Model\nmodel = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', num_classes=12, pretrained=False).to(DEVICE)\nif os.path.exists(WEIGHTS_PATH):\n    model.load_state_dict(torch.load(WEIGHTS_PATH, map_location=DEVICE))\n    print(f\"Successfully loaded model weights from: {WEIGHTS_PATH}\")\nelse:\n    raise FileNotFoundError(f\"Model checkpoint not found at: {WEIGHTS_PATH}\")\nmodel.eval()\n\n# 4. DICOM Slicing & Tri-Planar Reconstruction\ndef read_slice(fp):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=False)\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = cv2.resize(arr, TARGET_SIZE, interpolation=cv2.INTER_LINEAR)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        if p99 > p1:\n            arr = np.clip(arr, p1, p99)\n            arr = (arr - p1) / (p99 - p1)\n        else:\n            arr = np.zeros_like(arr)\n        return arr\n    except Exception:\n        return np.zeros(TARGET_SIZE, dtype=np.float32)\n\ndef process_study(uid):\n    study_path = os.path.join(TEST_SERIES_DIR, str(uid))\n    if not os.path.exists(study_path):\n        return np.zeros((24, 3, TARGET_SIZE[0], TARGET_SIZE[1]), dtype=np.float32)\n\n    tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    for root, _, files in os.walk(study_path):\n        dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n        if not dcms:\n            continue\n        try:\n            ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n            iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n            normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n            p_idx = np.argmax(np.abs(normal))\n            plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n            fps = [os.path.join(root, f) for f in dcms]\n            if len(fps) > len(tri_plane[plane]):\n                tri_plane[plane] = fps\n        except Exception:\n            continue\n\n    def build_plane_slabs(file_list):\n        if not file_list:\n            return np.zeros((SLICES_PER_PLANE, 3, TARGET_SIZE[0], TARGET_SIZE[1]), dtype=np.float32)\n        n = len(file_list)\n        idx = np.linspace(0, n - 1, SLICES_PER_PLANE, dtype=int) if n >= SLICES_PER_PLANE else np.pad(np.arange(n), (0, SLICES_PER_PLANE - n), mode='edge')\n        slices = [read_slice(file_list[i]) for i in idx]\n        slabs = []\n        for i in range(len(slices)):\n            p = slices[max(0, i - 1)]\n            c = slices[i]\n            n_slice = slices[min(len(slices) - 1, i + 1)]\n            slabs.append(np.stack([p, c, n_slice], axis=0))\n        return np.stack(slabs, axis=0)\n\n    full_vol = np.concatenate([\n        build_plane_slabs(tri_plane['Sagittal']),\n        build_plane_slabs(tri_plane['Coronal']),\n        build_plane_slabs(tri_plane['Axial'])\n    ], axis=0) # Shape: (24, 3, 224, 224)\n    return full_vol\n\n# 5. Execute Test Set Inference\nif os.path.exists(SAMPLE_SUB_PATH):\n    sub_df = pd.read_csv(SAMPLE_SUB_PATH)\n    test_uids = sub_df['StudyInstanceUID'].unique() if 'StudyInstanceUID' in sub_df.columns else sub_df.iloc[:, 0].unique()\nelse:\n    test_uids = [f for f in os.listdir(TEST_SERIES_DIR) if os.path.isdir(os.path.join(TEST_SERIES_DIR, f))]\n    sub_df = pd.DataFrame({'StudyInstanceUID': test_uids})\n\nprint(f\"Running inference across {len(test_uids)} test studies with 3D Flip TTA...\")\n\nplane_ids = torch.tensor([[0]*8 + [1]*8 + [2]*8], dtype=torch.long).to(DEVICE)\nall_preds = []\n\nwith torch.no_grad():\n    for uid in tqdm(test_uids, desc=\"Predicting\"):\n        vol = process_study(uid)\n        vol_tensor = torch.from_numpy(vol).unsqueeze(0).to(DEVICE)\n        vol_flip = torch.flip(vol_tensor, dims=[-1]) # Horizontal TTA\n\n        with autocast(device_type='cuda', dtype=torch.float16):\n            p1 = torch.sigmoid(model(vol_tensor, plane_ids))\n            p2 = torch.sigmoid(model(vol_flip, plane_ids))\n            pred = ((p1 + p2) / 2.0).cpu().numpy()[0]\n            \n        all_preds.append(pred)\n\nall_preds = np.array(all_preds)\n\n# 6. Rank Calibration for Macro-AUC\nrank_preds = np.zeros_like(all_preds)\nfor c in range(12):\n    if len(all_preds) > 1:\n        rank_preds[:, c] = (rankdata(all_preds[:, c]) - 1.0) / (len(all_preds) - 1.0 + 1e-8)\n    else:\n        rank_preds[:, c] = all_preds[:, c]\n\n# 7. Write submission.csv\nfinal_sub = pd.DataFrame({'StudyInstanceUID': test_uids})\nfor i, col in enumerate(LABEL_COLS):\n    final_sub[col] = rank_preds[:, i]\n\n# Match sample_submission structure exactly\nif os.path.exists(SAMPLE_SUB_PATH):\n    sample = pd.read_csv(SAMPLE_SUB_PATH)\n    id_col = sample.columns[0]\n    final_sub.rename(columns={'StudyInstanceUID': id_col}, inplace=True)\n    # Reorder columns to match sample submission exactly\n    cols_to_use = [c for c in sample.columns if c in final_sub.columns]\n    final_sub = final_sub[cols_to_use]\n\nfinal_sub.to_csv('/kaggle/working/submission.csv', index=False)\nprint(\"--> submission.csv created successfully!\")\nprint(f\"Shape: {final_sub.shape}\")\nprint(final_sub.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.586262Z","iopub.status.idle":"2026-08-30T07:37:09.58697Z","shell.execute_reply.started":"2026-08-30T07:37:09.586676Z","shell.execute_reply":"2026-08-30T07:37:09.586762Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\n\n# Check files in /kaggle/working and subdirectories\npth_files = glob.glob('/kaggle/working/**/*.pth', recursive=True) + glob.glob('./*.pth')\nprint(\"Found checkpoints:\")\nfor p in set(pth_files):\n    print(f\" - {p} ({os.path.getsize(p) / (1024*1024):.2f} MB)\")\n\nif not pth_files:\n    print(\"No .pth files found. Check if the training session restarted or if files were saved to the root directory:\")\n    print(os.listdir('/kaggle/working'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.587982Z","iopub.status.idle":"2026-08-30T07:37:09.588366Z","shell.execute_reply.started":"2026-08-30T07:37:09.588168Z","shell.execute_reply":"2026-08-30T07:37:09.588194Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport shutil\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom scipy.stats import rankdata\nfrom concurrent.futures import ProcessPoolExecutor\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom tqdm.auto import tqdm\nimport timm\n\n# 1. Environment & Path Setup\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\ntorch.manual_seed(42)\nnp.random.seed(42)\n\nCOMP_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\nif not os.path.exists(COMP_ROOT):\n    COMP_ROOT = '/kaggle/input/rsna-knee-abnormality-detection'\n\nTRAIN_CSV = os.path.join(COMP_ROOT, 'train.csv')\nDICOM_ROOT = os.path.join(COMP_ROOT, 'train_series')\nTEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test_series')\nif not os.path.exists(TEST_SERIES_DIR):\n    TEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test')\nSAMPLE_SUB_PATH = os.path.join(COMP_ROOT, 'sample_submission.csv')\n\nNPY_DIR = '/kaggle/working/cached_uint8_volumes'\nos.makedirs(NPY_DIR, exist_ok=True)\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\nTARGET_SIZE = (224, 224)\nSLICES_PER_PLANE = 8\nFOLD = 0\n\n# 2. Verify or Rebuild uint8 Cache\ndef read_slice_uint8(fp):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=False)\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = cv2.resize(arr, TARGET_SIZE, interpolation=cv2.INTER_LINEAR)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        arr = np.clip(arr, p1, p99)\n        arr = (arr - p1) / (p99 - p1) if p99 > p1 else np.zeros_like(arr)\n        return (arr * 255.0).astype(np.uint8)\n    except Exception:\n        return np.zeros(TARGET_SIZE, dtype=np.uint8)\n\ndef cache_study(uid):\n    out_file = os.path.join(NPY_DIR, f\"{uid}.npy\")\n    if os.path.exists(out_file) and os.path.getsize(out_file) > 1000:\n        return\n    study_path = os.path.join(DICOM_ROOT, str(uid))\n    if not os.path.exists(study_path):\n        np.save(out_file, np.zeros((24, TARGET_SIZE[0], TARGET_SIZE[1]), dtype=np.uint8))\n        return\n\n    tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    for root, _, files in os.walk(study_path):\n        dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n        if not dcms:\n            continue\n        try:\n            ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n            iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n            normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n            p_idx = np.argmax(np.abs(normal))\n            plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n            fps = [os.path.join(root, f) for f in dcms]\n            if len(fps) > len(tri_plane[plane]):\n                tri_plane[plane] = fps\n        except Exception:\n            continue\n\n    def sample_plane(file_list):\n        if not file_list:\n            return np.zeros((SLICES_PER_PLANE, TARGET_SIZE[0], TARGET_SIZE[1]), dtype=np.uint8)\n        n = len(file_list)\n        idx = np.linspace(0, n - 1, SLICES_PER_PLANE, dtype=int) if n >= SLICES_PER_PLANE else np.pad(np.arange(n), (0, SLICES_PER_PLANE - n), mode='edge')\n        return np.stack([read_slice_uint8(file_list[i]) for i in idx], axis=0)\n\n    full_vol = np.concatenate([\n        sample_plane(tri_plane['Sagittal']),\n        sample_plane(tri_plane['Coronal']),\n        sample_plane(tri_plane['Axial'])\n    ], axis=0)\n    np.save(out_file, full_vol)\n\ntrain_df = pd.read_csv('/kaggle/working/master_train_folds.csv')\ncached_files = set(os.listdir(NPY_DIR))\nmissing_uids = [uid for uid in train_df['StudyInstanceUID'].unique() if f\"{uid}.npy\" not in cached_files]\n\nif len(missing_uids) > 0:\n    print(f\"Caching {len(missing_uids)} missing study volumes to disk...\")\n    with ProcessPoolExecutor(max_workers=8) as executor:\n        list(tqdm(executor.map(cache_study, missing_uids), total=len(missing_uids), desc=\"Re-caching\"))\n\n# 3. Model Architecture & Dataset\nclass FastUint8Dataset(Dataset):\n    def __init__(self, df, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.is_train = is_train\n\n    def __len__(self):\n        return len(self.df)\n\n    def _build_plane_slabs(self, plane_slices):\n        slabs = []\n        for i in range(8):\n            p = plane_slices[max(0, i - 1)]\n            c = plane_slices[i]\n            n = plane_slices[min(7, i + 1)]\n            slabs.append(np.stack([p, c, n], axis=0))\n        return np.stack(slabs, axis=0)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        fp = os.path.join(NPY_DIR, f\"{uid}.npy\")\n        \n        raw = np.load(fp).astype(np.float32) / 255.0 if os.path.exists(fp) else np.zeros((24, 224, 224), dtype=np.float32)\n        sag = self._build_plane_slabs(raw[0:8])\n        cor = self._build_plane_slabs(raw[8:16])\n        axi = self._build_plane_slabs(raw[16:24])\n        vol = torch.from_numpy(np.concatenate([sag, cor, axi], axis=0))\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[-1])\n\n        plane_ids = torch.tensor([0]*8 + [1]*8 + [2]*8, dtype=torch.long)\n        targets = torch.tensor([float(row.get(col, 0.0)) for col in LABEL_COLS], dtype=torch.float32)\n        return vol, plane_ids, targets\n\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True, tau=1.0):\n        super().__init__()\n        self.num_classes = num_classes\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        if hasattr(self.encoder, 'set_grad_checkpointing'):\n            self.encoder.set_grad_checkpointing(True)\n            \n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        \n        self.slice_head = nn.Sequential(\n            nn.Linear(embed_dim, 256),\n            nn.SiLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x, plane_ids):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        feats = feats + self.plane_emb(plane_ids)\n        slice_logits = self.slice_head(feats)\n        return self.tau * torch.logsumexp(slice_logits / self.tau, dim=1)\n\n# 4. Train Fold 0\ntrain_set = FastUint8Dataset(train_df[train_df['fold'] != FOLD], is_train=True)\nval_set = FastUint8Dataset(train_df[train_df['fold'] == FOLD], is_train=False)\n\ntrain_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=4, pin_memory=True, prefetch_factor=2)\nval_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=4, pin_memory=True)\n\ntargets_np = train_df[train_df['fold'] != FOLD][LABEL_COLS].values\npos_counts = targets_np.sum(axis=0)\nneg_counts = len(targets_np) - pos_counts\npos_weights = torch.tensor(np.clip(neg_counts / (pos_counts + 1e-5), 1.0, 15.0), dtype=torch.float32).to(DEVICE)\n\nmodel = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', num_classes=12, pretrained=True, tau=1.0).to(DEVICE)\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)\n\noptimizer = torch.optim.AdamW([\n    {'params': model.encoder.parameters(), 'lr': 3e-5, 'weight_decay': 1e-2},\n    {'params': model.plane_emb.parameters(), 'lr': 5e-4, 'weight_decay': 1e-3},\n    {'params': model.slice_head.parameters(), 'lr': 5e-4, 'weight_decay': 1e-3}\n])\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=6, eta_min=1e-6)\nscaler = GradScaler('cuda')\n\nprint(f\"\\n--- Training Fold {FOLD} (6 Epochs) ---\")\nbest_auc = 0.0\n\nfor epoch in range(6):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    \n    for step, (images, plane_ids, targets) in enumerate(tqdm(train_loader, desc=f\"Epoch {epoch+1}/6 [Train]\")):\n        images, plane_ids, targets = images.to(DEVICE), plane_ids.to(DEVICE), targets.to(DEVICE)\n        \n        with autocast(device_type='cuda', dtype=torch.float16):\n            logits = model(images, plane_ids)\n            loss = criterion(logits, targets) / 2\n            \n        scaler.scale(loss).backward()\n        \n        if (step + 1) % 2 == 0 or (step + 1) == len(train_loader):\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        running_loss += loss.item() * 2\n        \n    scheduler.step()\n    \n    # Validation Loop\n    model.eval()\n    val_preds, val_targets = [], []\n    with torch.no_grad():\n        for images, plane_ids, targets in val_loader:\n            images, plane_ids = images.to(DEVICE), plane_ids.to(DEVICE)\n            with autocast(device_type='cuda', dtype=torch.float16):\n                probs = torch.sigmoid(model(images, plane_ids))\n            val_preds.append(probs.cpu().numpy())\n            val_targets.append(targets.numpy())\n            \n    val_preds = np.concatenate(val_preds, axis=0)\n    val_targets = np.concatenate(val_targets, axis=0)\n    aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n    epoch_auc = np.mean(aucs) if aucs else 0.0\n    print(f\"--> Epoch {epoch+1}/6 | Val Macro-AUC: {epoch_auc:.4f}\")\n    \n    if epoch_auc > best_auc:\n        best_auc = epoch_auc\n        torch.save(model.state_dict(), '/kaggle/working/best_smoothmax_mil_fold0.pth')\n        print(f\"  * Saved /kaggle/working/best_smoothmax_mil_fold0.pth (AUC: {best_auc:.4f})\")\n\n# 5. Direct Test Set Inference & Rank Submission\nprint(\"\\n--- Generating Final Submission via 3D Flip TTA ---\")\nmodel.load_state_dict(torch.load('/kaggle/working/best_smoothmax_mil_fold0.pth', map_location=DEVICE))\nmodel.eval()\n\ndef process_test_volume(uid):\n    study_path = os.path.join(TEST_SERIES_DIR, str(uid))\n    if not os.path.exists(study_path):\n        return np.zeros((24, 3, 224, 224), dtype=np.float32)\n\n    tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    for root, _, files in os.walk(study_path):\n        dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n        if not dcms:\n            continue\n        try:\n            ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n            iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n            normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n            p_idx = np.argmax(np.abs(normal))\n            plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n            fps = [os.path.join(root, f) for f in dcms]\n            if len(fps) > len(tri_plane[plane]):\n                tri_plane[plane] = fps\n        except Exception:\n            continue\n\n    def get_slabs(file_list):\n        if not file_list:\n            return np.zeros((8, 3, 224, 224), dtype=np.float32)\n        n = len(file_list)\n        idx = np.linspace(0, n - 1, 8, dtype=int) if n >= 8 else np.pad(np.arange(n), (0, 8 - n), mode='edge')\n        slices_2d = [read_slice_uint8(file_list[i]).astype(np.float32) / 255.0 for i in idx]\n        slabs = [np.stack([slices_2d[max(0, i-1)], slices_2d[i], slices_2d[min(7, i+1)]], axis=0) for i in range(8)]\n        return np.stack(slabs, axis=0)\n\n    return np.concatenate([\n        get_slabs(tri_plane['Sagittal']),\n        get_slabs(tri_plane['Coronal']),\n        get_slabs(tri_plane['Axial'])\n    ], axis=0)\n\nif os.path.exists(SAMPLE_SUB_PATH):\n    sample_df = pd.read_csv(SAMPLE_SUB_PATH)\n    test_uids = sample_df['StudyInstanceUID'].unique() if 'StudyInstanceUID' in sample_df.columns else sample_df.iloc[:, 0].unique()\nelse:\n    test_uids = [f for f in os.listdir(TEST_SERIES_DIR) if os.path.isdir(os.path.join(TEST_SERIES_DIR, f))]\n    sample_df = pd.DataFrame({'StudyInstanceUID': test_uids})\n\nplane_ids = torch.tensor([[0]*8 + [1]*8 + [2]*8], dtype=torch.long).to(DEVICE)\nall_preds = []\n\nwith torch.no_grad():\n    for uid in tqdm(test_uids, desc=\"Test Inference\"):\n        vol_np = process_test_volume(uid)\n        vol_tensor = torch.from_numpy(vol_np).unsqueeze(0).to(DEVICE)\n        vol_flip = torch.flip(vol_tensor, dims=[-1])\n        with autocast(device_type='cuda', dtype=torch.float16):\n            p1 = torch.sigmoid(model(vol_tensor, plane_ids))\n            p2 = torch.sigmoid(model(vol_flip, plane_ids))\n            pred = ((p1 + p2) / 2.0).cpu().numpy()[0]\n        all_preds.append(pred)\n\nall_preds = np.array(all_preds)\n\nrank_preds = np.zeros_like(all_preds)\nfor c in range(12):\n    rank_preds[:, c] = (rankdata(all_preds[:, c]) - 1.0) / (len(all_preds) - 1.0 + 1e-8) if len(all_preds) > 1 else all_preds[:, c]\n\nfinal_sub = pd.DataFrame({'StudyInstanceUID': test_uids})\nfor i, col in enumerate(LABEL_COLS):\n    final_sub[col] = rank_preds[:, i]\n\nif os.path.exists(SAMPLE_SUB_PATH):\n    id_col = sample_df.columns[0]\n    final_sub.rename(columns={'StudyInstanceUID': id_col}, inplace=True)\n    cols_to_use = [c for c in sample_df.columns if c in final_sub.columns]\n    final_sub = final_sub[cols_to_use]\n\nfinal_sub.to_csv('/kaggle/working/submission.csv', index=False)\nprint(\"\\n\" + \"=\"*50)\nprint(\"SUCCESS: /kaggle/working/submission.csv is ready for scoring!\")\nprint(\"=\"*50)\nprint(final_sub.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:09.589744Z","iopub.status.idle":"2026-08-30T07:37:09.590153Z","shell.execute_reply.started":"2026-08-30T07:37:09.589943Z","shell.execute_reply":"2026-08-30T07:37:09.589977Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\n\nNPY_DIR = '/kaggle/working/cached_uint8_volumes'\ntrain_df = pd.read_csv('/kaggle/working/master_train_folds.csv')\nuids = train_df['StudyInstanceUID'].unique()\n\nprint(f\"Loading {len(uids)} volumes (5.1 GB) directly into RAM...\")\nRAM_CACHE = {}\n\nfor uid in tqdm(uids, desc=\"RAM Preload\"):\n    fp = os.path.join(NPY_DIR, f\"{uid}.npy\")\n    if os.path.exists(fp):\n        RAM_CACHE[uid] = np.load(fp) # Shape: (24, 224, 224) uint8\n    else:\n        RAM_CACHE[uid] = np.zeros((24, 224, 224), dtype=np.uint8)\n\nprint(f\"Dataset fully resident in RAM ({len(RAM_CACHE)} volumes). Disk read latency is now 0.00 ms.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:13.014926Z","iopub.execute_input":"2026-08-30T07:37:13.015373Z","iopub.status.idle":"2026-08-30T07:37:13.66188Z","shell.execute_reply.started":"2026-08-30T07:37:13.015342Z","shell.execute_reply":"2026-08-30T07:37:13.661212Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom torch.amp import GradScaler, autocast\nfrom tqdm.auto import tqdm\nimport timm\n\n# Hardware Optimization\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\ntorch.backends.cudnn.benchmark = True\ntorch.manual_seed(42)\nnp.random.seed(42)\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\ntrain_df = pd.read_csv('/kaggle/working/master_train_folds.csv')\n\n# 1. Zero-Disk In-Memory Dataset\nclass InMemoryKneeDataset(Dataset):\n    def __init__(self, df, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.is_train = is_train\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = row['StudyInstanceUID']\n        \n        # Zero-latency RAM access\n        raw = torch.from_numpy(RAM_CACHE[uid]).float() / 255.0 # (24, 224, 224)\n\n        # Vectorized 2.5D slab construction\n        slabs = []\n        for p_start in (0, 8, 16):\n            p_slice = raw[p_start:p_start + 8]\n            prev_s = torch.roll(p_slice, shifts=1, dims=0)\n            prev_s[0] = p_slice[0]\n            next_s = torch.roll(p_slice, shifts=-1, dims=0)\n            next_s[-1] = p_slice[-1]\n            slabs.append(torch.stack([prev_s, p_slice, next_s], dim=1))\n\n        vol = torch.cat(slabs, dim=0) # (24, 3, 224, 224)\n\n        if self.is_train and np.random.rand() > 0.5:\n            vol = torch.flip(vol, dims=[-1])\n\n        plane_ids = torch.tensor([0]*8 + [1]*8 + [2]*8, dtype=torch.long)\n        targets = torch.tensor([float(row.get(col, 0.0)) for col in LABEL_COLS], dtype=torch.float32)\n        return vol, plane_ids, targets\n\n# 2. Model Architecture\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True, tau=1.0):\n        super().__init__()\n        self.num_classes = num_classes\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3, drop_rate=0.2)\n        if hasattr(self.encoder, 'set_grad_checkpointing'):\n            self.encoder.set_grad_checkpointing(True)\n            \n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        \n        self.slice_head = nn.Sequential(\n            nn.Linear(embed_dim, 256),\n            nn.SiLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x, plane_ids):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        feats = feats + self.plane_emb(plane_ids)\n        slice_logits = self.slice_head(feats)\n        return self.tau * torch.logsumexp(slice_logits / self.tau, dim=1)\n\n# 3. Fast Sequential 5-Fold Loop\nfold_scores = {}\n\nfor target_fold in range(5):\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    save_weight_path = f'/kaggle/working/best_convnext_fold{target_fold}.pth'\n    print(f\"\\n{'='*25} STARTING FOLD {target_fold}/4 {'='*25}\")\n    \n    train_subset = train_df[train_df['fold'] != target_fold]\n    val_subset = train_df[train_df['fold'] == target_fold]\n    \n    train_set = InMemoryKneeDataset(train_subset, is_train=True)\n    val_set = InMemoryKneeDataset(val_subset, is_train=False)\n    \n    # num_workers=0 avoids multiprocessing IPC overhead when data is in RAM\n    train_loader = DataLoader(train_set, batch_size=4, shuffle=True, num_workers=0, pin_memory=True)\n    val_loader = DataLoader(val_set, batch_size=4, shuffle=False, num_workers=0, pin_memory=True)\n    \n    targets_np = train_subset[LABEL_COLS].values\n    pos_counts = targets_np.sum(axis=0)\n    neg_counts = len(targets_np) - pos_counts\n    pos_weights = torch.tensor(np.clip(neg_counts / (pos_counts + 1e-5), 1.0, 15.0), dtype=torch.float32).to(DEVICE)\n    \n    model = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', num_classes=12, pretrained=True, tau=1.0).to(DEVICE)\n    criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)\n    \n    optimizer = torch.optim.AdamW([\n        {'params': model.encoder.parameters(), 'lr': 3e-5, 'weight_decay': 1e-2},\n        {'params': model.plane_emb.parameters(), 'lr': 5e-4, 'weight_decay': 1e-3},\n        {'params': model.slice_head.parameters(), 'lr': 5e-4, 'weight_decay': 1e-3}\n    ])\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=6, eta_min=1e-6)\n    scaler = GradScaler('cuda')\n    \n    best_fold_auc = 0.0\n    \n    for epoch in range(6):\n        model.train()\n        running_loss = 0.0\n        optimizer.zero_grad()\n        \n        pbar = tqdm(train_loader, desc=f\"Fold {target_fold} | Epoch {epoch+1}/6 [Train]\")\n        for step, (images, plane_ids, targets) in enumerate(pbar):\n            images, plane_ids, targets = images.to(DEVICE, non_blocking=True), plane_ids.to(DEVICE, non_blocking=True), targets.to(DEVICE, non_blocking=True)\n            \n            with autocast(device_type='cuda', dtype=torch.float16):\n                logits = model(images, plane_ids)\n                loss = criterion(logits, targets) / 2\n                \n            scaler.scale(loss).backward()\n            \n            if (step + 1) % 2 == 0 or (step + 1) == len(train_loader):\n                scaler.unscale_(optimizer)\n                torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n                scaler.step(optimizer)\n                scaler.update()\n                optimizer.zero_grad()\n                \n            running_loss += loss.item() * 2\n            pbar.set_postfix({'Loss': f\"{running_loss / (step + 1):.4f}\"})\n            \n        scheduler.step()\n        \n        # Fast Validation\n        model.eval()\n        val_preds, val_targets = [], []\n        with torch.no_grad():\n            for images, plane_ids, targets in val_loader:\n                images, plane_ids = images.to(DEVICE, non_blocking=True), plane_ids.to(DEVICE, non_blocking=True)\n                with autocast(device_type='cuda', dtype=torch.float16):\n                    probs = torch.sigmoid(model(images, plane_ids))\n                val_preds.append(probs.cpu().numpy())\n                val_targets.append(targets.numpy())\n                \n        val_preds = np.concatenate(val_preds, axis=0)\n        val_targets = np.concatenate(val_targets, axis=0)\n        \n        aucs = [roc_auc_score(val_targets[:, c], val_preds[:, c]) for c in range(12) if len(np.unique(val_targets[:, c])) > 1]\n        epoch_auc = np.mean(aucs) if aucs else 0.0\n        print(f\"--> Fold {target_fold} Epoch {epoch+1}/6 | Loss: {running_loss/len(train_loader):.4f} | Val AUC: {epoch_auc:.4f}\")\n        \n        if epoch_auc > best_fold_auc:\n            best_fold_auc = epoch_auc\n            torch.save(model.state_dict(), save_weight_path)\n            print(f\"  * Saved Checkpoint: {save_weight_path} (AUC: {best_fold_auc:.4f})\")\n            \n    fold_scores[target_fold] = best_fold_auc\n    print(f\"\\n[DONE] Fold {target_fold} Complete | Peak AUC: {best_fold_auc:.4f}\\n\")\n    \n    del model, optimizer, scheduler, train_loader, val_loader\n    gc.collect()\n    torch.cuda.empty_cache()\n\nprint(\"\\n\" + \"=\"*50)\nprint(f\"5-Fold Training Finished! Mean Out-Of-Fold Macro-AUC: {np.mean(list(fold_scores.values())):.4f}\")\nprint(\"=\"*50)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T07:37:33.807282Z","iopub.execute_input":"2026-08-30T07:37:33.807693Z","iopub.status.idle":"2026-08-30T12:40:35.445075Z","shell.execute_reply.started":"2026-08-30T07:37:33.807665Z","shell.execute_reply":"2026-08-30T12:40:35.444363Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport zipfile\nfrom IPython.display import FileLink, display\n\nzip_filename = '/kaggle/working/rsna_knee_5folds_weights.zip'\n\n# Bundle all checkpoints and fold metadata\nwith zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:\n    for f in os.listdir('/kaggle/working'):\n        if f.endswith('.pth') or f.endswith('.csv'):\n            file_path = os.path.join('/kaggle/working', f)\n            zipf.write(file_path, arcname=f)\n            print(f\"Added to archive: {f} ({os.path.getsize(file_path) / (1024*1024):.2f} MB)\")\n\nprint(\"\\nClick the link below to download the zip file directly:\")\ndisplay(FileLink('rsna_knee_5folds_weights.zip'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T12:42:23.057051Z","iopub.execute_input":"2026-08-30T12:42:23.057327Z","iopub.status.idle":"2026-08-30T12:42:51.901132Z","shell.execute_reply.started":"2026-08-30T12:42:23.057304Z","shell.execute_reply":"2026-08-30T12:42:51.90043Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Files in /kaggle/working/:\")\nprint(os.listdir('/kaggle/working/'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T13:08:23.275026Z","iopub.execute_input":"2026-08-30T13:08:23.275775Z","iopub.status.idle":"2026-08-30T13:08:23.280066Z","shell.execute_reply.started":"2026-08-30T13:08:23.275744Z","shell.execute_reply":"2026-08-30T13:08:23.279297Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom scipy.stats import rankdata\nimport torch\nimport torch.nn as nn\nfrom torch.amp import autocast\nfrom tqdm.auto import tqdm\nimport timm\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nCOMP_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\nif not os.path.exists(COMP_ROOT):\n    COMP_ROOT = '/kaggle/input/rsna-knee-abnormality-detection'\n\nTEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test_series')\nif not os.path.exists(TEST_SERIES_DIR):\n    TEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test')\n\nSAMPLE_SUB_PATH = os.path.join(COMP_ROOT, 'sample_submission.csv')\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 1. Model Architecture\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=False, tau=1.0):\n        super().__init__()\n        self.num_classes = num_classes\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3)\n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        \n        self.slice_head = nn.Sequential(\n            nn.Linear(embed_dim, 256),\n            nn.SiLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x, plane_ids):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        feats = feats + self.plane_emb(plane_ids)\n        slice_logits = self.slice_head(feats)\n        return self.tau * torch.logsumexp(slice_logits / self.tau, dim=1)\n\n# 2. Load All 5 Saved Fold Checkpoints\nmodels = []\nfor f in range(5):\n    weight_path = f'/kaggle/working/best_convnext_fold{f}.pth'\n    if os.path.exists(weight_path):\n        m = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', pretrained=False).to(DEVICE)\n        m.load_state_dict(torch.load(weight_path, map_location=DEVICE))\n        m.eval()\n        models.append(m)\n        print(f\"Loaded: {weight_path}\")\n\nprint(f\"\\nEnsemble active with {len(models)} fold models on {DEVICE}.\")\n\n# 3. Fast DICOM Processing\ndef read_slice(fp):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=False)\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = cv2.resize(arr, (224, 224), interpolation=cv2.INTER_LINEAR)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        arr = np.clip(arr, p1, p99)\n        return (arr - p1) / (p99 - p1) if p99 > p1 else np.zeros_like(arr)\n    except Exception:\n        return np.zeros((224, 224), dtype=np.float32)\n\ndef process_study(uid):\n    spath = os.path.join(TEST_SERIES_DIR, str(uid))\n    if not os.path.exists(spath):\n        return np.zeros((24, 3, 224, 224), dtype=np.float32)\n\n    tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n    for root, _, files in os.walk(spath):\n        dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n        if not dcms:\n            continue\n        try:\n            ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n            iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n            normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n            p_idx = np.argmax(np.abs(normal))\n            plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n            fps = [os.path.join(root, f) for f in dcms]\n            if len(fps) > len(tri_plane[plane]):\n                tri_plane[plane] = fps\n        except Exception:\n            continue\n\n    def get_slabs(flist):\n        if not flist:\n            return np.zeros((8, 3, 224, 224), dtype=np.float32)\n        n = len(flist)\n        idx = np.linspace(0, n - 1, 8, dtype=int) if n >= 8 else np.pad(np.arange(n), (0, 8 - n), mode='edge')\n        slices = [read_slice(flist[i]) for i in idx]\n        return np.stack([\n            np.stack([slices[max(0, i-1)], slices[i], slices[min(len(slices)-1, i+1)]], axis=0)\n            for i in range(len(slices))\n        ], axis=0)\n\n    return np.concatenate([\n        get_slabs(tri_plane['Sagittal']),\n        get_slabs(tri_plane['Coronal']),\n        get_slabs(tri_plane['Axial'])\n    ], axis=0)\n\n# 4. Ensemble Inference Loop\nif os.path.exists(SAMPLE_SUB_PATH):\n    sub_df = pd.read_csv(SAMPLE_SUB_PATH)\n    test_uids = sub_df['StudyInstanceUID'].unique() if 'StudyInstanceUID' in sub_df.columns else sub_df.iloc[:, 0].unique()\nelse:\n    test_uids = [f for f in os.listdir(TEST_SERIES_DIR) if os.path.isdir(os.path.join(TEST_SERIES_DIR, f))]\n    sub_df = pd.DataFrame({'StudyInstanceUID': test_uids})\n\nplane_ids = torch.tensor([[0]*8 + [1]*8 + [2]*8], dtype=torch.long).to(DEVICE)\nall_preds = []\n\nwith torch.no_grad():\n    for uid in tqdm(test_uids, desc=\"5-Fold Ensemble Predicting\"):\n        vol = torch.from_numpy(process_study(uid)).unsqueeze(0).to(DEVICE)\n        vol_flip = torch.flip(vol, dims=[-1])\n        \n        study_preds = []\n        for m in models:\n            with autocast(device_type='cuda', dtype=torch.float16):\n                p1 = torch.sigmoid(m(vol, plane_ids))\n                p2 = torch.sigmoid(m(vol_flip, plane_ids))\n                study_preds.append(((p1 + p2) / 2.0).cpu().numpy()[0])\n        all_preds.append(np.mean(study_preds, axis=0))\n\nall_preds = np.array(all_preds)\n\n# 5. Percentile Rank Calibration & Export\nrank_preds = np.zeros_like(all_preds)\nfor c in range(12):\n    rank_preds[:, c] = (rankdata(all_preds[:, c]) - 1.0) / (len(all_preds) - 1.0 + 1e-8) if len(all_preds) > 1 else all_preds[:, c]\n\nfinal_sub = pd.DataFrame({'StudyInstanceUID': test_uids})\nfor i, col in enumerate(LABEL_COLS):\n    final_sub[col] = rank_preds[:, i]\n\nif os.path.exists(SAMPLE_SUB_PATH):\n    sample = pd.read_csv(SAMPLE_SUB_PATH)\n    id_col = sample.columns[0]\n    final_sub.rename(columns={'StudyInstanceUID': id_col}, inplace=True)\n    cols = [c for c in sample.columns if c in final_sub.columns]\n    final_sub = final_sub[cols]\n\nfinal_sub.to_csv('/kaggle/working/submission.csv', index=False)\nprint(\"\\n\" + \"=\"*50)\nprint(\"SUCCESS: /kaggle/working/submission.csv generated!\")\nprint(\"=\"*50)\nprint(final_sub.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T13:09:34.213997Z","iopub.execute_input":"2026-08-30T13:09:34.214587Z","iopub.status.idle":"2026-08-30T13:09:38.185895Z","shell.execute_reply.started":"2026-08-30T13:09:34.214558Z","shell.execute_reply":"2026-08-30T13:09:38.185129Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport pandas as pd\n\nworking_files = os.listdir('/kaggle/working')\n\nprint(\"=\"*60)\nprint(\"CHECKPOINT & FOLD VERIFICATION\")\nprint(\"=\"*60)\n\n# 1. Check .pth Weight Files\nfor fold in range(5):\n    fname = f'best_convnext_fold{fold}.pth'\n    fpath = os.path.join('/kaggle/working', fname)\n    if fname in working_files:\n        size_mb = os.path.getsize(fpath) / (1024 * 1024)\n        try:\n            ckpt = torch.load(fpath, map_location='cpu')\n            num_keys = len(ckpt.keys())\n            print(f\"[OK] Fold {fold}: {fname} | Size: {size_mb:.2f} MB | State Dict Keys: {num_keys}\")\n        except Exception as e:\n            print(f\"[ERROR] Fold {fold} file corrupted: {e}\")\n    else:\n        print(f\"[MISSING] Fold {fold}: {fname} not found!\")\n\n# 2. Check Archive\nzip_name = 'rsna_knee_5folds_weights.zip'\nif zip_name in working_files:\n    zip_size = os.path.getsize(os.path.join('/kaggle/working', zip_name)) / (1024 * 1024)\n    print(f\"\\n[OK] Archive: {zip_name} | Size: {zip_size:.2f} MB\")\n\n# 3. Check Fold Split Mapping\nif 'master_train_folds.csv' in working_files:\n    df_folds = pd.read_csv('/kaggle/working/master_train_folds.csv')\n    print(f\"\\n[OK] Metadata: master_train_folds.csv | Total Studies: {len(df_folds)}\")\n    print(\"Studies per fold:\")\n    print(df_folds['fold'].value_counts().sort_index())\nprint(\"=\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T13:16:33.397689Z","iopub.execute_input":"2026-08-30T13:16:33.39844Z","iopub.status.idle":"2026-08-30T13:16:33.739642Z","shell.execute_reply.started":"2026-08-30T13:16:33.398412Z","shell.execute_reply":"2026-08-30T13:16:33.738983Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport torch.nn as nn\nimport timm\n\n# 1. Verify GPU\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {DEVICE}\")\n\n# 2. Define Architecture\nclass AttentionMILHead(nn.Module):\n    def __init__(self, in_features, num_classes=12):\n        super().__init__()\n        self.attention = nn.Sequential(\n            nn.Linear(in_features, 128),\n            nn.Tanh(),\n            nn.Linear(128, 1)\n        )\n        self.classifier = nn.Linear(in_features, num_classes)\n\n    def forward(self, feats):\n        weights = torch.softmax(self.attention(feats), dim=1)\n        bag_feat = torch.sum(weights * feats, dim=1)\n        return self.classifier(bag_feat)\n\nclass AttentionMILModel(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3)\n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        self.head = AttentionMILHead(in_features=embed_dim, num_classes=num_classes)\n\n    def forward(self, x, plane_ids):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        feats = feats + self.plane_emb(plane_ids)\n        return self.head(feats)\n\n# 3. Quick Sanity Test\nprint(\"Building and testing model...\")\nmodel = AttentionMILModel(backbone='convnext_tiny', pretrained=True).to(DEVICE)\n\n# Mock input tensor: Batch=2, 24 Slices, 3 Channels, 224x224\ndummy_input = torch.randn(2, 24, 3, 224, 224).to(DEVICE)\nplane_ids = torch.tensor([[0]*8 + [1]*8 + [2]*8] * 2, dtype=torch.long).to(DEVICE)\n\nout = model(dummy_input, plane_ids)\nprint(f\"Forward pass successful! Output shape: {out.shape} (Expected: [2, 12])\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T17:34:27.967559Z","iopub.execute_input":"2026-08-30T17:34:27.968282Z","iopub.status.idle":"2026-08-30T17:34:37.969729Z","shell.execute_reply.started":"2026-08-30T17:34:27.968251Z","shell.execute_reply":"2026-08-30T17:34:37.968671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import roc_auc_score\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.amp import autocast, GradScaler\nimport timm\n\n# 1. Configuration\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nNUM_CLASSES = 12\nEPOCHS = 5\nBATCH_SIZE = 4\nLR = 3e-4\nWEIGHT_DECAY = 1e-4\n\nCOMP_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\nif not os.path.exists(COMP_ROOT):\n    COMP_ROOT = '/kaggle/input/rsna-knee-abnormality-detection'\n\nTRAIN_SERIES_DIR = os.path.join(COMP_ROOT, 'train_series')\nif not os.path.exists(TRAIN_SERIES_DIR):\n    TRAIN_SERIES_DIR = os.path.join(COMP_ROOT, 'train')\n\nTRAIN_CSV = os.path.join(COMP_ROOT, 'train.csv')\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Asymmetric Loss for Multi-Label Imbalance\nclass AsymmetricLoss(nn.Module):\n    def __init__(self, gamma_neg=4, gamma_pos=1, clip=0.05, eps=1e-8):\n        super().__init__()\n        self.gamma_neg = gamma_neg\n        self.gamma_pos = gamma_pos\n        self.clip = clip\n        self.eps = eps\n\n    def forward(self, x, y):\n        xs_pos = torch.sigmoid(x)\n        xs_neg = 1.0 - xs_pos\n\n        if self.clip is not None and self.clip > 0:\n            xs_neg = (xs_neg + self.clip).clamp(max=1)\n\n        los_pos = y * torch.log(xs_pos.clamp(min=self.eps))\n        los_neg = (1 - y) * torch.log(xs_neg.clamp(min=self.eps))\n        loss = los_pos * ((1 - xs_pos) ** self.gamma_pos) + los_neg * ((xs_pos) ** self.gamma_neg)\n        return -loss.sum()\n\n# 3. Model Architecture\nclass AttentionMILHead(nn.Module):\n    def __init__(self, in_features, num_classes=12):\n        super().__init__()\n        self.attention = nn.Sequential(\n            nn.Linear(in_features, 128),\n            nn.Tanh(),\n            nn.Linear(128, 1)\n        )\n        self.classifier = nn.Linear(in_features, num_classes)\n\n    def forward(self, feats):\n        weights = torch.softmax(self.attention(feats), dim=1)\n        bag_feat = torch.sum(weights * feats, dim=1)\n        return self.classifier(bag_feat)\n\nclass AttentionMILModel(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, pretrained=True):\n        super().__init__()\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3)\n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        self.head = AttentionMILHead(in_features=embed_dim, num_classes=num_classes)\n\n    def forward(self, x, plane_ids):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        feats = feats + self.plane_emb(plane_ids)\n        return self.head(feats)\n\n# 4. Dataset Loader\ndef read_slice(fp):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=False)\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = cv2.resize(arr, (224, 224), interpolation=cv2.INTER_LINEAR)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        arr = np.clip(arr, p1, p99)\n        return (arr - p1) / (p99 - p1) if p99 > p1 else np.zeros_like(arr)\n    except Exception:\n        return np.zeros((224, 224), dtype=np.float32)\n\nclass RSNAKneeDataset(Dataset):\n    def __init__(self, df, root_dir, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.root_dir = root_dir\n        self.is_train = is_train\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        uid = str(row['StudyInstanceUID'])\n        spath = os.path.join(self.root_dir, uid)\n        \n        tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n        if os.path.exists(spath):\n            for root, _, files in os.walk(spath):\n                dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n                if not dcms: continue\n                try:\n                    ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n                    iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n                    normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n                    p_idx = np.argmax(np.abs(normal))\n                    plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n                    fps = [os.path.join(root, f) for f in dcms]\n                    if len(fps) > len(tri_plane[plane]):\n                        tri_plane[plane] = fps\n                except Exception:\n                    continue\n\n        def get_slabs(flist):\n            if not flist: return np.zeros((8, 3, 224, 224), dtype=np.float32)\n            n = len(flist)\n            idx = np.linspace(0, n - 1, 8, dtype=int) if n >= 8 else np.pad(np.arange(n), (0, 8 - n), mode='edge')\n            slices = [read_slice(flist[i]) for i in idx]\n            return np.stack([\n                np.stack([slices[max(0, i-1)], slices[i], slices[min(len(slices)-1, i+1)]], axis=0)\n                for i in range(len(slices))\n            ], axis=0)\n\n        vol = np.concatenate([\n            get_slabs(tri_plane['Sagittal']),\n            get_slabs(tri_plane['Coronal']),\n            get_slabs(tri_plane['Axial'])\n        ], axis=0)\n\n        labels = row[LABEL_COLS].values.astype(np.float32) if self.is_train else np.zeros(NUM_CLASSES, dtype=np.float32)\n        return torch.tensor(vol, dtype=torch.float32), torch.tensor(labels, dtype=torch.float32)\n\n# 5. Training Loop\ntrain_df = pd.read_csv(TRAIN_CSV)\nkf = KFold(n_splits=5, shuffle=True, random_state=42)\nplane_ids = torch.tensor([[0]*8 + [1]*8 + [2]*8] * BATCH_SIZE, dtype=torch.long).to(DEVICE)\n\nfor fold, (trn_idx, val_idx) in enumerate(kf.split(train_df)):\n    print(f\"\\n========== FOLD {fold} ==========\")\n    trn_ds = RSNAKneeDataset(train_df.iloc[trn_idx], TRAIN_SERIES_DIR, is_train=True)\n    val_ds = RSNAKneeDataset(train_df.iloc[val_idx], TRAIN_SERIES_DIR, is_train=True)\n    \n    trn_loader = DataLoader(trn_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True, drop_last=True)\n    val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\n    model = AttentionMILModel(backbone='convnext_tiny', pretrained=True).to(DEVICE)\n    optimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY)\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS * len(trn_loader), eta_min=1e-6)\n    criterion = AsymmetricLoss()\n    scaler = GradScaler('cuda')\n    \n    best_auc = 0.0\n\n    for epoch in range(EPOCHS):\n        model.train()\n        trn_loss = 0.0\n        for x, y in tqdm(trn_loader, desc=f\"Epoch {epoch+1}/{EPOCHS} [Train]\"):\n            x, y = x.to(DEVICE), y.to(DEVICE)\n            optimizer.zero_grad()\n            with autocast(device_type='cuda', dtype=torch.float16):\n                logits = model(x, plane_ids[:x.size(0)])\n                loss = criterion(logits, y)\n            \n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n            scheduler.step()\n            trn_loss += loss.item()\n\n        # Validation\n        model.eval()\n        val_preds, val_targets = [], []\n        with torch.no_grad():\n            for x, y in tqdm(val_loader, desc=f\"Epoch {epoch+1}/{EPOCHS} [Val]\"):\n                x = x.to(DEVICE)\n                with autocast(device_type='cuda', dtype=torch.float16):\n                    preds = torch.sigmoid(model(x, plane_ids[:x.size(0)]))\n                val_preds.append(preds.cpu().numpy())\n                val_targets.append(y.numpy())\n\n        val_preds = np.vstack(val_preds)\n        val_targets = np.vstack(val_targets)\n        \n        # Macro-AUC\n        aucs = []\n        for c in range(NUM_CLASSES):\n            if len(np.unique(val_targets[:, c])) > 1:\n                aucs.append(roc_auc_score(val_targets[:, c], val_preds[:, c]))\n        val_auc = np.mean(aucs) if aucs else 0.0\n\n        print(f\"Epoch {epoch+1}: Loss = {trn_loss/len(trn_loader):.4f} | Val Macro-AUC = {val_auc:.4f}\")\n\n        if val_auc > best_auc:\n            best_auc = val_auc\n            save_path = f'/kaggle/working/best_attention_fold{fold}.pth'\n            torch.save(model.state_dict(), save_path)\n            print(f\" Saved new best model checkpoint: {save_path} (AUC: {val_auc:.4f})\")\n\n    del model, optimizer, scheduler, trn_loader, val_loader\n    torch.cuda.empty_cache()\n    gc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T17:35:15.267911Z","iopub.execute_input":"2026-08-30T17:35:15.268767Z","iopub.status.idle":"2026-08-30T17:38:47.917797Z","shell.execute_reply.started":"2026-08-30T17:35:15.268734Z","shell.execute_reply":"2026-08-30T17:38:47.916688Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Quick test of your existing 5-fold weights on real data\nimport torch\nimport numpy as np\n\n# Load one existing model\nmodel = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', pretrained=False).to(DEVICE)\nmodel.load_state_dict(torch.load('/kaggle/input/rsna-knee-5fold-weights/best_convnext_fold0.pth', map_location=DEVICE))\nmodel.eval()\n\n# Run a test batch and print raw predictions\ndummy_x = torch.randn(1, 24, 3, 224, 224).to(DEVICE)\nwith torch.no_grad():\n    raw_pred = torch.sigmoid(model(dummy_x)).cpu().numpy()[0]\n\nprint(\"Model output distribution across 12 targets:\")\nfor col, p in zip(LABEL_COLS, raw_pred):\n    print(f\"  {col:<20}: {p:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T17:38:54.865134Z","iopub.execute_input":"2026-08-30T17:38:54.86563Z","iopub.status.idle":"2026-08-30T17:38:54.874906Z","shell.execute_reply.started":"2026-08-30T17:38:54.865592Z","shell.execute_reply":"2026-08-30T17:38:54.873348Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport torch.nn as nn\nimport timm\n\n# 1. Environment & Labels Setup\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Original Model Architecture Definition\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, tau=1.0, pretrained=False):\n        super().__init__()\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3)\n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        self.classifier = nn.Linear(embed_dim, num_classes)\n\n    def forward(self, x, plane_ids=None):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        if plane_ids is not None:\n            feats = feats + self.plane_emb(plane_ids)\n        logits = self.classifier(feats)\n        return torch.logsumexp(logits / self.tau, dim=1) * self.tau\n\n# 3. Locate and Load Existing Checkpoint\nweight_file = None\nfor root, _, files in os.walk('/kaggle/input'):\n    for f in files:\n        if f.endswith('.pth') and 'rsna-knee-abnormality-detection' not in root:\n            weight_file = os.path.join(root, f)\n            break\n    if weight_file:\n        break\n\nif not weight_file:\n    print(\"Error: No .pth weight file found in /kaggle/input.\")\nelse:\n    print(f\"Loading checkpoint from: {weight_file}\")\n    model = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', pretrained=False).to(DEVICE)\n    model.load_state_dict(torch.load(weight_file, map_location=DEVICE))\n    model.eval()\n\n    # 4. Run Test Inference\n    dummy_x = torch.randn(1, 24, 3, 224, 224).to(DEVICE)\n    dummy_planes = torch.tensor([[0]*8 + [1]*8 + [2]*8], dtype=torch.long).to(DEVICE)\n\n    with torch.no_grad():\n        raw_pred = torch.sigmoid(model(dummy_x, dummy_planes)).cpu().numpy()[0]\n\n    print(\"\\nModel output distribution across 12 targets:\")\n    for col, p in zip(LABEL_COLS, raw_pred):\n        print(f\"  {col:<20}: {p:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T17:40:43.999789Z","iopub.execute_input":"2026-08-30T17:40:44.000056Z","iopub.status.idle":"2026-08-30T17:41:29.475415Z","shell.execute_reply.started":"2026-08-30T17:40:44.000033Z","shell.execute_reply":"2026-08-30T17:41:29.474276Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport torch\nimport torch.nn as nn\nimport timm\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# Model Definition\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, tau=1.0, pretrained=False):\n        super().__init__()\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3)\n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        self.classifier = nn.Linear(embed_dim, num_classes)\n\n    def forward(self, x, plane_ids=None):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        if plane_ids is not None:\n            feats = feats + self.plane_emb(plane_ids)\n        logits = self.classifier(feats)\n        return torch.logsumexp(logits / self.tau, dim=1) * self.tau\n\n# Fast search: Find .pth files in attached datasets (skips competition DICOM folders)\npth_files = []\nfor entry in os.listdir('/kaggle/input'):\n    if 'rsna-knee-abnormality-detection' not in entry:\n        pth_files.extend(glob.glob(f'/kaggle/input/{entry}/**/*.pth', recursive=True))\n\nif not pth_files:\n    # Also check /kaggle/working/ in case files are stored locally\n    pth_files.extend(glob.glob('/kaggle/working/**/*.pth', recursive=True))\n\nif not pth_files:\n    print(\"No .pth files found. Available datasets in /kaggle/input:\")\n    print(os.listdir('/kaggle/input'))\nelse:\n    target_pth = pth_files[0]\n    print(f\"Testing checkpoint: {target_pth}\")\n    \n    model = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', pretrained=False).to(DEVICE)\n    model.load_state_dict(torch.load(target_pth, map_location=DEVICE))\n    model.eval()\n\n    # Pass dummy batch through model\n    dummy_x = torch.randn(1, 24, 3, 224, 224).to(DEVICE)\n    dummy_planes = torch.tensor([[0]*8 + [1]*8 + [2]*8], dtype=torch.long).to(DEVICE)\n\n    with torch.no_grad():\n        raw_pred = torch.sigmoid(model(dummy_x, dummy_planes)).cpu().numpy()[0]\n\n    print(\"\\nModel output distribution across 12 targets:\")\n    for col, p in zip(LABEL_COLS, raw_pred):\n        print(f\"  {col:<20}: {p:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T17:41:36.851888Z","iopub.execute_input":"2026-08-30T17:41:36.85276Z","iopub.status.idle":"2026-08-30T17:44:18.876266Z","shell.execute_reply.started":"2026-08-30T17:41:36.852728Z","shell.execute_reply":"2026-08-30T17:44:18.875428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(os.listdir('/kaggle/input'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T17:47:02.215016Z","iopub.execute_input":"2026-08-30T17:47:02.215816Z","iopub.status.idle":"2026-08-30T17:47:02.22029Z","shell.execute_reply.started":"2026-08-30T17:47:02.215785Z","shell.execute_reply":"2026-08-30T17:47:02.219571Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import glob\n\npth_files = glob.glob('/kaggle/input/**/*.pth', recursive=True)\nprint(f\"Found {len(pth_files)} model files:\")\nfor p in pth_files:\n    print(\" \", p)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T17:58:32.475918Z","iopub.execute_input":"2026-08-30T17:58:32.476585Z","iopub.status.idle":"2026-08-30T18:00:44.823793Z","shell.execute_reply.started":"2026-08-30T17:58:32.476554Z","shell.execute_reply":"2026-08-30T18:00:44.822857Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"Directories inside /kaggle/input:\")\nfor item in os.listdir('/kaggle/input'):\n    item_path = os.path.join('/kaggle/input', item)\n    if os.path.isdir(item_path):\n        print(f\"\\n📁 {item}/\")\n        for sub in os.listdir(item_path)[:10]:\n            print(f\"   └── {sub}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:01:33.683115Z","iopub.execute_input":"2026-08-30T18:01:33.683794Z","iopub.status.idle":"2026-08-30T18:01:33.689597Z","shell.execute_reply.started":"2026-08-30T18:01:33.683756Z","shell.execute_reply":"2026-08-30T18:01:33.68874Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nnotebook_dir = '/kaggle/input/notebooks/vikram8754'\n\nprint(f\"Scanning {notebook_dir}...\\n\")\nfor root, dirs, files in os.walk(notebook_dir):\n    level = root.replace(notebook_dir, '').count(os.sep)\n    indent = ' ' * 4 * level\n    print(f\"{indent}📁 {os.path.basename(root)}/\")\n    subindent = ' ' * 4 * (level + 1)\n    for f in files:\n        size_mb = os.path.getsize(os.path.join(root, f)) / (1024 * 1024)\n        print(f\"{subindent}📄 {f} ({size_mb:.2f} MB)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:02:19.036562Z","iopub.execute_input":"2026-08-30T18:02:19.037285Z","iopub.status.idle":"2026-08-30T18:02:19.045189Z","shell.execute_reply.started":"2026-08-30T18:02:19.037255Z","shell.execute_reply":"2026-08-30T18:02:19.044426Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\ntarget_dir = '/kaggle/input/notebooks/vikram8754/rsna-vikram'\n\nif os.path.exists(target_dir):\n    print(f\"Files inside {target_dir}:\")\n    for root, dirs, files in os.walk(target_dir):\n        for f in files:\n            full_path = os.path.join(root, f)\n            size_mb = os.path.getsize(full_path) / (1024 * 1024)\n            print(f\"  📄 {full_path} ({size_mb:.2f} MB)\")\nelse:\n    print(f\"Directory {target_dir} not found.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:02:56.705405Z","iopub.execute_input":"2026-08-30T18:02:56.706119Z","iopub.status.idle":"2026-08-30T18:02:56.7122Z","shell.execute_reply.started":"2026-08-30T18:02:56.706087Z","shell.execute_reply":"2026-08-30T18:02:56.711336Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import glob\n\npth_files = glob.glob('/kaggle/input/**/*.pth', recursive=True)\nprint(f\"Found {len(pth_files)} model files:\")\nfor p in pth_files:\n    print(\" \", p)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:20:33.141843Z","iopub.execute_input":"2026-08-30T18:20:33.142514Z","iopub.status.idle":"2026-08-30T18:22:44.652279Z","shell.execute_reply.started":"2026-08-30T18:20:33.142483Z","shell.execute_reply":"2026-08-30T18:22:44.651576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport timm\n\n# 1. Environment & Paths Configuration\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nBATCH_SIZE = 2\n\nCOMP_ROOT = '/kaggle/input/rsna-knee-abnormality-detection'\nif not os.path.exists(COMP_ROOT):\n    COMP_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\n\nTEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test_series')\nif not os.path.exists(TEST_SERIES_DIR):\n    TEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test')\n\nSAMPLE_SUB_CSV = os.path.join(COMP_ROOT, 'sample_submission.csv')\nsample_sub = pd.read_csv(SAMPLE_SUB_CSV)\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Model Architecture\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, tau=1.0, pretrained=False):\n        super().__init__()\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3)\n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        self.classifier = nn.Linear(embed_dim, num_classes)\n\n    def forward(self, x, plane_ids=None):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        if plane_ids is not None:\n            feats = feats + self.plane_emb(plane_ids)\n        logits = self.classifier(feats)\n        return torch.logsumexp(logits / self.tau, dim=1) * self.tau\n\n# 3. Fast DICOM Test Loader\ndef read_slice(fp):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=False)\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = cv2.resize(arr, (224, 224), interpolation=cv2.INTER_LINEAR)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        arr = np.clip(arr, p1, p99)\n        return (arr - p1) / (p99 - p1) if p99 > p1 else np.zeros_like(arr)\n    except Exception:\n        return np.zeros((224, 224), dtype=np.float32)\n\nclass RSNATestDataset(Dataset):\n    def __init__(self, df, root_dir):\n        self.df = df.reset_index(drop=True)\n        self.root_dir = root_dir\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        uid = str(self.df.iloc[idx]['StudyInstanceUID'])\n        spath = os.path.join(self.root_dir, uid)\n        \n        tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n        if os.path.exists(spath):\n            for root, _, files in os.walk(spath):\n                dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n                if not dcms: \n                    continue\n                try:\n                    ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n                    iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n                    normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n                    p_idx = np.argmax(np.abs(normal))\n                    plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n                    fps = [os.path.join(root, f) for f in dcms]\n                    if len(fps) > len(tri_plane[plane]):\n                        tri_plane[plane] = fps\n                except Exception:\n                    continue\n\n        def get_slabs(flist):\n            if not flist: \n                return np.zeros((8, 3, 224, 224), dtype=np.float32)\n            n = len(flist)\n            idx = np.linspace(0, n - 1, 8, dtype=int) if n >= 8 else np.pad(np.arange(n), (0, 8 - n), mode='edge')\n            slices = [read_slice(flist[i]) for i in idx]\n            return np.stack([\n                np.stack([slices[max(0, i-1)], slices[i], slices[min(len(slices)-1, i+1)]], axis=0)\n                for i in range(len(slices))\n            ], axis=0)\n\n        vol = np.concatenate([\n            get_slabs(tri_plane['Sagittal']),\n            get_slabs(tri_plane['Coronal']),\n            get_slabs(tri_plane['Axial'])\n        ], axis=0)\n\n        return torch.tensor(vol, dtype=torch.float32)\n\n# 4. Load 5 Models into Memory\nWEIGHT_PATHS = sorted(glob.glob('/kaggle/input/datasets/vikram8754/rsna-knee-weights/best_convnext_fold*.pth'))\nif not WEIGHT_PATHS:\n    WEIGHT_PATHS = sorted(glob.glob('/kaggle/input/datasets/vikram8754/rsna-knee-5fold-weights/best_convnext_fold*.pth'))\n\nprint(f\"Loaded {len(WEIGHT_PATHS)} model weights for 5-Fold Ensembling:\")\nfor p in WEIGHT_PATHS:\n    print(f\"  -> {p}\")\n\nmodels = []\nfor p in WEIGHT_PATHS:\n    m = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', pretrained=False).to(DEVICE)\n    m.load_state_dict(torch.load(p, map_location=DEVICE))\n    m.eval()\n    models.append(m)\n\n# 5. Execute 5-Fold Ensemble Inference\ntest_dataset = RSNATestDataset(sample_sub, TEST_SERIES_DIR)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nplane_ids = torch.tensor([[0]*8 + [1]*8 + [2]*8] * BATCH_SIZE, dtype=torch.long).to(DEVICE)\nall_preds = []\n\nwith torch.no_grad():\n    for x in tqdm(test_loader, desc=\"Ensemble Inference\"):\n        x = x.to(DEVICE)\n        curr_b = x.size(0)\n        p_ids = plane_ids[:curr_b]\n        \n        # Average probability predictions across all 5 models\n        batch_preds = torch.zeros((curr_b, len(LABEL_COLS)), device=DEVICE)\n        for m in models:\n            logits = m(x, p_ids)\n            batch_preds += torch.sigmoid(logits)\n        batch_preds /= len(models)\n        \n        all_preds.append(batch_preds.cpu().numpy())\n\nfinal_preds = np.vstack(all_preds)\n\n# 6. Format and Save Submission File\nsub_df = sample_sub.copy()\nfor i, col in enumerate(LABEL_COLS):\n    if col in sub_df.columns:\n        sub_df[col] = final_preds[:, i]\n\nsub_df.to_csv('submission.csv', index=False)\nprint(\"\\nSubmission generation complete. Validating output format:\\n\")\nprint(sub_df.head())\nprint(f\"\\nTotal rows: {len(sub_df)} | NaNs detected: {sub_df.isna().sum().sum()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:24:38.706045Z","iopub.execute_input":"2026-08-30T18:24:38.706766Z","iopub.status.idle":"2026-08-30T18:24:40.127556Z","shell.execute_reply.started":"2026-08-30T18:24:38.706733Z","shell.execute_reply":"2026-08-30T18:24:40.126431Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport timm\n\n# 1. Environment & Paths Configuration\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nBATCH_SIZE = 2\n\nCOMP_ROOT = '/kaggle/input/rsna-knee-abnormality-detection'\nif not os.path.exists(COMP_ROOT):\n    COMP_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\n\nTEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test_series')\nif not os.path.exists(TEST_SERIES_DIR):\n    TEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test')\n\nSAMPLE_SUB_CSV = os.path.join(COMP_ROOT, 'sample_submission.csv')\nsample_sub = pd.read_csv(SAMPLE_SUB_CSV)\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Exact Matching Model Architecture\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, hidden_dim=256, tau=1.0, pretrained=False):\n        super().__init__()\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3)\n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        \n        # Matches slice_head.0 and slice_head.3 keys from your checkpoint\n        self.slice_head = nn.Sequential(\n            nn.Linear(embed_dim, hidden_dim),\n            nn.GELU(),\n            nn.Dropout(0.2),\n            nn.Linear(hidden_dim, num_classes)\n        )\n\n    def forward(self, x, plane_ids=None):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        if plane_ids is not None:\n            feats = feats + self.plane_emb(plane_ids)\n        logits = self.slice_head(feats)\n        return torch.logsumexp(logits / self.tau, dim=1) * self.tau\n\n# 3. Fast DICOM Test Loader\ndef read_slice(fp):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=False)\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = cv2.resize(arr, (224, 224), interpolation=cv2.INTER_LINEAR)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        arr = np.clip(arr, p1, p99)\n        return (arr - p1) / (p99 - p1) if p99 > p1 else np.zeros_like(arr)\n    except Exception:\n        return np.zeros((224, 224), dtype=np.float32)\n\nclass RSNATestDataset(Dataset):\n    def __init__(self, df, root_dir):\n        self.df = df.reset_index(drop=True)\n        self.root_dir = root_dir\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        uid = str(self.df.iloc[idx]['StudyInstanceUID'])\n        spath = os.path.join(self.root_dir, uid)\n        \n        tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n        if os.path.exists(spath):\n            for root, _, files in os.walk(spath):\n                dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n                if not dcms: \n                    continue\n                try:\n                    ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n                    iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n                    normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n                    p_idx = np.argmax(np.abs(normal))\n                    plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n                    fps = [os.path.join(root, f) for f in dcms]\n                    if len(fps) > len(tri_plane[plane]):\n                        tri_plane[plane] = fps\n                except Exception:\n                    continue\n\n        def get_slabs(flist):\n            if not flist: \n                return np.zeros((8, 3, 224, 224), dtype=np.float32)\n            n = len(flist)\n            idx = np.linspace(0, n - 1, 8, dtype=int) if n >= 8 else np.pad(np.arange(n), (0, 8 - n), mode='edge')\n            slices = [read_slice(flist[i]) for i in idx]\n            return np.stack([\n                np.stack([slices[max(0, i-1)], slices[i], slices[min(len(slices)-1, i+1)]], axis=0)\n                for i in range(len(slices))\n            ], axis=0)\n\n        vol = np.concatenate([\n            get_slabs(tri_plane['Sagittal']),\n            get_slabs(tri_plane['Coronal']),\n            get_slabs(tri_plane['Axial'])\n        ], axis=0)\n\n        return torch.tensor(vol, dtype=torch.float32)\n\n# 4. Auto-Detect Head Shape & Load All 5 Models\nWEIGHT_PATHS = sorted(glob.glob('/kaggle/input/datasets/vikram8754/rsna-knee-weights/best_convnext_fold*.pth'))\nif not WEIGHT_PATHS:\n    WEIGHT_PATHS = sorted(glob.glob('/kaggle/input/datasets/vikram8754/rsna-knee-5fold-weights/best_convnext_fold*.pth'))\n\nprint(f\"Loading {len(WEIGHT_PATHS)} fold checkpoints...\")\n\n# Detect hidden_dim from state_dict keys\nsample_state = torch.load(WEIGHT_PATHS[0], map_location='cpu')\nhidden_dim = sample_state['slice_head.0.weight'].shape[0]\nprint(f\"Detected slice_head hidden dimension: {hidden_dim}\")\n\nmodels = []\nfor p in WEIGHT_PATHS:\n    m = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', hidden_dim=hidden_dim, pretrained=False).to(DEVICE)\n    m.load_state_dict(torch.load(p, map_location=DEVICE))\n    m.eval()\n    models.append(m)\n    print(f\" Successfully loaded: {os.path.basename(p)}\")\n\n# 5. Execute 5-Fold Ensemble Inference\ntest_dataset = RSNATestDataset(sample_sub, TEST_SERIES_DIR)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nplane_ids = torch.tensor([[0]*8 + [1]*8 + [2]*8] * BATCH_SIZE, dtype=torch.long).to(DEVICE)\nall_preds = []\n\nwith torch.no_grad():\n    for x in tqdm(test_loader, desc=\"Ensemble Inference\"):\n        x = x.to(DEVICE)\n        curr_b = x.size(0)\n        p_ids = plane_ids[:curr_b]\n        \n        batch_preds = torch.zeros((curr_b, len(LABEL_COLS)), device=DEVICE)\n        for m in models:\n            logits = m(x, p_ids)\n            batch_preds += torch.sigmoid(logits)\n        batch_preds /= len(models)\n        \n        all_preds.append(batch_preds.cpu().numpy())\n\nfinal_preds = np.vstack(all_preds)\n\n# 6. Format and Save Submission File\nsub_df = sample_sub.copy()\nfor i, col in enumerate(LABEL_COLS):\n    if col in sub_df.columns:\n        sub_df[col] = final_preds[:, i]\n\nsub_df.to_csv('submission.csv', index=False)\nprint(\"\\nSubmission generation complete. Validating output format:\\n\")\nprint(sub_df.head())\nprint(f\"\\nTotal rows: {len(sub_df)} | NaNs detected: {sub_df.isna().sum().sum()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:25:25.670519Z","iopub.execute_input":"2026-08-30T18:25:25.670799Z","iopub.status.idle":"2026-08-30T18:25:35.573009Z","shell.execute_reply.started":"2026-08-30T18:25:25.670777Z","shell.execute_reply":"2026-08-30T18:25:35.572107Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"Directories inside competition root:\")\nfor item in os.listdir(COMP_ROOT):\n    p = os.path.join(COMP_ROOT, item)\n    if os.path.isdir(p):\n        print(f\"  📁 {item}/ (files: {len(os.listdir(p))})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T14:27:53.161969Z","iopub.execute_input":"2026-08-31T14:27:53.162273Z","iopub.status.idle":"2026-08-31T14:27:53.182957Z","shell.execute_reply.started":"2026-08-31T14:27:53.162239Z","shell.execute_reply":"2026-08-31T14:27:53.181761Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport timm\n\n# 1. Environment & Paths Configuration\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nBATCH_SIZE = 2\n\n# Resolve Competition Paths\nCOMP_ROOT = '/kaggle/input/rsna-knee-abnormality-detection'\nif not os.path.exists(COMP_ROOT):\n    COMP_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\n\nTEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test_series')\nif not os.path.exists(TEST_SERIES_DIR):\n    TEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test')\n\nSAMPLE_SUB_CSV = os.path.join(COMP_ROOT, 'sample_submission.csv')\nsample_sub = pd.read_csv(SAMPLE_SUB_CSV)\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Model Architecture\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, hidden_dim=256, tau=1.0, pretrained=False):\n        super().__init__()\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3)\n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        self.slice_head = nn.Sequential(\n            nn.Linear(embed_dim, hidden_dim),\n            nn.GELU(),\n            nn.Dropout(0.2),\n            nn.Linear(hidden_dim, num_classes)\n        )\n\n    def forward(self, x, plane_ids=None):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        if plane_ids is not None:\n            feats = feats + self.plane_emb(plane_ids)\n        logits = self.slice_head(feats)\n        return torch.logsumexp(logits / self.tau, dim=1) * self.tau\n\n# 3. Robust DICOM Reader\ndef read_slice(fp):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=False)\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = cv2.resize(arr, (224, 224), interpolation=cv2.INTER_LINEAR)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        arr = np.clip(arr, p1, p99)\n        return (arr - p1) / (p99 - p1) if p99 > p1 else np.zeros_like(arr)\n    except Exception:\n        return np.zeros((224, 224), dtype=np.float32)\n\nclass RSNATestDataset(Dataset):\n    def __init__(self, df, root_dir):\n        self.df = df.reset_index(drop=True)\n        self.root_dir = root_dir\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        uid = str(self.df.iloc[idx]['StudyInstanceUID'])\n        spath = os.path.join(self.root_dir, uid)\n        \n        tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n        if os.path.exists(spath):\n            for root, _, files in os.walk(spath):\n                dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n                if not dcms: \n                    continue\n                try:\n                    ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n                    iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n                    normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n                    p_idx = np.argmax(np.abs(normal))\n                    plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n                    fps = [os.path.join(root, f) for f in dcms]\n                    if len(fps) > len(tri_plane[plane]):\n                        tri_plane[plane] = fps\n                except Exception:\n                    continue\n\n        def get_slabs(flist):\n            if not flist: \n                return np.zeros((8, 3, 224, 224), dtype=np.float32)\n            n = len(flist)\n            idx = np.linspace(0, n - 1, 8, dtype=int) if n >= 8 else np.pad(np.arange(n), (0, 8 - n), mode='edge')\n            slices = [read_slice(flist[i]) for i in idx]\n            return np.stack([\n                np.stack([slices[max(0, i-1)], slices[i], slices[min(len(slices)-1, i+1)]], axis=0)\n                for i in range(len(slices))\n            ], axis=0)\n\n        vol = np.concatenate([\n            get_slabs(tri_plane['Sagittal']),\n            get_slabs(tri_plane['Coronal']),\n            get_slabs(tri_plane['Axial'])\n        ], axis=0)\n\n        return torch.tensor(vol, dtype=torch.float32)\n\n# 4. Load Models\nWEIGHT_PATHS = sorted(list(set(glob.glob('/kaggle/input/**/best_convnext_fold*.pth', recursive=True))))\nprint(f\"Discovered {len(WEIGHT_PATHS)} unique weight checkpoints.\")\n\nsample_state = torch.load(WEIGHT_PATHS[0], map_location='cpu')\nhidden_dim = sample_state['slice_head.0.weight'].shape[0]\n\nmodels = []\nfor p in WEIGHT_PATHS[:5]:\n    m = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', hidden_dim=hidden_dim, pretrained=False).to(DEVICE)\n    m.load_state_dict(torch.load(p, map_location=DEVICE))\n    m.eval()\n    models.append(m)\n\n# 5. Ensemble Inference\ntest_dataset = RSNATestDataset(sample_sub, TEST_SERIES_DIR)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nplane_ids = torch.tensor([[0]*8 + [1]*8 + [2]*8] * BATCH_SIZE, dtype=torch.long).to(DEVICE)\nall_preds = []\n\nwith torch.no_grad():\n    for x in tqdm(test_loader, desc=\"Ensemble Inference\"):\n        x = x.to(DEVICE)\n        curr_b = x.size(0)\n        p_ids = plane_ids[:curr_b]\n        \n        batch_preds = torch.zeros((curr_b, len(LABEL_COLS)), device=DEVICE)\n        for m in models:\n            logits = m(x, p_ids)\n            batch_preds += torch.sigmoid(logits)\n        batch_preds /= len(models)\n        all_preds.append(batch_preds.cpu().numpy())\n\nfinal_preds = np.vstack(all_preds)\n\n# 6. Save Submission File\nsub_df = sample_sub.copy()\nfor i, col in enumerate(LABEL_COLS):\n    if col in sub_df.columns:\n        sub_df[col] = final_preds[:, i]\n\nsub_df.to_csv('submission.csv', index=False)\nprint(\"Submission saved successfully. Shape:\", sub_df.shape)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport glob\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport timm\n\n# 1. Environment & Paths Configuration\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nBATCH_SIZE = 2\n\n# Resolve Competition Paths\nCOMP_ROOT = '/kaggle/input/rsna-knee-abnormality-detection'\nif not os.path.exists(COMP_ROOT):\n    COMP_ROOT = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\n\nTEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test_series')\nif not os.path.exists(TEST_SERIES_DIR):\n    TEST_SERIES_DIR = os.path.join(COMP_ROOT, 'test')\n\nSAMPLE_SUB_CSV = os.path.join(COMP_ROOT, 'sample_submission.csv')\nsample_sub = pd.read_csv(SAMPLE_SUB_CSV)\n\nLABEL_COLS = [\n    'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus', \n    'Medial OA', 'Lateral OA', 'PF OA', 'Effusion', \n    'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n]\n\n# 2. Model Architecture\nclass SliceLevelSmoothMaxMIL(nn.Module):\n    def __init__(self, backbone='convnext_tiny', num_classes=12, hidden_dim=256, tau=1.0, pretrained=False):\n        super().__init__()\n        self.tau = tau\n        self.encoder = timm.create_model(backbone, pretrained=pretrained, num_classes=0, in_chans=3)\n        embed_dim = self.encoder.num_features\n        self.plane_emb = nn.Embedding(3, embed_dim)\n        self.slice_head = nn.Sequential(\n            nn.Linear(embed_dim, hidden_dim),\n            nn.GELU(),\n            nn.Dropout(0.2),\n            nn.Linear(hidden_dim, num_classes)\n        )\n\n    def forward(self, x, plane_ids=None):\n        B, S, C, H, W = x.shape\n        feats = self.encoder(x.view(B * S, C, H, W)).view(B, S, -1)\n        if plane_ids is not None:\n            feats = feats + self.plane_emb(plane_ids)\n        logits = self.slice_head(feats)\n        return torch.logsumexp(logits / self.tau, dim=1) * self.tau\n\n# 3. Robust DICOM Reader\ndef read_slice(fp):\n    try:\n        ds = pydicom.dcmread(fp, stop_before_pixels=False)\n        arr = ds.pixel_array.astype(np.float32)\n        if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':\n            arr = np.amax(arr) - arr\n        arr = cv2.resize(arr, (224, 224), interpolation=cv2.INTER_LINEAR)\n        p1, p99 = np.percentile(arr, 1), np.percentile(arr, 99)\n        arr = np.clip(arr, p1, p99)\n        return (arr - p1) / (p99 - p1) if p99 > p1 else np.zeros_like(arr)\n    except Exception:\n        return np.zeros((224, 224), dtype=np.float32)\n\nclass RSNATestDataset(Dataset):\n    def __init__(self, df, root_dir):\n        self.df = df.reset_index(drop=True)\n        self.root_dir = root_dir\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        uid = str(self.df.iloc[idx]['StudyInstanceUID'])\n        spath = os.path.join(self.root_dir, uid)\n        \n        tri_plane = {'Sagittal': [], 'Coronal': [], 'Axial': []}\n        if os.path.exists(spath):\n            for root, _, files in os.walk(spath):\n                dcms = sorted([f for f in files if not f.endswith(('.csv', '.txt'))])\n                if not dcms: \n                    continue\n                try:\n                    ds = pydicom.dcmread(os.path.join(root, dcms[0]), stop_before_pixels=True)\n                    iop = [float(v) for v in getattr(ds, 'ImageOrientationPatient', [1, 0, 0, 0, 1, 0])]\n                    normal = np.cross(np.array(iop[:3]), np.array(iop[3:6]))\n                    p_idx = np.argmax(np.abs(normal))\n                    plane = 'Sagittal' if p_idx == 0 else ('Coronal' if p_idx == 1 else 'Axial')\n                    fps = [os.path.join(root, f) for f in dcms]\n                    if len(fps) > len(tri_plane[plane]):\n                        tri_plane[plane] = fps\n                except Exception:\n                    continue\n\n        def get_slabs(flist):\n            if not flist: \n                return np.zeros((8, 3, 224, 224), dtype=np.float32)\n            n = len(flist)\n            idx = np.linspace(0, n - 1, 8, dtype=int) if n >= 8 else np.pad(np.arange(n), (0, 8 - n), mode='edge')\n            slices = [read_slice(flist[i]) for i in idx]\n            return np.stack([\n                np.stack([slices[max(0, i-1)], slices[i], slices[min(len(slices)-1, i+1)]], axis=0)\n                for i in range(len(slices))\n            ], axis=0)\n\n        vol = np.concatenate([\n            get_slabs(tri_plane['Sagittal']),\n            get_slabs(tri_plane['Coronal']),\n            get_slabs(tri_plane['Axial'])\n        ], axis=0)\n\n        return torch.tensor(vol, dtype=torch.float32)\n\n# 4. Load Models\nWEIGHT_PATHS = sorted(list(set(glob.glob('/kaggle/input/**/best_convnext_fold*.pth', recursive=True))))\nprint(f\"Discovered {len(WEIGHT_PATHS)} unique weight checkpoints.\")\n\nsample_state = torch.load(WEIGHT_PATHS[0], map_location='cpu')\nhidden_dim = sample_state['slice_head.0.weight'].shape[0]\n\nmodels = []\nfor p in WEIGHT_PATHS[:5]:\n    m = SliceLevelSmoothMaxMIL(backbone='convnext_tiny', hidden_dim=hidden_dim, pretrained=False).to(DEVICE)\n    m.load_state_dict(torch.load(p, map_location=DEVICE))\n    m.eval()\n    models.append(m)\n\n# 5. Ensemble Inference\ntest_dataset = RSNATestDataset(sample_sub, TEST_SERIES_DIR)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nplane_ids = torch.tensor([[0]*8 + [1]*8 + [2]*8] * BATCH_SIZE, dtype=torch.long).to(DEVICE)\nall_preds = []\n\nwith torch.no_grad():\n    for x in tqdm(test_loader, desc=\"Ensemble Inference\"):\n        x = x.to(DEVICE)\n        curr_b = x.size(0)\n        p_ids = plane_ids[:curr_b]\n        \n        batch_preds = torch.zeros((curr_b, len(LABEL_COLS)), device=DEVICE)\n        for m in models:\n            logits = m(x, p_ids)\n            batch_preds += torch.sigmoid(logits)\n        batch_preds /= len(models)\n        all_preds.append(batch_preds.cpu().numpy())\n\nfinal_preds = np.vstack(all_preds)\n\n# 6. Save Submission File\nsub_df = sample_sub.copy()\nfor i, col in enumerate(LABEL_COLS):\n    if col in sub_df.columns:\n        sub_df[col] = final_preds[:, i]\n\nsub_df.to_csv('submission.csv', index=False)\nprint(\"Submission saved successfully. Shape:\", sub_df.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-01T16:02:53.912283Z","iopub.execute_input":"2026-09-01T16:02:53.913047Z","iopub.status.idle":"2026-09-01T16:03:14.385356Z","shell.execute_reply.started":"2026-09-01T16:02:53.913014Z","shell.execute_reply":"2026-09-01T16:03:14.384157Z"}},"outputs":[],"execution_count":null}]}