Augmentation and balancing — write them yourself
The holes are filled; splitting and normalising work. Two parts remain — this time empty, with no holes: augmentation and class balancing.
What is already written, so you need not
make_loaders has grown. Most importantly, it already knows two things that
are easy to get wrong, so you do not write them:
# Two views of the same files: augmented for training, untouched for validation.
train_ds = datasets.ImageFolder(root, transform=build_transform(augment, mean, std))
val_ds = datasets.ImageFolder(root, transform=build_transform(False, mean, std))
The same files are read twice: the training copy with random changes, the
validation copy never. One shared ImageFolder with augmentation would mean
the validation accuracy is measured on different pictures every time (see step 7).
The only new places in data.py are the two that now say raise NotImplementedError.
1. Augmentation — build_transform
Each time, training pictures are changed a little differently. You do not need new photographs — the same picture is slightly different every epoch.
if augment:
steps += [
random horizontal flip,
random affine change:
rotation ±10°, shift up to 10 % in each direction,
scale from 0.9 to 1.1
]
In place of raise NotImplementedError write steps += [ ... ] with
transforms.RandomHorizontalFlip() and transforms.RandomAffine(...).
Note the order: build_transform first puts Grayscale, then your
augmentation, then ToTensor and Normalize. Random changes act on the picture
(PIL), while normalisation acts on the numbers.
Think about your categories. A horizontal flip changes nothing for a shoe or a T-shirt. But if your categories are "letter b" and "letter d", a flip turns one into the other — and you have broken the labels yourself. Augmentation must preserve what the network is supposed to recognise.
2. Class balancing — make_loaders
If one class has 20 pictures and the others 200, training batches are mostly made of the big classes. One answer is to draw the rare classes more often:
for each training picture:
weight = 1 / (how many training pictures are in its class)
sampler = WeightedRandomSampler(weights, num_samples=len(train_idx))
In place of raise NotImplementedError write three lines: a Counter to count
the classes (already imported), the list of weights, and a
WeightedRandomSampler. The result must land in the variable sampler.
It draws with replacement: the same rare photograph may be drawn several times per epoch. That is one of the costs — see step 7.
The test
test_data.py has grown. The new tests check that:
- without augmentation a picture loads identically every time;
- with augmentation — differently;
- validation pictures with
augment=Trueare the same as without it; - without balancing, classes appear as often as they exist (
[8, 32, 32]); - with balancing, the rare class (10 pictures) appears more than 85 times in 5 epochs (without balancing — 40; an even split would be ~120).
Run python -m unittest test_data (you already have the holes filled):
.EE.....E..
======================================================================
ERROR: test_augmentation_changes_training_images_every_time (...)
NotImplementedError: write the augmentation steps
ERROR: test_balancing_draws_the_rare_class_far_more_often (...)
NotImplementedError: write the balancing sampler
ERROR: test_validation_images_are_never_augmented (...)
NotImplementedError: write the augmentation steps
----------------------------------------------------------------------
Ran 11 tests in 0.8s
FAILED (errors=3)
(Shortened.) Eight tests pass, three are waiting for your code.
data.pyis still shown with its holes in the panel — the panel shows the skeleton, not your file. In your copy they are filled.
When it does not pass
AssertionError: 7 not greater than 85 : alpha drawn only 7 times in 5 epochs— the weights are inverted: you wrotecounts[...]rather than1.0 / counts[...]. Now big classes are drawn even more often.AssertionError: 40 not greater than 85— the sampler was built but not assigned to the variablesampler(it stayedNone).ValueError: sampler option is mutually exclusive with shuffle— if you try changing theDataLoaderlines. You need not:shuffle=(sampler is None)already handles it.