Winning Kaggle the Reproducible Way

Interactive chapter

This page shows the chapter notebook with its saved results. Constructed examples do not measure competition gains.

Chapter 48: Pseudo-Labeling for CV

Try the decision first

Does a stricter confidence threshold make pseudo-labeling pay, and does it remove the leak from a teacher that saw the validation labels?

Write your expectation before opening the worked results. Use the chapter activity to compare supported inputs. To change the calculation itself, run the notebook on GitHub.

Your answers stay on this page until you download them. They are not sent to a server.

Open the chapter activity

Worked notebook and saved results

Does a stricter confidence threshold make pseudo-labeling pay, and does it remove the leak from a teacher that saw the validation labels?

The chapter says a confidence threshold controls selection but does not prove correctness, and cannot repair teacher leakage. Sixteen constructed worlds put numbers on both statements: which threshold helps the student, and how large the cross-validation error from a leaky teacher is at each.

Constructed data and a teacher that memorises; the sizes of these effects are properties of this generator, not a competition result.

The experiment

Constructed binary task in six dimensions with three noisy clusters per class: 100 labeled rows, 1,000 unlabeled pool rows with known labels used only for scoring, and 2,000 fresh rows. The teacher is an extremely randomized trees classifier (it memorises its training rows); the student is a second trees model trained on the labeled rows plus the pool rows whose top teacher probability reaches the control's threshold, using the teacher's labels. The student is compared with the same model trained on labeled rows only. Three-fold cross-validation of the whole pipeline is run twice: with one teacher trained on all 100 labeled rows (so it has seen every validation label) and with a teacher rebuilt from each fold's training rows. Each estimate is compared with the score on fresh rows of the same student models, averaged over 16 worlds.

Calculation code
import numpy as np
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.model_selection import StratifiedKFold

from kaggle_companion.activities._common import clean

SEED = 48
DRAWS = 16                  # independent worlds; every estimate is a mean over draws
N_LABELED, N_POOL, N_FRESH = 100, 1000, 2000
DIM, NOISE = 6, 1.6        # features per row and within-cluster noise


def make_world(rng):
    """Three clusters per class in six dimensions: the same world supplies labeled rows, the unlabeled pool and fresh rows."""
    return rng.normal(size=(3, DIM)) * 2.0, rng.normal(size=(3, DIM)) * 2.0


def sample(world, n, rng):
    y = (rng.random(n) < 0.5).astype(int)
    cluster = rng.integers(0, 3, n)
    X = np.where(y[:, None] == 1, world[1][cluster], world[0][cluster]) + NOISE * rng.normal(size=(n, DIM))
    return X, y


def teacher(seed):
    """High-capacity teacher: it memorises the labeled rows it is trained on (which is what makes leakage matter)."""
    return ExtraTreesClassifier(n_estimators=30, random_state=seed, n_jobs=1)


def student(seed):
    return ExtraTreesClassifier(n_estimators=30, min_samples_leaf=3, random_state=seed, n_jobs=1)


def pseudo_set(teacher_X, teacher_y, pool, threshold, seed):
    """Fit a teacher, keep the pool rows whose top class probability reaches the threshold, and return rows and pseudo-labels."""
    prob = teacher(seed).fit(teacher_X, teacher_y).predict_proba(pool)
    keep = prob.max(1) >= threshold
    return pool[keep], prob.argmax(1)[keep], keep, prob.argmax(1)


def run(threshold):
    rng = np.random.default_rng(SEED)
    keys = ("coverage", "pseudo_accuracy", "supervised", "teacher_alone", "student", "leaky_estimate", "leaky_true", "nested_estimate", "nested_true",
            "supervised_estimate")
    rows = {k: [] for k in keys}
    for draw in range(DRAWS):
        world = make_world(rng)
        X, y = sample(world, N_LABELED, rng)
        pool, pool_y = sample(world, N_POOL, rng)
        X_fresh, y_fresh = sample(world, N_FRESH, rng)
        # Final pipeline: teacher on all labeled rows, student on labeled rows plus confident pseudo-labels.
        px, py, keep, guess = pseudo_set(X, y, pool, threshold, draw)
        rows["coverage"].append(keep.mean())
        rows["pseudo_accuracy"].append((py == pool_y[keep]).mean() if keep.any() else np.nan)
        rows["supervised"].append(student(draw).fit(X, y).score(X_fresh, y_fresh))
        rows["teacher_alone"].append(teacher(draw).fit(X, y).score(X_fresh, y_fresh))   # the other supervised candidate
        rows["student"].append(student(draw).fit(np.vstack([X, px]), np.r_[y, py]).score(X_fresh, y_fresh))
        # Three-fold cross-validation of the whole pipeline, with the teacher either global (leaky) or rebuilt per fold.
        cells = {k: [] for k in ("leaky_estimate", "leaky_true", "nested_estimate", "nested_true", "supervised_estimate")}
        for fit, hold in StratifiedKFold(3, shuffle=True, random_state=draw).split(X, y):
            for tag, (tx, ty) in (("leaky", (X, y)), ("nested", (X[fit], y[fit]))):    # global teacher sees the hold-out labels
                px_f, py_f, _, _ = pseudo_set(tx, ty, pool, threshold, draw)
                model = student(draw).fit(np.vstack([X[fit], px_f]), np.r_[y[fit], py_f])
                cells[tag + "_estimate"].append(model.score(X[hold], y[hold]))
                cells[tag + "_true"].append(model.score(X_fresh, y_fresh))
            cells["supervised_estimate"].append(student(draw).fit(X[fit], y[fit]).score(X[hold], y[hold]))
        for k, v in cells.items():
            rows[k].append(np.mean(v))
    mean = {k: float(np.nanmean(v)) for k, v in rows.items()}
    gain = np.array(rows["student"]) - np.array(rows["supervised"])
    gain_vs_teacher = np.array(rows["student"]) - np.array(rows["teacher_alone"])
    leak = np.array(rows["leaky_estimate"]) - np.array(rows["leaky_true"])
    honest = np.array(rows["nested_estimate"]) - np.array(rows["nested_true"])
    return clean({
        "threshold": threshold, **mean,
        "gain": gain.mean(), "gain_se": gain.std(ddof=1) / np.sqrt(DRAWS), "gain_win_rate": float((gain > 0).mean()),
        "gain_vs_teacher": gain_vs_teacher.mean(), "gain_vs_teacher_se": gain_vs_teacher.std(ddof=1) / np.sqrt(DRAWS),
        "leaky_error": leak.mean(), "leaky_error_se": leak.std(ddof=1) / np.sqrt(DRAWS),
        "nested_error": honest.mean(), "nested_error_se": honest.std(ddof=1) / np.sqrt(DRAWS),
        "per_draw": {"supervised": rows["supervised"], "student": rows["student"]},
        "labeled": N_LABELED, "pool": N_POOL, "fresh": N_FRESH, "draws": DRAWS,
    })

Measure it across the control

The control is confidence threshold for keeping a pseudo-label. Each value below runs the whole experiment from the generator up.

Calculation code
import pandas as pd
from kaggle_companion.activities.ch48 import explain

VALUES = [0.6, 0.8, 0.9, 0.99]
results = {value: run(value) for value in VALUES}
table = pd.DataFrame({str(v): explain(results[v], v)['metrics'] for v in VALUES})
print(table.to_string())
                                          0.6             0.8             0.9            0.99
Pseudo-labels kept                      88.8%           57.3%           35.3%            8.3%
Pseudo-label accuracy                   91.1%           96.2%           98.1%           99.7%
Student gain (standard error)  +0.020 (0.003)  +0.012 (0.003)  +0.006 (0.003)  -0.003 (0.003)
Leaky CV error                         +0.040          +0.043          +0.033          +0.029
Nested CV error                        +0.007          +0.015          +0.003          +0.015
Supervised accuracy, fresh              0.866           0.866           0.866           0.866

Look at it

The figure shows the default setting, confidence threshold for keeping a pseudo-label = 0.9.

Calculation code
import matplotlib.pyplot as plt
from kaggle_companion.activities.ch48 import draw, NCOLS, HEIGHT

DEFAULT = 0.9
fig, axes = plt.subplots(1, NCOLS, figsize=(6.6 if NCOLS == 1 else 10.4, HEIGHT), layout='constrained')
draw(axes, results[DEFAULT], DEFAULT)
plt.show()
Saved figure from this chapter calculation

What the result says

At threshold 0.9 the teacher's labels are kept for 35.3% of the pool and are 98.1% correct. The student scores 0.872 on fresh rows against 0.866 for the supervised model: 0.872 - 0.866 = 0.006 (standard error 0.003). The gain is small, between two and three standard errors, and may not survive another seed. Cross-validation with a teacher that saw all labeled rows reports 0.897 against a fresh score of 0.864, an error of +0.033; rebuilding the teacher inside each fold gives an error of +0.003. The teacher used alone scores 0.875 on fresh rows, so the student is -0.003 against the better of the two supervised models.

Apply this to your competition

Book location: Chapter 48, What Can Go Wrong.

Technical corrections

Apply a lesson from this chapter

Select a lesson whose conditions match your project. State how you would test it and what would make you reject it.

Compare labeled-domain pretraining with pseudo-label pretraining

Conditions

  • event localization has related external labeled video and unlabeled clips

Action

  • Compare related external labeled pretraining with pseudo-label pretraining under the same target labels and training budget.

Check

  • Proposed verification: Evaluate unseen videos under fixed training budgets and label-window alignment.

Limits

  • The pseudo-label failure is setup-specific and the metric name for the 0.02 gain is not repeated here.

Find source links and value grades in the lesson atlas

Reject unreadable and tiny images with a logged quality policy

Conditions

  • image downloads include unusable files

Action

  • Proposed adaptation of source observation: Log decode failures and compare an explicit minimum-resolution rule against retaining small images.

Check

  • Proposed verification: Inspect class-specific rejection rates and evaluate performance on low-resolution target examples.

Limits

  • Removing small images can skew classes; threshold is source-specific and no isolated gain is supplied.

Find source links and value grades in the lesson atlas