Unverified Commit c1341190 authored by Quan (Andy) Gan's avatar Quan (Andy) Gan Committed by GitHub
Browse files

[Sparse] Update HGNN notebook (#5206)

* Created using Colaboratory

* Created using Colaboratory
parent db772831
...@@ -55,25 +55,14 @@ ...@@ -55,25 +55,14 @@
"base_uri": "https://localhost:8080/" "base_uri": "https://localhost:8080/"
}, },
"id": "__2tKqL0eaB0", "id": "__2tKqL0eaB0",
"outputId": "bfd8f958-511b-47a5-b44f-8823421a62e4" "outputId": "5b5106f6-074b-42a5-fc4c-4936efd2cef8"
}, },
"execution_count": 5, "execution_count": 1,
"outputs": [ "outputs": [
{ {
"output_type": "stream", "output_type": "stream",
"name": "stdout", "name": "stdout",
"text": [ "text": [
"Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n",
"Collecting torchmetrics\n",
" Downloading torchmetrics-0.11.0-py3-none-any.whl (512 kB)\n",
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m512.4/512.4 KB\u001b[0m \u001b[31m8.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
"\u001b[?25hRequirement already satisfied: typing-extensions in /usr/local/lib/python3.8/dist-packages (from torchmetrics) (4.4.0)\n",
"Requirement already satisfied: numpy>=1.17.2 in /usr/local/lib/python3.8/dist-packages (from torchmetrics) (1.21.6)\n",
"Requirement already satisfied: torch>=1.8.1 in /usr/local/lib/python3.8/dist-packages (from torchmetrics) (1.13.0+cu116)\n",
"Requirement already satisfied: packaging in /usr/local/lib/python3.8/dist-packages (from torchmetrics) (21.3)\n",
"Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /usr/local/lib/python3.8/dist-packages (from packaging->torchmetrics) (3.0.9)\n",
"Installing collected packages: torchmetrics\n",
"Successfully installed torchmetrics-0.11.0\n",
"DGL installed!\n" "DGL installed!\n"
] ]
} }
...@@ -87,6 +76,8 @@ ...@@ -87,6 +76,8 @@
"A [**hypergraph**](https://en.wikipedia.org/wiki/Hypergraph) consists of *nodes* and *hyperedges*. Contrary to edges in graphs, a *hyperedge* can connect arbitrary number of nodes. For instance, the following figure shows a hypergraph with 11 nodes and 5 hyperedges drawn in different colors.\n", "A [**hypergraph**](https://en.wikipedia.org/wiki/Hypergraph) consists of *nodes* and *hyperedges*. Contrary to edges in graphs, a *hyperedge* can connect arbitrary number of nodes. For instance, the following figure shows a hypergraph with 11 nodes and 5 hyperedges drawn in different colors.\n",
"![](https://data.dgl.ai/tutorial/img/hgnn/hypergraph4.PNG)\n", "![](https://data.dgl.ai/tutorial/img/hgnn/hypergraph4.PNG)\n",
"\n", "\n",
"Hypergraphs are particularly useful when the relationships between data points within the dataset is not binary. For instance, more than two products can be co-purchased together in an e-commerce system, so the relationship of co-purchase is $n$-ary rather than binary, and therefore it is better described as a hypergraph rather than a normal graph.\n",
"\n",
"A hypergraph is usually characterized by its *incidence matrix* $H$, whose rows represent nodes and columns represent hyperedges. An entry $H_{ij}$ is 1 if hyperedge $j$ includes node $i$, or 0 otherwise. For example, the hypergraph in the figure above can be characterized by a $11 \\times 5$ matrix as follows:\n", "A hypergraph is usually characterized by its *incidence matrix* $H$, whose rows represent nodes and columns represent hyperedges. An entry $H_{ij}$ is 1 if hyperedge $j$ includes node $i$, or 0 otherwise. For example, the hypergraph in the figure above can be characterized by a $11 \\times 5$ matrix as follows:\n",
"\n", "\n",
"$$\n", "$$\n",
...@@ -129,9 +120,9 @@ ...@@ -129,9 +120,9 @@
"colab": { "colab": {
"base_uri": "https://localhost:8080/" "base_uri": "https://localhost:8080/"
}, },
"outputId": "4cc9fa37-7726-42e5-f01a-018c409044be" "outputId": "a1a576f6-1559-479c-9f3e-93e41a56833d"
}, },
"execution_count": 6, "execution_count": 2,
"outputs": [ "outputs": [
{ {
"output_type": "stream", "output_type": "stream",
...@@ -175,9 +166,9 @@ ...@@ -175,9 +166,9 @@
"colab": { "colab": {
"base_uri": "https://localhost:8080/" "base_uri": "https://localhost:8080/"
}, },
"outputId": "bb6538aa-26f8-42ee-8d54-a2ea5a4fbd51" "outputId": "ffe2c441-8c2c-48a7-cef2-4ef6e96548ec"
}, },
"execution_count": 7, "execution_count": 3,
"outputs": [ "outputs": [
{ {
"output_type": "stream", "output_type": "stream",
...@@ -204,7 +195,9 @@ ...@@ -204,7 +195,9 @@
"* $H \\in \\mathbb{R}^{N \\times M}$ is the incidence matrix of hypergraph with $N$ nodes and $M$ hyperedges.\n", "* $H \\in \\mathbb{R}^{N \\times M}$ is the incidence matrix of hypergraph with $N$ nodes and $M$ hyperedges.\n",
"* $D_v \\in \\mathbb{R}^{N \\times N}$ is a diagonal matrix representing node degrees, whose $i$-th diagonal element is $\\sum_{j=1}^M H_{ij}$.\n", "* $D_v \\in \\mathbb{R}^{N \\times N}$ is a diagonal matrix representing node degrees, whose $i$-th diagonal element is $\\sum_{j=1}^M H_{ij}$.\n",
"* $D_e \\in \\mathbb{R}^{M \\times M}$ is a diagonal matrix representing hyperedge degrees, whose $j$-th diagonal element is $\\sum_{i=1}^N H_{ij}$.\n", "* $D_e \\in \\mathbb{R}^{M \\times M}$ is a diagonal matrix representing hyperedge degrees, whose $j$-th diagonal element is $\\sum_{i=1}^N H_{ij}$.\n",
"* $B \\in \\mathbb{R}^{M \\times M}$ is a diagonal matrix representing the hyperedge weights, whose $j$-th diagonal element is the weight of $j$-th hyperedge. In our example, $B$ is an identity matrix." "* $B \\in \\mathbb{R}^{M \\times M}$ is a diagonal matrix representing the hyperedge weights, whose $j$-th diagonal element is the weight of $j$-th hyperedge. In our example, $B$ is an identity matrix.\n",
"\n",
"The following code builds a two-layer HGNN."
], ],
"metadata": { "metadata": {
"id": "7kxrINkVHrAi" "id": "7kxrINkVHrAi"
...@@ -213,9 +206,6 @@ ...@@ -213,9 +206,6 @@
{ {
"cell_type": "code", "cell_type": "code",
"source": [ "source": [
"\"\"\"\n",
"Hypergraph Neural Networks (https://arxiv.org/pdf/1809.09401.pdf)\n",
"\"\"\"\n",
"import dgl.sparse as dglsp\n", "import dgl.sparse as dglsp\n",
"import torch\n", "import torch\n",
"import torch.nn as nn\n", "import torch.nn as nn\n",
...@@ -229,28 +219,92 @@ ...@@ -229,28 +219,92 @@
" def __init__(self, H, in_size, out_size, hidden_dims=16):\n", " def __init__(self, H, in_size, out_size, hidden_dims=16):\n",
" super().__init__()\n", " super().__init__()\n",
"\n", "\n",
" self.Theta1 = nn.Linear(in_size, hidden_dims)\n", " self.W1 = nn.Linear(in_size, hidden_dims)\n",
" self.Theta2 = nn.Linear(hidden_dims, out_size)\n", " self.W2 = nn.Linear(hidden_dims, out_size)\n",
" self.dropout = nn.Dropout(0.5)\n", " self.dropout = nn.Dropout(0.5)\n",
"\n", "\n",
" ###########################################################\n", " ###########################################################\n",
" # (HIGHLIGHT) Compute the Laplacian with Sparse Matrix API\n", " # (HIGHLIGHT) Compute the Laplacian with Sparse Matrix API\n",
" ###########################################################\n", " ###########################################################\n",
" d_V = H.sum(1) # node degree\n", " # Compute node degree.\n",
" d_E = H.sum(0) # edge degree\n", " d_V = H.sum(1)\n",
" # Compute edge degree.\n",
" d_E = H.sum(0)\n",
" # Compute the inverse of the square root of the diagonal D_v.\n",
" D_v_invsqrt = dglsp.diag(d_V**-0.5)\n",
" # Compute the inverse of the diagonal D_e.\n",
" D_e_inv = dglsp.diag(d_E**-1)\n",
" # In our example, B is an identity matrix.\n",
" n_edges = d_E.shape[0]\n", " n_edges = d_E.shape[0]\n",
" D_V_invsqrt = dglsp.diag(d_V**-0.5) # D_V ** (-1/2)\n", " B = dglsp.identity((n_edges, n_edges))\n",
" D_E_inv = dglsp.diag(d_E**-1) # D_E ** (-1)\n", " # Compute Laplacian from the equation above.\n",
" W = dglsp.identity((n_edges, n_edges))\n", " self.L = D_v_invsqrt @ H @ B @ D_e_inv @ H.T @ D_v_invsqrt\n",
" self.laplacian = D_V_invsqrt @ H @ W @ D_E_inv @ H.T @ D_V_invsqrt\n",
"\n", "\n",
" def forward(self, X):\n", " def forward(self, X):\n",
" X = self.laplacian @ self.Theta1(self.dropout(X))\n", " X = self.L @ self.W1(self.dropout(X))\n",
" X = F.relu(X)\n", " X = F.relu(X)\n",
" X = self.laplacian @ self.Theta2(self.dropout(X))\n", " X = self.L @ self.W2(self.dropout(X))\n",
" return X\n", " return X"
],
"metadata": {
"id": "58WnPtPvT2mx"
},
"execution_count": 4,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Loading Data\n",
"\n",
"We use Cora citation network in our example. But instead of using the original \"cite\" relationship between papers, we consider the \"co-cite\" relationship between papers. We build a hypergraph from the original citation network where for each paper we construct a hyperedge that includes all the other papers it cited, as well as the paper itself.\n",
"\n",
"![](https://data.dgl.ai/tutorial/img/hgnn/equiv.PNG)\n",
"\n",
"Note that a hypergraph constructed this way has an incidence matrix exactly identical to the adjacency matrix of the original graph (plus an identity matrix for self-loops). This is because each hyperedge has a one-to-one correspondence to each paper. So we can directly take the graph's adjacency matrix and add an identity matrix to it, and we use it as the hypergraph's incidence matrix."
],
"metadata": {
"id": "bPrOHVaGwUD0"
}
},
{
"cell_type": "code",
"source": [
"def load_data():\n",
" dataset = CoraGraphDataset()\n",
"\n", "\n",
" graph = dataset[0]\n",
" src, dst = graph.edges()\n",
" H = dglsp.from_coo(dst, src)\n",
" H = H + dglsp.identity(H.shape)\n",
"\n",
" X = graph.ndata[\"feat\"]\n",
" Y = graph.ndata[\"label\"]\n",
" train_mask = graph.ndata[\"train_mask\"]\n",
" val_mask = graph.ndata[\"val_mask\"]\n",
" test_mask = graph.ndata[\"test_mask\"]\n",
" return H, X, Y, dataset.num_classes, train_mask, val_mask, test_mask"
],
"metadata": {
"id": "qI0j1J9pwTFg"
},
"execution_count": 5,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Training and Evaluation\n",
"\n", "\n",
"Now we can write the training and evaluation functions as follows."
],
"metadata": {
"id": "--rq1-r7wMST"
}
},
{
"cell_type": "code",
"source": [
"def train(model, optimizer, X, Y, train_mask):\n", "def train(model, optimizer, X, Y, train_mask):\n",
" model.train()\n", " model.train()\n",
" Y_hat = model(X)\n", " Y_hat = model(X)\n",
...@@ -275,67 +329,42 @@ ...@@ -275,67 +329,42 @@
" return val_acc, test_acc\n", " return val_acc, test_acc\n",
"\n", "\n",
"\n", "\n",
"def load_data():\n", "H, X, Y, num_classes, train_mask, val_mask, test_mask = load_data()\n",
" dataset = CoraGraphDataset()\n", "model = HGNN(H, X.shape[1], num_classes)\n",
"\n", "optimizer = torch.optim.Adam(model.parameters(), lr=0.001)\n",
" graph = dataset[0]\n", "\n",
" # The paper created a hypergraph from the original graph. For each node in\n", "with tqdm.trange(500) as tq:\n",
" # the original graph, a hyperedge in the hypergraph is created to connect\n", " for epoch in tq:\n",
" # its neighbors and itself. In this case, the incidence matrix of the\n", " train(model, optimizer, X, Y, train_mask)\n",
" # hypergraph is the same as the adjacency matrix of the original graph (with\n", " val_acc, test_acc = evaluate(\n",
" # self-loops).\n", " model, X, Y, val_mask, test_mask, num_classes\n",
" # We follow the paper and assume that the rows of the incidence matrix\n", " )\n",
" # are for nodes and the columns are for edges.\n", " tq.set_postfix(\n",
" src, dst = graph.edges()\n", " {\n",
" H = dglsp.from_coo(dst, src)\n", " \"Val acc\": f\"{val_acc:.5f}\",\n",
" H = H + dglsp.identity(H.shape)\n", " \"Test acc\": f\"{test_acc:.5f}\",\n",
"\n", " },\n",
" X = graph.ndata[\"feat\"]\n", " refresh=False,\n",
" Y = graph.ndata[\"label\"]\n", " )\n",
" train_mask = graph.ndata[\"train_mask\"]\n", "\n",
" val_mask = graph.ndata[\"val_mask\"]\n", "print(f\"Test acc: {test_acc:.3f}\")"
" test_mask = graph.ndata[\"test_mask\"]\n",
" return H, X, Y, dataset.num_classes, train_mask, val_mask, test_mask\n",
"\n",
"\n",
"def main():\n",
" H, X, Y, num_classes, train_mask, val_mask, test_mask = load_data()\n",
" model = HGNN(H, X.shape[1], num_classes)\n",
" optimizer = torch.optim.Adam(model.parameters(), lr=0.001)\n",
"\n",
" with tqdm.trange(500) as tq:\n",
" for epoch in tq:\n",
" train(model, optimizer, X, Y, train_mask)\n",
" val_acc, test_acc = evaluate(\n",
" model, X, Y, val_mask, test_mask, num_classes\n",
" )\n",
" tq.set_postfix(\n",
" {\n",
" \"Val acc\": f\"{val_acc:.5f}\",\n",
" \"Test acc\": f\"{test_acc:.5f}\",\n",
" },\n",
" refresh=False,\n",
" )\n",
"\n",
" print(f\"Test acc: {test_acc:.3f}\")\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" main()"
], ],
"metadata": { "metadata": {
"id": "58WnPtPvT2mx",
"colab": { "colab": {
"base_uri": "https://localhost:8080/" "base_uri": "https://localhost:8080/"
}, },
"outputId": "e27529b5-a097-4fe2-e1d6-7ae89484dd23" "id": "IfEc6JRXwHPt",
"outputId": "0172578a-6a1b-49eb-adcb-77ee1a949186"
}, },
"execution_count": 9, "execution_count": 6,
"outputs": [ "outputs": [
{ {
"output_type": "stream", "output_type": "stream",
"name": "stdout", "name": "stdout",
"text": [ "text": [
"Downloading /root/.dgl/cora_v2.zip from https://data.dgl.ai/dataset/cora_v2.zip...\n",
"Extracting file to /root/.dgl/cora_v2\n",
"Finished data loading and preprocessing.\n",
" NumNodes: 2708\n", " NumNodes: 2708\n",
" NumEdges: 10556\n", " NumEdges: 10556\n",
" NumFeats: 1433\n", " NumFeats: 1433\n",
...@@ -343,21 +372,21 @@ ...@@ -343,21 +372,21 @@
" NumTrainingSamples: 140\n", " NumTrainingSamples: 140\n",
" NumValidationSamples: 500\n", " NumValidationSamples: 500\n",
" NumTestSamples: 1000\n", " NumTestSamples: 1000\n",
"Done loading data from cached files.\n" "Done saving data into cached files.\n"
] ]
}, },
{ {
"output_type": "stream", "output_type": "stream",
"name": "stderr", "name": "stderr",
"text": [ "text": [
"100%|██████████| 500/500 [00:48<00:00, 10.24it/s, Val acc=0.76600, Test acc=0.76000]" "100%|██████████| 500/500 [00:57<00:00, 8.70it/s, Val acc=0.77800, Test acc=0.78100]"
] ]
}, },
{ {
"output_type": "stream", "output_type": "stream",
"name": "stdout", "name": "stdout",
"text": [ "text": [
"Test acc: 0.760\n" "Test acc: 0.781\n"
] ]
}, },
{ {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment