Reproducibility in Albumentations
On this page
- Choose the guarantee you need
- Recreate one pipeline's random sequence
- Reproduce a complete PyTorch DataLoader run
- Replay one suspicious sample
- Inspect patterns across many samples
- Save the declared policy
- Keep validation deterministic by design
- Keep custom transforms inside the pipeline RNG
- Record enough state to repeat the experiment
- Reproducibility does not prove correctness
- Related topics
You rerun a training experiment with the same augmentation seed, but the batches differ. Or one sample produces an unusually high loss, and you cannot recreate the crop and color shift that the model saw. Both look like reproducibility failures, but they require different solutions.
Augmentation changes the effective training distribution. The policy is therefore part of the experiment, and every sampled augmentation is part of the data seen by the model. A seed controls one random stream. Reproducing a complete run also requires the same sample order and worker configuration. Reproducing one suspicious sample requires a replay record.
Choose the guarantee you need
| What you need to reproduce | Use | Also keep fixed or record |
|---|---|---|
| The random sequence from one pipeline | A.Compose(..., seed=137) | Pipeline structure, input sequence, and call order |
A complete PyTorch DataLoader run | A Compose seed and a seeded torch.Generator | Worker count, worker persistence, sampler, batch settings, and dataset order |
| One exact sampled augmentation | A.ReplayCompose | Replay dictionary and compatible input targets |
| What happened across many samples | save_applied_params=True | Sample IDs and a metric such as loss or failure type |
| The declared augmentation policy | A.save and A.load | Library versions, dataset version, code revision, and training configuration |
A seed answers “can I recreate this random sequence under the same execution setup?” It does not identify what happened to a particular sample unless you also know where that sample appeared in the sequence.
![]()
Recreate one pipeline's random sequence
Compose owns an internal Python-style random generator and an internal NumPy-style random generator. Set the seed on Compose, not through global random.seed(...) or np.random.seed(...).
Two independently created pipelines with the same structure and seed produce the same sequence when they receive the same inputs in the same order:
import albumentations as A
import numpy as np
def make_transform() -> A.Compose:
return A.Compose(
[
A.RandomCrop(height=256, width=256),
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.5),
],
seed=137,
)
image = np.indices((320, 320, 3)).sum(axis=0).astype(np.uint8)
transform_a = make_transform()
transform_b = make_transform()
for _ in range(3):
result_a = transform_a(image=image)["image"]
result_b = transform_b(image=image)["image"]
assert np.array_equal(result_a, result_b)
Calls to one pipeline still advance its random state. A fixed seed gives you a reproducible sequence, not a constant output:
transform = make_transform()
first = transform(image=image)["image"]
second = transform(image=image)["image"]
# Restart the sequence and reproduce the first draw.
transform.set_random_seed(137)
repeated_first = transform(image=image)["image"]
assert np.array_equal(first, repeated_first)
first and second may differ because they are consecutive draws. Reconstructing the same pipeline or calling set_random_seed(137) restarts the sequence.
Global seeds do not control Compose
This code seeds Python and NumPy globally, but it does not seed the Albumentations pipeline:
import random
import albumentations as A
import numpy as np
random.seed(137)
np.random.seed(137)
transform = A.Compose(
[
A.RandomRotate90(p=0.5),
A.RandomBrightnessContrast(p=0.5),
],
# Intentionally omit seed here: this example shows that global seeds do not seed Compose.
)
Use A.Compose(..., seed=137) to control the pipeline without letting unrelated random operations elsewhere in the program change its sequence.
The same seed does not align different policies
Two structurally different pipelines may consume random values at different points. Giving both pipelines the same seed does not guarantee that their probability checks or sampled parameters align transform by transform.
For an A/B test, make each strategy reproducible on its own. Keep the dataset order, loader configuration, model initialization, and training settings fixed. Compare the resulting metrics; do not assume that corresponding transforms received corresponding random draws.
Reproduce a complete PyTorch DataLoader run
Compose(seed=137) controls augmentation decisions. A PyTorch DataLoader also controls shuffled sample order and the seeds assigned to worker processes. Reproducing the complete input pipeline requires both random sources:
- set
seed=137onA.Compose; - pass a dedicated
torch.Generatorwith a fixed seed toDataLoader.
The following example creates the dataset, augmentation pipeline, and loader twice. Both runs produce the same shuffled sample order and the same augmented batches:
from __future__ import annotations
import albumentations as A
import numpy as np
import torch
from torch.utils.data import DataLoader, Dataset
class ImageDataset(Dataset):
def __init__(self) -> None:
base_image = np.indices((64, 64, 3)).sum(axis=0).astype(np.uint8)
self.images = [
np.roll(base_image, shift=index, axis=1)
for index in range(32)
]
self.transform = A.Compose(
[
A.RandomCrop(height=48, width=48),
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.5),
],
seed=137,
)
def __len__(self) -> int:
return len(self.images)
def __getitem__(self, index: int) -> np.ndarray:
augmented = self.transform(image=self.images[index])["image"]
return augmented.copy()
def make_loader(num_workers: int) -> DataLoader:
loader_generator = torch.Generator()
loader_generator.manual_seed(137)
return DataLoader(
ImageDataset(),
batch_size=8,
shuffle=True,
num_workers=num_workers,
generator=loader_generator,
)
def collect_run(num_workers: int) -> list[torch.Tensor]:
return [batch.clone() for batch in make_loader(num_workers)]
if __name__ == "__main__":
first_run = collect_run(num_workers=4)
second_run = collect_run(num_workers=4)
assert len(first_run) == len(second_run)
assert all(
torch.equal(first_batch, second_batch)
for first_batch, second_batch in zip(first_run, second_run, strict=True)
)
The if __name__ == "__main__": guard makes the example safe on platforms that start workers with spawn.
Why worker configuration changes the sequence
PyTorch gives each worker a seed from the loader's random generator. Albumentations combines that worker seed with the Compose seed:
worker_seed = torch.initial_seed() % (2**32)
effective_seed = (compose_seed + worker_seed) % (2**32)
Each worker therefore gets a different augmentation stream. The streams remain reproducible when the complete loader setup remains the same.
| Setting | Why it affects the result |
|---|---|
DataLoader(..., generator=...) | Controls worker base seeds and shuffled sample order |
num_workers | Changes the number of random streams and which samples each stream receives |
persistent_workers | Controls whether worker-local dataset and random state survive between epochs |
| Sampler and distributed rank | Control which samples each process receives and in what order |
Batch size and drop_last | Change batch boundaries and which samples reach the model |
| Dataset order | Changes which sample consumes each draw from a worker stream |
A run with one worker can be reproducible, and a run with four workers can be reproducible, while the two runs produce different augmentations. Keep num_workers unchanged when comparing complete runs.
Persistent workers are also reproducible under a fixed setup. Their random streams continue across epochs instead of restarting. Changing persistent_workers changes worker lifetime and therefore changes the sequence.
You do not need a custom worker_init_fn to seed Albumentations. Create the pipeline once in the dataset and let Albumentations derive worker-specific streams. Resetting every worker or every sample to the same seed can duplicate augmentations and reduce training diversity.
Replay one suspicious sample
Suppose a detector receives a crop that removes most of an object, or a segmentation sample produces a high loss after a geometric transform. The declared policy only tells you which outcomes were allowed. A seed can regenerate the stream only when the call order and worker assignment are also known.
ReplayCompose records the concrete random decisions from one call:
import albumentations as A
import numpy as np
image = np.indices((320, 320, 3)).sum(axis=0).astype(np.uint8)
mask = np.zeros((320, 320), dtype=np.uint8)
mask[80:240, 100:220] = 1
transform = A.ReplayCompose(
[
A.RandomCrop(height=256, width=256),
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.5),
],
seed=137,
)
result = transform(image=image, mask=mask)
replayed = A.ReplayCompose.replay(
result["replay"],
image=image,
mask=mask,
)
assert np.array_equal(result["image"], replayed["image"])
assert np.array_equal(result["mask"], replayed["mask"])
Store the replay dictionary with the sample ID and model output when you need exact reconstruction later. Replay applies the same sampled geometry to the image and its masks, boxes, keypoints, or other compatible targets.
Inspect patterns across many samples
Exact replay is useful for one failure. Aggregate debugging needs a lighter record. Set save_applied_params=True to receive an applied_transforms list with the transforms that ran and the sampled configurations they exposed:
import albumentations as A
import numpy as np
image = np.indices((320, 320, 3)).sum(axis=0).astype(np.uint8)
sample_id = "train-000137"
sample_loss = 2.41
transform = A.Compose(
[
A.RandomCrop(height=256, width=256),
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.5),
],
save_applied_params=True,
seed=137,
)
result = transform(image=image)
debug_record = {
"sample_id": sample_id,
"applied_transforms": result["applied_transforms"],
"loss": sample_loss,
}
Records such as (sample_id, applied_transforms, loss) let you ask whether failures cluster around strong blur, extreme crops, dropout, color shifts, classes, cameras, object sizes, or target types.
Applied-parameter logs are designed for inspection and aggregate diagnostics. Use ReplayCompose when you need exact reconstruction of runtime details such as crop coordinates or dropout masks. See Replay and Applied-Parameter Debugging for the complete workflow.
Save the declared policy
A seed cannot tell a future reader which transforms, probabilities, target formats, or filtering thresholds produced the training distribution. Save the complete Compose object as an experiment artifact:
import albumentations as A
transform = A.Compose(
[
A.RandomCrop(height=256, width=256),
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.5),
],
seed=137,
)
A.save(transform, "augmentation-policy.json")
loaded_transform = A.load("augmentation-policy.json")
Serialization preserves the declared transform graph, pipeline options, seed, and target-processing configuration. It does not preserve the current position inside the random stream or the sampled decisions for a particular image. A loaded pipeline starts a new sequence from the saved seed.
Store the policy next to the model checkpoint, dataset version, metrics, code revision, dependency lockfile, and training configuration. See Serialization of Augmentation Pipelines for target-aware policies and custom-transform caveats.
Keep validation deterministic by design
A seeded stochastic pipeline still samples a new augmentation on every call. Ordinary validation and inference preprocessing should use deterministic operations so a sample receives the same preprocessing regardless of call order:
import albumentations as A
validation_transform = A.Compose(
[
A.Resize(height=256, width=256),
A.CenterCrop(height=224, width=224),
A.Normalize(
mean=(0.485, 0.456, 0.406),
std=(0.229, 0.224, 0.225),
),
],
seed=137,
)
This pipeline is deterministic because its transforms do not sample random parameters. The seed does not turn random transforms into deterministic preprocessing.
Augmented validation and stress tests are different: they deliberately measure model behavior under a named perturbation. Use fixed transform parameters and p=1.0 when you want every sample to receive the same stress condition, or save replay and applied-parameter records when the probe remains stochastic.
Keep custom transforms inside the pipeline RNG
A custom transform can break an otherwise reproducible pipeline if it calls global random or np.random. Use self.py_random for Python-style sampling and self.random_generator for NumPy-style sampling:
import albumentations as A
import numpy as np
class RandomGain(A.ImageOnlyTransform):
def get_params(self) -> dict[str, float]:
return {
"gain": self.py_random.uniform(0.8, 1.2),
}
def apply(
self,
img: np.ndarray,
gain: float,
**params,
) -> np.ndarray:
scaled = img.astype(np.float32) * gain
return np.clip(scaled, 0, 255).astype(img.dtype)
transform = A.Compose(
[RandomGain(p=1.0)],
seed=137,
)
The pipeline propagates its random state into the custom transform. See Creating Custom Transforms for parameter sampling and target-specific methods.
Record enough state to repeat the experiment
For an augmentation experiment, preserve:
- the serialized augmentation policy;
- the Albumentations version;
- the
Composeseed; - the
DataLoadergenerator seed; num_workers,persistent_workers, sampler, batch size, anddrop_last;- the dataset version, ordering rules, and sample IDs;
- replay records for samples that require exact reconstruction;
- applied-parameter logs used for diagnostics;
- the code revision and dependency lockfile.
Full training reproducibility also depends on model initialization, optimizer state, distributed sampling, accelerator algorithms, and framework settings. PyTorch documents those limits in Reproducibility.
Reproducibility does not prove correctness
Replay can reproduce a bad augmentation exactly. Serialization can preserve a poor policy faithfully. Applied-parameter logs can reveal that high-loss samples often coincide with a transform, but that correlation does not prove the transform caused the failures.
These mechanisms make the pipeline inspectable. Domain knowledge, ablations, validation slices, and held-out evaluation still determine whether the policy preserves labels and improves the model.