BiomedCLIP ViT-B/16 PubMedBERT-256 (ONNX, fp32 + fp16 bundle)
ONNX exports of
microsoft/BiomedCLIP-PubMedBERT_256-vit_base_patch16_224 — Microsoft's CLIP-style joint image+text embedder tuned for biomedical figures. Trained on
PMC-15M, a corpus of 15 million biomedical image-caption pairs scraped from PubMed Central papers (radiology, histology, microscopy, charts, gross pathology, illustrations). The biomedical counterpart to OpenAI CLIP.
This repo bundles both fp32 and fp16 precisions in one download — distribution symmetry, shared tokenizer files. Pick a precision via the .onnx filename in the onnx/ subdir.
Re-exported from upstream because the upstream repo only ships the open_clip checkpoint (open_clip_pytorch_model.bin + open_clip_config.json) — no ONNX, and optimum-cli has no exporter for open_clip-style architectures.
Credit: Sheng Zhang, Yanbo Xu, Naoto Usuyama, Hanwen Xu, Jaspreet Bagga, Robert Tinn, Sam Preston, Rajesh Rao, Mu Wei, Naveen Valluri, Cliff Wong, Andrea Tupini, Yu Gu, Mei-Hwa Chen, Hoifung Poon (Microsoft Research). Paper:
"BiomedCLIP: a multimodal biomedical foundation model pretrained from fifteen million scientific image-text pairs", 2023.
How it was produced
- Load
microsoft/BiomedCLIP-PubMedBERT_256-vit_base_patch16_224 via open_clip.create_model_and_transforms("hf-hub:microsoft/BiomedCLIP-...") (the upstream is an open_clip checkpoint, not a transformers.CLIPModel).
- Wrap the vision and text branches in thin
nn.Module adapters that call model.encode_image(..., normalize=False) / model.encode_text(..., normalize=False) — we want the raw 512-d projections; L2-normalisation happens at the call site.
torch.onnx.export each wrapper at opset 17 with do_constant_folding=True. Vision input shape [batch, 3, 224, 224] Float32; text input shape [batch, ≤256] Int64 (the _256 in the upstream repo name is the model's hard position-embedding limit).
onnxconverter_common.float16.convert_float_to_float16(..., keep_io_types=True) casts each fp32 graph in place to produce the _fp16.onnx sibling. keep_io_types=True is the load-bearing flag: wire-boundary tensors stay Float32 so the runtime feeds the same input tensors regardless of variant — only internal weights and activations run in half precision.
- Materialise the PubMedBERT tokenizer assets via
AutoTokenizer.from_pretrained("microsoft/BiomedNLP-PubMedBERT-base-uncased-abstract").save_pretrained(...). BiomedCLIP itself ships no tokenizer files; open_clip resolves them at runtime from that PubMedBERT repo. Bundling them here lets the engine load by path without hitting the hub.
Toolchain:
torch 2.4.x (CPU wheel — we never run inference at export time),
open_clip_torch >= 2.24,
transformers >= 4.40,
onnx >= 1.16,
onnxconverter-common >= 1.14. Full conversion script:
scripts/export-biomedclip.ps1 in the Heliosoph repo.
What this repo contains
BiomedCLIP is two-tower contrastive, so the export splits into two ONNX files per precision plus the shared tokenizer at the root.
onnx/ subdir — precision-specific files
| File | Variant | Size | Role |
|---|
vision_model.onnx | fp32 | ~340 MB | ViT-B/16 vision encoder + 512-d projection head |
vision_model_fp16.onnx | fp16 | ~170 MB | Half-precision ViT-B/16 vision encoder |
text_model.onnx | fp32 | ~430 MB | PubMedBERT text encoder + 512-d projection head |
text_model_fp16.onnx | fp16 | ~220 MB | Half-precision PubMedBERT text encoder |
Root — shared tokenizer files
| File | Role |
|---|
vocab.txt | PubMedBERT WordPiece vocabulary (biomedical, one wordpiece per line) |
tokenizer.json | Fast-tokenizer JSON form (unified tokenizer with pre-tokenizer + model) |
tokenizer_config.json | Tokenizer metadata (lowercase, max length, BERT special tokens) |
special_tokens_map.json | [CLS], [SEP], [PAD], [UNK], [MASK] mapping |
The tokenizer files are mirrored from
microsoft/BiomedNLP-PubMedBERT-base-uncased-abstract — BiomedCLIP itself ships no tokenizer assets, and
open_clip resolves them at runtime from that PubMedBERT repo. Bundling them here lets the engine load by path without hitting the hub.
Input / output (both variants)
| Tower | Input | Output |
|---|
| Vision | pixel_values — NCHW Float32 (yes, even for the fp16 variant — IO types are kept fp32), preprocessed RGB image at 224×224 | image_embeds — [batch, 512] Float32 (unnormalised; project to the unit sphere yourself) |
| Text | input_ids — [batch, ≤256] Int64 WordPiece token IDs with [CLS] / [SEP] wrapping | text_embeds — [batch, 512] Float32 (unnormalised) |
The text tower was exported with a single ONNX input (input_ids only — no attention_mask / token_type_ids). The position-embedding table has exactly 256 rows; sequences longer than 256 will index out of bounds inside the model and must be truncated by the caller. No padding is needed at the wire boundary; the runtime sends the actual token count per row and lets the model attend across the whole real sequence.
Image preprocessing inherits OpenAI CLIP's stats unchanged:
- Resize + center crop to 224×224
- RGB normalize with
mean=[0.48145466, 0.4578275, 0.40821073], std=[0.26862954, 0.26130258, 0.27577711]
How to use
Independent vision and text sessions; L2-normalise the outputs, then dot product equals cosine similarity:
1import onnxruntime as ort
2import numpy as np
3from PIL import Image
4from torchvision import transforms
5from transformers import AutoTokenizer
6
7# Pick a precision — same runtime code either way thanks to keep_io_types.
8PRECISION_SUFFIX = "" # "" for fp32, "_fp16" for fp16
9
10tok = AutoTokenizer.from_pretrained(".")
11vision_sess = ort.InferenceSession(f"onnx/vision_model{PRECISION_SUFFIX}.onnx")
12text_sess = ort.InferenceSession(f"onnx/text_model{PRECISION_SUFFIX}.onnx")
13
14# --- Image embedding ---
15preprocess = transforms.Compose([
16 transforms.Resize(224, interpolation=transforms.InterpolationMode.BICUBIC),
17 transforms.CenterCrop(224),
18 transforms.ToTensor(),
19 transforms.Normalize(
20 mean=[0.48145466, 0.4578275, 0.40821073],
21 std =[0.26862954, 0.26130258, 0.27577711]),
22])
23img = preprocess(Image.open("chest_xray.png").convert("RGB")).unsqueeze(0).numpy()
24image_embeds = vision_sess.run(None, {"pixel_values": img.astype(np.float32)})[0]
25image_embeds /= np.linalg.norm(image_embeds, axis=-1, keepdims=True)
26
27# --- Text embedding ---
28labels = [
29 "a chest X-ray showing pneumonia",
30 "a chest X-ray showing pneumothorax",
31 "a chest X-ray of a healthy patient",
32]
33text_embeds = []
34for label in labels:
35 ids = tok(label, return_tensors="np")["input_ids"].astype(np.int64)
36 # Truncate to the model's 256-token context if needed.
37 ids = ids[:, :256]
38 emb = text_sess.run(None, {"input_ids": ids})[0]
39 text_embeds.append(emb[0])
40text_embeds = np.stack(text_embeds)
41text_embeds /= np.linalg.norm(text_embeds, axis=-1, keepdims=True)
42
43# --- Zero-shot classification ---
44scores = (image_embeds @ text_embeds.T).flatten() # cosine similarity
45print(dict(zip(labels, scores)))
Which precision should I use?
- fp32 — full precision, identical numerics to the upstream
open_clip PyTorch reference. Default for retrieval-quality benchmarks and accuracy-sensitive medical-AI evaluation.
- fp16 — ~half the disk footprint (~390 MB vs ~770 MB total) and half the model-load memory. On GPU / NPU with native fp16: modest speedup (typically 1.5-2× over fp32 on consumer GPUs). On CPU runtimes that upcast fp16 → fp32 internally, runtime speed is identical to fp32 but you save the memory.
The keep_io_types=True setting means switching between them is a single file-path change — no code changes needed.
What it's good for
Zero-shot biomedical image classification (give a list of candidate diagnostic descriptions, rank them by similarity to the image), figure retrieval across PMC-scale corpora, cross-modal search ("show me histology slides described like X"), de-duplication of biomedical figures, and as a frozen feature extractor for downstream task-specific heads on small labeled datasets.
Not a diagnostic tool. Zero-shot scores reflect caption-vs-image similarity in the training distribution; clinical decisions need validated, regulated systems. The authors call this out explicitly in the upstream model card.
Related variants (not in this repo)
Microsoft has not published a small / large variant of BiomedCLIP at time of writing — this is the only public BiomedCLIP cut. For OpenAI's general-domain counterpart see
openai/clip-vit-base-patch32 (the Heliosoph catalog ships an ONNX clone of it as
clip-vit-base-patch32).
License
MIT — same as upstream
microsoft/BiomedCLIP-PubMedBERT_256-vit_base_patch16_224.
LICENSE file included.
open_clip +
torch.onnx export + fp16 conversion are numerical transformations only — no relicensing implication. The bundled PubMedBERT tokenizer files are also MIT-licensed by Microsoft Research.