这组文章整理自 2024 年的课程学习笔记,保留原练习、代码和图表。运行前请先看系列目录中的环境与数据说明。 查看系列目录。
Clustering using Gaussian Mixture Models
In this notebook, we will learn about mixture models that find applications in clustering. To start with, we will focus on two clusters and we will use the expectation-maximisation algorithm for optimisation.
1 | # import libraries |
Section 1: Gaussian Mixture Model
Following the lecture notes, the Gaussian Mixture Model (GMM) is given by:
P ( X = x ) = ∑ k = 1 K π k p k ( x ∣ θ ) . P(\boldsymbol{X}=\boldsymbol{x}) = \sum_{k=1}^K \pi_k p_k(\boldsymbol{x}|\boldsymbol{\theta})\, . P(X=x)=k=1∑Kπkpk(x∣θ).
Here K K K is the number of clusters described as mixture components, each of which are multivariate normal distributions:
p k ( x ∣ θ ) = ( 2 π ) − k / 2 det ( Σ k ) − 1 / 2 exp ( − 1 2 ( x − μ k ) T Σ k − 1 ( x − μ k ) ) , p_k(\boldsymbol{x}|\boldsymbol{\theta}) = {\displaystyle (2\pi )^{-k/2}\det({\boldsymbol {\Sigma }_k})^{-1/2}\,\exp \left(-{\frac {1}{2}}(\mathbf {x} -{\boldsymbol {\mu }_k})^{\!{\mathsf {T}}}{\boldsymbol {\Sigma }_k}^{-1}(\mathbf {x} -{\boldsymbol {\mu }_k})\right),} pk(x∣θ)=(2π)−k/2det(Σk)−1/2exp(−21(x−μk)TΣk−1(x−μk)),
where θ = { π k , μ k , Σ k } k = 1 , 2 , . . . , K \boldsymbol{\theta} = \{\pi_k,\mu_k, \Sigma_k \}_{k=1,2,...,K} θ={πk,μk,Σk}k=1,2,...,K is the vector of parameters consiting of the mixture weights π k \pi_k πk, mixture component means μ k \boldsymbol{\mu}_k μk and mixture component covariance matrices μ k \boldsymbol{\mu}_k μk.
We start by implementing a class for the GMM model.
1 | import copy |
We can perform ‘soft’ clustering of the data using the cluster probabilities of the data:
r i k ( θ ) = P ( Z = k ∣ X = x i , θ ) = π k p k ( x i ∣ θ ) ∑ k ′ = 1 K π k ′ p k ′ ( x i ∣ θ ) r_{ik}(\boldsymbol{\theta})=P(Z=k|\boldsymbol{X}=\boldsymbol{x}_i,\boldsymbol{\theta}) = \frac{\pi_k p_k(\boldsymbol{x}_i|\boldsymbol{\theta})}{\sum_{k'=1}^K \pi_{k'} p_{k'}(\boldsymbol{x}_i|\boldsymbol{\theta})} rik(θ)=P(Z=k∣X=xi,θ)=∑k′=1Kπk′pk′(xi∣θ)πkpk(xi∣θ)
This denotes the probability of data point i i i to belong to cluster k k k. Generally, this yields a distribution over each data point.
1 | ## EDIT THIS FUNCTION |
Let’s make sure that we pass the following test cases.
1 | np.random.seed(2) |
For visualisation it is useful to present the results as hard clusters on the output through the argmax of the cluster distribution:
1 | ## EDIT THIS FUNCTION |
Let us initially try GMM clustering on synthetic data. Sampling Gaussian mixtures is relatively straightforward.
1 | np.random.seed(42) |

We borrow some plot functions from the Python Data Science Handbook to visualise the mixture model.
See https://jakevdp.github.io/PythonDataScienceHandbook/05.12-gaussian-mixtures.html
1 | from matplotlib.patches import Ellipse |
We can now cluster the synthetic data using a GMM model with randomly initialised parameters:
1 | np.random.seed(4) |
1 | <ipython-input-7-0ad8cdc6bec6>:19: MatplotlibDeprecationWarning: Passing the angle parameter of __init__() positionally is deprecated since Matplotlib 3.6; the parameter will become keyword-only two minor releases later. |
As expected, the two clusters obtained from the randomly initialised GMM do not match the ground truth clusters at all because we first need to learn the parameters.
Section 2: Fitting Gaussian Mixture Models using the EM algorithm
We employ the EM algorithm to fit the data. The algorithm iteratively updates parameters of the Gausian Mixture. The algorithm is guaranteed to improve (or at least not worsen) the marginal likelihood of the data. The algorithm updates the mixture weights:
π k ( n + 1 ) = 1 N ∑ i = 1 N r i k ( θ ( n ) ) , \pi_k^{(n+1)} = \frac{1}{N}\sum_{i=1}^N r_{ik}(\boldsymbol{\theta}^{(n)}), πk(n+1)=N1i=1∑Nrik(θ(n)),
and computes the cluster means using a weighted mean:
μ k ( n + 1 ) = ∑ i = 1 N w i k ( θ ( n ) ) x i \boldsymbol{\mu}_k^{(n+1)} =\sum_{i=1}^N w_{ik}(\boldsymbol{\theta}^{(n)}) \boldsymbol{x}_i μk(n+1)=i=1∑Nwik(θ(n))xi
and similarly for the covariances according to
Σ k ( n + 1 ) = ∑ i = 1 N w i k ( θ ( n ) ) ( x i − μ k ) ( x i − μ k ) T . \boldsymbol{\Sigma}_k^{(n+1)}= \sum_{i=1}^N w_{ik}(\boldsymbol{\theta}^{(n)}) (\boldsymbol{x}_i-\boldsymbol{\mu}_k) (\boldsymbol{x}_i-\boldsymbol{\mu}_k)^T. Σk(n+1)=i=1∑Nwik(θ(n))(xi−μk)(xi−μk)T.
The weights are obtained from the cluster probabilities via
w i k ( θ ( n ) ) = r i k ( θ ( n ) ) ∑ i ′ r i ′ k ( θ ( n ) ) w_{ik}(\boldsymbol{\theta}^{(n)})=\frac{r_{ik}(\boldsymbol{\theta}^{(n)})}{\sum_{i'} r_{i'k}(\boldsymbol{\theta}^{(n)})} wik(θ(n))=∑i′ri′k(θ(n))rik(θ(n))
A single step of this iteration is implemented in the following function:
1 | ## EDIT THIS FUNCTION |
Let’s make sure we pass the following test case.
1 | np.random.seed(2) |
One EM iteration does not significantly improve the result:
1 | gmm_fit_step(gmm,data0) |
1 | <ipython-input-7-0ad8cdc6bec6>:19: MatplotlibDeprecationWarning: Passing the angle parameter of __init__() positionally is deprecated since Matplotlib 3.6; the parameter will become keyword-only two minor releases later. |

Let’s see if we can observe some improvement after several iterations:
1 | for _ in range(10): |
1 | <ipython-input-7-0ad8cdc6bec6>:19: MatplotlibDeprecationWarning: Passing the angle parameter of __init__() positionally is deprecated since Matplotlib 3.6; the parameter will become keyword-only two minor releases later. |

Section 3: Clustering of breast cancer data
In this final section we will again work with the Breast Cancer Wisconsin (Diagnostic) Data Set, which you first need to download and then load in this notebook. If you faced difficulties downloading this data set from Kaggle, you should download the file directly from Blackboard. The data set contains various aspects of cell nuclei of breast screening images of patients with (malignant) and without (benign) breast cancer. Our goal is to cluster the data without without the knowledge of the tumor being malignant or benign.
If you run this notebook locally on your machine, you will simply need to place the csv file in the same directory as this notebook.
If you run this notebook on Google Colab, you will need to use
from google.colab import files
upload = files.upload()
and then upload it from your local downloads directory.
1 | from google.colab import files |
1 | <input type="file" id="files-84fa5ae3-df7f-4d20-b21a-b3bde42dac5e" name="files[]" multiple disabled |
//
// Licensed under the Apache License, Version 2.0 (the “License”);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an “AS IS” BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
- @fileoverview Helpers for google.colab Python module.
*/
(function(scope) {
function span(text, styleAttributes = {}) {
const element = document.createElement(‘span’);
element.textContent = text;
for (const key of Object.keys(styleAttributes)) {
element.style[key] = styleAttributes[key];
}
return element;
}
// Max number of bytes which will be uploaded at a time.
const MAX_PAYLOAD_SIZE = 100 * 1024;
function _uploadFiles(inputId, outputId) {
const steps = uploadFilesStep(inputId, outputId);
const outputElement = document.getElementById(outputId);
// Cache steps on the outputElement to make it available for the next call
// to uploadFilesContinue from Python.
outputElement.steps = steps;
return _uploadFilesContinue(outputId);
}
// This is roughly an async generator (not supported in the browser yet),
// where there are multiple asynchronous steps and the Python side is going
// to poll for completion of each step.
// This uses a Promise to block the python side on completion of each step,
// then passes the result of the previous step as the input to the next step.
function _uploadFilesContinue(outputId) {
const outputElement = document.getElementById(outputId);
const steps = outputElement.steps;
const next = steps.next(outputElement.lastPromiseValue);
return Promise.resolve(next.value.promise).then((value) => {
// Cache the last promise value to make it available to the next
// step of the generator.
outputElement.lastPromiseValue = value;
return next.value.response;
});
}
/**
- Generator function which is called between each async step of the upload
- process.
- @param {string} inputId Element ID of the input file picker element.
- @param {string} outputId Element ID of the output display.
- @return {!Iterable<!Object>} Iterable of next steps.
/
function uploadFilesStep(inputId, outputId) {
const inputElement = document.getElementById(inputId);
inputElement.disabled = false;
const outputElement = document.getElementById(outputId);
outputElement.innerHTML = ‘’;
const pickedPromise = new Promise((resolve) => {
inputElement.addEventListener(‘change’, (e) => {
resolve(e.target.files);
});
});
const cancel = document.createElement(‘button’);
inputElement.parentElement.appendChild(cancel);
cancel.textContent = ‘Cancel upload’;
const cancelPromise = new Promise((resolve) => {
cancel.onclick = () => {
resolve(null);
};
});
// Wait for the user to pick the files.
const files = yield {
promise: Promise.race([pickedPromise, cancelPromise]),
response: {
action: ‘starting’,
}
};
cancel.remove();
// Disable the input element since further picks are not allowed.
inputElement.disabled = true;
if (!files) {
return {
response: {
action: ‘complete’,
}
};
}
for (const file of files) {
const li = document.createElement(‘li’);
li.append(span(file.name, {fontWeight: ‘bold’}));
li.append(span((${file.type || 'n/a'}) - ${file.size} bytes, +last modified: ${ file.lastModifiedDate ? file.lastModifiedDate.toLocaleDateString() : 'n/a'} - ));
const percent = span(‘0% done’);
li.appendChild(percent);
1 | outputElement.appendChild(li); |
}
// All done.
yield {
response: {
action: ‘complete’,
}
};
}
scope.google = scope.google || {};
scope.google.colab = scope.google.colab || {};
scope.google.colab._files = {
_uploadFiles,
_uploadFilesContinue,
};
})(self);
1 | Saving data.csv to data.csv |
We start by visualising the ground-truth labels with respect to two featurs, the Mean Radius and Mean Texture of the tumor.
1 | # plot for two features |

In the following, we apply GMM clustering to the data set only using these two features (Mean Radius and Mean Texture).
1 | # restrict features |
1 | <ipython-input-7-0ad8cdc6bec6>:19: MatplotlibDeprecationWarning: Passing the angle parameter of __init__() positionally is deprecated since Matplotlib 3.6; the parameter will become keyword-only two minor releases later. |

After initialising our GMM model we can fit it to the data.
1 | for _ in range(100): |
1 | <ipython-input-7-0ad8cdc6bec6>:19: MatplotlibDeprecationWarning: Passing the angle parameter of __init__() positionally is deprecated since Matplotlib 3.6; the parameter will become keyword-only two minor releases later. |

Questions:
- How do you know that the EM algorithm converged?
- How could the clustering of the cancer data set be improved?
- Can you quantify the uncertainty of cluster assignments for the cancer data set?
- Can you think of caveats when optimising hyperparameters of GMMs?
- What are suitable criteria for clustering quality using mixture models?

