# embetter: pretrained encoders as scikit-learn pipeline steps

> embetter turns a pile of pretrained text, image and multimodal encoders into scikit-learn compatible transformer steps, so a proof of concept becomes a make_pipeline call rather than a script that loads a model and remembers to normalise it. The library covers sentence transformers, timm vision encoders, CLIP, external API providers and a set of finetuning components, and it is explicit that the API is still being implemented.

**koaning/embetter** — just a bunch of useful embeddings for scikit-learn pipelines

- Repository: https://github.com/koaning/embetter
- Website: https://koaning.github.io/embetter/
- Stars: 530 · Forks: 18
- Language: Python
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/koaning-embetter

## A bag of encoders with one interface

embetter is a library of scikit-learn compatible embeddings for computer vision and text, and its stated purpose is to make it easy to build proof of concepts with scikit-learn pipelines, in particular for bulk labelling. The project description is deliberately informal: a bunch of useful embeddings to get started quickly.

The idea is that a pretrained encoder is the part of a machine learning script that is most often written badly. You load a checkpoint, you have to remember the right preprocessing, you have to normalise the vectors if you intend to compare them, and you have to make sure the batch size and the device handling work. Wrapping that in a scikit-learn transformer means the encoder becomes one element of a pipeline, with fit and transform, so the same object can be fitted, saved, cross validated and reused.

The scope is wider than text. The library covers text encoders, computer vision encoders, encoders from multimodal models, finetuning components and external providers that need an API key, and it is intended to play nicely with the author's other projects, bulk for vector search and scikit-partial for partial fitting, and to work with an approximate nearest neighbour store such as lancedb.

The project is by Vincent D. Warmerdam, the licence is MIT, the documentation site is at koaning.github.io/embetter, and the release history is steady: 0.7.0 in March 2025, 0.8.0 in August 2025 and 0.9.1 on 2026-09-14, which is also the date of the last push. The repository is not archived.

## One install, then only the extras you actually need

The base install is a single pip command:

```bash
python -m pip install embetter
```

The base package is deliberately light. The runtime dependencies listed in the project metadata are scikit-learn 1.0.0 or newer, pandas 1.0.0 or newer, diskcache 5.6.1 or newer, skops 0.8.0 or newer, and model2vec. There is no deep learning framework in that list, which is the point of the extras.

The heavy dependencies are optional and are selected by extra, so a project only pays for what it uses:

```bash
python -m pip install "embetter[text]"
python -m pip install "embetter[vision]"
python -m pip install "embetter[all]"
```

The full set of extras in the metadata goes further than those three. sbert and text both pull in sentence-transformers 2.2.2 or newer, vision pulls in timm 0.6.7 or newer, pytorch pulls in torch 1.12.0 or newer, and the provider extras pull in openai 1.59.8 or newer, cohere 4.11.2 or newer and ollama 0.5.3 or newer. The all extra is the union of sentence-transformers, timm and openai.

That structure is worth understanding for a proof of concept, because the naive thing to do is install everything and end up with a torch download in an environment that only needed a colour histogram. The text example in the documentation asks for the sbert extra specifically, and the vision and provider paths have their own.

## The module layout, and the statelessness caveat

The API is organised into modules by the kind of thing they do, and the documentation marks it as what is being implemented now, which is a signal that it moves.

embetter.grab holds helpers to grab text or an image from a pandas column, with ColumnGrabber as the entry point. embetter.vision holds representations and helpers for computer vision, with ImageLoader, TimmEncoder and ColorHistogramEncoder. embetter.text holds text representations, with SentenceEncoder, MatryoshkaEncoder and TextEncoder. embetter.multi holds representations from multimodal models, with ClipEncoder. embetter.finetune holds the finetuning components, with FeedForwardTuner, ContrastiveTuner, ContrastiveLearner and SbertLearner. embetter.external holds external embedding providers that typically need an API key, with CohereEncoder and OpenAIEncoder.

The property that makes all of these usable in a pipeline is stated plainly: they are scikit-learn compatible, so you apply them as you would normally in a scikit-learn pipeline, and they are stateless, which means they do not require training because they are all pretrained tools.

Statelessness is the concept to hold on to, because it cuts both ways. It is what lets a pipeline be fitted, pickled and reused without a training step for the encoder, and it is also what means there is nowhere to put a fine-tuned model inside the same object. The finetuning components exist for that, and they are separate from the encoders, so a project that wants fine-tuned vectors is combining two things rather than configuring one.

## A text pipeline in four lines of configuration

The text example is the shape of the whole library. You grab a column, you wrap an encoder, and the pipeline does the rest:

```python
import pandas as pd
from sklearn.pipeline import make_pipeline 
from sklearn.linear_model import LogisticRegression

from embetter.grab import ColumnGrabber
from embetter.text import SentenceEncoder

text_emb_pipeline = make_pipeline(
  ColumnGrabber("text"),
  SentenceEncoder('all-MiniLM-L6-v2')
)
```

The pipeline grabs the text column from a dataframe and feeds it into the all-MiniLM-L6-v2 model from sentence-transformers. The dataframe in the example has a text column and a label column, and the embedded features are produced by calling fit_transform on the pipeline with the dataframe and the labels.

Adding a classifier is one more element, and the example then fits and predicts:

```python
text_clf_pipeline = make_pipeline(
  ColumnGrabber("text"),
  SentenceEncoder('all-MiniLM-L6-v2'),
  LogisticRegression()
)
```

The point of the design is visible here. The logistic regression never sees raw strings, and the encoder never sees a label, so swapping the encoder for a different model, or the classifier for a different estimator, is a one line change. It also means the pipeline is a single object that can be cross validated and, because skops is a dependency, serialised through a format that scikit-learn itself can read.

## Images work the same way, with a loader in between

The image example follows the same pattern, with one extra step to turn a path into an image:

```python
import pandas as pd
from sklearn.pipeline import make_pipeline 
from embetter.grab import ColumnGrabber
from embetter.vision import ImageLoader
from embetter.multi import ClipEncoder

image_emb_pipeline = make_pipeline(
  ColumnGrabber("img_path"),
  ImageLoader(convert="RGB"),
  ClipEncoder()
)
```

The chain reads in order. ColumnGrabber pulls the img_path column, ImageLoader turns the paths into PIL.Image objects and converts them to RGB, and ClipEncoder embeds them. CLIP is in the multi module rather than the vision module because it handles both images and text, which is what makes it the natural choice when a dataset mixes modalities.

The conversion step is not a detail. Loading an image without forcing a colour mode gives you an array that varies with the source file, and a model that expects three channels will fail on some of them, so the convert argument in the loader is the setting that keeps a batch consistent.

For a case where you want a cheap, dependency-free representation, the vision module also carries ColorHistogramEncoder, which needs no model checkpoint at all, and the timm based TimmEncoder for the vision backbone zoo. That is the option to reach for when a colour histogram is enough to separate classes and you do not want a torch install.

## Out-of-core data through partial fit, and the packaging details

All the encoding tools are also compatible with the partial_fit mechanic in scikit-learn, which is what lets a pipeline handle a dataset that does not fit in memory, and the documentation pairs it with the scikit-partial project for exactly that purpose. For a bulk labelling setup, where the vectors are computed in chunks and labelled later, that is the property that decides whether the approach is usable.

The packaging is a plain setuptools project, not a compiled one. The build system requires setuptools 68.0 or newer and wheel, the project metadata declares Python 3.9 or newer, and the licence file is LICENCE with that spelling, referenced from the metadata as the file that carries the MIT terms.

The developer workflow is uv based, which shows up in the Makefile. The install target creates a virtual environment and installs the project in editable mode with the dev extra. The test target runs pytest with -n auto for parallel execution and -vv for verbose output. The lint target runs ruff check over the package and tests with --fix, and the ruff configuration sets a line length of 160 and selects the E, F and W rule sets while ignoring the line length and complexity rules. The release targets build and publish with uv, and the documentation targets copy the README into the docs directory and run mkdocs, with the docs extra pinning mkdocs-material, mkdocstrings and mktestdocs, the last of which checks that the code examples in the documentation actually run.

The dev extra also pulls in interrogate, which measures docstring coverage, and the repository has a datasets/ directory, so the test suite is not only unit tests. The presence of the datasets directory and the mktestdocs dependency says the project tests its own examples, which is the kind of check that keeps a documentation-heavy library honest.

## Conclusion

embetter fits a Python team that wants to try an embedding model inside an existing scikit-learn pipeline, particularly one doing bulk labelling, where the win is that the encoder, the column grabber and the downstream classifier live in one pipeline object that can be fitted, serialised and re-run. It is the wrong tool if you need stateful or fine-tuned embeddings in production, because the components are stateless and the finetuning components are separate, and it is the wrong tool if you need stability, since the documentation says the API is what is being implemented now and the version numbers move without a deprecation policy. Before you build on it, pin the version you tested with, install only the extra you need so that a proof of concept does not drag in torch, and remember that the external encoders send your data to a provider's API.

## FAQ

### What does embetter do and which encoders does it include?

embetter provides scikit-learn compatible embeddings for text, computer vision and multimodal models, so pretrained encoders can be used as steps in a scikit-learn pipeline. The text module has SentenceEncoder, MatryoshkaEncoder and TextEncoder, the vision module has ImageLoader, TimmEncoder and ColorHistogramEncoder, the multi module has ClipEncoder, and there are also finetuning components and external providers such as CohereEncoder and OpenAIEncoder.

### How do I install embetter without pulling in a deep learning framework?

The base install is python -m pip install embetter, which only needs scikit-learn, pandas, diskcache, skops and model2vec. The heavier dependencies are extras, with pip install "embetter[text]" for sentence-transformers and pip install "embetter[vision]" for timm, and there are also pytorch, openai, cohere and ollama extras for the corresponding paths.

### How do I build a text embedding pipeline with embetter?

Use make_pipeline with a ColumnGrabber for the column, a SentenceEncoder with the model name, and optionally a classifier such as LogisticRegression as the last step. The example in the documentation uses the all-MiniLM-L6-v2 model from sentence-transformers and calls fit_transform on the dataframe and its labels.

### Can embetter handle datasets that do not fit in memory?

Yes. All the encoding tools are compatible with the partial_fit mechanic in scikit-learn, and the documentation points at scikit-partial for building pipelines that handle out-of-core datasets, which is the case for bulk labelling setups where vectors are computed in chunks.

## Sources

- [koaning/embetter on GitHub](https://github.com/koaning/embetter)
- [License: MIT](https://github.com/koaning/embetter/blob/main/LICENSE)
- [Project website](https://koaning.github.io/embetter/)
- [README](https://github.com/koaning/embetter/blob/main/README.md)
- [Releases](https://github.com/koaning/embetter/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/koaning-embetter
