# How ML Pipelines in Lesson 13 of ai-engineering-from-scratch Ensure Reproducibility and Experiment Tracking

> Learn how ML pipelines in ai engineering from scratch Lesson 13 guarantee reproducibility and robust experiment tracking using deterministic seeds, version pinning, and MLflow.

- Repository: [Rohit Ghumare/ai-engineering-from-scratch](https://github.com/rohitg00/ai-engineering-from-scratch)
- Tags: deep-dive
- Published: 2026-06-12

---

**The ML pipeline in Lesson 13 ensures reproducibility by combining deterministic random seeds, version-pinned dependencies, and MLflow experiment tracking to create fully traceable and re-runnable machine learning workflows.**

The `ai-engineering-from-scratch` repository by rohitg00 provides a reference implementation for production-ready ML workflows in **Lesson 13** of **Phase 2 (ML Fundamentals)**. Located at `phases/02-ml-fundamentals/13-ml-pipelines/`, this lesson demonstrates how to build deterministic machine learning pipelines that eliminate hidden variability through explicit configuration management and comprehensive experiment tracking.

## The Three Pillars of Reproducibility

The pipeline architecture rests on three tightly coupled components that work together to guarantee identical results across different runs and machines.

### Deterministic Seeds and Environment Control

All stochastic operations are controlled through a centralized seeding mechanism. In [`phases/02-ml-fundamentals/13-ml-pipelines/code/main.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/phases/02-ml-fundamentals/13-ml-pipelines/code/main.py), the `set_seed()` function synchronizes random number generators across Python's `random` module, NumPy, and PyTorch:

```python
def set_seed(seed: int):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)

```

The environment is locked through the repository's root [`requirements.txt`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/requirements.txt), which pins exact package versions (e.g., `numpy==1.26.3`, `torch==2.2.0`). The lesson's documentation specifies the exact Python version and Git commit SHA required to recreate the environment, ensuring binary-level consistency across installations.

### Immutable Data Processing

The data pipeline in [`phases/02-ml-fundamentals/13-ml-pipelines/code/data_loader.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/phases/02-ml-fundamentals/13-ml-pipelines/code/data_loader.py) treats the source CSV as read-only and applies deterministic transformations. The `train_test_split` function uses the configured `random_state` to guarantee identical splits:

```python
X_train, X_val, y_train, y_val = train_test_split(
    X, y, test_size=0.2, random_state=seed, stratify=y
)

```

Preprocessing steps like `StandardScaler` are fit exclusively on training data and then applied to validation data, preventing data leakage while maintaining reproducibility. The source data in the `data/` folder is never mutated during execution.

### MLflow Experiment Tracking

Every training execution creates a self-contained MLflow run that captures the complete experiment context. The training script logs hyperparameters, metrics, artifacts, and system metadata:

```python
mlflow.start_run()
mlflow.log_params(cfg.__dict__)

for epoch in range(cfg.epochs):
    loss = train_one_epoch(net, optimizer, X_train, y_train, cfg.batch_size)
    mlflow.log_metric("train_loss", loss, step=epoch)

torch.save(net.state_dict(), "outputs/model.pt")
mlflow.log_artifact("outputs/model.pt")
mlflow.end_run()

```

The system automatically records the Git commit SHA, allowing future reproduction by checking out the exact code version used in the original run.

## Config-Driven Architecture

The pipeline eliminates hidden global state by centralizing all parameters in a `Config` dataclass defined in [`phases/02-ml-fundamentals/13-ml-pipelines/code/config.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/phases/02-ml-fundamentals/13-ml-pipelines/code/config.py):

```python
@dataclass
class Config:
    seed: int = 42
    lr: float = 1e-3
    batch_size: int = 32
    epochs: int = 10
    data_path: str = "data/toy.csv"

```

This design forces explicit parameter declaration. Any hyperparameter modification requires editing the configuration file, which is then logged by MLflow, creating an immutable record of the experimental conditions.

## Complete Pipeline Execution Flow

The end-to-end execution follows a strict linear flow that ensures reproducibility at each stage:

1. **Configuration Loading**: The `main()` function instantiates the `Config` dataclass
2. **Environment Seeding**: `set_seed(cfg.seed)` initializes all random generators
3. **Deterministic Data Loading**: `load_data()` splits and preprocesses data using the fixed seed
4. **Version-Controlled Training**: The model trains while MLflow captures metrics and artifacts
5. **Artifact Persistence**: Outputs write to the `outputs/` folder and are logged as MLflow artifacts

The pipeline structure ensures that checking out a specific Git commit and running [`main.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/main.py) produces bit-for-bit identical results, provided the environment matches the locked [`requirements.txt`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/requirements.txt).

## Summary

- **Deterministic seeding** across Python, NumPy, and PyTorch guarantees identical random number sequences in [`phases/02-ml-fundamentals/13-ml-pipelines/code/main.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/phases/02-ml-fundamentals/13-ml-pipelines/code/main.py)
- **Immutable data pipelines** in [`data_loader.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/data_loader.py) use `random_state` parameters to ensure consistent train/validation splits
- **MLflow integration** automatically logs hyperparameters, metrics, model artifacts, and Git commit SHAs for complete experiment traceability
- **Config dataclass** centralizes all parameters, eliminating hidden global state and ensuring all experimental variables are captured
- **Version-pinned dependencies** in [`requirements.txt`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/requirements.txt) prevent library version drift from affecting numerical results

## Frequently Asked Questions

### How does the pipeline prevent randomness from affecting results across different runs?

The pipeline implements a `set_seed()` function that synchronizes random number generators across Python's built-in `random` module, NumPy, and PyTorch. By calling this function with a fixed seed from the `Config` dataclass at the start of execution, the pipeline ensures that data shuffling, weight initialization, and dropout patterns remain identical across runs. This deterministic behavior is verified by the MLflow logging system, which records the seed value alongside other hyperparameters.

### What files are required to reproduce an experiment from the MLflow logs?

To reproduce any logged experiment, you need the Git commit SHA recorded in the MLflow run metadata, the [`requirements.txt`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/requirements.txt) file from the repository root, and the [`config.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/config.py) file used during that specific run. The MLflow UI stores the exact commit SHA, allowing you to checkout the precise code version. The `outputs/` folder contains the trained model artifacts, while the logged parameters contain the configuration values needed to recreate the exact experimental conditions.

### Why does the pipeline use a dataclass for configuration instead of command-line arguments?

The `Config` dataclass in [`phases/02-ml-fundamentals/13-ml-pipelines/code/config.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/phases/02-ml-fundamentals/13-ml-pipelines/code/config.py) provides a single source of truth for all experimental parameters that can be easily serialized and logged by MLflow. Unlike command-line arguments, which can be forgotten or mistyped, the dataclass ensures that every parameter is explicitly declared with type hints and default values. This approach also enables the entire configuration dictionary to be logged via `mlflow.log_params(cfg.__dict__)`, creating a complete, machine-readable record of the experimental setup that is stored alongside the results.

### How does the data pipeline ensure preprocessing consistency between training and validation sets?

The `load_data()` function in [`phases/02-ml-fundamentals/13-ml-pipelines/code/data_loader.py`](https://github.com/rohitg00/ai-engineering-from-scratch/blob/main/phases/02-ml-fundamentals/13-ml-pipelines/code/data_loader.py) fits the `StandardScaler` exclusively on the training data using `scaler.fit_transform(X_train)`, then applies the same transformation parameters to the validation data with `scaler.transform(X_val)`. This prevents data leakage while ensuring that the preprocessing steps remain deterministic and reproducible. The function also uses the fixed seed from the `Config` class for the `train_test_split` operation, guaranteeing that the same rows always appear in the training and validation sets.