Section 1 · Lesson 5

Training the model

The recipe is short: BCE plus Dice loss, light augmentation, AdamW with a one-cycle schedule, and a holdout score after every epoch.

You'll learn

The recipe

SettingValue in r34_baseline
Data9,114 training images, 2,279 holdout images (lesson 2)
ModelUnet with resnet34, ImageNet weights
LossBCE with logits + multilabel Dice (dice_w = 1.0)
OptimiserAdamW, learning rate 3e-4, weight decay 1e-4
ScheduleOne-cycle, peak at 10% of the steps
Batch size and epochs16 and 10
DeviceApple-silicon GPU (mps); 203 to 251 seconds per epoch

The whole training loop is short enough to read in one go:

opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-4)
steps = args.epochs * len(tr_dl)
sched = torch.optim.lr_scheduler.OneCycleLR(opt, max_lr=args.lr, total_steps=steps, pct_start=0.1)
bce = torch.nn.BCEWithLogitsLoss()
dice = pos_dice_loss if args.loss == "pos" else smp.losses.DiceLoss("multilabel", from_logits=True)

for img, msk in tr_dl:
    logits = model(img)
    loss = bce(logits, msk) + args.dice_w * dice(logits, msk)
    opt.zero_grad(set_to_none=True)
    loss.backward()
    opt.step()
    sched.step()

Loss

The loss adds two terms. Binary cross-entropy (BCE) looks at every pixel on its own and gives smooth gradients. Dice loss is \(1 - \mathrm{Dice}\) computed on the predicted probabilities, so it directly pushes up the overlap that the metric measures. It also copes with the fact that defect pixels are a tiny fraction of the image.

The project added a second option, --loss pos, for an experiment. It averages Dice only over pairs whose true mask is non-empty, so empty pairs don't pull the loss toward predicting nothing:

def pos_dice_loss(logits, targets):
    probs = logits.sigmoid()
    inter = (probs * targets).sum((-2, -1))
    denom = probs.sum((-2, -1)) + targets.sum((-2, -1))
    dice = (2 * inter + 1) / (denom + 1)
    pos = (targets.sum((-2, -1)) > 0).float()
    return ((1 - dice) * pos).sum() / pos.sum().clamp_min(1)

The r34_pos run used this loss with --oversample 2.0, which draws images containing defect_a or defect_b twice as often. The folder holds only its arguments file, and its log is empty, so this course can't report a result for it.

Augmentation

Augmentation shows the model slightly different versions of each image. The project applies these with the Albumentations library, to the image and the mask together:

A.Compose([
    A.HorizontalFlip(p=0.5),
    A.VerticalFlip(p=0.5),
    A.RandomBrightnessContrast(0.15, 0.15, p=0.5),
    A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=0,
                       border_mode=0, p=0.3),
])

The mask must receive the same geometric change as the image, otherwise the labels no longer line up. Brightness and contrast change only the image.

Augmentation lab

Real training image with its defect_d mask. Change the controls, or press Random to draw one augmentation with the project's probabilities. The mask moves with the image.

Preview only: the browser draws brightness and contrast with its own filter, which is close to but not identical to the Albumentations version.

The learning-rate schedule

The one-cycle schedule starts at a low rate, ramps up to max_lr over the first 10% of the steps, and then decays smoothly toward zero. Warm-up protects the pretrained weights early on, and the long decay lets the model settle. PyTorch's defaults start at max_lr / 25 and end at that start value divided by 10,000.

One-cycle schedule

There are 569 optimiser steps per epoch (9,114 images, batch size 16, last partial batch dropped). The chart uses PyTorch's default cosine annealing with max_lr 3e-4.

Reading the curves

The training script prints one JSON line per epoch with the loss and the holdout score at four thresholds. Here are the real numbers for the baseline:

EpochTrain lossHoldout score at 0.3Foreground Dice at 0.3Seconds
Two line charts: training loss falls from 1.13 to 0.44 for ResNet-34, and holdout score rises with some noise.
Training loss and holdout score for the two runs with logged histories.

Compare the runs

Four observations from the files:

The Milestone 1 starter model, trained for 8 epochs, reports a final validation loss of 0.2785 and an assessment score of 0.5627 to 0.5677 across thresholds. It is a reference point, not a rival: its numbers come from a different script and loss.

Why does the training code apply the same flip to both the image and the mask?

  1. Flipping the mask makes the loss smaller.
  2. The mask pixels must stay aligned with the image pixels they label.
  3. Flips are only applied at test time.

If only the image flipped, the labelled pixels would point at the wrong place. Geometric augmentations must move the image and mask together.

The one-cycle schedule with pct_start=0.1 and 10 epochs reaches its peak learning rate roughly when?

  1. At the end of epoch 1
  2. At the end of epoch 5
  3. At the last step

10% of the steps is one epoch of ten. After the peak, the rate decays toward zero for the remaining nine epochs.