learn-data-centric-ai-with-phoebe / Session 3 of 6
Learn Data-Centric AI with Phoebe · Session 3 of 6

Finding the wrong labels

Nobody reads 800 rows to find the 133 that are wrong. You need a detector that points at the likely ones, and you need to know how often it is right. Tonight we build confident learning by hand in numpy on the same real digits and the same declared plant as session 1: votes that never come from a row's own label, a separate bar for every digit, and a ten by ten table of counts whose off-diagonal cells are the flags. Then we score it honestly, including the rows it cannot see.

🟠 Advanced ML engineers, data scientists Python, numpy 45 min
0-3 · Welcome 3-18 · Votes, bars and the joint 18-40 · Build-along: confident learning in numpy 40-45 · Q&A
Part 0

Where we start

Session 2 fixed the rows where careful people disagree by fixing the guideline. What is left are rows where the label is simply wrong: a 7 someone typed as 1, a 4 filed as 9. On our 800 training digits the declared plant put 133 of them there. The frozen model is still kNN with k = 10, the test set is still locked, and the question tonight is narrow: which rows should a human look at first, and how many of those will turn out to be real errors?

🎫 Clear all three quiz questions to stamp your passport Live = presented in the 45 minutes Self-study = the full depth, after class ★ Build-along = run the code yourself

★ What you walk out with tonight. Your own implementation of confident learning in about forty lines of numpy, reproducing 98 flags on the 800 digits; the scores that go with it (precision 86.7 percent, recall 63.9 percent against the declared plant); the reason in-sample votes quietly halve recall; and a short list of what to ask before trusting anyone's flag count, including your own.

Part 1 · covers out-of-sample probabilities

A row must not vote for itself

5 min live

Confident learning, from Northcutt, Jiang and Chuang's 2021 paper in the Journal of Artificial Intelligence Research, needs exactly two inputs: a predicted probability for every row and every class, and the labels you were given. The paper is explicit that the probabilities must be out of sample, computed beforehand, "e.g. cross-validation", and that any model producing probabilities will do, because the method "decouples the model and data cleaning". Everything interesting about the detector follows from one discipline: the model that judges a row must never have seen that row's label.

OUT OF SAMPLE OR NOTHING: A ROW IS NEVER IN THE MODEL THAT VOTES ON IT FOUR FOLDS, k = 4 AS IN THE PAPER THE LEAK, IN SAMPLE fold 1 trains fold 2 held out fold 3 trains fold 4 trains kNN, k = 10 fitted on folds 1, 3, 4 only row 5, given 1 votes: 8 say 7, 2 say 1 repeat for each fold: every row is voted on exactly once out of sample: 98 flags precision 86.7%, recall 63.9% its nearest neighbour is itself distance 0, so its own label votes in sample: 42 flags precision 95.2%, recall 30.1% Scored against the declared plant of 133. In sample looks precise only because it flags just the obvious.
🔍 Click to zoom - the held-out fold is only ever asked, never trained on; drop that rule and recall falls from 63.9 to 30.1 percent
LiveWhy the votes must be out of sample2 min▶

Take row 5 of the training set. It carries the label 1. Split the 800 rows into four folds, fit kNN on the three folds that do not contain row 5, and ask its ten nearest neighbours what it is: 8 of them say 7 and 2 say 1. That is a strong, honest signal that the label is wrong, and in fact the image is a 7 the plant labelled 1.

Now ask the same question in sample, with row 5 still in the training set. Its nearest neighbour is itself, at distance zero, carrying the label 1. Every row gets one guaranteed vote for its own label, and the look-alike rows the plant favours are exactly the ones whose neighbourhoods are already split. On the 800, the in-sample detector flags only 42 rows, 40 of them real, for precision 95.2 percent and recall 30.1 percent, against 63.9 percent out of sample. The high precision is not a virtue: it only flags the rows so wrong that even a self-vote cannot rescue them. Over twenty seeds the pattern holds, with in-sample recall averaging 32.45 percent against 59.57 percent out of sample.

This is detector leakage, a row's own label leaking into the judgement of that label. It is a cousin of feature leakage, which Feature Engineering covers ↗; the cure here is the fold split, nothing more.

LiveWhat "probability" means for kNN2 min▶

Confident learning asks for an n by m table of predicted probabilities: here 800 rows by 10 digits. A kNN with k = 10 gives the simplest version there is: the share of the ten neighbours voting for each digit. Row 5's row of the table is 0.2 for 1, 0.8 for 7, zero elsewhere. The probabilities move in steps of 0.1, they are not calibrated in any formal sense, and it does not matter much, because the next step compares each row against a bar set by the same model on the same data.

The paper fixes k = 4 folds in its cross-validation experiments, and we do the same. Any model that outputs probabilities can replace kNN: logistic regression, a gradient-boosted tree, a fine-tuned network. What cannot change is that each row's probabilities come from a model fitted without it. The homework swaps the model and compares the flags.

Self-studyBuild it before you import it3 min▶

MIT's Introduction to Data-Centric AI, taught by Anish Athalye, Curtis Northcutt and Jonas Mueller, runs the label-errors lecture on the same pieces: out-of-sample predicted probabilities via cross-validation, "The thresholds for each class are the average predicted probability of examples in that class", and confident counting. Its lab asks for "your own implementation of automatic label error identification using Confident Learning". The open-source cleanlab library packages the method, and it is not taught here: writing it by hand once is how you learn which choices, the fold count, the tie rule, the model, move the flag list, and it gives you something you can check line by line.

Real world

The dataset everyone assumed was clean. MNIST is the first dataset most practitioners ever train on, and it has long been treated as correctly labelled. Northcutt, Jiang and Chuang's abstract reports that confident learning finds label errors in "the presumed error-free MNIST". The later test-set study puts numbers on it: of 100 MNIST test images flagged, human reviewers confirmed 15. Few errors, but not none, and a detector is the only practical way to find them.

Part 2 · covers per-class thresholds and the confident joint

Each digit gets its own bar

5 min live

A single cut-off such as "flag anything below 0.5" treats every digit alike, and the digits are not alike. Some are easy for the model and it is confident about all of them; others are look-alikes and it is never sure. Confident learning sets one threshold per class: t_j is the average self-confidence of the rows given label j, the mean probability the model assigns to the label they carry (equation 2 in the paper). A row counts as "confidently digit j" only if its probability for j clears digit j's own bar.

THE CONFIDENT JOINT ON THE 800: ROW = GIVEN LABEL, COLUMN = SUGGESTED DIGIT tint = confident and agreeing · rust = flagged, 98 rows in all · blank = zero SUGGESTED DIGIT 0 1 2 3 4 5 6 7 8 9 GIVEN 0 1 2 3 4 5 6 7 8 9 80 46 65 43 44 59 83 49 48 42 14 1 1 14 7 1 20 3 17 19 1 EACH DIGIT'S BAR, t_j 1.00 0.49 0.94 0.54 0.59 0.95 0.98 0.66 0.58 0.60 0s: all 10 votes 1s: about half 0 0.5 1.0 Read a rust cell as: 20 rows labelled 7 whose votes for 1 clear the 1s' bar. Spread-out rows land nowhere.
🔍 Click to zoom - the flags are the off-diagonal cells, and each column's bar is set by how confident the model is about that digit
LiveThe thresholds, and why a 1 needs fewer votes than a 02 min▶

On the 800 training digits the bars come out as printed by the build-along: 0: 1.00, 1: 0.49, 2: 0.94, 3: 0.54, 4: 0.59, 5: 0.95, 6: 0.98, 7: 0.66, 8: 0.58, 9: 0.60. A row given 0 has to win all ten votes for 0 to look confidently like a 0, because 0s are easy and every other 0 does. A row needs only 0.49 of its votes for 1 to look confidently like a 1, because the plant has scrambled 1s and 7s and the model is rarely sure about either.

That is the point of the per-class bar. The paper argues it makes the method robust to "heterogeneous class probability distributions" and to "class-imbalance": a class the model is over-confident about gets a proportionally higher bar, so its rows are not all waved through, and a class it is unsure about gets a lower one, so its rows are not all flagged.

LiveCounting into the joint, and the collision rule2 min▶

The confident joint C is a ten by ten table. For every row given label i, find each digit j whose probability clears t_j. If exactly one does, add one to C[i][j]. If several do, the collision rule of equation 1 applies: take the one with the highest probability among those over their bars. Diagonal cells are rows whose label the votes confirm; off-diagonal cells are rows where the votes confidently point elsewhere, and those are the flags.

On the 800 the diagonal holds 80, 46, 65, 43, 44, 59, 83, 49, 48 and 42, and the off-diagonals hold 98 rows. The big cells are the plant showing through: given 7 but suggested 1, 20 rows; given 9 suggested 4, 19; given 8 suggested 3, 17; given 1 suggested 7 and given 3 suggested 8, 14 each; given 4 suggested 9, 7. The small ones, 3 rows given 8 suggested 1 and single rows at 2 to 8, 3 to 7, 5 to 9 and 9 to 8, are where the detector disagrees with a label the plant never touched, and Part 3 checks them.

A row whose probabilities clear no bar at all, such as a near-uniform spread of votes, lands in no cell. The paper counts that as a feature: the joint is "robust to pure noise or examples from an alien class". It is also, as Part 3 shows, where the missed errors hide.

Self-studyCalibrating the joint, and the pruning variants3 min▶

The raw counts are not yet an estimate of the noise. Equation 3 of the paper calibrates C into Q, an estimate of the joint distribution of given and true labels: each row is rescaled so its sum matches the observed count of that given label, and the whole table is normalised to sum to 1. Q is what you would report as "an estimated share of 7s labelled 1".

The paper then compares several ways to choose which rows to prune. The baseline C_confusion flags every row whose argmax disagrees with its label, which the authors describe as "similar to a single iteration of INCV". C_{y,y*} flags the off-diagonals of the confident joint, which is what we compute tonight. Prune by Class removes, per given class, the rows with the lowest self-confidence; Prune by Noise Rate ranks them by margin, the gap between the best other class and the given one; C+NR takes the intersection of the two. None of these variants is computed here; the build-along implements C_{y,y*} and C_confusion only, so every number on this page traces to one of those two.

Real world

Missiles filed as projectiles. The paper's own headline example is one off-diagonal cell of a much larger joint. On ImageNet, confident learning estimates "645 missile images are mislabeled as their parent class projectile". That is exactly the shape of our given 7, suggested 1 cell: a systematic confusion between two neighbouring classes, found by counting, not by reading every image.

Part 3 · covers what the detector finds and misses

Precision, recall, and the rows it cannot see

5 min live

A flag count on its own tells you nothing. Tonight we can do better than usual, because the plant is declared: we know which 133 of the 800 labels are wrong, so every flag can be scored. On real data you never know that, and the second half of this Part is about what you do instead.

98 FLAGS AND 133 PLANTED ERRORS, AS REAL TRAINING ROWS CAUGHT: 85 OF 98 FLAGS FALSE ALARMS: 13 MISSED: 48 OF 133 given 1 is a 7 given 7 is a 1 given 8 is a 3 given 9 is a 4 given 3 CL says 8 given 2 CL says 8 given 4 is a 9 given 7 is a 1 given 1 is a 7 relabelled to the true digit right labels, edited wrongly no digit clears its bar Training rows 5, 25, 0, 24 · 63, 197 · 12, 20, 28, each drawn from its own 64 pixels. The plant is declared.
🔍 Click to zoom - 45 of the 48 missed rows are look-alikes whose votes split, so no digit clears its bar and they land in no cell
LiveConfident learning against the argmax baseline2 min▶

Score both detectors on the same out-of-sample votes against the declared plant. Confident learning flags 98 rows and 85 of them are planted errors: precision 86.7 percent, recall 63.9 percent. The argmax baseline, the paper's C_confusion, flags a row whenever its given label is not among the top-voted digits. It flags 141 rows and catches 109: precision 77.3 percent, recall 82.0 percent.

Detector, same votes, same 800 rowsFlaggedReal errorsPrecisionRecall of 133
Confident learning, out of sample988586.7%63.9%
Argmax disagrees (C_confusion)14110977.3%82.0%
Confident learning, in sample (the leak)424095.2%30.1%

The baseline finds more errors and raises more false alarms: 32 of its 141 flags are correct labels, against 13 of 98. Per-class thresholds trade some recall for precision, and which side of the trade you want depends on what happens to a flag. If every flag goes to a human reviewer, precision is reviewer time. If flags are relabelled automatically, every false alarm writes a new error into the data: all 13 of confident learning's false alarms were relabelled wrongly, while all 85 true catches went to their true digit. On twenty seeds of the same setup confident learning averages precision 88.76 percent and recall 59.57 percent, so tonight's seed is typical.

LiveWhat it misses, and the assumption it bends2 min▶

The 48 planted errors confident learning misses are not random. They are the look-alikes: the 7 drawn so like a 1 that its neighbours split, the 1 that half its neighbours call 7, the 9 that could pass for a 3. Their votes are spread, so the true digit's probability falls short of its bar, the given label falls short of its own, and the row lands in no cell of the joint. The same rule that protects the joint from pure noise hides these rows.

Part of the reason is the plant itself. Confident learning assumes class-conditional noise: the chance a label is flipped depends on the true class, not on the particular image. Our declared plant bends that assumption on purpose, because it picks the flips from the images that look most like the other digit, which is what tired labellers actually do. The test-set study reached a similar conclusion about real benchmarks, where errors are "primarily systematic mislabeling, not just random noise". Treat a detector's recall on image-dependent noise as lower than its paper numbers, and measure it.

Self-studyPrecision on real data varies by dataset3 min▶

Without a declared plant, the only way to know precision is to check a sample of flags by hand. Northcutt, Athalye and Mueller did it on the test sets of ten benchmarks: each flagged item went to five crowd workers, and it counted as an error only if fewer than 3 of 5 agreed with the given label. On average 51 percent of the algorithmically flagged candidates were confirmed as real errors. On MNIST, Table 2 shows 15 errors among 100 flagged, a precision of 15 percent. On clean data most flags are false alarms; on noisy data most are real. The same algorithm, run the same way, gives very different precision depending on how dirty the data was to begin with.

The paper's gallery of confirmed errors also shows the failure modes a reviewer will meet: an image that shows part of an object labelled as the whole, an object seen from an unusual viewpoint, and, on digits, a figure drawn so badly that any label is a guess. The authors add that their corrected labels "are not 100% perfect". A flag is a request for a human look, never a verdict.

Real world

Half the flags, on average. Across the ten benchmark test sets, a little over half of the candidates the algorithm flagged survived human review. On ImageNet's validation set 2,916 of 5,440 flags were confirmed; on CIFAR-10, 54 of 275. A team that auto-relabels every flag on a dataset like CIFAR-10 would likely introduce more errors than it removes. That is why session 6 puts a hand-checked sample into the data-edit spec.

What to ask for, if you lead the team. When someone reports "the detector found N bad labels", ask three things. How many rows were flagged, out of how many? What precision did a hand-checked random sample of the flags show, and how big was the sample? And how were the probabilities produced: which model, out of sample by how many folds? A flag count without a checked sample and a fold count is not yet a finding.

Build-along

Confident learning by hand, on the real digits

★ 22 min · everyone builds

Six steps in plain numpy, continuing from session 1's steps 1 to 4: the digits, the seeded split, the frozen kNN and the declared plant. Run those once, in order, then continue here in the same session, because the fold shuffle below draws from the same seeded generator. Every printed line is the script's real output, and it matches the session 4 bench row for row.

SIX STEPS: VOTES, BARS, JOINT, FLAGS, ONE EDIT, ONE SCORE Out-of-sample votes 4 folds, kNN k = 10 Per-class bars t_j = mean self-vote Confident joint 10 x 10 counts 98 flagged the off-diagonal cells Edit: remove or relabel remove 479, relabel 478 Frozen model kNN, k = 10 Locked test set 500 digits, scored only relabelled right: 85 of 98 Baseline on the given labels: 457 of 500. The detector never sees the test set; it only picks rows to edit.
🔍 Click to zoom - the test set sits at the end of the line and feeds nothing back into the detector

Out-of-sample votes. Assign each of the 800 training rows to one of four folds, shuffled with the seeded generator. For each fold, fit kNN on the other three and let it vote on the held-out rows. The result is an 800 by 10 table of vote counts out of 10; divide by 10 and it is the probability table confident learning needs. Row 5 is the 7 labelled 1 from session 1.

★ Step 1 · out-of-sample votes, four folds# continues from session 1, steps 1 to 4 (X, y, train, test, rng, K, score, g_train) folds = np.arange(800) % 4 rng.shuffle(folds) Xtr = X[train] def knn_votes(Xa, ya, Xq): D = ((Xq[:, None, :] - Xa[None, :, :]) ** 2).sum(-1) nb = np.argsort(D, axis=1, kind="stable")[:, :K] return np.array([np.bincount(ya[row], minlength=10) for row in nb]) V = np.zeros((800, 10), dtype=int) for f in range(4): hold = folds == f V[hold] = knn_votes(Xtr[~hold], g_train[~hold], Xtr[hold]) print(V[5], "given", g_train[5]) # [0 2 0 0 0 0 0 8 0 0] given 1

One bar per digit. For each digit j, average the self-confidence of the rows given j: the sum of their votes for j, divided by ten times their count. Look at the first two numbers before moving on.

★ Step 2 · per-class thresholdsn = np.bincount(g_train, minlength=10) S = np.array([V[g_train == j, j].sum() for j in range(10)]) t = S / (10 * n) print(np.round(t, 2)) # [1. 0.49 0.94 0.54 0.59 0.95 0.98 0.66 0.58 0.6 ]

Count the confident joint. For each row, list the digits whose vote share clears their own bar (compared in integers, so no rounding creeps in). No digit over the bar: skip the row. Otherwise take the most-voted digit among them, keeping the given label on a tie. The row lands in C[given][suggested], and any row whose suggestion differs from its label is a flag.

★ Step 3 · the confident jointdef confident_joint(V, g): n = np.bincount(g, minlength=10) S = np.array([V[g == j, j].sum() for j in range(10)]) C = np.zeros((10, 10), dtype=int) sug = g.copy() for r in range(len(g)): over = [l for l in range(10) if V[r, l] * n[l] >= S[l]] if not over: continue top = max(V[r, l] for l in over) best = [l for l in over if V[r, l] == top] j = g[r] if g[r] in best else best[0] C[g[r], j] += 1 sug[r] = j return C, sug C, sug = confident_joint(V, g_train) flag = sug != g_train print(C.diagonal(), "flagged:", flag.sum()) print([(i, j, C[i, j]) for i in range(10) for j in range(10) if i != j and C[i, j]]) # [80 46 65 43 44 59 83 49 48 42] flagged: 98 # [(1, 7, 14), (2, 8, 1), (3, 7, 1), (3, 8, 14), (4, 9, 7), (5, 9, 1), (7, 1, 20), (8, 1, 3), (8, 3, 17), (9, 4, 19), (9, 8, 1)]

Score the detector against the declared plant. This step exists only because the plant is declared: we know which labels are wrong. The same report function scores the argmax baseline on the same votes, and in step 5 it scores the leak.

★ Step 4 · score the detectorplanted = g_train != y[train] def report(name, fl): tp = (fl & planted).sum() print(f"{name}: flagged {fl.sum()}, caught {tp}, precision {100*tp/fl.sum():.1f}%, recall {100*tp/planted.sum():.1f}%") report("confident learning", flag) # the baseline: flag when the given label is not among the top-voted digits report("argmax disagrees", V[np.arange(800), g_train] != V.max(1)) # confident learning: flagged 98, caught 85, precision 86.7%, recall 63.9% # argmax disagrees: flagged 141, caught 109, precision 77.3%, recall 82.0%

Break it on purpose: the leak. Vote in sample, with every row still in the model that votes on it, and run exactly the same joint. Watch recall.

★ Step 5 · the leak: in-sample votesVin = knn_votes(Xtr, g_train, Xtr) _, sug_in = confident_joint(Vin, g_train) report("in-sample", sug_in != g_train) # in-sample: flagged 42, caught 40, precision 95.2%, recall 30.1%

Make the two edits and score them on the untouched test set. Remove the 98 flagged rows, or relabel them to the joint's suggestion, then hand each training set to the frozen model. Against the baseline of 457 on the given labels, both land near 479. One test digit is 0.2 points, so a one-digit difference here is not a result; session 4 has the twenty-seed sweep that says the two are about equal.

★ Step 6 · the two edits, scored on the untouched test setprint("remove:", score(Xtr[~flag], g_train[~flag]), "of 500") print("relabel:", score(Xtr, sug), "of 500") print("relabelled right:", int((sug[flag] == y[train][flag]).sum()), "of", int(flag.sum())) # remove: 479 of 500 # relabel: 478 of 500 # relabelled right: 85 of 98
Same frozen model, same 500 test digitsTraining rowsCorrectAccuracy
The labels you were given (session 1)80045791.4%
Remove what confident learning flags70247995.8%
Relabel what confident learning flags80047895.6%
The true labels (knowable only because the plant is declared)80048997.8%

Where the leak shows up downstream. On the session 4 bench, relabelling from in-sample votes instead of out-of-sample ones leaves 95 wrong labels in the 800 instead of 61, and the frozen model scores 94.8 percent instead of 95.6. Same algorithm, same model, one missing fold split.

Homework

Before session 4

30 min
  1. Change the fold count. Re-run steps 1 to 4 with 2, 4 and 10 folds and write down the flag count, precision and recall for each. Fewer folds means each vote comes from a smaller model; more folds means more compute. Which number moves most?
  2. Swap the model. Replace the kNN votes in step 1 with out-of-sample probabilities from scikit-learn's logistic regression (cross_val_predict with method="predict_proba"), recompute the thresholds with probabilities instead of vote counts, and compare the flag list with tonight's. How many flags do the two models share?
  3. Hand-check 20 flags from your own data. Run the detector on a labelled table you own, draw 20 flags at random, and look at each one. Count how many labels are really wrong.
  4. Bring the precision estimate to session 4. Your 20-flag count, divided by 20, is your first honest precision number. Bring it, with the sample size next to it; the bench will show what the choice between removing and relabelling does with a precision like yours.
Check yourself

Three questions

Clear all three and session 3 gets stamped in your passport.

1 · On tonight's 800 digits, a row given 1 counts as confidently a 1 with 0.49 of its votes, while a row given 0 needs 1.00. Why?

t_j is computed from the data: the mean probability the model gives rows labelled j for label j. A per-class bar is what makes the method robust to classes the model is naturally more or less confident about, and to class imbalance.

2 · The argmax baseline catches 109 planted errors against confident learning's 85. Why might a team still prefer confident learning?

Neither detector wins outright. The baseline flags 32 correct labels, confident learning 13. Which trade you want depends on what happens to a flag: human review makes precision a cost in reviewer time; automatic relabelling makes it a cost in new errors.

3 · Run the same confident joint on in-sample votes and recall drops from 63.9 to 30.1 percent, while precision rises to 95.2. What happened?

This is detector leakage: the row's own label leaks into the judgement of that label. The high precision is a symptom, not a strength. Over twenty seeds in-sample recall averages 32.45 percent against 59.57 out of sample.

Sources covered

What this session teaches, and where it came from

✓Northcutt, Jiang, Chuang (2021), Confident Learning, JAIR 70The two inputs, out-of-sample probabilities, k = 4, per-class thresholds, the confident joint and its collision rule, calibration to Q, the pruning variants and the class-conditional assumption, read from the full text
✓MIT, Introduction to Data-Centric AI, label errors lecture and labCross-validated probabilities, class thresholds as average predicted probability, and the implement-it-yourself lab, read from the lecture notes
✓Northcutt, Athalye, Mueller (2021), label errors in test setsThe five-worker check, the 51 percent average, MNIST's 15 of 100 and the failure modes, read from the NeurIPS paper
◐The cleanlab libraryNamed, not taught; the algorithm is implemented by hand so every flag can be checked
✓Every number in the build-alongComputed on the UCI Optical Recognition of Handwritten Digits (Alpaydin and Kaynak, 1998); the noise plant is constructed and declared

Every fact on this page, and its verification tier, is recorded in the course's source map.

Session 3 cheat sheet · pin this

Two inputsOut-of-sample predicted probabilities and the given labels. Any probabilistic model will do.
Out of sample or nothingk-fold, k = 4. In sample, recall fell from 63.9 to 30.1 percent.
t_j = mean self-confidenceOne bar per class: here 1.00 for 0s, 0.49 for 1s.
The confident jointRow = given, column = suggested. Off-diagonals are the flags: 98 on the 800.
Argmax vs CL141 flags at 77.3 percent precision against 98 at 86.7: more recall, more false alarms.
Check a sample by handPrecision swings by dataset: 51 percent on average, 15 percent on MNIST.