Sampling¶
Sampling helps you select a smaller, more useful subset from a large dataset. Typical goals are deciding what to label first, removing redundant data before training, or building a balanced review set. LightlyStudio's strategies use embeddings (and, where relevant, your metadata or annotations) to pick diverse, representative, or otherwise optimized subsets.
The rest of this page covers which strategy to pick, what each one does, and how to run a sampling in the GUI or in Python.
Choosing a strategy¶
Each strategy optimizes for a different goal. Start from what you are trying to achieve:
| I want to... | Use |
|---|---|
| Pick a diverse subset that covers the whole dataset | Diverse |
| Keep a diverse subset that still reflects the real distribution | Diverse + typicality |
| Find rare or unusual samples, such as edge cases, anomalies, or potentially mislabeled data | Outliers |
| Clean up a dataset by removing near-duplicate images | Deduplication |
| Rank images by a number I have and keep the top ones (e.g. model confidence) | Metadata weighting |
| Find more images based on failure cases | Similarity |
| Balance how many objects of each class I have | Class balancing |
| Balance the selection over a metadata field | Metadata balancing |
| Pick a diverse subset based on the objects images contain, not overall scene appearance | Subpart diversity |
You are not limited to one strategy. See Combining multiple strategies to weight several in a single run. For an end-to-end curation workflow that filters, deduplicates, and then samples a diverse training set, see the Curate a Traffic CCTV Dataset tutorial.
Sampling strategies¶
The examples below use the Python API. See Running a sampling for the GUI, for narrowing the candidate set first, and for exporting the result.
Diverse¶
When to use
You have a large pool of unlabeled data and want the most informative subset to label first. This is especially useful when many samples look alike, for example video frames from the same scene: diverse sampling spreads the selection across embedding space so every labeling effort adds new information instead of more of the same.
Diversity sampling picks samples that cover the dataset as broadly as possible based on embeddings, maximizing the spread across embedding space. Because it favors spread, it tends to over-represent rare, sparse regions; if you want the selection to stay closer to the real distribution, combine it with typicality.
import lightly_studio as ls
# Load your dataset
dataset = ls.ImageDataset.load_or_create()
dataset.add_images_from_path(path="/path/to/image_dataset")
# Sample a diverse subset of 10 samples.
dataset.query().sampling().diverse(
n_samples_to_select=10,
sampling_result_tag_name="diverse_sampling",
)
If your dataset has multiple embedding models, pass embedding_model_name to specify which one to use. See Sampling.diverse for the full API reference.
Deduplication¶
When to use
You want to clean up a dataset, for example after merging several sources, after applying augmentations, or just to drop the last few percent of near-duplicates. Unlike diverse sampling, which picks a fixed number of the most spread-out samples, deduplication keeps everything except samples that sit too close to one already kept, so you remove redundancy without deciding a target size up front.
Deduplication builds a subset in which no two selected samples are closer than stopping_condition_minimum_distance in embedding space. A sample is added to the result only if it is at least that far from every sample already selected; any sample that falls within the threshold is treated as a near-duplicate and skipped. Selection continues until n_samples_to_select samples have been collected or no sufficiently distinct sample remains, so fewer than n_samples_to_select samples may be returned.
import lightly_studio as ls
# Load your dataset
dataset = ls.ImageDataset.load_or_create()
dataset.add_images_from_path(path="/path/to/image_dataset")
# Select up to 100 samples, stopping early once the remaining samples are
# closer than 0.1 to the already selected ones.
dataset.query().sampling().deduplicate(
n_samples_to_select=100,
sampling_result_tag_name="deduplicated_sampling",
stopping_condition_minimum_distance=0.1,
)
The right value for stopping_condition_minimum_distance depends on the embedding model and the distances in your dataset. See Sampling.deduplicate for the full API reference.
Metadata weighting¶
When to use
You already have a number attached to each sample that says how much you want it, and you want to sample by that number. A common case is active learning: keep the images your model is least confident about. See Metadata for how to attach or compute such fields.
Metadata weighting simply prefers samples with a higher (or, with a negative strength, lower) value of one numeric metadata field. It does not balance or spread the selection in any way. Any float or int metadata field can be used as the weight.
import lightly_studio as ls
dataset = ls.ImageDataset.load_or_create()
# Sample the 5 items with the highest value of a custom "sharpness" metadata field.
dataset.query().sampling().metadata_weighting(
n_samples_to_select=5,
sampling_result_tag_name="sharpest_samples",
metadata_key="sharpness",
)
# Sample the 5 items with the lowest value of a custom "sharpness" metadata field.
dataset.query().sampling().metadata_weighting(
n_samples_to_select=5,
sampling_result_tag_name="blurriest_samples",
metadata_key="sharpness",
strength=-1
)
See Sampling.metadata_weighting for the full API reference.
Typicality and outlier¶
When to use
Two common cases. First, to keep a diverse but representative subset by combining typicality
with diversity (see the note below). Second, to surface outliers: the rare,
unusual samples, which are useful for finding edge cases, anomalies, or mislabeled data. Outliers
are not a separate strategy; you get them by weighting the typicality score with a negative
strength, so low-typicality (rare) samples are preferred (see the example below).
Typicality is a per-sample score derived from embeddings. Samples that are close to many other samples in embedding space (i.e. "typical" of the dataset) receive a high score; outliers receive a low score. It is computed with compute_typicality_metadata and then passed to metadata_weighting.
Diversity + typicality
On its own, diversity favors spread, so it can pull the selection toward an even mix of clusters even when the dataset is not evenly distributed. Adding typicality keeps dense regions weighted by how populated they are, so the selection stays diverse while still reflecting the real distribution. If your data is mostly cats with a few dogs, diversity alone could push you toward a roughly even cat/dog split, whereas diversity plus typicality keeps cats in the majority. It also means a large cluster (say 100 near-identical dogs of one breed) still contributes several samples rather than collapsing to one. Combine the two with multiple strategies.
import lightly_studio as ls
# Load your dataset
dataset = ls.ImageDataset.load_or_create()
dataset.add_images_from_path(path="/path/to/image_dataset")
# Compute and store typicality scores as metadata.
dataset.compute_typicality_metadata(metadata_name="typicality")
# Sample the 5 most typical items.
dataset.query().sampling().metadata_weighting(
n_samples_to_select=5,
sampling_result_tag_name="typical_sampling",
metadata_key="typicality",
)
# Sample 5 outliers.
dataset.query().sampling().metadata_weighting(
n_samples_to_select=5,
sampling_result_tag_name="outlier_sampling",
metadata_key="typicality",
strength=-1
)
If your dataset has multiple embedding models, pass embedding_model_name to select which one to use. See Dataset.compute_typicality_metadata for the full API reference.
Similarity¶
When to use
You have a few examples of something you want more of, such as a rare class, a specific scene, or a failure case, and want to mine the dataset for visually similar samples.
How this differs from search in the GUI
The text and image search in the GUI is for manually
mining the dataset from a single text prompt or image. Similarity sampling does the same idea
algorithmically: its query is a tag (a whole set of reference samples, not one input),
it runs unattended, and it can be combined with other strategies in one run. It also takes a
strength, so you can pull toward one tag with a positive weight while pushing away from
another tag with a negative weight (see below).
Similarity-based sampling selects samples based on their embedding similarity to a reference set. First, tag the samples you want to use as the query, then compute per-sample similarity scores with compute_similarity_metadata, and finally pass those scores to metadata_weighting.
import lightly_studio as ls
# Load your dataset
dataset = ls.ImageDataset.load_or_create()
dataset.add_images_from_path(path="/path/to/image_dataset")
# Define a query set by tagging some samples.
dataset[:5].add_tag("my_query_samples")
# Compute similarity to the tagged samples and store it as metadata.
# The method returns the name under which the metadata was stored.
metadata_name = dataset.compute_similarity_metadata(
query_tag_name="my_query_samples",
metadata_name="similarity_to_query", # optional. auto-generated when omitted.
)
# Sample the 10 items most similar to the query set.
dataset.query().sampling().metadata_weighting(
n_samples_to_select=10,
sampling_result_tag_name="similar_to_query_sampling",
metadata_key=metadata_name,
)
metadata_name is optional. When omitted, a unique name is generated automatically and returned. See Dataset.compute_similarity_metadata for the full API reference.
Positive and negative examples¶
To pull toward one tagged set while pushing away from another, use
EmbeddingSimilarityStrategy inside a
multi-strategy run: a positive strength prefers samples similar
to one tag, a negative strength avoids samples similar to another.
import lightly_studio as ls
from lightly_studio.sampling.sampling_config import EmbeddingSimilarityStrategy
dataset = ls.ImageDataset.load("my-dataset")
# Tag a set of examples you want more of, and a set you want to avoid.
dataset.match(...).add_tag("want_more")
dataset.match(...).add_tag("avoid")
# Prefer samples similar to "want_more" and dissimilar to "avoid".
dataset.query().sampling().multi_strategies(
n_samples_to_select=10,
sampling_result_tag_name="mined_samples",
sampling_strategies=[
EmbeddingSimilarityStrategy(query_tag_name="want_more", strength=1.0),
EmbeddingSimilarityStrategy(query_tag_name="avoid", strength=-1.0),
],
)
Class balancing¶
When to use
Your annotated data is imbalanced and you want to control the class mix of the selected subset, for example ensuring you have enough "pedestrians" in a driving dataset.
Class balancing selects samples based on the distribution of annotation classes.
Annotations required
This strategy requires the dataset to have annotations. It is primarily designed for object detection annotations. Segmentation masks may produce unexpected results, as mask definitions can vary (e.g., all pixels of a class in a single mask vs. multiple masks per class).
import lightly_studio as ls
# Load your dataset
dataset = ls.ImageDataset.load_or_create()
# Option 1: Balance classes uniformly (e.g. equal number of cats and dogs)
dataset.query().sampling().annotation_balancing(
n_samples_to_select=50,
sampling_result_tag_name="balanced_uniform",
target_distribution="uniform",
)
# Option 2: Mirror the class distribution of the input set
dataset.query().sampling().annotation_balancing(
n_samples_to_select=50,
sampling_result_tag_name="balanced_input",
target_distribution="input",
)
# Option 3: Define a specific target distribution (e.g. 20% cat, 80% dog)
dataset.query().sampling().annotation_balancing(
n_samples_to_select=50,
sampling_result_tag_name="balanced_custom",
target_distribution={"cat": 0.2, "dog": 0.8},
)
The three target_distribution options are:
| Value | Behavior |
|---|---|
"uniform" |
Equal share for every class present in the candidate input set |
"input" |
Mirrors the class distribution of the candidate input set |
{class: ratio, ...} |
Explicit target ratios. Give a ratio to only some classes; the other classes then share the remainder to 1.0 |
Metadata balancing¶
When to use
Your data is concentrated on a few conditions and you want the selection spread over them evenly, for example equal amounts of every weather or every recording city. Unlike class balancing, this needs no annotations: it balances a metadata field you already have.
Metadata balancing selects samples based on the distribution of the values of one metadata field. The field must be categorical, which means its values are strings or booleans. To rank samples by a numeric field instead, use metadata weighting.
import lightly_studio as ls
# Load your dataset
dataset = ls.ImageDataset.load_or_create()
# Option 1: Balance the values uniformly (e.g. equal amounts of every weather)
dataset.query().sampling().metadata_balancing(
n_samples_to_select=50,
sampling_result_tag_name="balanced_uniform",
metadata_key="weather",
target_distribution="uniform",
)
# Option 2: Mirror the value distribution of the input set
dataset.query().sampling().metadata_balancing(
n_samples_to_select=50,
sampling_result_tag_name="balanced_input",
metadata_key="weather",
target_distribution="input",
)
# Option 3: Define a specific target distribution (e.g. 30% sunny, 70% rainy)
dataset.query().sampling().metadata_balancing(
n_samples_to_select=50,
sampling_result_tag_name="balanced_custom",
metadata_key="weather",
target_distribution={"sunny": 0.3, "rainy": 0.7},
)
The target_distribution options are the same as for class balancing, but they
apply to the values of the metadata field:
| Value | Behavior |
|---|---|
"uniform" |
Equal share for every value present in the candidate input set |
"input" |
Mirrors the value distribution of the candidate input set |
{value: ratio, ...} |
Explicit target ratios. Give a ratio to only some values; the other values then share the remainder to 1.0 |
A boolean field is balanced over its two values. Name them with True and False, or with
the lowercase strings "true" and "false":
# Select 20% blurry images and 80% sharp ones.
dataset.query().sampling().metadata_balancing(
n_samples_to_select=50,
sampling_result_tag_name="balanced_blur",
metadata_key="is_blurry",
target_distribution={True: 0.2, False: 0.8},
)
Samples that have no value for the field stay available for selection, but the strategy does not move the selection towards or away from them. To balance more than one field, combine one strategy per field with multiple strategies. Each field is then balanced on its own, not over the combinations of their values.
Subpart diversity¶
When to use
Your dataset has object detection annotations and you care about the diversity of objects rather than scenes. Two images can look visually similar at the scene level yet contain very different objects; subpart diversity picks parent images by maximizing the spread of their annotation crop embeddings, so the selected set covers as wide a range of objects as possible.
Annotations required
This strategy requires annotation crops with embeddings. When you load data with
add_samples_from_coco, crop embeddings are generated automatically
(embed_annotations=True by default), so no extra embedding step is needed.
Subpart diversity selects parent images based on the diversity of their annotation crop embeddings rather than full-image embeddings. Each parent image contributes the embeddings of all its annotation crops, and sampling maximizes diversity across those crop embeddings. Because diversity is measured at the crop level, two images that look nearly identical at the scene level are treated very differently when they contain different objects.
import lightly_studio as ls
# Load the dataset with COCO object detection annotations.
# Crop embeddings are generated automatically by add_samples_from_coco.
dataset = ls.ImageDataset.create()
dataset.add_samples_from_coco(
annotations_json="/path/to/annotations.json",
images_path="/path/to/images",
annotation_type=ls.AnnotationType.OBJECT_DETECTION,
)
# Select 20 images whose object crops are as diverse as possible.
dataset.query().sampling().subpart_diversity(
n_samples_to_select=20,
sampling_result_tag_name="diverse_objects",
)
By default, crop embeddings from all annotation sources are merged. To restrict diversity
to a specific annotation source, pass annotation_source:
dataset.query().sampling().subpart_diversity(
n_samples_to_select=20,
sampling_result_tag_name="diverse_objects_from_one_source",
annotation_source="ground_truth",
)
See Sampling.subpart_diversity for the full API reference.
Running a sampling¶
You can run any strategy from the GUI for a quick, one-off selection, or from the Python API when you need reusable, configurable, or combined sampling in code.
In the GUI¶
Open the dialog from the Menu button in the top-right corner and select Sampling. The dialog
shows a dropdown with the sampling strategies available in the GUI. Specify the number of samples and
the tag name to store the result under. Python supports more strategies and lets you
combine them.
In Python¶
Each strategy is configured directly from a DatasetQuery via sampling(). This works for image datasets, video datasets, and video-frame datasets returned by VideoDataset.frames(). The sampled items are stored under the tag passed as sampling_result_tag_name, so you can filter or export them later. sampling_result_tag_name must be a tag name that does not yet exist in the dataset, unless it is the preselected_tag_name of the same run (see Continuing from preselected samples).
Filtering before sampling¶
By default, sampling considers all samples in the dataset. You can narrow the candidate set first with match(), and the sampling will only consider the matching samples:
import lightly_studio as ls
from lightly_studio.core.dataset_query import ImageSampleField
dataset = ls.ImageDataset.load_or_create()
# Sample 10 diverse items from images with width >= 1920 only.
dataset.match(ImageSampleField.width >= 1920).sampling().diverse(
n_samples_to_select=10,
sampling_result_tag_name="diverse_hd",
)
Videos can be filtered and sampled using VideoDataset.match(...).sampling() and video frames can be sampled through VideoDataset.frames().match(...).sampling():
import lightly_studio as ls
from lightly_studio.core.dataset_query import VideoFrameSampleField
dataset = ls.VideoDataset.load("my_video_dataset")
frames = dataset.frames()
for frame in frames.match(VideoFrameSampleField.frame_number > 1):
frame.metadata["score"] = float(frame.frame_number)
frames.match(VideoFrameSampleField.frame_number > 1).sampling().metadata_weighting(
n_samples_to_select=5,
sampling_result_tag_name="sampled_frames",
metadata_key="score",
)
See Search and Filter for more filtering options.
Continuing from preselected samples¶
Use preselected_tag_name to select a new batch while taking an earlier batch into
account. The preselected samples influence the strategy but are not added to the new
result tag. n_samples_to_select is the number of new samples to select.
sampling = dataset.query().sampling()
sampling.diverse(
n_samples_to_select=100,
sampling_result_tag_name="first_batch",
)
sampling.diverse(
n_samples_to_select=100,
sampling_result_tag_name="second_batch",
preselected_tag_name="first_batch",
)
The preselected tag must belong to the sampled collection, and all of its samples must
be part of the query being sampled. The option is available on every sampling method,
including multi_strategies().
To grow one tag instead of creating a new one per batch, pass the same name as both
preselected_tag_name and sampling_result_tag_name. The newly selected samples are
added to the existing tag, so it holds the whole selection after every run:
sampling = dataset.query().sampling()
sampling.diverse(
n_samples_to_select=100,
sampling_result_tag_name="training_set",
)
# "training_set" holds 100 samples.
sampling.diverse(
n_samples_to_select=100,
sampling_result_tag_name="training_set",
preselected_tag_name="training_set",
)
# "training_set" holds 200 samples.
Video sequence sampling¶
To select clips instead of individual frames, set selected_sequence_length to
the number of frames per sequence (for example 50). By default selected_sequence_length is
None and individual frames are selected. Each video is split into non-overlapping sequences of that
length; trailing frames that do not fill a full sequence are dropped. Sampling then chooses among sequence
proxies (mean of the frame embeddings), and every frame of each chosen sequence is tagged.
n_samples_to_select counts frames and must be a multiple of selected_sequence_length.
The number of selected sequences is n_samples_to_select / selected_sequence_length.
import lightly_studio as ls
dataset = ls.VideoDataset.load("my_video_dataset")
frames = dataset.frames()
# Select 2 sequences of 50 frames each (100 frames total).
frames.query().sampling().diverse(
n_samples_to_select=100,
sampling_result_tag_name="sequence_clips",
selected_sequence_length=50,
)
Only diversity selection on video frame collections supports selected_sequence_length.
Combining multiple strategies¶
You can combine several strategies into a single sampling run. All configured strategies are evaluated together and weighted by the strength parameter.
import lightly_studio as ls
from lightly_studio.sampling.sampling_config import (
MetadataWeightingStrategy,
EmbeddingDiversityStrategy,
)
# Load your dataset
dataset = ls.ImageDataset.load_or_create()
dataset.add_images_from_path(path="/path/to/image_dataset")
# Compute typicality and store it as `typicality` metadata
dataset.compute_typicality_metadata(metadata_name="typicality")
# Sample 10 items by combining typicality and diversity,
# with diversity weighted twice as strongly.
dataset.query().sampling().multi_strategies(
n_samples_to_select=10,
sampling_result_tag_name="multi_strategy_sampling",
sampling_strategies=[
MetadataWeightingStrategy(metadata_key="typicality", strength=1.0),
EmbeddingDiversityStrategy(embedding_model_name="my_model_name", strength=2.0),
],
)
Exporting the results¶
Every sampling run writes its result to the tag passed as sampling_result_tag_name. You can export those samples from the GUI, or query them in Python by matching on the tag.
import lightly_studio as ls
from lightly_studio.core.dataset_query import ImageSampleField
dataset = ls.ImageDataset.load("my-dataset")
sampled_items = (
dataset.match(ImageSampleField.tags.contains("diverse_sampling")).to_list()
)
with open("export.txt", "w") as f:
for sample in sampled_items:
f.write(f"{sample.file_path_abs}\n")
For more details on filtering by tag or exporting subsets, see Search and Filter and Export.