390 lines
140 KiB
Plaintext
390 lines
140 KiB
Plaintext
{
|
|||
|
|
"cells": [
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "0a22bd06",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"# Graph Laplacians and Spectral Clustering\n",
|
||
|
|
"\n",
|
||
|
|
"Companion notebook for the `graph` module of `optimiz-rs`.\n",
|
||
|
|
"\n",
|
||
|
|
"Reference documentation: [graph_spectral.html](https://optimiz-r.readthedocs.io/en/latest/algorithms/graph_spectral.html)\n",
|
||
|
|
"\n",
|
||
|
|
"We demonstrate the four Python-exposed primitives:\n",
|
||
|
|
"\n",
|
||
|
|
"1. `combinatorial_laplacian_py`\n",
|
||
|
|
"2. `normalised_laplacian_py`\n",
|
||
|
|
"3. `random_walk_laplacian_py`\n",
|
||
|
|
"4. `spectral_cluster_py`\n",
|
||
|
|
"\n",
|
||
|
|
"Each is verified against an analytic ground truth."
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 1,
|
||
|
|
"id": "3e190cea",
|
||
|
|
"metadata": {
|
||
|
|
"execution": {
|
||
|
|
"iopub.execute_input": "2026-05-12T09:44:15.206941Z",
|
||
|
|
"iopub.status.busy": "2026-05-12T09:44:15.206599Z",
|
||
|
|
"iopub.status.idle": "2026-05-12T09:44:16.002530Z",
|
||
|
|
"shell.execute_reply": "2026-05-12T09:44:16.000885Z"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"import numpy as np\n",
|
||
|
|
"import matplotlib.pyplot as plt\n",
|
||
|
|
"from optimizr import _core as opt\n",
|
||
|
|
"\n",
|
||
|
|
"rng = np.random.default_rng(42)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "af83bd65",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"## 1. Combinatorial Laplacian\n",
|
||
|
|
"\n",
|
||
|
|
"For a non-negative symmetric weight matrix $W \\in \\mathbb{R}^{n\\times n}$ with degree matrix $D = \\mathrm{diag}(W \\mathbf{1})$:\n",
|
||
|
|
"\n",
|
||
|
|
"$$\n",
|
||
|
|
"L \\;=\\; D - W .\n",
|
||
|
|
"$$\n",
|
||
|
|
"\n",
|
||
|
|
"It is symmetric positive semidefinite and $L \\mathbf{1} = 0$, so the constant vector lies in its kernel."
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 2,
|
||
|
|
"id": "c0ae98c1",
|
||
|
|
"metadata": {
|
||
|
|
"execution": {
|
||
|
|
"iopub.execute_input": "2026-05-12T09:44:16.006248Z",
|
||
|
|
"iopub.status.busy": "2026-05-12T09:44:16.005847Z",
|
||
|
|
"iopub.status.idle": "2026-05-12T09:44:16.362683Z",
|
||
|
|
"shell.execute_reply": "2026-05-12T09:44:16.361515Z"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"L =\n",
|
||
|
|
" [[ 2. -1. -1.]\n",
|
||
|
|
" [-1. 2. -1.]\n",
|
||
|
|
" [-1. -1. 2.]]\n",
|
||
|
|
"|| L @ 1 ||_inf = 0.0\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"image/png": "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
|
||
|
|
"text/plain": [
|
||
|
|
"<Figure size 400x320 with 2 Axes>"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "display_data"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Triangle graph (3-clique).\n",
|
||
|
|
"W3 = [\n",
|
||
|
|
" [0.0, 1.0, 1.0],\n",
|
||
|
|
" [1.0, 0.0, 1.0],\n",
|
||
|
|
" [1.0, 1.0, 0.0],\n",
|
||
|
|
"]\n",
|
||
|
|
"L = np.array(opt.combinatorial_laplacian_py(W3))\n",
|
||
|
|
"print('L =\\n', L)\n",
|
||
|
|
"\n",
|
||
|
|
"# Analytic check: L @ 1 = 0.\n",
|
||
|
|
"ones = np.ones(3)\n",
|
||
|
|
"err_kernel = float(np.max(np.abs(L @ ones)))\n",
|
||
|
|
"print('|| L @ 1 ||_inf =', err_kernel)\n",
|
||
|
|
"assert err_kernel < 1e-12\n",
|
||
|
|
"\n",
|
||
|
|
"fig, ax = plt.subplots(figsize=(4, 3.2))\n",
|
||
|
|
"im = ax.imshow(L, cmap='RdBu_r', vmin=-2, vmax=2)\n",
|
||
|
|
"ax.set_title('Combinatorial Laplacian (3-clique)')\n",
|
||
|
|
"plt.colorbar(im, ax=ax)\n",
|
||
|
|
"plt.tight_layout()\n",
|
||
|
|
"plt.show()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "13cb95ba",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"## 2. Symmetric normalised Laplacian\n",
|
||
|
|
"\n",
|
||
|
|
"$$\n",
|
||
|
|
"L_{\\mathrm{sym}} \\;=\\; I - D^{-1/2} W D^{-1/2}.\n",
|
||
|
|
"$$\n",
|
||
|
|
"\n",
|
||
|
|
"Eigenvalues lie in $[0, 2]$ and the multiplicity of the zero eigenvalue equals the number of connected components."
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 3,
|
||
|
|
"id": "cc02bdc7",
|
||
|
|
"metadata": {
|
||
|
|
"execution": {
|
||
|
|
"iopub.execute_input": "2026-05-12T09:44:16.365813Z",
|
||
|
|
"iopub.status.busy": "2026-05-12T09:44:16.365536Z",
|
||
|
|
"iopub.status.idle": "2026-05-12T09:44:17.003051Z",
|
||
|
|
"shell.execute_reply": "2026-05-12T09:44:17.001539Z"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"eigenvalues = [4.4408921e-16 4.4408921e-16 1.5000000e+00 1.5000000e+00 1.5000000e+00\n",
|
||
|
|
" 1.5000000e+00]\n",
|
||
|
|
"number of zero eigenvalues = 2\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"image/png": "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
|
||
|
|
"text/plain": [
|
||
|
|
"<Figure size 800x320 with 3 Axes>"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "display_data"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Two disconnected triangles -> two zero eigenvalues.\n",
|
||
|
|
"W6 = np.zeros((6, 6))\n",
|
||
|
|
"for (i, j) in [(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)]:\n",
|
||
|
|
" W6[i, j] = 1.0\n",
|
||
|
|
" W6[j, i] = 1.0\n",
|
||
|
|
"Lsym = np.array(opt.normalised_laplacian_py(W6.tolist()))\n",
|
||
|
|
"eigvals = np.sort(np.linalg.eigvalsh(Lsym))\n",
|
||
|
|
"print('eigenvalues =', eigvals)\n",
|
||
|
|
"\n",
|
||
|
|
"# Two connected components -> exactly two near-zero eigenvalues.\n",
|
||
|
|
"n_zero = int(np.sum(np.abs(eigvals) < 1e-10))\n",
|
||
|
|
"print('number of zero eigenvalues =', n_zero)\n",
|
||
|
|
"assert n_zero == 2\n",
|
||
|
|
"\n",
|
||
|
|
"fig, axes = plt.subplots(1, 2, figsize=(8, 3.2))\n",
|
||
|
|
"im = axes[0].imshow(Lsym, cmap='RdBu_r', vmin=-1, vmax=1)\n",
|
||
|
|
"axes[0].set_title(r'$L_{\\mathrm{sym}}$ (2 disconnected triangles)')\n",
|
||
|
|
"plt.colorbar(im, ax=axes[0])\n",
|
||
|
|
"axes[1].plot(eigvals, 'o-')\n",
|
||
|
|
"axes[1].axhline(0.0, color='k', linewidth=0.5)\n",
|
||
|
|
"axes[1].set_xlabel('index')\n",
|
||
|
|
"axes[1].set_ylabel('eigenvalue')\n",
|
||
|
|
"axes[1].set_title('Spectrum of $L_{\\\\mathrm{sym}}$')\n",
|
||
|
|
"plt.tight_layout()\n",
|
||
|
|
"plt.show()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "4ee010f7",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"## 3. Random-walk Laplacian\n",
|
||
|
|
"\n",
|
||
|
|
"$$\n",
|
||
|
|
"L_{\\mathrm{rw}} \\;=\\; I - D^{-1} W.\n",
|
||
|
|
"$$\n",
|
||
|
|
"\n",
|
||
|
|
"The matrix $P = D^{-1} W$ is the row-stochastic transition matrix of the simple random walk on the graph. Hence $L_{\\mathrm{rw}} \\mathbf{1} = 0$."
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 4,
|
||
|
|
"id": "249bb72a",
|
||
|
|
"metadata": {
|
||
|
|
"execution": {
|
||
|
|
"iopub.execute_input": "2026-05-12T09:44:17.006260Z",
|
||
|
|
"iopub.status.busy": "2026-05-12T09:44:17.005978Z",
|
||
|
|
"iopub.status.idle": "2026-05-12T09:44:17.298401Z",
|
||
|
|
"shell.execute_reply": "2026-05-12T09:44:17.297301Z"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"row sums of P = [1. 1. 1. 1. 1. 1.]\n",
|
||
|
|
"|| P @ 1 - 1 ||_inf = 0.0\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"image/png": "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
|
||
|
|
"text/plain": [
|
||
|
|
"<Figure size 400x320 with 2 Axes>"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "display_data"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"Lrw = np.array(opt.random_walk_laplacian_py(W6.tolist()))\n",
|
||
|
|
"P = np.eye(6) - Lrw\n",
|
||
|
|
"row_sums = P.sum(axis=1)\n",
|
||
|
|
"print('row sums of P =', row_sums)\n",
|
||
|
|
"err_stochastic = float(np.max(np.abs(row_sums - 1.0)))\n",
|
||
|
|
"print('|| P @ 1 - 1 ||_inf =', err_stochastic)\n",
|
||
|
|
"assert err_stochastic < 1e-12\n",
|
||
|
|
"\n",
|
||
|
|
"fig, ax = plt.subplots(figsize=(4, 3.2))\n",
|
||
|
|
"im = ax.imshow(Lrw, cmap='RdBu_r', vmin=-1, vmax=1)\n",
|
||
|
|
"ax.set_title(r'$L_{\\mathrm{rw}}$ (2 disconnected triangles)')\n",
|
||
|
|
"plt.colorbar(im, ax=ax)\n",
|
||
|
|
"plt.tight_layout()\n",
|
||
|
|
"plt.show()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "baaf3fe7",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"## 4. Spectral clustering (Ng--Jordan--Weiss)\n",
|
||
|
|
"\n",
|
||
|
|
"Given a similarity matrix $W$ and a target number of clusters $k$:\n",
|
||
|
|
"\n",
|
||
|
|
"1. Build $L_{\\mathrm{sym}} = I - D^{-1/2} W D^{-1/2}$.\n",
|
||
|
|
"2. Stack the $k$ eigenvectors of $L_{\\mathrm{sym}}$ associated with the smallest eigenvalues as columns of $U \\in \\mathbb{R}^{n \\times k}$.\n",
|
||
|
|
"3. Row-normalise $U$ and apply Lloyd's $k$-means with k-means++ seeding to its rows.\n",
|
||
|
|
"\n",
|
||
|
|
"We test on two well-separated 2D Gaussian blobs. Ground truth: cluster purity equals $1.0$."
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 5,
|
||
|
|
"id": "ddbc3936",
|
||
|
|
"metadata": {
|
||
|
|
"execution": {
|
||
|
|
"iopub.execute_input": "2026-05-12T09:44:17.302450Z",
|
||
|
|
"iopub.status.busy": "2026-05-12T09:44:17.302058Z",
|
||
|
|
"iopub.status.idle": "2026-05-12T09:44:17.805918Z",
|
||
|
|
"shell.execute_reply": "2026-05-12T09:44:17.803760Z"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"first 6 eigenvalues = [-6.47704929e-16 6.07023906e-07 9.56672916e-01 9.57721212e-01\n",
|
||
|
|
" 9.82109668e-01 9.84579761e-01]\n",
|
||
|
|
"fiedler value = 6.070239058352574e-07\n",
|
||
|
|
"cluster purity = 1.0\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"image/png": "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
|
||
|
|
"text/plain": [
|
||
|
|
"<Figure size 900x360 with 2 Axes>"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "display_data"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"n_per = 30\n",
|
||
|
|
"X1 = rng.normal(loc=[-3.0, 0.0], scale=0.35, size=(n_per, 2))\n",
|
||
|
|
"X2 = rng.normal(loc=[+3.0, 0.0], scale=0.35, size=(n_per, 2))\n",
|
||
|
|
"X = np.vstack([X1, X2])\n",
|
||
|
|
"y_true = np.array([0] * n_per + [1] * n_per)\n",
|
||
|
|
"n = X.shape[0]\n",
|
||
|
|
"\n",
|
||
|
|
"# Gaussian similarity, zero diagonal.\n",
|
||
|
|
"sigma = 1.0\n",
|
||
|
|
"D2 = np.sum((X[:, None, :] - X[None, :, :]) ** 2, axis=-1)\n",
|
||
|
|
"W = np.exp(-D2 / (2.0 * sigma ** 2))\n",
|
||
|
|
"np.fill_diagonal(W, 0.0)\n",
|
||
|
|
"\n",
|
||
|
|
"result = opt.spectral_cluster_py(W.tolist(), k=2, n_kmeans_iter=200, seed=7)\n",
|
||
|
|
"labels = np.array(result['labels'])\n",
|
||
|
|
"eigvals = np.array(result['eigenvalues'])\n",
|
||
|
|
"fiedler = result['fiedler_value']\n",
|
||
|
|
"print('first 6 eigenvalues =', eigvals[:6])\n",
|
||
|
|
"print('fiedler value =', fiedler)\n",
|
||
|
|
"\n",
|
||
|
|
"# Cluster purity (label-permutation invariant).\n",
|
||
|
|
"def purity(y_true, y_pred):\n",
|
||
|
|
" classes = np.unique(y_pred)\n",
|
||
|
|
" correct = 0\n",
|
||
|
|
" for c in classes:\n",
|
||
|
|
" mask = y_pred == c\n",
|
||
|
|
" if mask.any():\n",
|
||
|
|
" correct += int(np.bincount(y_true[mask]).max())\n",
|
||
|
|
" return correct / len(y_true)\n",
|
||
|
|
"\n",
|
||
|
|
"p = purity(y_true, labels)\n",
|
||
|
|
"print('cluster purity =', p)\n",
|
||
|
|
"assert p == 1.0\n",
|
||
|
|
"\n",
|
||
|
|
"fig, axes = plt.subplots(1, 2, figsize=(9, 3.6))\n",
|
||
|
|
"axes[0].scatter(X[:, 0], X[:, 1], c=labels, cmap='coolwarm', edgecolor='k')\n",
|
||
|
|
"axes[0].set_title('Spectral cluster labels')\n",
|
||
|
|
"axes[0].set_xlabel('x')\n",
|
||
|
|
"axes[0].set_ylabel('y')\n",
|
||
|
|
"axes[1].plot(eigvals[:10], 'o-')\n",
|
||
|
|
"axes[1].axhline(0.0, color='k', linewidth=0.5)\n",
|
||
|
|
"axes[1].set_xlabel('index')\n",
|
||
|
|
"axes[1].set_ylabel('eigenvalue')\n",
|
||
|
|
"axes[1].set_title(r'Smallest 10 eigenvalues of $L_{\\mathrm{sym}}$')\n",
|
||
|
|
"plt.tight_layout()\n",
|
||
|
|
"plt.show()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "17d3fce2",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"## Summary\n",
|
||
|
|
"\n",
|
||
|
|
"Verified against analytic ground truth:\n",
|
||
|
|
"\n",
|
||
|
|
"- $L \\mathbf{1} = 0$ for the combinatorial Laplacian (3-clique) — error $< 10^{-12}$.\n",
|
||
|
|
"- For two disconnected triangles, $L_{\\mathrm{sym}}$ has exactly $2$ zero eigenvalues — error $< 10^{-10}$.\n",
|
||
|
|
"- $P = I - L_{\\mathrm{rw}}$ is row-stochastic, $P \\mathbf{1} = \\mathbf{1}$ — error $< 10^{-12}$.\n",
|
||
|
|
"- Spectral clustering recovers two well-separated Gaussian blobs with purity $= 1.0$ — error $= 0$."
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"metadata": {
|
||
|
|
"language_info": {
|
||
|
|
"codemirror_mode": {
|
||
|
|
"name": "ipython",
|
||
|
|
"version": 3
|
||
|
|
},
|
||
|
|
"file_extension": ".py",
|
||
|
|
"mimetype": "text/x-python",
|
||
|
|
"name": "python",
|
||
|
|
"nbconvert_exporter": "python",
|
||
|
|
"pygments_lexer": "ipython3",
|
||
|
|
"version": "3.11.13"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"nbformat": 4,
|
||
|
|
"nbformat_minor": 5
|
||
|
|
}
|