Skip to main content
Version: 0.12

Classifiers

Embedding-based classifiers for text, images, and audio. Three families are shipped:

  • Centroid — mean embedding per class, nearest-cosine prediction. Zero hyperparameters, strong baseline.
  • KNN (HNSW) — approximate nearest-neighbour vote. Best when classes have local structure rather than a single global centroid.
  • SetFit — few-shot contrastive fine-tuning of a sentence transformer with an optional LP-FT recipe.

All three share one API surface: a tagged embedder config picks the modality, and the classifier itself is modality-agnostic. Add image or audio data by swapping the embedder; everything else — training, evaluation, persistence, the inference endpoint — stays the same.

:::info Changed in 0.12.1 The classifier and embedder surface is now reachable from every SDK (Godot, Unity, Unreal, Python, Lua) and ships in the per-platform binaries. For consistency with the new image/audio variants, the text-prediction call was renamed: predict(...) is now predict_text(...) (e.g. Unity Classifiers.PredictClassifiers.PredictText, Unreal ClassifierPredictClassifierPredictText, C FFI atelico_classifier_predictatelico_classifier_predict_text). There are no compatibility aliases — if you called the 0.12.0 name, rename it. The image and audio calls (predict_image / predict_audio) are new in this surface. :::

Picking an embedder

Classifiers are configured with a ClassifierEmbedderConfig, a tagged enum over the per-modality embedders shipped by atelico-embed:

VariantEmbedderNotes
Text(EmbedderConfig)Sentence transformers — AllMiniLML6V2, BGEBaseENV15, BGESmallENV15, etc.Default for text
Vision(VisionEmbedderConfig)DINOv2-Small or DINOv2-LargeNew in 0.9 — image classifiers

Mixing modalities in one classifier is not supported — train one classifier per modality.

Dataset format

A single JSONL row carries exactly one of text, image_path, or audio_path, plus a label:

{"text": "US stocks rally on tech earnings", "label": "Business"}
{"image_path": "data/cats/cat_001.jpg", "label": "cat"}
{"image_path": "data/dogs/dog_017.jpg", "label": "dog"}

Rows that mix input fields, or omit all of them, are rejected at load time with an explicit error.

Training (Rust)

Training happens in Rust via the atelico-classifiers crate; the resulting model is then loaded into the engine for serving from any binding. Example (image-modality centroid):

use atelico_classifiers::{
centroid::{CentroidClassifier, CentroidConfig},
data::InMemoryDataset,
embedder_config::ClassifierEmbedderConfig,
};
use atelico_embed::{
vision_embedder::{VisionEmbedderConfig, VisionEmbeddingModel},
EmbedInputOwned,
};
use std::path::PathBuf;

let inputs: Vec<EmbedInputOwned> = vec![
EmbedInputOwned::image(PathBuf::from("data/cats/01.jpg")),
EmbedInputOwned::image(PathBuf::from("data/dogs/01.jpg")),
];
let labels = vec!["cat".into(), "dog".into()];
let dataset = InMemoryDataset::new(inputs, labels);

let cfg = CentroidConfig {
embedder: ClassifierEmbedderConfig::Vision(VisionEmbedderConfig {
model: VisionEmbeddingModel::DINOv2Small,
batch_size: 4,
}),
};
let mut clf = CentroidClassifier::new(cfg, /* assets */ None)?;
clf.train(&dataset, /* batch_size */ 4)?;
clf.save(std::path::Path::new("./models/animals"))?;
# Ok::<_, anyhow::Error>(())

Swap CentroidConfig for KnnConfig or SetFitConfig to use the other classifier types — the call sites stay identical.

Loading and predicting

Every binding exposes the same three steps:

  1. Load the classifier under a logical id — load_classifier(classifier_id, directory), where directory is the folder the trained model was saved to.
  2. Predict by modality — predict_text(id, text, top_k), predict_image(id, image_path, top_k), or predict_audio(id, audio_path, top_k).
  3. Read the result: a top label, its probability, and a top list of the top-k {label, probability} pairs.

The top_k parameter (optional)

top_k controls how many ranked predictions come back in the top list. It is optional:

top_klabel / probabilitytop list
omitted / nullthe single best classall labels, ranked by probability
0the single best classempty (just the top label, no ranking)
N (≥ 1)the single best classthe N highest-probability labels

label and probability always describe the single most likely class, regardless of top_k.

Serving — text input

Once a classifier is saved to disk, load it under an ID and call predict_text.

import json
import atelico

engine = atelico.Engine()
# (Implementation note: classifier loading is currently performed at engine
# startup via the ATELICO_CLASSIFIERS environment variable; programmatic load
# from Python follows the same pattern as other subsystems.)

result = json.loads(engine.classifier_predict_text("sentiment", "I love this!", top_k=3))
print(result["label"], result["probability"])

Serving — image input (DINOv2)

The classifier referenced by model_id must have been trained with a Vision embedder. Calling the image endpoint against a text-only classifier returns an error.

import json
import atelico

engine = atelico.Engine()
result = json.loads(engine.classifier_predict_image(
"animals",
"/abs/path/to/cat.jpg",
top_k=3,
))
print(result["label"], result["probability"])

Serving — audio input

predict_audio classifies a sound clip against a classifier backed by an audio embedder. The result schema and top_k rules are identical to text and image.

:::caution Audio format: WAV / RIFF only The audio classifier decodes WAV / RIFF files only. MP3, FLAC, and other compressed formats are not supported — convert to WAV first. :::

import json
import atelico

engine = atelico.Engine()
result = json.loads(engine.classifier_predict_audio(
"ambience",
"/abs/path/to/clip.wav",
top_k=3,
))
print(result["label"], result["probability"])

Download the embeddings first (gating)

Image and audio classifiers compute their embeddings with a model (e.g. DINOv2-Small for vision) that lives in the on-device cache. Predict calls resolve those weights from the cache only — they never trigger a silent blocking download. If the weights aren't present yet, the call fails (or returns an empty result) so you stay in control of when the download happens.

The contract:

  • Check with is_model_cached(model_id).
  • Fetch with model_download_async(model_id) if it's missing, and wait for it to finish before predicting.

The model id to gate on is the embedder the classifier was trained with, by modality:

ModalityEmbedder model id
Image"dinov2-small" (DINOv2-Small, the default image embedder), "dinov2-large"
Audio"clap" (the CLAP audio embedder)
Textthe text embedder id, e.g. "bge-small-en"

Worked example: a "turn left / turn right" image classifier

A common in-game use is reacting to what's on screen. Train a centroid classifier over DINOv2-Small image embeddings with two labels — left and right — then, at runtime, make sure DINOv2-Small is cached and classify a frame:

import json, atelico
engine = atelico.Engine()

# 1. Gate: make sure the image embedder is on device.
if not engine.is_model_cached("dinov2-small"):
engine.model_download_async("dinov2-small") # await completion in real code

# 2. Load the trained classifier and predict a frame.
engine.load_classifier("steering", "/abs/path/to/models/steering")
result = json.loads(engine.classifier_predict_image("steering", "/abs/path/frame.png", top_k=0))
print(result["label"]) # "left" or "right"

Using top_k=0 here returns just the winning label with an empty top list — all you need when you only act on the top class.

DINOv2 vision embeddings

VisionEmbeddingModel ships two sizes:

VariantHF repoEmbedding dimUse when
DINOv2Smallfacebook/dinov2-small384Default. Fast, ~22M params. Good for general object / scene categories.
DINOv2Largefacebook/dinov2-large1024Higher quality at ~300M params. Worth the cost when classes are visually subtle.

DINOv2 produces strong general-purpose visual features without per-task pre-training, which makes the Centroid classifier a surprisingly capable baseline for image tasks — try it first before reaching for SetFit fine-tuning.

The same VisionEmbedder is also exposed as a standalone embedder via atelico-embed if you only need raw image vectors (e.g. for similarity search, clustering, or feeding the Hybrid Search store).

Persistence and serving

Trained classifiers persist to disk (safetensors for SetFit, JSON for centroid / KNN) and are loaded into the engine via the ATELICO_CLASSIFIERS environment variable on startup, or programmatically through the SDK and bindings shown above.

The same classifier infrastructure also powers the Guardrails ML-classifier layer for content moderation.