import numpy as npimport plotly.graph_objects as gofrom plotly.subplots import make_subplots# Set random seed for reproducibilitynp.random.seed(42)# Generate 2D MVN data with correlation 0.7n_points =500mean = [0, 0]cov = [[1, 0.7], [0.7, 1]]X_original = np.random.multivariate_normal(mean, cov, n_points)# Define transformation functionsdef get_transformation(name):if name =='Identity':return np.eye(2), np.array([0, 0])elif name =='Rotate 45°': c, s = np.cos(np.pi/4), np.sin(np.pi/4)return np.array([[c, -s], [s, c]]), np.array([0, 0])elif name =='Rotate 90°': c, s = np.cos(np.pi/2), np.sin(np.pi/2)return np.array([[c, -s], [s, c]]), np.array([0, 0])elif name =='Rotate 180°': c, s = np.cos(np.pi), np.sin(np.pi)return np.array([[c, -s], [s, c]]), np.array([0, 0])elif name =='Rotate 270°': c, s = np.cos(3*np.pi/2), np.sin(3*np.pi/2)return np.array([[c, -s], [s, c]]), np.array([0, 0])elif name =='Stretch X by 2':return np.array([[2, 0], [0, 1]]), np.array([0, 0])elif name =='Rotate 45° + Stretch X': c, s = np.cos(np.pi/4), np.sin(np.pi/4) R = np.array([[c, -s], [s, c]]) S = np.array([[2, 0], [0, 1]])return S @ R, np.array([0, 0])elif name =='Shift by (1,1)':return np.eye(2), np.array([1, 1])elif name =='Decorrelate (PCA)': eigenvalues, eigenvectors = np.linalg.eig(cov)return eigenvectors.T, np.array([0, 0])# Create figure with subplotsfig = make_subplots( rows=1, cols=2, subplot_titles=('Original Data', 'Transformed Data'), horizontal_spacing=0.15)# Transformation optionstransformations = ['Identity', 'Rotate 45°', 'Rotate 90°', 'Rotate 180°', 'Rotate 270°', 'Stretch X by 2', 'Rotate 45° + Stretch X', 'Shift by (1,1)', 'Decorrelate (PCA)']# Add traces for each transformation (initially invisible)for i, trans_name inenumerate(transformations): A, b = get_transformation(trans_name) X_transformed = (A @ X_original.T).T + b# Original data (always visible) fig.add_trace( go.Scatter( x=X_original[:, 0], y=X_original[:, 1], mode='markers', marker=dict(size=4, color='blue', opacity=0.6), name='Original', showlegend=False, visible=(i ==0) # Only first one visible ), row=1, col=1 )# Transformed data fig.add_trace( go.Scatter( x=X_transformed[:, 0], y=X_transformed[:, 1], mode='markers', marker=dict(size=4, color='red', opacity=0.6), name='Transformed', showlegend=False, visible=(i ==0) # Only first one visible ), row=1, col=2 )# Get initial transformation for annotationA_init, b_init = get_transformation('Identity')# Create dropdown menubuttons = []for i, trans_name inenumerate(transformations): A, b = get_transformation(trans_name)# Format matrix A and vector b as text - horizontal layout matrix_text = (f"A = [{A[0,0]:6.3f}{A[0,1]:6.3f}] "f"b = [{b[0]:6.3f}]<br>"f" [{A[1,0]:6.3f}{A[1,1]:6.3f}] "f" [{b[1]:6.3f}]")# Create visibility array: show traces for this transformation visible = [False] * (len(transformations) *2) visible[i *2] =True# Original data for this transformation visible[i *2+1] =True# Transformed data for this transformation buttons.append(dict( label=trans_name, method='update', args=[ {'visible': visible}, {'title.text': f'Affine Transformation: {trans_name}','annotations': [dict( text=matrix_text, x=0.5, y=1.05, # Moved above the plot xref='paper', yref='paper', showarrow=False, font=dict(size=13, family='monospace'), align='center', xanchor='center', yanchor='bottom' ) ] } ] ) )# Initial annotation text - horizontal layoutmatrix_text_init = (f"A = [{A_init[0,0]:6.3f}{A_init[0,1]:6.3f}] "f"b = [{b_init[0]:6.3f}]<br>"f" [{A_init[1,0]:6.3f}{A_init[1,1]:6.3f}] "f" [{b_init[1]:6.3f}]")# Add dropdown menufig.update_layout( updatemenus=[dict( buttons=buttons, direction='down', showactive=True, x=0.5, xanchor='center', y=0.98, # Moved down slightly to make room for annotation yanchor='top' ) ], title_text='Affine Transformation: Identity', height=550, showlegend=False, margin=dict(t=120), # Increased top margin for annotation annotations=[dict( text=matrix_text_init, x=0.5, y=1.05, # Above the plot xref='paper', yref='paper', showarrow=False, font=dict(size=13, family='monospace'), align='center', xanchor='center', yanchor='bottom' ) ])# Set axes to be equal and centeredaxis_range = [-4, 4]fig.update_xaxes(range=axis_range, constrain='domain', row=1, col=1)fig.update_yaxes(range=axis_range, scaleanchor="x", scaleratio=1, row=1, col=1)fig.update_xaxes(range=axis_range, constrain='domain', row=1, col=2)fig.update_yaxes(range=axis_range, scaleanchor="x2", scaleratio=1, row=1, col=2)# Add gridfig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='lightgray')fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='lightgray')fig.show()
MNIST Digits Dataset
Let’s start with the MNIST handwritted digits. Out goal is to explore how PCA can be used to reduce the dimension of image data, and how this affects classification accuracy.
Let’s start with two baseline models: multinomial logistic regression and XGBoost on the full 784-dimensional data.
Code
import torchimport torchvisionimport numpy as npimport xgboost as xgbfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import accuracy_score# 1. Load MNIST datasetprint("Loading MNIST dataset...")transform = torchvision.transforms.ToTensor()mnist_train = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)mnist_test = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)# Convert to numpy and flatten (28x28 -> 784)X_train_mnist = mnist_train.data.numpy().reshape(-1, 784).astype(np.float32) /255.0y_train_mnist = mnist_train.targets.numpy()X_test_mnist = mnist_test.data.numpy().reshape(-1, 784).astype(np.float32) /255.0y_test_mnist = mnist_test.targets.numpy()# 2. Center the dataprint("Centering MNIST data...")mean_mnist = np.mean(X_train_mnist, axis=0)X_train_mnist = X_train_mnist - mean_mnistX_test_mnist = X_test_mnist - mean_mnistprint(f"Training set shape: {X_train_mnist.shape}")print(f"Test set shape: {X_test_mnist.shape}\n")# 3. Multinomial Logistic Regressionprint("Training Multinomial Logistic Regression on MNIST...")log_reg_mnist = LogisticRegression( multi_class='multinomial', solver='lbfgs', max_iter=1000, random_state=42)log_reg_mnist.fit(X_train_mnist, y_train_mnist)train_acc_lr = log_reg_mnist.score(X_train_mnist, y_train_mnist)test_acc_lr = log_reg_mnist.score(X_test_mnist, y_test_mnist)print(f"Logistic Regression Training Accuracy: {train_acc_lr:.4f}")print(f"Logistic Regression Test Accuracy: {test_acc_lr:.4f}\n")# 4. XGBoostprint("Training XGBoost on MNIST (this may take a minute)...")xgb_clf_mnist = xgb.XGBClassifier( n_estimators=100, max_depth=6, learning_rate=0.1, random_state=42, eval_metric='mlogloss', tree_method='hist'# Faster traiing)xgb_clf_mnist.fit(X_train_mnist, y_train_mnist)train_acc_xgb = xgb_clf_mnist.score(X_train_mnist, y_train_mnist)test_acc_xgb = xgb_clf_mnist.score(X_test_mnist, y_test_mnist)print(f"XGBoost Training Accuracy: {train_acc_xgb:.4f}")print(f"XGBoost Test Accuracy: {test_acc_xgb:.4f}")
Loading MNIST dataset...
Centering MNIST data...
Training set shape: (60000, 784)
Test set shape: (10000, 784)
Training Multinomial Logistic Regression on MNIST...
Logistic Regression Training Accuracy: 0.9393
Logistic Regression Test Accuracy: 0.9256
Training XGBoost on MNIST (this may take a minute)...
XGBoost Training Accuracy: 0.9960
XGBoost Test Accuracy: 0.9702
Now, figure out the reconstruction error with PCA:
Code
from sklearn.decomposition import PCAimport matplotlib.pyplot as plt# Fit PCA on the full MNIST training data to get all componentsprint("Fitting PCA with all components on MNIST to compute reconstruction error...")pca_mnist_full = PCA(random_state=42)pca_mnist_full.fit(X_train_mnist)# Get all eigenvalues (explained variance)eigenvalues_mnist = pca_mnist_full.explained_variance_# Define range of k values to testk_values_mnist =list(range(1, 785))# Compute reconstruction error for each kreconstruction_errors_mnist = []for k in k_values_mnist:# Average of eigenvalues from k onwards (the ones we didn't keep)if k <len(eigenvalues_mnist): leftover_eigenvalues_mnist = eigenvalues_mnist[k:] reconstruction_error_mnist = np.mean(leftover_eigenvalues_mnist)else: reconstruction_error_mnist =0.0 reconstruction_errors_mnist.append(reconstruction_error_mnist)print(f"Computed reconstruction errors for k = 1 to {len(k_values_mnist)}")# Plot reconstruction error vs kplt.figure(figsize=(12, 6))plt.plot(k_values_mnist, reconstruction_errors_mnist, 'b-', linewidth=2)plt.xlabel('Number of PCA Components (k)', fontsize=12)plt.ylabel('Reconstruction Error (Avg. Leftover Eigenvalues)', fontsize=12)plt.title('MNIST: PCA Reconstruction Error vs Number of Components', fontsize=14, fontweight='bold')plt.grid(True, alpha=0.3)plt.xlim([1, 784])plt.tight_layout()plt.show()
Fitting PCA with all components on MNIST to compute reconstruction error...
Computed reconstruction errors for k = 1 to 784
Let’s see if we can improve our accuracy for MNL using PCA to reduce dimension. We’ll try dimensions from 20 to 500 in steps of 20, plus the full 784 dimensions. We’ll then assess the accuracy on the test set.
Code
print("Running PCA + Multinomial Logistic Regression for different dimensions on MNIST...")print("="*80)# Define the range of PCA components: 20 to 500 in steps of 20k_values_acc =list(range(20, 501, 20))# Add full dimension (784)k_values_acc.append(784)# Store accuraciestrain_accuracies_mnist_pca = []test_accuracies_mnist_pca = []for k in k_values_acc:if k ==784:# No PCA needed for full dimension, use original data X_train_reduced = X_train_mnist X_test_reduced = X_test_mnistelse:# Fit PCA pca = PCA(n_components=k, random_state=42) X_train_reduced = pca.fit_transform(X_train_mnist) X_test_reduced = pca.transform(X_test_mnist)# Train Logistic Regression# Increasing max_iter slightly to ensure convergence for all k lr = LogisticRegression( multi_class='multinomial', solver='lbfgs', max_iter=1000, random_state=42 ) lr.fit(X_train_reduced, y_train_mnist)# Calculate accuracy train_acc = lr.score(X_train_reduced, y_train_mnist) test_acc = lr.score(X_test_reduced, y_test_mnist) train_accuracies_mnist_pca.append(train_acc) test_accuracies_mnist_pca.append(test_acc)print(f"k = {k:3d} | Train Acc: {train_acc:.4f} | Test Acc: {test_acc:.4f}")print("="*80)# Plot accuracy vs kplt.figure(figsize=(12, 6))plt.plot(k_values_acc, train_accuracies_mnist_pca, 'b-o', label='Training Accuracy', linewidth=2, markersize=5)plt.plot(k_values_acc, test_accuracies_mnist_pca, 'r-o', label='Test Accuracy', linewidth=2, markersize=5)plt.xlabel('Number of PCA Components (k)', fontsize=12)plt.ylabel('Accuracy', fontsize=12)plt.title('MNIST: Logistic Regression Accuracy vs PCA Dimensions', fontsize=14, fontweight='bold')plt.legend(fontsize=11)plt.grid(True, alpha=0.3)plt.tight_layout()plt.show()# Find best test accuracybest_idx_mnist = np.argmax(test_accuracies_mnist_pca)best_k_mnist = k_values_acc[best_idx_mnist]best_test_acc_mnist = test_accuracies_mnist_pca[best_idx_mnist]print(f"Best test accuracy: {best_test_acc_mnist:.4f} at k = {best_k_mnist}")
Running PCA + Multinomial Logistic Regression for different dimensions on MNIST...
================================================================================
k = 20 | Train Acc: 0.8753 | Test Acc: 0.8810
k = 40 | Train Acc: 0.9030 | Test Acc: 0.9071
k = 60 | Train Acc: 0.9138 | Test Acc: 0.9165
k = 80 | Train Acc: 0.9187 | Test Acc: 0.9187
k = 100 | Train Acc: 0.9227 | Test Acc: 0.9213
k = 120 | Train Acc: 0.9239 | Test Acc: 0.9224
k = 140 | Train Acc: 0.9261 | Test Acc: 0.9242
k = 160 | Train Acc: 0.9270 | Test Acc: 0.9248
k = 180 | Train Acc: 0.9297 | Test Acc: 0.9254
k = 200 | Train Acc: 0.9304 | Test Acc: 0.9266
k = 220 | Train Acc: 0.9311 | Test Acc: 0.9251
k = 240 | Train Acc: 0.9320 | Test Acc: 0.9258
k = 260 | Train Acc: 0.9321 | Test Acc: 0.9257
k = 280 | Train Acc: 0.9330 | Test Acc: 0.9256
k = 300 | Train Acc: 0.9334 | Test Acc: 0.9252
k = 320 | Train Acc: 0.9339 | Test Acc: 0.9249
k = 340 | Train Acc: 0.9350 | Test Acc: 0.9241
k = 360 | Train Acc: 0.9355 | Test Acc: 0.9247
k = 380 | Train Acc: 0.9359 | Test Acc: 0.9249
k = 400 | Train Acc: 0.9363 | Test Acc: 0.9256
k = 420 | Train Acc: 0.9371 | Test Acc: 0.9255
k = 440 | Train Acc: 0.9378 | Test Acc: 0.9256
k = 460 | Train Acc: 0.9381 | Test Acc: 0.9260
k = 480 | Train Acc: 0.9383 | Test Acc: 0.9265
k = 500 | Train Acc: 0.9385 | Test Acc: 0.9255
k = 784 | Train Acc: 0.9393 | Test Acc: 0.9256
================================================================================
Best test accuracy: 0.9266 at k = 200
Back to CIFAR-10
Let’s start by loading in the CIFAR-10 data again and using XGBoost for the ten class classification problem. Judge the accuracy on a held out data set of size 10k.
Code
import numpy as npimport matplotlib.pyplot as pltimport torchimport torchvisionimport torchvision.transforms as transforms# Load CIFAR-10 train and test setsprint("Loading CIFAR-10 dataset...")transform = transforms.ToTensor()trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)# CIFAR-10 class namesclass_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']print(f"Training set size: {len(trainset)} images")print(f"Test set size: {len(testset)} images")print(f"Image shape: 32×32×3 = 3,072 dimensions")print(f"Number of classes: {len(class_names)}")# Find one example from each classfig, axes = plt.subplots(2, 5, figsize=(6, 2.5)) # Much smaller figureaxes = axes.flatten()# Get one example per classexamples_found = {i: Nonefor i inrange(10)}for idx inrange(len(trainset)): img, label = trainset[idx]if examples_found[label] isNone: examples_found[label] = imgifall(v isnotNonefor v in examples_found.values()):break# Displayfor class_idx, (ax, class_name) inenumerate(zip(axes, class_names)): img = examples_found[class_idx]# Convert from CxHxW to HxWxC for matplotlib img_np = img.permute(1, 2, 0).numpy() ax.imshow(img_np) ax.set_title(class_name, fontsize=8, fontweight='bold') # Smaller font ax.axis('off')plt.suptitle('CIFAR-10: One Example from Each Class (p = 3,072 dimensions)', fontsize=10, fontweight='bold', y=1.00) # Smaller titleplt.tight_layout()plt.show()print(f"\nDimensionality: 3,072 features (32×32×3 RGB pixels)")print(f"Training samples: {len(trainset)} images")print(f"Test samples: {len(testset)} images")
Loading CIFAR-10 dataset...
Training set size: 50000 images
Test set size: 10000 images
Image shape: 32×32×3 = 3,072 dimensions
Number of classes: 10
Dimensionality: 3,072 features (32×32×3 RGB pixels)
Training samples: 50000 images
Test samples: 10000 images
Fit the XGBoost model with reasonable hyperparameter values.
Code
import xgboost as xgbfrom sklearn.metrics import confusion_matriximport seaborn as sns# Flatten CIFAR-10 images for logistic regressionprint("Preparing CIFAR-10 data for Logistic Regression...")# Convert datasets to numpy arrays and flattenX_train_cifar = []y_train_cifar = []for img, label in trainset: X_train_cifar.append(img.numpy().flatten()) y_train_cifar.append(label)X_test_cifar = []y_test_cifar = []for img, label in testset: X_test_cifar.append(img.numpy().flatten()) y_test_cifar.append(label)X_train_cifar = np.array(X_train_cifar)y_train_cifar = np.array(y_train_cifar)X_test_cifar = np.array(X_test_cifar)y_test_cifar = np.array(y_test_cifar)# Center the dataprint("Centering the data...")mean_cifar = np.mean(X_train_cifar, axis=0)X_train_cifar = X_train_cifar - mean_cifarX_test_cifar = X_test_cifar - mean_cifar
Preparing CIFAR-10 data for Logistic Regression...
Centering the data...
Code
# Fit XGBoost Classifier on CIFAR-10print("Fitting XGBoost Classifier on CIFAR-10...")print("This may take a few minutes...\n")xgb_clf_cifar = xgb.XGBClassifier( n_estimators=100, max_depth=6, learning_rate=0.1, random_state=42, eval_metric='mlogloss', tree_method='hist'# Faster for large datasets)xgb_clf_cifar.fit(X_train_cifar, y_train_cifar)# Compute accuraciestrain_acc_cifar_xgb = xgb_clf_cifar.score(X_train_cifar, y_train_cifar)test_acc_cifar_xgb = xgb_clf_cifar.score(X_test_cifar, y_test_cifar)print(f"XGBoost on CIFAR-10")print(f"Training Accuracy: {train_acc_cifar_xgb:.4f}")print(f"Test Accuracy: {test_acc_cifar_xgb:.4f}\n")# Generate predictions for confusion matrixy_pred_cifar_xgb = xgb_clf_cifar.predict(X_test_cifar)# Compute confusion matrixcm_xgb = confusion_matrix(y_test_cifar, y_pred_cifar_xgb)# Plot confusion matrixplt.figure(figsize=(10, 8))sns.heatmap(cm_xgb, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names)plt.title(f'CIFAR-10 XGBoost Confusion Matrix\nTest Accuracy: {test_acc_cifar_xgb:.4f}')plt.ylabel('True Label')plt.xlabel('Predicted Label')plt.tight_layout()plt.show()# Print per-class accuracyprint("\nPer-class accuracy:")for i, class_name inenumerate(class_names): class_accuracy = cm_xgb[i, i] / cm_xgb[i, :].sum()print(f"{class_name:12s}: {class_accuracy:.4f}")
Fitting XGBoost Classifier on CIFAR-10...
This may take a few minutes...
XGBoost on CIFAR-10
Training Accuracy: 0.8401
Test Accuracy: 0.5252
Per-class accuracy:
airplane : 0.5880
automobile : 0.6040
bird : 0.3860
cat : 0.3490
deer : 0.4260
dog : 0.4430
frog : 0.6370
horse : 0.5350
ship : 0.6690
truck : 0.6150
To start, let’s check the reconstruction error of PCA for CIFAR-10 as we vary the number of components used.
Code
from sklearn.decomposition import PCA# Fit PCA on the full training data to get all componentsprint("Fitting PCA with all components to compute reconstruction error...")pca_full = PCA(random_state=42)pca_full.fit(X_train_cifar)# Get all eigenvalues (explained variance)eigenvalues = pca_full.explained_variance_# Define range of k values to testk_values =list(range(1, 3073))# Compute reconstruction error for each k# Reconstruction error = average of leftover eigenvalues (those not used)reconstruction_errors = []for k in k_values:# Average of eigenvalues from k onwards (the ones we didn't keep)if k <len(eigenvalues): leftover_eigenvalues = eigenvalues[k:] reconstruction_error = np.mean(leftover_eigenvalues)else: reconstruction_error =0.0 reconstruction_errors.append(reconstruction_error)print(f"Computed reconstruction errors for k = 1 to {len(k_values)}")# Plot reconstruction error vs kplt.figure(figsize=(12, 6))plt.plot(k_values, reconstruction_errors, 'b-', linewidth=2)plt.xlabel('Number of PCA Components (k)', fontsize=12)plt.ylabel('Reconstruction Error (Avg. Leftover Eigenvalues)', fontsize=12)plt.title('CIFAR-10: PCA Reconstruction Error vs Number of Components', fontsize=14, fontweight='bold')plt.grid(True, alpha=0.3)plt.xlim([1, 3072])plt.tight_layout()plt.show()
Fitting PCA with all components to compute reconstruction error...
Computed reconstruction errors for k = 1 to 3072
Looking at this plot, we can see that the sweet spot seems to be somewhere between 300 and 600.
Now, let’s do PCA on the data to reduce the dimension from 3072 to something much smaller. We’re going to try (300,320,…,600). For each of these values, we’ll project the data down to that dimension using PCA, fit a multinomial logistic regression on the training data, and then judge the accuracy on the same validation set.
Code
from sklearn.decomposition import PCAfrom sklearn.linear_model import LogisticRegressionfrom sklearn.model_selection import train_test_split# Fit multinomial logistic regression on original features (full dimensionality)print("Fitting Multinomial Logistic Regression on full features (3072 dimensions)...")log_reg_full = LogisticRegression( multi_class='multinomial', solver='lbfgs', max_iter=1000, random_state=42)# Fit on the centered original datalog_reg_full.fit(X_train_cifar, y_train_cifar)# Compute accuraciestrain_acc_full = log_reg_full.score(X_train_cifar, y_train_cifar)test_acc_full = log_reg_full.score(X_test_cifar, y_test_cifar)print(f"\nLogistic Regression on Full CIFAR-10 (3072 features)")print(f"Training Accuracy: {train_acc_full:.4f}")print(f"Test Accuracy: {test_acc_full:.4f}")
Fitting Multinomial Logistic Regression on full features (3072 dimensions)...
Logistic Regression on Full CIFAR-10 (3072 features)
Training Accuracy: 0.4991
Test Accuracy: 0.3856
/home/kmcalist/.local/lib/python3.10/site-packages/sklearn/linear_model/_logistic.py:460: ConvergenceWarning:
lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.
Increase the number of iterations (max_iter) or scale the data as shown in:
https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
Code
# Use the full training set (already centered) and test set for PCA# No need to split into train/validation since we're using the existing test set# Define the range of PCA components to tryn_components_list =list(range(300, 601, 20))# Store accuraciestrain_accuracies = []test_accuracies = []print("Running PCA + Multinomial Logistic Regression for different dimensions...")print("="*80)for n_comp in n_components_list:# Apply PCA pca = PCA(n_components=n_comp, random_state=42) X_train_pca = pca.fit_transform(X_train_cifar) X_test_pca = pca.transform(X_test_cifar)# Fit multinomial logistic regression log_reg = LogisticRegression( multi_class='multinomial', solver='lbfgs', max_iter=1000, random_state=42 ) log_reg.fit(X_train_pca, y_train_cifar)# Compute accuracies train_acc = log_reg.score(X_train_pca, y_train_cifar) test_acc = log_reg.score(X_test_pca, y_test_cifar) train_accuracies.append(train_acc) test_accuracies.append(test_acc)if n_comp %100==0or n_comp ==20:print(f"n_components = {n_comp:4d} | Train: {train_acc:.4f} | Test: {test_acc:.4f}")print("="*80)print("\nCompleted!\n")
It’s a nonlinear manifold embedded in a higher dimensional space! PCA can only get us so far - dimensionality reduction can only get us so far if we aren’t capturing the true underlying structure of the data.
Instead, let’s do it end to end with a neural network.
Code
import torchfrom torch.utils.data import DataLoader, TensorDatasetimport pytorch_lightning as plfrom pytorch_lightning.callbacks import ModelCheckpointimport torch.nn as nnimport torch.nn.functional as F# Define the neural network modelclass CIFAR10Classifier(pl.LightningModule):def__init__(self, input_dim=3072, hidden_dim=1024, num_classes=10, learning_rate=0.01, dropout=0.1):super().__init__()self.save_hyperparameters()# 4 hidden layers with batch normalizationself.fc1 = nn.Linear(input_dim, 1024)self.bn1 = nn.BatchNorm1d(1024)self.fc2 = nn.Linear(1024, 512)self.bn2 = nn.BatchNorm1d(512)self.fc3 = nn.Linear(512, 256)self.bn3 = nn.BatchNorm1d(256)#self.fc4 = nn.Linear(hidden_dim, hidden_dim)#self.bn4 = nn.BatchNorm1d(hidden_dim)self.fc5 = nn.Linear(256, num_classes)self.dropout = nn.Dropout(dropout)self.learning_rate = learning_ratedef forward(self, x): x =self.fc1(x) x =self.bn1(x) x = F.relu(x) x =self.dropout(x) x =self.fc2(x) x =self.bn2(x) x = F.relu(x) x =self.dropout(x) x =self.fc3(x) x =self.bn3(x) x = F.relu(x) x =self.dropout(x)#x = self.fc4(x)#x = self.bn4(x)#x = F.relu(x) x =self.dropout(x) x =self.fc5(x) # Logits for cross-entropyreturn xdef training_step(self, batch, batch_idx): x, y = batch logits =self(x) loss = F.cross_entropy(logits, y) acc = (logits.argmax(dim=1) == y).float().mean()self.log('train_loss', loss, prog_bar=True)self.log('train_acc', acc, prog_bar=True)return lossdef validation_step(self, batch, batch_idx): x, y = batch logits =self(x) loss = F.cross_entropy(logits, y) acc = (logits.argmax(dim=1) == y).float().mean()self.log('val_loss', loss, prog_bar=True)self.log('val_acc', acc, prog_bar=True)return lossdef test_step(self, batch, batch_idx): x, y = batch logits =self(x) loss = F.cross_entropy(logits, y) acc = (logits.argmax(dim=1) == y).float().mean()self.log('test_loss', loss)self.log('test_acc', acc)return lossdef configure_optimizers(self): optimizer = torch.optim.AdamW(self.parameters(), lr=self.learning_rate, weight_decay=0.01# Add weight decay (L2 regularization) )return optimizer# Prepare dataprint("Preparing data for PyTorch Lightning...")# Convert to PyTorch tensorsX_train_tensor = torch.FloatTensor(X_train_cifar)y_train_tensor = torch.LongTensor(y_train_cifar)X_test_tensor = torch.FloatTensor(X_test_cifar)y_test_tensor = torch.LongTensor(y_test_cifar)# Create datasetstrain_dataset = TensorDataset(X_train_tensor, y_train_tensor)test_dataset = TensorDataset(X_test_tensor, y_test_tensor)# Create data loaderstrain_loader = DataLoader(train_dataset, batch_size=256, shuffle=True, num_workers=4)test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False, num_workers=4)# Initialize modelmodel = CIFAR10Classifier(input_dim=3072, hidden_dim=128, num_classes=10, learning_rate=0.001)# Set up trainertrainer = pl.Trainer( max_epochs=50, accelerator='gpu'if torch.cuda.is_available() else'cpu', devices=1, callbacks=[ ModelCheckpoint( monitor='val_acc', mode='max', filename='cifar10-{epoch:02d}-{val_acc:.4f}' ) ], log_every_n_steps=50)# Train the modelprint("\nTraining neural network on CIFAR-10...")print(f"Using device: {'GPU'if torch.cuda.is_available() else'CPU'}")trainer.fit(model, train_loader, test_loader)# Test the modelprint("\nTesting the model...")test_results = trainer.test(model, test_loader)print(f"\nFinal Test Accuracy: {test_results[0]['test_acc']:.4f}")print(f"Final Test Loss: {test_results[0]['test_loss']:.4f}")
GPU available: True (cuda), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
You are using a CUDA device ('NVIDIA GeForce RTX 5090') that has Tensor Cores. To properly utilize them, you should set `torch.set_float32_matmul_precision('medium' | 'high')` which will trade-off precision for performance. For more details, read https://pytorch.org/docs/stable/generated/torch.set_float32_matmul_precision.html#torch.set_float32_matmul_precision
2026-01-29 12:57:34.818428: I tensorflow/core/util/port.cc:113] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
2026-01-29 12:57:34.832501: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:9261] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2026-01-29 12:57:34.832520: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:607] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2026-01-29 12:57:34.833045: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1515] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2026-01-29 12:57:34.835982: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F AVX512_VNNI AVX512_BF16 AVX_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2026-01-29 12:57:35.321123: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]
| Name | Type | Params | Mode
------------------------------------------------
0 | fc1 | Linear | 3.1 M | train
1 | bn1 | BatchNorm1d | 2.0 K | train
2 | fc2 | Linear | 524 K | train
3 | bn2 | BatchNorm1d | 1.0 K | train
4 | fc3 | Linear | 131 K | train
5 | bn3 | BatchNorm1d | 512 | train
6 | fc5 | Linear | 2.6 K | train
7 | dropout | Dropout | 0 | train
------------------------------------------------
3.8 M Trainable params
0 Non-trainable params
3.8 M Total params
15.236 Total estimated model params size (MB)
/home/kmcalist/.local/lib/python3.10/site-packages/pytorch_lightning/utilities/_pytree.py:21: `isinstance(treespec, LeafSpec)` is deprecated, use `isinstance(treespec, TreeSpec) and treespec.is_leaf()` instead.
`Trainer.fit` stopped: `max_epochs=50` reached.
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]
Preparing data for PyTorch Lightning...
Training neural network on CIFAR-10...
Using device: GPU
Testing the model...
Final Test Accuracy: 0.5739
Final Test Loss: 2.3761
import osfrom pytorch_lightning.callbacks import ModelCheckpointimport glob# Load the best model checkpoint# Find the best checkpoint filecheckpoint_files = glob.glob('lightning_logs/version_0/checkpoints/*.ckpt')if checkpoint_files:# Get the most recent checkpoint (or you can sort by validation accuracy) best_checkpoint =max(checkpoint_files, key=lambda x: os.path.getctime(x))print(f"Loading checkpoint: {best_checkpoint}")# Load the model from checkpoint loaded_model = CIFAR10Classifier.load_from_checkpoint(best_checkpoint)# Evaluate on test set trainer_eval = pl.Trainer( accelerator='gpu'if torch.cuda.is_available() else'cpu', devices=1 ) results = trainer_eval.test(loaded_model, test_loader)print(f"\nValidation (Test) Accuracy: {results[0]['test_acc']:.4f}")print(f"Validation (Test) Loss: {results[0]['test_loss']:.4f}")else:print("No checkpoint files found. Using the last trained model.")print(f"\nValidation (Test) Accuracy: {test_results[0]['test_acc']:.4f}")print(f"Validation (Test) Loss: {test_results[0]['test_loss']:.4f}")
---title: "Lecture 6: PCA and Dimensionality Reduction"format: html: code-fold: true code-summary: "Show code" code-tools: true toc: truejupyter: python3---```{python}import numpy as npimport plotly.graph_objects as gofrom plotly.subplots import make_subplots# Set random seed for reproducibilitynp.random.seed(42)# Generate 2D MVN data with correlation 0.7n_points =500mean = [0, 0]cov = [[1, 0.7], [0.7, 1]]X_original = np.random.multivariate_normal(mean, cov, n_points)# Define transformation functionsdef get_transformation(name):if name =='Identity':return np.eye(2), np.array([0, 0])elif name =='Rotate 45°': c, s = np.cos(np.pi/4), np.sin(np.pi/4)return np.array([[c, -s], [s, c]]), np.array([0, 0])elif name =='Rotate 90°': c, s = np.cos(np.pi/2), np.sin(np.pi/2)return np.array([[c, -s], [s, c]]), np.array([0, 0])elif name =='Rotate 180°': c, s = np.cos(np.pi), np.sin(np.pi)return np.array([[c, -s], [s, c]]), np.array([0, 0])elif name =='Rotate 270°': c, s = np.cos(3*np.pi/2), np.sin(3*np.pi/2)return np.array([[c, -s], [s, c]]), np.array([0, 0])elif name =='Stretch X by 2':return np.array([[2, 0], [0, 1]]), np.array([0, 0])elif name =='Rotate 45° + Stretch X': c, s = np.cos(np.pi/4), np.sin(np.pi/4) R = np.array([[c, -s], [s, c]]) S = np.array([[2, 0], [0, 1]])return S @ R, np.array([0, 0])elif name =='Shift by (1,1)':return np.eye(2), np.array([1, 1])elif name =='Decorrelate (PCA)': eigenvalues, eigenvectors = np.linalg.eig(cov)return eigenvectors.T, np.array([0, 0])# Create figure with subplotsfig = make_subplots( rows=1, cols=2, subplot_titles=('Original Data', 'Transformed Data'), horizontal_spacing=0.15)# Transformation optionstransformations = ['Identity', 'Rotate 45°', 'Rotate 90°', 'Rotate 180°', 'Rotate 270°', 'Stretch X by 2', 'Rotate 45° + Stretch X', 'Shift by (1,1)', 'Decorrelate (PCA)']# Add traces for each transformation (initially invisible)for i, trans_name inenumerate(transformations): A, b = get_transformation(trans_name) X_transformed = (A @ X_original.T).T + b# Original data (always visible) fig.add_trace( go.Scatter( x=X_original[:, 0], y=X_original[:, 1], mode='markers', marker=dict(size=4, color='blue', opacity=0.6), name='Original', showlegend=False, visible=(i ==0) # Only first one visible ), row=1, col=1 )# Transformed data fig.add_trace( go.Scatter( x=X_transformed[:, 0], y=X_transformed[:, 1], mode='markers', marker=dict(size=4, color='red', opacity=0.6), name='Transformed', showlegend=False, visible=(i ==0) # Only first one visible ), row=1, col=2 )# Get initial transformation for annotationA_init, b_init = get_transformation('Identity')# Create dropdown menubuttons = []for i, trans_name inenumerate(transformations): A, b = get_transformation(trans_name)# Format matrix A and vector b as text - horizontal layout matrix_text = (f"A = [{A[0,0]:6.3f}{A[0,1]:6.3f}] "f"b = [{b[0]:6.3f}]<br>"f" [{A[1,0]:6.3f}{A[1,1]:6.3f}] "f" [{b[1]:6.3f}]")# Create visibility array: show traces for this transformation visible = [False] * (len(transformations) *2) visible[i *2] =True# Original data for this transformation visible[i *2+1] =True# Transformed data for this transformation buttons.append(dict( label=trans_name, method='update', args=[ {'visible': visible}, {'title.text': f'Affine Transformation: {trans_name}','annotations': [dict( text=matrix_text, x=0.5, y=1.05, # Moved above the plot xref='paper', yref='paper', showarrow=False, font=dict(size=13, family='monospace'), align='center', xanchor='center', yanchor='bottom' ) ] } ] ) )# Initial annotation text - horizontal layoutmatrix_text_init = (f"A = [{A_init[0,0]:6.3f}{A_init[0,1]:6.3f}] "f"b = [{b_init[0]:6.3f}]<br>"f" [{A_init[1,0]:6.3f}{A_init[1,1]:6.3f}] "f" [{b_init[1]:6.3f}]")# Add dropdown menufig.update_layout( updatemenus=[dict( buttons=buttons, direction='down', showactive=True, x=0.5, xanchor='center', y=0.98, # Moved down slightly to make room for annotation yanchor='top' ) ], title_text='Affine Transformation: Identity', height=550, showlegend=False, margin=dict(t=120), # Increased top margin for annotation annotations=[dict( text=matrix_text_init, x=0.5, y=1.05, # Above the plot xref='paper', yref='paper', showarrow=False, font=dict(size=13, family='monospace'), align='center', xanchor='center', yanchor='bottom' ) ])# Set axes to be equal and centeredaxis_range = [-4, 4]fig.update_xaxes(range=axis_range, constrain='domain', row=1, col=1)fig.update_yaxes(range=axis_range, scaleanchor="x", scaleratio=1, row=1, col=1)fig.update_xaxes(range=axis_range, constrain='domain', row=1, col=2)fig.update_yaxes(range=axis_range, scaleanchor="x2", scaleratio=1, row=1, col=2)# Add gridfig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='lightgray')fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='lightgray')fig.show()```## MNIST Digits DatasetLet's start with the MNIST handwritted digits. Out goal is to explore how PCA can be used to reduce the dimension of image data, and how this affects classification accuracy.Let's start with two baseline models: multinomial logistic regression and XGBoost on the full 784-dimensional data.```{python}import torchimport torchvisionimport numpy as npimport xgboost as xgbfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import accuracy_score# 1. Load MNIST datasetprint("Loading MNIST dataset...")transform = torchvision.transforms.ToTensor()mnist_train = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)mnist_test = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)# Convert to numpy and flatten (28x28 -> 784)X_train_mnist = mnist_train.data.numpy().reshape(-1, 784).astype(np.float32) /255.0y_train_mnist = mnist_train.targets.numpy()X_test_mnist = mnist_test.data.numpy().reshape(-1, 784).astype(np.float32) /255.0y_test_mnist = mnist_test.targets.numpy()# 2. Center the dataprint("Centering MNIST data...")mean_mnist = np.mean(X_train_mnist, axis=0)X_train_mnist = X_train_mnist - mean_mnistX_test_mnist = X_test_mnist - mean_mnistprint(f"Training set shape: {X_train_mnist.shape}")print(f"Test set shape: {X_test_mnist.shape}\n")# 3. Multinomial Logistic Regressionprint("Training Multinomial Logistic Regression on MNIST...")log_reg_mnist = LogisticRegression( multi_class='multinomial', solver='lbfgs', max_iter=1000, random_state=42)log_reg_mnist.fit(X_train_mnist, y_train_mnist)train_acc_lr = log_reg_mnist.score(X_train_mnist, y_train_mnist)test_acc_lr = log_reg_mnist.score(X_test_mnist, y_test_mnist)print(f"Logistic Regression Training Accuracy: {train_acc_lr:.4f}")print(f"Logistic Regression Test Accuracy: {test_acc_lr:.4f}\n")# 4. XGBoostprint("Training XGBoost on MNIST (this may take a minute)...")xgb_clf_mnist = xgb.XGBClassifier( n_estimators=100, max_depth=6, learning_rate=0.1, random_state=42, eval_metric='mlogloss', tree_method='hist'# Faster traiing)xgb_clf_mnist.fit(X_train_mnist, y_train_mnist)train_acc_xgb = xgb_clf_mnist.score(X_train_mnist, y_train_mnist)test_acc_xgb = xgb_clf_mnist.score(X_test_mnist, y_test_mnist)print(f"XGBoost Training Accuracy: {train_acc_xgb:.4f}")print(f"XGBoost Test Accuracy: {test_acc_xgb:.4f}")```Now, figure out the reconstruction error with PCA:```{python}from sklearn.decomposition import PCAimport matplotlib.pyplot as plt# Fit PCA on the full MNIST training data to get all componentsprint("Fitting PCA with all components on MNIST to compute reconstruction error...")pca_mnist_full = PCA(random_state=42)pca_mnist_full.fit(X_train_mnist)# Get all eigenvalues (explained variance)eigenvalues_mnist = pca_mnist_full.explained_variance_# Define range of k values to testk_values_mnist =list(range(1, 785))# Compute reconstruction error for each kreconstruction_errors_mnist = []for k in k_values_mnist:# Average of eigenvalues from k onwards (the ones we didn't keep)if k <len(eigenvalues_mnist): leftover_eigenvalues_mnist = eigenvalues_mnist[k:] reconstruction_error_mnist = np.mean(leftover_eigenvalues_mnist)else: reconstruction_error_mnist =0.0 reconstruction_errors_mnist.append(reconstruction_error_mnist)print(f"Computed reconstruction errors for k = 1 to {len(k_values_mnist)}")# Plot reconstruction error vs kplt.figure(figsize=(12, 6))plt.plot(k_values_mnist, reconstruction_errors_mnist, 'b-', linewidth=2)plt.xlabel('Number of PCA Components (k)', fontsize=12)plt.ylabel('Reconstruction Error (Avg. Leftover Eigenvalues)', fontsize=12)plt.title('MNIST: PCA Reconstruction Error vs Number of Components', fontsize=14, fontweight='bold')plt.grid(True, alpha=0.3)plt.xlim([1, 784])plt.tight_layout()plt.show()```Let's see if we can improve our accuracy for MNL using PCA to reduce dimension. We'll try dimensions from 20 to 500 in steps of 20, plus the full 784 dimensions. We'll then assess the accuracy on the test set.```{python}print("Running PCA + Multinomial Logistic Regression for different dimensions on MNIST...")print("="*80)# Define the range of PCA components: 20 to 500 in steps of 20k_values_acc =list(range(20, 501, 20))# Add full dimension (784)k_values_acc.append(784)# Store accuraciestrain_accuracies_mnist_pca = []test_accuracies_mnist_pca = []for k in k_values_acc:if k ==784:# No PCA needed for full dimension, use original data X_train_reduced = X_train_mnist X_test_reduced = X_test_mnistelse:# Fit PCA pca = PCA(n_components=k, random_state=42) X_train_reduced = pca.fit_transform(X_train_mnist) X_test_reduced = pca.transform(X_test_mnist)# Train Logistic Regression# Increasing max_iter slightly to ensure convergence for all k lr = LogisticRegression( multi_class='multinomial', solver='lbfgs', max_iter=1000, random_state=42 ) lr.fit(X_train_reduced, y_train_mnist)# Calculate accuracy train_acc = lr.score(X_train_reduced, y_train_mnist) test_acc = lr.score(X_test_reduced, y_test_mnist) train_accuracies_mnist_pca.append(train_acc) test_accuracies_mnist_pca.append(test_acc)print(f"k = {k:3d} | Train Acc: {train_acc:.4f} | Test Acc: {test_acc:.4f}")print("="*80)# Plot accuracy vs kplt.figure(figsize=(12, 6))plt.plot(k_values_acc, train_accuracies_mnist_pca, 'b-o', label='Training Accuracy', linewidth=2, markersize=5)plt.plot(k_values_acc, test_accuracies_mnist_pca, 'r-o', label='Test Accuracy', linewidth=2, markersize=5)plt.xlabel('Number of PCA Components (k)', fontsize=12)plt.ylabel('Accuracy', fontsize=12)plt.title('MNIST: Logistic Regression Accuracy vs PCA Dimensions', fontsize=14, fontweight='bold')plt.legend(fontsize=11)plt.grid(True, alpha=0.3)plt.tight_layout()plt.show()# Find best test accuracybest_idx_mnist = np.argmax(test_accuracies_mnist_pca)best_k_mnist = k_values_acc[best_idx_mnist]best_test_acc_mnist = test_accuracies_mnist_pca[best_idx_mnist]print(f"Best test accuracy: {best_test_acc_mnist:.4f} at k = {best_k_mnist}")```## Back to CIFAR-10Let's start by loading in the CIFAR-10 data again and using XGBoost for the ten class classification problem. Judge the accuracy on a held out data set of size 10k.```{python}import numpy as npimport matplotlib.pyplot as pltimport torchimport torchvisionimport torchvision.transforms as transforms# Load CIFAR-10 train and test setsprint("Loading CIFAR-10 dataset...")transform = transforms.ToTensor()trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)# CIFAR-10 class namesclass_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']print(f"Training set size: {len(trainset)} images")print(f"Test set size: {len(testset)} images")print(f"Image shape: 32×32×3 = 3,072 dimensions")print(f"Number of classes: {len(class_names)}")# Find one example from each classfig, axes = plt.subplots(2, 5, figsize=(6, 2.5)) # Much smaller figureaxes = axes.flatten()# Get one example per classexamples_found = {i: Nonefor i inrange(10)}for idx inrange(len(trainset)): img, label = trainset[idx]if examples_found[label] isNone: examples_found[label] = imgifall(v isnotNonefor v in examples_found.values()):break# Displayfor class_idx, (ax, class_name) inenumerate(zip(axes, class_names)): img = examples_found[class_idx]# Convert from CxHxW to HxWxC for matplotlib img_np = img.permute(1, 2, 0).numpy() ax.imshow(img_np) ax.set_title(class_name, fontsize=8, fontweight='bold') # Smaller font ax.axis('off')plt.suptitle('CIFAR-10: One Example from Each Class (p = 3,072 dimensions)', fontsize=10, fontweight='bold', y=1.00) # Smaller titleplt.tight_layout()plt.show()print(f"\nDimensionality: 3,072 features (32×32×3 RGB pixels)")print(f"Training samples: {len(trainset)} images")print(f"Test samples: {len(testset)} images")```Fit the XGBoost model with reasonable hyperparameter values.```{python}import xgboost as xgbfrom sklearn.metrics import confusion_matriximport seaborn as sns# Flatten CIFAR-10 images for logistic regressionprint("Preparing CIFAR-10 data for Logistic Regression...")# Convert datasets to numpy arrays and flattenX_train_cifar = []y_train_cifar = []for img, label in trainset: X_train_cifar.append(img.numpy().flatten()) y_train_cifar.append(label)X_test_cifar = []y_test_cifar = []for img, label in testset: X_test_cifar.append(img.numpy().flatten()) y_test_cifar.append(label)X_train_cifar = np.array(X_train_cifar)y_train_cifar = np.array(y_train_cifar)X_test_cifar = np.array(X_test_cifar)y_test_cifar = np.array(y_test_cifar)# Center the dataprint("Centering the data...")mean_cifar = np.mean(X_train_cifar, axis=0)X_train_cifar = X_train_cifar - mean_cifarX_test_cifar = X_test_cifar - mean_cifar``````{python}# Fit XGBoost Classifier on CIFAR-10print("Fitting XGBoost Classifier on CIFAR-10...")print("This may take a few minutes...\n")xgb_clf_cifar = xgb.XGBClassifier( n_estimators=100, max_depth=6, learning_rate=0.1, random_state=42, eval_metric='mlogloss', tree_method='hist'# Faster for large datasets)xgb_clf_cifar.fit(X_train_cifar, y_train_cifar)# Compute accuraciestrain_acc_cifar_xgb = xgb_clf_cifar.score(X_train_cifar, y_train_cifar)test_acc_cifar_xgb = xgb_clf_cifar.score(X_test_cifar, y_test_cifar)print(f"XGBoost on CIFAR-10")print(f"Training Accuracy: {train_acc_cifar_xgb:.4f}")print(f"Test Accuracy: {test_acc_cifar_xgb:.4f}\n")# Generate predictions for confusion matrixy_pred_cifar_xgb = xgb_clf_cifar.predict(X_test_cifar)# Compute confusion matrixcm_xgb = confusion_matrix(y_test_cifar, y_pred_cifar_xgb)# Plot confusion matrixplt.figure(figsize=(10, 8))sns.heatmap(cm_xgb, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names)plt.title(f'CIFAR-10 XGBoost Confusion Matrix\nTest Accuracy: {test_acc_cifar_xgb:.4f}')plt.ylabel('True Label')plt.xlabel('Predicted Label')plt.tight_layout()plt.show()# Print per-class accuracyprint("\nPer-class accuracy:")for i, class_name inenumerate(class_names): class_accuracy = cm_xgb[i, i] / cm_xgb[i, :].sum()print(f"{class_name:12s}: {class_accuracy:.4f}")```To start, let's check the reconstruction error of PCA for CIFAR-10 as we vary the number of components used.```{python}from sklearn.decomposition import PCA# Fit PCA on the full training data to get all componentsprint("Fitting PCA with all components to compute reconstruction error...")pca_full = PCA(random_state=42)pca_full.fit(X_train_cifar)# Get all eigenvalues (explained variance)eigenvalues = pca_full.explained_variance_# Define range of k values to testk_values =list(range(1, 3073))# Compute reconstruction error for each k# Reconstruction error = average of leftover eigenvalues (those not used)reconstruction_errors = []for k in k_values:# Average of eigenvalues from k onwards (the ones we didn't keep)if k <len(eigenvalues): leftover_eigenvalues = eigenvalues[k:] reconstruction_error = np.mean(leftover_eigenvalues)else: reconstruction_error =0.0 reconstruction_errors.append(reconstruction_error)print(f"Computed reconstruction errors for k = 1 to {len(k_values)}")# Plot reconstruction error vs kplt.figure(figsize=(12, 6))plt.plot(k_values, reconstruction_errors, 'b-', linewidth=2)plt.xlabel('Number of PCA Components (k)', fontsize=12)plt.ylabel('Reconstruction Error (Avg. Leftover Eigenvalues)', fontsize=12)plt.title('CIFAR-10: PCA Reconstruction Error vs Number of Components', fontsize=14, fontweight='bold')plt.grid(True, alpha=0.3)plt.xlim([1, 3072])plt.tight_layout()plt.show()```Looking at this plot, we can see that the sweet spot seems to be somewhere between 300 and 600.Now, let's do PCA on the data to reduce the dimension from 3072 to something much smaller. We're going to try (300,320,...,600). For each of these values, we'll project the data down to that dimension using PCA, fit a multinomial logistic regression on the training data, and then judge the accuracy on the same validation set.```{python}from sklearn.decomposition import PCAfrom sklearn.linear_model import LogisticRegressionfrom sklearn.model_selection import train_test_split# Fit multinomial logistic regression on original features (full dimensionality)print("Fitting Multinomial Logistic Regression on full features (3072 dimensions)...")log_reg_full = LogisticRegression( multi_class='multinomial', solver='lbfgs', max_iter=1000, random_state=42)# Fit on the centered original datalog_reg_full.fit(X_train_cifar, y_train_cifar)# Compute accuraciestrain_acc_full = log_reg_full.score(X_train_cifar, y_train_cifar)test_acc_full = log_reg_full.score(X_test_cifar, y_test_cifar)print(f"\nLogistic Regression on Full CIFAR-10 (3072 features)")print(f"Training Accuracy: {train_acc_full:.4f}")print(f"Test Accuracy: {test_acc_full:.4f}")``````{python}# Use the full training set (already centered) and test set for PCA# No need to split into train/validation since we're using the existing test set# Define the range of PCA components to tryn_components_list =list(range(300, 601, 20))# Store accuraciestrain_accuracies = []test_accuracies = []print("Running PCA + Multinomial Logistic Regression for different dimensions...")print("="*80)for n_comp in n_components_list:# Apply PCA pca = PCA(n_components=n_comp, random_state=42) X_train_pca = pca.fit_transform(X_train_cifar) X_test_pca = pca.transform(X_test_cifar)# Fit multinomial logistic regression log_reg = LogisticRegression( multi_class='multinomial', solver='lbfgs', max_iter=1000, random_state=42 ) log_reg.fit(X_train_pca, y_train_cifar)# Compute accuracies train_acc = log_reg.score(X_train_pca, y_train_cifar) test_acc = log_reg.score(X_test_pca, y_test_cifar) train_accuracies.append(train_acc) test_accuracies.append(test_acc)if n_comp %100==0or n_comp ==20:print(f"n_components = {n_comp:4d} | Train: {train_acc:.4f} | Test: {test_acc:.4f}")print("="*80)print("\nCompleted!\n")``````{python}# Plot the resultsplt.figure(figsize=(12, 6))plt.plot(n_components_list, train_accuracies, 'b-', label='Training Accuracy', linewidth=2)plt.plot(n_components_list, test_accuracies, 'r-', label='Test Accuracy', linewidth=2)plt.xlabel('Number of PCA Components', fontsize=12)plt.ylabel('Accuracy', fontsize=12)plt.title('CIFAR-10: Multinomial Logistic Regression Accuracy vs PCA Dimensions', fontsize=14, fontweight='bold')plt.legend(fontsize=11)plt.grid(True, alpha=0.3)plt.xlim([300, 600])plt.tight_layout()plt.show()# Find best test accuracybest_idx = np.argmax(test_accuracies)best_n_comp = n_components_list[best_idx]best_test_acc = test_accuracies[best_idx]print(f"Best test accuracy: {best_test_acc:.4f} at {best_n_comp} components")print(f"Corresponding test accuracy: {best_test_acc:.4f}")``````{python}print("Running PCA + XGBoost for different dimensions...")print("="*80)# Define the range of PCA components to try for XGBoost (50, 100, ..., 500)n_components_xgb_list =list(range(50, 501, 50))# Store accuraciestrain_accuracies_xgb_pca = []test_accuracies_xgb_pca = []for n_comp in n_components_xgb_list:# Apply PCA pca_xgb = PCA(n_components=n_comp, random_state=42) X_train_pca = pca_xgb.fit_transform(X_train_cifar) X_test_pca = pca_xgb.transform(X_test_cifar)# Fit XGBoost Classifier# Using same parameters as before for consistency xgb_clf_pca = xgb.XGBClassifier( n_estimators=100, max_depth=6, learning_rate=0.1, random_state=42, eval_metric='mlogloss', tree_method='hist' ) xgb_clf_pca.fit(X_train_pca, y_train_cifar)# Compute accuracies train_acc = xgb_clf_pca.score(X_train_pca, y_train_cifar) test_acc = xgb_clf_pca.score(X_test_pca, y_test_cifar) train_accuracies_xgb_pca.append(train_acc) test_accuracies_xgb_pca.append(test_acc)print(f"n_components = {n_comp:4d} | Train: {train_acc:.4f} | Test: {test_acc:.4f}")print("="*80)print("\nCompleted!\n")# Plot the resultsplt.figure(figsize=(12, 6))plt.plot(n_components_xgb_list, train_accuracies_xgb_pca, 'b-', label='Training Accuracy', linewidth=2)plt.plot(n_components_xgb_list, test_accuracies_xgb_pca, 'r-', label='Test Accuracy', linewidth=2)plt.xlabel('Number of PCA Components', fontsize=12)plt.ylabel('Accuracy', fontsize=12)plt.title('CIFAR-10: XGBoost Accuracy vs PCA Dimensions', fontsize=14, fontweight='bold')plt.legend(fontsize=11)plt.grid(True, alpha=0.3)plt.xticks(n_components_xgb_list)plt.tight_layout()plt.show()# Find best test accuracybest_idx_xgb = np.argmax(test_accuracies_xgb_pca)best_n_comp_xgb = n_components_xgb_list[best_idx_xgb]best_test_acc_xgb = test_accuracies_xgb_pca[best_idx_xgb]print(f"Best XGBoost test accuracy: {best_test_acc_xgb:.4f} at {best_n_comp_xgb} components")```It's a nonlinear manifold embedded in a higher dimensional space! PCA can only get us so far - dimensionality reduction can only get us so far if we aren't capturing the true underlying structure of the data.Instead, let's do it end to end with a neural network.```{python}import torchfrom torch.utils.data import DataLoader, TensorDatasetimport pytorch_lightning as plfrom pytorch_lightning.callbacks import ModelCheckpointimport torch.nn as nnimport torch.nn.functional as F# Define the neural network modelclass CIFAR10Classifier(pl.LightningModule):def__init__(self, input_dim=3072, hidden_dim=1024, num_classes=10, learning_rate=0.01, dropout=0.1):super().__init__()self.save_hyperparameters()# 4 hidden layers with batch normalizationself.fc1 = nn.Linear(input_dim, 1024)self.bn1 = nn.BatchNorm1d(1024)self.fc2 = nn.Linear(1024, 512)self.bn2 = nn.BatchNorm1d(512)self.fc3 = nn.Linear(512, 256)self.bn3 = nn.BatchNorm1d(256)#self.fc4 = nn.Linear(hidden_dim, hidden_dim)#self.bn4 = nn.BatchNorm1d(hidden_dim)self.fc5 = nn.Linear(256, num_classes)self.dropout = nn.Dropout(dropout)self.learning_rate = learning_ratedef forward(self, x): x =self.fc1(x) x =self.bn1(x) x = F.relu(x) x =self.dropout(x) x =self.fc2(x) x =self.bn2(x) x = F.relu(x) x =self.dropout(x) x =self.fc3(x) x =self.bn3(x) x = F.relu(x) x =self.dropout(x)#x = self.fc4(x)#x = self.bn4(x)#x = F.relu(x) x =self.dropout(x) x =self.fc5(x) # Logits for cross-entropyreturn xdef training_step(self, batch, batch_idx): x, y = batch logits =self(x) loss = F.cross_entropy(logits, y) acc = (logits.argmax(dim=1) == y).float().mean()self.log('train_loss', loss, prog_bar=True)self.log('train_acc', acc, prog_bar=True)return lossdef validation_step(self, batch, batch_idx): x, y = batch logits =self(x) loss = F.cross_entropy(logits, y) acc = (logits.argmax(dim=1) == y).float().mean()self.log('val_loss', loss, prog_bar=True)self.log('val_acc', acc, prog_bar=True)return lossdef test_step(self, batch, batch_idx): x, y = batch logits =self(x) loss = F.cross_entropy(logits, y) acc = (logits.argmax(dim=1) == y).float().mean()self.log('test_loss', loss)self.log('test_acc', acc)return lossdef configure_optimizers(self): optimizer = torch.optim.AdamW(self.parameters(), lr=self.learning_rate, weight_decay=0.01# Add weight decay (L2 regularization) )return optimizer# Prepare dataprint("Preparing data for PyTorch Lightning...")# Convert to PyTorch tensorsX_train_tensor = torch.FloatTensor(X_train_cifar)y_train_tensor = torch.LongTensor(y_train_cifar)X_test_tensor = torch.FloatTensor(X_test_cifar)y_test_tensor = torch.LongTensor(y_test_cifar)# Create datasetstrain_dataset = TensorDataset(X_train_tensor, y_train_tensor)test_dataset = TensorDataset(X_test_tensor, y_test_tensor)# Create data loaderstrain_loader = DataLoader(train_dataset, batch_size=256, shuffle=True, num_workers=4)test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False, num_workers=4)# Initialize modelmodel = CIFAR10Classifier(input_dim=3072, hidden_dim=128, num_classes=10, learning_rate=0.001)# Set up trainertrainer = pl.Trainer( max_epochs=50, accelerator='gpu'if torch.cuda.is_available() else'cpu', devices=1, callbacks=[ ModelCheckpoint( monitor='val_acc', mode='max', filename='cifar10-{epoch:02d}-{val_acc:.4f}' ) ], log_every_n_steps=50)# Train the modelprint("\nTraining neural network on CIFAR-10...")print(f"Using device: {'GPU'if torch.cuda.is_available() else'CPU'}")trainer.fit(model, train_loader, test_loader)# Test the modelprint("\nTesting the model...")test_results = trainer.test(model, test_loader)print(f"\nFinal Test Accuracy: {test_results[0]['test_acc']:.4f}")print(f"Final Test Loss: {test_results[0]['test_loss']:.4f}")``````{python}import osfrom pytorch_lightning.callbacks import ModelCheckpointimport glob# Load the best model checkpoint# Find the best checkpoint filecheckpoint_files = glob.glob('lightning_logs/version_0/checkpoints/*.ckpt')if checkpoint_files:# Get the most recent checkpoint (or you can sort by validation accuracy) best_checkpoint =max(checkpoint_files, key=lambda x: os.path.getctime(x))print(f"Loading checkpoint: {best_checkpoint}")# Load the model from checkpoint loaded_model = CIFAR10Classifier.load_from_checkpoint(best_checkpoint)# Evaluate on test set trainer_eval = pl.Trainer( accelerator='gpu'if torch.cuda.is_available() else'cpu', devices=1 ) results = trainer_eval.test(loaded_model, test_loader)print(f"\nValidation (Test) Accuracy: {results[0]['test_acc']:.4f}")print(f"Validation (Test) Loss: {results[0]['test_loss']:.4f}")else:print("No checkpoint files found. Using the last trained model.")print(f"\nValidation (Test) Accuracy: {test_results[0]['test_acc']:.4f}")print(f"Validation (Test) Loss: {test_results[0]['test_loss']:.4f}")```