Code

A model you can reload, and new images

You know how to build the matrix. But it only becomes useful once you have something to put into it: a model that can be loaded from disk, and images nobody has seen. This step makes two small changes — both written for you, because they are plumbing, not an exercise.

1. data.py: a folder of "new images"

make_loaders from lesson 9 splits a folder into training and validation. A fresh test/ folder needs no splitting: it needs every image, untouched, in the same order.

def make_eval_loader(
    root: str, batch_size: int = 32, size: int = 224, rgb: bool = True
) -> tuple[DataLoader, list[str]]:

It calls the same build_transform (same size, same normalisation, no augmentation) and a DataLoader without shuffling. The only new thing is that nothing is split.

2. transfer.py: a model you can rebuild

So far transfer.py saved nothing after training — the model vanished with the process. Three changes:

  • build_resnet(..., pretrained=True) — a new parameter. When you turn it off, a fresh ResNet-18 is created without ImageNet weights and nothing is downloaded. You will need it when we rebuild the architecture only to fill it with saved weights.
  • --out model.pt — where to save.
  • torch.save({...}) at the end.

Look at what we save:

torch.save(
    {"state_dict": model.state_dict(), "classes": classes, "size": args.size},
    args.out,
)

Not just the weights. The class list and the size the model was trained with travel along. If you kept only the state_dict you would lose two things:

  1. The class names. ImageFolder sorts classes in alphabetical order, not in the order you had in your head. With a hand-typed list the same picture gets another name:
right class list : ['coat', 'pullover', 'sneaker', 'tshirt'] -> sneaker
a list typed by hand: ['tshirt', 'sneaker', 'pullover', 'coat'] -> pullover

The same image — a sneaker. The model did not err, but every prediction carries the wrong name, and nothing reports it.

  1. The size. A prediction for one image must prepare it the same way as in training: the same size. If you do not save it, you have to guess.

A bare state_dict (122 keys for ResNet-18) has no class list in it.

The tests grew

test_evaluate.py gained two new blocks: make_eval_loader (every image, order unchanged, three channels, the right size) and build_resnet(pretrained=False) (works offline, the head has as many outputs as there are classes). The earlier seven tests are untouched.

$ python -m unittest
...........
Ran 11 tests in 0.361s

OK

Training now saves the model

If you do not have a model.pt yet, train — the same command as in lesson 10, with --finetune, which now writes the file:

python transfer.py --data data --epochs 5 --lr 0.001 --finetune

It takes a few minutes (on our CPU, in two runs — 148 and 199 seconds). model.pt comes out at ~45 MB (ours: 44,785,163 bytes): ResNet-18 with 11.2 million weights.

data.py and transfer.py show the full files in the panel with the changed lines highlighted. The rest stays exactly as you had it in lesson 10.