AI Hints
Home›Computer Vision Tutorial›Trust in Production
Computer Vision · Lesson 71 of 111

Finding the Unknown

Anomaly detection.

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

A new kind of disease appears on the apples, one that nobody's ever seen. Your bruise model was never trained on it, so it says, fine. And how would you ever collect examples of defects that haven't happened yet? There's a neat way round this. Stop learning what's bad. Learn what's normal.

Finding the Unknown
From the video: Finding the Unknown
  • A disease nobody's seen
  • The model says 'fine'
  • Learn what's normal

Learning normal

We train on good apples only. There isn't a single bruise in the training set. PCA learns to compress and rebuild images of normal apples. It's good at that, because it's seen thousands. But it's bad at rebuilding something that it's never seen. So the rebuilding error becomes our score. We set a limit from good images only. Run it. Five percent of good apples are flagged, which is the limit that we chose. And it flags one hundred percent of the bruised apples, without ever having seen one. That's the power of this approach. Today's best tools, such as PatchCore, use the same idea, with deep features.

anomaly.pypython
import numpy as np
from sklearn.decomposition import PCA
from patch import make

good, _ = make(1500, bruise_rate=0)
test_good, _ = make(200, seed=5, bruise_rate=0)
test_bad, _ = make(200, seed=6, bruise_rate=1)

flat = lambda x: x.reshape(len(x), -1).numpy()
pca = PCA(30, random_state=0).fit(flat(good))

def score(x):
    a = flat(x)
    return ((a - pca.inverse_transform(pca.transform(a))) ** 2).mean(1)

limit = np.quantile(score(test_good), 0.95)
print(f"Limit: {limit:.5f}")
print(f"Good flagged:    {(score(test_good) > limit).mean():.0%}")
print(f"Bruised flagged: {(score(test_bad) > limit).mean():.0%}")
Output
Limit: 0.00186
Good flagged:    5%
Bruised flagged: 100%
Also uses patch.py
import numpy as np
import cv2
import torch

def make(n, seed=0, light=1.0, bruise_rate=0.5):
    """Simulated apple photos. Label 1 means bruised."""
    rng = np.random.default_rng(seed)
    X, y = [], []
    for _ in range(n):
        img = np.full((48, 48, 3), (70, 75, 80), np.uint8)
        c = tuple(int(v) for v in rng.integers(19, 30, 2))
        r = int(rng.integers(15, 20))
        cv2.circle(img, c, r, (35, 40, int(rng.integers(150, 225))), -1, cv2.LINE_AA)
        bad = rng.random() < bruise_rate
        if bad:
            at = (c[0] + int(rng.integers(-8, 9)), c[1] + int(rng.integers(-8, 9)))
            cv2.circle(img, at, int(rng.integers(3, 6)), (25, 45, 70), -1, cv2.LINE_AA)
        img = img * light + rng.normal(0, 8, img.shape)
        X.append(img.clip(0, 255) / 255)
        y.append(int(bad))
    X = torch.tensor(np.array(X), dtype=torch.float32).permute(0, 3, 1, 2)
    return X, torch.tensor(y)

def net():
    return torch.nn.Sequential(
        torch.nn.Conv2d(3, 16, 3, padding=1), torch.nn.ReLU(), torch.nn.MaxPool2d(2),
        torch.nn.Conv2d(16, 32, 3, padding=1), torch.nn.ReLU(), torch.nn.MaxPool2d(2),
        torch.nn.Flatten(), torch.nn.Linear(32 * 12 * 12, 2))

def train(model, X, y, epochs=8):
    opt = torch.optim.Adam(model.parameters(), lr=2e-3)
    for _ in range(epochs):
        for i in torch.randperm(len(X)).split(64):
            loss = torch.nn.functional.cross_entropy(model(X[i]), y[i])
            opt.zero_grad(); loss.backward(); opt.step()
    return model

def accuracy(model, X, y):
    return (model(X).argmax(1) == y).float().mean().item()

When to use anomaly detection

Use it when defects are rare or unpredictable, and normal products look alike. It's a perfect match for factories. It's less good when normal items vary widely. It can't tell you what the defect is, only that something's wrong. So teams often combine the two approaches. A classifier for known defects, and anomaly detection as a safety net.

Rare, unknown defectsa perfect match
Consistent productsnormal looks alike
No defect nameonly 'something's wrong'
Combine themclassifier plus safety net

Summary

Learn what's normal, and anything else stands out. Next, why you shouldn't trust how confident a model says it is.

Key takeaways
  • Train on good examples only
  • Rebuilding error is the score
  • Use it as a safety net
Next up: Overconfident Models — when ninety-eight percent sure means wrong