Multi-file Configs & Overrides¶
Real projects don't have a single, fixed configuration. You run different experiments, try different augmentation pipelines, swap schedulers, test on different datasets. Copying entire config files for each variation leads to duplication and drift.
EzConfy solves this with multi-file merging and programmatic overrides.
The Idea: One Base, Many Variations¶
The core pattern is simple:
- A base config defines everything that stays the same across experiments
- Small override files each change only the part that varies
- You compose them at load time — later files override earlier ones
Your training script never changes. You just pick which YAML files to combine.
Multiple Config Files¶
Pass a list of files to config_paths. They are deep-merged in order — later files win on conflicts:
epochs: 50
lr: 0.001
batch_size: 32
model:
_target_type_: torchvision.models:resnet18
_init_args_:
num_classes: 10
augmentation:
_target_type_: torchvision.transforms:Compose
_init_args_:
transforms:
- _target_type_: torchvision.transforms:ToTensor
optimizer:
_target_type_: torch.optim:Adam
_init_args_:
params: ${model.parameters()}
lr: ${lr}
scheduler:
_target_type_: torch.optim.lr_scheduler:StepLR
_init_args_:
optimizer: ${optimizer}
step_size: 10
augmentation:
_target_type_: torchvision.transforms:Compose
_init_args_:
transforms:
- _target_type_: torchvision.transforms:RandomHorizontalFlip
- _target_type_: torchvision.transforms:RandomCrop
_init_args_:
size: 32
padding: 4
- _target_type_: torchvision.transforms:ToTensor
- _target_type_: torchvision.transforms:Normalize
_init_args_:
mean: [0.4914, 0.4822, 0.4465]
std: [0.2470, 0.2435, 0.2616]
Result
The model, optimizer, scheduler, and training params come from base.yaml. Only augmentation is replaced by heavy_augment.yaml. Everything else is untouched.
Deep Merge Behavior¶
Nested dictionaries are merged recursively, not replaced entirely:
Note
Non-dict values (scalars, lists) are fully replaced by the later file.
Composing Multiple Override Files¶
You are not limited to two files. Combine a base with several focused override files, each responsible for one concern:
cfg = ConfigBuilder.from_files(
config_paths=[
"configs/base.yaml", # full baseline
"configs/resnet50.yaml", # swap backbone
"configs/cosine_scheduler.yaml", # swap scheduler
"configs/heavy_augment.yaml", # swap augmentation
],
)
Each override file is small and single-purpose:
Mix and match components freely. Want ResNet50 with heavy augmentation and cosine scheduler? Combine those three files. Want ResNet18 with light augmentation and step scheduler? Use just the base. No duplication, no drift.
Programmatic Overrides¶
For quick tweaks — hyperparameter sweeps, CLI arguments, test-specific values — pass an overrides dictionary. It is applied after all files are merged, so it always wins:
cfg = ConfigBuilder.from_files(
config_paths=["configs/base.yaml", "configs/resnet50.yaml"],
overrides={"lr": 0.01, "batch_size": 128},
)
Hyperparameter sweep
Typical Project Layout¶
project/
configs/
base.yaml # complete baseline experiment
backbones/
resnet18.yaml # swap backbone to ResNet18
resnet50.yaml # swap backbone to ResNet50
augmentations/
light.yaml # minimal augmentation
heavy.yaml # aggressive augmentation
schedulers/
step.yaml # StepLR scheduler
cosine.yaml # CosineAnnealing scheduler
schema.yaml # validation schema
train.py
# train.py
import sys
from ezconfy import ConfigBuilder
# Example: python train.py resnet50 heavy cosine
backbone, augment, scheduler = sys.argv[1], sys.argv[2], sys.argv[3]
cfg = ConfigBuilder.from_files(
config_paths=[
"configs/base.yaml",
f"configs/backbones/{backbone}.yaml",
f"configs/augmentations/{augment}.yaml",
f"configs/schedulers/{scheduler}.yaml",
],
schema_path="schema.yaml",
)
Each experiment is fully described by its combination of YAML files — easy to reproduce, easy to compare, easy to version-control.