← All courses ← Course home

Lesson 02 of 07

Train ClothingCNN on Fashion-MNIST

Train the shared model on a Mac with MPS or CPU. Save clothing.pt. Then check that the NCHW float32 tensor shapes still match.

Agent brief (llms.md)

Environment setup

Use macOS with Python 3.11 or newer. uv manages the virtual environment. PyTorch uses Apple Silicon MPS when it is available, and CPU if it is not. This course does not need CUDA.

curl -LsSf https://astral.sh/uv/install.sh | sh
mkdir -p clothing-android-litert && cd clothing-android-litert
uv init --python 3.11
uv venv
uv add torch torchvision
uv run python -c "import torch, torchvision; print(torch.__version__, torchvision.__version__); print('MPS:', torch.backends.mps.is_available())"
uv run python train.py

Save the script as train.py and always run it with uv run. If you do not have Apple Silicon, the script uses CPU.

Fashion-MNIST is a set of 28×28 grayscale images and ten clothing classes. Keep this exact label order: T-shirt/top, Trouser, Pullover, Dress, Coat, Sandal, Shirt, Sneaker, Bag, Ankle boot. The Android app must use the same order.

Complete training script

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

DEVICE = torch.device("mps" if torch.backends.mps.is_available() else "cpu")
BATCH_SIZE = 128
NUM_CLASSES = 10
transform = transforms.ToTensor()  # float32 grayscale, [0, 1]
train_data = datasets.FashionMNIST("data", train=True, download=True, transform=transform)
test_data = datasets.FashionMNIST("data", train=False, download=True, transform=transform)
train_loader = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(test_data, batch_size=BATCH_SIZE)

class ClothingCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
        )
        self.classifier = nn.Sequential(
            nn.Flatten(), nn.Linear(64 * 7 * 7, 128),
            nn.ReLU(), nn.Linear(128, NUM_CLASSES),
        )
    def forward(self, image):
        return self.classifier(self.features(image))

model = ClothingCNN().to(DEVICE)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
print("training on", DEVICE)
for epoch in range(3):
    model.train()
    for images, labels in train_loader:
        images, labels = images.to(DEVICE), labels.to(DEVICE)
        optimizer.zero_grad()
        loss_fn(model(images), labels).backward()
        optimizer.step()
    model.eval()
    correct = total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            predictions = model(images.to(DEVICE)).argmax(1).cpu()
            correct += (predictions == labels).sum().item()
            total += labels.numel()
    print(f"epoch {epoch + 1}: accuracy={correct / total:.3f}")

model = model.cpu().eval()
torch.save(model.state_dict(), "clothing.pt")
with torch.no_grad():
    image, label = test_data[0]
    logits = model(image.unsqueeze(0))
    assert image.shape == (1, 28, 28)
    assert logits.shape == (1, NUM_CLASSES)
    print("sample:", label, "->", logits.argmax(1).item())

Check the tensor contract

model = ClothingCNN()
model.load_state_dict(torch.load("clothing.pt", map_location="cpu", weights_only=True))
model.eval()
example = torch.zeros(1, 1, 28, 28, dtype=torch.float32)
logits = model(example)
assert example.shape == (1, 1, 28, 28)
assert example.dtype == torch.float32
assert logits.shape == (1, 10)
assert logits.dtype == torch.float32
print("contract OK")

clothing.pt stores the trained weights, so keep the ClothingCNN class for conversion. Move the model to CPU before you export. Then copy this file to the Linux machine you use for conversion.

Key concepts

  • Fashion-MNIST is 28x28 grayscale images and ten clothing classes in a fixed order.
  • ClothingCNN uses ToTensor() so pixels are float32 in 0-1.
  • Pick mps when available, else cpu. No CUDA.
  • Move the model to CPU, save clothing.pt, and check shapes [1, 1, 28, 28] in and [1, 10] out.

Takeaways

  • Keep the ClothingCNN class. Linux conversion needs the same definition.
  • Do not apply softmax during training. Cross-entropy expects raw logits.
  • Copy clothing.pt to the Linux host before lesson 03.