Drills

Drills

Four tasks, from easy to harder. All of them live in one new file, drills.py, next to evaluate.py (on the right): it already has the new-image matrix TEST, the class names and empty skeletons.

Try each function on TEST and compare with the given value. Try on your own first; the solutions are at the end of the step.


1. accuracy(cm)

Return the accuracy (0…1) from the confusion matrix alone: the diagonal divided by everything.

On TEST you should get 0.935.


2. most_confused(cm, classes)

Return the triple (true class, called class, how many times) — the largest value outside the diagonal.

On TEST: ('coat', 'pullover', 11).

Hint: a copy of the matrix with zeros on the diagonal, then argmax. Ask yourself: what must you not damage — the original matrix?


3. macro_f1(cm) — and why it exists

Return the average F1 across classes (every class weighs the same), using your own class_scores.

On TEST: 0.9357.

Now build the matrix for "a model that always answers not a sandal" — made up, 10,000 images, 990 of them sandals:

always_no = torch.tensor([[9010, 0], [990, 0]])

Compute accuracy and macro_f1. Explain in one sentence why the two numbers differ so much. (It is the same trap you may recognise from lesson 3: the accuracy of a neuron that always answers "not a sandal" was 90.1 %.)


4. Error: margin_of_error(correct, n) and images_needed(p, margin)

The first: with the formula from step 7, return half the width of the 95 % interval (0…1). The second: how many images are needed so that the error is no larger than margin, when the accuracy is about p.

Check: margin_of_error(153, 160) ≈ 0.0317, margin_of_error(374, 400) ≈ 0.0242.

Then answer: how many new images do you need for the error to be ±2 points at about 90 % accuracy? And ±1 point? What does this say about a test/ folder with 20 photos per class?


Solutions

def accuracy(cm):
    return cm.diagonal().sum().item() / cm.sum().item()


def most_confused(cm, classes):
    off = cm.clone()          # do not touch the original
    off.fill_diagonal_(0)     # right answers do not matter here
    i, j = divmod(off.argmax().item(), cm.shape[1])
    return classes[i], classes[j], off[i, j].item()


def macro_f1(cm):
    return sum(class_scores(cm, i)[2] for i in range(cm.shape[0])) / cm.shape[0]


def margin_of_error(correct, n):
    p = correct / n
    return 1.96 * math.sqrt(p * (1 - p) / n)


def images_needed(p, margin):
    return math.ceil(1.96**2 * p * (1 - p) / margin**2)

What you should get:

accuracy(TEST)               0.935
most_confused(TEST, CLASSES) ('coat', 'pullover', 11)
macro_f1(TEST)               0.9357
always_no:  accuracy 0.901   macro_f1 0.474
margin_of_error(153, 160)    0.0317
margin_of_error(374, 400)    0.0242
images_needed(0.9, 0.02)     865
images_needed(0.9, 0.01)     3458

Explanation of 3. always_no says "not a sandal" and is right 9010 times out of 10,000 — 90.1 %. But the one class the whole task is about has recall 0.00 and precision 0.00. Macro F1 averages over classes, not images, so that zero becomes visible: 0.474. When classes are unequal, accuracy lulls you — the same lesson as in lesson 9 with coat (1 in 4).

Explanation of 4. ±2 points takes 865 images, ±1 point takes 3,458. The error shrinks like 1/√n: to halve it you need four times as many images. With 20 photos per class and ~90 % accuracy, one class's recall carries more than ±13 points (1.96·√(0.9·0.1/20) ≈ 0.13) — so your small test/ can show a direction, but not a fine difference between two models.