Memory-Distilled Selection (MeDS)

June 17, 2026 · View on GitHub

Official implementation of the ICML 2026 paper "Memory-Distilled Selection for Noise-Robust Anomaly Detection". (arxiv).

MeDS is a training framework that makes unsupervised anomaly detection robust to contaminated training data — i.e. training sets that contain an unknown fraction of anomalous samples. It is not a single model; it is a three-stage training algorithm that can be applied on top of existing AD baselines.

This repository provides MeDS applied to two strong baselines:

ModelFolderBackboneDecoder
Noisy Dinomalydinomaly/DINOv2 ViT-S/B/L (+ many other ViTs)linear-attention bottleneck
Noisy INP-Formerinpformer/DINOv2 ViT-S/B/Linformative-patch prototypes

Reported MVTec-AD I-AUROC at 40% noise: 99.16% (Dinomaly+MeDS) and 99.17% (INP-Former+MeDS) — state-of-the-art on MVTec-AD, VisA, and Real-IAD under noisy settings. See the paper, Table 1.


The MeDS pipeline

MeDS executes three stages in order:

            ┌───────────────────┐    ┌──────────────────┐    ┌──────────────────┐
   train →  │ 1. Bootstrap      │ →  │ 2. Score         │ →  │ 3. Progressive   │
   data     │    memory         │    │    distillation  │    │    fine-tuning   │
            │    ensemble       │    │  (init s_θ)      │    │  on clean subset │
            └───────────────────┘    └──────────────────┘    └──────────────────┘
                low-pass filter        early-learning bias       self-selection

Stage 1 — Bootstrap memory ensemble. Build B = 100 sparse memory banks by randomly subsampling patch features from a frozen ViT encoder. Sparse subsampling acts as a low-pass filter that separates normal from anomalous patches (paper §4.1, Theorem 1).

Stage 2 — Memory-score distillation. Train a small reconstruction-score network s_θ to predict the memory-bank anomaly score. The early-learning bias of neural nets sharpens the normal/anomaly boundary, fixing the limit imposed by the frozen encoder (paper §4.2).

Stage 3 — Fine-tune with progressive selection. Iteratively fine-tune s_θ on a self-selected clean subset S_t, growing the trusted set as the model improves (MAD-based thresholding, paper §4.3). This yields fine-grained pixel localization without overfitting to anomalous samples.

The result: stable performance across all noise ratios (0%, 10%, 20%, 40%) with no noise-ratio-specific hyperparameter tuning.


Repository layout

MeDS/
├── README.md                  # this file — framework overview
├── LICENSE                    # MIT
├── requirements.txt           # combined deps for both models (loose pins)
├── install_packages.sh        # exact pins for reproducing paper numbers
├── Dockerfile                 # nvidia/cuda:11.8 + miniconda + pinned deps
├── release.sh                 # docker build + run wrapper
├── .gitignore

├── shared/                    # infrastructure used by both models
│   ├── dinov1/                # DINOv1 ViT
│   ├── dinov2/                # DINOv2 ViT (default backbone)
│   ├── beit/                  # BEiT ViT
│   ├── flops_profiler/        # FLOPs accounting
│   └── optimizers/            # StableAdamW, RAdam, AdaBelief, ...

├── dinomaly/                  # Noisy Dinomaly + MeDS
│   ├── README.md
│   ├── _meds_paths.py         # adds ../shared to sys.path
│   ├── dataset.py             # MVTec / VisA / Real-IAD datasets
│   ├── utils.py               # losses, anomaly maps, ader_evaluator (GPU metrics)
│   ├── models/uad.py          # ViTill, ViTillv2, ViTAD, ReContrast
│   ├── step_1_memory_score_generation.py
│   ├── step_2_distillation.py
│   ├── step_3_data_selection_with_distilled_model.py
│   ├── step_3_data_selection_with_memory_score.py
│   ├── real_iad/              # Real-IAD variants
│   ├── data/                  # data preparation utilities + noisy-CSV files
│   └── scripts/run_step_*.sh

└── inpformer/                 # Noisy INP-Former + MeDS
    ├── README.md
    ├── _meds_paths.py
    ├── dataset.py             # MVTec / Real-IAD datasets
    ├── utils.py               # ader_evaluator (GPU metrics)
    ├── aug_funcs.py           # rotation / flip / grey augmentations
    ├── models/uad.py          # INP_Former (informative-patch prototypes)
    ├── step_1_memory_score_generation.py
    ├── step_2_distillation.py
    ├── step_3_data_selection_with_distilled_model.py
    ├── step_3_data_selection_with_memory_score.py
    ├── inp_former_multi_class*.py    # multi-class baseline variants
    ├── baseline_original.py          # non-MeDS reference run
    └── scripts/run_step_*.sh

Why shared/ and _meds_paths.py?

Dinov1/Dinov2/BEiT backbones and optimizers are byte-identical between the two models, so they live once in shared/. Each model script imports _meds_paths as its first line; that helper prepends MeDS/shared/ to sys.path, so existing imports like from dinov1.utils import trunc_normal_ and from optimizers import StableAdamW work unchanged.

Optimized metric backend

Both models compute pixel/image AUROC, AP, F1, and AUPRO through a single GPU-accelerated function — ader_evaluator — backed by the adeval library. The Dinomaly side originally used a CPU sklearn + custom compute_pro path; it now calls ader_evaluator by default and falls back to the CPU path if adeval is not installed. To force the CPU path:

metrics = evaluation_batch(model, loader, device, use_adeval=False)

Installation

You have three options, in increasing order of reproducibility:

Option A — pip install -r requirements.txt (loose pins)

git clone <this-repo> MeDS
cd MeDS
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

Good for quick experimentation against whatever versions of PyTorch / sklearn / etc. happen to resolve.

Option B — install_packages.sh (exact pins, host install)

Reproduces the exact environment the paper numbers were obtained with: PyTorch 2.1.2 + CUDA 11.8, plus pinned numpy / sklearn / timm / opencv versions.

bash install_packages.sh

CUDA 11.8 + a recent NVIDIA driver must already be available on the host. See install_packages.sh for the full list of pins.

Provides a self-contained nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04 environment with Miniconda + all pinned dependencies pre-installed. Build & launch:

bash release.sh

release.sh builds the image (tag meds:local by default), removes any previous container of the same name, and launches an interactive shell with:

FlagWhy
--gpus allGPU passthrough (required for training and the GPU evaluator)
--shm-size=16gDataLoader workers need shared memory beyond the 64 MB default
--ipc=hostNCCL / shared-tensor IPC across worker processes
-v $DATA_HOST:/dataHost dataset root (MVTec-AD, VisA, Real-IAD, noisy variants)
-v $OUTPUT_HOST:/outputsHost output root (memory scores, checkpoints, metrics)
-v $CODE_HOST:/workspace/MeDSLive source mount — edits don't need a rebuild

Override defaults via env vars before running, e.g.:

IMAGE_NAME=my/meds:dev \
DATA_HOST=/mnt/datasets \
OUTPUT_HOST=/mnt/runs/meds \
bash release.sh

The container drops you in /workspace/MeDS. From there you can run the per-model pipeline (cd dinomaly && bash scripts/run_step_1.sh, etc.) — see the per-model READMEs.

adeval

adeval is optional but recommended — it makes pixel-level evaluation ~10× faster. It is installed by install_packages.sh and the Dockerfile. On hosts without it, evaluation_batch automatically falls back to a CPU sklearn + compute_pro path.

CUDA-enabled PyTorch (≥1.12) is required for the GPU evaluator and for training in any reasonable time.


Datasets

DatasetSourceUsed for
MVTec-ADhttps://www.mvtec.com/company/research/datasets/mvtec-adboth models, primary benchmark
VisAhttps://github.com/amazon-science/spot-diffboth models
Real-IADhttps://realiad4ad.github.io/Real-IAD/Dinomaly (scripts in dinomaly/real_iad/)

Noisy dataset construction

To inject label noise into MVTec-AD, use the pre-computed CSV files in dinomaly/data/MVTech/ (or generate your own):

cd MeDS/dinomaly
python data/create_noisy_dataset.py \
    --original_dataset /path/to/mvtec_anomaly_detection \
    --csv_file        data/MVTech/train_mvtec_nr10_seed_0.csv \
    --output_dir      /path/to/MVTec_nr10_seed_0

CSV naming: train_mvtec_nr{ratio}_seed_{seed}.csv with nr ∈ {0, 10, 20, 40} and seed ∈ {0, 10, 20, 30}. nr0 is the original clean dataset.


Quick start

Each model has identical step semantics; pick a folder and run the three stages in order.

cd MeDS/dinomaly            # or MeDS/inpformer
bash scripts/run_step_1.sh           # bootstrap memory ensemble → memory scores
bash scripts/run_step_2.sh           # distill memory scores → init s_θ
bash scripts/run_step_3_distilled.sh # progressive selection + fine-tune

# Alternative for stage 3: select using raw memory scores instead of the distilled model
bash scripts/run_step_3_memory.sh

See the per-model READMEs for argument-level documentation: