Run it and measure — honestly
All 11 tests pass. Now we connect data.py to train.py.
What changes in train.py
The training loop does not change. Only where the data comes from and how many outputs the model has:
from data import class_counts, make_loaders
...
def build_model(name, hidden, dropout, num_classes: int = 10):
...
parser.add_argument("--data", default=None, help="folder of class folders")
parser.add_argument("--augment", action="store_true")
parser.add_argument("--balance", action="store_true")
parser.add_argument("--batch-size", type=int, default=64)
...
if args.data:
print("class counts:", class_counts(args.data))
loader, val_loader, classes = make_loaders(
args.data, args.batch_size, augment=args.augment, balance=args.balance
)
else:
...
model = build_model(args.model, tuple(args.hidden), args.dropout, len(classes))
In the panel you see two files in our versions: data.py with the holes
filled and the augmentation / balancing code written, and train.py. If yours
pass the tests, keep yours.
build_model(..., num_classes) matters. Before, the model always had 10
outputs. With a folder of 4 classes, 10 outputs raise no error — the model
quietly has 6 unused outputs. len(classes) comes from the folder.
Run it — two commands
python train.py --model cnn --data standin_data --epochs 30 --batch-size 32 --dropout 0.3
python train.py --model cnn --data standin_data --epochs 30 --batch-size 32 --dropout 0.3 --augment
For your own photographs replace standin_data with data (or your folder's name).
Our runs on the stand-in dataset (4 classes × 200; 640 for training, 160 for validation; seed 42):
class counts: {'coat': 200, 'pullover': 200, 'sneaker': 200, 'tshirt': 200}
cnn: 11,076 parameters, 4 classes
epoch 1 loss 0.5417 train acc 74.8% | val loss 0.5127 val acc 76.9%
...
epoch 30 loss 0.1059 train acc 95.9% | val loss 0.2072 val acc 92.5%
final: train 95.9% val 92.5% gap +3.4%
class counts: {'coat': 200, 'pullover': 200, 'sneaker': 200, 'tshirt': 200}
cnn: 11,076 parameters, 4 classes
epoch 1 loss 0.7723 train acc 64.2% | val loss 0.6535 val acc 69.4%
...
epoch 30 loss 0.2731 train acc 90.0% | val loss 0.2438 val acc 91.9%
final: train 91.1% val 91.9% gap -0.8%
Your numbers will differ by roughly 1–2 points.
What it means — carefully
| train | val | gap | |
|---|---|---|---|
| without augmentation | 95.9 % | 92.5 % | +3.4 |
| with augmentation | 91.1 % | 91.9 % | −0.8 |
- The gap "vanished" (+3.4 → −0.8). But a negative gap is no miracle: "training accuracy" here is measured on augmented pictures, which are harder than ordinary ones. It fell (95.9 → 91.1) while validation did not.
- Validation accuracy did not go up. 92.5 % and 91.9 % — a difference of 0.6 points, i.e. one picture out of 160. A tie.
- That is not just our seed's bad luck. For three seeds (0, 1, 2): 91.7 % without and 91.5 % with augmentation (30 epochs); 91.7 % and 92.3 % (60 epochs). The differences are a few pictures out of 160, with no direction.
What augmentation really did: it reduced the overlap with the training pictures. The network can no longer memorise them so easily. What it did not do: raise accuracy on our dataset, because that dataset is easy anyway — four well-separated classes at 28 × 28.
With less data — worse still?
We tried 60 pictures per class (48 for validation; 40 epochs; three seeds):
augment=False: val ['0.854', '0.938', '0.938'] mean 0.910
augment=True : val ['0.875', '0.854', '0.875'] mean 0.868
Here augmentation looks worse (86.8 % vs 91.0 %). But one validation picture = 2.1 points, and the spread across seeds is as large as the difference (from 85.4 % to 93.8 %), so we prove nothing. But one thing you should not expect: that augmentation "automatically rescues a small dataset".
If your data is harder (natural photographs, a lot of noise), augmentation may help more: in lesson 10 you will see an experiment on CIFAR-10 photographs where the same augmentation lifted a from-scratch CNN from 49.4 % to 56.2 % (one seed, 160 validation pictures). But check on your own data; do not take our word for it.
When it does not pass
mat1 and mat2 shapes cannot be multiplied (1792x28 and 784x256)— you ran--model mlp --data standin_data. An MLP wants flat[N, 784], while the folder gives[N, 1, 28, 28]. (1792 = 64 · 28 — the batch gets "merged".)--model cnnaccepts both shapes.- No error, but the model has 10 outputs — you forgot to pass
num_classes. Six outputs are never trained, yet the loss still computes. The opposite case (4 folders, 3 outputs) would giveIndexError: Target 3 is out of bounds— at least a loud error. FileNotFoundError: Couldn't find any class folder in data.—--datapoints at a folder with no class subfolders (too high up or too low down).FileNotFoundError: Found no valid file for the classes ...— a class folder has nojpg/png/...files; check thatprepare.pyreally wrote there.