Skip to content

Add image_features_indices: declared base64 image columns become DINOv3 features - #1260

Closed
alanprior wants to merge 6 commits into
mainfrom
alan/transform-image
Closed

alanprior wants to merge 6 commits into
mainfrom
alan/transform-image

Conversation

@alanprior

@alanprior alanprior commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

What & why

People keep asking for TabPFN on images plus tabular fields, medical imaging in particular. This adds the recipe of the MulTaBench paper (https://arxiv.org/abs/2605.10616), where TabPFN on frozen DINOv3 embeddings beats the alternatives on image-tabular tasks: an image sits in a DataFrame cell as a base64 string (or bytes), the user names the column in image_features_indices, and TabPFN swaps the column for a small block of numeric features before validation. Nothing is detected.

How it works

  • preprocessing/images.py::ImageTransformer mirrors TextTransformer. base.expand_images runs first, then expand_dates and expand_text, each returning the expanded input, its fitted transformer and the declared categorical positions in the expanded input. Each declared image column is decoded, embedded by a frozen DINOv3 ViT-S/16 (CLS token, 384 dims, batches of 64, no grad), standardised and reduced by a PCA fit on the training rows, to min(IMAGE_N_COMPONENTS, n_rows, 384) features named <column>_img_<i>.
  • The encoder lives in a module-level cache keyed by model id and is never stored on the transformer, so save_fitted_tabpfn_model files stay small; estimator_to_device re-points the transformer's device on load.
  • A missing or undecodable cell is refused by column and row, at fit and at predict; a zero row would mean "the mean training image". A changed embedding width at predict is refused, as is an array after an expanding fit.
  • Estimators expose image_transformer_; categorical_features_indices_ now gives the declared positions after image, date and text expansion; tuning clones fit on the already-embedded array. Differentiable input rejects declared image columns. The fine-tuning estimators do not run it.

How to use

pip install "tabpfn[image]"   # transformers, torchvision, pillow
TabPFNClassifier(image_features_indices=[3])

The default encoder's weights are gated: accept the license at https://huggingface.co/facebook/dinov3-vits16-pretrain-lvd1689m and run hf auth login once, or point inference_config={"IMAGE_ENCODER_MODEL": ...} at an ungated encoder.

examples/tabpfn_with_images.py runs three MulTaBench tasks (Amazon Bestseller by default, Glaucoma SMDG and Hateful Meme selectable) with tabular-only, picture-only and combined models. The default run takes about two minutes on an A100 and gives R2 0.164 / 0.569 / 0.633 for the three.

Design decisions

  • Declared only, never sniffed: an undeclared base64 column is left alone and none of the optional dependencies is imported.
  • TRANSFORM_IMAGE defaults to True since it only acts on declared columns; off, declared columns are refused with a message naming the flag, like dates.
  • Optional extra rather than a hard dependency; torchvision is in it because transformers ships DINOv3 with a fast image processor only.
  • No L2 normalisation and no fine-tuning: the paper's frozen protocol.
  • Tests run against a stub encoder so CI downloads nothing; one slow test uses the real encoder and skips without the extra or a Hub token.

Deferred follow-ups

  • File paths or URLs as cells (only base64 and bytes for now).
  • Larger encoders or a fine-tuned head.

🤖 Generated with Claude Code

alanprior and others added 5 commits September 10, 2026 19:59
A DataFrame column named in `image_features_indices` holds one image per
cell, as a base64 string or bytes. The transformer embeds each image with
a frozen DINOv3 ViT-S/16 (CLS token), standardises and reduces the
embeddings with a PCA fit on the training rows, and replaces the column
by those components. Nothing is detected: an undeclared column is left
alone and none of the optional dependencies is imported. Missing or
undecodable cells are refused by column and row, at fit and at predict.
The encoder is cached per process and never stored on the transformer,
so saved estimators stay small. Requires the `tabpfn[image]` extra.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`TRANSFORM_IMAGE` is on by default and acts only on declared columns;
off, declared columns are refused. `IMAGE_N_COMPONENTS` sets the number
of PCA components per image column and `IMAGE_ENCODER_MODEL` the Hub id
of the encoder, so an ungated model can stand in for DINOv3. Also adds
the validator for `image_features_indices`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Images are expanded first, before dates and text, in
`expand_images_dates_and_text`; declared categorical positions are
shifted accordingly. Fitted estimators expose `image_transformer_`,
which runs at every predict site, in `get_embeddings` and in the batched
predictors, and follows the estimator's device on load. Tuning clones
fit on the already-embedded array. Differentiable input rejects declared
image columns. Tests use a stub encoder so CI downloads nothing, plus one
slow test against the real encoder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`examples/tabpfn_with_images.py` predicts an Amazon best seller's price
from its photo and ratings by default; Glaucoma SMDG and Hateful Meme are
selectable. It fits tabular-only, picture-only and combined models and
prints each score.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@alanprior alanprior changed the title Alan/transform image Add image_features_indices: declared base64 image columns become DINOv3 features Sep 10, 2026
An unbounded direct dependency makes the lowest-direct resolution lock
its oldest release: pillow 6.2.0 has no wheel for Python 3.10 and fails
to build when matplotlib pulls it in. torchvision 0.20 pairs with the
package's torch floor, pillow 10.4 ships wheels for every supported
platform.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 795573a. Configure here.

Comment thread changelog/1260.added.md
@@ -0,0 +1 @@
`image_features_indices` declares DataFrame columns that hold one image per cell, as a base64 string or bytes. Each such column is replaced by up to `IMAGE_N_COMPONENTS` numeric features (30 by default): the CLS embedding of a frozen DINOv3 ViT-S/16, standardised and reduced by a PCA fit on the training rows. Nothing is detected: without a declaration nothing changes and none of the optional dependencies is imported. `TRANSFORM_IMAGE` is on by default; off, declared image columns are refused. A missing or undecodable cell is refused by column and row, at fit and at predict. Requires `pip install "tabpfn[image]"` and accepting the encoder's license on the Hugging Face Hub; `IMAGE_ENCODER_MODEL` swaps in another encoder. Fitted estimators expose `image_transformer_`, and `categorical_features_indices_` now gives the declared categorical positions after image, date and text expansion. Not run by the fine-tuning estimators.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changelog fragment exceeds user-facing scope

Low Severity

The towncrier fragment is a long implementation write-up rather than a one- or two-sentence user note. It names fitted internals such as image_transformer_ and categorical_features_indices_, and retells encoder, PCA, refusal, extra, and fine-tuning details that belong in the PR body, not the concatenated user changelog.

Fix in Cursor Fix in Web

Triggered by learned rule: Towncrier fragments: user-facing change only; keep concise

Reviewed by Cursor Bugbot for commit 795573a. Configure here.

@alanprior

Copy link
Copy Markdown
Contributor Author

We will reimplement this in TabPFN-Extensions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant