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 32: Denoising Autoencoders for Tabular Pretraining

Try the decision first

Does a lower reconstruction error mean the pretrained features help a small labeled model more than an untrained encoder would?

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 lower reconstruction error mean the pretrained features help a small labeled model more than an untrained encoder would?

Reconstruction loss is the number pretraining produces for free, so it is the number people optimize. It says how well the pretext task was fitted, not whether the target's information survived, and the untrained control is rarely run.

Constructed data; pretraining adds little over an untrained encoder in this generator, which is a property of the setup, not a general claim about denoising autoencoders.

The experiment

Fourteen constructed datasets: 20 features driven by four hidden factors plus noise, 3,000 unlabeled rows, 300 labeled rows and 3,000 new rows. The label depends on a product of two factors and one more factor. A 64-unit hidden-layer network (wider than the input) is trained on two swap-noise copies of every unlabeled row to rebuild the clean row; the control is the swap probability (0 gives a plain autoencoder). Its hidden activations are appended to the 20 features of a logistic regression fitted on the 300 labeled rows and scored by AUC on the new rows. Two controls: the raw features alone, and the hidden activations of an untrained network with the same shape.

Calculation code
import warnings

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler

from kaggle_companion.activities._common import clean

SEED = 32
REPLICATES = 14         # independent constructed datasets; every estimate is their mean
FEATURES, FACTORS, HIDDEN = 20, 4, 64
N_UNLABELED, N_LABELED, N_NEW = 3000, 300, 3000


def generate(rng):
    """Twenty noisy features driven by four hidden factors. The label depends on a product of two factors, so a
    plain linear model on the features cannot read it. Unlabeled rows are plentiful; labeled rows are few."""
    loading = rng.normal(size=(FACTORS, FEATURES))

    def rows(n):
        f = rng.normal(size=(n, FACTORS))
        return f, f @ loading + 0.5 * rng.normal(size=(n, FEATURES))

    def label(f):
        logit = 1.6 * f[:, 0] * f[:, 1] + 1.0 * f[:, 2]
        return (rng.random(len(f)) < 1 / (1 + np.exp(-logit))).astype(int)

    _, X_unlabeled = rows(N_UNLABELED)
    f_lab, X_lab = rows(N_LABELED)
    f_new, X_new = rows(N_NEW)
    return X_unlabeled, (X_lab, label(f_lab)), (X_new, label(f_new))


def swap_noise(X, p_swap, rng):
    """Replace each cell with probability p_swap by the same column's value from a random donor row."""
    corrupted = X.copy()
    mask = rng.random(X.shape) < p_swap
    donors = rng.integers(0, len(X), X.shape)
    corrupted[mask] = X[donors[mask], np.nonzero(mask)[1]]
    return corrupted


def autoencoder(seed, trained, X_in=None, X_target=None):
    """One hidden layer of 64 ReLU units (wider than the 20 inputs), fitted to map corrupted rows to clean rows.
    trained=False takes one step at a negligible learning rate, which leaves the random initial weights in place."""
    net = MLPRegressor(hidden_layer_sizes=(HIDDEN,), batch_size=256, learning_rate_init=0.003 if trained else 1e-9,
                       max_iter=30 if trained else 1, random_state=seed)
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")        # the iteration cap is deliberate
        net.fit(X_in, X_target)
    return net


def encode(net, X):
    """The hidden-layer activations: the learned (or, untrained, random) representation of each row."""
    return np.maximum(0, X @ net.coefs_[0] + net.intercepts_[0])


def downstream_auc(train_features, y, new_features, y_new):
    scaler = StandardScaler().fit(train_features)
    model = LogisticRegression(C=0.3, max_iter=500).fit(scaler.transform(train_features), y)
    return roc_auc_score(y_new, model.predict_proba(scaler.transform(new_features))[:, 1])


def one_replicate(p_swap, seed):
    rng = np.random.default_rng(seed)
    X_unl, (X_lab, y_lab), (X_new, y_new) = generate(rng)
    scaler = StandardScaler().fit(X_unl)
    X_unl, X_lab, X_new = scaler.transform(X_unl), scaler.transform(X_lab), scaler.transform(X_new)
    clean_rows = np.vstack([X_unl, X_unl])                                         # two corrupted copies of every unlabeled row
    corrupted = np.vstack([swap_noise(X_unl, p_swap, rng), swap_noise(X_unl, p_swap, rng)])
    dae = autoencoder(seed, True, corrupted, clean_rows)
    untrained = autoencoder(seed + 7, False, corrupted[:300], clean_rows[:300])

    def auc_with_encoder(net):                                                      # raw features plus the encoder's activations
        return downstream_auc(np.hstack([X_lab, encode(net, X_lab)]), y_lab, np.hstack([X_new, encode(net, X_new)]), y_new)

    return {
        "reconstruction_dae": float(np.mean((dae.predict(X_new) - X_new) ** 2)),       # clean new rows in, clean rows out
        "reconstruction_untrained": float(np.mean((untrained.predict(X_new) - X_new) ** 2)),
        "auc_raw": downstream_auc(X_lab, y_lab, X_new, y_new),
        "auc_untrained": auc_with_encoder(untrained),
        "auc_dae": auc_with_encoder(dae),
    }


def run(p_swap):
    reps = [one_replicate(p_swap, SEED * 100 + i) for i in range(REPLICATES)]
    mean = lambda key: float(np.mean([r[key] for r in reps]))
    keys = ("reconstruction_dae", "reconstruction_untrained", "auc_raw", "auc_untrained", "auc_dae")
    paired = [r["auc_dae"] - r["auc_untrained"] for r in reps]
    return clean({
        "p_swap": p_swap, **{k: mean(k) for k in keys},
        "dae_minus_untrained": float(np.mean(paired)), "dae_minus_untrained_sd": float(np.std(paired)),
        "per_replicate_auc": {k: [r["auc_" + k] for r in reps] for k in ("raw", "untrained", "dae")},
        "replicates": REPLICATES, "unlabeled_rows": N_UNLABELED, "labeled_rows": N_LABELED, "new_rows": N_NEW,
    })

Measure it across the control

The control is swap probability per cell. Each value below runs the whole experiment from the generator up.

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

VALUES = [0, 0.1, 0.3, 0.6]
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    0.1    0.3    0.6
Reconstruction, trained    0.003  0.037  0.083  0.184
Reconstruction, untrained  1.405  1.405  1.405  1.405
AUC raw features           0.676  0.676  0.676  0.676
AUC + untrained encoder    0.765  0.765  0.765  0.765
AUC + trained encoder      0.751  0.768  0.770  0.776

Look at it

The figure shows the default setting, swap probability per cell = 0.1.

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

DEFAULT = 0.1
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 swap probability 0.1 the trained autoencoder reconstructs new rows with error 0.037 against 1.405 for the untrained network. Its features give a downstream AUC of 0.768; the raw features alone give 0.676 and an untrained encoder of the same shape 0.765. So 0.768 - 0.765 = 0.003 is what pretraining adds beyond a random encoder (one standard deviation over datasets: 0.011), while 0.768 - 0.676 = 0.092 is the gain over raw features.

Apply this to your competition

Book location: Chapter 32, Setting Realistic Expectations.

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.

Test overcomplete denoising features as a neural complement

Conditions

  • boosted trees outperform raw tabular neural networks

Action

  • Proposed conditional application: Compare rank-Gauss normalized swap-noise DAE features with plain neural inputs, then assess the model's ensemble contribution separately from standalone AUC.

Check

  • Proposed check: Compare fold-fitted DAE+NN/plain NN and incremental blend AUC at equal data splits; measure GPU time/memory.

Limits

  • Approximate recalled gain; huge overcomplete layers and transductive fitting risks require care.

Find source links and value grades in the lesson atlas

Treat two-dimensional autoencoder embeddings as a diagnostic

Conditions

  • high-dimensional patient features require exploratory visualization

Action

  • Proposed adaptation of source observation: Evaluate a low-dimensional unsupervised embedding for visualization while retaining the original features for predictive comparisons.

Check

  • Proposed verification: Distinguish diagnostic plots from held-out predictive evidence and document whether fitting on unlabeled test data is permitted.

Limits

  • The source demonstrates a visualization, not a predictive gain; combined train/test fitting is transductive and may be unsuitable elsewhere.

Find source links and value grades in the lesson atlas

Check a denoised feature view for ensemble diversity

Conditions

  • tree models may benefit from a complementary learned feature view

Action

  • Proposed conditional application: Train a three-hidden-layer swish denoising autoencoder and feed its denoised data into LightGBM.

Check

  • Proposed check: Compare raw and DAE-denoised LightGBM and their blend on fixed group-aware holdouts; record residual correlation.

Limits

  • Autoencoder details are partial; weak/significant CV wording conflicts and no causal benefit established.

Find source links and value grades in the lesson atlas