Your first PyTorch model
Time to put the pieces together. In this lesson you will train a real classifier on Fashion-MNIST — 70,000 small grayscale images of clothing (T-shirts, trousers, sandals, bags) in 10 classes. It is small enough to train on a laptop CPU in a few minutes yet harder than handwritten digits, so it is a good first project for anyone interested in apparel or e-commerce catalogues.
Step 1: Load the data
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
tfm = transforms.ToTensor() # PIL image -> float tensor in [0, 1], shape (1, 28, 28)
train_ds = datasets.FashionMNIST("data", train=True, download=True, transform=tfm)
test_ds = datasets.FashionMNIST("data", train=False, download=True, transform=tfm)
train_dl = DataLoader(train_ds, batch_size=64, shuffle=True)
test_dl = DataLoader(test_ds, batch_size=256)
x, y = next(iter(train_dl))
print(x.shape, y.shape) # torch.Size([64, 1, 28, 28]) torch.Size([64])
print(train_ds.classes)
Training data is shuffled so each batch is a fair mix of classes; test data does not need shuffling.
Step 2: Define the model
Subclassing nn.Module is the standard pattern. Layers go in __init__, the computation goes in forward.
class FashionNet(nn.Module):
def __init__(self, n_classes: int = 10):
super().__init__()
self.net = nn.Sequential(
nn.Flatten(), # (N, 1, 28, 28) -> (N, 784)
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, n_classes), # raw scores (logits)
)
def forward(self, x):
return self.net(x)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = FashionNet().to(device)
print(model)
Notice there is no softmax at the end. nn.CrossEntropyLoss expects raw logits and applies log-softmax internally, which is more numerically stable.
Step 3: Loss and optimizer
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
Step 4: Train and evaluate
def train_one_epoch():
model.train()
total = 0.0
for xb, yb in train_dl:
xb, yb = xb.to(device), yb.to(device)
optimizer.zero_grad()
loss = loss_fn(model(xb), yb)
loss.backward()
optimizer.step()
total += loss.item() * xb.size(0)
return total / len(train_dl.dataset)
@torch.no_grad()
def evaluate():
model.eval()
correct = 0
for xb, yb in test_dl:
xb, yb = xb.to(device), yb.to(device)
preds = model(xb).argmax(dim=1)
correct += (preds == yb).sum().item()
return correct / len(test_dl.dataset)
for epoch in range(5):
loss = train_one_epoch()
acc = evaluate()
print(f"epoch {epoch + 1}: train loss {loss:.3f}, test acc {acc:.3%}")
Expect roughly 87–89% test accuracy after five epochs. That is a solid baseline; the CNN lesson will push it above 90%.
Step 5: Predict and save
model.eval()
img, label = test_ds[0]
with torch.no_grad():
probs = torch.softmax(model(img.unsqueeze(0).to(device)), dim=1)
print("predicted:", train_ds.classes[probs.argmax().item()],
"| actual:", train_ds.classes[label])
torch.save(model.state_dict(), "fashion_net.pt")
# later, in another script
restored = FashionNet()
restored.load_state_dict(torch.load("fashion_net.pt", map_location="cpu"))
restored.eval()
Save the state_dict (the weights), not the whole model object; it is portable and robust to code refactoring.
What each piece does
| Piece | Role |
|---|---|
Dataset |
Knows how to fetch one example |
DataLoader |
Batches, shuffles and loads in parallel |
nn.Module |
Holds layers and parameters, defines forward |
| Loss function | Scores predictions against labels |
| Optimizer | Updates parameters from gradients |
model.train() / model.eval() |
Switch dropout and batch-norm behaviour |
torch.no_grad() |
Disables gradient tracking for faster inference |
Common mistakes
- Adding
nn.SoftmaxbeforeCrossEntropyLoss— the loss then applies softmax twice and training stalls. - Forgetting
model.eval()during evaluation. - Moving the model to GPU but not the batches (or vice versa).
- Printing only the last batch's loss, which is noisy; average over the epoch instead.
Practice
Train FashionNet for 10 epochs and record test accuracy after each one. Then try three changes one at a time — a hidden size of 512, a learning rate of 1e-2, and SGD with momentum 0.9 instead of Adam — and write a short table comparing final accuracy. Finally, print the five test images the model is most confident about but gets wrong, and describe any pattern you notice.