Skip to content

Commit 0079adb

Browse files
committed
add Week 4 X-hour: dimensionality reduction slides + notebook
1 parent 5005bc5 commit 0079adb

9 files changed

Lines changed: 976 additions & 661 deletions

slides/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ Explore concepts hands-on with our interactive web demos! Each demo runs directl
153153
**Thursday X-hour (Lecture 13):** Dimensionality Reduction
154154
- PCA and UMAP for visualization
155155
- Reading: [McInnes & Healy (2018)](https://arxiv.org/pdf/1802.03426) - UMAP
156+
- 📓 [X-hour Notebook](https://contextlab.github.io/llm-course/slides/week4/xhour_dimred_demo.html)
156157
- 🎮 **Try it:** [Embeddings Visualization](https://contextlab.github.io/llm-course/demos/embeddings/)
157158
- 📊 [Slides PDF](https://contextlab.github.io/llm-course/slides/week4/lecture13.pdf) | 🌐 [Slides HTML](https://contextlab.github.io/llm-course/slides/week4/lecture13.html)
158159

219 KB
Loading
223 KB
Loading
222 KB
Loading
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate dimensionality reduction visualization figures for Lecture 13.
4+
5+
Creates three PNG figures showing 20 Newsgroups embeddings projected to 2D
6+
using PCA, t-SNE, and UMAP. Uses Avenir font to match slide theme.
7+
8+
Usage:
9+
python generate_dimred_figures.py
10+
11+
Output:
12+
figures/pca_visualization.png
13+
figures/tsne_visualization.png
14+
figures/umap_visualization.png
15+
"""
16+
17+
import os
18+
import numpy as np
19+
import matplotlib
20+
21+
matplotlib.use("Agg") # Headless backend
22+
import matplotlib.pyplot as plt
23+
from sklearn.datasets import fetch_20newsgroups
24+
from sklearn.feature_extraction.text import TfidfVectorizer
25+
from sklearn.decomposition import TruncatedSVD, PCA
26+
from sklearn.manifold import TSNE
27+
28+
# Set random seed for reproducibility
29+
np.random.seed(42)
30+
31+
# Font configuration - use Avenir to match slide theme
32+
FONT_FAMILY = ["Avenir", "Avenir Next", "Helvetica Neue", "DejaVu Sans", "sans-serif"]
33+
plt.rcParams["font.family"] = FONT_FAMILY
34+
plt.rcParams["font.size"] = 12
35+
plt.rcParams["axes.titlesize"] = 16
36+
plt.rcParams["axes.labelsize"] = 14
37+
plt.rcParams["legend.fontsize"] = 10
38+
39+
# Categories for 20 Newsgroups
40+
CATEGORIES = [
41+
"sci.space",
42+
"sci.med",
43+
"rec.sport.hockey",
44+
"rec.sport.baseball",
45+
"talk.politics.misc",
46+
"talk.religion.misc",
47+
"comp.graphics",
48+
"comp.os.ms-windows.misc",
49+
]
50+
51+
# Shortened category names for legend
52+
CATEGORY_SHORT_NAMES = {
53+
"sci.space": "Space",
54+
"sci.med": "Medicine",
55+
"rec.sport.hockey": "Hockey",
56+
"rec.sport.baseball": "Baseball",
57+
"talk.politics.misc": "Politics",
58+
"talk.religion.misc": "Religion",
59+
"comp.graphics": "Graphics",
60+
"comp.os.ms-windows.misc": "Windows",
61+
}
62+
63+
# Color palette (Dartmouth-inspired with good contrast)
64+
COLORS = [
65+
"#00693e", # Dartmouth Green
66+
"#267aba", # Blue
67+
"#ffa00f", # Orange
68+
"#9d162e", # Red
69+
"#8a6996", # Purple
70+
"#a5d75f", # Light Green
71+
"#003c73", # Navy
72+
"#d94415", # Burnt Orange
73+
]
74+
75+
76+
def load_data():
77+
"""Load 20 Newsgroups dataset and create embeddings."""
78+
print("Loading 20 Newsgroups dataset...")
79+
newsgroups = fetch_20newsgroups(
80+
subset="all",
81+
categories=CATEGORIES,
82+
shuffle=True,
83+
random_state=42,
84+
remove=("headers", "footers", "quotes"),
85+
)
86+
87+
documents = newsgroups.data
88+
labels = newsgroups.target
89+
label_names = newsgroups.target_names
90+
91+
print(f" Loaded {len(documents)} documents across {len(CATEGORIES)} categories")
92+
93+
# Create TF-IDF embeddings
94+
print("Creating TF-IDF embeddings...")
95+
tfidf = TfidfVectorizer(
96+
max_features=5000, min_df=5, max_df=0.5, stop_words="english"
97+
)
98+
tfidf_matrix = tfidf.fit_transform(documents)
99+
100+
# Reduce to 100D for faster processing
101+
print("Reducing to 100 dimensions with SVD...")
102+
svd = TruncatedSVD(n_components=100, random_state=42)
103+
embeddings = svd.fit_transform(tfidf_matrix)
104+
105+
print(f" Embeddings shape: {embeddings.shape}")
106+
print(f" Explained variance: {svd.explained_variance_ratio_.sum():.2%}")
107+
108+
return embeddings, labels, label_names
109+
110+
111+
def create_scatter_plot(coords_2d, labels, label_names, title, filename):
112+
"""Create and save a scatter plot."""
113+
fig, ax = plt.subplots(figsize=(10, 8))
114+
115+
# Plot each category
116+
for i, category in enumerate(label_names):
117+
mask = labels == i
118+
short_name = CATEGORY_SHORT_NAMES.get(category, category)
119+
ax.scatter(
120+
coords_2d[mask, 0],
121+
coords_2d[mask, 1],
122+
c=COLORS[i % len(COLORS)],
123+
label=short_name,
124+
alpha=0.6,
125+
s=20,
126+
edgecolors="none",
127+
)
128+
129+
ax.set_title(title, fontweight="bold", pad=15)
130+
ax.set_xlabel("Dimension 1")
131+
ax.set_ylabel("Dimension 2")
132+
133+
# Legend outside plot
134+
ax.legend(
135+
loc="center left",
136+
bbox_to_anchor=(1.02, 0.5),
137+
frameon=True,
138+
fancybox=True,
139+
shadow=False,
140+
)
141+
142+
# Clean up axes
143+
ax.spines["top"].set_visible(False)
144+
ax.spines["right"].set_visible(False)
145+
ax.tick_params(axis="both", which="both", length=0)
146+
ax.set_xticks([])
147+
ax.set_yticks([])
148+
149+
plt.tight_layout()
150+
plt.savefig(filename, dpi=150, bbox_inches="tight", facecolor="white")
151+
plt.close()
152+
153+
print(f" Saved: {filename}")
154+
155+
156+
def apply_pca(embeddings, labels, label_names, output_dir):
157+
"""Apply PCA and create visualization."""
158+
print("\nApplying PCA...")
159+
pca = PCA(n_components=2, random_state=42)
160+
coords_2d = pca.fit_transform(embeddings)
161+
162+
variance_explained = sum(pca.explained_variance_ratio_)
163+
print(f" Variance explained: {variance_explained:.2%}")
164+
165+
title = (
166+
f"PCA: 20 Newsgroups Embeddings\n(Variance explained: {variance_explained:.1%})"
167+
)
168+
filename = os.path.join(output_dir, "pca_visualization.png")
169+
create_scatter_plot(coords_2d, labels, label_names, title, filename)
170+
171+
172+
def apply_tsne(embeddings, labels, label_names, output_dir):
173+
"""Apply t-SNE and create visualization."""
174+
print("\nApplying t-SNE (this may take a minute)...")
175+
# Use a subset for faster computation (exact method is O(n^2))
176+
n_samples = min(1000, len(embeddings))
177+
indices = np.random.choice(len(embeddings), n_samples, replace=False)
178+
embeddings_subset = embeddings[indices]
179+
labels_subset = labels[indices]
180+
181+
tsne = TSNE(
182+
n_components=2,
183+
perplexity=30,
184+
n_iter=1000,
185+
random_state=42,
186+
init="pca",
187+
method="exact", # Use exact method to avoid threading issues
188+
)
189+
coords_2d = tsne.fit_transform(embeddings_subset)
190+
191+
title = f"t-SNE: 20 Newsgroups Embeddings\n(Perplexity=30, n={n_samples})"
192+
filename = os.path.join(output_dir, "tsne_visualization.png")
193+
create_scatter_plot(coords_2d, labels_subset, label_names, title, filename)
194+
195+
196+
def apply_umap(embeddings, labels, label_names, output_dir):
197+
"""Apply UMAP and create visualization."""
198+
print("\nApplying UMAP...")
199+
try:
200+
import umap
201+
202+
reducer = umap.UMAP(
203+
n_components=2,
204+
n_neighbors=15,
205+
min_dist=0.1,
206+
metric="cosine",
207+
random_state=42,
208+
)
209+
coords_2d = reducer.fit_transform(embeddings)
210+
211+
title = "UMAP: 20 Newsgroups Embeddings\n(n_neighbors=15, min_dist=0.1)"
212+
filename = os.path.join(output_dir, "umap_visualization.png")
213+
create_scatter_plot(coords_2d, labels, label_names, title, filename)
214+
except (ImportError, SystemError) as e:
215+
print(f" WARNING: UMAP failed ({e}). Creating fallback using PCA.")
216+
# Fallback to PCA for the UMAP slot
217+
from sklearn.decomposition import PCA
218+
219+
pca = PCA(n_components=2, random_state=42)
220+
coords_2d = pca.fit_transform(embeddings)
221+
222+
title = (
223+
"UMAP: 20 Newsgroups Embeddings\n(Fallback: PCA shown - UMAP unavailable)"
224+
)
225+
filename = os.path.join(output_dir, "umap_visualization.png")
226+
create_scatter_plot(coords_2d, labels, label_names, title, filename)
227+
228+
229+
def main():
230+
"""Main function to generate all figures."""
231+
# Get script directory
232+
script_dir = os.path.dirname(os.path.abspath(__file__))
233+
output_dir = os.path.join(script_dir, "figures")
234+
235+
# Create output directory
236+
os.makedirs(output_dir, exist_ok=True)
237+
print(f"Output directory: {output_dir}\n")
238+
239+
# Load data and create embeddings
240+
embeddings, labels, label_names = load_data()
241+
242+
# Apply each dimensionality reduction technique
243+
apply_pca(embeddings, labels, label_names, output_dir)
244+
apply_tsne(embeddings, labels, label_names, output_dir)
245+
apply_umap(embeddings, labels, label_names, output_dir)
246+
247+
print("\n" + "=" * 50)
248+
print("All figures generated successfully!")
249+
print("=" * 50)
250+
251+
# List output files
252+
print("\nGenerated files:")
253+
for f in sorted(os.listdir(output_dir)):
254+
if f.endswith(".png"):
255+
filepath = os.path.join(output_dir, f)
256+
size_kb = os.path.getsize(filepath) / 1024
257+
print(f" {f} ({size_kb:.1f} KB)")
258+
259+
260+
if __name__ == "__main__":
261+
main()

0 commit comments

Comments
 (0)