Skip to course content
Free PyTorch course

Advanced Deep Learning with PyTorch

Unit 02.05: Project step: create a reusable dataloader

Everything from this module, assembled into one function you will use for the rest of the course.

One function, with the leak already closed

The function takes a DataFrame and returns training and validation loaders. The important detail is *where* the scaling statistics come from: the training rows only.

Computing a mean over the whole frame lets validation data influence the transformation applied to training data. The score improves and the improvement is fictional.

import pandas as pd
import torch
from torch.utils.data import DataLoader, TensorDataset, random_split


def make_loaders(df, feature_cols, target_col, batch_size=32, seed=0):
    """Return train/val loaders from a dataframe, with the split seeded."""
    X = torch.tensor(df[feature_cols].to_numpy(), dtype=torch.float32)
    y = torch.tensor(df[target_col].to_numpy(), dtype=torch.float32).unsqueeze(1)

    # Standardise using TRAINING statistics only -- computing them over the
    # whole frame leaks validation information into the model.
    n_train = int(len(df) * 0.8)
    generator = torch.Generator().manual_seed(seed)
    train_ds, val_ds = random_split(TensorDataset(X, y),
                                    [n_train, len(df) - n_train], generator=generator)
    train_X = X[train_ds.indices]
    mean, std = train_X.mean(0), train_X.std(0).clamp(min=1e-8)

    scaled = TensorDataset((X - mean) / std, y)
    train = torch.utils.data.Subset(scaled, train_ds.indices)
    val = torch.utils.data.Subset(scaled, val_ds.indices)
    return (DataLoader(train, batch_size=batch_size, shuffle=True),
            DataLoader(val, batch_size=batch_size),
            {"mean": mean.tolist(), "std": std.tolist()})


torch.manual_seed(0)
frame = pd.DataFrame({
    "hours": torch.rand(200).mul(10).tolist(),
    "attempts": torch.randint(1, 5, (200,)).tolist(),
    "passed": torch.randint(0, 2, (200,)).tolist(),
})
train_loader, val_loader, stats = make_loaders(frame, ["hours", "attempts"], "passed")

print("train batches:", len(train_loader), " val batches:", len(val_loader))
xb, yb = next(iter(train_loader))
print("batch shapes :", tuple(xb.shape), tuple(yb.shape))
print("scaling stats came from training rows only:", [round(v, 2) for v in stats["mean"]])

The returned stats dictionary carries the training mean and standard deviation. Keep it — at prediction time you must apply the *same* transformation, and re-deriving it from new data is another route to the same leak.

The mistake this prevents

Standardising before splitting. It is one line shorter, it looks harmless, and it quietly contaminates every result that follows.

Takeaway

Split first, compute statistics on training data only, and return them so the same transformation can be reapplied later.