Medical VLM toolkit: inference, confidence estimation, hallucination scoring (HEDGE/VASE), and calibration (incl. HAC)
Project description
MedVLM: Inference, Confidence, and Calibration in Medical VQA
Project Page | Documentation | Installation | Quick Start | Examples | Models & Datasets | Citation
Installation
pip install -e .
Requirements: Python >= 3.10, PyTorch >= 2.6, CUDA GPU recommended.
For GPU inference with Flash Attention (recommended):
pip install flash-attn --no-build-isolation --no-deps
Quick Start
import medvlm
# 1. Load data (auto-downloads from HuggingFace)
dataset = medvlm.load_dataset("vqa_rad", split="test", question_type="closed")
# 2. Load model
model, processor, config = medvlm.load_model("qwen3vl_2b")
# 3. Compute confidence (sampling-based, N=20)
results = medvlm.compute_confidence(
model, processor, config, dataset,
method="sampling", num_samples=20,
)
# 4. Evaluate calibration
report = medvlm.evaluate_calibration(
[r.is_correct for r in results],
[r.confidence for r in results],
)
print(f"ECE: {report['ece']:.4f}, Accuracy: {report['accuracy']:.4f}")
Post-hoc calibration
from medvlm import CalibrationPipeline, train_val_test_split
val_set, test_set = train_val_test_split(dataset, val_fraction=0.3)
# ... compute confidence on both splits ...
cal = CalibrationPipeline(method="platt")
cal.fit(val_conf, val_corr)
calibrated = cal.transform(test_conf)
Hallucination scores (SE / RadFlag / VASE)
Compute per-question hallucination scores via the full HEDGE pipeline (greedy + $N$ high-temperature samples + $N$ distorted-image samples, clustered; powered by hedge-bench). Useful on their own (for ranking or filtering questions by uncertainty) or as the $h$ input to HAC calibration below.
hedge = medvlm.compute_hedge_scores(
model, processor, config, dataset,
question_type="closed", # "closed" -> yes/no clustering, "open" -> embedding
n_samples=10,
alpha=1.0, # VASE contrastive weight
)
h = np.array([r.VASE for r in hedge]) # or r.SE, r.RadFlag
Each HedgeResult also carries the raw samples and their log-likelihoods,
so you can re-cluster or try a different score later without re-generating.
HAC calibration (with hallucination scores)
cal = CalibrationPipeline(method="hac_platt")
cal.fit(val_conf, val_corr, hallucination_scores=val_h)
calibrated = cal.transform(test_conf, hallucination_scores=test_h)
Custom data
compute_confidence and compute_hedge_scores accept any iterable of dicts
with image, question, and optional answer --- you don't need to go
through load_dataset:
examples = [
{"image": pil_image, "question": "Is there a fracture?", "answer": "yes"},
{"image": pil_image_2, "question": "What organ is shown?"}, # answer optional
]
results = medvlm.compute_confidence(
model, processor, config, examples,
method="sampling", num_samples=20,
)
hedge = medvlm.compute_hedge_scores(
model, processor, config, examples,
question_type="closed", n_samples=10,
)
If answer is omitted, ground_truth and is_correct on each result are None.
For a val/test split on a plain list, split it yourself --- train_val_test_split
is a thin wrapper around HuggingFace Dataset methods:
import random
random.Random(42).shuffle(examples)
k = int(0.3 * len(examples))
val_set, test_set = examples[:k], examples[k:]
Examples
Three self-contained scripts that reproduce the paper's main findings:
| Script | What it shows | GPU time |
|---|---|---|
overconfidence.py |
VLMs are overconfident | ~1 min (2B) |
calibration.py |
Post-hoc calibration reduces ECE | ~2 min (2B) |
hac.py |
HAC improves AUROC | ~2 min (2B) |
CUDA_VISIBLE_DEVICES=0 python examples/overconfidence.py
CUDA_VISIBLE_DEVICES=0 python examples/calibration.py
CUDA_VISIBLE_DEVICES=0 python examples/hac.py --model qwen3vl_8b
Confidence Methods
| Method | Description | API |
|---|---|---|
| Sampling | Generate N answers, confidence = P(majority) | method="sampling", num_samples=20 |
| Verbalized | Model self-reports confidence (0--100%) | method="verbalized", variant="vanilla" |
Verbalized variants: vanilla, vanilla_cot, punish, top_k, two_stage, linguistic.
Calibration Methods
Standard (confidence only)
| Method | Description |
|---|---|
temperature_scaling |
Scale logits by learned temperature T |
platt |
Logistic regression on $\mathrm{logit}(c) = \log(c/(1-c))$ (textbook Platt) |
platt_confidence |
Logistic regression on raw confidence $c$: $\sigma(a \cdot c + b)$ |
isotonic |
Non-parametric monotonic mapping |
histogram_binning |
Bin-wise accuracy replacement |
HAC (confidence + hallucination scores)
| Method | Formula | Key idea |
|---|---|---|
hac_platt |
$\sigma(a \cdot \mathrm{logit}(c) + b \cdot h + d)$ | Hallucination lowers confidence (b <= 0) |
hac_platt_confidence |
$\sigma(a \cdot c + b \cdot h + d)$ | Same, but feeds raw $c$ instead of $\mathrm{logit}(c)$ |
hac_gate |
$c \cdot \sigma(-a \cdot h + b)$ | Hallucination gates (attenuates) confidence |
plattvsplatt_confidence: by default we feed $\mathrm{logit}(c) = \log(c/(1-c))$ into Platt scaling and HAC-Platt --- the "textbook" Platt input. The_confidencevariants feed the raw confidence $c$ instead. On our benchmarks (5-fold CV across 3 datasets × 8 models × 2 question types = 240 folds/method) the two are nearly interchangeable; the logit-input default is marginally better with sampling-based confidence, while raw $c$ is marginally better with verbalized confidence on ECE:
Conf source Method ECE ↓ ACE ↓ AUROC ↑ Sampling platt_confidence.093 .179 .653 Sampling platt.084 .178 .653 Sampling hac_platt_confidence.106 .174 .685 Sampling hac_platt.103 .175 .688 Verbalized platt_confidence.063 .178 .606 Verbalized platt.069 .181 .606 Verbalized hac_platt_confidence.101 .178 .671 Verbalized hac_platt.100 .180 .671
Models & Datasets
Models
| Key | Model | Parameters |
|---|---|---|
qwen3vl_2b |
Qwen/Qwen3-VL-2B-Instruct | 2B |
qwen3vl_8b |
Qwen/Qwen3-VL-8B-Instruct | 8B |
qwen3vl_32b |
Qwen/Qwen3-VL-32B-Instruct | 32B |
internvl3_2b |
OpenGVLab/InternVL3-2B-hf | 2B |
internvl3_8b |
OpenGVLab/InternVL3-8B-hf | 8B |
internvl3_38b |
OpenGVLab/InternVL3-38B-hf | 38B |
llava_next_7b |
llava-hf/llava-v1.6-mistral-7b-hf | 7B |
llava_next_34b |
llava-hf/llava-v1.6-34b-hf | 34B |
model, processor, config = medvlm.load_model("qwen3vl_8b")
model, processor, config = medvlm.load_model("qwen3vl_8b", quantization="8bit")
Datasets
| Dataset | Key | Questions | Source |
|---|---|---|---|
| VQA-RAD | vqa_rad |
451 | auto-download (HF) |
| SLAKE | slake |
1,061 | auto-download (HF) |
| VQA-Med-2019 | vqa_med_2019 |
500 | manual download |
| VQA-Med-2020 | vqa_med_2020 |
500 | manual download |
| VQA-Med-2021 | vqa_med_2021 |
500 | manual download |
dataset = medvlm.load_dataset("vqa_rad", split="test", question_type="closed")
Manual download (VQA-Med)
The VQA-Med datasets are hosted on GitHub and must be downloaded manually.
All three years ship the test images as a .zip inside the repo, so you
need to unzip after cloning. Example for VQA-Med-2019:
git clone https://github.com/abachaa/VQA-Med-2019.git data/vqa_med_2019
unzip -q data/vqa_med_2019/VQAMed2019Test/VQAMed2019_Test_Images.zip \
-d data/vqa_med_2019/VQAMed2019Test/
Then point load_dataset at the test directory:
dataset = medvlm.load_dataset(
"vqa_med_2019",
data_path="data/vqa_med_2019/VQAMed2019Test",
)
For the expected directory layout of all three VQA-Med years, see
data/README.md or the Data & Models docs page.
Pipeline Scripts
For running full-scale experiments across all models and datasets:
experiments/01_generate_sampling.py # Sampling confidence (GPU)
experiments/02_generate_verbalized.py # Verbalized confidence (GPU)
experiments/03_generate_hedge.py # HEDGE/VASE scores (GPU)
experiments/04_postprocess.py # LLM judge + clustering (GPU)
experiments/05_export_raw_table.py # Export CSV (CPU)
Citation
@article{byun2026overconfidence,
title={Overconfidence and Calibration in Medical VQA: Empirical Findings and Hallucination-Aware Mitigation},
author={Byun, Ji Young and Park, Young-Jin and Corbeil, Jean-Philippe and Ben Abacha, Asma},
journal={arXiv preprint arXiv:2604.02543},
year={2026},
url={https://arxiv.org/abs/2604.02543},
}
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file medvlm-0.1.0.tar.gz.
File metadata
- Download URL: medvlm-0.1.0.tar.gz
- Upload date:
- Size: 49.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4b6c0647ceb932017ea3e8dbb369522283200a11cbb746626f296297fee9aea1
|
|
| MD5 |
dd2f7d7d4eb7846f4c29a61d3c956fcc
|
|
| BLAKE2b-256 |
da59e53481781582c80289bd2b7a4dde7b5a72437a53449078ce15c54b373be2
|
File details
Details for the file medvlm-0.1.0-py3-none-any.whl.
File metadata
- Download URL: medvlm-0.1.0-py3-none-any.whl
- Upload date:
- Size: 50.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
148747df682d15075008a82b0dca7d3f97df411668fd7b05f8f4371bc6c28d46
|
|
| MD5 |
e4af38ef2fd577a84ebe09e11b8aa397
|
|
| BLAKE2b-256 |
0f0cdbf03f4439a9d80c727a5943cecee390b4edff83235e2301bd74f43e7c5b
|