---
name: ml-conventions
description: Experiment naming, model versioning, reproducibility via random seeds, config-as-code patterns, and team conventions for ML projects
topics: [ml, conventions, reproducibility, versioning, config, experiments]
---

ML projects without conventions degenerate into chaos within weeks: unnamed experiments with lost hyperparameters, models named `model_v2_final_FINAL.pkl`, and results that cannot be reproduced. Unlike software engineering where the compiler enforces structure, ML workflows are loose scripts and notebooks that require disciplined conventions to remain comprehensible. Establish these conventions at project start and encode them in tooling so they are followed by default, not willpower.

## Summary

ML conventions cover experiment naming (structured, searchable identifiers), model versioning (semantic or content-addressed), reproducibility (seeding all random sources, recording environment), and config-as-code (no magic numbers in code, all hyperparameters in config files). These conventions are not optional hygiene — they are the infrastructure that makes ML engineering a repeatable discipline rather than a research lottery.

## Deep Guidance

### Experiment Naming

Every training run that produces artifacts or results must have a unique, human-readable identifier. Ad-hoc names like `test`, `v2`, or `new_model` are unusable at scale:

**Recommended format**: `{model_type}-{dataset}-{date}-{purpose}[-{variant}]`

Examples:
- `resnet50-imagenet-20240315-baseline`
- `bert-sst2-20240315-lr-sweep`
- `xgboost-churn-20240320-feature-v3`
- `gpt2-reviews-20240322-dropout-ablation`

**Rules**:
- All lowercase, hyphen-separated (no spaces, no underscores)
- Date in `YYYYMMDD` format (sorts chronologically)
- Purpose is human-readable and specific — not `experiment1` or `test`
- Variant suffix for ablations and sweeps (`-v2`, `-no-dropout`, `-lr-1e-3`)

Many teams use auto-generated experiment IDs (MLflow assigns UUID-based IDs automatically) and rely on tagging/metadata for search. This is fine as a secondary system, but always add a human-readable display name.

### Model Versioning

Model versioning is distinct from experiment tracking. A version is a production artifact; an experiment is a training run:

**Semantic versioning for models**:
- `v{major}.{minor}.{patch}` — consistent with software versioning
- Major: Breaking change in model interface (input/output schema, preprocessing contract)
- Minor: Meaningful accuracy improvement or new feature support
- Patch: Bug fix, minor data update, no interface change

**Content-addressed versioning** (used by MLflow Model Registry, DVC):
- Models are identified by a hash of their weights + config
- Prevents accidental overwriting
- Enables exact reproducibility — "which weights produced this prediction?"

**Registry-based model lifecycle**:
```
Staging → Validation → Production → Archived
```
- Never promote directly to Production — always pass through Staging validation
- Keep at least one previous Production version for instant rollback
- Document promotion reason: "Promoted: +2.3% AUC on Q1 eval set, latency within budget"

### Reproducibility

ML reproducibility means: given the same code, data, and config, the same model is produced. Achieve it through four controls:

**1. Random seed management**

Set all random sources before any computation:
```python
import random
import numpy as np
import torch

def set_seed(seed: int) -> None:
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    # For full determinism (may impact performance)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False
```

Record the seed in experiment config. Default seed: `42` (or any fixed value — consistency matters more than the value). When running hyperparameter sweeps with multiple seeds, record all seeds and report mean ± std.

**2. Dependency pinning**

Pin all dependencies to exact versions:
```toml
# pyproject.toml (Poetry)
[tool.poetry.dependencies]
python = "3.11.4"
torch = "2.1.0"
transformers = "4.35.2"
numpy = "1.26.0"
```

Use `poetry.lock` or `requirements.txt` generated by `pip freeze`. Never use unpinned dependencies (`torch>=2.0`) in a training environment.

**3. Data versioning**

Record the exact dataset version used for each training run:
- DVC: content-addressed data with `dvc add` and `.dvc` pointers
- Dataset registry: log dataset name + version + hash in experiment metadata
- SQL-based datasets: log the query hash and execution timestamp

**4. Environment reproducibility**

Capture the full environment:
```bash
# Save environment
conda env export > environment.yml
pip freeze > requirements-frozen.txt

# Record GPU driver and CUDA version
nvidia-smi --query-gpu=driver_version,name --format=csv
nvcc --version
```

For full environment isolation, use Docker. The Dockerfile is the environment specification.

### Config-as-Code

No magic numbers in code. Every hyperparameter, data path, and training setting belongs in a config file:

**Bad** (magic numbers scattered in code):
```python
optimizer = Adam(model.parameters(), lr=0.001)
scheduler = CosineAnnealingLR(optimizer, T_max=100)
train_loader = DataLoader(dataset, batch_size=32, num_workers=4)
```

**Good** (config-driven):
```yaml
# configs/train.yaml
training:
  seed: 42
  epochs: 100
  batch_size: 32
  num_workers: 4

optimizer:
  type: adam
  lr: 1.0e-3
  weight_decay: 1.0e-4

scheduler:
  type: cosine_annealing
  t_max: 100
```

```python
# src/training/train.py
def train(cfg: DictConfig) -> None:
    set_seed(cfg.training.seed)
    optimizer = build_optimizer(model, cfg.optimizer)
    scheduler = build_scheduler(optimizer, cfg.scheduler)
```

Use **Hydra** (Meta) or **OmegaConf** for hierarchical config management with CLI override support:
```bash
# Override from CLI without changing config files
python train.py optimizer.lr=1e-4 training.batch_size=64
```

**Config file organization**:
```
configs/
  base.yaml           # Default config for all experiments
  model/
    resnet50.yaml
    vit-b16.yaml
  data/
    imagenet.yaml
    cifar10.yaml
  training/
    fast.yaml         # Low-epoch for debugging
    full.yaml         # Production training
```

### Code and Notebook Conventions

**Notebooks are for exploration, not production**:
- Notebooks belong in `notebooks/` — never in `src/`
- Notebooks must be cleared before committing (no large outputs committed to git)
- Meaningful results from notebooks are refactored into `src/` modules with tests

**Module structure conventions**:
- `src/data/` — dataset classes, data loaders, preprocessing transforms
- `src/models/` — model architectures (no training logic)
- `src/training/` — training loop, loss functions, callbacks
- `src/evaluation/` — metrics, evaluation runners
- `src/serving/` — inference code, prediction pipelines

**Naming conventions**:
- Files: `snake_case.py`
- Classes: `PascalCase` (e.g., `ResNet50Classifier`, `ChurnDataset`)
- Functions: `snake_case` (e.g., `compute_f1_score`, `load_checkpoint`)
- Constants: `UPPER_SNAKE_CASE` (e.g., `MAX_SEQ_LENGTH = 512`)
- Config keys: `snake_case` in YAML

### Checklist Before Starting a Training Run

```
[ ] Experiment name follows naming convention
[ ] Random seed set and recorded in config
[ ] Config file committed (not just command-line overrides)
[ ] Dataset version recorded
[ ] Experiment tracker (MLflow/W&B) initialized with run metadata
[ ] Code committed to git (note the commit SHA in the experiment)
[ ] Output directory created and named consistently
[ ] Hardware/environment recorded
```
