Skip to main content
Version: 0.13

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.Predict → Classifiers.PredictText, Unreal ClassifierPredict → ClassifierPredictText, C FFI atelico_classifier_predict → atelico_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.