
CSCI 3151 — Foundations of Machine Learning
By the end of this module, you should be able to:
Think of our ML pipeline as:
\[ x \in \mathcal{X} \;\xrightarrow{\;\phi\;}\; \boxed{z \in \mathcal{Z}} \;\xrightarrow{\;g\;}\; \hat{y} \in \mathcal{Y} \]
Representation learning n.: is the process of training a model so that it maps raw data into a new space of representations (embeddings / feature vectors) that make important tasks (like classification, prediction, retrieval, generation) easier to solve (Bengio, Courville, and Vincent 2013; Goodfellow, Bengio, and Courville 2016).
We often factor a big model into:
This factorization is what lets us:
Consider a 2D feature space where each point is an image of a digit, coloured by its true label (0–9):

We’ll make this intuition precise shortly.
Representation function
\(\phi: \mathcal{X} \to \mathbb{R}^d\), where \(d\) is the embedding dimension.
Task-aligned representation
\(\phi\) is task-aligned if a simple model on top of \(z = \phi(x)\) performs well.
Invariance
A representation is invariant to a transformation \(T\) (e.g., small translation, rotation, noise) if:
\[ \phi(T(x)) \approx \phi(x) \]
Equivariance
Equivariance means transformations in input space induce predictable transformations in feature space:
\[ \phi(T(x)) \approx A_T \, \phi(x) \]
(e.g., shifting an image shifts feature maps in a CNN).
There is no single universal definition, but common desiderata include:
We’ll use these as a checklist when we look at code examples.
Consider a 2D t-SNE plot of patient health records where:
Problems:
This is a bad representation for disease risk: it violates task alignment and bakes in spurious correlations.
For a \(K\)-class classification problem:
\[ \hat{y} = \arg\max_{k \in \{1,\dots,K\}} \left( w_k^\top z + b_k \right) \]
Why do we like linear separability?
Let \(x\) be inputs, \(y\) labels, \(z = \phi(x)\) representations.
Two desirable properties (often in tension):
Task information preserved
\[
I(z; y) \quad \text{large (mutual information)}
\] so that representations retain predictive signal about labels.
Nuisance compression
\[
I(z; u) \quad \text{small}
\] where \(u\) encodes nuisance factors (lighting, site, identity if we care about anonymity, etc.)
We rarely compute these mutual informations exactly in practice, but this information-bottleneck picture is a useful mental model for what “good” representations are trying to achieve.
Consider a parametric encoder \(\phi_\theta\) and a simple linear head \(g_w\).
Different training objectives push \(\phi_\theta\) in different directions:
Supervised classification objective
\[
\min_{\theta, w} \; \mathbb{E}_{(x,y)}\big[ \ell(g_w(\phi_\theta(x)), y) \big]
\] Encourages representations where examples of the same class cluster together and are separable from other classes.
Autoencoder / reconstruction objective (Vincent et al. 2008)
\[
\min_{\theta, \psi} \; \mathbb{E}_{x}\big[ \| x - \tilde{x} \|^2 \big],
\quad \tilde{x} = \text{dec}_\psi(\phi_\theta(x))
\] where \(\text{dec}_\psi\) is a decoder that maps a representation \(z\) back into data space, producing a reconstruction \(\hat{x}\).
Encourages information-preserving (but not necessarily task-aligned) representations.
Contrastive objective (very simplified) (Chen et al. 2020)
\[
\min_{\theta} \; \mathbb{E}\big[ \ell_{\text{contrastive}}(\phi_\theta(x), \phi_\theta(x^+), \{\phi_\theta(x^-)\}) \big]
\] Encourages representations where augmentations of the same input map close together, and different inputs map far apart.
We’ll see echoes of these when we connect to self-supervised methods later in the course.
Goal: compare a “classical” compressed representation (PCA) to raw pixels for digit classification.
Dataset:
sklearn.datasets.load_digits()Steps:
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score, classification_report
# 1. Load data
digits = load_digits()
X, y = digits.data, digits.target # X: (n_samples, 64)
# 2. Train/test split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
# 3. Baseline: logistic regression on raw pixels
pipe_raw = Pipeline([
("scaler", StandardScaler()),
("logreg", LogisticRegression(max_iter=1000, multi_class="multinomial"))
])
pipe_raw.fit(X_train, y_train)
y_pred_raw = pipe_raw.predict(X_test)
acc_raw = accuracy_score(y_test, y_pred_raw)
print(f"Test accuracy (raw pixels): {acc_raw:.3f}")
print(classification_report(y_test, y_pred_raw))Test accuracy (raw pixels): 0.981
precision recall f1-score support
0 1.00 1.00 1.00 54
1 0.95 0.95 0.95 55
2 1.00 1.00 1.00 53
3 1.00 1.00 1.00 55
4 0.98 0.98 0.98 54
5 1.00 0.98 0.99 55
6 1.00 0.98 0.99 54
7 1.00 1.00 1.00 54
8 0.91 0.94 0.92 52
9 0.98 0.98 0.98 54
accuracy 0.98 540
macro avg 0.98 0.98 0.98 540
weighted avg 0.98 0.98 0.98 540
from sklearn.decomposition import PCA
import pandas as pd
# 4. Representation: PCA to 32 dimensions, then logistic regression
pipe_pca = Pipeline([
("scaler", StandardScaler()),
("pca", PCA(n_components=32, random_state=42)),
("logreg", LogisticRegression(max_iter=1000, multi_class="multinomial"))
])
pipe_pca.fit(X_train, y_train)
y_pred_pca = pipe_pca.predict(X_test)
acc_pca = accuracy_score(y_test, y_pred_pca)
# 5. Put results in a small table
results = pd.DataFrame([
{"Representation": "Raw pixels", "Dim": X.shape[1], "Test accuracy": acc_raw},
{"Representation": "PCA(32)", "Dim": 32, "Test accuracy": acc_pca},
])
results| Representation | Dim | Test accuracy | |
|---|---|---|---|
| 0 | Raw pixels | 64 | 0.981481 |
| 1 | PCA(32) | 32 | 0.951852 |
Tip
Some courses or tutorials act as though every intervention will help.
It’s important to dissipate that fiction, but also be aware of nuances.
import matplotlib.pyplot as plt
# Fit PCA(2) on *train* data only for visualization
pca2 = PCA(n_components=2, random_state=42)
X_train_scaled = StandardScaler().fit_transform(X_train)
X_train_2d = pca2.fit_transform(X_train_scaled)
plt.figure(figsize=(6, 5))
scatter = plt.scatter(
X_train_2d[:, 0], X_train_2d[:, 1],
c=y_train, s=12, alpha=0.8
)
plt.xlabel("PC 1")
plt.ylabel("PC 2")
plt.title("Digits: PCA(2) representation coloured by true digit")
plt.colorbar(scatter, ticks=range(10), label="Digit")
plt.tight_layout()
plt.show()
✏️ Check
Using our checklist:
Caveats
Goal: compare a cheap classifier on:
Dataset:
💡 Idea:
import torch
from torch import nn, optim
from torch.utils.data import DataLoader, Subset
from torchvision import datasets, transforms
# 1. Transforms & dataset (simple normalization)
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5],
std=[0.5, 0.5, 0.5])
])
train_dataset = datasets.CIFAR10(
root="data", train=True, download=True, transform=transform
)
test_dataset = datasets.CIFAR10(
root="data", train=False, download=True, transform=transform
)
# Subsample train set for speed
subset_indices = torch.randperm(len(train_dataset))[:5000]
train_subset = Subset(train_dataset, subset_indices)
train_loader = DataLoader(train_subset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False)class SmallCNN(nn.Module):
def __init__(self, feature_dim=256, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2), # 16x16
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2), # 8x8
)
self.flatten = nn.Flatten()
self.fc = nn.Sequential(
nn.Linear(64 * 8 * 8, feature_dim),
nn.ReLU()
)
self.classifier = nn.Linear(feature_dim, num_classes)
def forward(self, x, return_features=False):
h = self.features(x)
h = self.flatten(h)
z = self.fc(h) # representation
logits = self.classifier(z) # head
if return_features:
return logits, z
return logits
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SmallCNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)def train_epoch(model, loader, optimizer, device):
model.train()
running_loss, running_correct, total = 0.0, 0, 0
for x, y in loader:
x, y = x.to(device), y.to(device)
optimizer.zero_grad()
logits = model(x)
loss = criterion(logits, y)
loss.backward()
optimizer.step()
running_loss += loss.item() * x.size(0)
preds = logits.argmax(dim=1)
running_correct += (preds == y).sum().item()
total += x.size(0)
return running_loss / total, running_correct / total
for epoch in range(5): # a few epochs for demonstration
loss, acc = train_epoch(model, train_loader, optimizer, device)
print(f"Epoch {epoch+1}: loss={loss:.4f}, train_acc={acc:.3f}")Epoch 1: loss=1.8534, train_acc=0.324
Epoch 2: loss=1.4503, train_acc=0.481
Epoch 3: loss=1.2750, train_acc=0.545
Epoch 4: loss=1.1245, train_acc=0.599
Epoch 5: loss=0.9804, train_acc=0.648
Note
In practice, you’d also track validation accuracy, but here our main goal is to learn a useful feature extractor, not to fully tune performance.
We take the CNN trained on CIFAR-10, chop off the final classifier, and run images through it to get \(d\)-dimensional feature vectors from the penultimate layer.
We treat those vectors as learned representations and can then train a simple linear classifier (or visualize them with PCA/t-SNE) on top.
import numpy as np
def extract_representations(model, loader, device):
model.eval()
features_list, labels_list = [], []
with torch.no_grad():
for x, y in loader:
x = x.to(device)
logits, z = model(x, return_features=True)
features_list.append(z.cpu().numpy())
labels_list.append(y.numpy())
return np.concatenate(features_list, axis=0), np.concatenate(labels_list, axis=0)
# 1. Extract train/test features
Z_train, y_train = extract_representations(model, train_loader, device)
Z_test, y_test = extract_representations(model, test_loader, device)
print("Train features shape:", Z_train.shape) # e.g., (5000, 256)
print("Test features shape:", Z_test.shape)Train features shape: (5000, 256)
Test features shape: (10000, 256)
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from tqdm import tqdm
# 2. Baseline: linear classifier on raw flattened pixels
# Build flattened pixel arrays for the *same* subsets
def collect_flat_pixels(loader):
X_list, y_list = [], []
for x, y in loader:
# x: (B, C, H, W)
X_list.append(x.view(x.size(0), -1).numpy())
y_list.append(y.numpy())
return np.concatenate(X_list, axis=0), np.concatenate(y_list, axis=0)
X_train_flat, y_train_flat = collect_flat_pixels(train_loader)
X_test_flat, y_test_flat = collect_flat_pixels(test_loader)
logreg_pixels = LogisticRegression(
max_iter=200, multi_class="multinomial", n_jobs=-1
)
logreg_pixels.fit(X_train_flat, y_train_flat)
y_pred_pixels = logreg_pixels.predict(X_test_flat)
acc_pixels = accuracy_score(y_test_flat, y_pred_pixels)
# 3. Linear classifier on CNN representations
logreg_repr = LogisticRegression(
max_iter=200, multi_class="multinomial", n_jobs=-1
)
logreg_repr.fit(Z_train, y_train)
y_pred_repr = logreg_repr.predict(Z_test)
acc_repr = accuracy_score(y_test, y_pred_repr)
print("Test accuracy (linear on pixels):", acc_pixels)
print("Test accuracy (linear on CNN features):", acc_repr)Test accuracy (linear on pixels): 0.28
Test accuracy (linear on CNN features): 0.5499
import pandas as pd
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
results = pd.DataFrame([
{"Representation": "Flattened pixels", "Dim": X_train_flat.shape[1],
"Test accuracy": acc_pixels},
{"Representation": "CNN features", "Dim": Z_train.shape[1],
"Test accuracy": acc_repr},
])
results| Representation | Dim | Test accuracy | |
|---|---|---|---|
| 0 | Flattened pixels | 3072 | 0.2800 |
| 1 | CNN features | 256 | 0.5499 |
# 2D PCA of CNN features for visualization
pca2 = PCA(n_components=2, random_state=42)
Z_test_2d = pca2.fit_transform(Z_test)
plt.figure(figsize=(6, 5))
scatter = plt.scatter(
Z_test_2d[:, 0], Z_test_2d[:, 1],
c=y_test, s=8, alpha=0.7
)
plt.xlabel("PC 1")
plt.ylabel("PC 2")
plt.title("CIFAR-10: PCA(2) of CNN representations")
plt.colorbar(scatter, ticks=range(10), label="Class")
plt.tight_layout()
plt.show()
What we typically see
Using our checklist:
🔑 Key lesson
A “good” representation is one where simple downstream models work well and where geometry aligns with task semantics.
We need to align:
Example diagnostics:
Some practical proxies (beyond loss):
Important
These are task-dependent; there is no single scalar “representation goodness score” that works for everything.
A few examples where representation learning is central:
Research questions you could pursue later:
How do we measure representation quality rigorously?
(e.g., representational similarity / CKA-style metrics (Kornblith et al. 2019))
How can we enforce fairness and privacy at the level of representations?
(fair representations and DP training (Zemel et al. 2013; Abadi et al. 2016))
Can we design objectives that produce universally useful embeddings across many tasks?
(large-scale pretraining and multimodal models (Radford et al. 2021; Brown et al. 2020))
Which of the following best describes a task-aligned representation?
A. One that perfectly reconstructs the input.
B. One where a simple model achieves high performance on the target task.
C. One that minimizes the mutual information \(I(z; x)\).
D. One that uses as few dimensions as possible.
In Example 2 (CIFAR-10), why does a linear classifier on CNN features typically outperform a linear classifier on flattened pixels?
A. CNN features have more dimensions than pixels.
B. CNN features are explicitly optimized to be linearly separable for the training labels.
C. Linear classifiers cannot operate directly on pixel inputs.
D. The CNN features are random and thus more robust.
Which of the following is not a common desideratum for a good representation?
A. Invariance to nuisance transformations.
B. High mutual information with irrelevant environment variables.
C. Smoothness: small input changes → small representation changes.
D. Task alignment for simple downstream heads.
Suppose you train an autoencoder on images and use its latent code as a representation for classification. Which concern is most appropriate?
A. Latent codes will always be linearly separable for any label.
B. Reconstruction objectives may preserve details irrelevant or harmful for the classification task.
C. Autoencoders never leak sensitive attributes.
D. Autoencoders cannot be trained on images with more than 64x64 pixels.
