Project: MLP from Data to Evaluation
This project assembles the full neural-foundations workflow: local data, stratified splits, train-only preprocessing, explicit shapes/dtypes/device, a nonlinear MLP, stable logits loss, gradient state, optimizer updates, validation-based checkpoint selection, evaluation mode, and one final test measurement.
Prerequisites
Complete Debugging Training Runs and all earlier lessons in this path. Use the repository's pinned PyTorch 2.14.1 CPU environment.
Learning goals
- build a complete CPU-safe classification run with no download, account, accelerator, or secret.
- isolate training, validation, and test roles while fitting preprocessing on training data only.
- select a checkpoint on validation loss and report held-out test loss and accuracy honestly.
Worked example
The fixture contains 400 noisy two-dimensional points around four local centers. Opposite centers share a label, creating a nonlinear XOR-like task. Each center contributes 60 training, 20 validation, and 20 test examples, so all splits contain both labels and all four regions. The fixed model and 80-epoch budget are declared before test evaluation.
import copy
import torch
from torch import nn
torch.manual_seed(43)
torch.set_num_threads(1)
torch.use_deterministic_algorithms(True)
device = torch.device("cpu")
generator = torch.Generator().manual_seed(430)
centers = torch.tensor([[-1.0, -1.0], [-1.0, 1.0], [1.0, -1.0], [1.0, 1.0]])
center_labels = torch.tensor([0, 1, 1, 0], dtype=torch.long)
features, targets = [], []
for center, label in zip(centers, center_labels):
features.append(center + 0.28 * torch.randn((100, 2), generator=generator))
targets.append(torch.full((100,), label, dtype=torch.long))
X = torch.cat(features)
y = torch.cat(targets)
train_parts, validation_parts, test_parts = [], [], []
for start in range(0, 400, 100):
order = start + torch.randperm(100, generator=generator)
train_parts.append(order[:60])
validation_parts.append(order[60:80])
test_parts.append(order[80:])
train_index = torch.cat(train_parts)
validation_index = torch.cat(validation_parts)
test_index = torch.cat(test_parts)
assert len(set(train_index.tolist()) & set(test_index.tolist())) == 0
# Fit preprocessing on training features only, then reuse it unchanged.
mean = X[train_index].mean(dim=0)
std = X[train_index].std(dim=0)
def prepare(index):
return ((X[index] - mean) / std).to(device), y[index].to(device)
X_train, y_train = prepare(train_index)
X_validation, y_validation = prepare(validation_index)
X_test, y_test = prepare(test_index)
model = nn.Sequential(
nn.Linear(2, 8),
nn.ReLU(),
nn.Dropout(0.1),
nn.Linear(8, 2),
).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=0.03, weight_decay=0.01)
loss_function = nn.CrossEntropyLoss()
def evaluate(inputs, labels):
model.eval()
with torch.inference_mode():
logits = model(inputs)
loss = loss_function(logits, labels).item()
accuracy = (logits.argmax(dim=1) == labels).float().mean().item()
return loss, accuracy
best_loss, best_epoch, best_state = float("inf"), 0, None
for epoch in range(1, 81):
model.train()
optimizer.zero_grad(set_to_none=True)
training_loss = loss_function(model(X_train), y_train)
training_loss.backward()
optimizer.step()
validation_loss, _ = evaluate(X_validation, y_validation)
if validation_loss < best_loss:
best_loss = validation_loss
best_epoch = epoch
best_state = copy.deepcopy(model.state_dict())
model.load_state_dict(best_state)
train_loss, train_accuracy = evaluate(X_train, y_train)
validation_loss, validation_accuracy = evaluate(X_validation, y_validation)
test_loss, test_accuracy = evaluate(X_test, y_test) # first and only test use
print(f"splits train={len(train_index)} validation={len(validation_index)} test={len(test_index)}")
print(f"shape={tuple(X_train.shape)} dtype={X_train.dtype} device={X_train.device}")
print(f"selected_epoch={best_epoch}")
print(f"train loss={train_loss:.4f} accuracy={train_accuracy:.3f}")
print(f"validation loss={validation_loss:.4f} accuracy={validation_accuracy:.3f}")
print(f"test loss={test_loss:.4f} accuracy={test_accuracy:.3f}")
Output with the pinned CPU environment:
splits train=240 validation=80 test=80
shape=(240, 2) dtype=torch.float32 device=cpu
selected_epoch=80
train loss=0.0309 accuracy=1.000
validation loss=0.0403 accuracy=0.988
test loss=0.0401 accuracy=1.000
The validation result selects among the 80 epoch checkpoints; it is therefore selection evidence, not an unbiased final estimate. The test split is evaluated only after the checkpoint is fixed. Re-running design choices after reading test performance would turn the test split into another validation set and require new untouched evidence.
Perfect test accuracy here is not a production claim. The fixture is deliberately small, balanced, synthetic, and cleanly separated. It contains no subgroup, calibration, shift, asymmetric-cost, privacy, latency, or external-validity evidence. Across other hardware or PyTorch versions, controlled seeds do not guarantee bitwise-identical floating-point results.
Common mistakes
- Standardizing before splitting, which leaks validation/test distribution information into training.
- Tuning hidden width, epochs, regularization, or threshold from the test result.
- Evaluating with dropout active or assuming
inference_mode()callseval()automatically. - Saving a live model object without its code/version/config/data contract instead of an explicit reviewed state and metadata.
- Reporting accuracy without the split sizes, class coverage, loss, selection process, and fixture limitations.
Practice
Increase the noise scale from 0.28 to 0.70 without changing the split or model. Before running, predict which metric should degrade first. Then add a confusion matrix computed after the checkpoint is fixed, and explain why choosing a new architecture from the test confusion matrix would invalidate the final test role.
Next steps
The foundation batch is complete. Return to the Deep Learning & Modern AI overview; the computer-vision path continues with images as tensors, where spatial axes and local structure add new modeling assumptions.
Primary sources
Checked 2026-10-16: PyTorch optimization tutorial, PyTorch CrossEntropyLoss, Autograd mechanics and evaluation mode, PyTorch reproducibility, and PyTorch numerical accuracy.