这组文章整理自 2024 年的课程学习笔记,保留原练习、代码和图表。运行前请先看系列目录中的环境与数据说明。 查看系列目录。
Linear dimensionality reduction
The purpose of this notebook is to understand and implement two linear dimensionality reduction methods:
- Principle component analysis (PCA)
- Non-Negative Matrix Factorization (NMF)
The main idea of these methods is to approximate our data by a linear combination of a few components, which span the space for a low-dimensional representation of the data. Such representation is often useful to visualise and inspect the structure of the data in an easier way, allowing us to better understand their properties. Here we illustrate the application of PCA and NMF to a dataset of hand-written digits (MNIST).
1 | # Imports in alphabetical order |
Let’s first load the data. We will use the MNIST dataset for this problem.
1 | # Loading the images |
1 | # Plotting the second image |

Let’s now pre-process the data.
The steps below ensure that our images will have zero mean and unitary variance. These pre-processing
steps are also known as data normalisation/standardisation or feature scaling.
The pre-processing steps are:
- Converting an unsigned integer (
u-int8) encoding each pixel to a floating point number between 0 and 1. - Subtracting from each pixel the mean μ\boldsymbol \muμ across pixels.
- Scaling by 1σ\frac{1}{\sigma}σ1 where σ\sigmaσ is the stardard deviation.
1 | # Pre-processing |
1 | ## EDIT THIS FUNCTION |
1 | X_norm = standardise(X) |
1 | (500, 784) |
1 | # Plotting the second image (now standardised) |

1 | # Let's verify that a given pixel across all the images now has zero mean and unitary standard deviation |
1. PCA
Now we will implement PCA. Before we do that, let’s pause for a moment and
think about the steps for performing PCA. Assume that we are performing PCA on
some dataset X\mathbf{X}X for kkk principal components.
We then need to perform the following steps, which we break into parts:
- Compute the covariance matrix C\mathbf CC.
- Find eigenvalues and corresponding eigenvectors for the covariance matrix, C=VLV⊤\mathbf C = \mathbf V \mathbf L \mathbf V^\topC=VLV⊤. The matrix V\mathbf{V}V is composed of the eigenvectors of matrix C\mathbf{C}C as its columns, while L\mathbf{L}L is a diagonal matrix with the eigenvalues of C\mathbf{C}C as its diagonal elements.
- Sort by the largest eigenvalues and the corresponding eigenvectors.
- Compute the projection onto the spaced spanned by the top eigenvectors.
1 | ## EDIT THIS FUNCTION |
Test the PCA function for three principal components (k=3k=3k=3) and print the eigenvalues.
1 | k = 3 # Our number of principal components |
1 | [45.28147173 30.46538702 27.12873262] |
Now consider a larger number such as k=100k=100k=100. Plot the spectrum, i.e., the histogram of the eigenvalues of C\mathbf{C}C with the density parameter set to True.
1 | k = 100 |

If we randomly permute the pixels of each image, which is equivalent to ‘randomising’ the data, the large eigenvalues disappear. Reshuffle every column separately to remove correlations between pixels
1 | X_norm_random = np.copy(X_norm) |
1 | # The spectrum of eigenvalues this 'random' matrix has not large values! |

Question: what can you say about the previous plot? Compare it with the case in which the columns were not randomly permuted (k=100k=100k=100).
Let’s go back to the original case with k=100k=100k=100 and analyse what fraction of the overall variance is explained by the components.
1 | C = covariance_matrix(X_norm) |

Find the number of components that allows us to explain at least 80% of the total variance.
1 | opt_k = np.argmax(np.array(explained_variances) >= 0.8) # <-- SOLUTION |
1 | 64 |
Let’s now try and understand what these different principle components represent for a given image. Plot the first 20 eigenvectors as images.
1 | nrow = 5; ncol = 4; # <-- SOLUTION |

Using opt_k principal components, try to approximate the image of index_image equal to 5.
1 | index_image = 5 |

Exercise: compute the estimated image using kkk components that account for 7070%70 and 90%90\%90% of the total variance.
Consider now digits 5 and 7 only, and the first two principal components. Plot the data points in a 2D space based on their projections onto eigenvectors 1 and 2 (PC1 and PC2).
1 | ind_5 = [i for i in range(len(labels)) if labels[i] == '5'] # <-- SOLUTION |
1 | plt.scatter(X_pca[:n_datapoints, 0], X_pca[:n_datapoints, 1], label='Digit 5') |

Exercise:
- Define an error between the original and the approximated image. A standard choice is the Mean Squared Error (MSE).
- Calculate the MSE for all images.
- Plot the average MSE as a function of kkk. Can we use this plot to decide
opt_k?
2. NMF
Now we will look at non-negative matrix factorisation. NMF is a matrix factorisation method where we constrain the matrices to have nonnegative elements (while PCA produces components with elements that could be both positive and negative).
NMF factors our N-by-p data matrix X\mathbf XX into matrices with nonnegative elements W\mathbf WW (N-by-r) and H\mathbf HH (r-by-p), i.e. X∼WH\mathbf X \sim \mathbf W\mathbf HX∼WH. WH\mathbf W\mathbf HWH is lower-rank (r<pr < pr<p), hence it gives a low-dimensional approximation of X\mathbf XX.
Note that for non-negative matrix factorisation we require the input matrix to be non-negative. Therefore, we must normalise between 0 and 1 instead of the standard normalisation which we used for PCA.
1 | ## EDIT THIS FUNCTION |
1 | print("Min:",np.min(X), "and max:",np.max(X)) |
1 | Min: 0.0 and max: 1.0 |
1 | ## Redefine the matrix X before applying a new normalization ## |
As explained in the lecture notes, we find WWW and HHH as the local minima of cost functions that represent the quality of the approximation of XXX by WHWHWH. We will see the implementation of the optimisation of the two main cost functions used for NMF.
The first is the square of the Euclidean distance between the data XXX and the product WHWHWH:
$
||\mathbf{X} - \mathbf{WH}||^2 = \sum_{ij}(X_{ij} - (\mathbf{WH})_{ij})^2
$
Local minima of this function are found by the Lee and Seung’s multiplicative update rules
$
H_{jk}^{n+1}\leftarrow H_{jk}^{n}
\frac{(( \mathbf{W}^n)^T \mathbf{X}){jk}}{((\mathbf{W}^n)^T \mathbf{W}^n \mathbf{H}^n){jk}}
$
and
$
W_{ij}^{n+1}\leftarrow W_{ij}^{n}
\frac{(\mathbf{X}(\mathbf{H}^{n+1})^T){ij}}{(\mathbf{W}^n \mathbf{H}^{n+1} (\mathbf{H}^{n+1})^T){ij}}
$
which are repeated for several iterations nnn until W\mathbf{W}W and H\mathbf{H}H converge. It is important to note that updates are done on an element by element basis and not by matrix multiplication.
1 | ## EDIT HERE |
Now implement Lee and Seung’s multiplicative update rule. First implement the update on H and then the update on W, finally compute the objective function.
1 | # choosing the number of dimensions (r) on which to project |
1 | np.testing.assert_allclose(cost_values[-1], 0.057035312210066795) |
We should next check to confirm that we have converged to a solution by plotting the value of our objective function over the iterations.
1 | # plotting the cost, to check convergence |

1 | plt.scatter(W[:,0],W[:,1]) |

1 | plt.scatter(H[0,:],H[1,:]) |

We can observe that these matrices have non-negative entries as we expected, and also they are sparse (which is a desirable property!).
We can see above that we have converged in optimising our NMF according to the Euclidean distance objective function. Next, we repeat the optimisation monitoring the trend of a different objective function, the divergence DDD defined as:
$
D(\mathbf{X}||\mathbf{WH}) = \sum_{ij}\left[X_{ij} \log\left(\frac{X_{ij}}{(\mathbf{WH}){ij}}\right) - X{ij} +(\mathbf{WH})_{ij}\right]
$
for which we are guaranteed to find local minima by slightly different multiplicative update rules:
$
H_{jk}^{n+1}\leftarrow H_{jk}^{n}
\frac{ \sum_i W^n_{ij}X_{ik} / (\mathbf{W}^n\mathbf{H}^n){ik}}{\sum_i W^n{ij}}
$
and
$
W_{ij}^{n+1}\leftarrow W_{ij}^{n}
\frac{\sum_k H_{jk}X_{ik} / (\mathbf{W}^n\mathbf{H}^{n+1}){ik} }{\sum_k H^{n+1}{jk}}
$
1 | ## EDIT HERE |
1 | np.testing.assert_allclose(cost_values[-1], 0.10888229870847117) |
1 | # plotting the cost, to check convergence |

1 | plt.scatter(W[:,0],W[:,1]) |

1 | plt.scatter(H[0,:],H[1,:]) |

As an exercise, you can check that the result of NMF is the same if you
use another version of cost function, the one from the paper:
https://www.nature.com/articles/44565.
Finally, we can have a look at the difference components resulting from our NMF and see what information they might contain.
1 | nrow = 1; ncol = 2; # <-- SOLUTION |

Try another value of r.
1 | # choosing the number of dimensions (r) on which to project |
1 | np.testing.assert_allclose(cost_values[-1], 0.07084365237320929) |
1 | nrow = 2; ncol = 5; # <-- SOLUTION |

We can now do this using sklearn and check that our methodology was implemented correctly. We expect this to be slightly different, the sklearn implementation relies on the optimisation of a slightly different objective function:
https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.NMF.html
1 | r = 2 |

1 | H = nmf.components_ |

1 | nrow = 1; ncol = 2; # <-- SOLUTION |


