YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
- RSNA Knee MRI — Model Architecture
- 1. Pipeline overview
- 2. Input & the 12 output labels
- 3. Data preparation (before the model sees anything)
- 4. DICOM preprocessing → 2.5D clip
- 5. Backbone: ConvNeXtV2-Tiny
- 6. Label-Aware Multi-Series Attention
- 7. Label Token Transformer
- 8. Vectorized Label Heads
- 9. Loss function
- 10. Training setup
- 11. Files in this repo
- 1. Pipeline overview
RSNA Knee MRI — Model Architecture
1. Pipeline overview
DICOM series (multiple per study)
│
▼
2.5D clip (prev, center, next slice) ← each series is cut into 3-channel clips
│
▼
ConvNeXtV2-Tiny (shared backbone) ← ALL clips in the batch pass through ONCE
│
▼
+ Metadata embedding ← Anatomical_Plane / Fluid_Sensitive / Fat_Suppression
│
▼
Label-Aware Multi-Series Attention ← 12 learned queries (1 per label), attend over clips
│
▼
12 label tokens → Label Token Transformer ← labels exchange information with each other
│
▼
Vectorized Label Heads (12 heads in parallel)
│
▼
12 probabilities (sigmoid), each in [0, 1]
Mermaid version (renders in viewers that support it):
flowchart TD
A[DICOM series x N] --> B["2.5D clip sampler (prev/center/next)"]
B --> C["ConvNeXtV2-Tiny backbone (shared, runs once for the whole batch)"]
C --> D["Projection: Linear -> LayerNorm -> GELU -> Dropout (embed_dim=384)"]
D --> E["Label-Aware Series Attention (12 learned per-label queries)"]
F["Metadata: Anatomical_Plane / Fluid_Sensitive / Fat_Suppression"] --> E
G["Soft priors: PLANE_PRIOR / FLUID_PRIOR / FAT_PRIOR (additive bias, not a hard filter)"] --> E
E --> H["Label Token Transformer (2 layers, 8 heads)"]
H --> I["Vectorized Label Heads (12 parallel MLPs)"]
I --> J["12 sigmoid probabilities"]
2. Input & the 12 output labels
Each study (one patient / one scan) contains multiple series (Sagittal / Coronal / Axial, with or without fat suppression, fluid-sensitive sequences, etc.). The model predicts 12 independent probabilities:
| # | Label | Meaning |
|---|---|---|
| 1 | ACL | Anterior cruciate ligament |
| 2 | MCL | Medial collateral ligament |
| 3 | Medial Meniscus | Medial meniscus tear |
| 4 | Lateral Meniscus | Lateral meniscus tear |
| 5 | Medial OA | Medial compartment osteoarthritis |
| 6 | Lateral OA | Lateral compartment osteoarthritis |
| 7 | PF OA | Patellofemoral osteoarthritis |
| 8 | Effusion | Joint effusion |
| 9 | Synovitis | Synovial inflammation |
| 10 | Baker's | Baker's (popliteal) cyst |
| 11 | Contusion | Bone contusion / bone marrow edema |
| 12 | Fracture | Fracture |
Missing labels (NaN) are never treated as 0 — they are masked out of the loss everywhere (both during training and when merging manual labels).
3. Data preparation (before the model sees anything)
train_series.csvis many-to-one withtrain.csv(1 Study → many Series), so it's split intotrain_series_level.csv(1 row per series) andtrain_study_level.csv(1 row per study, used for training).- Manual labels (a manual-review CSV, merged by
StudyInstanceUID) override the official labels wherever the value is valid (0/1); invalid or missing values become NaN and stay missing — never inferred. - Data is split with GroupKFold on
StudyInstanceUID— never split by slice or by series, to avoid leakage between train/val. - Series metadata is aggregated up to the study level: series counts per plane, fluid-sensitive/fat-suppression counts and ratios, and counts of important plane×fluid×fat combinations (e.g. Sagittal + Fluid + Fat).
4. DICOM preprocessing → 2.5D clip
For each DICOM slice:
- Read the pixel array, apply
RescaleSlope/RescaleIntercept. - Invert if
PhotometricInterpretation == MONOCHROME1. - Percentile windowing (default 1st–99th percentile) → normalize to
[0, 1]. - Resize to
img_size(default 224×224). - Per-slice z-score (mean/std of that individual slice), then clip to
[-5, 5].
Slices within a series are sorted by their real physical position
(ImagePositionPatient projected onto the slice-plane normal), not just
InstanceNumber (which is only used as a fallback when position data is
missing).
A 2.5D clip = 3 consecutive slices [prev, center, next] sampled around
a center slice → a [3, H, W] tensor, treated as a 3-channel image by a 2D
backbone (no 3D convolutions involved).
Horizontal flip augmentation is deliberately not used, because flipping left/right would change medial/lateral semantics (medial vs. lateral meniscus, etc.) — this is an intentional design constraint, not an omission.
Series selection when a study has more series than max_series
(default 8): the selection preserves diversity — the best series per plane
first, then the best series per unmet (fluid, fat) combination, and the
remaining slots filled by a score (log(n_slices) plus bonuses for a valid
plane / fluid / fat). This is purely an input-filtering step, not part
of the model's backward pass.
5. Backbone: ConvNeXtV2-Tiny
- Model:
convnextv2_tiny.fcmae_ft_in22k_in1k(viatimm), ImageNet pretrained,global_pool="avg", original classifier removed (num_classes=0). - Every clip from every series and every study in a batch is concatenated
into one large tensor and passed through the backbone exactly once
(a key GPU optimization) — per-study separation only happens later, at
the attention step, using
offsetsto slice the batch back apart. - Gradient checkpointing is enabled to save VRAM.
- After the backbone:
Linear → LayerNorm → GELU → Dropoutprojects down toembed_dim(default 384) — this is the feature vector for a single clip.
6. Label-Aware Multi-Series Attention
This is the core of the architecture — instead of average-pooling across series, each of the 12 labels gets its own learned query vector, which learns which series matter for that specific finding.
Query:
[12, embed_dim], learned (one query per label).Key/Value: linear projections of the clip features, after adding a metadata embedding (plane embedding + fluid/fat linear projections) — so attention is aware of which plane a clip belongs to, and whether it's fluid-sensitive/fat-suppressed.
Additive bias on the attention scores, for every (label, clip) pair, made of two parts:
Domain-knowledge soft priors (fixed constants, not learned) — e.g. ACL is best seen on Sagittal, PF OA is best seen on Axial:
Label Sagittal Coronal Axial ACL 1.00 0.65 0.40 MCL 0.65 1.00 0.45 Medial/Lateral Meniscus 0.90–1.00 0.90–1.00 0.60–0.65 Medial/Lateral OA 0.85–0.90 1.00 0.40 PF OA 0.90 0.55 1.00 Effusion / Synovitis 0.90 0.85 0.90 Baker's 0.95 0.70 1.00 Contusion / Fracture 1.00 0.90–0.95 0.80 (there are equivalent
FLUID_PRIORandFAT_SUPPRESSION_PRIORtables — one coefficient per label indicating how much fluid-sensitive / fat-suppressed sequences help detect it.)Learned bias (
plane_bias,fluid_bias,fat_bias) — lets the model deviate from the prior when the actual data suggests otherwise.
→ The priors are only an additive suggestion (soft bias), NOT a hard filter: the model can still learn to go against the prior when needed.
Output: for each study, 12
label_featvectors (one per label), each a weighted (attention-based) combination of every clip in that study, with weights depending on both the image content and the plane/sequence of that clip.
7. Label Token Transformer
- A learned per-label embedding (
label_embedding) is added tolabel_feat. - These 12 "label tokens" pass through a small
TransformerEncoder(default 2 layers, 8 heads, pre-norm, GELU) — the purpose is to let labels exchange information with each other before independent classification (e.g. joint effusion and synovitis tend to co-occur, an ACL tear is often accompanied by bone marrow edema, etc.).
8. Vectorized Label Heads
- Functionally equivalent to 12 independent MLP heads
(
LayerNorm → Linear → GELU → Dropout → Linear), but implemented as batched 3D tensors +einsuminstead of 12 separate modules in aModuleList— faster, with less Python overhead, and mathematically identical to running 12 separate heads. - Output: 12 logits (pre-sigmoid) →
sigmoidproduces the final 12 probabilities in[0, 1].
9. Loss function
- Robust Asymmetric Loss (ASL), masked for missing (NaN) labels:
- Each label has its own
gamma_neg(label-specific negative focusing) — rare/hard labels (e.g. Fracture, Baker's) get penalized more heavily for false negatives. label_weights(computed from prevalence:1/√prevalence, clipped to[1, 3], normalized by the mean) rebalance rare vs. common labels.
- Each label has its own
- Pairwise ranking loss: for each label, positive/negative score pairs within a batch are compared, penalizing cases where a positive score isn't higher than a negative one — this complements ASL and improves the ranking quality (useful for AUC).
- Total loss =
0.85 × ASL + 0.15 × ranking loss.
10. Training setup
| Component | Default value |
|---|---|
| Optimizer | AdamW, backbone LR 2e-5, head LR 1e-4, weight decay 1e-4 |
| Scheduler | Cosine with warmup (1 epoch) |
| Epochs | 15 |
| Batch size | 4 (gradient accumulation ×2 → effective batch size 8) |
| Mixed precision | AMP (enabled by default) |
| EMA | decay 0.9997, EMA weights used for validation/inference |
| Grad clip | 1.0 |
| K-fold | GroupKFold on StudyInstanceUID, 5 folds |
| DICOM cache | mmap + uint8 (quantized from [-5,5] → [0,255]), written incrementally per series |
11. Files in this repo
| File | Role |
|---|---|
| Original training script | Prepares the data, builds the DICOM cache, trains + validates across K-folds, saves best_model.pth / last_checkpoint.pth, and produces submission.csv in Kaggle's required format. |
infer_folder.py |
A standalone inference script — takes any folder of DICOM series (no Kaggle-specific structure required), auto-discovers studies/series, auto-infers Anatomical_Plane/Fluid_Sensitive/Fat_Suppression from DICOM headers (can be overridden with a metadata CSV), loads a trained checkpoint, runs the exact architecture described above, and writes the 12 label probabilities to a CSV. Configured by editing plain variables in the "USER CONFIG" block at the top of the file — no CLI needed. |
infer_folder.py reuses exactly the architecture described in sections
5–8 (a 1:1 copy of the LabelAwareSeriesAttention, LabelTokenTransformer,
VectorizedLabelHeads, and RSNAKneeModel classes) to guarantee
compatibility with trained checkpoints — it only differs in how metadata is
obtained (inferred from DICOM headers instead of read from
train_series.csv/test_series.csv) and drops the multi-process DICOM
caching layer, which isn't needed when testing just a handful of studies.