M56: Other Forms of Learning

CSCI 3151 — Foundations of Machine Learning

Frank Rudzicz

Why this module matters

Imagine you are building a support-ticket triage model for a company with:

  • 250,000 historical text tickets,
  • only 1,200 human-labeled examples,
  • a labeling cost of even 2 minutes per item.

That means labeling just 10,000 more items would take roughly:

\[ 10{,}000 \times 2 \text{ minutes} = 20{,}000 \text{ minutes} \approx 333 \text{ hours} \]

So the practical question is not:

Can we get more data?

It is:

Can we get more useful supervision without hand-labeling everything?

Learning outcomes

By the end of this module, you should be able to:

  • explain why self-supervised and semi-supervised learning become attractive when labels are scarce but raw data is abundant;
  • distinguish supervised, semi-supervised, and self-supervised objectives in terms of what supervision signal is available;
  • analyze the cluster / low-density assumptions that often justify semi-supervised methods;
  • implement a simple pseudo-labeling or consistency-regularization baseline and evaluate it critically;
  • justify when a self-supervised pretext task is likely to help a downstream task, and when it may fail.

The central picture

Code
flowchart LR
    A[Raw data<br/>lots of examples] --> B{Do we have labels?}
    B -->|many labels| C[Supervised learning]
    B -->|few labels +<br> many unlabeled examples| D[Semi-supervised learning]
    B -->|no task labels but <br>structure in data| E[Self-supervised learning]
    E --> F[Learn representations]
    F --> G[Fine-tune / linear probe / <br>transfer]
    D --> H[Joint supervised + unlabeled-data objective]

flowchart LR
    A[Raw data<br/>lots of examples] --> B{Do we have labels?}
    B -->|many labels| C[Supervised learning]
    B -->|few labels +<br> many unlabeled examples| D[Semi-supervised learning]
    B -->|no task labels but <br>structure in data| E[Self-supervised learning]
    E --> F[Learn representations]
    F --> G[Fine-tune / linear probe / <br>transfer]
    D --> H[Joint supervised + unlabeled-data objective]

The difference is not that one is “more advanced.”
The difference is where the training signal comes from.

Conceptual scaffold

A quick decision heuristic

Code
flowchart TD
    A[Need a model for a <br>downstream task] --> B{How many trusted labels<br> do you have?}
    B -->|enough| C[Start supervised]
    B -->|few| D{Do you also have lots<br>of unlabeled data?}
    D -->|no| E[Collect labels or simplify task]
    D -->|yes| F{Can you define a useful<br>pretext task or augmentation?}
    F -->|yes| G[Try self-supervised pretraining]
    F -->|also yes for same task| H[Try semi-supervised training]
    G --> I[Fine-tune / linear probe]
    H --> I

flowchart TD
    A[Need a model for a <br>downstream task] --> B{How many trusted labels<br> do you have?}
    B -->|enough| C[Start supervised]
    B -->|few| D{Do you also have lots<br>of unlabeled data?}
    D -->|no| E[Collect labels or simplify task]
    D -->|yes| F{Can you define a useful<br>pretext task or augmentation?}
    F -->|yes| G[Try self-supervised pretraining]
    F -->|also yes for same task| H[Try semi-supervised training]
    G --> I[Fine-tune / linear probe]
    H --> I

Use this as a rule of thumb:

  • semi-supervised when unlabeled data should help the same task directly;
  • self-supervised when unlabeled data can first help learn a good representation.

Precise definitions

Let

  • \(\mathcal{D}_L = \{(x_i, y_i)\}_{i=1}^{n_L}\) be labeled data,
  • \(\mathcal{D}_U = \{x_j\}_{j=1}^{n_U}\) be unlabeled data.

Supervised learning uses only \(\mathcal{D}_L\) to learn the task \(x \mapsto y\).

Semi-supervised learning uses both \(\mathcal{D}_L\) and \(\mathcal{D}_U\) for the same downstream task.

Self-supervised learning creates a training signal from the data itself, for example by:

  • masking part of the input and predicting it;
  • reconstructing a corrupted input;
  • bringing two views of the same example closer in representation space.

A helpful distinction:

  • Semi-supervised asks:
    How can unlabeled data help the supervised task directly?
  • Self-supervised asks:
    How can I learn useful representations before I even have many labels?

Three common patterns

Code
flowchart TD
    A[Input x] --> B[Corrupt / mask / augment x]
    B --> C[Predict missing part<br/>or match two views]
    C --> D[Representation z]

    E[Labeled subset] --> F[Task loss]
    D --> F
    G[Unlabeled subset] --> H[Consistency or<br>pseudo-label signal]
    H --> F

flowchart TD
    A[Input x] --> B[Corrupt / mask / augment x]
    B --> C[Predict missing part<br/>or match two views]
    C --> D[Representation z]

    E[Labeled subset] --> F[Task loss]
    D --> F
    G[Unlabeled subset] --> H[Consistency or<br>pseudo-label signal]
    H --> F

Read this as:

  • self-supervised: create a pretext task to learn \(z\);
  • semi-supervised: combine task loss with an unlabeled-data loss;
  • sometimes we do both in sequence.

The key assumptions

These methods help only when the unlabeled data tells us something useful about task structure.

Common assumptions:

  1. Cluster assumption
    points in the same high-density region should usually share a label.

  2. Low-density separation
    good decision boundaries should avoid dense regions of data.

  3. Augmentation / invariance assumption
    label-preserving transformations should not change the prediction much.

  4. Transfer assumption
    the pretext task must force the model to learn features that are useful later.

If these assumptions fail, unlabeled data can actively mislead the model.

❌ Anti-example: when the idea is misused

Suppose we build pseudo-labels for a medical-image classifier using high confidence on an unlabeled hospital dataset.

That can go wrong if:

  • the unlabeled pool comes from a different scanner or population;
  • the model is confidently wrong on minority subgroups;
  • we treat all transformations as label-preserving when some are not;
  • we add thousands of pseudo-labels and drown out the few trusted labels.

This is a classic anti-example:

“More data” is not the same thing as “more correct supervision.”

Unlabeled data is only helpful when its structure aligns with the target task and data-generating process.

Math lens

One common semi-supervised objective

A standard pattern is

\[ \mathcal{L}(\theta) = \underbrace{ \frac{1}{|\mathcal{D}_L|} \sum_{(x,y)\in \mathcal{D}_L} \ell(f_\theta(x), y) }_{\text{supervised task loss}} + \lambda \underbrace{ \frac{1}{|\mathcal{D}_U|} \sum_{x\in \mathcal{D}_U} \ell_u(f_\theta, x) }_{\text{unlabeled-data term}} \]

Examples of \(\ell_u\):

  • pseudo-labeling
    use the model’s own high-confidence predictions as temporary labels;

  • consistency regularization
    require similar predictions for two perturbations \(t_1(x), t_2(x)\).

The tuning parameter \(\lambda\) matters:
too small and unlabeled data does little; too large and noisy pseudo-supervision can dominate.

Self-supervised objectives

Three common examples:

1. Reconstruction / denoising (Vincent et al. 2010) \[ \mathcal{L}_{\text{denoise}}(\theta) = \sum_x \| g_\theta(\tilde{x}) - x \|^2 \] where \(\tilde{x}\) is a corrupted version of \(x\).

2. Masked prediction (Devlin et al. 2019) \[ \mathcal{L}_{\text{mask}}(\theta) = -\sum_x \log p_\theta(x_{\text{masked}} \mid x_{\text{visible}}) \]

3. Contrastive learning (Chen et al. 2020) \[ \mathcal{L}_{\text{ctr}}(\theta) = -\log \frac{\exp(\operatorname{sim}(z_i, z_i^+)/\tau)} {\sum_j \exp(\operatorname{sim}(z_i, z_j)/\tau)} \] where

  • \(z_i\) is the learned representation (embedding) of anchor example \(x_i\);
  • \(z_i^+\) is the encoder output for another label-preserving view of the same example, such as a second crop, mask, or corruption;
  • \(\tau > 0\) is the temperature parameter, which controls how sharply similarities are scaled inside the softmax.

All three try to make the representation \(z\) capture stable, reusable structure.

The goal is not to solve the final task directly.
The goal is to learn features that make the final task easier with fewer labels.

🔬 Reasoning sketch: why consistency can help

Suppose two nearby points \(x\) and \(x'\) come from the same dense region of data.

If our unlabeled objective penalizes disagreement,

\[ \ell_u \propto d\!\left(f_\theta(x), f_\theta(x')\right), \]

then placing a decision boundary inside that dense region creates lots of local disagreements and increases loss.

So, under the cluster assumption, minimizing consistency loss tends to encourage boundaries that move toward low-density regions between clusters.

Warning

This is not a guarantee in every dataset.

But it is the key intuition behind many semi-supervised methods:

  • consistency inside clusters;
  • separation between clusters;
  • better use of unlabeled structure when labels are sparse.

Worked example 1

Semi-supervised learning by pseudo-labeling

Task: handwritten digit classification with only 5 labeled examples per class.

Question: can unlabeled examples help?

Design: train a classifier on the tiny labeled set, use it to assign high-confidence pseudo-labels to unlabeled examples, add those pseudo-labeled examples back into training, and compare the result to a supervised-only baseline.

Design subtlety: the confidence threshold for accepting pseudo-labels matters.

  • low threshold \(\rightarrow\) more pseudo-labels, but noisier ones;
  • high threshold \(\rightarrow\) cleaner pseudo-labels, but fewer of them.
Code
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score

rng = np.random.default_rng(3151)

digits = load_digits()
X = digits.data / 16.0
y = digits.target

X_trainval, X_test, y_trainval, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=3151
)
X_train, X_val, y_train, y_val = train_test_split(
    X_trainval, y_trainval, test_size=0.25, stratify=y_trainval, random_state=3151
)

def choose_k_per_class(y, k=5, seed=3151):
    rng_local = np.random.default_rng(seed)
    idx = []
    for cls in np.unique(y):
        cls_idx = np.where(y == cls)[0]
        picked = rng_local.choice(cls_idx, size=k, replace=False)
        idx.extend(picked.tolist())
    idx = np.array(sorted(idx))
    rest = np.setdiff1d(np.arange(len(y)), idx)
    return idx, rest

labeled_idx, unlabeled_idx = choose_k_per_class(y_train, k=5, seed=3151)

X_lab, y_lab = X_train[labeled_idx], y_train[labeled_idx]
X_unlab, y_unlab_hidden = X_train[unlabeled_idx], y_train[unlabeled_idx]

baseline_lr = make_pipeline(
    StandardScaler(),
    LogisticRegression(max_iter=4000, multi_class="auto")
)
baseline_lr.fit(X_lab, y_lab)

baseline_val_acc = accuracy_score(y_val, baseline_lr.predict(X_val))
baseline_test_acc = accuracy_score(y_test, baseline_lr.predict(X_test))
baseline_test_f1 = f1_score(y_test, baseline_lr.predict(X_test), average="macro")

def pseudo_label_rounds(X_lab, y_lab, X_unlab, y_unlab_hidden, threshold, rounds=3):
    X_current = X_lab.copy()
    y_current = y_lab.copy()
    remaining = np.arange(len(X_unlab))
    trace = []

    for r in range(rounds):
        model = make_pipeline(
            StandardScaler(),
            LogisticRegression(max_iter=4000, multi_class="auto")
        )
        model.fit(X_current, y_current)

        if len(remaining) == 0:
            break

        probs = model.predict_proba(X_unlab[remaining])
        conf = probs.max(axis=1)
        pred = probs.argmax(axis=1)
        keep = conf >= threshold

        added = int(np.sum(keep))
        pseudo_acc = float(np.mean(pred[keep] == y_unlab_hidden[remaining][keep])) if added > 0 else np.nan

        trace.append(
            {"round": r + 1, "added": added, "pseudo_label_accuracy": pseudo_acc}
        )

        if added == 0:
            break

        X_current = np.vstack([X_current, X_unlab[remaining][keep]])
        y_current = np.concatenate([y_current, pred[keep]])
        remaining = remaining[~keep]

    final_model = make_pipeline(
        StandardScaler(),
        LogisticRegression(max_iter=4000, multi_class="auto")
    )
    final_model.fit(X_current, y_current)
    return final_model, pd.DataFrame(trace), len(y_current)

thresholds = [0.80, 0.90, 0.95, 0.99]
rows = []
models = {}

for thr in thresholds:
    model, trace_df, final_n = pseudo_label_rounds(
        X_lab, y_lab, X_unlab, y_unlab_hidden, threshold=thr, rounds=3
    )
    models[thr] = model
    rows.append({
        "threshold": thr,
        "initial_labeled": len(y_lab),
        "final_training_size": final_n,
        "val_accuracy": accuracy_score(y_val, model.predict(X_val)),
        "test_accuracy": accuracy_score(y_test, model.predict(X_test)),
        "test_macro_f1": f1_score(y_test, model.predict(X_test), average="macro"),
        "mean_pseudo_label_accuracy": trace_df["pseudo_label_accuracy"].mean()
            if not trace_df.empty else np.nan
    })

pseudo_results = pd.DataFrame(rows).sort_values("threshold")
best_thr = pseudo_results.loc[pseudo_results["val_accuracy"].idxmax(), "threshold"]
best_pseudo_model = models[best_thr]

comparison_df = pd.DataFrame({
    "model": ["supervised-only baseline", f"pseudo-labeling (thr={best_thr:.2f})"],
    "training_examples_used": [len(y_lab), int(pseudo_results.loc[pseudo_results['threshold'] == best_thr, 'final_training_size'].iloc[0])],
    "validation_accuracy": [baseline_val_acc, accuracy_score(y_val, best_pseudo_model.predict(X_val))],
    "test_accuracy": [baseline_test_acc, accuracy_score(y_test, best_pseudo_model.predict(X_test))],
    "test_macro_f1": [baseline_test_f1, f1_score(y_test, best_pseudo_model.predict(X_test), average="macro")]
})

pseudo_results.round(3)
threshold initial_labeled final_training_size val_accuracy test_accuracy test_macro_f1 mean_pseudo_label_accuracy
0 0.80 50 902 0.892 0.886 0.885 0.898
1 0.90 50 719 0.869 0.861 0.856 0.968
2 0.95 50 479 0.847 0.822 0.815 0.977
3 0.99 50 66 0.828 0.819 0.817 1.000

Warning

The hidden-label pseudo-label accuracy above is included only as a teaching diagnostic. In a real unlabeled pool, you would not know it directly.

Threshold choice really matters

Code
fig, ax = plt.subplots(figsize=(8.5, 4.8))

ax.plot(
    pseudo_results["threshold"],
    pseudo_results["val_accuracy"],
    marker="o",
    label="validation accuracy"
)
ax.plot(
    pseudo_results["threshold"],
    pseudo_results["mean_pseudo_label_accuracy"],
    marker="s",
    label="pseudo-label accuracy (teaching diagnostic)"
)
ax.axhline(baseline_val_acc, linestyle="--", linewidth=1.5, label="baseline val accuracy")

offsets = [10, -16, 10, -16]
for i, (_, row) in enumerate(pseudo_results.iterrows()):
    ax.annotate(
        f"+{int(row['final_training_size'] - len(y_lab))}",
        (row["threshold"], row["val_accuracy"]),
        textcoords="offset points",
        xytext=(0, offsets[i % len(offsets)]),
        ha="center",
        fontsize=9
    )

ax.set_xlabel("confidence threshold")
ax.set_ylabel("score")
ax.set_title("Pseudo-label threshold trade-off")
ax.set_ylim(0.4, 1.02)
ax.legend()
plt.show()

Note

Higher thresholds give cleaner pseudo-labels, but far fewer of them. Lower thresholds add much more data, but with more noise. The best validation result comes from balancing those two effects.

What to notice

A good analysis is not just “semi-supervised worked” or “semi-supervised failed.”

It should say why:

  • lower thresholds usually add more data, but with more label noise;
  • higher thresholds usually add cleaner pseudo-labels, but may add too little data to matter;
  • validation data should choose the threshold, not the test set;
  • macro-F1 is worth checking because accuracy alone can hide uneven per-class behaviour.

This is exactly the kind of scientific control this course has emphasized all term.

🔬 A tiny consistency-regularization code sketch

Pseudo-labeling uses model predictions as temporary labels.
Consistency regularization instead asks the model to give similar predictions under small perturbations.

# x_u is an unlabeled minibatch
p1 = model(augment(x_u)) # `augment` is stochastic. It will provide different views here.
p2 = model(augment(x_u))

supervised = cross_entropy(model(x_l), y_l)
consistency = ((p1.softmax(dim=1) - p2.softmax(dim=1)) ** 2).mean()

loss = supervised + lambda_u * consistency
loss.backward()
optimizer.step()
optimizer.zero_grad()

This does not invent hard labels.
It says: for label-preserving perturbations, predictions should be stable.

Worked example 2

Self-supervised pretraining with a denoising autoencoder

Now we switch the question.

Instead of using unlabeled data to label the task directly, we ask:

Can unlabeled data help us learn a representation that works better with very few labels?

We will:

  1. ignore labels and train a tiny denoising autoencoder on the training images;
  2. use the encoder representation as features;
  3. fit a small supervised classifier on the same tiny labeled subset as before.

Design subtlety: the corruption strength matters.

  • too weak \(\rightarrow\) trivial pretext task;
  • too strong \(\rightarrow\) the pretext task stops matching useful structure.
Code
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.decomposition import PCA

torch.manual_seed(3151)
device = "cpu"

class TinyDenoisingAE(nn.Module):
    def __init__(self, input_dim=64, hidden_dim=128, latent_dim=32):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, latent_dim)
        )
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, input_dim),
            nn.Sigmoid()
        )

    def forward(self, x):
        z = self.encoder(x)
        return self.decoder(z)

    def encode(self, x):
        return self.encoder(x)

ae = TinyDenoisingAE().to(device)
optimizer = torch.optim.Adam(ae.parameters(), lr=1e-3)
loss_fn = nn.MSELoss()

X_pretrain = torch.tensor(X_train, dtype=torch.float32)
train_loader = DataLoader(TensorDataset(X_pretrain), batch_size=128, shuffle=True)

noise_std = 0.1 #0.25
ae_history = []

for epoch in range(100):
    running = 0.0
    n_seen = 0
    ae.train()
    for (xb,) in train_loader:
        xb = xb.to(device)
        noisy = (xb + noise_std * torch.randn_like(xb)).clamp(0.0, 1.0)

        optimizer.zero_grad()
        recon = ae(noisy)
        loss = loss_fn(recon, xb)
        loss.backward()
        optimizer.step()

        running += loss.item() * len(xb)
        n_seen += len(xb)

    ae_history.append(running / n_seen)

def encode_array(model, X_np):
    model.eval()
    with torch.no_grad():
        x = torch.tensor(X_np, dtype=torch.float32).to(device)
        z = model.encode(x).cpu().numpy()
    return z

Z_lab = encode_array(ae, X_lab)
Z_val = encode_array(ae, X_val)
Z_test = encode_array(ae, X_test)

ssl_lr = make_pipeline(
    StandardScaler(),
    LogisticRegression(max_iter=4000, multi_class="auto")
)
ssl_lr.fit(Z_lab, y_lab)

ssl_compare = pd.DataFrame({
    "representation": ["raw pixels", "self-supervised encoder features"],
    "labeled_examples": [len(y_lab), len(y_lab)],
    "validation_accuracy": [
        baseline_val_acc,
        accuracy_score(y_val, ssl_lr.predict(Z_val))
    ],
    "test_accuracy": [
        baseline_test_acc,
        accuracy_score(y_test, ssl_lr.predict(Z_test))
    ],
    "test_macro_f1": [
        baseline_test_f1,
        f1_score(y_test, ssl_lr.predict(Z_test), average="macro")
    ]
})

ae.eval()
sample_x = torch.tensor(X_val[:6], dtype=torch.float32)
sample_noisy = (sample_x + noise_std * torch.randn_like(sample_x)).clamp(0.0, 1.0)
with torch.no_grad():
    sample_recon = ae(sample_noisy).cpu().numpy()

subset = slice(0, 350)
raw_2d = PCA(n_components=2).fit_transform(X_test[subset])
enc_2d = PCA(n_components=2).fit_transform(Z_test[subset])
y_subset = y_test[subset]

#ssl_compare.round(3)

Downstream comparison table

Code
ssl_compare.round(3)
representation labeled_examples validation_accuracy test_accuracy test_macro_f1
0 raw pixels 50 0.814 0.800 0.798
1 self-supervised encoder features 50 0.844 0.828 0.825

Original, noisy, reconstructed

Code
fig, axes = plt.subplots(3, 6, figsize=(10, 5))

for j in range(6):
    axes[0, j].imshow(sample_x[j].reshape(8, 8), cmap="gray_r")
    axes[1, j].imshow(sample_noisy[j].reshape(8, 8), cmap="gray_r")
    axes[2, j].imshow(sample_recon[j].reshape(8, 8), cmap="gray_r")

for ax in axes.ravel():
    ax.set_xticks([])
    ax.set_yticks([])

axes[0, 0].set_ylabel("original")
axes[1, 0].set_ylabel("noisy")
axes[2, 0].set_ylabel("recon")

fig.suptitle("Denoising autoencoder pretext task", y=1.02)
plt.tight_layout()
plt.show()

Raw pixels vs learned features

Code
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

pca_raw = PCA(n_components=2).fit_transform(X_test)
pca_ssl = PCA(n_components=2).fit_transform(Z_test)

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

sc1 = axes[0].scatter(pca_raw[:, 0], pca_raw[:, 1], c=y_test, cmap="tab10", alpha=0.7)
sc2 = axes[1].scatter(pca_ssl[:, 0], pca_ssl[:, 1], c=y_test, cmap="tab10", alpha=0.7)

axes[0].set_title("PCA of raw-pixel features")
axes[1].set_title("PCA of self-supervised encoder features")
axes[0].set_xlabel("PC1")
axes[0].set_ylabel("PC2")
axes[1].set_xlabel("PC1")
axes[1].set_ylabel("PC2")

fig.subplots_adjust(right=0.88)

cbar_ax = fig.add_axes([0.90, 0.18, 0.02, 0.64])
cbar = fig.colorbar(sc2, cax=cbar_ax)
cbar.set_label("digit")

plt.show()

Interpretation

Why might this help?

  • the model is forced to represent stable visual structure rather than memorize a tiny labeled set;
  • the downstream classifier then works in a space that may already separate strokes, shapes, and local patterns;
  • the PCA view is only a diagnostic, but it helps us see whether the representation becomes more structured;
  • improvement is not guaranteed:
    • a poor pretext task can learn the wrong invariances;
    • on easy datasets, raw pixels may already be enough;
    • I set hidden_dim ⬆️ and latent_dim ⬆️ and noise_std ⬇️, which helped
    • the pretraining distribution must still resemble the downstream one.

Bird’s eye view of final thoughts

Method landscape: four patterns to know

Family Supervision source Typical objective Best mental model Main risk
Pseudo-labeling model’s own confident predictions add temporary labels on unlabeled data “bootstrap the task” confirmation of early mistakes
Consistency regularization perturbation agreement make predictions stable across views “same example, same answer” bad augmentations enforce wrong invariances
Masked prediction hidden part of input predict the missing piece “fill in the blank” pretext task may not match downstream needs
Contrastive learning positive vs negative pairs pull matched views together, push others apart “who belongs with whom?” poor pair construction or collapse

Pseudo-labeling vs consistency regularization

Both are semi-supervised, but they use unlabeled data differently.

Pseudo-labeling - turns a model prediction into a temporary hard target; - often requires a confidence threshold; - can give strong gains when predictions are already mostly correct.

Consistency regularization - does not require hard targets; - asks for stable predictions under label-preserving perturbations; - often behaves more gently early in training.

A useful rule:

pseudo-labeling says “I think this is class 3”
consistency says “I should say nearly the same thing after a small perturbation”

Masked prediction vs contrastive learning

Both are self-supervised, but they produce representations differently.

Masked prediction

  • hide part of the input;
  • predict the missing token / patch / feature;
  • common in language and sequence modelling (Devlin et al. 2019).

Contrastive learning

  • create two related views of the same example;
  • pull those views together in embedding space;
  • push unrelated examples apart.

A useful rule:

masked prediction learns from reconstruction pressure
contrastive learning learns from relative similarity pressure

Contrastive learning visual

Code
flowchart LR
    X["Example x"] --> A["Augmentation t1(x)"]
    X --> B["Augmentation t2(x)"]
    A --> ZA["Embedding z"]
    B --> ZB["Embedding z+"]
    N1["Other example"] --> ZN1["Embedding z1-"]
    N2["Other example"] --> ZN2["Embedding z2-"]

    ZA --- ZB
    ZA -. far .- ZN1
    ZA -. far .- ZN2

flowchart LR
    X["Example x"] --> A["Augmentation t1(x)"]
    X --> B["Augmentation t2(x)"]
    A --> ZA["Embedding z"]
    B --> ZB["Embedding z+"]
    N1["Other example"] --> ZN1["Embedding z1-"]
    N2["Other example"] --> ZN2["Embedding z2-"]

    ZA --- ZB
    ZA -. far .- ZN1
    ZA -. far .- ZN2

The representation should make the two views of the same item close, and views of different items relatively far apart.

🔬 Brief code sketch: contrastive learning

x1 = augment(x_batch)
x2 = augment(x_batch)

z1 = encoder(x1)
z2 = encoder(x2)

z1 = normalize(z1, dim=1)
z2 = normalize(z2, dim=1)

similarity = z1 @ z2.T / tau
loss = contrastive_nt_xent(similarity)

This is the pattern behind many “two-view” methods.
The implementation details vary, but the core idea is always view matching in embedding space.

🔬 A bridge to A4: masked prediction and BERT-style pretraining

The worked examples above are image-based because they are small and self-contained.

But the lab connected to this module is explicitly text-oriented:

  • denoising or next-token prediction on text;
  • reuse the encoder in a downstream classification task;
  • compare pretrained representations against training from scratch.

So the image autoencoder example should be read as a structural analogue of what happens in text:

  • BERT-style masked language modeling: hide tokens and predict them;
  • GPT-style next-token prediction: predict the next symbol from context;
  • then fine-tune or linear-probe the learned representation on a downstream task.

That is the conceptual bridge from today’s examples to A4.

🔬 Very brief note: where reinforcement learning fits

Reinforcement learning (RL) is different from both self-supervised and semi-supervised learning (Sutton and Barto 2018; Mnih et al. 2015; Schulman et al. 2017).

  • In semi-supervised learning, the extra signal comes from unlabeled examples for the same task.
  • In self-supervised learning, the signal is generated from the structure of the input itself.
  • In RL, the signal comes from rewards obtained through interaction with an environment.

Note

So RL belongs to the broader family of learning settings. This is not the focus of this module.

See CSCI 4250!

Summary

Evaluation and quality checks

For this module, the evaluation question is not just:

“Did the model’s score go up?”

It is:

“Did unlabeled data help for the reason we think it did?”

Useful checks (Oliver et al. 2018):

  • validation accuracy / macro-F1 for model or threshold selection;
  • test performance only once, at the end;
  • ablation against a supervised-only baseline with the same number of labeled examples;
  • sensitivity to threshold / corruption / augmentation strength;
  • sanity checks on the unlabeled pool:
    • same population?
    • same feature space?
    • no leakage from test data?

Warning

When labels are scarce, variance is often high, so one run can be misleading.

Common failure modes

  1. Confirmation loops in pseudo-labeling
    the model reinforces its own early mistakes.

  2. Broken invariances
    an augmentation changes the label, but we pretend it does not.

  3. Unhelpful pretext tasks
    the model solves the pretext objective without learning features useful for the downstream task.

  4. Distribution mismatch
    unlabeled data comes from a different source than the labeled task.

  5. Overclaiming from pretty representations
    nice clusters in embedding plots do not automatically mean the representation is good for decisions.

Ethics and risks

These methods often rely on large unlabeled datasets, which introduces practical risks:

  • bias amplification: pseudo-labels may be more accurate for already well-represented groups, reinforcing disparities;
  • privacy: “unlabeled” does not mean harmless—raw text, images, or logs may still contain sensitive information;
  • representation harms: a pretraining objective can encode spurious correlations that later transfer into downstream tasks;
  • robustness / misuse: high-confidence pseudo-labels can look authoritative even when they are wrong.

Simple mitigations:

  • compare subgroup performance when possible;
  • document where unlabeled data came from;
  • inspect failure cases, not just averages;
  • keep a clean supervised baseline for reference;
  • avoid claiming that self-supervision magically removes bias.

Why this matters in modern ML

Modern ML systems often use a two-stage recipe:

  1. pretrain on huge unlabeled corpora;
  2. adapt with smaller amounts of task-specific supervision.

That is the story behind many influential systems:

  • BERT and masked language modeling;
  • GPT and next-token prediction;
  • 🔗CLIP and contrastive image–text alignment;
  • wav2vec and self-supervised speech representations.

Quick checks

Q1.: A semi-supervised method is most likely to help when:

A. the unlabeled data is from a completely different population than the labeled data
B. points in dense regions tend to share labels
C. the model is already perfect on the labeled subset
D. we choose thresholds using the test set

Answer: B — semi-supervised methods often rely on cluster / low-density assumptions.

Q2. Why can a very low pseudo-label confidence threshold be dangerous?

A. it uses too little unlabeled data
B. it increases computation but never changes learning
C. it can flood training with noisy pseudo-labels
D. it guarantees overfitting

Answer: C — a low threshold adds more examples, but many may be incorrectly labeled.

Q3. A denoising autoencoder is self-supervised because:

A. it uses a human to annotate reconstructions
B. it predicts labels from a small supervised set
C. it creates a prediction task from the input itself
D. it is always trained with contrastive loss

Answer: C — the target comes from the original input, not from external labels.

Q4. You observe that self-supervised pretraining improves validation accuracy on a tiny labeled set. What is the best next check?

A. stop immediately and report only the best number
B. inspect sensitivity to pretext-task design and compare against a supervised baseline
C. add the test set into training to stabilize results
D. assume the learned representation is fair

Answer: B — we want to know whether the gain is robust and whether it comes from the intended mechanism.

References

Chen, Ting, Simon Kornblith, Mohammad Norouzi, and Geoffrey Hinton. 2020. “A Simple Framework for Contrastive Learning of Visual Representations.” In Proceedings of the 37th International Conference on Machine Learning, 119:1597–607. Proceedings of Machine Learning Research. PMLR. https://arxiv.org/abs/2002.05709.
Devlin, Jacob, Ming-Wei Chang, Kenton Lee, and Kristina Toutanova. 2019. “BERT: Pre-Training of Deep Bidirectional Transformers for Language Understanding.” In Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies (NAACL-HLT), 4171–86.
Mnih, Volodymyr, Koray Kavukcuoglu, David Silver, Andrei A. Rusu, Joel Veness, Marc G. Bellemare, Alex Graves, et al. 2015. “Human-Level Control Through Deep Reinforcement Learning.” Nature 518 (7540): 529–33. https://doi.org/10.1038/nature14236.
Oliver, Avital, Augustus Odena, Colin Raffel, Ekin D. Cubuk, and Ian J. Goodfellow. 2018. “Realistic Evaluation of Deep Semi-Supervised Learning Algorithms.” arXiv Preprint arXiv:1804.09170. https://arxiv.org/abs/1804.09170.
Schulman, John, Filip Wolski, Prafulla Dhariwal, Alec Radford, and Oleg Klimov. 2017. “Proximal Policy Optimization Algorithms.” arXiv Preprint arXiv:1707.06347. https://arxiv.org/abs/1707.06347.
Sutton, Richard S., and Andrew G. Barto. 2018. Reinforcement Learning: An Introduction. 2nd ed. Cambridge, MA: MIT Press.
Vincent, Pascal, Hugo Larochelle, Isabelle Lajoie, Yoshua Bengio, and Pierre-Antoine Manzagol. 2010. “Stacked Denoising Autoencoders: Learning Useful Representations in a Deep Network with a Local Denoising Criterion.” Journal of Machine Learning Research 11: 3371–3408.