AI Hints
Computer Vision · Lesson 16 of 111

Transfer Learning for Vision

Teaching a giant a new task.

⏱ 2 min read▶ Video at 26:20🎓 Free
Watch this lesson in the Computer Vision courseWatch on YouTube ↗Full Computer Vision course

Orchard Vision has photographed fifteen hundred apples. Good ones and bruised ones. That's nowhere near enough to train a network from scratch, which would need millions. But it's plenty, if you borrow the eyes of a pretrained model.

Transfer Learning for Vision
From the video: Transfer Learning for Vision
  • Fifteen hundred photos
  • Far too few from scratch
  • Borrow pretrained eyes

Features plus a new head

We remove the network's final layer, so it now gives five hundred and twelve numbers describing each image. We run all of our images through it once. Then we train a single linear layer on those numbers, which takes only seconds. We use small photos here in place of apples. Run it. Very strong accuracy, from fifteen hundred images, with almost no compute. This is often the first thing that you should try on a new vision problem.

transfer.pypython
import torch
from torch import nn
from torchvision import datasets, models

w = models.ResNet18_Weights.DEFAULT
tf = w.transforms()
train = torch.utils.data.Subset(datasets.CIFAR10("data", True, tf), range(1500))
test = torch.utils.data.Subset(datasets.CIFAR10("data", False, tf), range(500))

net = models.resnet18(weights=w).eval()
net.fc = nn.Identity()

def features(ds):
    xs, ys = [], []
    with torch.no_grad():
        for x, y in torch.utils.data.DataLoader(ds, batch_size=64):
            xs.append(net(x)); ys.append(y)
    return torch.cat(xs), torch.cat(ys)

Xtr, ytr = features(train)
Xte, yte = features(test)

head = nn.Linear(512, 10)
opt = torch.optim.AdamW(head.parameters(), lr=1e-3)
for _ in range(150):
    loss = nn.functional.cross_entropy(head(Xtr), ytr)
    opt.zero_grad(); loss.backward(); opt.step()

print("Training images:", len(ytr))
print("Test accuracy:", round((head(Xte).argmax(1) == yte).float().mean().item(), 3))
Output
Training images: 1500
Test accuracy: 0.792

Getting more from your data

When data is tight, augmentation creates useful variations. Crops, flips, colour changes, and small rotations. Run it. One photo becomes eight quite different views. The label stays the same, so the model learns what really matters. For apples, colour jitter should be gentle though, since colour tells you about ripeness. Always ask what your augmentation might break.

augment.pypython
import torch
import numpy as np
from PIL import Image
from torchvision.transforms import v2
from skimage import data
import matplotlib.pyplot as plt

image = Image.fromarray(data.chelsea())
aug = v2.Compose([v2.RandomResizedCrop(240, scale=(0.5, 1.0)), v2.RandomHorizontalFlip(),
                  v2.ColorJitter(0.4, 0.4, 0.4, 0.05), v2.RandomRotation(12)])

torch.manual_seed(0)
fig, ax = plt.subplots(2, 4, figsize=(9, 4.4))
for a in ax.ravel():
    a.imshow(aug(image)); a.axis("off")
plt.tight_layout(); plt.savefig("augment.png", dpi=110)
print("One apple photo can become endless training examples")
Output
One apple photo can become endless training examples

Which mistakes cost more?

Finally, measure the right thing. Here are one hundred apples, ten of them bruised. Accuracy is ninety five percent. But look at bruised recall in the report. It's only seventy percent, so three bad apples slipped through. In a food business, that matters more than the overall number. Always read the rare class.

apple_report.pypython
import numpy as np
from sklearn.metrics import confusion_matrix, classification_report

truth = np.array(["good"] * 90 + ["bruised"] * 10)
pred = truth.copy()
pred[np.array([3, 17])] = "bruised"
pred[np.array([92, 95, 98])] = "good"

print(confusion_matrix(truth, pred, labels=["good", "bruised"]))
print(classification_report(truth, pred, digits=2))
Output
[[88  2]
 [ 3  7]]
              precision    recall  f1-score   support

     bruised       0.78      0.70      0.74        10
        good       0.97      0.98      0.97        90

    accuracy                           0.95       100
   macro avg       0.87      0.84      0.85       100
weighted avg       0.95      0.95      0.95       100

Putting a price on mistakes

A missed bruise might cost two dollars in returns. Throwing away a good apple costs twenty five cents. So where's the best setting? We compare three thresholds, in dollars. Run it. The cheapest option isn't always the most accurate one. Now the owner can make a business decision. This is how a vision system goes from a nice demo to something that pays for itself.

apple_cost.pypython
cost_missed_bruise = 2.0
cost_false_reject = 0.25
apples = 100000
rate = 0.04

for name, miss, reject in [("Strict", 0.05, 0.08), ("Balanced", 0.15, 0.03), ("Loose", 0.40, 0.01)]:
    missed = apples * rate * miss * cost_missed_bruise
    wasted = apples * (1 - rate) * reject * cost_false_reject
    print(f"{name:<9} missed ${missed:>6,.0f}  wasted ${wasted:>6,.0f}  total ${missed + wasted:>6,.0f}")
Output
Strict    missed $   400  wasted $ 1,920  total $ 2,320
Balanced  missed $ 1,200  wasted $   720  total $ 1,920
Loose     missed $ 3,200  wasted $   240  total $ 3,440

Summary

Pretrained features, gentle augmentation, and the right metric. AppleEye can now grade an apple in a photo. But the sorting line has many apples, all at once. Next, finding each object in a scene.

Key takeaways
  • Replace the head, train it
  • Augment with care
  • Price your mistakes
Next up: Object Detection — what, where, and how many