From 1a9ead62c220551a3a186f1c7954d1139c6ec6ad Mon Sep 17 00:00:00 2001 From: Hellsegga Date: Thu, 1 Jun 2023 20:08:25 +0200 Subject: [PATCH 1/4] Initial commit of SNN --- test/nn/simplicial/test_snn_layer.py | 42 +++ topomodelx/nn/simplicial/snn_layer.py | 104 +++++++ tutorials/simplicial/snn_train.ipynb | 427 ++++++++++++++++++++++++++ 3 files changed, 573 insertions(+) create mode 100644 test/nn/simplicial/test_snn_layer.py create mode 100644 topomodelx/nn/simplicial/snn_layer.py create mode 100644 tutorials/simplicial/snn_train.ipynb diff --git a/test/nn/simplicial/test_snn_layer.py b/test/nn/simplicial/test_snn_layer.py new file mode 100644 index 00000000..3bab1de0 --- /dev/null +++ b/test/nn/simplicial/test_snn_layer.py @@ -0,0 +1,42 @@ +"""Test the SNN layer.""" + +import torch + +from topomodelx.nn.simplicial.snn_layer import SNNLayer + + +class TestSNNLayer: + """Test the SNN layer.""" + + def test_forward(self): + """Test the forward pass of the HSN layer.""" + in_channels = 5 + out_channels = 5 + n_nodes = 10 + K = 5 + lapl_0 = torch.randint(0, 2, (n_nodes, n_nodes)).float() + + x_0 = torch.randn(n_nodes, in_channels) + + snn = SNNLayer(in_channels, out_channels, K) + output = snn.forward(x_0, lapl_0) + + assert output.shape == (n_nodes, out_channels) + + def test_reset_parameters(self): + """Test the reset of the parameters.""" + in_channels = 5 + out_channels = 5 + K = 5 + + snn = SNNLayer(in_channels, out_channels, K) + snn.reset_parameters() + + for module in snn.modules(): + if isinstance(module, torch.nn.Conv2d): + torch.testing.assert_allclose( + module.weight, torch.zeros_like(module.weight) + ) + torch.testing.assert_allclose( + module.bias, torch.zeros_like(module.bias) + ) diff --git a/topomodelx/nn/simplicial/snn_layer.py b/topomodelx/nn/simplicial/snn_layer.py new file mode 100644 index 00000000..bf6e0f12 --- /dev/null +++ b/topomodelx/nn/simplicial/snn_layer.py @@ -0,0 +1,104 @@ +"""Simplicial Neural Network Layer.""" +import torch + +from topomodelx.base.aggregation import Aggregation +from topomodelx.base.conv import Conv + + +class SNNLayer(torch.nn.Module): + """Layer of a Simplicial Neural Network (SNN). + + Implementation of the SNN layer proposed in [SNN20]. + + + References + ---------- + .. [SNN20] Stefania Ebli, Michael Defferrard and Gard Spreemann. + Simplicial Neural Networks. + Topological Data Analysis and Beyond workshop at NeurIPS. + https://arxiv.org/abs/2010.03633 + + Parameters + ---------- + K : int + Maximum polynomial degree for Laplacian. + in_channels : int + Dimension of features on each simplicial cell. + out_channels : int + Dimension of output representation on each simplicial cell. + initialization : string + Initialization method. + """ + + def __init__( + self, + in_channels, + out_channels, + K + ): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.K = K + + self.convs = [Conv(in_channels=in_channels, out_channels=out_channels, update_func="relu") for _ in range(self.K)] + + self.aggr = Aggregation(aggr_func="sum", update_func="relu") + + def reset_parameters(self): + r"""Reset learnable parameters.""" + + for conv in self.convs: + conv.reset_parameters() + + def forward(self, x, laplacian): + r"""Forward pass. + + The forward pass was initially proposed in [SNN20]_. + Its equations are adapted from [TNN23]_, graphically illustrated in [PSHM23]_. + + .. math:: + \begin{align*} + &🟥 \quad m_{y \rightarrow x}^{p, (d \rightarrow d)} = ((H_{d})^p)\_{xy} \cdot h_y^{t,(d)} \cdot \Theta^{t,p}\\ + &🟧 \quad m_{x}^{p, (d \rightarrow d)} = \sum_{y \in (\mathcal{L}\_\uparrow + \mathcal{L}\_\downarrow)(x)} m_{y \rightarrow x}^{p, (d \rightarrow d)}\\ + &🟧 \quad m_x^{(d \rightarrow d)} = \sum_{p=1}^{P_1} (m_{x}^{p,(d \rightarrow d)})^{p}\\ + &🟩 \quad m_x^{(d)} = m_x^{(d \rightarrow d)}\\ + &🟦 \quad h_x^{t+1, (d)} = \sigma (m_{x}^{(d)}) + \end{align*} + + References + ---------- + .. [SNN20] Stefania Ebli, Michael Defferrard and Gard Spreemann. + Simplicial Neural Networks. + Topological Data Analysis and Beyond workshop at NeurIPS. + https://arxiv.org/abs/2010.03633 + .. [TNN23] Equations of Topological Neural Networks. + https://github.com/awesome-tnns/awesome-tnns/ + .. [PSHM23] Papillon, Sanborn, Hajij, Miolane. + Architectures of Topological Deep Learning: A Survey on Topological Neural Networks. + (2023) https://arxiv.org/abs/2304.10031. + + Parameters + ---------- + x: torch.Tensor, shape=[n_simplices, in_channels] + Input features on the simplices of the simplicial complex for the given simplicial degree. + laplacian : torch.sparse, shape=[n_simplices, n_simplices] + Simplicial Laplacian matrix for the given simplicial degree. + + Returns + ------- + _ : torch.Tensor, shape=[n_simplices, out_channels] + Output features on the nodes of the simplicial complex. + """ + + outputs = [] + laplacian_power = torch.eye(laplacian.shape[0]) + outputs.append(self.convs[0](x, laplacian_power)) + + for i in range(1, self.K): + laplacian_power = torch.mm(laplacian_power, laplacian) + + outputs.append(self.convs[i](x, laplacian_power)) + + x = self.aggr(outputs) + return x diff --git a/tutorials/simplicial/snn_train.ipynb b/tutorials/simplicial/snn_train.ipynb new file mode 100644 index 00000000..413aea35 --- /dev/null +++ b/tutorials/simplicial/snn_train.ipynb @@ -0,0 +1,427 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Train a Simplicial Neural Network (SNN)\n", + "\n", + "In this notebook, we will create and train a Simplicial Neural Network, as proposed in the paper by [Ebli et. al : Simplicial Neural Networks (2020)](https://arxiv.org/abs/2010.03633). \n", + "\n", + "We train the model to perform binary node classification using the KarateClub benchmark dataset. \n", + "\n", + "The equations of one layer of this neural network are given by:\n", + "\n", + "\n", + "🟥 $\\quad m_{y \\rightarrow x}^{p, (d \\rightarrow d)} = ((H_{d})^p)\\_{xy} \\cdot h_y^{t,(d)} \\cdot \\Theta^{t,p}$\n", + "\n", + "🟧 $\\quad m_{x}^{p, (d \\rightarrow d)} = \\sum_{y \\in (\\mathcal{L}\\_\\uparrow + \\mathcal{L}\\_\\downarrow)(x)} m_{y \\rightarrow x}^{p, (d \\rightarrow d)}$\n", + "\n", + "🟧 $\\quad m_x^{(d \\rightarrow d)} = \\sum_{p=1}^{P_1} (m_{x}^{p,(d \\rightarrow d)})^{p}$\n", + "\n", + "🟩 $\\quad m_x^{(d)} = m_x^{(d \\rightarrow d)}$\n", + "\n", + "🟦 $\\quad h_x^{t+1, (d)} = \\sigma (m_{x}^{(d)})$\n", + "\n", + "Note that since the forward function is defined separately for each simplicial degree, a layer is created for a particular simplicial degree $d$ as in the equations above. Multiple layers can be created and combined in a neural network architecture if multiple degrees are to be used.\n", + "\n", + "Where the notations are defined in [Papillon et al : Architectures of Topological Deep Learning: A Survey of Topological Neural Networks (2023)](https://arxiv.org/abs/2304.10031)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "import numpy as np\n", + "\n", + "from torch_geometric.datasets.karate import KarateClub\n", + "from torch_geometric.utils.convert import to_networkx\n", + "from toponetx import SimplicialComplex\n", + "from topomodelx.nn.simplicial.snn_layer import SNNLayer" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Pre-processing\n", + "\n", + "## Import dataset ##\n", + "\n", + "The first step is to import the Karate Club (https://www.jstor.org/stable/3629752) dataset. This is a singular graph with 34 nodes that belong to two different social groups. We will use these groups for the task of node-level binary classification.\n", + "\n", + "We must first lift our graph dataset into the simplicial complex domain." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = KarateClub()\n", + "simplex = SimplicialComplex(to_networkx(dataset[0]))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "NodeView([(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,), (8,), (9,), (10,), (11,), (12,), (13,), (14,), (15,), (16,), (17,), (18,), (19,), (20,), (21,), (22,), (23,), (24,), (25,), (26,), (27,), (28,), (29,), (30,), (31,), (32,), (33,)])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "simplex.nodes" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define neighborhood structures. ##\n", + "\n", + "For the Simplicial Neural Network we will use the (normalized) Simplician Laplacian matrices of different degrees. In this example since we have data and labels on nodes only, we use degree 0 only. We also convert the neighborhood structures to torch tensors." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "lapl0 = simplex.normalized_laplacian_matrix(rank=0)\n", + "\n", + "lapl0 = torch.from_numpy(lapl0.todense()).to_sparse()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([34, 34])" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lapl0.shape" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import signal ##\n", + "\n", + "Since our task will be node classification, we must retrieve an input signal on the nodes. The signal will have shape $n_\\text{nodes} \\times$ in_channels, where in_channels is the dimension of each cell's feature. Here, we have in_channels = channels_nodes $ = 34$. This is because the Karate dataset encodes the identity of each of the 34 nodes as a one hot encoder." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "torch.Size([34, 34])\n" + ] + } + ], + "source": [ + "x_nodes = dataset[0].x\n", + "print(x_nodes.shape)\n", + "channels_nodes = x_nodes.shape[-1]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "34" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "channels_nodes" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define binary labels\n", + "We retrieve the labels associated to the nodes of each input simplex. In the KarateClub dataset, two social groups emerge. So we assign binary labels to the nodes indicating of which group they are a part.\n", + "\n", + "We convert the binary labels into one-hot encoder form, and keep the first four nodes' true labels for the purpose of testing." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "y = np.array(\n", + " [\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 0,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 0,\n", + " 0,\n", + " 1,\n", + " 1,\n", + " 0,\n", + " 1,\n", + " 0,\n", + " 1,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " ]\n", + ")\n", + "y_true = np.zeros((34, 2))\n", + "y_true[:, 0] = y\n", + "y_true[:, 1] = 1 - y\n", + "y_test = y_true[:4]\n", + "y_train = y_true[-30:]\n", + "\n", + "y_train = torch.from_numpy(y_train)\n", + "y_test = torch.from_numpy(y_test)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create the Neural Network\n", + "\n", + "Using the SNNLayer class, we create a neural network with stacked layers. A linear layer at the end produces an output with shape $n_\\text{nodes} \\times 2$, so we can compare with our binary labels." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "class SNN(torch.nn.Module):\n", + " \"\"\"Simplicial Neural Network Implementation for binary node classification.\n", + "\n", + " Parameters\n", + " ---------\n", + " channels : int\n", + " Dimension of features\n", + " n_layers : int\n", + " Amount of message passing layers.\n", + "\n", + " \"\"\"\n", + "\n", + " def __init__(self, in_channels, out_channels, hidden_channels, K=5):\n", + " super().__init__()\n", + " layers = []\n", + "\n", + " layers.append(SNNLayer(in_channels=in_channels, out_channels=hidden_channels, K=K))\n", + " layers.append(SNNLayer(in_channels=hidden_channels, out_channels=out_channels, K=K))\n", + "\n", + " self.linear = torch.nn.Linear(out_channels, 2)\n", + " self.layers = layers\n", + "\n", + " def forward(self, x_0, lapl_0):\n", + " \"\"\"Forward computation.\n", + "\n", + " Parameters\n", + " ---------\n", + " x_0 : tensor\n", + " shape = [n_nodes, channels]\n", + " Node features.\n", + "\n", + " lapl_0 : tensor\n", + " shape = [n_nodes, n_edges]\n", + " Boundary matrix of rank 1.\n", + "\n", + "\n", + " Returns\n", + " --------\n", + " _ : tensor\n", + " shape = [n_nodes, 2]\n", + " One-hot labels assigned to nodes.\n", + "\n", + " \"\"\"\n", + " for layer in self.layers:\n", + " x_0 = layer(x_0, lapl_0)\n", + " return torch.sigmoid(self.linear(x_0))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Train the Neural Network\n", + "\n", + "We specify the model with our pre-made neighborhood structures and specify an optimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "model = SNN(\n", + " in_channels=channels_nodes,\n", + " out_channels=channels_nodes,\n", + " hidden_channels=30,\n", + " K=5\n", + ")\n", + "optimizer = torch.optim.Adam(model.parameters(), lr=0.4)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following cell performs the training, looping over the network for a low number of epochs." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch: 1 loss: 0.7309 Train_acc: 0.0000\n", + "Epoch: 2 loss: 0.6826 Train_acc: 0.0000\n", + "Test_acc: 0.0000\n", + "Epoch: 3 loss: 0.6652 Train_acc: 0.0000\n", + "Epoch: 4 loss: 0.6620 Train_acc: 0.0000\n", + "Test_acc: 0.0000\n", + "Epoch: 5 loss: 0.6516 Train_acc: 0.0000\n" + ] + } + ], + "source": [ + "test_interval = 2\n", + "num_epochs = 5\n", + "for epoch_i in range(1, num_epochs + 1):\n", + " epoch_loss = []\n", + " model.train()\n", + " optimizer.zero_grad()\n", + "\n", + " y_hat = model(x_nodes, lapl0)\n", + " loss = torch.nn.functional.binary_cross_entropy_with_logits(\n", + " y_hat[-len(y_train) :].float(), y_train.float()\n", + " )\n", + " epoch_loss.append(loss.item())\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " y_pred = torch.where(y_hat > 0.5, torch.tensor(1), torch.tensor(0))\n", + " accuracy = (y_pred == y_hat).all(dim=1).float().mean().item()\n", + " print(\n", + " f\"Epoch: {epoch_i} loss: {np.mean(epoch_loss):.4f} Train_acc: {accuracy:.4f}\",\n", + " flush=True,\n", + " )\n", + " if epoch_i % test_interval == 0:\n", + " with torch.no_grad():\n", + " y_hat_test = model(x_nodes, lapl0)\n", + " y_pred_test = torch.sigmoid(y_hat_test).ge(0.5).float()\n", + " test_accuracy = (\n", + " torch.eq(y_pred_test[: len(y_test)], y_test)\n", + " .all(dim=1)\n", + " .float()\n", + " .mean()\n", + " .item()\n", + " )\n", + " print(f\"Test_acc: {test_accuracy:.4f}\", flush=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From b6e08d4c6c3ebb95dbf9f9f2673cccc3aeadc5da Mon Sep 17 00:00:00 2001 From: Hellsegga Date: Fri, 2 Jun 2023 17:54:42 +0200 Subject: [PATCH 2/4] Reformatted snn_layer with black --- topomodelx/nn/simplicial/snn_layer.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/topomodelx/nn/simplicial/snn_layer.py b/topomodelx/nn/simplicial/snn_layer.py index bf6e0f12..8044da7a 100644 --- a/topomodelx/nn/simplicial/snn_layer.py +++ b/topomodelx/nn/simplicial/snn_layer.py @@ -30,18 +30,16 @@ class SNNLayer(torch.nn.Module): Initialization method. """ - def __init__( - self, - in_channels, - out_channels, - K - ): + def __init__(self, in_channels, out_channels, K): super().__init__() self.in_channels = in_channels self.out_channels = out_channels self.K = K - self.convs = [Conv(in_channels=in_channels, out_channels=out_channels, update_func="relu") for _ in range(self.K)] + self.convs = [ + Conv(in_channels=in_channels, out_channels=out_channels, update_func="relu") + for _ in range(self.K) + ] self.aggr = Aggregation(aggr_func="sum", update_func="relu") @@ -96,7 +94,7 @@ def forward(self, x, laplacian): outputs.append(self.convs[0](x, laplacian_power)) for i in range(1, self.K): - laplacian_power = torch.mm(laplacian_power, laplacian) + laplacian_power = torch.mm(laplacian_power, laplacian) outputs.append(self.convs[i](x, laplacian_power)) From 5382ee0ff330c7fddd2ba7ff0b3bea8d515dfa50 Mon Sep 17 00:00:00 2001 From: Hellsegga Date: Tue, 27 Jun 2023 13:11:41 +0200 Subject: [PATCH 3/4] Using torch.nn.ModuleList --- topomodelx/nn/simplicial/snn_layer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/topomodelx/nn/simplicial/snn_layer.py b/topomodelx/nn/simplicial/snn_layer.py index 8044da7a..28a8278e 100644 --- a/topomodelx/nn/simplicial/snn_layer.py +++ b/topomodelx/nn/simplicial/snn_layer.py @@ -36,11 +36,13 @@ def __init__(self, in_channels, out_channels, K): self.out_channels = out_channels self.K = K - self.convs = [ + convs = [ Conv(in_channels=in_channels, out_channels=out_channels, update_func="relu") for _ in range(self.K) ] + self.convs = torch.nn.ModuleList(convs) + self.aggr = Aggregation(aggr_func="sum", update_func="relu") def reset_parameters(self): From edb885a0bde9220a11279cb0186887867ce9e93b Mon Sep 17 00:00:00 2001 From: Hellsegga Date: Wed, 12 Jul 2023 15:15:30 +0200 Subject: [PATCH 4/4] Tutorial is now based on coauthorship dataset --- tutorials/simplicial/snn_train.ipynb | 398 ++++++++++++++++----------- 1 file changed, 233 insertions(+), 165 deletions(-) diff --git a/tutorials/simplicial/snn_train.ipynb b/tutorials/simplicial/snn_train.ipynb index 413aea35..43260cae 100644 --- a/tutorials/simplicial/snn_train.ipynb +++ b/tutorials/simplicial/snn_train.ipynb @@ -9,7 +9,7 @@ "\n", "In this notebook, we will create and train a Simplicial Neural Network, as proposed in the paper by [Ebli et. al : Simplicial Neural Networks (2020)](https://arxiv.org/abs/2010.03633). \n", "\n", - "We train the model to perform binary node classification using the KarateClub benchmark dataset. \n", + "We train the model to perform regression on citation count on a coauthorship dataset \n", "\n", "The equations of one layer of this neural network are given by:\n", "\n", @@ -24,9 +24,10 @@ "\n", "🟦 $\\quad h_x^{t+1, (d)} = \\sigma (m_{x}^{(d)})$\n", "\n", - "Note that since the forward function is defined separately for each simplicial degree, a layer is created for a particular simplicial degree $d$ as in the equations above. Multiple layers can be created and combined in a neural network architecture if multiple degrees are to be used.\n", "\n", - "Where the notations are defined in [Papillon et al : Architectures of Topological Deep Learning: A Survey of Topological Neural Networks (2023)](https://arxiv.org/abs/2304.10031)." + "Where the notations are defined in [Papillon et al : Architectures of Topological Deep Learning: A Survey of Topological Neural Networks (2023)](https://arxiv.org/abs/2304.10031).\n", + "\n", + "Note that since the forward function is defined separately for each simplicial degree, a layer is created for a particular simplicial degree $d$ as in the equations above. Depending on the problem, multiple layers may be created and combined in a neural network architecture." ] }, { @@ -37,11 +38,10 @@ "source": [ "import torch\n", "import numpy as np\n", - "\n", - "from torch_geometric.datasets.karate import KarateClub\n", + "from matplotlib import pyplot as plt\n", "from torch_geometric.utils.convert import to_networkx\n", - "from toponetx import SimplicialComplex\n", - "from topomodelx.nn.simplicial.snn_layer import SNNLayer" + "from topomodelx.nn.simplicial.snn_layer import SNNLayer\n", + "import toponetx.datasets as datasets" ] }, { @@ -49,13 +49,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Pre-processing\n", + "## Load and process the dataset\n", + "\n", + "Load the coauthorship network from [Ebli et. al : Simplicial Neural Networks (2020)](https://arxiv.org/abs/2010.03633) as a simplicial complex.\n", "\n", - "## Import dataset ##\n", + "The coauthorship network is a simplicial complex where a paper with k authors is represented by a (k-1)-simplex.\n", "\n", - "The first step is to import the Karate Club (https://www.jstor.org/stable/3629752) dataset. This is a singular graph with 34 nodes that belong to two different social groups. We will use these groups for the task of node-level binary classification.\n", + "The dataset is pre-processed as in the original paper. From the Semantic Scholar Open Research Corpus 80 papers with number of citations between 5 and 10 were sampled.\n", + "The papers constitute simplices in the complex, which is completed with subsimplices (seen as collaborations between subsets of authors) to form a simplicial complex.\n", "\n", - "We must first lift our graph dataset into the simplicial complex domain." + "An attribute named \"citations\" is added to each simplex, corresponding to the sum of citations of all papers on which the authors represented by the simplex collaborated. This will be used as feature and target in our regression problem.\n", + "\n", + "The resulting simplicial complex is of dimension 10 and contains 24552 simplices in total. See the original paper for a more detailed description of the dataset." ] }, { @@ -64,8 +69,7 @@ "metadata": {}, "outputs": [], "source": [ - "dataset = KarateClub()\n", - "simplex = SimplicialComplex(to_networkx(dataset[0]))" + "sc = datasets.graph.coauthorship()" ] }, { @@ -74,18 +78,15 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "NodeView([(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,), (8,), (9,), (10,), (11,), (12,), (13,), (14,), (15,), (16,), (17,), (18,), (19,), (20,), (21,), (22,), (23,), (24,), (25,), (26,), (27,), (28,), (29,), (30,), (31,), (32,), (33,)])" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Total number of simplices: 24552\n" + ] } ], "source": [ - "simplex.nodes" + "print(\"Total number of simplices:\", sum([len(sc.skeleton(i)) for i in range(sc.dim+1)]))" ] }, { @@ -93,9 +94,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Define neighborhood structures. ##\n", + "Below we mask the features (number of citations for each simplex, for each degree), similar to the way it is done in the original paper.\n", "\n", - "For the Simplicial Neural Network we will use the (normalized) Simplician Laplacian matrices of different degrees. In this example since we have data and labels on nodes only, we use degree 0 only. We also convert the neighborhood structures to torch tensors." + "For each simplicial degree, *ratio_mask* simplices will be masked:\n", + "- At training time they will be replaced by the median value for the feature and will not be included when computing the loss.\n", + "- At test time on the other hand, these are the simplices for which test loss and accuracy are computed." ] }, { @@ -104,29 +107,29 @@ "metadata": {}, "outputs": [], "source": [ - "lapl0 = simplex.normalized_laplacian_matrix(rank=0)\n", - "\n", - "lapl0 = torch.from_numpy(lapl0.todense()).to_sparse()" + "ratio_mask = 0.15" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 5, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "torch.Size([34, 34])" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "lapl0.shape" + "features_target = {}\n", + "for i in range(sc.dim+1):\n", + " features_target[i] = np.array(list(sc.get_simplex_attributes(name=\"citations\", rank=i).values()))\n", + "\n", + "masks_train = {}\n", + "masks_test = {}\n", + "features_input = {}\n", + "\n", + "for i in range(len(features_target)):\n", + " masks_test[i] = np.random.choice(range(features_target[i].shape[0]), size=int(features_target[i].shape[0]*ratio_mask), replace=False)\n", + " masks_train[i] = list(set(range(features_target[i].shape[0])).difference(set(masks_test[i])))\n", + " features_input[i] = features_target[i].copy()\n", + " features_input[i][masks_test[i]] = np.median(features_target[i])\n", + "\n" ] }, { @@ -134,113 +137,71 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Import signal ##\n", + "## Define neighborhood structures. ##\n", + "\n", + "For the Simplicial Neural Network we will use the (normalized) Simplician Laplacian matrix.\n", "\n", - "Since our task will be node classification, we must retrieve an input signal on the nodes. The signal will have shape $n_\\text{nodes} \\times$ in_channels, where in_channels is the dimension of each cell's feature. Here, we have in_channels = channels_nodes $ = 34$. This is because the Karate dataset encodes the identity of each of the 34 nodes as a one hot encoder." + "We can define the simplicial degree (rank) for which we want to run the regression problem below (since we have features on all simplicial degrees). Note that when changing the rank we get different dimension for the Laplacian matrix and the feature vector, which may lead to longer training time, and the network might need to be tuned differently." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "torch.Size([34, 34])\n" - ] - } - ], + "outputs": [], "source": [ - "x_nodes = dataset[0].x\n", - "print(x_nodes.shape)\n", - "channels_nodes = x_nodes.shape[-1]" + "rank = 0" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, + "outputs": [], + "source": [ + "lapl = sc.normalized_laplacian_matrix(rank=rank)\n", + "\n", + "lapl = torch.from_numpy(lapl.todense()).to_sparse()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "34" + "torch.Size([352, 352])" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "channels_nodes" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define binary labels\n", - "We retrieve the labels associated to the nodes of each input simplex. In the KarateClub dataset, two social groups emerge. So we assign binary labels to the nodes indicating of which group they are a part.\n", - "\n", - "We convert the binary labels into one-hot encoder form, and keep the first four nodes' true labels for the purpose of testing." + "lapl.shape" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "torch.Size([352, 1])\n" + ] + } + ], "source": [ - "y = np.array(\n", - " [\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 0,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 0,\n", - " 0,\n", - " 1,\n", - " 1,\n", - " 0,\n", - " 1,\n", - " 0,\n", - " 1,\n", - " 0,\n", - " 0,\n", - " 0,\n", - " 0,\n", - " 0,\n", - " 0,\n", - " 0,\n", - " 0,\n", - " 0,\n", - " 0,\n", - " 0,\n", - " 0,\n", - " ]\n", - ")\n", - "y_true = np.zeros((34, 2))\n", - "y_true[:, 0] = y\n", - "y_true[:, 1] = 1 - y\n", - "y_test = y_true[:4]\n", - "y_train = y_true[-30:]\n", - "\n", - "y_train = torch.from_numpy(y_train)\n", - "y_test = torch.from_numpy(y_test)" + "x_nodes = torch.from_numpy(features_input[rank].reshape(-1, 1)).float()\n", + "y_nodes = torch.from_numpy(features_target[rank].reshape(-1, 1)).float()\n", + "print(x_nodes.shape)\n", + "channels_nodes = x_nodes.shape[1]" ] }, { @@ -249,13 +210,12 @@ "metadata": {}, "source": [ "# Create the Neural Network\n", - "\n", - "Using the SNNLayer class, we create a neural network with stacked layers. A linear layer at the end produces an output with shape $n_\\text{nodes} \\times 2$, so we can compare with our binary labels." + "\n" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -273,38 +233,39 @@ "\n", " def __init__(self, in_channels, out_channels, hidden_channels, K=5):\n", " super().__init__()\n", - " layers = []\n", + " layers = torch.nn.ModuleList()\n", "\n", " layers.append(SNNLayer(in_channels=in_channels, out_channels=hidden_channels, K=K))\n", + " layers.append(SNNLayer(in_channels=hidden_channels, out_channels=hidden_channels, K=K))\n", " layers.append(SNNLayer(in_channels=hidden_channels, out_channels=out_channels, K=K))\n", "\n", - " self.linear = torch.nn.Linear(out_channels, 2)\n", + " self.linear = torch.nn.Linear(out_channels, 1)\n", " self.layers = layers\n", "\n", - " def forward(self, x_0, lapl_0):\n", + " def forward(self, x, lapl):\n", " \"\"\"Forward computation.\n", "\n", " Parameters\n", " ---------\n", - " x_0 : tensor\n", - " shape = [n_nodes, channels]\n", + " x : tensor\n", + " shape = [n_simplices, channels]\n", " Node features.\n", "\n", - " lapl_0 : tensor\n", - " shape = [n_nodes, n_edges]\n", - " Boundary matrix of rank 1.\n", + " lapl : tensor\n", + " shape = [n_simplices, n_simplices]\n", + " Laplacian for the given rank\n", "\n", "\n", " Returns\n", " --------\n", " _ : tensor\n", - " shape = [n_nodes, 2]\n", - " One-hot labels assigned to nodes.\n", + " shape = [n_simplices, channels]\n", + " \n", "\n", " \"\"\"\n", " for layer in self.layers:\n", - " x_0 = layer(x_0, lapl_0)\n", - " return torch.sigmoid(self.linear(x_0))" + " x = layer(x, lapl)\n", + " return self.linear(x)" ] }, { @@ -319,7 +280,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -329,7 +290,30 @@ " hidden_channels=30,\n", " K=5\n", ")\n", - "optimizer = torch.optim.Adam(model.parameters(), lr=0.4)" + "optimizer = torch.optim.Adam(model.parameters(), lr=0.01)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parameter counts:\n", + "Total number of parameters: 4802\n" + ] + } + ], + "source": [ + "num_params = 0\n", + "print(\"Parameter counts:\")\n", + "for param in model.parameters():\n", + " p = np.array(param.shape, dtype=int).prod()\n", + " num_params += p\n", + "print(\"Total number of parameters: %d\" %(num_params))" ] }, { @@ -342,57 +326,141 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Epoch: 1 loss: 0.7309 Train_acc: 0.0000\n", - "Epoch: 2 loss: 0.6826 Train_acc: 0.0000\n", - "Test_acc: 0.0000\n", - "Epoch: 3 loss: 0.6652 Train_acc: 0.0000\n", - "Epoch: 4 loss: 0.6620 Train_acc: 0.0000\n", - "Test_acc: 0.0000\n", - "Epoch: 5 loss: 0.6516 Train_acc: 0.0000\n" + "Epoch: 0 loss: 10.8426\n", + "Epoch: 10 loss: 2.1569\n", + "Epoch: 20 loss: 1.1768\n", + "Epoch: 30 loss: 0.8447\n", + "Epoch: 40 loss: 0.7254\n", + "Epoch: 50 loss: 1.4321\n", + "Epoch: 60 loss: 0.5802\n", + "Epoch: 70 loss: 0.2790\n", + "Epoch: 80 loss: 0.3477\n", + "Epoch: 90 loss: 0.5995\n", + "Epoch: 100 loss: 0.1706\n" ] } ], "source": [ - "test_interval = 2\n", - "num_epochs = 5\n", - "for epoch_i in range(1, num_epochs + 1):\n", + "train_losses = []\n", + "test_losses = []\n", + "test_interval = 10\n", + "num_epochs = 100\n", + "for epoch_i in range(0, num_epochs + 1):\n", " epoch_loss = []\n", " model.train()\n", " optimizer.zero_grad()\n", "\n", - " y_hat = model(x_nodes, lapl0)\n", - " loss = torch.nn.functional.binary_cross_entropy_with_logits(\n", - " y_hat[-len(y_train) :].float(), y_train.float()\n", + " y_hat = model(x_nodes, lapl)\n", + " loss = torch.nn.functional.l1_loss(\n", + " y_hat[masks_train[rank]], y_nodes[masks_train[rank]], reduction=\"mean\"\n", " )\n", + " train_losses.append(loss.detach().numpy())\n", " epoch_loss.append(loss.item())\n", " loss.backward()\n", " optimizer.step()\n", "\n", - " y_pred = torch.where(y_hat > 0.5, torch.tensor(1), torch.tensor(0))\n", - " accuracy = (y_pred == y_hat).all(dim=1).float().mean().item()\n", - " print(\n", - " f\"Epoch: {epoch_i} loss: {np.mean(epoch_loss):.4f} Train_acc: {accuracy:.4f}\",\n", - " flush=True,\n", - " )\n", + " \n", " if epoch_i % test_interval == 0:\n", - " with torch.no_grad():\n", - " y_hat_test = model(x_nodes, lapl0)\n", - " y_pred_test = torch.sigmoid(y_hat_test).ge(0.5).float()\n", - " test_accuracy = (\n", - " torch.eq(y_pred_test[: len(y_test)], y_test)\n", - " .all(dim=1)\n", - " .float()\n", - " .mean()\n", - " .item()\n", - " )\n", - " print(f\"Test_acc: {test_accuracy:.4f}\", flush=True)" + " print(\n", + " f\"Epoch: {epoch_i} loss: {np.mean(epoch_loss):.4f}\",\n", + " flush=True,\n", + " )\n", + " with torch.no_grad():\n", + " loss = torch.nn.functional.l1_loss(y_hat[masks_test[rank]], y_nodes[masks_test[rank]])\n", + " test_losses.append(loss)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Test set loss')" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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oiiKdXV14tNPPy2mpKSIarVaTEpKEhctWuTQ2nPfvn3iTTfdJPbo0UPUarVidHS0OHPmTHHPnj3yNl988YV4xRVXiNHR0aJGoxF79Ogh3nXXXWJeXl6zPw8iou7E2bVKFK2l0CNHjhT9/PzEoKAgMTU1VXzsscfE3NxcURRb9z4siqK4bds2ceTIkaJGo2nxmsTrEZEjQRS74CtuIiIiIiKiTsQ5NkRERERE5PUY2BARERERkddjYENERERERF6PgQ0REREREXk9BjZEREREROT1GNgQEREREZHX87gFOi0WC3JzcxEUFARBENw9HCKibkUURVRUVCA+Ph4KBb/7kvDaRETkHm25LnlcYJObm4ukpCR3D4OIqFvLzs5GYmKiu4fhMXhtIiJyr9ZclzwusAkKCgJgHXxwcLCbR0NE1L3o9XokJSXJ78VkxWsTEZF7tOW65HGBjZTiDw4O5sWDiMhNWG7liNcmIiL3as11iQXURERERETk9RjYEBERERGR12NgQ0REREREXo+BDREREREReT0GNkRERERE5PUY2BARERERkddjYENERERERF6PgQ0REREREXk9BjZEREREROT1GNgQEREREZHX87nA5sWfjmP2W79jw9ECdw+FiIjI5TYdL8DCj/ehvLrO3UMhIvIoPhfYnL1Qhf1ZZcgrr3H3UIiIiFzujU2n8cPBPHx7MNfdQyEi8ig+F9gEaFUAgEqD2c0jISIicr3skmoAwKHzZe4dCBGRh/G5wCZQqwQAVBlMbh4JERGRa1UbTSiqNAIADp4vd/NoiIg8i88FNv5yxoaBDRGRrzGbzXjiiSeQkpICPz8/9O7dG88++yxEUWz2dZs3b8aIESOg1WrRp08frFy5smsG7GLnS+vLrE8WVKDayGsdEZHE5wKbQFtgw4wNEZHvefHFF7F06VL897//xbFjx/Diiy/i3//+N954440mX3P27FnMmDEDU6ZMQXp6Oh588EEsWLAA69at68KRu4ZUhgYAFhE4mqt342iIiDyLyt0DcLUAja0Ujd9iERH5nG3btuGaa67BjBkzAADJycn45JNPsGvXriZfs2zZMqSkpODll18GAAwcOBBbt27Fq6++imnTpnXJuF3FPrABgAPnyzEqOdxNoyEi8iw+l7EJkDM2bB5ARORrxo8fj40bN+LkyZMAgAMHDmDr1q2YPn16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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(10,4))\n", + "plt.subplot(121)\n", + "plt.plot(train_losses)\n", + "plt.title(\"Train set loss\")\n", + "plt.subplot(122)\n", + "plt.plot(test_losses)\n", + "plt.title(\"Test set loss\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also compute a measure of accuracy for the train and test sets, where a prediction is considered accurate if it deviates at most 10% from the target.\n", + "\n", + "Unfortunately the results are not comparable to those in the original paper. When looking at the implementation for the original paper (https://github.com/stefaniaebli/simplicial_neural_networks) a potential problem with the masking was found which could explain the difference (it seems that two masks are used, one for the imputation of the feature and one for the computation of the loss, where we use the same mask)." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train accuracy: tensor(100.)\n" + ] + } + ], + "source": [ + "ground_truth = y_nodes[masks_train[rank]]\n", + "preds = y_hat[masks_train[rank]]\n", + "\n", + "print(\"Train accuracy: \", (torch.abs((preds-ground_truth)/ground_truth)<0.1).sum()/preds.shape[0]*100)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Test accuracy: tensor(5.7692)\n" + ] + } + ], + "source": [ + "ground_truth = y_nodes[masks_test[rank]]\n", + "preds = y_hat[masks_test[rank]]\n", + "\n", + "print(\"Test accuracy: \", (torch.abs((preds-ground_truth)/ground_truth)<0.1).sum()/preds.shape[0]*100)" ] }, {