CSCI 3151 — Foundations of Machine Learning
By the end of this module, you should be able to:
Common pain points with high-dimensional data:
Dimensionality reduction tries to:
PCA is the canonical linear method for this.
We assume:
Linear dimensionality reduction:
🤔 Questions
Left: original 2D cloud with projection onto the first principal component (PC1)
Right: the same cloud after rotating the plane so that PC1 is exactly the new x-axis.
Principal Component Analysis (PCA) finds orthogonal directions in feature space:
The first principal component:
Subsequent components:
We have a tabular dataset with:
age (years),income (dollars),num_visits (counts),has_disease (binary label).A common but problematic workflow:
✏️ What goes wrong?
This is a form of data leakage:
the representation itself is tuned using information from the test set.
✏️ Fix:
Pipeline so that fit/transform happen with correct splits.Assume:
Define the (empirical) covariance matrix: \[ \begin{aligned} \Sigma &= \frac{1}{n}\, \tilde X^\top \tilde X \\ &= \frac{1}{n} \sum_{i=1}^n (x_i - \mu)(x_i - \mu)^\top \in \mathbb{R}^{d \times d}. \end{aligned} \]
Note
Many stats texts use \(1/(n-1)\) instead of \(1/n\).
For PCA this only rescales the eigenvalues, not the principal directions.
For any unit vector \(w \in \mathbb{R}^d\) with \(\|w\|_2 = 1\):
The variance of these coordinates is \[ \operatorname{Var}(z_i) = \operatorname{Var}\big(w^\top (x_i - \mu)\big) = w^\top \Sigma w. \]
Note
PCA chooses directions \(w\) that maximize this projected variance, subject to the directions being orthogonal to each other.
Goal for the first principal component:
\[ \begin{aligned} \max_{w_1} \quad & w_1^\top \Sigma w_1 \\ \text{s.t.} \quad & \|w_1\|_2^2 = 1. \end{aligned} \]
This is a constrained optimization problem.
Using 🔗 Lagrange multipliers:
So:
Once we have \(w_1\), the first principal component, we look for a second direction \(w_2\) that:
\[ \begin{aligned} \max_{w_2} \quad & w_2^\top \Sigma w_2 \\ \text{s.t.} \quad & \|w_2\|_2^2 = 1, \\ & w_2^\top w_1 = 0. \end{aligned} \]
Similarly, we obtain:
Continuing:
Then:
Another view:
We choose:
\[ \min_{\hat{X} : \operatorname{rank}(\hat{X}) \le k} \|X - \hat{X}\|_F^2, \]
where \(\|\cdot\|_F\) is the 🔗 Frobenius norm (sum of squared entries).
It turns out that:
🤓 Intuition
Let the singular value decomposition (SVD) of \(X\) be:
\[ X = U \Sigma_X V^\top, \]
where:
Facts:
So:
Input: Data matrix \(X \in \mathbb{R}^{n \times d}\), desired dimension \(k\).
In scikit-learn:
sklearn.decomposition.PCA handles most of this for you.We start with the (thin) SVD of \(X \in \mathbb{R}^{n \times d}\):
\[ X = U \Sigma V^\top, \]
where
Conceptually:
\[ X = \underbrace{ \begin{bmatrix} \color{red}{|} & & \color{red}{|} & | & & | \\ \color{red}{u_1} & \color{red}{\dots} & \color{red}{u_k} & u_{k+1} & \dots & u_d \\ \color{red}{|} & & \color{red}{|} & | & & | \end{bmatrix} }_{\color{red}{\text{keep first $k$ columns}}} \underbrace{ \begin{bmatrix} \color{red}{\sigma_1} & & & & \\ & \ddots & & & \\ & & \color{red}{\sigma_k} & & \\ & & & 0 & \\ & & & & \ddots \end{bmatrix} }_{\text{singular values (energy)}} \underbrace{ \begin{bmatrix} \color{red}{|} & & \color{red}{|} & | & & | \\ \color{red}{v_1} & \color{red}{\dots} & \color{red}{v_k} & v_{k+1} & \dots & v_d \\ \color{red}{|} & & \color{red}{|} & | & & | \end{bmatrix}^\top }_{\color{red}{\text{keep first $k$ rows}}} \]
To form the best rank-\(k\) approximation, we drop the grey parts and keep only the highlighted “top-\(k\) energy”:
\[ X_k = U_k \Sigma_k V_k^\top = \sum_{j=1}^k \sigma_j \, u_j v_j^\top. \]
Why these \(k\)?
Visually, you can imagine:
🔬 Aside
Eckart–Young: among all rank-\(k\) matrices, \(X_k\) is the closest to \(X\) in Frobenius norm: \[ X_k = \arg\min_{\operatorname{rank}(Y)\le k} \|X - Y\|_F. \] So we keep exactly the \(k\) directions that preserve the most “energy” (variance).
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(3151)
n = 300
mean = [0.0, 0.0]
cov = [[3.0, 2.5],
[2.5, 3.0]] # strong positive correlation
X = rng.multivariate_normal(mean, cov, size=n)
fig, ax = plt.subplots()
ax.scatter(X[:, 0], X[:, 1], alpha=0.5)
ax.set_xlabel("Feature 1")
ax.set_ylabel("Feature 2")
ax.set_title("Correlated 2D data")
plt.tight_layout()
plt.show()Figure 3: Correlated 2D data before PCA.
from sklearn.decomposition import PCA
pca_2d = PCA(n_components=2)
X_pca = pca_2d.fit_transform(X)
print("Explained variance ratios:", pca_2d.explained_variance_ratio_)
# Plot original data and principal components as arrows
fig, ax = plt.subplots()
ax.scatter(X[:, 0], X[:, 1], alpha=0.4)
origin = X.mean(axis=0)
for length, vector in zip(pca_2d.explained_variance_ratio_, pca_2d.components_):
# scale arrow length for visualization
arrow = vector * length * 10
ax.arrow(origin[0], origin[1],
arrow[0], arrow[1],
head_width=0.2, color="black")
ax.set_xlabel("Feature 1")
ax.set_ylabel("Feature 2")
ax.set_title("PCA components on correlated 2D data")
ax.set_aspect("equal", adjustable="box")
plt.tight_layout()
plt.show()Explained variance ratios: [0.90215352 0.09784648]
Figure 4: PCA components overlaid on the 2D data.
# Keep only the first component
pca_1d = PCA(n_components=1)
X_1d = pca_1d.fit_transform(X) # shape (n, 1)
X_recon = pca_1d.inverse_transform(X_1d) # shape (n, 2)
fig, ax = plt.subplots()
ax.scatter(X[:, 0], X[:, 1], alpha=0.2, label="Original")
ax.scatter(X_recon[:, 0], X_recon[:, 1], alpha=0.6, label="Reconstructed (1D PCA)")
for i in range(0, len(X), 20):
ax.plot([X[i, 0], X_recon[i, 0]],
[X[i, 1], X_recon[i, 1]], ":", linewidth=0.5)
ax.set_xlabel("Feature 1")
ax.set_ylabel("Feature 2")
ax.set_title("PCA reconstruction from 1D")
ax.legend()
plt.tight_layout()
plt.show()Note
Decision context:
Question:
We’ll compare:
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.decomposition import PCA
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score
import pandas as pd
digits = load_digits()
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=3151, stratify=y
)
X_train.shape, X_test.shape((1257, 64), (540, 64))
baseline_pipe = Pipeline(
steps=[
("scaler", StandardScaler()),
("logreg", LogisticRegression(max_iter=1000, multi_class="auto"))
]
)
baseline_pipe.fit(X_train, y_train)
y_pred_base = baseline_pipe.predict(X_test)
acc_base = accuracy_score(y_test, y_pred_base)
print(f"Baseline test accuracy (no PCA): {acc_base:.3f}")Baseline test accuracy (no PCA): 0.967
results = []
for k in [5, 10, 20, 40, 64]:
pipe = Pipeline(
steps=[
("scaler", StandardScaler()),
("pca", PCA(n_components=k)),
("logreg", LogisticRegression(max_iter=1000, multi_class="auto"))
]
)
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
acc = accuracy_score(y_test, y_pred)
results.append({"n_components": k, "test_accuracy": acc})
results_df = pd.DataFrame(results)
results_df| n_components | test_accuracy | |
|---|---|---|
| 0 | 5 | 0.803704 |
| 1 | 10 | 0.875926 |
| 2 | 20 | 0.942593 |
| 3 | 40 | 0.961111 |
| 4 | 64 | 0.966667 |
fig, ax = plt.subplots()
ax.plot(results_df["n_components"], results_df["test_accuracy"], marker="o")
ax.axhline(acc_base, linestyle="--", label="Baseline (no PCA)")
ax.set_xlabel("Number of PCA components (k)")
ax.set_ylabel("Test accuracy")
ax.set_title("Digits classification: accuracy vs PCA dimension")
ax.legend()
plt.tight_layout()
plt.show()Design choices:
Warning
When you apply PCA in a pipeline, always ask:
👍️ Rule of thumb
PCA and its descendants show up everywhere:
Image compression & face recognition
Classical eigenfaces use PCA on aligned face images to represent faces as linear combinations of principal components. Modern deep methods (e.g., FaceNet) learn nonlinear embeddings in a similar spirit (Turk and Pentland 1991; Schroff, Kalenichenko, and Philbin 2015).
Genomics & population genetics
PCA of genotype matrices reveals axes of ancestry and population structure, with a solid statistical treatment for large-scale genetics (Patterson, Price, and Reich 2006).
Recommendation systems
Matrix factorization models for user–item ratings are essentially low-rank (PCA/SVD-like) approximations of the rating matrix (Koren, Bell, and Volinsky 2009).
Connections & generalizations:
Kernel PCA
Nonlinear PCA in a feature space defined by a kernel, obtained by diagonalizing a kernel matrix rather than the covariance (Schölkopf, Smola, and Müller 1998).
Deep autoencoders & VAEs
Deep autoencoders and variational autoencoders learn nonlinear low-dimensional codes, extending PCA’s “bottleneck” idea to powerful generative models; recent work even blends kernel PCA with deep autoencoders for industrial fault detection (Hinton and Salakhutdinov 2006; Kingma and Welling 2014; Ren et al. 2024).
Modern visualization methods (t-SNE, UMAP)
t-SNE and UMAP focus on preserving local neighborhoods in low-dimensional embeddings and are standard tools for high-dimensional exploratory plots (e.g., single-cell RNA-seq, image features) (Maaten and Hinton 2008; McInnes, Healy, and Melville 2018).
Q1 (MCQ).
Which of the following is closest to what PCA does?
A. Finds a linear classifier that minimizes misclassification error.
B. Finds directions that maximize the variance of the projected data, subject to orthogonality.
C. Finds directions that maximize classification accuracy using labels.
D. Finds directions that minimize the number of non-zero coefficients.
Q2 (short).
Why is it important to center the data before performing PCA?
Write a 1–2 sentence answer.
Q3 (numeric).
You run PCA and get the following explained variance ratios:
If you keep only the first two components, what fraction of the total variance do you retain?
Q4 (conceptual).
You have two features:
height_cm in centimeters,income_dollars in dollars.You apply PCA without scaling.
What is a likely issue, and how could you fix it?
