Section 1 · Lesson 7

Inference, ensembling, and submission

At prediction time the project averages probabilities over flipped copies and over models, applies the tuned rules, and writes each mask as an RLE string.

You'll learn
Test image128 × 800 Each model ×3 viewsnone, h-flip, v-flip Averageprobabilities Post-processthr, comp_min,area_min RLE encodeone row perimage and class
The inference pipeline in src/predict.py.

Test-time augmentation

Test-time augmentation (TTA) predicts on a few altered copies of the same image and averages the results. predict_probs in src/train.py uses the original image and two flips. It flips each prediction back before averaging:

p = torch.sigmoid(model(img))
if tta:
    p = p + torch.sigmoid(model(img.flip(-1))).flip(-1)   # horizontal flip
    p = p + torch.sigmoid(model(img.flip(-2))).flip(-2)   # vertical flip
    p = p / 3
out.append((p * 255).round().byte().cpu().numpy())

The model saw flipped images in training, so the flipped views are valid inputs. Averaging smooths out view-specific mistakes. On the baseline, TTA lifts the holdout score at tau 0.3 from 0.6284 to 0.6420.

Ensembling

An ensemble averages the probabilities of several models, each of which already includes its TTA. src/evaluate.py does this for holdout predictions, and src/predict.py does the same on the test images:

# predict.py
acc = None
for name in args.runs:
    cfg = json.load(open(KAGGLE / "runs" / name / "args.json"))
    model = build_model(cfg["arch"], cfg["encoder"], pretrained=False).to(device)
    model.load_state_dict(torch.load(KAGGLE / "runs" / name / "model.pt", map_location=device))
    p = predict_probs(model, dl, device, tta=True).astype(np.float32)
    acc = p if acc is None else acc + p
probs = np.round(acc / len(args.runs)).astype(np.uint8)

Averaging views and models

One pixel, two models, three views each. Set the probabilities and the threshold. Watch how a single confident outlier loses influence.

Model A (views: none, h-flip, v-flip)

Model B (views: none, h-flip, v-flip)

What the project files do and don't show. The code supports ensembles: evaluate.py r34_baseline effb3 --out ens averages two runs and tunes the post-processing on the average. But the repository holds holdout probabilities and an evaluation only for r34_baseline. The effb3 folder has weights and no probabilities. So the project has no measured ensemble score, and this course doesn't report one. The 0.6486 figure is a single ResNet-34 model with TTA and tuned post-processing.

Writing the submission

Once the probabilities are averaged, predict.py applies the tuned parameters (--params runs/ens/postproc.json) and encodes each non-empty mask. The docstring shows models retrained on all the data (--full), whose names end with _full:

uv run python src/predict.py r34_full effb3_full --params runs/ens/postproc.json
for i, image_id in enumerate(ids.image_id):
    mask = postproc.apply(probs[i], params)
    for c, cls in enumerate(CLASSES):
        rles[(image_id, cls)] = rle_encode(mask[c]) if mask[c].any() else " "
sub["mask_rle"] = [rles[(i, c)] for i, c in zip(sub.image_id, sub.defect_type)]
sub.to_csv(args.out, index=False)

The submission mirrors sample_submission.csv: the columns ID, image_id, defect_type, and mask_rle, with four rows for each of the 3,000 test images, so 12,000 rows. A single space marks "no defect". The encoder is the inverse of the decoder from lesson 1:

def rle_encode(mask):
    pixels = np.concatenate([[0], mask.flatten(order="F").astype(np.uint8), [0]])
    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1
    runs[1::2] -= runs[::2]
    return " ".join(map(str, runs))

The code writes submission.csv locally. The project notebook states that the file is written locally and isn't uploaded, and the folder doesn't contain one, so the course reports no leaderboard score.

From probabilities to a submission row

Choose a holdout image and a class. The demo applies the project's tuned post-processing and shows the row it would write. Then it decodes the string again to check the round trip.

The demo uses half-resolution maps (64 × 400), so the pixel numbers differ from a real 128 × 800 submission. The method is the same.

Why does predict_probs flip each TTA prediction back before averaging?

  1. To make the image brighter.
  2. So that all three probability maps line up pixel by pixel before they are averaged.
  3. Because the metric expects flipped masks.

A prediction on a flipped image is itself flipped. Flipping it back puts every probability on the same pixel as in the original orientation.

What goes in mask_rle when the post-processing predicts no defect?

  1. A single space.
  2. The number 0.
  3. The row is omitted.

The submission keeps four rows per image. predict.py writes " " for empty masks, as the sample submission does.