├── README.md ├── __pycache__ └── tutorial_utils.cpython-38.pyc ├── asset ├── dgl-mp.png ├── dgl-query.png ├── dgl_logo.png ├── docker_images.png ├── docker_pull.png ├── enzymes.png ├── gnn_ep0.png ├── gnn_ep_anime.gif ├── inference.png ├── karat_club.png ├── sagemaker.pdf ├── sagemaker.pptx └── user_guide_graphch_2.png ├── basics ├── .ipynb_checkpoints │ ├── 1_load_data-checkpoint.ipynb │ ├── 2_heterogenous_graph-checkpoint.ipynb │ ├── 3_gnn-checkpoint.ipynb │ ├── 4_link_predict-checkpoint.ipynb │ ├── 5_gpu-checkpoint.ipynb │ ├── 6_message_passing-checkpoint.ipynb │ └── tutorial_utils-checkpoint.py ├── 1_load_data.ipynb ├── 2_heterogenous_graph.ipynb ├── 3_gnn.ipynb ├── 4_link_predict.ipynb ├── 5_gpu.ipynb ├── 6_message_passing.ipynb ├── __pycache__ │ └── tutorial_utils.cpython-38.pyc └── tutorial_utils.py ├── data ├── edges.csv ├── gen_data.py └── nodes.csv └── large_graph ├── .ipynb_checkpoints ├── 1_node_classification-checkpoint.ipynb ├── 2_unsupervised_learning_and_link_prediction-checkpoint.ipynb └── 3_single_machine_multiple_GPU_training-checkpoint.ipynb ├── 1_node_classification.ipynb ├── 2_unsupervised_learning_and_link_prediction.ipynb ├── 3_single_machine_multiple_GPU_training.ipynb ├── assets ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── anim.gif ├── bipartite.png └── seed.png ├── sampling.pptx └── utils.py /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DGL 한국어 튜토리얼 5 | ============================================== 6 | 7 |
drawing
8 | 9 |
10 | 11 | Deep Graph Library(DGL)은 기존의 DL 프레임워크(e.g. PyTorch, MXNet, Gluon 등)위에 그래프 뉴럴 네트워크 모델을 간편하게 구현하기 위한 `Python` 패키지입니다. DGL은 아키텍쳐 디자인 상에서 [NetworkX](https://networkx.org/)의 API와 패러다임을 따르고 지향하고 있습니다. 12 | 13 | 14 | DGL은 그래프 뉴럴넷 구현에서의 Keras로 비유되곤 합니다. 다양한 API 함수들이 제공되고, 다양한 백엔드 프레임워크를 취향에 맞게 사용할 수 있습니다. 해당 튜토리얼에서는, [PyTorch](https://pytorch.org/) 백엔드를 사용한 예제를 제공합니다. 간편하고 직관적인 구현이 DGL의 강점입니다. 15 | 16 | 해당 튜토리얼은 [DGL 공식 API 문서](https://docs.dgl.ai/en/latest/new-tutorial/)와 [WWW20](https://github.com/dglai/WWW20-Hands-on-Tutorial), [KDD20](https://github.com/dglai/KDD20-Hands-on-Tutorial)의 내용을 번역, 재구성하여 작성되었으며, DGL 메인 컨트리뷰터 [Minjie Wang](https://github.com/jermainewang)님의 동의와 자문을 구해 만들어 졌음을 밝힙니다. 17 | 18 | 공부하는 과정에서 정리하는 목적에서 만든 자료이기 때문에, 부족한 부분이 많습니다. 19 | 잘못된 내용이나 수정이 필요한 부분은 issue나 PR, 메일로 알려주시면 감사하겠습니다! 20 | 21 | 22 | 23 | ---- 24 | 25 |
26 | 27 | Contents 28 | -------- 29 | 30 | 1. [DGL 기본](/basics/) 31 | 2. [대용량 그래프 데이터 조작하기](/large_graph) 32 | 33 | 34 |
35 | 36 | 37 | ## 시작하기 38 | 39 | 해당 튜토리얼은 jupyter notebook으로 작성되어 있습니다. 간편한 환경 셋팅을 위해 docker 컨테이너를 실행하여 jupyter lab 환경에서 실습을 진행합니다. 40 | 41 | ### git clone 42 | 43 | ``` 44 | git clone https://github.com/myeonghak/DGL-tutorial.git 45 | cd DGL-tutorial 46 | ``` 47 | 48 | ### docker setting 49 | docker를 사용해 실습 환경을 셋팅합니다. 50 | 로컬 환경에서의 셋팅은 [여기](https://docs.dgl.ai/en/0.4.x/install/)를 참고해 주세요. 51 | 52 | 53 | **1. 도커 이미지 가져오기** 54 | ``` 55 | docker pull nilsine11202/dgl-tutorial:1.0 56 | ``` 57 |
drawing
58 | image pull이 잘 이루어 졌다면, 아래와 같은 내용을 확인해 볼 수 있습니다. 59 | 60 | 61 | ``` 62 | docker images 63 | ``` 64 | 65 | 66 |
drawing
67 | 68 | 69 | 70 |
71 | 72 | 73 | 74 | **2. 컨테이너 실행** 75 | 76 | 받아온 이미지를 사용해 컨테이너를 실행합니다. 77 | 78 | ``` 79 | docker run --runtime nvidia -it --name dgl_tuto -p 8885:8885 -v /home:/workspace -d nilsine11202/dgl-tutorial:1.0 /bin/bash 80 | 81 | # docker run --runtime nvidia -it --name dgl_tuto --shm-size 128G -p 8885:8885 -v /home:/workspace -d nilsine11202/dgl-tutorial:1.0 /bin/bash 82 | # (large-graph 예제 실행시 Bus error (core dumped) model share memory 에러가 발생할 경우, 위처럼 --shm-size 인자로 도커 컨테이너의 shared memory를 늘림으로써 해결할 수 있습니다) 83 | 84 | ``` 85 | `dgl_tuto`: 컨테이너의 이름으로 사용한 임의의 명칭입니다. 원하시는 이름으로 바꾸어 사용하세요. 86 | `8885:8885`: jupyter lab 포팅을 위한 포트를 지정해 줍니다. 원하는 포트로 바꾸어 사용할 수 있습니다. 로컬호스트가 사용하지 않을 법한 포트명을 임의로 지정해 주었습니다. 87 | `/home:/workspace`: docker 컨테이너 내부의 `/home` 디렉터리를 로컬의 `/workspace` 디렉터리로 맵핑해 주었습니다. 역시 원하는 디렉터리로 바꾸어 사용하셔도 됩니다. 88 | 89 |
90 | 91 | **3. 컨테이너로 배시 실행** 92 | ``` 93 | docker exec -it dgl_tuto bash 94 | ``` 95 | 실행이 성공하면, 컨테이너의 bash에 접근할 수 있습니다. 96 | 97 |
98 | 99 | **4. 주피터 랩 실행하기** 100 | ``` 101 | jupyter lab --ip=0.0.0.0 --port=8885 --allow-root 102 | ``` 103 | docker 컨테이너의 배시에서 위의 커맨드를 실행하면, 주피터 환경에 접근할 수 있습니다. 104 | 105 | 106 |
107 | 108 | 109 | ## TO DO 110 | 111 | * custom graph dataset 만들기 112 | * graph visualization 113 | * local 환경 셋팅 114 | * 추천 시스템 예제 적용 115 | -------------------------------------------------------------------------------- /__pycache__/tutorial_utils.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/__pycache__/tutorial_utils.cpython-38.pyc -------------------------------------------------------------------------------- /asset/dgl-mp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/dgl-mp.png -------------------------------------------------------------------------------- /asset/dgl-query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/dgl-query.png -------------------------------------------------------------------------------- /asset/dgl_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/dgl_logo.png -------------------------------------------------------------------------------- /asset/docker_images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/docker_images.png -------------------------------------------------------------------------------- /asset/docker_pull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/docker_pull.png -------------------------------------------------------------------------------- /asset/enzymes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/enzymes.png -------------------------------------------------------------------------------- /asset/gnn_ep0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/gnn_ep0.png -------------------------------------------------------------------------------- /asset/gnn_ep_anime.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/gnn_ep_anime.gif -------------------------------------------------------------------------------- /asset/inference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/inference.png -------------------------------------------------------------------------------- /asset/karat_club.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/karat_club.png -------------------------------------------------------------------------------- /asset/sagemaker.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/sagemaker.pdf -------------------------------------------------------------------------------- /asset/sagemaker.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/sagemaker.pptx -------------------------------------------------------------------------------- /asset/user_guide_graphch_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/asset/user_guide_graphch_2.png -------------------------------------------------------------------------------- /basics/.ipynb_checkpoints/2_heterogenous_graph-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# `Heterogenous_graph`" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [ 15 | { 16 | "name": "stderr", 17 | "output_type": "stream", 18 | "text": [ 19 | "Using backend: pytorch\n" 20 | ] 21 | } 22 | ], 23 | "source": [ 24 | "import dgl\n", 25 | "import torch" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "이질적(heterogenous) 그래프의 경우, 서로 다른 관계에 놓인 노드들은 소스(source) 노드가 되거나 목적지(destination) 노드가 됩니다. \n", 33 | "`srcdata`와 `dstdata`는 (이름에서 source data, destination data가 보이죠?) 특히 이 두 타입의 노드에 저장되어 있습니다. \n", 34 | "이질적 그래프에 대한 더 자세한 내용을 알고 싶으시면, [DGL User Guide 1.5 Heterogeneous Graphs](https://docs.dgl.ai/guide/graph-heterogeneous.html#guide-graph-heterogeneous) 를 확인해 주세요." 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "![image.png](../asset/user_guide_graphch_2.png)" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "이질적 그래프는 다양한 타입의 노드와 엣지를 가질 수 있습니다. \n", 49 | "특정 타입의 노드/엣지는 각각의 독립적인 ID space와 피처 공간을 갖습니다. \n", 50 | "예를 들어, 위의 그림의 경우 user와 game 노드의 아이디는 각각 0부터 시작하고(독립적인 ID space), 각각 다른 피처를 갖습니다(독립적인 피처 공간). \n", 51 | "\n", 52 | "\n", 53 | "위의 예시에는 2개의 노드 타입(user, game)과 2개의 엣지 타입(follow, plays)이 있군요. \n", 54 | "유저끼리는 follow를 할것이고, 유저와 게임 사이에는 play라는 관계가 나오겠죠? \n" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 2, 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "name": "stdout", 64 | "output_type": "stream", 65 | "text": [ 66 | "Graph(num_nodes={'disease': 3, 'drug': 3, 'gene': 4},\n", 67 | " num_edges={('drug', 'interacts', 'drug'): 2, ('drug', 'interacts', 'gene'): 2, ('drug', 'treats', 'disease'): 1},\n", 68 | " metagraph=[('drug', 'drug', 'interacts'), ('drug', 'gene', 'interacts'), ('drug', 'disease', 'treats')])\n" 69 | ] 70 | } 71 | ], 72 | "source": [ 73 | "# 3개의 노드 타입과 3개의 엣지 타입을 가진 이질적 그래프를 생성합니다\n", 74 | "# key 값의 3개 값 중 가운데는 \"관계\"를 나타내고, 양쪽은 source/destination node를 각각 나타냅니다.\n", 75 | "# value 값의 부분에 있는 2 덩이의 torch.tensor는 각각 source/destination 노드의 피처를 의미합니다.\n", 76 | "\n", 77 | "heterograph_data = {\n", 78 | " ('drug', 'interacts', 'drug'): (torch.tensor([0, 1]), torch.tensor([1, 2])),\n", 79 | " ('drug', 'interacts', 'gene'): (torch.tensor([0, 1]), torch.tensor([2, 3])),\n", 80 | " ('drug', 'treats', 'disease'): (torch.tensor([1]), torch.tensor([2]))\n", 81 | "}\n", 82 | "hetero_g = dgl.heterograph(heterograph_data)\n", 83 | "print(hetero_g)" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 3, 89 | "metadata": {}, 90 | "outputs": [ 91 | { 92 | "data": { 93 | "text/plain": [ 94 | "Graph(num_nodes={'disease': 3, 'drug': 3, 'gene': 4},\n", 95 | " num_edges={('drug', 'interacts', 'drug'): 2, ('drug', 'interacts', 'gene'): 2, ('drug', 'treats', 'disease'): 1},\n", 96 | " metagraph=[('drug', 'drug', 'interacts'), ('drug', 'gene', 'interacts'), ('drug', 'disease', 'treats')])" 97 | ] 98 | }, 99 | "execution_count": 3, 100 | "metadata": {}, 101 | "output_type": "execute_result" 102 | } 103 | ], 104 | "source": [ 105 | "hetero_g" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 4, 111 | "metadata": {}, 112 | "outputs": [ 113 | { 114 | "name": "stdout", 115 | "output_type": "stream", 116 | "text": [ 117 | "Graph(num_nodes={'drug': 3, 'gene': 4},\n", 118 | " num_edges={('drug', 'interacts', 'gene'): 2},\n", 119 | " metagraph=[('drug', 'gene', 'interacts')])\n" 120 | ] 121 | } 122 | ], 123 | "source": [ 124 | "# 한 관계 'durg->interacts->gene'를 추출해 sub graph를 만듭니다.\n", 125 | "sub_g = dgl.edge_type_subgraph(hetero_g, [('drug', 'interacts', 'gene')])\n", 126 | "print(sub_g)" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 5, 132 | "metadata": {}, 133 | "outputs": [ 134 | { 135 | "name": "stdout", 136 | "output_type": "stream", 137 | "text": [ 138 | "Graph(num_nodes={'drug': 3, 'gene': 4},\n", 139 | " num_edges={('drug', 'interacts', 'gene'): 2},\n", 140 | " metagraph=[('drug', 'gene', 'interacts')])\n" 141 | ] 142 | } 143 | ], 144 | "source": [ 145 | "# 소스와 목적지 노드에 피처를 할당합니다. drug 노드와 gene 노드의 수가 다르다는 점에 주목해 주세요.\n", 146 | "\n", 147 | "sub_g.srcdata['src_h'] = torch.randn(3,3)\n", 148 | "sub_g.dstdata['dst_h'] = torch.randn(4,2)\n", 149 | "print(sub_g)" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 6, 155 | "metadata": {}, 156 | "outputs": [ 157 | { 158 | "data": { 159 | "text/plain": [ 160 | "{'src_h': tensor([[-1.2368, -0.4933, -1.4143],\n", 161 | " [ 0.8217, 0.9421, -0.8660],\n", 162 | " [-0.1927, -0.1469, -1.1906]])}" 163 | ] 164 | }, 165 | "execution_count": 6, 166 | "metadata": {}, 167 | "output_type": "execute_result" 168 | } 169 | ], 170 | "source": [ 171 | "sub_g.srcdata" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 7, 177 | "metadata": {}, 178 | "outputs": [ 179 | { 180 | "data": { 181 | "text/plain": [ 182 | "{'dst_h': tensor([[-1.0101, -1.4877],\n", 183 | " [-0.6508, -1.2819],\n", 184 | " [ 0.7221, 0.8284],\n", 185 | " [ 1.5760, -0.2282]])}" 186 | ] 187 | }, 188 | "execution_count": 7, 189 | "metadata": {}, 190 | "output_type": "execute_result" 191 | } 192 | ], 193 | "source": [ 194 | "sub_g.dstdata" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "`srcdata`와 `dstdata`에 대한 더 많은 사용법은 5_message_passing과 large graph task 튜토리얼에서 확인하실 수 있습니다." 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": null, 207 | "metadata": {}, 208 | "outputs": [], 209 | "source": [] 210 | } 211 | ], 212 | "metadata": { 213 | "kernelspec": { 214 | "display_name": "Python 3", 215 | "language": "python", 216 | "name": "python3" 217 | }, 218 | "language_info": { 219 | "codemirror_mode": { 220 | "name": "ipython", 221 | "version": 3 222 | }, 223 | "file_extension": ".py", 224 | "mimetype": "text/x-python", 225 | "name": "python", 226 | "nbconvert_exporter": "python", 227 | "pygments_lexer": "ipython3", 228 | "version": "3.8.3" 229 | } 230 | }, 231 | "nbformat": 4, 232 | "nbformat_minor": 4 233 | } 234 | -------------------------------------------------------------------------------- /basics/.ipynb_checkpoints/4_link_predict-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 그래프 뉴럴 네트워크를 사용한 링크 예측\n", 8 | "\n", 9 | "GNN은 그래프 데이터에 대한 많은 머신러닝 task를 해결하는 데 강력한 툴입니다. \n", 10 | "이 튜토리얼에서는, 링크 예측을 위해 GNN을 사용하는 기본적인 워크플로우를 배울 수 있습니다. \n", 11 | "여기서 Zachery의 카라테 클럽 그래프를 다시 사용합니다. 하지만, 이번에는 두 멤버 사이의 관계를 예측하는 작업을 시도해 봅니다.\n", 12 | "\n", 13 | "이 튜토리얼에서, 다음을 배울 수 있습니다.\n", 14 | "* 링크 예측을 위한 학습/테스트 셋을 준비하는 방법\n", 15 | "* GNN 기반 링크 예측 모델을 구축하는 법\n", 16 | "* 모델을 학습하고, 그 결과를 검증하는 법\n" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "metadata": {}, 23 | "outputs": [ 24 | { 25 | "name": "stderr", 26 | "output_type": "stream", 27 | "text": [ 28 | "Using backend: pytorch\n" 29 | ] 30 | } 31 | ], 32 | "source": [ 33 | "import dgl\n", 34 | "import torch\n", 35 | "import torch.nn as nn\n", 36 | "import torch.nn.functional as F\n", 37 | "import itertools\n", 38 | "import numpy as np\n", 39 | "import scipy.sparse as sp" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "## 그래프와 피처 로드\n", 47 | "\n", 48 | "최근 튜토리얼 [세션](./3_gnn.ipynb)에 이어, Zachery의 카라테 클럽 그래프를 불러들여 노드 임베딩을 만듭니다." 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 2, 54 | "metadata": {}, 55 | "outputs": [ 56 | { 57 | "name": "stdout", 58 | "output_type": "stream", 59 | "text": [ 60 | "Graph(num_nodes=34, num_edges=156,\n", 61 | " ndata_schemes={'club': Scheme(shape=(), dtype=torch.int64), 'club_onehot': Scheme(shape=(2,), dtype=torch.int64)}\n", 62 | " edata_schemes={})\n" 63 | ] 64 | }, 65 | { 66 | "data": { 67 | "text/plain": [ 68 | "Parameter containing:\n", 69 | "tensor([[ 0.3916, -0.0327, -0.1260, -0.2869, 0.2973],\n", 70 | " [ 0.3182, 0.0220, 0.1522, -0.2716, 0.2918],\n", 71 | " [ 0.1206, -0.1165, -0.3727, 0.0779, -0.1156],\n", 72 | " [-0.1390, -0.1939, 0.1905, -0.3710, 0.3080],\n", 73 | " [ 0.1564, -0.1069, -0.0932, -0.0339, -0.0070],\n", 74 | " [-0.1578, 0.2431, -0.0528, 0.0016, 0.0280],\n", 75 | " [ 0.0519, -0.3260, -0.1438, -0.0300, -0.3647],\n", 76 | " [-0.2717, -0.0301, -0.1534, 0.1794, -0.1963],\n", 77 | " [ 0.3325, -0.1624, 0.0535, 0.1135, 0.0627],\n", 78 | " [-0.1526, -0.2020, 0.3215, 0.2962, 0.1816],\n", 79 | " [-0.3414, -0.1588, 0.3631, 0.2354, 0.0751],\n", 80 | " [ 0.0152, 0.0471, 0.3071, -0.2435, 0.2512],\n", 81 | " [-0.2102, -0.2189, -0.3463, -0.3863, -0.3508],\n", 82 | " [-0.3871, 0.2010, 0.2501, -0.3321, -0.2337],\n", 83 | " [ 0.1315, -0.2012, -0.0101, -0.3694, 0.2327],\n", 84 | " [ 0.1247, 0.3643, 0.2281, 0.3333, 0.2420],\n", 85 | " [ 0.0464, 0.0643, 0.2621, -0.0140, -0.3357],\n", 86 | " [-0.2679, 0.1186, -0.0057, 0.0680, -0.3183],\n", 87 | " [ 0.2513, 0.2763, -0.0192, 0.2784, 0.2850],\n", 88 | " [-0.2071, 0.3648, -0.2017, 0.0965, -0.2182],\n", 89 | " [ 0.0360, 0.1442, 0.2142, 0.1483, 0.0775],\n", 90 | " [-0.1826, -0.1986, -0.2250, 0.2691, 0.1778],\n", 91 | " [ 0.0243, 0.3686, 0.0827, 0.2662, -0.3841],\n", 92 | " [ 0.1415, 0.1779, -0.2522, -0.0017, 0.3368],\n", 93 | " [-0.1565, 0.3861, 0.2884, 0.0710, 0.0044],\n", 94 | " [-0.3624, 0.2424, -0.3842, 0.3670, -0.3307],\n", 95 | " [-0.2098, -0.2707, -0.0492, 0.1154, 0.2686],\n", 96 | " [-0.3806, 0.0607, 0.3294, 0.1253, 0.0544],\n", 97 | " [-0.3274, 0.0050, 0.2674, 0.1631, 0.1791],\n", 98 | " [ 0.3784, 0.0215, -0.2735, -0.2311, -0.2277],\n", 99 | " [ 0.2785, 0.3565, 0.1780, 0.3515, -0.3060],\n", 100 | " [ 0.3370, 0.2554, 0.2643, -0.2294, -0.0243],\n", 101 | " [-0.3900, -0.0826, 0.3686, -0.1113, -0.2340],\n", 102 | " [-0.0703, -0.0797, 0.3513, 0.3304, -0.2040]], requires_grad=True)" 103 | ] 104 | }, 105 | "execution_count": 2, 106 | "metadata": {}, 107 | "output_type": "execute_result" 108 | } 109 | ], 110 | "source": [ 111 | "from tutorial_utils import load_zachery\n", 112 | "\n", 113 | "# ----------- 0. load graph -------------- #\n", 114 | "g = load_zachery()\n", 115 | "print(g)\n", 116 | "\n", 117 | "# ----------- 1. node features -------------- #\n", 118 | "node_embed = nn.Embedding(g.number_of_nodes(), 5) # 각 노드는 5차원의 임베딩을 가지고 있습니다.\n", 119 | "inputs = node_embed.weight # 노드 피처로써 이 임베딩 가중치를 사용합니다.\n", 120 | "nn.init.xavier_uniform_(inputs)" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "## 학습/테스트 셋을 준비 합니다" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "일반적으로, 링크 예측 데이터셋은 *positive*와 *negative* 엣지라는 2 타입의 엣지를 포함하고 있습니다. \n", 135 | "positive 엣지는 보통 그래프 내에 이미 존재하는 엣지로부터 가져옵니다. \n", 136 | "이 예제에서, 50개의 임의의 엣지를 골라 테스트에 사용하고 나머지는 학습에 사용합니다." 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 3, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "# 학습과 테스트를 위해 엣지 셋을 분할합니다.\n", 146 | "u, v = g.edges()\n", 147 | "eids = np.arange(g.number_of_edges())\n", 148 | "eids = np.random.permutation(eids)\n", 149 | "test_pos_u, test_pos_v = u[eids[:50]], v[eids[:50]]\n", 150 | "train_pos_u, train_pos_v = u[eids[50:]], v[eids[50:]]" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "negative 엣지의 수가 크기때문에, 보통 샘플링 해주는 것이 좋습니다. \n", 158 | "적절한 negative 샘플링 알고리즘을 선택하는 방법에 대한 문제는 널리 연구되는 주제로, 이 튜토리얼의 범위를 벗어납니다. \n", 159 | "우리의 예제 그래프는 상당히 작기 때문에(노드 34개뿐), 모든 결측 엣지를 나열해 임의로 50개를 테스트에 사용하고, 150개를 학습에 사용합니다.\n" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": 4, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "# 모든 negative 엣지를 찾아 학습과 테스트용으로 분할\n", 169 | "adj = sp.coo_matrix((np.ones(len(u)), (u.numpy(), v.numpy())))\n", 170 | "adj_neg = 1 - adj.todense() - np.eye(34)\n", 171 | "neg_u, neg_v = np.where(adj_neg != 0)\n", 172 | "neg_eids = np.random.choice(len(neg_u), 200)\n", 173 | "test_neg_u, test_neg_v = neg_u[neg_eids[:50]], neg_v[neg_eids[:50]]\n", 174 | "train_neg_u, train_neg_v = neg_u[neg_eids[50:]], neg_v[neg_eids[50:]]" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "Put positive and negative edges together and form training and testing sets." 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 5, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "# Create training set.\n", 191 | "train_u = torch.cat([torch.as_tensor(train_pos_u), torch.as_tensor(train_neg_u)])\n", 192 | "train_v = torch.cat([torch.as_tensor(train_pos_v), torch.as_tensor(train_neg_v)])\n", 193 | "train_label = torch.cat([torch.zeros(len(train_pos_u)), torch.ones(len(train_neg_u))])\n", 194 | "\n", 195 | "# Create testing set.\n", 196 | "test_u = torch.cat([torch.as_tensor(test_pos_u), torch.as_tensor(test_neg_u)])\n", 197 | "test_v = torch.cat([torch.as_tensor(test_pos_v), torch.as_tensor(test_neg_v)])\n", 198 | "test_label = torch.cat([torch.zeros(len(test_pos_u)), torch.ones(len(test_neg_u))])" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": {}, 204 | "source": [ 205 | "## GraphSAGE 모델 정의하기\n", 206 | "\n", 207 | "우리의 모델은 2개 레이어로 구성되어 있는데, 각각 새로운 노드 표현(representation)을 이웃의 정보를 통합함으로써 계산합니다. \n", 208 | "수식은 다음과 같습니다. ([이전 튜토리얼](./3_gnn.ipynb)과 약간 다릅니다.)\n", 209 | "\n", 210 | "$$\n", 211 | "h_{\\mathcal{N}(v)}^k\\leftarrow \\text{AGGREGATE}_k\\{h_u^{k-1},\\forall u\\in\\mathcal{N}(v)\\}\n", 212 | "$$\n", 213 | "\n", 214 | "$$\n", 215 | "h_v^k\\leftarrow \\text{ReLU}\\left(W^k\\cdot \\text{CONCAT}(h_v^{k-1}, h_{\\mathcal{N}(v)}^k) \\right)\n", 216 | "$$\n", 217 | "\n", 218 | "DGL은 많은 유명한 이웃 통합(neighbor aggregation) 모듈의 구현체를 제공합니다. 모두 쉽게 한 줄의 코드로 호출하여 사용할 수 있습니다. \n", 219 | "지원되는 모델의 전체 리스트는 [graph convolution modules](https://docs.dgl.ai/api/python/nn.pytorch.html#module-dgl.nn.pytorch.conv)에서 보실 수 있습니다." 220 | ] 221 | }, 222 | { 223 | "cell_type": "code", 224 | "execution_count": 6, 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "from dgl.nn import SAGEConv\n", 229 | "\n", 230 | "# ----------- 2. create model -------------- #\n", 231 | "# 2개의 레이어를 가진 GraphSAGE 모델 구축\n", 232 | "class GraphSAGE(nn.Module):\n", 233 | " def __init__(self, in_feats, h_feats):\n", 234 | " super(GraphSAGE, self).__init__()\n", 235 | " self.conv1 = SAGEConv(in_feats, h_feats, 'mean')\n", 236 | " self.conv2 = SAGEConv(h_feats, h_feats, 'mean')\n", 237 | " \n", 238 | " def forward(self, g, in_feat):\n", 239 | " h = self.conv1(g, in_feat)\n", 240 | " h = F.relu(h)\n", 241 | " h = self.conv2(g, h)\n", 242 | " return h\n", 243 | " \n", 244 | "# 주어진 차원의 모델 생성\n", 245 | "# 인풋 레이어 차원: 5, 노드 임베딩\n", 246 | "# 히든 레이어 차원: 16\n", 247 | "net = GraphSAGE(5, 16)" 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "그 뒤, 모델을 아래의 손실함수를 사용해 최적화합니다.\n", 255 | "\n", 256 | "$$\n", 257 | "\\hat{y}_{u\\sim v} = \\sigma(h_u^T h_v)\n", 258 | "$$\n", 259 | "\n", 260 | "$$\n", 261 | "\\mathcal{L} = -\\sum_{u\\sim v\\in \\mathcal{D}}\\left( y_{u\\sim v}\\log(\\hat{y}_{u\\sim v}) + (1-y_{u\\sim v})\\log(1-\\hat{y}_{u\\sim v})) \\right)\n", 262 | "$$\n", 263 | "\n", 264 | "기본적으로, 모델은 엣지의 두 끝지점(노드)의 표현을 내적함으로써 각 엣지에 대한 점수를 계산합니다. \n", 265 | "그 뒤 타겟 y가 0 혹은 1인 binary cross entropy loss를 계산합니다. 여기서 0 혹은 1은 해당 엣지가 positive인지 아닌지를 나타냅니다.\n" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": 7, 271 | "metadata": {}, 272 | "outputs": [ 273 | { 274 | "name": "stdout", 275 | "output_type": "stream", 276 | "text": [ 277 | "In epoch 0, loss: 2.790684938430786\n", 278 | "In epoch 5, loss: 0.699927568435669\n", 279 | "In epoch 10, loss: 0.6101277470588684\n", 280 | "In epoch 15, loss: 0.577601969242096\n", 281 | "In epoch 20, loss: 0.5298259854316711\n", 282 | "In epoch 25, loss: 0.46006080508232117\n", 283 | "In epoch 30, loss: 0.3879762589931488\n", 284 | "In epoch 35, loss: 0.3463674783706665\n", 285 | "In epoch 40, loss: 0.32085341215133667\n", 286 | "In epoch 45, loss: 0.29568690061569214\n", 287 | "In epoch 50, loss: 0.27307477593421936\n", 288 | "In epoch 55, loss: 0.25122061371803284\n", 289 | "In epoch 60, loss: 0.2311725616455078\n", 290 | "In epoch 65, loss: 0.21075379848480225\n", 291 | "In epoch 70, loss: 0.19010187685489655\n", 292 | "In epoch 75, loss: 0.1696164608001709\n", 293 | "In epoch 80, loss: 0.15120071172714233\n", 294 | "In epoch 85, loss: 0.13269738852977753\n", 295 | "In epoch 90, loss: 0.11472604423761368\n", 296 | "In epoch 95, loss: 0.09768754243850708\n" 297 | ] 298 | } 299 | ], 300 | "source": [ 301 | "# ----------- 3. set up loss and optimizer -------------- #\n", 302 | "# 이 경우, 학습 루프의 손실\n", 303 | "optimizer = torch.optim.Adam(itertools.chain(net.parameters(), node_embed.parameters()), lr=0.01)\n", 304 | "\n", 305 | "# ----------- 4. training -------------------------------- #\n", 306 | "all_logits = []\n", 307 | "for e in range(100):\n", 308 | " # forward\n", 309 | " logits = net(g, inputs)\n", 310 | " pred = torch.sigmoid((logits[train_u] * logits[train_v]).sum(dim=1))\n", 311 | " \n", 312 | " # 손실 계산\n", 313 | " loss = F.binary_cross_entropy(pred, train_label)\n", 314 | " \n", 315 | " # backward\n", 316 | " optimizer.zero_grad()\n", 317 | " loss.backward()\n", 318 | " optimizer.step()\n", 319 | " all_logits.append(logits.detach())\n", 320 | " \n", 321 | " if e % 5 == 0:\n", 322 | " print('In epoch {}, loss: {}'.format(e, loss))" 323 | ] 324 | }, 325 | { 326 | "cell_type": "markdown", 327 | "metadata": {}, 328 | "source": [ 329 | "결과를 확인해 봅니다." 330 | ] 331 | }, 332 | { 333 | "cell_type": "code", 334 | "execution_count": 8, 335 | "metadata": {}, 336 | "outputs": [ 337 | { 338 | "name": "stdout", 339 | "output_type": "stream", 340 | "text": [ 341 | "Accuracy 0.83\n" 342 | ] 343 | } 344 | ], 345 | "source": [ 346 | "# ----------- 5. check results ------------------------ #\n", 347 | "pred = torch.sigmoid((logits[test_u] * logits[test_v]).sum(dim=1))\n", 348 | "print('Accuracy', ((pred >= 0.5) == test_label).sum().item() / len(pred))" 349 | ] 350 | }, 351 | { 352 | "cell_type": "code", 353 | "execution_count": null, 354 | "metadata": {}, 355 | "outputs": [], 356 | "source": [] 357 | } 358 | ], 359 | "metadata": { 360 | "kernelspec": { 361 | "display_name": "Python 3", 362 | "language": "python", 363 | "name": "python3" 364 | }, 365 | "language_info": { 366 | "codemirror_mode": { 367 | "name": "ipython", 368 | "version": 3 369 | }, 370 | "file_extension": ".py", 371 | "mimetype": "text/x-python", 372 | "name": "python", 373 | "nbconvert_exporter": "python", 374 | "pygments_lexer": "ipython3", 375 | "version": "3.8.3" 376 | } 377 | }, 378 | "nbformat": 4, 379 | "nbformat_minor": 4 380 | } 381 | -------------------------------------------------------------------------------- /basics/.ipynb_checkpoints/5_gpu-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# GPU를 사용해 학습 가속하기\n", 8 | "\n", 9 | "이번 튜토리얼에서, 다음을 배우게 됩니다.\n", 10 | "\n", 11 | "* 그래프와 피처 데이터를 GPU로 복사하는 방법\n", 12 | "* GNN 모델을 GPU 위에 학습하는 방법\n" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 1, 18 | "metadata": {}, 19 | "outputs": [ 20 | { 21 | "name": "stderr", 22 | "output_type": "stream", 23 | "text": [ 24 | "Using backend: pytorch\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "import dgl\n", 30 | "import torch\n", 31 | "import torch.nn as nn\n", 32 | "import torch.nn.functional as F\n", 33 | "import itertools" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "## 그래프와 피처 데이터를 GPU에 복사\n", 41 | "\n", 42 | "먼저 이전 세션에 사용한 Zachery의 카라테 클럽 그래프와 노드 라벨를 로드합니다." 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 2, 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "name": "stdout", 52 | "output_type": "stream", 53 | "text": [ 54 | "Graph(num_nodes=34, num_edges=156,\n", 55 | " ndata_schemes={'club': Scheme(shape=(), dtype=torch.int64), 'club_onehot': Scheme(shape=(2,), dtype=torch.int64)}\n", 56 | " edata_schemes={})\n" 57 | ] 58 | } 59 | ], 60 | "source": [ 61 | "from tutorial_utils import load_zachery\n", 62 | "\n", 63 | "# ----------- 0. load graph -------------- #\n", 64 | "g = load_zachery()\n", 65 | "print(g)" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": {}, 71 | "source": [ 72 | "이제 그래프와 모든 그래프의 피처 데이터는 CPU에 적재되어 있습니다. `to` API를 사용해 다른 연산장치로 복사해 보세요." 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 3, 78 | "metadata": {}, 79 | "outputs": [ 80 | { 81 | "name": "stdout", 82 | "output_type": "stream", 83 | "text": [ 84 | "Current device: cpu\n", 85 | "New device: cuda:0\n" 86 | ] 87 | } 88 | ], 89 | "source": [ 90 | "print('Current device:', g.device)\n", 91 | "g = g.to('cuda:0')\n", 92 | "print('New device:', g.device)" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "Verify that features are also copied to GPU." 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 4, 105 | "metadata": {}, 106 | "outputs": [ 107 | { 108 | "name": "stdout", 109 | "output_type": "stream", 110 | "text": [ 111 | "cuda:0\n", 112 | "cuda:0\n" 113 | ] 114 | } 115 | ], 116 | "source": [ 117 | "print(g.ndata['club'].device)\n", 118 | "print(g.ndata['club_onehot'].device)" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "## GNN 모델을 GPU에 생성하기\n", 126 | "\n", 127 | "이 스텝은 CNN이나 RNN 모델을 GPU에 생성하는 것과 같습니다. \n", 128 | "PyTorch에서, `to` API를 사용해 이를 수행할 수 있습니다." 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 5, 134 | "metadata": {}, 135 | "outputs": [ 136 | { 137 | "data": { 138 | "text/plain": [ 139 | "Parameter containing:\n", 140 | "tensor([[-0.1941, 0.3631, -0.2146, -0.0416, -0.3040],\n", 141 | " [-0.2014, 0.3393, -0.1062, -0.2229, 0.3331],\n", 142 | " [-0.2843, 0.3027, 0.2389, -0.0853, 0.3283],\n", 143 | " [ 0.0482, 0.1986, -0.2904, 0.0818, -0.1860],\n", 144 | " [-0.0844, -0.3876, 0.1654, -0.2600, 0.3482],\n", 145 | " [ 0.1354, -0.1090, 0.0389, 0.2281, -0.2484],\n", 146 | " [-0.3592, 0.1807, -0.2933, -0.2188, 0.2301],\n", 147 | " [ 0.2413, 0.3598, -0.2222, -0.2795, -0.1307],\n", 148 | " [-0.3511, -0.1753, 0.2872, -0.2322, 0.0094],\n", 149 | " [ 0.1164, 0.0620, 0.0414, -0.1472, -0.2698],\n", 150 | " [-0.1343, 0.3105, -0.3529, 0.3063, -0.1436],\n", 151 | " [ 0.2085, 0.0502, -0.2477, 0.2870, -0.0683],\n", 152 | " [-0.2910, -0.2569, -0.1903, -0.0875, -0.3270],\n", 153 | " [ 0.1782, 0.2922, -0.2446, -0.0885, -0.3430],\n", 154 | " [-0.0839, 0.0179, 0.2307, -0.1886, 0.0091],\n", 155 | " [-0.2154, -0.2445, 0.2925, 0.1994, -0.3176],\n", 156 | " [-0.0218, -0.1832, -0.2908, 0.3639, -0.3595],\n", 157 | " [ 0.1016, -0.0547, -0.3834, 0.2332, 0.1355],\n", 158 | " [ 0.2532, -0.2284, 0.1160, 0.2327, -0.2220],\n", 159 | " [-0.1158, 0.0133, 0.1556, 0.1818, -0.1308],\n", 160 | " [ 0.1331, -0.0689, -0.2183, 0.0959, -0.2642],\n", 161 | " [ 0.1583, 0.0246, -0.1268, 0.3539, -0.3599],\n", 162 | " [ 0.2765, -0.1886, -0.1546, -0.3603, -0.3806],\n", 163 | " [ 0.3022, 0.0966, 0.0634, -0.3355, 0.1699],\n", 164 | " [ 0.0687, -0.0457, 0.2138, 0.3206, 0.3198],\n", 165 | " [-0.1122, 0.2960, -0.3739, -0.2899, 0.3898],\n", 166 | " [ 0.1120, 0.2343, -0.2354, -0.1214, 0.3795],\n", 167 | " [-0.3419, 0.0163, -0.2615, 0.1877, 0.0776],\n", 168 | " [ 0.3821, 0.3670, 0.2761, -0.2352, 0.2398],\n", 169 | " [ 0.3302, -0.1100, -0.3390, 0.2329, 0.0696],\n", 170 | " [ 0.0951, 0.0089, 0.1248, -0.0494, -0.2868],\n", 171 | " [ 0.0962, 0.1329, 0.2705, -0.0595, 0.2363],\n", 172 | " [ 0.0876, -0.0845, -0.2818, -0.1904, 0.1882],\n", 173 | " [-0.2267, -0.3101, 0.0753, -0.2404, -0.3339]], device='cuda:0',\n", 174 | " requires_grad=True)" 175 | ] 176 | }, 177 | "execution_count": 5, 178 | "metadata": {}, 179 | "output_type": "execute_result" 180 | } 181 | ], 182 | "source": [ 183 | "# ----------- 1. node features -------------- #\n", 184 | "node_embed = nn.Embedding(g.number_of_nodes(), 5) # 각 노드는 5차원의 임베딩을 가지고 있습니다.\n", 185 | "# Copy node embeddings to GPU\n", 186 | "node_embed = node_embed.to('cuda:0')\n", 187 | "inputs = node_embed.weight # 노드 피처로써 이 임베딩 가중치를 사용합니다.\n", 188 | "nn.init.xavier_uniform_(inputs)" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "커뮤니티의 라벨은 `'club'`이라는 노드 피처에 저장되어 있습니다. (0은 instructor의 커뮤니티, 1은 club president의 커뮤니티). \n", 196 | "오로지 0과 33번 노드에만 라벨링 되어 있습니다." 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 6, 202 | "metadata": {}, 203 | "outputs": [ 204 | { 205 | "name": "stdout", 206 | "output_type": "stream", 207 | "text": [ 208 | "Labels tensor([0, 1], device='cuda:0')\n" 209 | ] 210 | } 211 | ], 212 | "source": [ 213 | "labels = g.ndata['club']\n", 214 | "labeled_nodes = [0, 33]\n", 215 | "print('Labels', labels[labeled_nodes])" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "## GraphSAGE 모델 정의하기\n", 223 | "\n", 224 | "우리의 모델은 2개 레이어로 구성되어 있는데, 각각 새로운 노드 표현(representation)을 이웃의 정보를 통합함으로써 계산합니다. \n", 225 | "수식은 다음과 같습니다. \n", 226 | "\n", 227 | "\n", 228 | "$$\n", 229 | "h_{\\mathcal{N}(v)}^k\\leftarrow \\text{AGGREGATE}_k\\{h_u^{k-1},\\forall u\\in\\mathcal{N}(v)\\}\n", 230 | "$$\n", 231 | "\n", 232 | "$$\n", 233 | "h_v^k\\leftarrow \\sigma\\left(W^k\\cdot \\text{CONCAT}(h_v^{k-1}, h_{\\mathcal{N}(v)}^k) \\right)\n", 234 | "$$\n", 235 | "\n", 236 | "DGL은 많은 유명한 이웃 통합(neighbor aggregation) 모듈의 구현체를 제공합니다. 모두 쉽게 한 줄의 코드로 호출하여 사용할 수 있습니다. \n", 237 | "지원되는 모델의 전체 리스트는 [graph convolution modules](https://docs.dgl.ai/api/python/nn.pytorch.html#module-dgl.nn.pytorch.conv)에서 보실 수 있습니다." 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": 7, 243 | "metadata": {}, 244 | "outputs": [], 245 | "source": [ 246 | "from dgl.nn import SAGEConv\n", 247 | "\n", 248 | "# ----------- 2. create model -------------- #\n", 249 | "# 2개의 레이어를 가진 GraphSAGE 모델 구축\n", 250 | "class GraphSAGE(nn.Module):\n", 251 | " def __init__(self, in_feats, h_feats, num_classes):\n", 252 | " super(GraphSAGE, self).__init__()\n", 253 | " self.conv1 = SAGEConv(in_feats, h_feats, 'mean')\n", 254 | " self.conv2 = SAGEConv(h_feats, num_classes, 'mean')\n", 255 | " \n", 256 | " def forward(self, g, in_feat):\n", 257 | " h = self.conv1(g, in_feat)\n", 258 | " h = F.relu(h)\n", 259 | " h = self.conv2(g, h)\n", 260 | " return h\n", 261 | " \n", 262 | "# 주어진 차원의 모델 생성\n", 263 | "# 인풋 레이어 차원: 5, 노드 임베딩\n", 264 | "# 히든 레이어 차원: 16\n", 265 | "# 아웃풋 레이어 차원: 2, 클래스가 2개 있기 때문, 0과 1\n", 266 | "\n", 267 | "net = GraphSAGE(5, 16, 2)" 268 | ] 269 | }, 270 | { 271 | "cell_type": "markdown", 272 | "metadata": {}, 273 | "source": [ 274 | "네트워크를 GPU에 복사함" 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": 8, 280 | "metadata": {}, 281 | "outputs": [], 282 | "source": [ 283 | "net = net.to('cuda:0')" 284 | ] 285 | }, 286 | { 287 | "cell_type": "code", 288 | "execution_count": 9, 289 | "metadata": {}, 290 | "outputs": [ 291 | { 292 | "name": "stdout", 293 | "output_type": "stream", 294 | "text": [ 295 | "In epoch 0, loss: 0.8203792572021484\n", 296 | "In epoch 5, loss: 0.4111963212490082\n", 297 | "In epoch 10, loss: 0.21308189630508423\n", 298 | "In epoch 15, loss: 0.08275498449802399\n", 299 | "In epoch 20, loss: 0.03401576727628708\n", 300 | "In epoch 25, loss: 0.01440766267478466\n", 301 | "In epoch 30, loss: 0.006833690218627453\n", 302 | "In epoch 35, loss: 0.0037461717147380114\n", 303 | "In epoch 40, loss: 0.0023471189197152853\n", 304 | "In epoch 45, loss: 0.0016530591528862715\n", 305 | "In epoch 50, loss: 0.0012706018751487136\n", 306 | "In epoch 55, loss: 0.0010407611262053251\n", 307 | "In epoch 60, loss: 0.0008915828075259924\n", 308 | "In epoch 65, loss: 0.0007876282907091081\n", 309 | "In epoch 70, loss: 0.000710575666744262\n", 310 | "In epoch 75, loss: 0.0006503689801320434\n", 311 | "In epoch 80, loss: 0.000602009822614491\n", 312 | "In epoch 85, loss: 0.0005602584569714963\n", 313 | "In epoch 90, loss: 0.0005215413984842598\n", 314 | "In epoch 95, loss: 0.000484131567645818\n" 315 | ] 316 | } 317 | ], 318 | "source": [ 319 | "# ----------- 3. set up loss and optimizer -------------- #\n", 320 | "# 이 경우, 학습 루프의 손실\n", 321 | "\n", 322 | "optimizer = torch.optim.Adam(itertools.chain(net.parameters(), node_embed.parameters()), lr=0.01)\n", 323 | "\n", 324 | "# ----------- 4. training -------------------------------- #\n", 325 | "all_logits = []\n", 326 | "for e in range(100):\n", 327 | " # forward\n", 328 | " logits = net(g, inputs)\n", 329 | " \n", 330 | " # 손실 계산\n", 331 | " logp = F.log_softmax(logits, 1)\n", 332 | " loss = F.nll_loss(logp[labeled_nodes], labels[labeled_nodes])\n", 333 | " \n", 334 | " # backward\n", 335 | " optimizer.zero_grad()\n", 336 | " loss.backward()\n", 337 | " optimizer.step()\n", 338 | " all_logits.append(logits.detach())\n", 339 | " \n", 340 | " if e % 5 == 0:\n", 341 | " print('In epoch {}, loss: {}'.format(e, loss))" 342 | ] 343 | }, 344 | { 345 | "cell_type": "code", 346 | "execution_count": 10, 347 | "metadata": {}, 348 | "outputs": [ 349 | { 350 | "name": "stdout", 351 | "output_type": "stream", 352 | "text": [ 353 | "Accuracy 0.9411764705882353\n" 354 | ] 355 | } 356 | ], 357 | "source": [ 358 | "# ----------- 5. check results ------------------------ #\n", 359 | "pred = torch.argmax(logits, axis=1)\n", 360 | "print('Accuracy', (pred == labels).sum().item() / len(pred))" 361 | ] 362 | }, 363 | { 364 | "cell_type": "markdown", 365 | "metadata": {}, 366 | "source": [ 367 | "**한 GPU 메모리에 그래프와 피처 데이터를 적재할 수 없으면 어떻게 하나요?** \n", 368 | "\n", 369 | "* GNN을 천제 그래프에 대해 수행하는 대신에, 몇몇 subgraph에 대해 수행해 수렴시켜보세요.\n", 370 | "* 다른 샘플을 다른 GPU에 올림으로써 더 빠른 가속을 경험해 보세요.\n", 371 | "* 그래프를 여러 머신에 분할하여 분산된 형태로 학습시켜보세요.\n", 372 | "\n", 373 | "추후에 이러한 방법론을 각각 살펴볼 예정입니다." 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": null, 379 | "metadata": {}, 380 | "outputs": [], 381 | "source": [] 382 | } 383 | ], 384 | "metadata": { 385 | "kernelspec": { 386 | "display_name": "Python 3", 387 | "language": "python", 388 | "name": "python3" 389 | }, 390 | "language_info": { 391 | "codemirror_mode": { 392 | "name": "ipython", 393 | "version": 3 394 | }, 395 | "file_extension": ".py", 396 | "mimetype": "text/x-python", 397 | "name": "python", 398 | "nbconvert_exporter": "python", 399 | "pygments_lexer": "ipython3", 400 | "version": "3.8.3" 401 | } 402 | }, 403 | "nbformat": 4, 404 | "nbformat_minor": 4 405 | } 406 | -------------------------------------------------------------------------------- /basics/.ipynb_checkpoints/6_message_passing-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Customize Graph Convolution using Message Passing APIs\n", 8 | "# Message Passing API로 그래프 컨볼루션 커스터마이징하기\n", 9 | "\n", 10 | "\n", 11 | "이전 세션까지, built-in된 [graph convolution modules](https://docs.dgl.ai/api/python/nn.pytorch.html#module-dgl.nn.pytorch.conv)을 사용해 다중 레이어 그래프 뉴럴넷을 구축했습니다. \n", 12 | "하지만, 때때로 이웃 정보를 통합하는 새로운 방법을 개발하고 싶을 수도 있겠죠. \n", 13 | "DGL의 message passing API는 이런 상황을 위해 설계되었습니다. \n", 14 | "\n", 15 | "이 튜토리얼에서, 이런 것들을 배울 수 있습니다. \n", 16 | "\n", 17 | "* DGL의 `nn.SAGEConv` 모듈의 내부는 어떻게 돌아갈까? \n", 18 | "* DGL의 message passing API\n", 19 | "* 새로운 그래프 컨볼루션 모듈 설계하기" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 1, 25 | "metadata": {}, 26 | "outputs": [ 27 | { 28 | "name": "stderr", 29 | "output_type": "stream", 30 | "text": [ 31 | "Using backend: pytorch\n" 32 | ] 33 | } 34 | ], 35 | "source": [ 36 | "import dgl\n", 37 | "import torch\n", 38 | "import torch.nn as nn\n", 39 | "import torch.nn.functional as F" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "## Message passing과 GNN\n", 47 | "\n", 48 | "DGL은 [Gilmer et al.](https://arxiv.org/abs/1704.01212)에 의해 제안된 Message Passing Neural Network에서 고안된 *message passing 패러다임*을 따릅니다. \n", 49 | "기본적으로, 연구진은 많은 GNN 모델이 다음 프레임워크에 들어맞는다는 것을 발견했습니다. \n", 50 | "\n", 51 | "$$\n", 52 | "m_{u\\sim v}^{(l)} = M^{(l)}\\left(h_v^{(l-1)}, h_u^{(l-1)}, e_{u\\sim v}^{(l-1)}\\right)\n", 53 | "$$\n", 54 | "\n", 55 | "$$\n", 56 | "m_{v}^{(l)} = \\sum_{u\\in\\mathcal{N}(v)}m_{u\\sim v}^{(l)}\n", 57 | "$$\n", 58 | "\n", 59 | "$$\n", 60 | "h_v^{(l)} = U^{(l)}\\left(h_v^{(l-1)}, m_v^{(l)}\\right)\n", 61 | "$$\n", 62 | "\n", 63 | "DGL은 $M^{(l)}$ 을 *message function*라 부르며, $\\sum$을 the *reduce function*이라 부릅니다. \n", 64 | "\n", 65 | "여기서 $\\sum$은 어떤 함수든 표현할 수 있으며 꼭 반드시 summation일 필요는 없습니다." 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": {}, 71 | "source": [ 72 | "가령, GraphSAGE 모델은 다음의 수식적인 형태를 갖고 있습니다.\n", 73 | "\n", 74 | "$$\n", 75 | "h_{\\mathcal{N}(v)}^k\\leftarrow \\text{Average}\\{h_u^{k-1},\\forall u\\in\\mathcal{N}(v)\\}\n", 76 | "$$\n", 77 | "\n", 78 | "$$\n", 79 | "h_v^k\\leftarrow \\text{ReLU}\\left(W^k\\cdot \\text{CONCAT}(h_v^{k-1}, h_{\\mathcal{N}(v)}^k) \\right)\n", 80 | "$$\n", 81 | "\n", 82 | "message passing이 유방향적이라는 것을 볼 수 있죠. \n", 83 | "즉, 한 노드 $u$에서 $v$로 보내진 메시지는 반대 방향인 노드 $v$에서 노드 $u$로 보내진 다른 메시지와 꼭 같을 필요는 없다는 말입니다. \n", 84 | "\n", 85 | "DGL 그래프는 message passing을 수행하는 데 사용할 `srcdata` 와 `dstdata`라는 녀석을 제공합니다. \n", 86 | "먼저 인풋 노드 피처를 `srcdata`에 넣고, message passing을 수행하면, \n", 87 | "`dstdata`로부터 message passing의 결과를 가져올 수 있습니다.\n", 88 | "\n", 89 | "
\n", 90 | " 주의: 전체 그래프(full graph)의 message passing에서, 인풋 노드와 아웃풋 노드는 전체 노드 집합입니다. 그러므로, 동질적(homogeneous) 그래프(즉 오직 1개의 노드 타입과 1개의 엣지 타입만을 가지고 있는 그래프)의 srcdatadstdatandata와 동일합니다. \n", 91 | " 튜토리얼 섹션 내의 모든 그래프는 동질적입니다.\n", 92 | "
\n", 93 | "\n", 94 | "예를 들어, 여기에서 GraphSAGE 컨볼루션을 DGL로 어떻게 구현하는지 보여줍니다.\n", 95 | "For example, here is how you can implement GraphSAGE convolution in DGL." 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 2, 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [ 104 | "import dgl.function as fn\n", 105 | "\n", 106 | "class SAGEConv(nn.Module):\n", 107 | " \"\"\"Graph convolution module used by the GraphSAGE model.\n", 108 | " \n", 109 | " Parameters\n", 110 | " ----------\n", 111 | " in_feat : int\n", 112 | " Input feature size.\n", 113 | " out_feat : int\n", 114 | " Output feature size.\n", 115 | " \"\"\"\n", 116 | " def __init__(self, in_feat, out_feat):\n", 117 | " super(SAGEConv, self).__init__()\n", 118 | " # A linear submodule for projecting the input and neighbor feature to the output.\n", 119 | " self.linear = nn.Linear(in_feat * 2, out_feat)\n", 120 | " \n", 121 | " def forward(self, g, h):\n", 122 | " \"\"\"Forward computation\n", 123 | " \n", 124 | " Parameters\n", 125 | " ----------\n", 126 | " g : Graph\n", 127 | " The input graph.\n", 128 | " h : Tensor\n", 129 | " The input node feature.\n", 130 | " \"\"\"\n", 131 | " with g.local_scope():\n", 132 | " g.srcdata['h'] = h\n", 133 | " # update_all is a message passing API.\n", 134 | " g.update_all(fn.copy_u('h', 'm'), fn.mean('m', 'h_neigh'))\n", 135 | " h_neigh = g.dstdata['h_neigh']\n", 136 | " h_total = torch.cat([h_dst, h_neigh], dim=1)\n", 137 | " return F.relu(self.linear(h_total))" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "코드의 가운데 부분은 `g.update_all` 함수인데, 이는 이웃 피처를 수집하고 평균을 내는 역할을 합니다. \n", 145 | "\n", 146 | "여기에 총 3개의 개념이 등장합니다. \n", 147 | "\n", 148 | "* Message 함수 `fn.copy_u('h', 'm')`는 *messages*가 이웃에 전달될 때 '`h`'의 노드 피처를 복사함\n", 149 | "* Reduce 함수 `fn.mean('m', 'h_neigh')`는 모든 수신된 `'m'`의 message를 평균내고 그 결과를 새로운 노드 피처 `'h_neigh'`에 저장함.\n", 150 | "* `update_all`은 DGL에게 message를 시작하고 모든 노드와 엣지에 대해 reduce 함수를 실행하게 합니다.\n" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "\n", 158 | "## 더욱 정밀한 커스터마이징\n", 159 | "\n", 160 | "DGL에서는, `dgl.function` 패키지에서 많은 built-in message와 reduce 함수를 제공합니다. \n", 161 | "\n", 162 | "![api](../asset/dgl-mp.png)\n", 163 | "\n", 164 | "더 많은 정보는 [the API doc](https://docs.dgl.ai/api/python/function.html)에서 보실 수 있습니다." 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "이 API들은 새로운 그래프 컨볼루션 모듈을 빠르게 구현할 수 있도록 해줍니다. \n", 172 | "예를 들어, 아래는 이웃의 표현을 가중 평균으로 통합하는 새로운 `SAGEConv`를 구현합니다. \n", 173 | "`edata`가 message passing에 참여할 수도 있는 엣지 피처를 가지고 있을 수 있다는 것을 주목해 주세요." 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": 3, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "class SAGEConv(nn.Module):\n", 183 | " \"\"\"Graph convolution module used by the GraphSAGE model.\n", 184 | " \n", 185 | " Parameters\n", 186 | " ----------\n", 187 | " in_feat : int\n", 188 | " Input feature size.\n", 189 | " out_feat : int\n", 190 | " Output feature size.\n", 191 | " \"\"\"\n", 192 | " def __init__(self, in_feat, out_feat):\n", 193 | " super(SAGEConv, self).__init__()\n", 194 | " # A linear submodule for projecting the input and neighbor feature to the output.\n", 195 | " self.linear = nn.Linear(in_feat * 2, out_feat)\n", 196 | " \n", 197 | " def forward(self, g, h, w):\n", 198 | " \"\"\"Forward computation\n", 199 | " \n", 200 | " Parameters\n", 201 | " ----------\n", 202 | " g : Graph\n", 203 | " The input graph.\n", 204 | " h : Tensor\n", 205 | " The input node feature.\n", 206 | " w : Tensor\n", 207 | " The edge weight.\n", 208 | " \"\"\"\n", 209 | " h_dst = h[:g.number_of_dst_nodes()]\n", 210 | " with g.local_scope():\n", 211 | " g.srcdata['h'] = h\n", 212 | " g.edata['w'] = w\n", 213 | " # update_all is a message passing API.\n", 214 | " g.update_all(fn.u_mul_e('h', 'w', 'm'), fn.mean('m', 'h_neigh'))\n", 215 | " h_neigh = g.dstdata['h_neigh']\n", 216 | " h_total = torch.cat([h_dst, h_neigh], dim=1)\n", 217 | " return F.relu(self.linear(h_total))" 218 | ] 219 | }, 220 | { 221 | "cell_type": "markdown", 222 | "metadata": {}, 223 | "source": [ 224 | "## 사용자 정의 함수를 통한 훨씬 더 정교한 커스터마이징\n", 225 | "\n", 226 | "DGL은 최고의 자유도를 위해 사용자 정의 message와 reduce 함수를 허용합니다. \n", 227 | "여기에서, 사용자 정의 message 함수는 `fn.u_mul_e('h', 'w', 'm')`와 동일합니다." 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": 4, 233 | "metadata": {}, 234 | "outputs": [], 235 | "source": [ 236 | "def u_mul_e_udf(edges):\n", 237 | " return {'m' : edges.src['h'] * edges.data['w']}" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "metadata": {}, 243 | "source": [ 244 | "`edges`는 3개로 구성되어 있습니다. `src`, `data` 그리고 `dst`입니다. \n", 245 | "소스 노드 피처, 엣지 피처, 목적지 노드 피처를 모든 엣지에 대해 표현해 줍니다." 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "metadata": {}, 251 | "source": [ 252 | "## Recap\n", 253 | "## 복습\n", 254 | "\n", 255 | "* `srcdata` 와 `dstdata`를 인풋 노드 피처를 할당하고 아웃풋 노드 피처를 가져오는 데 사용하세요.\n", 256 | "* `dgl.function`의 built-in message와 reduce 함수를 사용해 새로운 NN 모듈을 커스터마이징 하세요.\n", 257 | "* 사용자 정의 함수는 훨씬 더 정교한 커스터마이징을 제공합니다." 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": null, 263 | "metadata": {}, 264 | "outputs": [], 265 | "source": [] 266 | } 267 | ], 268 | "metadata": { 269 | "kernelspec": { 270 | "display_name": "Python 3", 271 | "language": "python", 272 | "name": "python3" 273 | }, 274 | "language_info": { 275 | "codemirror_mode": { 276 | "name": "ipython", 277 | "version": 3 278 | }, 279 | "file_extension": ".py", 280 | "mimetype": "text/x-python", 281 | "name": "python", 282 | "nbconvert_exporter": "python", 283 | "pygments_lexer": "ipython3", 284 | "version": "3.8.3" 285 | } 286 | }, 287 | "nbformat": 4, 288 | "nbformat_minor": 4 289 | } 290 | -------------------------------------------------------------------------------- /basics/.ipynb_checkpoints/tutorial_utils-checkpoint.py: -------------------------------------------------------------------------------- 1 | import dgl 2 | import pandas as pd 3 | import torch 4 | import torch.nn.functional as F 5 | 6 | def load_zachery(): 7 | nodes_data = pd.read_csv('../data/nodes.csv') 8 | edges_data = pd.read_csv('../data/edges.csv') 9 | src = edges_data['Src'].to_numpy() 10 | dst = edges_data['Dst'].to_numpy() 11 | g = dgl.graph((src, dst)) 12 | club = nodes_data['Club'].to_list() 13 | # Convert to categorical integer values with 0 for 'Mr. Hi', 1 for 'Officer'. 14 | club = torch.tensor([c == 'Officer' for c in club]).long() 15 | # We can also convert it to one-hot encoding. 16 | club_onehot = F.one_hot(club) 17 | g.ndata.update({'club' : club, 'club_onehot' : club_onehot}) 18 | return g 19 | -------------------------------------------------------------------------------- /basics/2_heterogenous_graph.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# `Heterogenous_graph`" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [ 15 | { 16 | "name": "stderr", 17 | "output_type": "stream", 18 | "text": [ 19 | "Using backend: pytorch\n" 20 | ] 21 | } 22 | ], 23 | "source": [ 24 | "import dgl\n", 25 | "import torch" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "이질적(heterogenous) 그래프의 경우, 서로 다른 관계에 놓인 노드들은 소스(source) 노드가 되거나 목적지(destination) 노드가 됩니다. \n", 33 | "`srcdata`와 `dstdata`는 (이름에서 source data, destination data가 보이죠?) 특히 이 두 타입의 노드에 저장되어 있습니다. \n", 34 | "이질적 그래프에 대한 더 자세한 내용을 알고 싶으시면, [DGL User Guide 1.5 Heterogeneous Graphs](https://docs.dgl.ai/guide/graph-heterogeneous.html#guide-graph-heterogeneous) 를 확인해 주세요." 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "![image.png](../asset/user_guide_graphch_2.png)" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "이질적 그래프는 다양한 타입의 노드와 엣지를 가질 수 있습니다. \n", 49 | "특정 타입의 노드/엣지는 각각의 독립적인 ID space와 피처 공간을 갖습니다. \n", 50 | "예를 들어, 위의 그림의 경우 user와 game 노드의 아이디는 각각 0부터 시작하고(독립적인 ID space), 각각 다른 피처를 갖습니다(독립적인 피처 공간). \n", 51 | "\n", 52 | "\n", 53 | "위의 예시에는 2개의 노드 타입(user, game)과 2개의 엣지 타입(follow, plays)이 있군요. \n", 54 | "유저끼리는 follow를 할것이고, 유저와 게임 사이에는 play라는 관계가 나오겠죠? \n" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 2, 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "name": "stdout", 64 | "output_type": "stream", 65 | "text": [ 66 | "Graph(num_nodes={'disease': 3, 'drug': 3, 'gene': 4},\n", 67 | " num_edges={('drug', 'interacts', 'drug'): 2, ('drug', 'interacts', 'gene'): 2, ('drug', 'treats', 'disease'): 1},\n", 68 | " metagraph=[('drug', 'drug', 'interacts'), ('drug', 'gene', 'interacts'), ('drug', 'disease', 'treats')])\n" 69 | ] 70 | } 71 | ], 72 | "source": [ 73 | "# 3개의 노드 타입과 3개의 엣지 타입을 가진 이질적 그래프를 생성합니다\n", 74 | "# key 값의 3개 값 중 가운데는 \"관계\"를 나타내고, 양쪽은 source/destination node를 각각 나타냅니다.\n", 75 | "# value 값의 부분에 있는 2 덩이의 torch.tensor는 각각 source/destination 노드의 피처를 의미합니다.\n", 76 | "\n", 77 | "heterograph_data = {\n", 78 | " ('drug', 'interacts', 'drug'): (torch.tensor([0, 1]), torch.tensor([1, 2])),\n", 79 | " ('drug', 'interacts', 'gene'): (torch.tensor([0, 1]), torch.tensor([2, 3])),\n", 80 | " ('drug', 'treats', 'disease'): (torch.tensor([1]), torch.tensor([2]))\n", 81 | "}\n", 82 | "hetero_g = dgl.heterograph(heterograph_data)\n", 83 | "print(hetero_g)" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 3, 89 | "metadata": {}, 90 | "outputs": [ 91 | { 92 | "data": { 93 | "text/plain": [ 94 | "Graph(num_nodes={'disease': 3, 'drug': 3, 'gene': 4},\n", 95 | " num_edges={('drug', 'interacts', 'drug'): 2, ('drug', 'interacts', 'gene'): 2, ('drug', 'treats', 'disease'): 1},\n", 96 | " metagraph=[('drug', 'drug', 'interacts'), ('drug', 'gene', 'interacts'), ('drug', 'disease', 'treats')])" 97 | ] 98 | }, 99 | "execution_count": 3, 100 | "metadata": {}, 101 | "output_type": "execute_result" 102 | } 103 | ], 104 | "source": [ 105 | "hetero_g" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 4, 111 | "metadata": {}, 112 | "outputs": [ 113 | { 114 | "name": "stdout", 115 | "output_type": "stream", 116 | "text": [ 117 | "Graph(num_nodes={'drug': 3, 'gene': 4},\n", 118 | " num_edges={('drug', 'interacts', 'gene'): 2},\n", 119 | " metagraph=[('drug', 'gene', 'interacts')])\n" 120 | ] 121 | } 122 | ], 123 | "source": [ 124 | "# 한 관계 'durg->interacts->gene'를 추출해 sub graph를 만듭니다.\n", 125 | "sub_g = dgl.edge_type_subgraph(hetero_g, [('drug', 'interacts', 'gene')])\n", 126 | "print(sub_g)" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 5, 132 | "metadata": {}, 133 | "outputs": [ 134 | { 135 | "name": "stdout", 136 | "output_type": "stream", 137 | "text": [ 138 | "Graph(num_nodes={'drug': 3, 'gene': 4},\n", 139 | " num_edges={('drug', 'interacts', 'gene'): 2},\n", 140 | " metagraph=[('drug', 'gene', 'interacts')])\n" 141 | ] 142 | } 143 | ], 144 | "source": [ 145 | "# 소스와 목적지 노드에 피처를 할당합니다. drug 노드와 gene 노드의 수가 다르다는 점에 주목해 주세요.\n", 146 | "\n", 147 | "sub_g.srcdata['src_h'] = torch.randn(3,3)\n", 148 | "sub_g.dstdata['dst_h'] = torch.randn(4,2)\n", 149 | "print(sub_g)" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 6, 155 | "metadata": {}, 156 | "outputs": [ 157 | { 158 | "data": { 159 | "text/plain": [ 160 | "{'src_h': tensor([[-1.2368, -0.4933, -1.4143],\n", 161 | " [ 0.8217, 0.9421, -0.8660],\n", 162 | " [-0.1927, -0.1469, -1.1906]])}" 163 | ] 164 | }, 165 | "execution_count": 6, 166 | "metadata": {}, 167 | "output_type": "execute_result" 168 | } 169 | ], 170 | "source": [ 171 | "sub_g.srcdata" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 7, 177 | "metadata": {}, 178 | "outputs": [ 179 | { 180 | "data": { 181 | "text/plain": [ 182 | "{'dst_h': tensor([[-1.0101, -1.4877],\n", 183 | " [-0.6508, -1.2819],\n", 184 | " [ 0.7221, 0.8284],\n", 185 | " [ 1.5760, -0.2282]])}" 186 | ] 187 | }, 188 | "execution_count": 7, 189 | "metadata": {}, 190 | "output_type": "execute_result" 191 | } 192 | ], 193 | "source": [ 194 | "sub_g.dstdata" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "`srcdata`와 `dstdata`에 대한 더 많은 사용법은 5_message_passing과 large graph task 튜토리얼에서 확인하실 수 있습니다." 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": null, 207 | "metadata": {}, 208 | "outputs": [], 209 | "source": [] 210 | } 211 | ], 212 | "metadata": { 213 | "kernelspec": { 214 | "display_name": "Python 3", 215 | "language": "python", 216 | "name": "python3" 217 | }, 218 | "language_info": { 219 | "codemirror_mode": { 220 | "name": "ipython", 221 | "version": 3 222 | }, 223 | "file_extension": ".py", 224 | "mimetype": "text/x-python", 225 | "name": "python", 226 | "nbconvert_exporter": "python", 227 | "pygments_lexer": "ipython3", 228 | "version": "3.8.3" 229 | } 230 | }, 231 | "nbformat": 4, 232 | "nbformat_minor": 4 233 | } 234 | -------------------------------------------------------------------------------- /basics/4_link_predict.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 그래프 뉴럴 네트워크를 사용한 링크 예측\n", 8 | "\n", 9 | "GNN은 그래프 데이터에 대한 많은 머신러닝 task를 해결하는 데 강력한 툴입니다. \n", 10 | "이 튜토리얼에서는, 링크 예측을 위해 GNN을 사용하는 기본적인 워크플로우를 배울 수 있습니다. \n", 11 | "여기서 Zachery의 카라테 클럽 그래프를 다시 사용합니다. 하지만, 이번에는 두 멤버 사이의 관계를 예측하는 작업을 시도해 봅니다.\n", 12 | "\n", 13 | "이 튜토리얼에서, 다음을 배울 수 있습니다.\n", 14 | "* 링크 예측을 위한 학습/테스트 셋을 준비하는 방법\n", 15 | "* GNN 기반 링크 예측 모델을 구축하는 법\n", 16 | "* 모델을 학습하고, 그 결과를 검증하는 법\n" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "metadata": {}, 23 | "outputs": [ 24 | { 25 | "name": "stderr", 26 | "output_type": "stream", 27 | "text": [ 28 | "Using backend: pytorch\n" 29 | ] 30 | } 31 | ], 32 | "source": [ 33 | "import dgl\n", 34 | "import torch\n", 35 | "import torch.nn as nn\n", 36 | "import torch.nn.functional as F\n", 37 | "import itertools\n", 38 | "import numpy as np\n", 39 | "import scipy.sparse as sp" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "## 그래프와 피처 로드\n", 47 | "\n", 48 | "최근 튜토리얼 [세션](./3_gnn.ipynb)에 이어, Zachery의 카라테 클럽 그래프를 불러들여 노드 임베딩을 만듭니다." 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 2, 54 | "metadata": {}, 55 | "outputs": [ 56 | { 57 | "name": "stdout", 58 | "output_type": "stream", 59 | "text": [ 60 | "Graph(num_nodes=34, num_edges=156,\n", 61 | " ndata_schemes={'club': Scheme(shape=(), dtype=torch.int64), 'club_onehot': Scheme(shape=(2,), dtype=torch.int64)}\n", 62 | " edata_schemes={})\n" 63 | ] 64 | }, 65 | { 66 | "data": { 67 | "text/plain": [ 68 | "Parameter containing:\n", 69 | "tensor([[ 0.3916, -0.0327, -0.1260, -0.2869, 0.2973],\n", 70 | " [ 0.3182, 0.0220, 0.1522, -0.2716, 0.2918],\n", 71 | " [ 0.1206, -0.1165, -0.3727, 0.0779, -0.1156],\n", 72 | " [-0.1390, -0.1939, 0.1905, -0.3710, 0.3080],\n", 73 | " [ 0.1564, -0.1069, -0.0932, -0.0339, -0.0070],\n", 74 | " [-0.1578, 0.2431, -0.0528, 0.0016, 0.0280],\n", 75 | " [ 0.0519, -0.3260, -0.1438, -0.0300, -0.3647],\n", 76 | " [-0.2717, -0.0301, -0.1534, 0.1794, -0.1963],\n", 77 | " [ 0.3325, -0.1624, 0.0535, 0.1135, 0.0627],\n", 78 | " [-0.1526, -0.2020, 0.3215, 0.2962, 0.1816],\n", 79 | " [-0.3414, -0.1588, 0.3631, 0.2354, 0.0751],\n", 80 | " [ 0.0152, 0.0471, 0.3071, -0.2435, 0.2512],\n", 81 | " [-0.2102, -0.2189, -0.3463, -0.3863, -0.3508],\n", 82 | " [-0.3871, 0.2010, 0.2501, -0.3321, -0.2337],\n", 83 | " [ 0.1315, -0.2012, -0.0101, -0.3694, 0.2327],\n", 84 | " [ 0.1247, 0.3643, 0.2281, 0.3333, 0.2420],\n", 85 | " [ 0.0464, 0.0643, 0.2621, -0.0140, -0.3357],\n", 86 | " [-0.2679, 0.1186, -0.0057, 0.0680, -0.3183],\n", 87 | " [ 0.2513, 0.2763, -0.0192, 0.2784, 0.2850],\n", 88 | " [-0.2071, 0.3648, -0.2017, 0.0965, -0.2182],\n", 89 | " [ 0.0360, 0.1442, 0.2142, 0.1483, 0.0775],\n", 90 | " [-0.1826, -0.1986, -0.2250, 0.2691, 0.1778],\n", 91 | " [ 0.0243, 0.3686, 0.0827, 0.2662, -0.3841],\n", 92 | " [ 0.1415, 0.1779, -0.2522, -0.0017, 0.3368],\n", 93 | " [-0.1565, 0.3861, 0.2884, 0.0710, 0.0044],\n", 94 | " [-0.3624, 0.2424, -0.3842, 0.3670, -0.3307],\n", 95 | " [-0.2098, -0.2707, -0.0492, 0.1154, 0.2686],\n", 96 | " [-0.3806, 0.0607, 0.3294, 0.1253, 0.0544],\n", 97 | " [-0.3274, 0.0050, 0.2674, 0.1631, 0.1791],\n", 98 | " [ 0.3784, 0.0215, -0.2735, -0.2311, -0.2277],\n", 99 | " [ 0.2785, 0.3565, 0.1780, 0.3515, -0.3060],\n", 100 | " [ 0.3370, 0.2554, 0.2643, -0.2294, -0.0243],\n", 101 | " [-0.3900, -0.0826, 0.3686, -0.1113, -0.2340],\n", 102 | " [-0.0703, -0.0797, 0.3513, 0.3304, -0.2040]], requires_grad=True)" 103 | ] 104 | }, 105 | "execution_count": 2, 106 | "metadata": {}, 107 | "output_type": "execute_result" 108 | } 109 | ], 110 | "source": [ 111 | "from tutorial_utils import load_zachery\n", 112 | "\n", 113 | "# ----------- 0. load graph -------------- #\n", 114 | "g = load_zachery()\n", 115 | "print(g)\n", 116 | "\n", 117 | "# ----------- 1. node features -------------- #\n", 118 | "node_embed = nn.Embedding(g.number_of_nodes(), 5) # 각 노드는 5차원의 임베딩을 가지고 있습니다.\n", 119 | "inputs = node_embed.weight # 노드 피처로써 이 임베딩 가중치를 사용합니다.\n", 120 | "nn.init.xavier_uniform_(inputs)" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "## 학습/테스트 셋을 준비 합니다" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "일반적으로, 링크 예측 데이터셋은 *positive*와 *negative* 엣지라는 2 타입의 엣지를 포함하고 있습니다. \n", 135 | "positive 엣지는 보통 그래프 내에 이미 존재하는 엣지로부터 가져옵니다. \n", 136 | "이 예제에서, 50개의 임의의 엣지를 골라 테스트에 사용하고 나머지는 학습에 사용합니다." 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 3, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "# 학습과 테스트를 위해 엣지 셋을 분할합니다.\n", 146 | "u, v = g.edges()\n", 147 | "eids = np.arange(g.number_of_edges())\n", 148 | "eids = np.random.permutation(eids)\n", 149 | "test_pos_u, test_pos_v = u[eids[:50]], v[eids[:50]]\n", 150 | "train_pos_u, train_pos_v = u[eids[50:]], v[eids[50:]]" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "negative 엣지의 수가 크기때문에, 보통 샘플링 해주는 것이 좋습니다. \n", 158 | "적절한 negative 샘플링 알고리즘을 선택하는 방법에 대한 문제는 널리 연구되는 주제로, 이 튜토리얼의 범위를 벗어납니다. \n", 159 | "우리의 예제 그래프는 상당히 작기 때문에(노드 34개뿐), 모든 결측 엣지를 나열해 임의로 50개를 테스트에 사용하고, 150개를 학습에 사용합니다.\n" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": 4, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "# 모든 negative 엣지를 찾아 학습과 테스트용으로 분할\n", 169 | "adj = sp.coo_matrix((np.ones(len(u)), (u.numpy(), v.numpy())))\n", 170 | "adj_neg = 1 - adj.todense() - np.eye(34)\n", 171 | "neg_u, neg_v = np.where(adj_neg != 0)\n", 172 | "neg_eids = np.random.choice(len(neg_u), 200)\n", 173 | "test_neg_u, test_neg_v = neg_u[neg_eids[:50]], neg_v[neg_eids[:50]]\n", 174 | "train_neg_u, train_neg_v = neg_u[neg_eids[50:]], neg_v[neg_eids[50:]]" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "Put positive and negative edges together and form training and testing sets." 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 5, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "# Create training set.\n", 191 | "train_u = torch.cat([torch.as_tensor(train_pos_u), torch.as_tensor(train_neg_u)])\n", 192 | "train_v = torch.cat([torch.as_tensor(train_pos_v), torch.as_tensor(train_neg_v)])\n", 193 | "train_label = torch.cat([torch.zeros(len(train_pos_u)), torch.ones(len(train_neg_u))])\n", 194 | "\n", 195 | "# Create testing set.\n", 196 | "test_u = torch.cat([torch.as_tensor(test_pos_u), torch.as_tensor(test_neg_u)])\n", 197 | "test_v = torch.cat([torch.as_tensor(test_pos_v), torch.as_tensor(test_neg_v)])\n", 198 | "test_label = torch.cat([torch.zeros(len(test_pos_u)), torch.ones(len(test_neg_u))])" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": {}, 204 | "source": [ 205 | "## GraphSAGE 모델 정의하기\n", 206 | "\n", 207 | "우리의 모델은 2개 레이어로 구성되어 있는데, 각각 새로운 노드 표현(representation)을 이웃의 정보를 통합함으로써 계산합니다. \n", 208 | "수식은 다음과 같습니다. ([이전 튜토리얼](./3_gnn.ipynb)과 약간 다릅니다.)\n", 209 | "\n", 210 | "$$\n", 211 | "h_{\\mathcal{N}(v)}^k\\leftarrow \\text{AGGREGATE}_k\\{h_u^{k-1},\\forall u\\in\\mathcal{N}(v)\\}\n", 212 | "$$\n", 213 | "\n", 214 | "$$\n", 215 | "h_v^k\\leftarrow \\text{ReLU}\\left(W^k\\cdot \\text{CONCAT}(h_v^{k-1}, h_{\\mathcal{N}(v)}^k) \\right)\n", 216 | "$$\n", 217 | "\n", 218 | "DGL은 많은 유명한 이웃 통합(neighbor aggregation) 모듈의 구현체를 제공합니다. 모두 쉽게 한 줄의 코드로 호출하여 사용할 수 있습니다. \n", 219 | "지원되는 모델의 전체 리스트는 [graph convolution modules](https://docs.dgl.ai/api/python/nn.pytorch.html#module-dgl.nn.pytorch.conv)에서 보실 수 있습니다." 220 | ] 221 | }, 222 | { 223 | "cell_type": "code", 224 | "execution_count": 6, 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "from dgl.nn import SAGEConv\n", 229 | "\n", 230 | "# ----------- 2. create model -------------- #\n", 231 | "# 2개의 레이어를 가진 GraphSAGE 모델 구축\n", 232 | "class GraphSAGE(nn.Module):\n", 233 | " def __init__(self, in_feats, h_feats):\n", 234 | " super(GraphSAGE, self).__init__()\n", 235 | " self.conv1 = SAGEConv(in_feats, h_feats, 'mean')\n", 236 | " self.conv2 = SAGEConv(h_feats, h_feats, 'mean')\n", 237 | " \n", 238 | " def forward(self, g, in_feat):\n", 239 | " h = self.conv1(g, in_feat)\n", 240 | " h = F.relu(h)\n", 241 | " h = self.conv2(g, h)\n", 242 | " return h\n", 243 | " \n", 244 | "# 주어진 차원의 모델 생성\n", 245 | "# 인풋 레이어 차원: 5, 노드 임베딩\n", 246 | "# 히든 레이어 차원: 16\n", 247 | "net = GraphSAGE(5, 16)" 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "그 뒤, 모델을 아래의 손실함수를 사용해 최적화합니다.\n", 255 | "\n", 256 | "$$\n", 257 | "\\hat{y}_{u\\sim v} = \\sigma(h_u^T h_v)\n", 258 | "$$\n", 259 | "\n", 260 | "$$\n", 261 | "\\mathcal{L} = -\\sum_{u\\sim v\\in \\mathcal{D}}\\left( y_{u\\sim v}\\log(\\hat{y}_{u\\sim v}) + (1-y_{u\\sim v})\\log(1-\\hat{y}_{u\\sim v})) \\right)\n", 262 | "$$\n", 263 | "\n", 264 | "기본적으로, 모델은 엣지의 두 끝지점(노드)의 표현을 내적함으로써 각 엣지에 대한 점수를 계산합니다. \n", 265 | "그 뒤 타겟 y가 0 혹은 1인 binary cross entropy loss를 계산합니다. 여기서 0 혹은 1은 해당 엣지가 positive인지 아닌지를 나타냅니다.\n" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": 7, 271 | "metadata": {}, 272 | "outputs": [ 273 | { 274 | "name": "stdout", 275 | "output_type": "stream", 276 | "text": [ 277 | "In epoch 0, loss: 2.790684938430786\n", 278 | "In epoch 5, loss: 0.699927568435669\n", 279 | "In epoch 10, loss: 0.6101277470588684\n", 280 | "In epoch 15, loss: 0.577601969242096\n", 281 | "In epoch 20, loss: 0.5298259854316711\n", 282 | "In epoch 25, loss: 0.46006080508232117\n", 283 | "In epoch 30, loss: 0.3879762589931488\n", 284 | "In epoch 35, loss: 0.3463674783706665\n", 285 | "In epoch 40, loss: 0.32085341215133667\n", 286 | "In epoch 45, loss: 0.29568690061569214\n", 287 | "In epoch 50, loss: 0.27307477593421936\n", 288 | "In epoch 55, loss: 0.25122061371803284\n", 289 | "In epoch 60, loss: 0.2311725616455078\n", 290 | "In epoch 65, loss: 0.21075379848480225\n", 291 | "In epoch 70, loss: 0.19010187685489655\n", 292 | "In epoch 75, loss: 0.1696164608001709\n", 293 | "In epoch 80, loss: 0.15120071172714233\n", 294 | "In epoch 85, loss: 0.13269738852977753\n", 295 | "In epoch 90, loss: 0.11472604423761368\n", 296 | "In epoch 95, loss: 0.09768754243850708\n" 297 | ] 298 | } 299 | ], 300 | "source": [ 301 | "# ----------- 3. set up loss and optimizer -------------- #\n", 302 | "# 이 경우, 학습 루프의 손실\n", 303 | "optimizer = torch.optim.Adam(itertools.chain(net.parameters(), node_embed.parameters()), lr=0.01)\n", 304 | "\n", 305 | "# ----------- 4. training -------------------------------- #\n", 306 | "all_logits = []\n", 307 | "for e in range(100):\n", 308 | " # forward\n", 309 | " logits = net(g, inputs)\n", 310 | " pred = torch.sigmoid((logits[train_u] * logits[train_v]).sum(dim=1))\n", 311 | " \n", 312 | " # 손실 계산\n", 313 | " loss = F.binary_cross_entropy(pred, train_label)\n", 314 | " \n", 315 | " # backward\n", 316 | " optimizer.zero_grad()\n", 317 | " loss.backward()\n", 318 | " optimizer.step()\n", 319 | " all_logits.append(logits.detach())\n", 320 | " \n", 321 | " if e % 5 == 0:\n", 322 | " print('In epoch {}, loss: {}'.format(e, loss))" 323 | ] 324 | }, 325 | { 326 | "cell_type": "markdown", 327 | "metadata": {}, 328 | "source": [ 329 | "결과를 확인해 봅니다." 330 | ] 331 | }, 332 | { 333 | "cell_type": "code", 334 | "execution_count": 8, 335 | "metadata": {}, 336 | "outputs": [ 337 | { 338 | "name": "stdout", 339 | "output_type": "stream", 340 | "text": [ 341 | "Accuracy 0.83\n" 342 | ] 343 | } 344 | ], 345 | "source": [ 346 | "# ----------- 5. check results ------------------------ #\n", 347 | "pred = torch.sigmoid((logits[test_u] * logits[test_v]).sum(dim=1))\n", 348 | "print('Accuracy', ((pred >= 0.5) == test_label).sum().item() / len(pred))" 349 | ] 350 | }, 351 | { 352 | "cell_type": "code", 353 | "execution_count": null, 354 | "metadata": {}, 355 | "outputs": [], 356 | "source": [] 357 | } 358 | ], 359 | "metadata": { 360 | "kernelspec": { 361 | "display_name": "Python 3", 362 | "language": "python", 363 | "name": "python3" 364 | }, 365 | "language_info": { 366 | "codemirror_mode": { 367 | "name": "ipython", 368 | "version": 3 369 | }, 370 | "file_extension": ".py", 371 | "mimetype": "text/x-python", 372 | "name": "python", 373 | "nbconvert_exporter": "python", 374 | "pygments_lexer": "ipython3", 375 | "version": "3.8.3" 376 | } 377 | }, 378 | "nbformat": 4, 379 | "nbformat_minor": 4 380 | } 381 | -------------------------------------------------------------------------------- /basics/5_gpu.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# GPU를 사용해 학습 가속하기\n", 8 | "\n", 9 | "이번 튜토리얼에서, 다음을 배우게 됩니다.\n", 10 | "\n", 11 | "* 그래프와 피처 데이터를 GPU로 복사하는 방법\n", 12 | "* GNN 모델을 GPU 위에 학습하는 방법\n" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 1, 18 | "metadata": {}, 19 | "outputs": [ 20 | { 21 | "name": "stderr", 22 | "output_type": "stream", 23 | "text": [ 24 | "Using backend: pytorch\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "import dgl\n", 30 | "import torch\n", 31 | "import torch.nn as nn\n", 32 | "import torch.nn.functional as F\n", 33 | "import itertools" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "## 그래프와 피처 데이터를 GPU에 복사\n", 41 | "\n", 42 | "먼저 이전 세션에 사용한 Zachery의 카라테 클럽 그래프와 노드 라벨를 로드합니다." 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 2, 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "name": "stdout", 52 | "output_type": "stream", 53 | "text": [ 54 | "Graph(num_nodes=34, num_edges=156,\n", 55 | " ndata_schemes={'club': Scheme(shape=(), dtype=torch.int64), 'club_onehot': Scheme(shape=(2,), dtype=torch.int64)}\n", 56 | " edata_schemes={})\n" 57 | ] 58 | } 59 | ], 60 | "source": [ 61 | "from tutorial_utils import load_zachery\n", 62 | "\n", 63 | "# ----------- 0. load graph -------------- #\n", 64 | "g = load_zachery()\n", 65 | "print(g)" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": {}, 71 | "source": [ 72 | "이제 그래프와 모든 그래프의 피처 데이터는 CPU에 적재되어 있습니다. `to` API를 사용해 다른 연산장치로 복사해 보세요." 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 3, 78 | "metadata": {}, 79 | "outputs": [ 80 | { 81 | "name": "stdout", 82 | "output_type": "stream", 83 | "text": [ 84 | "Current device: cpu\n", 85 | "New device: cuda:0\n" 86 | ] 87 | } 88 | ], 89 | "source": [ 90 | "print('Current device:', g.device)\n", 91 | "g = g.to('cuda:0')\n", 92 | "print('New device:', g.device)" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "Verify that features are also copied to GPU." 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 4, 105 | "metadata": {}, 106 | "outputs": [ 107 | { 108 | "name": "stdout", 109 | "output_type": "stream", 110 | "text": [ 111 | "cuda:0\n", 112 | "cuda:0\n" 113 | ] 114 | } 115 | ], 116 | "source": [ 117 | "print(g.ndata['club'].device)\n", 118 | "print(g.ndata['club_onehot'].device)" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "## GNN 모델을 GPU에 생성하기\n", 126 | "\n", 127 | "이 스텝은 CNN이나 RNN 모델을 GPU에 생성하는 것과 같습니다. \n", 128 | "PyTorch에서, `to` API를 사용해 이를 수행할 수 있습니다." 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 5, 134 | "metadata": {}, 135 | "outputs": [ 136 | { 137 | "data": { 138 | "text/plain": [ 139 | "Parameter containing:\n", 140 | "tensor([[-0.1941, 0.3631, -0.2146, -0.0416, -0.3040],\n", 141 | " [-0.2014, 0.3393, -0.1062, -0.2229, 0.3331],\n", 142 | " [-0.2843, 0.3027, 0.2389, -0.0853, 0.3283],\n", 143 | " [ 0.0482, 0.1986, -0.2904, 0.0818, -0.1860],\n", 144 | " [-0.0844, -0.3876, 0.1654, -0.2600, 0.3482],\n", 145 | " [ 0.1354, -0.1090, 0.0389, 0.2281, -0.2484],\n", 146 | " [-0.3592, 0.1807, -0.2933, -0.2188, 0.2301],\n", 147 | " [ 0.2413, 0.3598, -0.2222, -0.2795, -0.1307],\n", 148 | " [-0.3511, -0.1753, 0.2872, -0.2322, 0.0094],\n", 149 | " [ 0.1164, 0.0620, 0.0414, -0.1472, -0.2698],\n", 150 | " [-0.1343, 0.3105, -0.3529, 0.3063, -0.1436],\n", 151 | " [ 0.2085, 0.0502, -0.2477, 0.2870, -0.0683],\n", 152 | " [-0.2910, -0.2569, -0.1903, -0.0875, -0.3270],\n", 153 | " [ 0.1782, 0.2922, -0.2446, -0.0885, -0.3430],\n", 154 | " [-0.0839, 0.0179, 0.2307, -0.1886, 0.0091],\n", 155 | " [-0.2154, -0.2445, 0.2925, 0.1994, -0.3176],\n", 156 | " [-0.0218, -0.1832, -0.2908, 0.3639, -0.3595],\n", 157 | " [ 0.1016, -0.0547, -0.3834, 0.2332, 0.1355],\n", 158 | " [ 0.2532, -0.2284, 0.1160, 0.2327, -0.2220],\n", 159 | " [-0.1158, 0.0133, 0.1556, 0.1818, -0.1308],\n", 160 | " [ 0.1331, -0.0689, -0.2183, 0.0959, -0.2642],\n", 161 | " [ 0.1583, 0.0246, -0.1268, 0.3539, -0.3599],\n", 162 | " [ 0.2765, -0.1886, -0.1546, -0.3603, -0.3806],\n", 163 | " [ 0.3022, 0.0966, 0.0634, -0.3355, 0.1699],\n", 164 | " [ 0.0687, -0.0457, 0.2138, 0.3206, 0.3198],\n", 165 | " [-0.1122, 0.2960, -0.3739, -0.2899, 0.3898],\n", 166 | " [ 0.1120, 0.2343, -0.2354, -0.1214, 0.3795],\n", 167 | " [-0.3419, 0.0163, -0.2615, 0.1877, 0.0776],\n", 168 | " [ 0.3821, 0.3670, 0.2761, -0.2352, 0.2398],\n", 169 | " [ 0.3302, -0.1100, -0.3390, 0.2329, 0.0696],\n", 170 | " [ 0.0951, 0.0089, 0.1248, -0.0494, -0.2868],\n", 171 | " [ 0.0962, 0.1329, 0.2705, -0.0595, 0.2363],\n", 172 | " [ 0.0876, -0.0845, -0.2818, -0.1904, 0.1882],\n", 173 | " [-0.2267, -0.3101, 0.0753, -0.2404, -0.3339]], device='cuda:0',\n", 174 | " requires_grad=True)" 175 | ] 176 | }, 177 | "execution_count": 5, 178 | "metadata": {}, 179 | "output_type": "execute_result" 180 | } 181 | ], 182 | "source": [ 183 | "# ----------- 1. node features -------------- #\n", 184 | "node_embed = nn.Embedding(g.number_of_nodes(), 5) # 각 노드는 5차원의 임베딩을 가지고 있습니다.\n", 185 | "# Copy node embeddings to GPU\n", 186 | "node_embed = node_embed.to('cuda:0')\n", 187 | "inputs = node_embed.weight # 노드 피처로써 이 임베딩 가중치를 사용합니다.\n", 188 | "nn.init.xavier_uniform_(inputs)" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "커뮤니티의 라벨은 `'club'`이라는 노드 피처에 저장되어 있습니다. (0은 instructor의 커뮤니티, 1은 club president의 커뮤니티). \n", 196 | "오로지 0과 33번 노드에만 라벨링 되어 있습니다." 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 6, 202 | "metadata": {}, 203 | "outputs": [ 204 | { 205 | "name": "stdout", 206 | "output_type": "stream", 207 | "text": [ 208 | "Labels tensor([0, 1], device='cuda:0')\n" 209 | ] 210 | } 211 | ], 212 | "source": [ 213 | "labels = g.ndata['club']\n", 214 | "labeled_nodes = [0, 33]\n", 215 | "print('Labels', labels[labeled_nodes])" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "## GraphSAGE 모델 정의하기\n", 223 | "\n", 224 | "우리의 모델은 2개 레이어로 구성되어 있는데, 각각 새로운 노드 표현(representation)을 이웃의 정보를 통합함으로써 계산합니다. \n", 225 | "수식은 다음과 같습니다. \n", 226 | "\n", 227 | "\n", 228 | "$$\n", 229 | "h_{\\mathcal{N}(v)}^k\\leftarrow \\text{AGGREGATE}_k\\{h_u^{k-1},\\forall u\\in\\mathcal{N}(v)\\}\n", 230 | "$$\n", 231 | "\n", 232 | "$$\n", 233 | "h_v^k\\leftarrow \\sigma\\left(W^k\\cdot \\text{CONCAT}(h_v^{k-1}, h_{\\mathcal{N}(v)}^k) \\right)\n", 234 | "$$\n", 235 | "\n", 236 | "DGL은 많은 유명한 이웃 통합(neighbor aggregation) 모듈의 구현체를 제공합니다. 모두 쉽게 한 줄의 코드로 호출하여 사용할 수 있습니다. \n", 237 | "지원되는 모델의 전체 리스트는 [graph convolution modules](https://docs.dgl.ai/api/python/nn.pytorch.html#module-dgl.nn.pytorch.conv)에서 보실 수 있습니다." 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": 7, 243 | "metadata": {}, 244 | "outputs": [], 245 | "source": [ 246 | "from dgl.nn import SAGEConv\n", 247 | "\n", 248 | "# ----------- 2. create model -------------- #\n", 249 | "# 2개의 레이어를 가진 GraphSAGE 모델 구축\n", 250 | "class GraphSAGE(nn.Module):\n", 251 | " def __init__(self, in_feats, h_feats, num_classes):\n", 252 | " super(GraphSAGE, self).__init__()\n", 253 | " self.conv1 = SAGEConv(in_feats, h_feats, 'mean')\n", 254 | " self.conv2 = SAGEConv(h_feats, num_classes, 'mean')\n", 255 | " \n", 256 | " def forward(self, g, in_feat):\n", 257 | " h = self.conv1(g, in_feat)\n", 258 | " h = F.relu(h)\n", 259 | " h = self.conv2(g, h)\n", 260 | " return h\n", 261 | " \n", 262 | "# 주어진 차원의 모델 생성\n", 263 | "# 인풋 레이어 차원: 5, 노드 임베딩\n", 264 | "# 히든 레이어 차원: 16\n", 265 | "# 아웃풋 레이어 차원: 2, 클래스가 2개 있기 때문, 0과 1\n", 266 | "\n", 267 | "net = GraphSAGE(5, 16, 2)" 268 | ] 269 | }, 270 | { 271 | "cell_type": "markdown", 272 | "metadata": {}, 273 | "source": [ 274 | "네트워크를 GPU에 복사함" 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": 8, 280 | "metadata": {}, 281 | "outputs": [], 282 | "source": [ 283 | "net = net.to('cuda:0')" 284 | ] 285 | }, 286 | { 287 | "cell_type": "code", 288 | "execution_count": 9, 289 | "metadata": {}, 290 | "outputs": [ 291 | { 292 | "name": "stdout", 293 | "output_type": "stream", 294 | "text": [ 295 | "In epoch 0, loss: 0.8203792572021484\n", 296 | "In epoch 5, loss: 0.4111963212490082\n", 297 | "In epoch 10, loss: 0.21308189630508423\n", 298 | "In epoch 15, loss: 0.08275498449802399\n", 299 | "In epoch 20, loss: 0.03401576727628708\n", 300 | "In epoch 25, loss: 0.01440766267478466\n", 301 | "In epoch 30, loss: 0.006833690218627453\n", 302 | "In epoch 35, loss: 0.0037461717147380114\n", 303 | "In epoch 40, loss: 0.0023471189197152853\n", 304 | "In epoch 45, loss: 0.0016530591528862715\n", 305 | "In epoch 50, loss: 0.0012706018751487136\n", 306 | "In epoch 55, loss: 0.0010407611262053251\n", 307 | "In epoch 60, loss: 0.0008915828075259924\n", 308 | "In epoch 65, loss: 0.0007876282907091081\n", 309 | "In epoch 70, loss: 0.000710575666744262\n", 310 | "In epoch 75, loss: 0.0006503689801320434\n", 311 | "In epoch 80, loss: 0.000602009822614491\n", 312 | "In epoch 85, loss: 0.0005602584569714963\n", 313 | "In epoch 90, loss: 0.0005215413984842598\n", 314 | "In epoch 95, loss: 0.000484131567645818\n" 315 | ] 316 | } 317 | ], 318 | "source": [ 319 | "# ----------- 3. set up loss and optimizer -------------- #\n", 320 | "# 이 경우, 학습 루프의 손실\n", 321 | "\n", 322 | "optimizer = torch.optim.Adam(itertools.chain(net.parameters(), node_embed.parameters()), lr=0.01)\n", 323 | "\n", 324 | "# ----------- 4. training -------------------------------- #\n", 325 | "all_logits = []\n", 326 | "for e in range(100):\n", 327 | " # forward\n", 328 | " logits = net(g, inputs)\n", 329 | " \n", 330 | " # 손실 계산\n", 331 | " logp = F.log_softmax(logits, 1)\n", 332 | " loss = F.nll_loss(logp[labeled_nodes], labels[labeled_nodes])\n", 333 | " \n", 334 | " # backward\n", 335 | " optimizer.zero_grad()\n", 336 | " loss.backward()\n", 337 | " optimizer.step()\n", 338 | " all_logits.append(logits.detach())\n", 339 | " \n", 340 | " if e % 5 == 0:\n", 341 | " print('In epoch {}, loss: {}'.format(e, loss))" 342 | ] 343 | }, 344 | { 345 | "cell_type": "code", 346 | "execution_count": 10, 347 | "metadata": {}, 348 | "outputs": [ 349 | { 350 | "name": "stdout", 351 | "output_type": "stream", 352 | "text": [ 353 | "Accuracy 0.9411764705882353\n" 354 | ] 355 | } 356 | ], 357 | "source": [ 358 | "# ----------- 5. check results ------------------------ #\n", 359 | "pred = torch.argmax(logits, axis=1)\n", 360 | "print('Accuracy', (pred == labels).sum().item() / len(pred))" 361 | ] 362 | }, 363 | { 364 | "cell_type": "markdown", 365 | "metadata": {}, 366 | "source": [ 367 | "**한 GPU 메모리에 그래프와 피처 데이터를 적재할 수 없으면 어떻게 하나요?** \n", 368 | "\n", 369 | "* GNN을 천제 그래프에 대해 수행하는 대신에, 몇몇 subgraph에 대해 수행해 수렴시켜보세요.\n", 370 | "* 다른 샘플을 다른 GPU에 올림으로써 더 빠른 가속을 경험해 보세요.\n", 371 | "* 그래프를 여러 머신에 분할하여 분산된 형태로 학습시켜보세요.\n", 372 | "\n", 373 | "추후에 이러한 방법론을 각각 살펴볼 예정입니다." 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": null, 379 | "metadata": {}, 380 | "outputs": [], 381 | "source": [] 382 | } 383 | ], 384 | "metadata": { 385 | "kernelspec": { 386 | "display_name": "Python 3", 387 | "language": "python", 388 | "name": "python3" 389 | }, 390 | "language_info": { 391 | "codemirror_mode": { 392 | "name": "ipython", 393 | "version": 3 394 | }, 395 | "file_extension": ".py", 396 | "mimetype": "text/x-python", 397 | "name": "python", 398 | "nbconvert_exporter": "python", 399 | "pygments_lexer": "ipython3", 400 | "version": "3.8.3" 401 | } 402 | }, 403 | "nbformat": 4, 404 | "nbformat_minor": 4 405 | } 406 | -------------------------------------------------------------------------------- /basics/6_message_passing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Customize Graph Convolution using Message Passing APIs\n", 8 | "# Message Passing API로 그래프 컨볼루션 커스터마이징하기\n", 9 | "\n", 10 | "\n", 11 | "이전 세션까지, built-in된 [graph convolution modules](https://docs.dgl.ai/api/python/nn.pytorch.html#module-dgl.nn.pytorch.conv)을 사용해 다중 레이어 그래프 뉴럴넷을 구축했습니다. \n", 12 | "하지만, 때때로 이웃 정보를 통합하는 새로운 방법을 개발하고 싶을 수도 있겠죠. \n", 13 | "DGL의 message passing API는 이런 상황을 위해 설계되었습니다. \n", 14 | "\n", 15 | "이 튜토리얼에서, 이런 것들을 배울 수 있습니다. \n", 16 | "\n", 17 | "* DGL의 `nn.SAGEConv` 모듈의 내부는 어떻게 돌아갈까? \n", 18 | "* DGL의 message passing API\n", 19 | "* 새로운 그래프 컨볼루션 모듈 설계하기" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 1, 25 | "metadata": {}, 26 | "outputs": [ 27 | { 28 | "name": "stderr", 29 | "output_type": "stream", 30 | "text": [ 31 | "Using backend: pytorch\n" 32 | ] 33 | } 34 | ], 35 | "source": [ 36 | "import dgl\n", 37 | "import torch\n", 38 | "import torch.nn as nn\n", 39 | "import torch.nn.functional as F" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "## Message passing과 GNN\n", 47 | "\n", 48 | "DGL은 [Gilmer et al.](https://arxiv.org/abs/1704.01212)에 의해 제안된 Message Passing Neural Network에서 고안된 *message passing 패러다임*을 따릅니다. \n", 49 | "기본적으로, 연구진은 많은 GNN 모델이 다음 프레임워크에 들어맞는다는 것을 발견했습니다. \n", 50 | "\n", 51 | "$$\n", 52 | "m_{u\\sim v}^{(l)} = M^{(l)}\\left(h_v^{(l-1)}, h_u^{(l-1)}, e_{u\\sim v}^{(l-1)}\\right)\n", 53 | "$$\n", 54 | "\n", 55 | "$$\n", 56 | "m_{v}^{(l)} = \\sum_{u\\in\\mathcal{N}(v)}m_{u\\sim v}^{(l)}\n", 57 | "$$\n", 58 | "\n", 59 | "$$\n", 60 | "h_v^{(l)} = U^{(l)}\\left(h_v^{(l-1)}, m_v^{(l)}\\right)\n", 61 | "$$\n", 62 | "\n", 63 | "DGL은 $M^{(l)}$ 을 *message function*라 부르며, $\\sum$을 the *reduce function*이라 부릅니다. \n", 64 | "\n", 65 | "여기서 $\\sum$은 어떤 함수든 표현할 수 있으며 꼭 반드시 summation일 필요는 없습니다." 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": {}, 71 | "source": [ 72 | "가령, GraphSAGE 모델은 다음의 수식적인 형태를 갖고 있습니다.\n", 73 | "\n", 74 | "$$\n", 75 | "h_{\\mathcal{N}(v)}^k\\leftarrow \\text{Average}\\{h_u^{k-1},\\forall u\\in\\mathcal{N}(v)\\}\n", 76 | "$$\n", 77 | "\n", 78 | "$$\n", 79 | "h_v^k\\leftarrow \\text{ReLU}\\left(W^k\\cdot \\text{CONCAT}(h_v^{k-1}, h_{\\mathcal{N}(v)}^k) \\right)\n", 80 | "$$\n", 81 | "\n", 82 | "message passing이 유방향적이라는 것을 볼 수 있죠. \n", 83 | "즉, 한 노드 $u$에서 $v$로 보내진 메시지는 반대 방향인 노드 $v$에서 노드 $u$로 보내진 다른 메시지와 꼭 같을 필요는 없다는 말입니다. \n", 84 | "\n", 85 | "DGL 그래프는 message passing을 수행하는 데 사용할 `srcdata` 와 `dstdata`라는 녀석을 제공합니다. \n", 86 | "먼저 인풋 노드 피처를 `srcdata`에 넣고, message passing을 수행하면, \n", 87 | "`dstdata`로부터 message passing의 결과를 가져올 수 있습니다.\n", 88 | "\n", 89 | "
\n", 90 | " 주의: 전체 그래프(full graph)의 message passing에서, 인풋 노드와 아웃풋 노드는 전체 노드 집합입니다. 그러므로, 동질적(homogeneous) 그래프(즉 오직 1개의 노드 타입과 1개의 엣지 타입만을 가지고 있는 그래프)의 srcdatadstdatandata와 동일합니다. \n", 91 | " 튜토리얼 섹션 내의 모든 그래프는 동질적입니다.\n", 92 | "
\n", 93 | "\n", 94 | "예를 들어, 여기에서 GraphSAGE 컨볼루션을 DGL로 어떻게 구현하는지 보여줍니다.\n", 95 | "For example, here is how you can implement GraphSAGE convolution in DGL." 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 2, 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [ 104 | "import dgl.function as fn\n", 105 | "\n", 106 | "class SAGEConv(nn.Module):\n", 107 | " \"\"\"Graph convolution module used by the GraphSAGE model.\n", 108 | " \n", 109 | " Parameters\n", 110 | " ----------\n", 111 | " in_feat : int\n", 112 | " Input feature size.\n", 113 | " out_feat : int\n", 114 | " Output feature size.\n", 115 | " \"\"\"\n", 116 | " def __init__(self, in_feat, out_feat):\n", 117 | " super(SAGEConv, self).__init__()\n", 118 | " # A linear submodule for projecting the input and neighbor feature to the output.\n", 119 | " self.linear = nn.Linear(in_feat * 2, out_feat)\n", 120 | " \n", 121 | " def forward(self, g, h):\n", 122 | " \"\"\"Forward computation\n", 123 | " \n", 124 | " Parameters\n", 125 | " ----------\n", 126 | " g : Graph\n", 127 | " The input graph.\n", 128 | " h : Tensor\n", 129 | " The input node feature.\n", 130 | " \"\"\"\n", 131 | " with g.local_scope():\n", 132 | " g.srcdata['h'] = h\n", 133 | " # update_all is a message passing API.\n", 134 | " g.update_all(fn.copy_u('h', 'm'), fn.mean('m', 'h_neigh'))\n", 135 | " h_neigh = g.dstdata['h_neigh']\n", 136 | " h_total = torch.cat([h_dst, h_neigh], dim=1)\n", 137 | " return F.relu(self.linear(h_total))" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "코드의 가운데 부분은 `g.update_all` 함수인데, 이는 이웃 피처를 수집하고 평균을 내는 역할을 합니다. \n", 145 | "\n", 146 | "여기에 총 3개의 개념이 등장합니다. \n", 147 | "\n", 148 | "* Message 함수 `fn.copy_u('h', 'm')`는 *messages*가 이웃에 전달될 때 '`h`'의 노드 피처를 복사함\n", 149 | "* Reduce 함수 `fn.mean('m', 'h_neigh')`는 모든 수신된 `'m'`의 message를 평균내고 그 결과를 새로운 노드 피처 `'h_neigh'`에 저장함.\n", 150 | "* `update_all`은 DGL에게 message를 시작하고 모든 노드와 엣지에 대해 reduce 함수를 실행하게 합니다.\n" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "\n", 158 | "## 더욱 정밀한 커스터마이징\n", 159 | "\n", 160 | "DGL에서는, `dgl.function` 패키지에서 많은 built-in message와 reduce 함수를 제공합니다. \n", 161 | "\n", 162 | "![api](../asset/dgl-mp.png)\n", 163 | "\n", 164 | "더 많은 정보는 [the API doc](https://docs.dgl.ai/api/python/function.html)에서 보실 수 있습니다." 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "이 API들은 새로운 그래프 컨볼루션 모듈을 빠르게 구현할 수 있도록 해줍니다. \n", 172 | "예를 들어, 아래는 이웃의 표현을 가중 평균으로 통합하는 새로운 `SAGEConv`를 구현합니다. \n", 173 | "`edata`가 message passing에 참여할 수도 있는 엣지 피처를 가지고 있을 수 있다는 것을 주목해 주세요." 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": 3, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "class SAGEConv(nn.Module):\n", 183 | " \"\"\"Graph convolution module used by the GraphSAGE model.\n", 184 | " \n", 185 | " Parameters\n", 186 | " ----------\n", 187 | " in_feat : int\n", 188 | " Input feature size.\n", 189 | " out_feat : int\n", 190 | " Output feature size.\n", 191 | " \"\"\"\n", 192 | " def __init__(self, in_feat, out_feat):\n", 193 | " super(SAGEConv, self).__init__()\n", 194 | " # A linear submodule for projecting the input and neighbor feature to the output.\n", 195 | " self.linear = nn.Linear(in_feat * 2, out_feat)\n", 196 | " \n", 197 | " def forward(self, g, h, w):\n", 198 | " \"\"\"Forward computation\n", 199 | " \n", 200 | " Parameters\n", 201 | " ----------\n", 202 | " g : Graph\n", 203 | " The input graph.\n", 204 | " h : Tensor\n", 205 | " The input node feature.\n", 206 | " w : Tensor\n", 207 | " The edge weight.\n", 208 | " \"\"\"\n", 209 | " h_dst = h[:g.number_of_dst_nodes()]\n", 210 | " with g.local_scope():\n", 211 | " g.srcdata['h'] = h\n", 212 | " g.edata['w'] = w\n", 213 | " # update_all is a message passing API.\n", 214 | " g.update_all(fn.u_mul_e('h', 'w', 'm'), fn.mean('m', 'h_neigh'))\n", 215 | " h_neigh = g.dstdata['h_neigh']\n", 216 | " h_total = torch.cat([h_dst, h_neigh], dim=1)\n", 217 | " return F.relu(self.linear(h_total))" 218 | ] 219 | }, 220 | { 221 | "cell_type": "markdown", 222 | "metadata": {}, 223 | "source": [ 224 | "## 사용자 정의 함수를 통한 훨씬 더 정교한 커스터마이징\n", 225 | "\n", 226 | "DGL은 최고의 자유도를 위해 사용자 정의 message와 reduce 함수를 허용합니다. \n", 227 | "여기에서, 사용자 정의 message 함수는 `fn.u_mul_e('h', 'w', 'm')`와 동일합니다." 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": 4, 233 | "metadata": {}, 234 | "outputs": [], 235 | "source": [ 236 | "def u_mul_e_udf(edges):\n", 237 | " return {'m' : edges.src['h'] * edges.data['w']}" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "metadata": {}, 243 | "source": [ 244 | "`edges`는 3개로 구성되어 있습니다. `src`, `data` 그리고 `dst`입니다. \n", 245 | "소스 노드 피처, 엣지 피처, 목적지 노드 피처를 모든 엣지에 대해 표현해 줍니다." 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "metadata": {}, 251 | "source": [ 252 | "## Recap\n", 253 | "## 복습\n", 254 | "\n", 255 | "* `srcdata` 와 `dstdata`를 인풋 노드 피처를 할당하고 아웃풋 노드 피처를 가져오는 데 사용하세요.\n", 256 | "* `dgl.function`의 built-in message와 reduce 함수를 사용해 새로운 NN 모듈을 커스터마이징 하세요.\n", 257 | "* 사용자 정의 함수는 훨씬 더 정교한 커스터마이징을 제공합니다." 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": null, 263 | "metadata": {}, 264 | "outputs": [], 265 | "source": [] 266 | } 267 | ], 268 | "metadata": { 269 | "kernelspec": { 270 | "display_name": "Python 3", 271 | "language": "python", 272 | "name": "python3" 273 | }, 274 | "language_info": { 275 | "codemirror_mode": { 276 | "name": "ipython", 277 | "version": 3 278 | }, 279 | "file_extension": ".py", 280 | "mimetype": "text/x-python", 281 | "name": "python", 282 | "nbconvert_exporter": "python", 283 | "pygments_lexer": "ipython3", 284 | "version": "3.8.3" 285 | } 286 | }, 287 | "nbformat": 4, 288 | "nbformat_minor": 4 289 | } 290 | -------------------------------------------------------------------------------- /basics/__pycache__/tutorial_utils.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/basics/__pycache__/tutorial_utils.cpython-38.pyc -------------------------------------------------------------------------------- /basics/tutorial_utils.py: -------------------------------------------------------------------------------- 1 | import dgl 2 | import pandas as pd 3 | import torch 4 | import torch.nn.functional as F 5 | 6 | def load_zachery(): 7 | nodes_data = pd.read_csv('../data/nodes.csv') 8 | edges_data = pd.read_csv('../data/edges.csv') 9 | src = edges_data['Src'].to_numpy() 10 | dst = edges_data['Dst'].to_numpy() 11 | g = dgl.graph((src, dst)) 12 | club = nodes_data['Club'].to_list() 13 | # Convert to categorical integer values with 0 for 'Mr. Hi', 1 for 'Officer'. 14 | club = torch.tensor([c == 'Officer' for c in club]).long() 15 | # We can also convert it to one-hot encoding. 16 | club_onehot = F.one_hot(club) 17 | g.ndata.update({'club' : club, 'club_onehot' : club_onehot}) 18 | return g 19 | -------------------------------------------------------------------------------- /data/edges.csv: -------------------------------------------------------------------------------- 1 | Src,Dst,Weight 2 | 0,1,0.31845103596456925 3 | 0,2,0.5512145529252186 4 | 0,3,0.22741585224191552 5 | 0,4,0.2669188689251851 6 | 0,5,0.47544947326394815 7 | 0,6,0.8862627361494558 8 | 0,7,0.16042605375040297 9 | 0,8,0.7459807864037868 10 | 0,10,0.5892903561029029 11 | 0,11,0.47815888753487035 12 | 0,12,0.782470682321906 13 | 0,13,0.4179567052866201 14 | 0,17,0.3685358669980614 15 | 0,19,0.9551929987199811 16 | 0,21,0.9613567741407174 17 | 0,31,0.726227171317607 18 | 1,0,0.6481096579934986 19 | 1,2,0.37891536859801955 20 | 1,3,0.24363852899322458 21 | 1,7,0.41483571945746534 22 | 1,13,0.6155209924586298 23 | 1,17,0.2680276146366586 24 | 1,19,0.11295441102477333 25 | 1,21,0.08552253827088485 26 | 1,30,0.5935657977437264 27 | 2,0,0.5362144558950368 28 | 2,1,0.5768112213837137 29 | 2,3,0.43712621359822357 30 | 2,7,0.13078796894587796 31 | 2,8,0.08022424811393303 32 | 2,9,0.14176452681346274 33 | 2,13,0.8180530705514824 34 | 2,27,0.8320016262502158 35 | 2,28,0.409540145234772 36 | 2,32,0.2642001276499689 37 | 3,0,0.04615295242576234 38 | 3,1,0.7049149713067023 39 | 3,2,0.8540307600498042 40 | 3,7,0.46208027528965967 41 | 3,12,0.6784210067516615 42 | 3,13,0.3301297641821943 43 | 4,0,0.8832405252248015 44 | 4,6,0.6669881098899139 45 | 4,10,0.9640899941433938 46 | 5,0,0.4268133485450748 47 | 5,6,0.8149795460997955 48 | 5,10,0.9267811195268474 49 | 5,16,0.565166945765339 50 | 6,0,0.026799684543950875 51 | 6,4,0.9402764505035385 52 | 6,5,0.6631523621900474 53 | 6,16,0.28025742709759016 54 | 7,0,0.9254762282755662 55 | 7,1,0.796690523489831 56 | 7,2,0.0979476899619427 57 | 7,3,0.7074291143964807 58 | 8,0,0.7761087565695074 59 | 8,2,0.3073975630293794 60 | 8,30,0.7605817165692309 61 | 8,32,0.04947830225770011 62 | 8,33,0.6309335405401543 63 | 9,2,0.17380258005907812 64 | 9,33,0.9785414932201859 65 | 10,0,0.7191944186343044 66 | 10,4,0.6595607436981878 67 | 10,5,0.5389153328170596 68 | 11,0,0.8284252928975201 69 | 12,0,0.08159504164928544 70 | 12,3,0.026621551124401566 71 | 13,0,0.37654143271899876 72 | 13,1,0.698648970574733 73 | 13,2,0.1974780964394499 74 | 13,3,0.45021972444903435 75 | 13,33,0.9722036402765037 76 | 14,32,0.009557409279240536 77 | 14,33,0.589593597849638 78 | 15,32,0.9878091453185213 79 | 15,33,0.056667558149276376 80 | 16,5,0.9045359763970754 81 | 16,6,0.16879835545426658 82 | 17,0,0.7730618928346192 83 | 17,1,0.612815141007156 84 | 18,32,0.4986886436717537 85 | 18,33,0.39903509029811446 86 | 19,0,0.08989463618875349 87 | 19,1,0.7528198786718245 88 | 19,33,0.8144615833348146 89 | 20,32,0.6850036047325682 90 | 20,33,0.10859338317785638 91 | 21,0,0.20571853793912853 92 | 21,1,0.8687748452451053 93 | 22,32,0.008113164327674838 94 | 22,33,0.36145242064640726 95 | 23,25,0.19801959093221744 96 | 23,27,0.7132375875281998 97 | 23,29,0.8363094707133548 98 | 23,32,0.28537615612136547 99 | 23,33,0.0772935150077827 100 | 24,25,0.26813609940254624 101 | 24,27,0.22638821516538454 102 | 24,31,0.642997701810025 103 | 25,23,0.14459300691102495 104 | 25,24,0.7946476714989169 105 | 25,31,0.7388561092944019 106 | 26,29,0.445934837683155 107 | 26,33,0.4916511327260056 108 | 27,2,0.9527176446503433 109 | 27,23,0.7422628198042871 110 | 27,24,0.23101654883380685 111 | 27,33,0.9550339587184191 112 | 28,2,0.3339314258188022 113 | 28,31,0.34149893586394486 114 | 28,33,0.8180157469491468 115 | 29,23,0.4771935478203284 116 | 29,26,0.12938838154434495 117 | 29,32,0.1215136458344257 118 | 29,33,0.019569167078249627 119 | 30,1,0.6393264342401126 120 | 30,8,0.6646644798466513 121 | 30,32,0.1479691524369151 122 | 30,33,0.6403112524880046 123 | 31,0,0.1394065169246964 124 | 31,24,0.134245083586921 125 | 31,25,0.6243484303605552 126 | 31,28,0.3482911865356624 127 | 31,32,0.23331307519961453 128 | 31,33,0.4593599814263031 129 | 32,2,0.8839177811841961 130 | 32,8,0.5539934876068489 131 | 32,14,0.41970743621036855 132 | 32,15,0.8168582822265494 133 | 32,18,0.30481639228312607 134 | 32,20,0.07279882286966943 135 | 32,22,0.3978619031804904 136 | 32,23,0.20690915689699585 137 | 32,29,0.2338575632865122 138 | 32,30,0.8955881108049515 139 | 32,31,0.7316583958942351 140 | 32,33,0.0033742797158278215 141 | 33,8,0.3631579670659171 142 | 33,9,0.5292689687034333 143 | 33,13,0.7535800037841369 144 | 33,14,0.6738394218089896 145 | 33,15,0.8140789052125266 146 | 33,18,0.8968652515555932 147 | 33,19,0.45952115221569223 148 | 33,20,0.6897437506770194 149 | 33,22,0.5883557598002018 150 | 33,23,0.004996264899124525 151 | 33,26,0.04515847947583995 152 | 33,27,0.8556199432433349 153 | 33,28,0.2664787836457956 154 | 33,29,0.2799011634702968 155 | 33,30,0.6521544693031561 156 | 33,31,0.8285364872414698 157 | 33,32,0.8426561777783549 158 | -------------------------------------------------------------------------------- /data/gen_data.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import torch 3 | import scipy.sparse as sp 4 | import pandas as pd 5 | import numpy as np 6 | import random 7 | 8 | g = nx.karate_club_graph().to_undirected().to_directed() 9 | ids = [] 10 | clubs = [] 11 | ages = [] 12 | for nid, attr in g.nodes(data=True): 13 | ids.append(nid) 14 | clubs.append(attr['club']) 15 | ages.append(random.randint(30, 50)) 16 | nodes = pd.DataFrame({'Id' : ids, 'Club' : clubs, 'Age' : ages}) 17 | print(nodes) 18 | src = [] 19 | dst = [] 20 | weight = [] 21 | for u, v in g.edges(): 22 | src.append(u) 23 | dst.append(v) 24 | weight.append(random.random()) 25 | edges = pd.DataFrame({'Src' : src, 'Dst' : dst, 'Weight' : weight}) 26 | print(edges) 27 | 28 | nodes.to_csv('nodes.csv', index=False) 29 | edges.to_csv('edges.csv', index=False) 30 | 31 | #with open('edges.txt', 'w') as f: 32 | # for u, v in zip(src, dst): 33 | # f.write('{} {}\n'.format(u, v)) 34 | # 35 | #torch.save(torch.tensor(src), 'src.pt') 36 | #torch.save(torch.tensor(dst), 'dst.pt') 37 | # 38 | #spmat = nx.to_scipy_sparse_matrix(g) 39 | #print(spmat) 40 | #sp.save_npz('scipy_adj.npz', spmat) 41 | # 42 | #from networkx.readwrite import json_graph 43 | #import json 44 | # 45 | #with open('adj.json', 'w') as f: 46 | # json.dump(json_graph.adjacency_data(g), f) 47 | # 48 | #node_feat = torch.randn((34, 5)) / 10. 49 | #edge_feat = torch.ones((156,)) 50 | #torch.save(node_feat, 'node_feat.pt') 51 | #torch.save(edge_feat, 'edge_feat.pt') 52 | -------------------------------------------------------------------------------- /data/nodes.csv: -------------------------------------------------------------------------------- 1 | Id,Club,Age 2 | 0,Mr. Hi,45 3 | 1,Mr. Hi,33 4 | 2,Mr. Hi,36 5 | 3,Mr. Hi,31 6 | 4,Mr. Hi,41 7 | 5,Mr. Hi,42 8 | 6,Mr. Hi,48 9 | 7,Mr. Hi,41 10 | 8,Mr. Hi,30 11 | 9,Officer,35 12 | 10,Mr. Hi,38 13 | 11,Mr. Hi,44 14 | 12,Mr. Hi,37 15 | 13,Mr. Hi,39 16 | 14,Officer,36 17 | 15,Officer,38 18 | 16,Mr. Hi,47 19 | 17,Mr. Hi,45 20 | 18,Officer,41 21 | 19,Mr. Hi,31 22 | 20,Officer,31 23 | 21,Mr. Hi,44 24 | 22,Officer,42 25 | 23,Officer,32 26 | 24,Officer,30 27 | 25,Officer,50 28 | 26,Officer,30 29 | 27,Officer,43 30 | 28,Officer,48 31 | 29,Officer,40 32 | 30,Officer,39 33 | 31,Officer,45 34 | 32,Officer,47 35 | 33,Officer,33 36 | -------------------------------------------------------------------------------- /large_graph/.ipynb_checkpoints/2_unsupervised_learning_and_link_prediction-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 대규모 그래프에서의 링크 예측을 위한 GNN의 확률적(Storchastic) 학습 \n", 8 | "\n", 9 | "이번 튜토리얼에서는, 다중 레이어 GraphSAGE 모델을 비지도학습 방식으로 학습시키는 방법을 OGB가 제공하는 Amazon Copurchase Netword 데이터의 링크 예측을 통해 배워봅니다. \n", 10 | "데이터셋은 240만 노드와 6100만 엣지를 포함하고 있으며, 따라서 단일 GPU에 올라가지 않습니다.\n", 11 | "\n", 12 | "이 튜토리얼의 내용은 다음을 포함합니다. \n", 13 | "\n", 14 | "* GNN 모델을 그래프 크기에 상관없이 1개의 GPU를 가진 단일 머신으로 학습하기 \n", 15 | "* 링크 예측 task를 수행하는 GNN 모델 학습하기\n", 16 | "* 비지도 학습을 위한 GNN 모델 학습하기\n", 17 | "\n", 18 | "이 튜토리얼은 이전의 튜토리얼에서 다운받은 데이터를 활용합니다. " 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "## Link Prediction Overview" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "링크 예측의 목표는 두개의 주어진 노드 사이에 엣지가 존재하는지를 예측하는 것입니다. \n", 33 | "보통 이런 문제를 $s_{uv} = \\phi(\\boldsymbol{h}^{(l)}_u, \\boldsymbol{h}^{(l)}_v)$라는 점수를 예측하는 문제로 수식화 하는데요, \n", 34 | "이는 두 노드 사이에 존재하는 엣지의 likelihood를 의미합니다. \n", 35 | "\n", 36 | "또, 모델을 *네거티브 샘플링 negative sampling*을 통해 학습합니다. \n", 37 | "즉, 실재 존재하는 엣지와 \"존재하지 않는\" 엣지의 점수를 비교함으로써 학습한다는 의미입니다.\n" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "일반적인 손실함수 중 하나는 negative log-likelihood 입니다." 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "$$\n", 52 | "\\mathcal{L} = -\\log \\sigma\\left(s_{uv}\\right) - Q \\mathbb{E}_{v^- \\in P^-(v)}\\left[ \\sigma\\left(-s_{uv^-}\\right) \\right]\n", 53 | "$$" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "BPR이나 margin loss와 같은 다른 손실함수를 사용할 수도 있습니다. \n", 61 | "\n", 62 | "위의 수식이 implicit matrix factorization 혹은 워드 임베딩 학습과 비슷하다는 점에 주목해 주세요." 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "## GNN을 사용한 비지도학습의 개요\n", 70 | "\n", 71 | "링크 예측 그 자체는 한 노드가 다른 노드와 상호작용할지 예측하는 추천과 같은 다양한 작업에서 이미 유용성을 입증했습니다. \n", 72 | "또 링크 예측은 모든 노드의 잠재 표현을 학습하고자 하는, 비지도 학습의 상황에서도 유용합니다.\n", 73 | "\n", 74 | "모델은 두 노드가 엣지로 연결 되어 있을지 아닐지를 예측하는 비지도 학습적인 방식으로 학습될 것이고, \n", 75 | "학습된 표현은 최근접 이웃(nearest neighbor, NN) 검색 혹은 추후의 분류 모델 학습에 활용될 수 있겠죠. \n", 76 | "\n", 77 | "또, 목적 함수는 노드 분류를 위한 지도학습의 cross-entropy loss와 결합될 수 있습니다.\n" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "metadata": {}, 83 | "source": [ 84 | "\n", 85 | "## 데이터셋 로드하기\n", 86 | "\n", 87 | "이전 튜토리얼에서 전처리된 데이터셋을 직접 가져오겠습니다.\n" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 1, 93 | "metadata": {}, 94 | "outputs": [ 95 | { 96 | "name": "stderr", 97 | "output_type": "stream", 98 | "text": [ 99 | "Using backend: pytorch\n" 100 | ] 101 | } 102 | ], 103 | "source": [ 104 | "import dgl\n", 105 | "import torch\n", 106 | "import numpy as np\n", 107 | "import utils\n", 108 | "import pickle\n", 109 | "\n", 110 | "with open('data.pkl', 'rb') as f:\n", 111 | " data = pickle.load(f)\n", 112 | "graph, node_features, node_labels, train_nids, valid_nids, test_nids = data\n", 113 | "graph.create_formats_()" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": {}, 119 | "source": [ 120 | "## 이웃 샘플링으로 데이터 로더 정의하기\n", 121 | "\n", 122 | "노드 분류와는 다르게, 엣지에 걸쳐 iterate해야합니다. 그 뒤 이웃 샘플링과 GNN을 사용해 해당 노드들의 출력 표현을 계산해야 합니다. \n", 123 | "\n", 124 | "DGL은 `EdgeDataLoader`을 제공합니다. 이 메서드는 엣지 분류 혹은 링크 예측을 위해 엣지를 iterate하도록 도와줍니다. \n", 125 | "\n", 126 | "링크 예측을 수행하기 위해, negative sampler를 제공해 주어야 합니다. \n", 127 | "\n", 128 | "동질적(homogeneous) 그래프에서는, negative sample는 아래의 양식을 가진 어떤 callable 객체든 가능합니다. \n", 129 | "\n", 130 | "```python\n", 131 | "def negative_sampler(g: DGLGraph, eids: Tensor) -> Tuple[Tensor, Tensor]:\n", 132 | " pass\n", 133 | "```\n", 134 | "\n", 135 | "첫번째 인자는 원래 그래프이고, 두번째 인자는 엣지 ID의 미니배치를 의미합니다. \n", 136 | "이 함수는 $u$-$v^-$ 노드 ID 텐서의 쌍을 negative example로 반환합니다. \n", 137 | "\n", 138 | "\n", 139 | "다음 코드는 `k`개의 $v^-$를 각 $u$에 대해 $P^-(v) \\propto d(v)^{0.75}$의 분포를 따라 샘플링 함으로써,\n", 140 | "그래프 내에 존재하지 않는 엣지를 찾는 negative sampler 기능을 수행합니다. \n", 141 | "여기서 $d(v)$는 $v$의 차수(degree)를 의미합니다." 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 2, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "class NegativeSampler(object):\n", 151 | " def __init__(self, g, k):\n", 152 | " self.k = k\n", 153 | " self.weights = g.in_degrees().float() ** 0.75\n", 154 | " def __call__(self, g, eids):\n", 155 | " src, _ = g.find_edges(eids)\n", 156 | " src = src.repeat_interleave(self.k)\n", 157 | " dst = self.weights.multinomial(len(src), replacement=True)\n", 158 | " return src, dst" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "negative sampler를 정의한 뒤, edge 데이터 로더를 이웃 샘플링으로 정의할 수 있습니다. \n", 166 | "여기서는 1개의 positive example에 대해 5개의 negative example을 만들어 주겠습니다." 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": 3, 172 | "metadata": {}, 173 | "outputs": [], 174 | "source": [ 175 | "sampler = dgl.dataloading.MultiLayerNeighborSampler([4, 4, 4])\n", 176 | "k = 5\n", 177 | "train_dataloader = dgl.dataloading.EdgeDataLoader(\n", 178 | " graph, torch.arange(graph.number_of_edges()), sampler,\n", 179 | " negative_sampler=NegativeSampler(graph, k),\n", 180 | " batch_size=1024,\n", 181 | " shuffle=True,\n", 182 | " drop_last=False,\n", 183 | " num_workers=4\n", 184 | ")" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "`train_dataloader`에서 미니배치 하나를 뜯어볼까요?" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 4, 197 | "metadata": {}, 198 | "outputs": [ 199 | { 200 | "name": "stdout", 201 | "output_type": "stream", 202 | "text": [ 203 | "(tensor([1147853, 2426712, 1342, ..., 292546, 134170, 102404]), Graph(num_nodes=7141, num_edges=1024,\n", 204 | " ndata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64)}\n", 205 | " edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64)}), Graph(num_nodes=7141, num_edges=5120,\n", 206 | " ndata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64)}\n", 207 | " edata_schemes={}), [Block(num_src_nodes=230241, num_dst_nodes=112984, num_edges=415458), Block(num_src_nodes=112984, num_dst_nodes=33355, num_edges=126722), Block(num_src_nodes=33355, num_dst_nodes=7141, num_edges=28040)])\n" 208 | ] 209 | } 210 | ], 211 | "source": [ 212 | "example_minibatch = next(iter(train_dataloader))\n", 213 | "print(example_minibatch)" 214 | ] 215 | }, 216 | { 217 | "cell_type": "markdown", 218 | "metadata": {}, 219 | "source": [ 220 | "이 예제 미니배치는 4개의 구성요소로 이루어져 있습니다.\n", 221 | "\n", 222 | "* 출력 노드의 표현을 계산하기 위해 필요한 입력 노드 리스트\n", 223 | "* 미니배치 내에서 샘플링된 노드에서 유도된 subgraph (negative example의 노드 포함)와 미니배치 내에서 샘플링된 엣지들\n", 224 | "* 미니배치 내에서 샘플링된 노드에서 유도된 subgraph (negative example의 노드 포함)와 negative sampler에서 샘플링된 존재하지 않는 엣지들\n", 225 | "* bipartite 그래프의 리스트, 각 레이어마다 하나씩" 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": 5, 231 | "metadata": {}, 232 | "outputs": [ 233 | { 234 | "name": "stdout", 235 | "output_type": "stream", 236 | "text": [ 237 | "Number of input nodes: 230241\n", 238 | "Positive graph # nodes: 7141 # edges: 1024\n", 239 | "Negative graph # noeds: 7141 # edges: 5120\n", 240 | "[Block(num_src_nodes=230241, num_dst_nodes=112984, num_edges=415458), Block(num_src_nodes=112984, num_dst_nodes=33355, num_edges=126722), Block(num_src_nodes=33355, num_dst_nodes=7141, num_edges=28040)]\n" 241 | ] 242 | } 243 | ], 244 | "source": [ 245 | "input_nodes, pos_graph, neg_graph, bipartites = example_minibatch\n", 246 | "print('Number of input nodes:', len(input_nodes))\n", 247 | "print('Positive graph # nodes:', pos_graph.number_of_nodes(), '# edges:', pos_graph.number_of_edges())\n", 248 | "print('Negative graph # noeds:', neg_graph.number_of_nodes(), '# edges:', neg_graph.number_of_edges())\n", 249 | "print(bipartites)" 250 | ] 251 | }, 252 | { 253 | "cell_type": "markdown", 254 | "metadata": {}, 255 | "source": [ 256 | "## 노드 표현을 위한 모델 정의\n", 257 | "\n", 258 | "모델은 아래와 같이 정의됩니다." 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": 6, 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [ 267 | "import torch.nn as nn\n", 268 | "import torch.nn.functional as F\n", 269 | "import dgl.nn as dglnn\n", 270 | "\n", 271 | "class SAGE(nn.Module):\n", 272 | " def __init__(self, in_feats, n_hidden, n_layers):\n", 273 | " super().__init__()\n", 274 | " self.n_layers = n_layers\n", 275 | " self.n_hidden = n_hidden\n", 276 | " self.layers = nn.ModuleList()\n", 277 | " self.layers.append(dglnn.SAGEConv(in_feats, n_hidden, 'mean'))\n", 278 | " for i in range(1, n_layers):\n", 279 | " self.layers.append(dglnn.SAGEConv(n_hidden, n_hidden, 'mean'))\n", 280 | " \n", 281 | " def forward(self, bipartites, x):\n", 282 | " for l, (layer, bipartite) in enumerate(zip(self.layers, bipartites)):\n", 283 | " x = layer(bipartite, x)\n", 284 | " if l != self.n_layers - 1:\n", 285 | " x = F.relu(x)\n", 286 | " return x" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "## GNN에서 노드 표현 얻기\n", 294 | "\n", 295 | "이전 튜토리얼에서는, 이웃 샘플링 없이 GNN 모델의 offline 추론을 수행하는 것에 대해 이야기 했었죠. \n", 296 | "그 방법을 그대로 복붙해서, 비지도 학습 환경에서의 GNN으로부터 노드 표현 출력값을 계산하는 데 사용할 수 있겠습니다. " 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": 7, 302 | "metadata": {}, 303 | "outputs": [], 304 | "source": [ 305 | "def inference(model, graph, input_features, batch_size):\n", 306 | " nodes = torch.arange(graph.number_of_nodes())\n", 307 | " \n", 308 | " sampler = dgl.dataloading.MultiLayerNeighborSampler([None]) # one layer at a time, taking all neighbors\n", 309 | " dataloader = dgl.dataloading.NodeDataLoader(\n", 310 | " graph, nodes, sampler,\n", 311 | " batch_size=batch_size,\n", 312 | " shuffle=False,\n", 313 | " drop_last=False,\n", 314 | " num_workers=0)\n", 315 | " \n", 316 | " with torch.no_grad():\n", 317 | " for l, layer in enumerate(model.layers):\n", 318 | " # Allocate a buffer of output representations for every node\n", 319 | " # Note that the buffer is on CPU memory.\n", 320 | " output_features = torch.zeros(graph.number_of_nodes(), model.n_hidden)\n", 321 | "\n", 322 | " for input_nodes, output_nodes, bipartites in tqdm.tqdm(dataloader):\n", 323 | " bipartite = bipartites[0].to(torch.device('cuda'))\n", 324 | "\n", 325 | " x = input_features[input_nodes].cuda()\n", 326 | "\n", 327 | " # the following code is identical to the loop body in model.forward()\n", 328 | " x = layer(bipartite, x)\n", 329 | " if l != model.n_layers - 1:\n", 330 | " x = F.relu(x)\n", 331 | "\n", 332 | " output_features[output_nodes] = x.cpu()\n", 333 | " input_features = output_features\n", 334 | " return output_features" 335 | ] 336 | }, 337 | { 338 | "cell_type": "markdown", 339 | "metadata": {}, 340 | "source": [ 341 | "## 엣지 스코어 예측 모델 정의하기\n", 342 | "\n", 343 | "미니 배치에서 필요한 노드 표현을 얻은 위에는, \n", 344 | "샘플링된 미니 배치의 존재하는/존재하지 않는 엣지에 대한 스코어를 예측하고 싶겠죠? \n", 345 | "\n", 346 | "이는 `apply_edges` 메서드로 쉽게 구현할 수 있습니다. \n", 347 | "여기서는, 두 대상 노드의 표현의 내적을 계산함으로써 단순히 예산할 수 있습니다." 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": 8, 353 | "metadata": {}, 354 | "outputs": [], 355 | "source": [ 356 | "class ScorePredictor(nn.Module):\n", 357 | " def forward(self, subgraph, x):\n", 358 | " with subgraph.local_scope():\n", 359 | " subgraph.ndata['x'] = x\n", 360 | " subgraph.apply_edges(dgl.function.u_dot_v('x', 'x', 'score'))\n", 361 | " return subgraph.edata['score']" 362 | ] 363 | }, 364 | { 365 | "cell_type": "markdown", 366 | "metadata": {}, 367 | "source": [ 368 | "## 학습된 임베딩의 성능 평가하기\n", 369 | "\n", 370 | "이 튜토리얼에서, 출력 임베딩을 학습 셋의 입력으로 사용해, 선형 분류 모델을 학습하여 출력 임베딩의 성능을 평가할 예정입니다. \n", 371 | "그 뒤, 검증/테스트 셋에 대해 정확도를 측정해 보겠습니다. " 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": 9, 377 | "metadata": {}, 378 | "outputs": [], 379 | "source": [ 380 | "import sklearn.linear_model\n", 381 | "import sklearn.metrics\n", 382 | "def evaluate(emb, label, train_nids, valid_nids, test_nids):\n", 383 | " classifier = sklearn.linear_model.LogisticRegression(solver='lbfgs', multi_class='multinomial', verbose=1, max_iter=1000)\n", 384 | " classifier.fit(emb[train_nids], label[train_nids])\n", 385 | " valid_pred = classifier.predict(emb[valid_nids])\n", 386 | " test_pred = classifier.predict(emb[test_nids])\n", 387 | " valid_acc = sklearn.metrics.accuracy_score(label[valid_nids], valid_pred)\n", 388 | " test_acc = sklearn.metrics.accuracy_score(label[test_nids], test_pred)\n", 389 | " return valid_acc, test_acc" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "metadata": {}, 395 | "source": [ 396 | "## 학습 루프 정의하기\n", 397 | "\n", 398 | "다음 코드는 모델을 초기화하고 최적화기(optimizer)를 정의합니다.\n" 399 | ] 400 | }, 401 | { 402 | "cell_type": "code", 403 | "execution_count": 10, 404 | "metadata": {}, 405 | "outputs": [], 406 | "source": [ 407 | "model = SAGE(node_features.shape[1], 128, 3).cuda()\n", 408 | "predictor = ScorePredictor().cuda()\n", 409 | "opt = torch.optim.Adam(list(model.parameters()) + list(predictor.parameters()))" 410 | ] 411 | }, 412 | { 413 | "cell_type": "markdown", 414 | "metadata": {}, 415 | "source": [ 416 | "아래는 비지도 학습과 평가를 수행하는 학습 루프로, \n", 417 | "validation set에 대해 최적의 성능을 보이는 모델을 저장하는 기능도 포함하고 있습니다." 418 | ] 419 | }, 420 | { 421 | "cell_type": "code", 422 | "execution_count": null, 423 | "metadata": {}, 424 | "outputs": [ 425 | { 426 | "name": "stderr", 427 | "output_type": "stream", 428 | "text": [ 429 | " 44%|████▍ | 26699/60410 [1:45:31<2:11:56, 4.26it/s, loss=0.614]" 430 | ] 431 | } 432 | ], 433 | "source": [ 434 | "import tqdm\n", 435 | "import sklearn.metrics\n", 436 | "\n", 437 | "best_accuracy = 0\n", 438 | "best_model_path = 'model.pt'\n", 439 | "for epoch in range(10):\n", 440 | " model.train()\n", 441 | " \n", 442 | " with tqdm.tqdm(train_dataloader) as tq:\n", 443 | " for step, (input_nodes, pos_graph, neg_graph, bipartites) in enumerate(tq):\n", 444 | " bipartites = [b.to(torch.device('cuda')) for b in bipartites]\n", 445 | " pos_graph = pos_graph.to(torch.device('cuda'))\n", 446 | " neg_graph = neg_graph.to(torch.device('cuda'))\n", 447 | " inputs = node_features[input_nodes].cuda()\n", 448 | " outputs = model(bipartites, inputs)\n", 449 | " pos_score = predictor(pos_graph, outputs)\n", 450 | " neg_score = predictor(neg_graph, outputs)\n", 451 | " \n", 452 | " score = torch.cat([pos_score, neg_score])\n", 453 | " label = torch.cat([torch.ones_like(pos_score), torch.zeros_like(neg_score)])\n", 454 | " loss = F.binary_cross_entropy_with_logits(score, label)\n", 455 | " \n", 456 | " opt.zero_grad()\n", 457 | " loss.backward()\n", 458 | " opt.step()\n", 459 | " \n", 460 | " tq.set_postfix({'loss': '%.03f' % loss.item()}, refresh=False)\n", 461 | " \n", 462 | " model.eval()\n", 463 | " emb = inference(model, graph, node_features, 16384)\n", 464 | " valid_acc, test_acc = evaluate(emb.numpy(), node_labels.numpy())\n", 465 | " print('Epoch {} Validation Accuracy {} Test Accuracy {}'.format(epoch, valid_acc, test_acc))\n", 466 | " if best_accuracy < valid_acc:\n", 467 | " best_accuracy = valid_acc\n", 468 | " torch.save(model.state_dict(), best_model_path)" 469 | ] 470 | }, 471 | { 472 | "cell_type": "markdown", 473 | "metadata": {}, 474 | "source": [ 475 | "## 결론\n", 476 | "\n", 477 | "이 튜토리얼에서, 비지도 학습 방식으로 다중 레이어 GraphSAGE 모델을 학습하는 방법을 GPU에 올라가지 않는 대규모 데이터셋의 링크 예측을 통해 배워 보았습니다. \n", 478 | "여기서 배운 이 방법은 어떤 사이즈의 그래프에 대해서도 확장될 수 있고, 단일 머신의 1개 GPU로도 작동합니다." 479 | ] 480 | }, 481 | { 482 | "cell_type": "markdown", 483 | "metadata": {}, 484 | "source": [ 485 | "## 다음은 무엇을 배우나요?\n", 486 | "\n", 487 | "다음 튜토리얼은 학습 절차를 단일 머신의 다중 GPU에 대해 scale-out하는 방법에 대해 배웁니다." 488 | ] 489 | } 490 | ], 491 | "metadata": { 492 | "kernelspec": { 493 | "display_name": "Python 3", 494 | "language": "python", 495 | "name": "python3" 496 | }, 497 | "language_info": { 498 | "codemirror_mode": { 499 | "name": "ipython", 500 | "version": 3 501 | }, 502 | "file_extension": ".py", 503 | "mimetype": "text/x-python", 504 | "name": "python", 505 | "nbconvert_exporter": "python", 506 | "pygments_lexer": "ipython3", 507 | "version": "3.8.3" 508 | } 509 | }, 510 | "nbformat": 4, 511 | "nbformat_minor": 4 512 | } 513 | -------------------------------------------------------------------------------- /large_graph/.ipynb_checkpoints/3_single_machine_multiple_GPU_training-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 다중 GPU를 사용한 GNN의 확률적(Storchastic) 학습 \n", 8 | "\n", 9 | "\n", 10 | "이번 튜토리얼에서는 Multi GPU 환경에서 노드 분류를 위한 다중 레이어 GraphSAGE 모델을 학습하는 방법을 배워보겠습니다. \n", 11 | "사용할 데이터셋은 OGB에서 제공하는 Amazon Copurchase Network으로, 240만 노드와 6100만 엣지를 포함하고 있으므로, 단일 GPU에는 올라가지 않습니다. \n", 12 | "\n", 13 | "\n", 14 | "이 튜토리얼은 다음 내용을 포함하고 있습니다. \n", 15 | "\n", 16 | "* `torch.nn.parallel.DistributedDataParallel` 메서드를 사용해 그래프 크기에 상관없이 GNN 모델을 단일 머신, 다중 GPU으로 학습하기.\n", 17 | "\n", 18 | "PyTorch `DistributedDataParallel` (혹은 짧게 말해 DDP)는 multi-GPU 학습의 일반적인 해결책입니다. \n", 19 | "DGL과 PyTorch DDP를 결합하는 것은 매우 쉬운데, 평범한 PyTorch 어플리케이션에서 적용하는 방법과 같이 하면 됩니다.\n", 20 | "\n", 21 | "* 데이터를 각 GPU에 대해 분할하기\n", 22 | "* PyTorch DDP를 사용해 모델 파라미터를 분배합니다\n", 23 | "* 이웃 샘플링 전략을 각자의 방법으로 커스터마이징합니다." 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 1, 29 | "metadata": {}, 30 | "outputs": [ 31 | { 32 | "name": "stderr", 33 | "output_type": "stream", 34 | "text": [ 35 | "Using backend: pytorch\n" 36 | ] 37 | } 38 | ], 39 | "source": [ 40 | "import numpy as np\n", 41 | "import dgl\n", 42 | "import torch\n", 43 | "import dgl.nn as dglnn\n", 44 | "import torch.nn as nn\n", 45 | "from torch.nn.parallel import DistributedDataParallel\n", 46 | "import torch.nn.functional as F\n", 47 | "import torch.multiprocessing as mp\n", 48 | "import sklearn.metrics\n", 49 | "import tqdm\n", 50 | "\n", 51 | "import utils" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "## 데이터셋 로드하기\n", 59 | "\n", 60 | "아래 코드는 첫번째 튜토리얼에서 복사되었습니다." 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 2, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "def load_data():\n", 70 | " import pickle\n", 71 | "\n", 72 | " with open('data.pkl', 'rb') as f:\n", 73 | " data = pickle.load(f)\n", 74 | " graph, node_features, node_labels, train_nids, valid_nids, test_nids = data\n", 75 | " utils.prepare_mp(graph)\n", 76 | " \n", 77 | " num_features = node_features.shape[1]\n", 78 | " num_classes = (node_labels.max() + 1).item()\n", 79 | " \n", 80 | " return graph, node_features, node_labels, train_nids, valid_nids, test_nids, num_features, num_classes" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "## 이웃 샘플링 커스터마이징하기\n", 88 | "\n", 89 | "이전 튜토리얼에서, `NodeDataLoader`와 `MultiLayerNeighborSampler`를 사용하는 방법을 배워 보았습니다. \n", 90 | "사실, `MultiLayerNeighborSampler`를 우리 마음대로 정한 샘플링 전략으로 대체할 수 있습니다. \n", 91 | "\n", 92 | "커스터마이징은 간단합니다. \n", 93 | "각 GNN 레이어에 대해, message passing에서 포함되는 엣지를 그래프로 지정해주면 됩니다. \n", 94 | "이 그래프는 기존 그래프와 같은 노드를 갖게 됩니다. \n", 95 | "\n", 96 | "예를 들어, `MultiLayerNeighborSampler`는 아래와 같이 구현됩니다." 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 3, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "class MultiLayerNeighborSampler(dgl.dataloading.BlockSampler):\n", 106 | " def __init__(self, fanouts):\n", 107 | " super().__init__(len(fanouts), return_eids=False)\n", 108 | " self.fanouts = fanouts\n", 109 | " \n", 110 | " def sample_frontier(self, layer_id, g, seed_nodes):\n", 111 | " fanout = self.fanouts[layer_id]\n", 112 | " return dgl.sampling.sample_neighbors(g, seed_nodes, fanout)" 113 | ] 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "metadata": {}, 118 | "source": [ 119 | "## Distributed Data Parallel (DDP)를 위한 데이터 로더 정의하기\n", 120 | "\n", 121 | "PyTorch DDP에서, 각 worker process는 정수값인 *rank*로 할당됩니다. \n", 122 | "이 rank는 worker process가 데이터셋의 어떤 파티션을 처리할지를 나타냅니다. \n", 123 | "\n", 124 | "따라서 데이터 로더 관점에서의 단일 GPU 경우와 다중 GPU 학습 간 유일한 차이점은, \n", 125 | "데이터 로더가 노드의 일부 파티션에 대해서만 iterate한다는 점입니다.\n" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 4, 131 | "metadata": {}, 132 | "outputs": [], 133 | "source": [ 134 | "def create_dataloader(rank, world_size, graph, nids):\n", 135 | " partition_size = len(nids) // world_size\n", 136 | " partition_offset = partition_size * rank\n", 137 | " nids = nids[partition_offset:partition_offset+partition_size]\n", 138 | " \n", 139 | " sampler = MultiLayerNeighborSampler([4, 4, 4])\n", 140 | " dataloader = dgl.dataloading.NodeDataLoader(\n", 141 | " graph, nids, sampler,\n", 142 | " batch_size=1024,\n", 143 | " shuffle=True,\n", 144 | " drop_last=False,\n", 145 | " num_workers=0\n", 146 | " )\n", 147 | " \n", 148 | " return dataloader" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "## 모델 정의하기\n", 156 | "\n", 157 | "모델 구현은 첫번째 튜토리얼에서 본 것과 정확히 동일합니다." 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": 5, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "class SAGE(nn.Module):\n", 167 | " def __init__(self, in_feats, n_hidden, n_classes, n_layers):\n", 168 | " super().__init__()\n", 169 | " self.n_layers = n_layers\n", 170 | " self.n_hidden = n_hidden\n", 171 | " self.n_classes = n_classes\n", 172 | " self.layers = nn.ModuleList()\n", 173 | " self.layers.append(dglnn.SAGEConv(in_feats, n_hidden, 'mean'))\n", 174 | " for i in range(1, n_layers - 1):\n", 175 | " self.layers.append(dglnn.SAGEConv(n_hidden, n_hidden, 'mean'))\n", 176 | " self.layers.append(dglnn.SAGEConv(n_hidden, n_classes, 'mean'))\n", 177 | " \n", 178 | " def forward(self, bipartites, x):\n", 179 | " for l, (layer, bipartite) in enumerate(zip(self.layers, bipartites)):\n", 180 | " x = layer(bipartite, x)\n", 181 | " if l != self.n_layers - 1:\n", 182 | " x = F.relu(x)\n", 183 | " return x" 184 | ] 185 | }, 186 | { 187 | "cell_type": "markdown", 188 | "metadata": {}, 189 | "source": [ 190 | "## 모델을 여러 GPU에 분배하기\n", 191 | "\n", 192 | "PyTorch DDP는 모델의 분산과 가중치의 synchronization을 관리해 줍니다. \n", 193 | "DGL에서는, 모델을 단순히 `torch.nn.parallel.DistributedDataParallel`으로 감싸 줌으로써 이 PyTorch DDP의 이점을 그대로 누릴 수 있습니다.\n", 194 | "\n", 195 | "분산 학습에서 추천되는 방식은 한 GPU에 학습 process를 하나만 가져가는 것입니다. \n", 196 | "이로써, 모델 instantiation 중에 process rank를 지정해줄 수도 있게 되는데, 이 rank가 GPU ID와 동일해지게 됩니다." 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 6, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "def init_model(rank, in_feats, n_hidden, n_classes, n_layers):\n", 206 | " model = SAGE(in_feats, n_hidden, n_classes, n_layers).to(rank)\n", 207 | " return DistributedDataParallel(model, device_ids=[rank], output_device=rank)" 208 | ] 209 | }, 210 | { 211 | "cell_type": "markdown", 212 | "metadata": {}, 213 | "source": [ 214 | "## 1개 process를 위한 학습 루프\n", 215 | "\n", 216 | "학습 루프는 다른 PyTorch DDP 어플리케이션과 똑같이 생겼습니다." 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": 7, 222 | "metadata": {}, 223 | "outputs": [], 224 | "source": [ 225 | "@utils.fix_openmp\n", 226 | "def train(rank, world_size, data):\n", 227 | " # data is the output of load_data\n", 228 | " torch.distributed.init_process_group(\n", 229 | " backend='nccl',\n", 230 | " init_method='tcp://127.0.0.1:12345',\n", 231 | " world_size=world_size,\n", 232 | " rank=rank)\n", 233 | " torch.cuda.set_device(rank)\n", 234 | " \n", 235 | " graph, node_features, node_labels, train_nids, valid_nids, test_nids, num_features, num_classes = data\n", 236 | " \n", 237 | " train_dataloader = create_dataloader(rank, world_size, graph, train_nids)\n", 238 | " # We only use one worker for validation\n", 239 | " valid_dataloader = create_dataloader(0, 1, graph, valid_nids)\n", 240 | " \n", 241 | " model = init_model(rank, num_features, 128, num_classes, 3)\n", 242 | " opt = torch.optim.Adam(model.parameters())\n", 243 | " torch.distributed.barrier()\n", 244 | " \n", 245 | " best_accuracy = 0\n", 246 | " best_model_path = 'model.pt'\n", 247 | " for epoch in range(10):\n", 248 | " model.train()\n", 249 | "\n", 250 | " for step, (input_nodes, output_nodes, bipartites) in enumerate(train_dataloader):\n", 251 | " bipartites = [b.to(rank) for b in bipartites]\n", 252 | " inputs = node_features[input_nodes].cuda()\n", 253 | " labels = node_labels[output_nodes].cuda()\n", 254 | " predictions = model(bipartites, inputs)\n", 255 | "\n", 256 | " loss = F.cross_entropy(predictions, labels)\n", 257 | " opt.zero_grad()\n", 258 | " loss.backward()\n", 259 | " opt.step()\n", 260 | "\n", 261 | " accuracy = sklearn.metrics.accuracy_score(labels.cpu().numpy(), predictions.argmax(1).detach().cpu().numpy())\n", 262 | "\n", 263 | " if rank == 0 and step % 10 == 0:\n", 264 | " print('Epoch {:05d} Step {:05d} Loss {:.04f}'.format(epoch, step, loss.item()))\n", 265 | "\n", 266 | " torch.distributed.barrier()\n", 267 | " \n", 268 | " if rank == 0:\n", 269 | " model.eval()\n", 270 | " predictions = []\n", 271 | " labels = []\n", 272 | " with torch.no_grad():\n", 273 | " for input_nodes, output_nodes, bipartites in valid_dataloader:\n", 274 | " bipartites = [b.to(rank) for b in bipartites]\n", 275 | " inputs = node_features[input_nodes].cuda()\n", 276 | " labels.append(node_labels[output_nodes].numpy())\n", 277 | " predictions.append(model.module(bipartites, inputs).argmax(1).cpu().numpy())\n", 278 | " predictions = np.concatenate(predictions)\n", 279 | " labels = np.concatenate(labels)\n", 280 | " accuracy = sklearn.metrics.accuracy_score(labels, predictions)\n", 281 | " print('Epoch {} Validation Accuracy {}'.format(epoch, accuracy))\n", 282 | " if best_accuracy < accuracy:\n", 283 | " best_accuracy = accuracy\n", 284 | " torch.save(model.module.state_dict(), best_model_path)\n", 285 | " \n", 286 | " torch.distributed.barrier()" 287 | ] 288 | }, 289 | { 290 | "cell_type": "code", 291 | "execution_count": 8, 292 | "metadata": {}, 293 | "outputs": [ 294 | { 295 | "name": "stdout", 296 | "output_type": "stream", 297 | "text": [ 298 | "Epoch 00000 Step 00000 Loss 5.7553\n", 299 | "Epoch 00000 Step 00010 Loss 2.6858\n", 300 | "Epoch 00000 Step 00020 Loss 2.1455\n", 301 | "Epoch 00000 Step 00030 Loss 1.7148\n", 302 | "Epoch 00000 Step 00040 Loss 1.6470\n", 303 | "Epoch 0 Validation Accuracy 0.7247158151717824\n", 304 | "Epoch 00001 Step 00000 Loss 1.3390\n", 305 | "Epoch 00001 Step 00010 Loss 1.3108\n", 306 | "Epoch 00001 Step 00020 Loss 1.3176\n", 307 | "Epoch 00001 Step 00030 Loss 1.4312\n", 308 | "Epoch 00001 Step 00040 Loss 1.1797\n", 309 | "Epoch 1 Validation Accuracy 0.7972687739999491\n", 310 | "Epoch 00002 Step 00000 Loss 1.0574\n", 311 | "Epoch 00002 Step 00010 Loss 1.1461\n", 312 | "Epoch 00002 Step 00020 Loss 1.0746\n", 313 | "Epoch 00002 Step 00030 Loss 1.0027\n", 314 | "Epoch 00002 Step 00040 Loss 0.9308\n", 315 | "Epoch 2 Validation Accuracy 0.8152480736464665\n", 316 | "Epoch 00003 Step 00000 Loss 0.9768\n", 317 | "Epoch 00003 Step 00010 Loss 1.0767\n", 318 | "Epoch 00003 Step 00020 Loss 0.9237\n", 319 | "Epoch 00003 Step 00030 Loss 1.0979\n", 320 | "Epoch 00003 Step 00040 Loss 0.8528\n", 321 | "Epoch 3 Validation Accuracy 0.83111664928922\n", 322 | "Epoch 00004 Step 00000 Loss 0.9134\n", 323 | "Epoch 00004 Step 00010 Loss 0.9284\n", 324 | "Epoch 00004 Step 00020 Loss 0.8158\n", 325 | "Epoch 00004 Step 00030 Loss 0.9542\n", 326 | "Epoch 00004 Step 00040 Loss 0.9215\n", 327 | "Epoch 4 Validation Accuracy 0.839508684484907\n", 328 | "Epoch 00005 Step 00000 Loss 0.9607\n", 329 | "Epoch 00005 Step 00010 Loss 0.9081\n", 330 | "Epoch 00005 Step 00020 Loss 0.8607\n", 331 | "Epoch 00005 Step 00030 Loss 0.8400\n", 332 | "Epoch 00005 Step 00040 Loss 0.8883\n", 333 | "Epoch 5 Validation Accuracy 0.8434249675762276\n", 334 | "Epoch 00006 Step 00000 Loss 0.7871\n", 335 | "Epoch 00006 Step 00010 Loss 0.9050\n", 336 | "Epoch 00006 Step 00020 Loss 0.8587\n", 337 | "Epoch 00006 Step 00030 Loss 0.7345\n", 338 | "Epoch 00006 Step 00040 Loss 0.7846\n", 339 | "Epoch 6 Validation Accuracy 0.8497317091778348\n", 340 | "Epoch 00007 Step 00000 Loss 0.7165\n", 341 | "Epoch 00007 Step 00010 Loss 0.8370\n", 342 | "Epoch 00007 Step 00020 Loss 0.8072\n", 343 | "Epoch 00007 Step 00030 Loss 0.7852\n", 344 | "Epoch 00007 Step 00040 Loss 0.8651\n", 345 | "Epoch 7 Validation Accuracy 0.853012232027058\n", 346 | "Epoch 00008 Step 00000 Loss 0.8609\n", 347 | "Epoch 00008 Step 00010 Loss 0.6784\n", 348 | "Epoch 00008 Step 00020 Loss 0.7328\n", 349 | "Epoch 00008 Step 00030 Loss 0.8150\n", 350 | "Epoch 00008 Step 00040 Loss 0.8347\n", 351 | "Epoch 8 Validation Accuracy 0.852732497520535\n", 352 | "Epoch 00009 Step 00000 Loss 0.7051\n", 353 | "Epoch 00009 Step 00010 Loss 0.7738\n", 354 | "Epoch 00009 Step 00020 Loss 0.8157\n", 355 | "Epoch 00009 Step 00030 Loss 0.7437\n", 356 | "Epoch 00009 Step 00040 Loss 0.7249\n", 357 | "Epoch 9 Validation Accuracy 0.8549703735727182\n" 358 | ] 359 | } 360 | ], 361 | "source": [ 362 | "if __name__ == '__main__':\n", 363 | " procs = []\n", 364 | " data = load_data()\n", 365 | " for proc_id in range(4): # 4 gpus\n", 366 | " p = mp.Process(target=train, args=(proc_id, 4, data))\n", 367 | " p.start()\n", 368 | " procs.append(p)\n", 369 | " for p in procs:\n", 370 | " p.join()" 371 | ] 372 | }, 373 | { 374 | "cell_type": "markdown", 375 | "metadata": {}, 376 | "source": [ 377 | "## 결론\n", 378 | "\n", 379 | "이 튜토리얼에서, GPU에 올라가지 않는 대규모 데이터에서 노드 분류를 위한 다중 레이어 GraphSAGE 모델을 학습하는 방법을 배웠습니다. \n", 380 | "여기서 배운 이 방법은 어떤 사이즈의 그래프에서든 확장될 수 있으며, \n", 381 | "단일 머신의 *몇 개의 GPU 에서든* 작동합니다." 382 | ] 383 | }, 384 | { 385 | "cell_type": "markdown", 386 | "metadata": {}, 387 | "source": [ 388 | "## 추가 자료: DDP로 학습할 때의 주의점\n", 389 | "\n", 390 | "DDP 코드를 작성할 때, 이 두가지 에러를 겪을 수 있습니다. \n", 391 | "\n", 392 | "* `Cannot re-initialize CUDA in forked subprocess` \n", 393 | "\n", 394 | " 이는 `mp.Process`를 사용해 subprocess를 만들기 전에 CUDA context를 초기화 해서 발생합니다.\n", 395 | " 해결책은 다음과 같습니다. \n", 396 | " \n", 397 | " * `mp.Process`를 호출하기 전에, CUDA context를 초기화할 수 있는 모든 가능한 코드를 제거합니다. \n", 398 | " 예를 들어, `mp.Process`를 호출하기 전에 GPU의 갯수를 `torch.cuda.device_count()`로 확인할 수 없습니다. \n", 399 | " 왜냐하면, 갯수를 확인하는 `torch.cuda.device_count()`는 CUDA context를 초기화하기 때문입니다. \n", 400 | " \n", 401 | " CUDA context가 초기화 되었는지의 여부를 `torch.cuda.is_initialized()`로 확인해볼 수 있습니다.\n", 402 | " \n", 403 | " * `mp.Process`로 forking하지 마시고, `torch.multiprocessing.spawn()`를 사용해 process를 생성하세요. \n", 404 | " (전자 방식의) 불리점은, 파이썬이 이 방법으로 생성된 모든 process에 대해 그래프 storage를 복제한다는 점입니다. \n", 405 | " 메모리 소비량이 선형적으로 증가하게 되지요.\n", 406 | " \n", 407 | "* 학습 프로세스가 미니배치 iteration중에 멈춤\n", 408 | " 이 원인은 다음과 같습니다. [lasting bug in the interaction between GNU OpenMP and `fork`](https://github.com/pytorch/pytorch/issues/17199) \n", 409 | " 다른 해결책은, `mp.Process`의 목표 함수를 데코레이터 `utils.fix_openmp`를 사용해 감싸는 것입니다. \n", 410 | " 이 방식은 이 튜토리얼에서 구현되어 있습니다." 411 | ] 412 | } 413 | ], 414 | "metadata": { 415 | "kernelspec": { 416 | "display_name": "Python 3", 417 | "language": "python", 418 | "name": "python3" 419 | }, 420 | "language_info": { 421 | "codemirror_mode": { 422 | "name": "ipython", 423 | "version": 3 424 | }, 425 | "file_extension": ".py", 426 | "mimetype": "text/x-python", 427 | "name": "python", 428 | "nbconvert_exporter": "python", 429 | "pygments_lexer": "ipython3", 430 | "version": "3.8.3" 431 | } 432 | }, 433 | "nbformat": 4, 434 | "nbformat_minor": 4 435 | } 436 | -------------------------------------------------------------------------------- /large_graph/1_node_classification.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 대규모 그래프에서의 노드 분류를 위한 GNN의 확률적(Stochastic) 학습\n", 8 | "\n", 9 | "이번 튜토리얼에서는, OGB에서 제공하는 Amazon Copurchase Network 데이터로 노드 분류를 수행하는 멀티 레이어 GraphSAGE를 학습하는 방법을 배워 봅니다. \n", 10 | "이 데이터셋은 240만 노드와 6,100만 엣지를 포함하며, 따라서 단독 GPU에 모두 올려 사용할 수 없습니다. \n", 11 | "\n", 12 | "이번 튜토리얼의 컨텐츠는 다음을 포함합니다. \n", 13 | "\n", 14 | "* CSV 형식과 같은 형식으로 저장된 자기만의 데이터로 DGL 그래프 만들기\n", 15 | "* GNN 모델을 1개의 머신으로, 1개의 GPU만을 사용해, 어떤 크기의 그래프든 학습하기" 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "## 데이터셋 로드하기\n", 23 | "\n", 24 | "\n", 25 | "OGB에서 제공하는 파이썬 패키지를 직접 사용할 수 있지만, 설명을 위해 수동으로 데이터셋을 다운받고, 내용물을 확인하고, 오직 `numpy`로만 처리하겠습니다. " 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 2, 31 | "metadata": {}, 32 | "outputs": [ 33 | { 34 | "name": "stdout", 35 | "output_type": "stream", 36 | "text": [ 37 | "--2021-02-18 05:48:29-- https://snap.stanford.edu/ogb/data/nodeproppred/products.zip\n", 38 | "Resolving snap.stanford.edu (snap.stanford.edu)... 171.64.75.80\n", 39 | "Connecting to snap.stanford.edu (snap.stanford.edu)|171.64.75.80|:443... connected.\n", 40 | "HTTP request sent, awaiting response... 200 OK\n", 41 | "Length: 1480993786 (1.4G) [application/zip]\n", 42 | "Saving to: ‘products.zip’\n", 43 | "\n", 44 | "products.zip 100%[===================>] 1.38G 11.5MB/s in 1m 47s \n", 45 | "\n", 46 | "2021-02-18 05:50:17 (13.2 MB/s) - ‘products.zip’ saved [1480993786/1480993786]\n", 47 | "\n" 48 | ] 49 | } 50 | ], 51 | "source": [ 52 | "!wget https://snap.stanford.edu/ogb/data/nodeproppred/products.zip" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 4, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "name": "stdout", 62 | "output_type": "stream", 63 | "text": [ 64 | "Archive: products.zip\n", 65 | " creating: products/\n", 66 | " creating: products/split/\n", 67 | " creating: products/split/sales_ranking/\n", 68 | " inflating: products/split/sales_ranking/test.csv.gz \n", 69 | " inflating: products/split/sales_ranking/train.csv.gz \n", 70 | " inflating: products/split/sales_ranking/valid.csv.gz \n", 71 | " creating: products/processed/\n", 72 | " creating: products/raw/\n", 73 | " inflating: products/raw/node-label.csv.gz \n", 74 | " extracting: products/raw/num-node-list.csv.gz \n", 75 | " extracting: products/raw/num-edge-list.csv.gz \n", 76 | " inflating: products/raw/node-feat.csv.gz \n", 77 | " inflating: products/raw/edge.csv.gz \n", 78 | " creating: products/mapping/\n", 79 | " inflating: products/mapping/README.md \n", 80 | " extracting: products/mapping/labelidx2productcategory.csv.gz \n", 81 | " inflating: products/mapping/nodeidx2asin.csv.gz \n", 82 | " inflating: products/RELEASE_v1.txt \n" 83 | ] 84 | } 85 | ], 86 | "source": [ 87 | "!unzip -o products.zip" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "metadata": {}, 93 | "source": [ 94 | "이 데이터셋에는 다음 파일들이 포함되어 있습니다:\n", 95 | "\n", 96 | "* `products/raw/edge.csv` (source-destination pairs)\n", 97 | "* `products/raw/node-feat.csv` (node features)\n", 98 | "* `products/raw/node-label.csv` (node labels)\n", 99 | "* `products/raw/num-edge-list.csv` (number of edges)\n", 100 | "* `products/raw/num-node-list.csv` (number of nodes)\n", 101 | "\n", 102 | "이 중에서 처음 3개의 csv 파일만을 사용하겠습니다. \n", 103 | "\n", 104 | "추가로, 이 데이터셋에는 학습-검증-테스트셋 분할을 정의하는 파일들이 `products/split/sales_ranking` 디렉터리에 포함되어 있습니다. \n", 105 | "`train.csv`, `valid.csv` 그리고 `test.csv` 모두는 학습/검증/테스트셋의 노드 ID가 한 줄에 하나씩 포함된 텍스트 파일입니다. \n", 106 | "\n", 107 | "\n", 108 | "
\n", 109 | " 주의: 노드 ID는 0부터 (전체 노드의 숫자-1)까지 이어지는 정수여야 합니다. 만약 노드 ID가 연속되지 않거나 0부터 시작된다면(가령, 100000부터 시작한다던지.), \n", 110 | " 라벨을 직접 다시 달아주어야 합니다. 판다스 데이터프레임의 astype 메서드는 ID들의 타입을 \"category\"로 바꾸어 줌으로써 간편하게 재라벨링할 수 있습니다. \n", 111 | "
" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 5, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "import pandas as pd\n", 121 | "edges = pd.read_csv('products/raw/edge.csv.gz', header=None).values\n", 122 | "node_features = pd.read_csv('products/raw/node-feat.csv.gz', header=None).values\n", 123 | "node_labels = pd.read_csv('products/raw/node-label.csv.gz', header=None).values[:, 0]\n", 124 | "\n", 125 | "# pd.read_csv는 칼럼 1개짜리 데이터프레임을 호출하므로, 1차원 배열로 만들어줍니다.\n", 126 | "train_nids = pd.read_csv('products/split/sales_ranking/train.csv.gz', header=None).values[:, 0]\n", 127 | "valid_nids = pd.read_csv('products/split/sales_ranking/valid.csv.gz', header=None).values[:, 0]\n", 128 | "test_nids = pd.read_csv('products/split/sales_ranking/test.csv.gz', header=None).values[:, 0]" 129 | ] 130 | }, 131 | { 132 | "cell_type": "markdown", 133 | "metadata": {}, 134 | "source": [ 135 | "아래와 같이 그래프를 구축합니다." 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": 6, 141 | "metadata": {}, 142 | "outputs": [ 143 | { 144 | "name": "stderr", 145 | "output_type": "stream", 146 | "text": [ 147 | "Using backend: pytorch\n" 148 | ] 149 | } 150 | ], 151 | "source": [ 152 | "import dgl\n", 153 | "import torch\n", 154 | "\n", 155 | "graph = dgl.graph((edges[:, 0], edges[:, 1]))\n", 156 | "node_features = torch.FloatTensor(node_features)\n", 157 | "node_labels = torch.LongTensor(node_labels)\n", 158 | "\n", 159 | "# 그래프와 피처, 그리고 학습-검증-테스트 분할 정보를 이후의 튜토리얼에서 사용하기 위해 분할합니다.\n", 160 | "\n", 161 | "import pickle\n", 162 | "with open('data.pkl', 'wb') as f:\n", 163 | " pickle.dump((graph, node_features, node_labels, train_nids, valid_nids, test_nids), f)" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": 7, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "# 저장한 파일로부터 그래프를 다시 호출합니다.\n", 173 | "\n", 174 | "import dgl\n", 175 | "import torch\n", 176 | "import numpy as np\n", 177 | "import pickle\n", 178 | "with open('data.pkl', 'rb') as f:\n", 179 | " graph, node_features, node_labels, train_nids, valid_nids, test_nids = pickle.load(f)" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "metadata": {}, 185 | "source": [ 186 | "그래프, 피처, 라벨의 사이즈를 아래와 같이 확인할 수 있습니다." 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": 28, 192 | "metadata": {}, 193 | "outputs": [ 194 | { 195 | "name": "stdout", 196 | "output_type": "stream", 197 | "text": [ 198 | "그래프 정보\n", 199 | "Graph(num_nodes=2449029, num_edges=61859140,\n", 200 | " ndata_schemes={}\n", 201 | " edata_schemes={})\n", 202 | "노드 피처의 shape: torch.Size([2449029, 100])\n", 203 | "노드 라벨의 shape: torch.Size([2449029])\n", 204 | "클래스의 수: 47\n" 205 | ] 206 | } 207 | ], 208 | "source": [ 209 | "print('그래프 정보')\n", 210 | "print(graph)\n", 211 | "print('노드 피처의 shape:', node_features.shape)\n", 212 | "print('노드 라벨의 shape:', node_labels.shape)\n", 213 | "\n", 214 | "num_features = node_features.shape[1]\n", 215 | "num_classes = (node_labels.max() + 1).item()\n", 216 | "print('클래스의 수:', num_classes)" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "metadata": {}, 222 | "source": [ 223 | "## 이웃 샘플링으로 데이터 로더 정의하기\n", 224 | "\n", 225 | "### 이웃 샘플링 개요\n", 226 | "\n", 227 | "\n", 228 | "message passing의 수식은 일반적으로 아래의 형태를 따릅니다." 229 | ] 230 | }, 231 | { 232 | "cell_type": "markdown", 233 | "metadata": {}, 234 | "source": [ 235 | "$$\n", 236 | "\\begin{gathered}\n", 237 | " \\boldsymbol{a}_v^{(l)} = \\rho^{(l)} \\left(\n", 238 | " \\left\\lbrace\n", 239 | " \\boldsymbol{h}_u^{(l-1)} : u \\in \\mathcal{N} \\left( v \\right)\n", 240 | " \\right\\rbrace\n", 241 | " \\right)\n", 242 | "\\\\\n", 243 | " \\boldsymbol{h}_v^{(l)} = \\phi^{(l)} \\left(\n", 244 | " \\boldsymbol{h}_v^{(l-1)}, \\boldsymbol{a}_v^{(l)}\n", 245 | " \\right)\n", 246 | "\\end{gathered}\n", 247 | "$$" 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "$\\rho^{(l)}$ 와 $\\phi^{(l)}$는 파라미터화된 함수이고, $\\mathcal{N}(v)$은 그래프 $\\mathcal{G}$ 내에 있는 $v$의 predecessors(혹은 *이웃*이라고도 불립니다.)를 나타냅니다." 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": {}, 260 | "source": [ 261 | "$$\n", 262 | "\\mathcal{N} \\left( v \\right) = \\left\\lbrace\n", 263 | " s \\left( e \\right) : e \\in \\mathbb{E}, t \\left( e \\right) = v\n", 264 | "\\right\\rbrace\n", 265 | "$$" 266 | ] 267 | }, 268 | { 269 | "cell_type": "markdown", 270 | "metadata": {}, 271 | "source": [ 272 | "예를 들어, 아래의 빨간 노드를 message passing을 통해 업데이트 하기 위해서는\n", 273 | "\n", 274 | "\n", 275 | "![Imgur](assets/1.png)\n", 276 | "\n", 277 | "그 이웃의 노드 피처를 통합할 필요가 있습니다. 아래의 녹색 노드를 보세요.\n", 278 | "\n", 279 | "![Imgur](assets/2.png)" 280 | ] 281 | }, 282 | { 283 | "cell_type": "markdown", 284 | "metadata": {}, 285 | "source": [ 286 | "한 노드의 출력을 계산할 때 다중 레이어의 message passing이 어떻게 작동하는지 살펴 봅시다. \n", 287 | "아래의 내용은, GNN이 seed 노드로 간주하여 계산하는 결과값을 만들어 내는 노드에 대한 설명입니다. \n", 288 | "\n", 289 | "\n", 290 | "2-레이어 GNN으로 seed 노드 8의 출력값을 계산하는 상황을 생각해 봅시다. 아래의 그래프에서 빨간색으로 칠해져 있습니다.\n", 291 | "\n", 292 | "![Imgur](assets/seed.png)\n", 293 | "\n", 294 | "수식은 다음과 같습니다." 295 | ] 296 | }, 297 | { 298 | "cell_type": "markdown", 299 | "metadata": {}, 300 | "source": [ 301 | "$$\n", 302 | "\\begin{gathered}\n", 303 | " \\boldsymbol{a}_8^{(2)} = \\rho^{(2)} \\left(\n", 304 | " \\left\\lbrace\n", 305 | " \\boldsymbol{h}_u^{(1)} : u \\in \\mathcal{N} \\left( 8 \\right)\n", 306 | " \\right\\rbrace\n", 307 | " \\right) = \\rho^{(2)} \\left(\n", 308 | " \\left\\lbrace\n", 309 | " \\boldsymbol{h}_4^{(1)}, \\boldsymbol{h}_5^{(1)},\n", 310 | " \\boldsymbol{h}_7^{(1)}, \\boldsymbol{h}_{11}^{(1)}\n", 311 | " \\right\\rbrace\n", 312 | " \\right)\n", 313 | "\\\\\n", 314 | " \\boldsymbol{h}_8^{(2)} = \\phi^{(2)} \\left(\n", 315 | " \\boldsymbol{h}_8^{(1)}, \\boldsymbol{a}_8^{(2)}\n", 316 | " \\right)\n", 317 | "\\end{gathered}\n", 318 | "$$" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": {}, 324 | "source": [ 325 | "\n", 326 | "수식에서 볼 수 있듯이, $\\boldsymbol{h}_8^{(2)}$ 을 계산하기 위해서는, 4,5,7,11번(녹색으로 칠해진) 노드에서 message를 아래의 시각화된 엣지를 따라 받아야 합니다." 327 | ] 328 | }, 329 | { 330 | "cell_type": "markdown", 331 | "metadata": {}, 332 | "source": [ 333 | "![Imgur](assets/3.png)" 334 | ] 335 | }, 336 | { 337 | "cell_type": "markdown", 338 | "metadata": {}, 339 | "source": [ 340 | " $\\boldsymbol{h}_\\cdot^{(1)}$의 값들은 첫번째 GNN 레이어로부터 나온 출력값입니다. \n", 341 | " 이러한 값들을 빨간색, 녹색 노드에서 계산하기 위해서는, 아래 시각화된 엣지들에 대한 message passing도 수행할 필요가 있습니다. \n" 342 | ] 343 | }, 344 | { 345 | "cell_type": "markdown", 346 | "metadata": {}, 347 | "source": [ 348 | "![Imgur](assets/4.png)" 349 | ] 350 | }, 351 | { 352 | "cell_type": "markdown", 353 | "metadata": {}, 354 | "source": [ 355 | "따라서, 빨간 노드의 2-레이어 GNN 표현을 계산하기 위해서는, 빨간 노드의 입력 피처값 뿐만 아니라 녹색, 노란색 노드의 입력 피처값도 필요합니다. \n", 356 | "이 레이어에서 빨간 노드의 이웃들을 다시 취해 준다는 사실에 주목해 주세요. \n", 357 | "\n", 358 | "연산 의존성(computation dependency)을 결정하는 이 절차는 message 통합의 반대 방향에서 이루어 진다는 점에 주목해 주세요. \n", 359 | "즉, 출력 층에 가장 가까운 레이어부터 시작해 입력까지 거꾸로 작동한다는 말이지요." 360 | ] 361 | }, 362 | { 363 | "cell_type": "markdown", 364 | "metadata": {}, 365 | "source": [ 366 | "많지 않은 노드의 표현을 계산하는 작업이 종종 훨씬 더 큰 수의 노드의 입력 피처를 필요로 한다는 점도 알 수 있습니다. \n", 367 | "message 통합을 위해 모든 이웃을 취해주는 일은 보통 너무 큰 비용이 들어갑니다. 필요한 노드를 감안하면 그래프의 큰 부분을 포함하기 때문이죠. \n", 368 | "\n", 369 | "이웃 샘플링은 message 통합 수행 시 이웃의 무작위적인 부분집합을 선택함으로써 이런 문제를 해결합니다. \n", 370 | "예를 들어, $\\boldsymbol{h}_8^{(1)}$를 계산하기 위해, 2개의 이웃 노드를 골라 통합할 수 있습니다." 371 | ] 372 | }, 373 | { 374 | "cell_type": "markdown", 375 | "metadata": {}, 376 | "source": [ 377 | "![Imgur](assets/5.png)" 378 | ] 379 | }, 380 | { 381 | "cell_type": "markdown", 382 | "metadata": {}, 383 | "source": [ 384 | "비슷한 방식으로, 빨간색 그리고 녹색 노드의 첫번째 레이어 표현을 계산하기 위해, 각 노드에서 2개의 이웃 노드만을 취하는 이웃 샘플링을 수행할 수 있습니다. 빨간 노드의 이웃 노드들을 이번 레이어에서 또 취해주어야 한다는 점에 주목해 주세요." 385 | ] 386 | }, 387 | { 388 | "cell_type": "markdown", 389 | "metadata": {}, 390 | "source": [ 391 | "![Imgur](assets/6.png)" 392 | ] 393 | }, 394 | { 395 | "cell_type": "markdown", 396 | "metadata": {}, 397 | "source": [ 398 | "이러한 방식으로 입력 피처를 위해 필요한 노드가 줄어들었음을 알 수 있습니다." 399 | ] 400 | }, 401 | { 402 | "cell_type": "markdown", 403 | "metadata": {}, 404 | "source": [ 405 | "## DGL에서 이웃 샘플러와 데이터로더 정의하기\n", 406 | "\n", 407 | "DGL은 데이터셋을 미니배치로 반복하며 이러한 연산 의존성(computation dependencies)을 생성하는 유용한 툴을 제공합니다. \n", 408 | "노드 분류 작업에서, `dgl.dataloading.NodeDataLoader`를 사용해 데이터셋에 걸쳐 반복할 수 있으며, \n", 409 | "`dgl.dataloading.MultiLayerNeighborSampler`를 사용하여 이웃 샘플링을 통한 다중 레이어 GNN에서의 노드의 연산 의존성을 생성할 수 있습니다. \n", 410 | "\n", 411 | "`dgl.dataloading.NodeDataLoader`의 문법은 PyTorch의 `DataLoader`와 거의 유사한데, \n", 412 | "이에 더해 연산 의존성을 생성할 그래프와 반복할 노드 ID 집합, 그리고 여러분이 정의한 이웃 샘플러가 필요합니다. \n", 413 | "\n", 414 | "이웃 샘플링을 사용한 3-레이어 GraphSAGE를 학습시켜 봅시다. \n", 415 | "여기서 각 노드는 각 레이어마다 4개의 이웃 노드로부터 message를 받습니다. \n", 416 | "data loader와 이웃 샘플러를 정의하는 코드는 아래와 같이 생겼습니다." 417 | ] 418 | }, 419 | { 420 | "cell_type": "code", 421 | "execution_count": 9, 422 | "metadata": {}, 423 | "outputs": [], 424 | "source": [ 425 | "sampler = dgl.dataloading.MultiLayerNeighborSampler([4, 4, 4])\n", 426 | "train_dataloader = dgl.dataloading.NodeDataLoader(\n", 427 | " graph, train_nids, sampler,\n", 428 | " batch_size=1024,\n", 429 | " shuffle=True,\n", 430 | " drop_last=False,\n", 431 | " num_workers=0\n", 432 | ")" 433 | ] 434 | }, 435 | { 436 | "cell_type": "markdown", 437 | "metadata": {}, 438 | "source": [ 439 | "우리가 만든 data loader에 걸쳐 반복할 수 있겠죠. 그 결과를 확인해 봅시다." 440 | ] 441 | }, 442 | { 443 | "cell_type": "code", 444 | "execution_count": 10, 445 | "metadata": {}, 446 | "outputs": [ 447 | { 448 | "name": "stdout", 449 | "output_type": "stream", 450 | "text": [ 451 | "(tensor([175920, 31182, 105499, ..., 56057, 2244, 18221]), tensor([175920, 31182, 105499, ..., 43014, 124137, 84344]), [Block(num_src_nodes=34487, num_dst_nodes=15719, num_edges=51005), Block(num_src_nodes=15719, num_dst_nodes=4598, num_edges=16060), Block(num_src_nodes=4598, num_dst_nodes=1024, num_edges=3716)])\n" 452 | ] 453 | } 454 | ], 455 | "source": [ 456 | "example_minibatch = next(iter(train_dataloader))\n", 457 | "print(example_minibatch)" 458 | ] 459 | }, 460 | { 461 | "cell_type": "markdown", 462 | "metadata": {}, 463 | "source": [ 464 | "`NodeDataLoader`는 1회 iteration마다 3개의 item을 제공합니다. \n", 465 | "\n", 466 | "* 출력을 계산하기 위해 필요한 입력 피처를 가진 노드의 입력 노드 리스트 \n", 467 | "* GNN 표현이 계산될 출력 노드 리스트\n", 468 | "* 각 레이어의 연산 의존성 리스트\n" 469 | ] 470 | }, 471 | { 472 | "cell_type": "code", 473 | "execution_count": 11, 474 | "metadata": {}, 475 | "outputs": [ 476 | { 477 | "name": "stdout", 478 | "output_type": "stream", 479 | "text": [ 480 | "To compute 1024 nodes' output we need 34487 nodes' input features\n" 481 | ] 482 | } 483 | ], 484 | "source": [ 485 | "input_nodes, output_nodes, bipartites = example_minibatch\n", 486 | "print(\"To compute {} nodes' output we need {} nodes' input features\".format(len(output_nodes), len(input_nodes)))" 487 | ] 488 | }, 489 | { 490 | "cell_type": "markdown", 491 | "metadata": {}, 492 | "source": [ 493 | "변수 `bipartites`는 각 레이어에서 어떻게 message가 통합되는지를 보여줍니다. \n", 494 | "이 이름이 암시하듯이, 이는 bipartite 그래프의 **리스트** 입니다. \n", 495 | "그런데 왜 DGL이 동질적(homogeneous) 그래프를 학습시키는 데 bipartite graph를 반환할까요? \n", 496 | "\n", 497 | "그 이유는 GNN 레이어에서 주어진 입력을 위한 노드의 수와 아웃풋을 위한 노드의 수가 다르기 때문입니다. 위의 예시를 다시 들어 설명하겠습니다." 498 | ] 499 | }, 500 | { 501 | "cell_type": "markdown", 502 | "metadata": {}, 503 | "source": [ 504 | "![Imgur](assets/6.png)" 505 | ] 506 | }, 507 | { 508 | "cell_type": "markdown", 509 | "metadata": {}, 510 | "source": [ 511 | "이 GNN 레이어는 노드 3개의 표현을 출력할 것입니다(2개의 녹색 노드, 그리고 1개의 빨간 노드) 그러나 입력을 위해서는 7개의 노드가 필요하죠(녹색 노드와 빨간 노드, 거기에 4개의 노란 노드까지). \n", 512 | "오직 bipartite 그래프만이 이런 계산을 묘사할 수 있을 것입니다." 513 | ] 514 | }, 515 | { 516 | "cell_type": "markdown", 517 | "metadata": {}, 518 | "source": [ 519 | "![](assets/bipartite.png)" 520 | ] 521 | }, 522 | { 523 | "cell_type": "markdown", 524 | "metadata": {}, 525 | "source": [ 526 | "GNN의 미니배치 학습은 보통 이런 bipartite 그래프 상의 message passing을 포함합니다." 527 | ] 528 | }, 529 | { 530 | "cell_type": "code", 531 | "execution_count": 29, 532 | "metadata": {}, 533 | "outputs": [ 534 | { 535 | "name": "stdout", 536 | "output_type": "stream", 537 | "text": [ 538 | "[Block(num_src_nodes=26109, num_dst_nodes=8397, num_edges=30082), Block(num_src_nodes=8397, num_dst_nodes=1987, num_edges=7595), Block(num_src_nodes=1987, num_dst_nodes=411, num_edges=1590)]\n" 539 | ] 540 | } 541 | ], 542 | "source": [ 543 | "print(bipartites)" 544 | ] 545 | }, 546 | { 547 | "cell_type": "markdown", 548 | "metadata": {}, 549 | "source": [ 550 | "## 모델 정의하기\n", 551 | "\n", 552 | "모델은 아래처럼 쓰여질 수 있습니다." 553 | ] 554 | }, 555 | { 556 | "cell_type": "code", 557 | "execution_count": 13, 558 | "metadata": {}, 559 | "outputs": [], 560 | "source": [ 561 | "import torch.nn as nn\n", 562 | "import torch.nn.functional as F\n", 563 | "import dgl.nn as dglnn\n", 564 | "\n", 565 | "class SAGE(nn.Module):\n", 566 | " def __init__(self, in_feats, n_hidden, n_classes, n_layers):\n", 567 | " super().__init__()\n", 568 | " self.n_layers = n_layers\n", 569 | " self.n_hidden = n_hidden\n", 570 | " self.n_classes = n_classes\n", 571 | " self.layers = nn.ModuleList()\n", 572 | " self.layers.append(dglnn.SAGEConv(in_feats, n_hidden, 'mean'))\n", 573 | " for i in range(1, n_layers - 1):\n", 574 | " self.layers.append(dglnn.SAGEConv(n_hidden, n_hidden, 'mean'))\n", 575 | " self.layers.append(dglnn.SAGEConv(n_hidden, n_classes, 'mean'))\n", 576 | " \n", 577 | " def forward(self, bipartites, x):\n", 578 | " for l, (layer, bipartite) in enumerate(zip(self.layers, bipartites)):\n", 579 | " x = layer(bipartite, x)\n", 580 | " if l != self.n_layers - 1:\n", 581 | " x = F.relu(x)\n", 582 | " return x" 583 | ] 584 | }, 585 | { 586 | "cell_type": "markdown", 587 | "metadata": {}, 588 | "source": [ 589 | "여기서, 데이터 로더에 의해 생성된 한 쌍의 NN 모듈 레이어와 bipartite 그래프를 반복해 사용하고 있음을 볼 수 있습니다." 590 | ] 591 | }, 592 | { 593 | "cell_type": "markdown", 594 | "metadata": {}, 595 | "source": [ 596 | "## 학습 루프 정의하기\n", 597 | "\n", 598 | "아래의 내용은 모델을 초기화하고, optimizer를 정의합니다." 599 | ] 600 | }, 601 | { 602 | "cell_type": "code", 603 | "execution_count": 14, 604 | "metadata": {}, 605 | "outputs": [], 606 | "source": [ 607 | "model = SAGE(num_features, 128, num_classes, 3).cuda()\n", 608 | "opt = torch.optim.Adam(model.parameters())" 609 | ] 610 | }, 611 | { 612 | "cell_type": "markdown", 613 | "metadata": {}, 614 | "source": [ 615 | "모델 선택의 validation score를 계산할 때, 이 때 역시도 보통은 이웃 샘플링을 사용할 수 있습니다. \n", 616 | "이를 위해선, 다른 데이터 로더를 정의할 필요가 있습니다." 617 | ] 618 | }, 619 | { 620 | "cell_type": "code", 621 | "execution_count": 15, 622 | "metadata": {}, 623 | "outputs": [], 624 | "source": [ 625 | "valid_dataloader = dgl.dataloading.NodeDataLoader(\n", 626 | " graph, valid_nids, sampler,\n", 627 | " batch_size=1024,\n", 628 | " shuffle=False,\n", 629 | " drop_last=False,\n", 630 | " num_workers=0\n", 631 | ")" 632 | ] 633 | }, 634 | { 635 | "cell_type": "markdown", 636 | "metadata": {}, 637 | "source": [ 638 | "아래는 매 epoch마다 validation을 수행하는 학습 루프입니다. \n", 639 | "또한 가장 좋은 validation accuracy를 가진 모델을 파일로 저장해 줍니다." 640 | ] 641 | }, 642 | { 643 | "cell_type": "code", 644 | "execution_count": 16, 645 | "metadata": {}, 646 | "outputs": [ 647 | { 648 | "name": "stderr", 649 | "output_type": "stream", 650 | "text": [ 651 | "100%|██████████| 193/193 [00:06<00:00, 30.77it/s, loss=0.177, acc=1.000]\n", 652 | "100%|██████████| 39/39 [00:01<00:00, 28.98it/s]\n", 653 | " 2%|▏ | 3/193 [00:00<00:06, 29.41it/s, loss=0.730, acc=0.804]" 654 | ] 655 | }, 656 | { 657 | "name": "stdout", 658 | "output_type": "stream", 659 | "text": [ 660 | "Epoch 0 Validation Accuracy 0.8209190550059762\n" 661 | ] 662 | }, 663 | { 664 | "name": "stderr", 665 | "output_type": "stream", 666 | "text": [ 667 | "100%|██████████| 193/193 [00:06<00:00, 31.34it/s, loss=2.047, acc=0.714]\n", 668 | "100%|██████████| 39/39 [00:01<00:00, 29.21it/s]\n", 669 | " 2%|▏ | 4/193 [00:00<00:06, 31.42it/s, loss=0.715, acc=0.814]" 670 | ] 671 | }, 672 | { 673 | "name": "stdout", 674 | "output_type": "stream", 675 | "text": [ 676 | "Epoch 1 Validation Accuracy 0.843857284540854\n" 677 | ] 678 | }, 679 | { 680 | "name": "stderr", 681 | "output_type": "stream", 682 | "text": [ 683 | "100%|██████████| 193/193 [00:05<00:00, 32.43it/s, loss=0.770, acc=0.714]\n", 684 | "100%|██████████| 39/39 [00:01<00:00, 28.99it/s]\n", 685 | " 2%|▏ | 3/193 [00:00<00:06, 28.95it/s, loss=0.587, acc=0.832]" 686 | ] 687 | }, 688 | { 689 | "name": "stdout", 690 | "output_type": "stream", 691 | "text": [ 692 | "Epoch 2 Validation Accuracy 0.8591155303511939\n" 693 | ] 694 | }, 695 | { 696 | "name": "stderr", 697 | "output_type": "stream", 698 | "text": [ 699 | "100%|██████████| 193/193 [00:06<00:00, 30.99it/s, loss=0.167, acc=0.857]\n", 700 | "100%|██████████| 39/39 [00:01<00:00, 29.14it/s]\n", 701 | " 2%|▏ | 3/193 [00:00<00:06, 28.37it/s, loss=0.634, acc=0.828]" 702 | ] 703 | }, 704 | { 705 | "name": "stdout", 706 | "output_type": "stream", 707 | "text": [ 708 | "Epoch 3 Validation Accuracy 0.8594969864964525\n" 709 | ] 710 | }, 711 | { 712 | "name": "stderr", 713 | "output_type": "stream", 714 | "text": [ 715 | "100%|██████████| 193/193 [00:06<00:00, 32.07it/s, loss=0.836, acc=0.714]\n", 716 | "100%|██████████| 39/39 [00:01<00:00, 30.05it/s]\n", 717 | " 2%|▏ | 3/193 [00:00<00:06, 28.08it/s, loss=0.479, acc=0.866]" 718 | ] 719 | }, 720 | { 721 | "name": "stdout", 722 | "output_type": "stream", 723 | "text": [ 724 | "Epoch 4 Validation Accuracy 0.870025176105587\n" 725 | ] 726 | }, 727 | { 728 | "name": "stderr", 729 | "output_type": "stream", 730 | "text": [ 731 | "100%|██████████| 193/193 [00:06<00:00, 31.74it/s, loss=0.559, acc=0.714]\n", 732 | "100%|██████████| 39/39 [00:01<00:00, 27.97it/s]\n", 733 | " 2%|▏ | 3/193 [00:00<00:07, 26.78it/s, loss=0.496, acc=0.869]" 734 | ] 735 | }, 736 | { 737 | "name": "stdout", 738 | "output_type": "stream", 739 | "text": [ 740 | "Epoch 5 Validation Accuracy 0.8706100755283167\n" 741 | ] 742 | }, 743 | { 744 | "name": "stderr", 745 | "output_type": "stream", 746 | "text": [ 747 | "100%|██████████| 193/193 [00:06<00:00, 29.46it/s, loss=0.045, acc=1.000]\n", 748 | "100%|██████████| 39/39 [00:01<00:00, 27.69it/s]\n", 749 | " 2%|▏ | 3/193 [00:00<00:06, 27.70it/s, loss=0.461, acc=0.886]" 750 | ] 751 | }, 752 | { 753 | "name": "stdout", 754 | "output_type": "stream", 755 | "text": [ 756 | "Epoch 6 Validation Accuracy 0.87434834575185\n" 757 | ] 758 | }, 759 | { 760 | "name": "stderr", 761 | "output_type": "stream", 762 | "text": [ 763 | "100%|██████████| 193/193 [00:06<00:00, 30.33it/s, loss=0.074, acc=1.000]\n", 764 | "100%|██████████| 39/39 [00:01<00:00, 26.72it/s]\n", 765 | " 2%|▏ | 3/193 [00:00<00:07, 26.66it/s, loss=0.435, acc=0.877]" 766 | ] 767 | }, 768 | { 769 | "name": "stdout", 770 | "output_type": "stream", 771 | "text": [ 772 | "Epoch 7 Validation Accuracy 0.8752129796811027\n" 773 | ] 774 | }, 775 | { 776 | "name": "stderr", 777 | "output_type": "stream", 778 | "text": [ 779 | "100%|██████████| 193/193 [00:06<00:00, 30.80it/s, loss=0.129, acc=1.000]\n", 780 | "100%|██████████| 39/39 [00:01<00:00, 29.04it/s]\n", 781 | " 2%|▏ | 4/193 [00:00<00:06, 30.74it/s, loss=0.429, acc=0.892]" 782 | ] 783 | }, 784 | { 785 | "name": "stdout", 786 | "output_type": "stream", 787 | "text": [ 788 | "Epoch 8 Validation Accuracy 0.8772474124558146\n" 789 | ] 790 | }, 791 | { 792 | "name": "stderr", 793 | "output_type": "stream", 794 | "text": [ 795 | "100%|██████████| 193/193 [00:06<00:00, 32.16it/s, loss=0.167, acc=1.000]\n", 796 | "100%|██████████| 39/39 [00:01<00:00, 29.13it/s]" 797 | ] 798 | }, 799 | { 800 | "name": "stdout", 801 | "output_type": "stream", 802 | "text": [ 803 | "Epoch 9 Validation Accuracy 0.8803244920275666\n" 804 | ] 805 | }, 806 | { 807 | "name": "stderr", 808 | "output_type": "stream", 809 | "text": [ 810 | "\n" 811 | ] 812 | } 813 | ], 814 | "source": [ 815 | "import tqdm\n", 816 | "import sklearn.metrics\n", 817 | "\n", 818 | "best_accuracy = 0\n", 819 | "best_model_path = 'model.pt'\n", 820 | "for epoch in range(10):\n", 821 | " model.train()\n", 822 | " \n", 823 | " with tqdm.tqdm(train_dataloader) as tq:\n", 824 | " for step, (input_nodes, output_nodes, bipartites) in enumerate(tq):\n", 825 | " bipartites = [b.to(torch.device('cuda')) for b in bipartites]\n", 826 | " inputs = node_features[input_nodes].cuda()\n", 827 | " labels = node_labels[output_nodes].cuda()\n", 828 | " predictions = model(bipartites, inputs)\n", 829 | "\n", 830 | " loss = F.cross_entropy(predictions, labels)\n", 831 | " opt.zero_grad()\n", 832 | " loss.backward()\n", 833 | " opt.step()\n", 834 | "\n", 835 | " accuracy = sklearn.metrics.accuracy_score(labels.cpu().numpy(), predictions.argmax(1).detach().cpu().numpy())\n", 836 | " \n", 837 | " tq.set_postfix({'loss': '%.03f' % loss.item(), 'acc': '%.03f' % accuracy}, refresh=False)\n", 838 | " \n", 839 | " model.eval()\n", 840 | " \n", 841 | " predictions = []\n", 842 | " labels = []\n", 843 | " with tqdm.tqdm(valid_dataloader) as tq, torch.no_grad():\n", 844 | " for input_nodes, output_nodes, bipartites in tq:\n", 845 | " bipartites = [b.to(torch.device('cuda')) for b in bipartites]\n", 846 | " inputs = node_features[input_nodes].cuda()\n", 847 | " labels.append(node_labels[output_nodes].numpy())\n", 848 | " predictions.append(model(bipartites, inputs).argmax(1).cpu().numpy())\n", 849 | " predictions = np.concatenate(predictions)\n", 850 | " labels = np.concatenate(labels)\n", 851 | " accuracy = sklearn.metrics.accuracy_score(labels, predictions)\n", 852 | " print('Epoch {} Validation Accuracy {}'.format(epoch, accuracy))\n", 853 | " if best_accuracy < accuracy:\n", 854 | " best_accuracy = accuracy\n", 855 | " torch.save(model.state_dict(), best_model_path)" 856 | ] 857 | }, 858 | { 859 | "cell_type": "markdown", 860 | "metadata": {}, 861 | "source": [ 862 | "## 이웃 샘플링 없이 Offline에서 추론하기 \n", 863 | "\n", 864 | "\n", 865 | "일반적으로 offline 추론에서는 이웃 샘플링에 의해 발생하는 무작위성을 제거하기 위해 전체 이웃에 대해 통합을 진행하는 것이 바람직합니다. \n", 866 | "하지만, 같은 방법을 학습 단계에서도 사용하는 것은 비효율적인데, 그 까닭은 너무 불필요한 연산이 많아지기 때문입니다. \n", 867 | "더욱이, 단순히 모든 이웃을 취해 이웃 샘플링을 수행하는 것은 종종 GPU 메모리를 모두 잡아먹을 수도 있는데, 이는 입력 피처를 위해 필요한 노드의 수가 GPU 메모리에 올려지기에 너무 클 수 있기 때문입니다. \n", 868 | "\n", 869 | "\n", 870 | "대신, 레이어마다 표현을 계산해주면 됩니다. \n", 871 | "즉, 먼저 모든 노드에 대해 첫번째 GNN 레이어의 출력 값을 계산하고, \n", 872 | "그 뒤 두번째 레이어의 출력 값을 모든 노드에 대해 계산하는 데 이 때 첫번째 GNN 레이어의 출력을 입력 값으로 사용하는 식입니다. \n", 873 | "이러한 방식은 학습 시에 사용된 것과는 다른 알고리즘이 됩니다. \n", 874 | "\n", 875 | "\n", 876 | "학습 중에는 노드에 걸쳐 돌아가는 외부 루프와, 레이어에 걸쳐 돌아가는 내부 루프가 있습니다. \n", 877 | "반대로, 추론 단계에서는 레이어에 걸쳐 돌아가는 외부 루프와 노드에 걸쳐 돌아가는 내부 루프가 있게 됩니다. \n", 878 | "\n", 879 | "만약 무작위성에 대해 크게 신경쓰지 않는다면, (가령 validation 상에서 모델을 선택하는 중이라던지) \n", 880 | "`dgl.dataloading.MultiLayerNeighborSampler`와 `dgl.dataloading.NodeDataLoader`를 사용해 offline 추론을 수행할 수 있습니다. \n", 881 | "이는 노드의 수가 적은 경우 evaluation을 수행하는 데 보통 더 빠르기 때문입니다. " 882 | ] 883 | }, 884 | { 885 | "cell_type": "markdown", 886 | "metadata": {}, 887 | "source": [ 888 | "![Imgur](assets/anim.gif)" 889 | ] 890 | }, 891 | { 892 | "cell_type": "code", 893 | "execution_count": 17, 894 | "metadata": {}, 895 | "outputs": [], 896 | "source": [ 897 | "def inference(model, graph, input_features, batch_size):\n", 898 | " nodes = torch.arange(graph.number_of_nodes())\n", 899 | " \n", 900 | " sampler = dgl.dataloading.MultiLayerNeighborSampler([None]) # one layer at a time, taking all neighbors\n", 901 | " dataloader = dgl.dataloading.NodeDataLoader(\n", 902 | " graph, nodes, sampler,\n", 903 | " batch_size=batch_size,\n", 904 | " shuffle=False,\n", 905 | " drop_last=False,\n", 906 | " num_workers=0)\n", 907 | " \n", 908 | " with torch.no_grad():\n", 909 | " for l, layer in enumerate(model.layers):\n", 910 | " # Allocate a buffer of output representations for every node\n", 911 | " # Note that the buffer is on CPU memory.\n", 912 | " output_features = torch.zeros(\n", 913 | " graph.number_of_nodes(), model.n_hidden if l != model.n_layers - 1 else model.n_classes)\n", 914 | "\n", 915 | " for input_nodes, output_nodes, bipartites in tqdm.tqdm(dataloader):\n", 916 | " bipartite = bipartites[0].to(torch.device('cuda'))\n", 917 | "\n", 918 | " x = input_features[input_nodes].cuda()\n", 919 | "\n", 920 | " # the following code is identical to the loop body in model.forward()\n", 921 | " x = layer(bipartite, x)\n", 922 | " if l != model.n_layers - 1:\n", 923 | " x = F.relu(x)\n", 924 | "\n", 925 | " output_features[output_nodes] = x.cpu()\n", 926 | " input_features = output_features\n", 927 | " return output_features" 928 | ] 929 | }, 930 | { 931 | "cell_type": "markdown", 932 | "metadata": {}, 933 | "source": [ 934 | "아래의 코드는 이전에 저장된 파일에서부터 최적의 모델을 호출해 offline 추론을 수행합니다. \n", 935 | "그 뒤 테스트 셋에 대해 정확도를 계산합니다." 936 | ] 937 | }, 938 | { 939 | "cell_type": "code", 940 | "execution_count": 18, 941 | "metadata": {}, 942 | "outputs": [ 943 | { 944 | "name": "stderr", 945 | "output_type": "stream", 946 | "text": [ 947 | "100%|██████████| 299/299 [00:33<00:00, 8.88it/s]\n", 948 | "100%|██████████| 299/299 [00:25<00:00, 11.92it/s]\n", 949 | "100%|██████████| 299/299 [00:25<00:00, 11.64it/s]\n" 950 | ] 951 | } 952 | ], 953 | "source": [ 954 | "model.load_state_dict(torch.load(best_model_path))\n", 955 | "all_predictions = inference(model, graph, node_features, 8192)" 956 | ] 957 | }, 958 | { 959 | "cell_type": "code", 960 | "execution_count": 19, 961 | "metadata": {}, 962 | "outputs": [ 963 | { 964 | "name": "stdout", 965 | "output_type": "stream", 966 | "text": [ 967 | "Test accuracy: 0.7300458950851998\n" 968 | ] 969 | } 970 | ], 971 | "source": [ 972 | "test_predictions = all_predictions[test_nids].argmax(1)\n", 973 | "test_labels = node_labels[test_nids]\n", 974 | "test_accuracy = sklearn.metrics.accuracy_score(test_predictions.numpy(), test_labels.numpy())\n", 975 | "print('Test accuracy:', test_accuracy)" 976 | ] 977 | }, 978 | { 979 | "cell_type": "markdown", 980 | "metadata": {}, 981 | "source": [ 982 | "## 결론\n", 983 | "\n", 984 | "이 튜토리얼에서, 다중-레이어 GraphSAGE 모델을 이웃 샘플링을 통해 GPU에 맞지 않을 정도로 큰 데이터셋에 대해 학습하는 방법을 배웠습니다. \n", 985 | "지금 배운 이 방법은 어떤 사이즈의 그래프에도 확장 가능하며, 1개의 GPU를 가진 1개의 머신에서도 돌아갈 것입니다." 986 | ] 987 | }, 988 | { 989 | "cell_type": "markdown", 990 | "metadata": {}, 991 | "source": [ 992 | "\n", 993 | "## 다음은 무엇인가요?\n", 994 | "\n", 995 | "다음 튜토리얼은 똑같은 GraphSAGE 모델을 비지도학습적인 방식으로 link prediction 태스크에 학습해 봅니다. \n", 996 | "즉, 두 노드 사이에 엣지가 존재하는지 아닌지를 예측해 봅니다." 997 | ] 998 | }, 999 | { 1000 | "cell_type": "code", 1001 | "execution_count": null, 1002 | "metadata": {}, 1003 | "outputs": [], 1004 | "source": [] 1005 | } 1006 | ], 1007 | "metadata": { 1008 | "kernelspec": { 1009 | "display_name": "Python 3", 1010 | "language": "python", 1011 | "name": "python3" 1012 | }, 1013 | "language_info": { 1014 | "codemirror_mode": { 1015 | "name": "ipython", 1016 | "version": 3 1017 | }, 1018 | "file_extension": ".py", 1019 | "mimetype": "text/x-python", 1020 | "name": "python", 1021 | "nbconvert_exporter": "python", 1022 | "pygments_lexer": "ipython3", 1023 | "version": "3.8.3" 1024 | } 1025 | }, 1026 | "nbformat": 4, 1027 | "nbformat_minor": 4 1028 | } 1029 | -------------------------------------------------------------------------------- /large_graph/2_unsupervised_learning_and_link_prediction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 대규모 그래프에서의 링크 예측을 위한 GNN의 확률적(Storchastic) 학습 \n", 8 | "\n", 9 | "이번 튜토리얼에서는, 다중 레이어 GraphSAGE 모델을 비지도학습 방식으로 학습시키는 방법을 OGB가 제공하는 Amazon Copurchase Netword 데이터의 링크 예측을 통해 배워봅니다. \n", 10 | "데이터셋은 240만 노드와 6100만 엣지를 포함하고 있으며, 따라서 단일 GPU에 올라가지 않습니다.\n", 11 | "\n", 12 | "이 튜토리얼의 내용은 다음을 포함합니다. \n", 13 | "\n", 14 | "* GNN 모델을 그래프 크기에 상관없이 1개의 GPU를 가진 단일 머신으로 학습하기 \n", 15 | "* 링크 예측 task를 수행하는 GNN 모델 학습하기\n", 16 | "* 비지도 학습을 위한 GNN 모델 학습하기\n", 17 | "\n", 18 | "이 튜토리얼은 이전의 튜토리얼에서 다운받은 데이터를 활용합니다. " 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "## Link Prediction Overview" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "링크 예측의 목표는 두개의 주어진 노드 사이에 엣지가 존재하는지를 예측하는 것입니다. \n", 33 | "보통 이런 문제를 $s_{uv} = \\phi(\\boldsymbol{h}^{(l)}_u, \\boldsymbol{h}^{(l)}_v)$라는 점수를 예측하는 문제로 수식화 하는데요, \n", 34 | "이는 두 노드 사이에 존재하는 엣지의 likelihood를 의미합니다. \n", 35 | "\n", 36 | "또, 모델을 *네거티브 샘플링 negative sampling*을 통해 학습합니다. \n", 37 | "즉, 실재 존재하는 엣지와 \"존재하지 않는\" 엣지의 점수를 비교함으로써 학습한다는 의미입니다.\n" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "일반적인 손실함수 중 하나는 negative log-likelihood 입니다." 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "$$\n", 52 | "\\mathcal{L} = -\\log \\sigma\\left(s_{uv}\\right) - Q \\mathbb{E}_{v^- \\in P^-(v)}\\left[ \\sigma\\left(-s_{uv^-}\\right) \\right]\n", 53 | "$$" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "BPR이나 margin loss와 같은 다른 손실함수를 사용할 수도 있습니다. \n", 61 | "\n", 62 | "위의 수식이 implicit matrix factorization 혹은 워드 임베딩 학습과 비슷하다는 점에 주목해 주세요." 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "## GNN을 사용한 비지도학습의 개요\n", 70 | "\n", 71 | "링크 예측 그 자체는 한 노드가 다른 노드와 상호작용할지 예측하는 추천과 같은 다양한 작업에서 이미 유용성을 입증했습니다. \n", 72 | "또 링크 예측은 모든 노드의 잠재 표현을 학습하고자 하는, 비지도 학습의 상황에서도 유용합니다.\n", 73 | "\n", 74 | "모델은 두 노드가 엣지로 연결 되어 있을지 아닐지를 예측하는 비지도 학습적인 방식으로 학습될 것이고, \n", 75 | "학습된 표현은 최근접 이웃(nearest neighbor, NN) 검색 혹은 추후의 분류 모델 학습에 활용될 수 있겠죠. \n", 76 | "\n", 77 | "또, 목적 함수는 노드 분류를 위한 지도학습의 cross-entropy loss와 결합될 수 있습니다.\n" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "metadata": {}, 83 | "source": [ 84 | "\n", 85 | "## 데이터셋 로드하기\n", 86 | "\n", 87 | "이전 튜토리얼에서 전처리된 데이터셋을 직접 가져오겠습니다.\n" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 1, 93 | "metadata": {}, 94 | "outputs": [ 95 | { 96 | "name": "stderr", 97 | "output_type": "stream", 98 | "text": [ 99 | "Using backend: pytorch\n" 100 | ] 101 | } 102 | ], 103 | "source": [ 104 | "import dgl\n", 105 | "import torch\n", 106 | "import numpy as np\n", 107 | "import utils\n", 108 | "import pickle\n", 109 | "\n", 110 | "with open('data.pkl', 'rb') as f:\n", 111 | " data = pickle.load(f)\n", 112 | "graph, node_features, node_labels, train_nids, valid_nids, test_nids = data\n", 113 | "graph.create_formats_()" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": {}, 119 | "source": [ 120 | "## 이웃 샘플링으로 데이터 로더 정의하기\n", 121 | "\n", 122 | "노드 분류와는 다르게, 엣지에 걸쳐 iterate해야합니다. 그 뒤 이웃 샘플링과 GNN을 사용해 해당 노드들의 출력 표현을 계산해야 합니다. \n", 123 | "\n", 124 | "DGL은 `EdgeDataLoader`을 제공합니다. 이 메서드는 엣지 분류 혹은 링크 예측을 위해 엣지를 iterate하도록 도와줍니다. \n", 125 | "\n", 126 | "링크 예측을 수행하기 위해, negative sampler를 제공해 주어야 합니다. \n", 127 | "\n", 128 | "동질적(homogeneous) 그래프에서는, negative sample는 아래의 양식을 가진 어떤 callable 객체든 가능합니다. \n", 129 | "\n", 130 | "```python\n", 131 | "def negative_sampler(g: DGLGraph, eids: Tensor) -> Tuple[Tensor, Tensor]:\n", 132 | " pass\n", 133 | "```\n", 134 | "\n", 135 | "첫번째 인자는 원래 그래프이고, 두번째 인자는 엣지 ID의 미니배치를 의미합니다. \n", 136 | "이 함수는 $u$-$v^-$ 노드 ID 텐서의 쌍을 negative example로 반환합니다. \n", 137 | "\n", 138 | "\n", 139 | "다음 코드는 `k`개의 $v^-$를 각 $u$에 대해 $P^-(v) \\propto d(v)^{0.75}$의 분포를 따라 샘플링 함으로써,\n", 140 | "그래프 내에 존재하지 않는 엣지를 찾는 negative sampler 기능을 수행합니다. \n", 141 | "여기서 $d(v)$는 $v$의 차수(degree)를 의미합니다." 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 2, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "class NegativeSampler(object):\n", 151 | " def __init__(self, g, k):\n", 152 | " self.k = k\n", 153 | " self.weights = g.in_degrees().float() ** 0.75\n", 154 | " def __call__(self, g, eids):\n", 155 | " src, _ = g.find_edges(eids)\n", 156 | " src = src.repeat_interleave(self.k)\n", 157 | " dst = self.weights.multinomial(len(src), replacement=True)\n", 158 | " return src, dst" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "negative sampler를 정의한 뒤, edge 데이터 로더를 이웃 샘플링으로 정의할 수 있습니다. \n", 166 | "여기서는 1개의 positive example에 대해 5개의 negative example을 만들어 주겠습니다." 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": 3, 172 | "metadata": {}, 173 | "outputs": [], 174 | "source": [ 175 | "sampler = dgl.dataloading.MultiLayerNeighborSampler([4, 4, 4])\n", 176 | "k = 5\n", 177 | "train_dataloader = dgl.dataloading.EdgeDataLoader(\n", 178 | " graph, torch.arange(graph.number_of_edges()), sampler,\n", 179 | " negative_sampler=NegativeSampler(graph, k),\n", 180 | " batch_size=1024,\n", 181 | " shuffle=True,\n", 182 | " drop_last=False,\n", 183 | " num_workers=4\n", 184 | ")" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "`train_dataloader`에서 미니배치 하나를 뜯어볼까요?" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 4, 197 | "metadata": {}, 198 | "outputs": [ 199 | { 200 | "name": "stdout", 201 | "output_type": "stream", 202 | "text": [ 203 | "(tensor([1147853, 2426712, 1342, ..., 292546, 134170, 102404]), Graph(num_nodes=7141, num_edges=1024,\n", 204 | " ndata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64)}\n", 205 | " edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64)}), Graph(num_nodes=7141, num_edges=5120,\n", 206 | " ndata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64)}\n", 207 | " edata_schemes={}), [Block(num_src_nodes=230241, num_dst_nodes=112984, num_edges=415458), Block(num_src_nodes=112984, num_dst_nodes=33355, num_edges=126722), Block(num_src_nodes=33355, num_dst_nodes=7141, num_edges=28040)])\n" 208 | ] 209 | } 210 | ], 211 | "source": [ 212 | "example_minibatch = next(iter(train_dataloader))\n", 213 | "print(example_minibatch)" 214 | ] 215 | }, 216 | { 217 | "cell_type": "markdown", 218 | "metadata": {}, 219 | "source": [ 220 | "이 예제 미니배치는 4개의 구성요소로 이루어져 있습니다.\n", 221 | "\n", 222 | "* 출력 노드의 표현을 계산하기 위해 필요한 입력 노드 리스트\n", 223 | "* 미니배치 내에서 샘플링된 노드에서 유도된 subgraph (negative example의 노드 포함)와 미니배치 내에서 샘플링된 엣지들\n", 224 | "* 미니배치 내에서 샘플링된 노드에서 유도된 subgraph (negative example의 노드 포함)와 negative sampler에서 샘플링된 존재하지 않는 엣지들\n", 225 | "* bipartite 그래프의 리스트, 각 레이어마다 하나씩" 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": 5, 231 | "metadata": {}, 232 | "outputs": [ 233 | { 234 | "name": "stdout", 235 | "output_type": "stream", 236 | "text": [ 237 | "Number of input nodes: 230241\n", 238 | "Positive graph # nodes: 7141 # edges: 1024\n", 239 | "Negative graph # noeds: 7141 # edges: 5120\n", 240 | "[Block(num_src_nodes=230241, num_dst_nodes=112984, num_edges=415458), Block(num_src_nodes=112984, num_dst_nodes=33355, num_edges=126722), Block(num_src_nodes=33355, num_dst_nodes=7141, num_edges=28040)]\n" 241 | ] 242 | } 243 | ], 244 | "source": [ 245 | "input_nodes, pos_graph, neg_graph, bipartites = example_minibatch\n", 246 | "print('Number of input nodes:', len(input_nodes))\n", 247 | "print('Positive graph # nodes:', pos_graph.number_of_nodes(), '# edges:', pos_graph.number_of_edges())\n", 248 | "print('Negative graph # noeds:', neg_graph.number_of_nodes(), '# edges:', neg_graph.number_of_edges())\n", 249 | "print(bipartites)" 250 | ] 251 | }, 252 | { 253 | "cell_type": "markdown", 254 | "metadata": {}, 255 | "source": [ 256 | "## 노드 표현을 위한 모델 정의\n", 257 | "\n", 258 | "모델은 아래와 같이 정의됩니다." 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": 6, 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [ 267 | "import torch.nn as nn\n", 268 | "import torch.nn.functional as F\n", 269 | "import dgl.nn as dglnn\n", 270 | "\n", 271 | "class SAGE(nn.Module):\n", 272 | " def __init__(self, in_feats, n_hidden, n_layers):\n", 273 | " super().__init__()\n", 274 | " self.n_layers = n_layers\n", 275 | " self.n_hidden = n_hidden\n", 276 | " self.layers = nn.ModuleList()\n", 277 | " self.layers.append(dglnn.SAGEConv(in_feats, n_hidden, 'mean'))\n", 278 | " for i in range(1, n_layers):\n", 279 | " self.layers.append(dglnn.SAGEConv(n_hidden, n_hidden, 'mean'))\n", 280 | " \n", 281 | " def forward(self, bipartites, x):\n", 282 | " for l, (layer, bipartite) in enumerate(zip(self.layers, bipartites)):\n", 283 | " x = layer(bipartite, x)\n", 284 | " if l != self.n_layers - 1:\n", 285 | " x = F.relu(x)\n", 286 | " return x" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "## GNN에서 노드 표현 얻기\n", 294 | "\n", 295 | "이전 튜토리얼에서는, 이웃 샘플링 없이 GNN 모델의 offline 추론을 수행하는 것에 대해 이야기 했었죠. \n", 296 | "그 방법을 그대로 복붙해서, 비지도 학습 환경에서의 GNN으로부터 노드 표현 출력값을 계산하는 데 사용할 수 있겠습니다. " 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": 7, 302 | "metadata": {}, 303 | "outputs": [], 304 | "source": [ 305 | "def inference(model, graph, input_features, batch_size):\n", 306 | " nodes = torch.arange(graph.number_of_nodes())\n", 307 | " \n", 308 | " sampler = dgl.dataloading.MultiLayerNeighborSampler([None]) # one layer at a time, taking all neighbors\n", 309 | " dataloader = dgl.dataloading.NodeDataLoader(\n", 310 | " graph, nodes, sampler,\n", 311 | " batch_size=batch_size,\n", 312 | " shuffle=False,\n", 313 | " drop_last=False,\n", 314 | " num_workers=0)\n", 315 | " \n", 316 | " with torch.no_grad():\n", 317 | " for l, layer in enumerate(model.layers):\n", 318 | " # Allocate a buffer of output representations for every node\n", 319 | " # Note that the buffer is on CPU memory.\n", 320 | " output_features = torch.zeros(graph.number_of_nodes(), model.n_hidden)\n", 321 | "\n", 322 | " for input_nodes, output_nodes, bipartites in tqdm.tqdm(dataloader):\n", 323 | " bipartite = bipartites[0].to(torch.device('cuda'))\n", 324 | "\n", 325 | " x = input_features[input_nodes].cuda()\n", 326 | "\n", 327 | " # the following code is identical to the loop body in model.forward()\n", 328 | " x = layer(bipartite, x)\n", 329 | " if l != model.n_layers - 1:\n", 330 | " x = F.relu(x)\n", 331 | "\n", 332 | " output_features[output_nodes] = x.cpu()\n", 333 | " input_features = output_features\n", 334 | " return output_features" 335 | ] 336 | }, 337 | { 338 | "cell_type": "markdown", 339 | "metadata": {}, 340 | "source": [ 341 | "## 엣지 스코어 예측 모델 정의하기\n", 342 | "\n", 343 | "미니 배치에서 필요한 노드 표현을 얻은 위에는, \n", 344 | "샘플링된 미니 배치의 존재하는/존재하지 않는 엣지에 대한 스코어를 예측하고 싶겠죠? \n", 345 | "\n", 346 | "이는 `apply_edges` 메서드로 쉽게 구현할 수 있습니다. \n", 347 | "여기서는, 두 대상 노드의 표현의 내적을 계산함으로써 단순히 예산할 수 있습니다." 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": 8, 353 | "metadata": {}, 354 | "outputs": [], 355 | "source": [ 356 | "class ScorePredictor(nn.Module):\n", 357 | " def forward(self, subgraph, x):\n", 358 | " with subgraph.local_scope():\n", 359 | " subgraph.ndata['x'] = x\n", 360 | " subgraph.apply_edges(dgl.function.u_dot_v('x', 'x', 'score'))\n", 361 | " return subgraph.edata['score']" 362 | ] 363 | }, 364 | { 365 | "cell_type": "markdown", 366 | "metadata": {}, 367 | "source": [ 368 | "## 학습된 임베딩의 성능 평가하기\n", 369 | "\n", 370 | "이 튜토리얼에서, 출력 임베딩을 학습 셋의 입력으로 사용해, 선형 분류 모델을 학습하여 출력 임베딩의 성능을 평가할 예정입니다. \n", 371 | "그 뒤, 검증/테스트 셋에 대해 정확도를 측정해 보겠습니다. " 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": 9, 377 | "metadata": {}, 378 | "outputs": [], 379 | "source": [ 380 | "import sklearn.linear_model\n", 381 | "import sklearn.metrics\n", 382 | "def evaluate(emb, label, train_nids, valid_nids, test_nids):\n", 383 | " classifier = sklearn.linear_model.LogisticRegression(solver='lbfgs', multi_class='multinomial', verbose=1, max_iter=1000)\n", 384 | " classifier.fit(emb[train_nids], label[train_nids])\n", 385 | " valid_pred = classifier.predict(emb[valid_nids])\n", 386 | " test_pred = classifier.predict(emb[test_nids])\n", 387 | " valid_acc = sklearn.metrics.accuracy_score(label[valid_nids], valid_pred)\n", 388 | " test_acc = sklearn.metrics.accuracy_score(label[test_nids], test_pred)\n", 389 | " return valid_acc, test_acc" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "metadata": {}, 395 | "source": [ 396 | "## 학습 루프 정의하기\n", 397 | "\n", 398 | "다음 코드는 모델을 초기화하고 최적화기(optimizer)를 정의합니다.\n" 399 | ] 400 | }, 401 | { 402 | "cell_type": "code", 403 | "execution_count": 10, 404 | "metadata": {}, 405 | "outputs": [], 406 | "source": [ 407 | "model = SAGE(node_features.shape[1], 128, 3).cuda()\n", 408 | "predictor = ScorePredictor().cuda()\n", 409 | "opt = torch.optim.Adam(list(model.parameters()) + list(predictor.parameters()))" 410 | ] 411 | }, 412 | { 413 | "cell_type": "markdown", 414 | "metadata": {}, 415 | "source": [ 416 | "아래는 비지도 학습과 평가를 수행하는 학습 루프로, \n", 417 | "validation set에 대해 최적의 성능을 보이는 모델을 저장하는 기능도 포함하고 있습니다." 418 | ] 419 | }, 420 | { 421 | "cell_type": "code", 422 | "execution_count": null, 423 | "metadata": {}, 424 | "outputs": [ 425 | { 426 | "name": "stderr", 427 | "output_type": "stream", 428 | "text": [ 429 | " 44%|████▍ | 26699/60410 [1:45:31<2:11:56, 4.26it/s, loss=0.614]" 430 | ] 431 | } 432 | ], 433 | "source": [ 434 | "import tqdm\n", 435 | "import sklearn.metrics\n", 436 | "\n", 437 | "best_accuracy = 0\n", 438 | "best_model_path = 'model.pt'\n", 439 | "for epoch in range(10):\n", 440 | " model.train()\n", 441 | " \n", 442 | " with tqdm.tqdm(train_dataloader) as tq:\n", 443 | " for step, (input_nodes, pos_graph, neg_graph, bipartites) in enumerate(tq):\n", 444 | " bipartites = [b.to(torch.device('cuda')) for b in bipartites]\n", 445 | " pos_graph = pos_graph.to(torch.device('cuda'))\n", 446 | " neg_graph = neg_graph.to(torch.device('cuda'))\n", 447 | " inputs = node_features[input_nodes].cuda()\n", 448 | " outputs = model(bipartites, inputs)\n", 449 | " pos_score = predictor(pos_graph, outputs)\n", 450 | " neg_score = predictor(neg_graph, outputs)\n", 451 | " \n", 452 | " score = torch.cat([pos_score, neg_score])\n", 453 | " label = torch.cat([torch.ones_like(pos_score), torch.zeros_like(neg_score)])\n", 454 | " loss = F.binary_cross_entropy_with_logits(score, label)\n", 455 | " \n", 456 | " opt.zero_grad()\n", 457 | " loss.backward()\n", 458 | " opt.step()\n", 459 | " \n", 460 | " tq.set_postfix({'loss': '%.03f' % loss.item()}, refresh=False)\n", 461 | " \n", 462 | " model.eval()\n", 463 | " emb = inference(model, graph, node_features, 16384)\n", 464 | " valid_acc, test_acc = evaluate(emb.numpy(), node_labels.numpy())\n", 465 | " print('Epoch {} Validation Accuracy {} Test Accuracy {}'.format(epoch, valid_acc, test_acc))\n", 466 | " if best_accuracy < valid_acc:\n", 467 | " best_accuracy = valid_acc\n", 468 | " torch.save(model.state_dict(), best_model_path)" 469 | ] 470 | }, 471 | { 472 | "cell_type": "markdown", 473 | "metadata": {}, 474 | "source": [ 475 | "## 결론\n", 476 | "\n", 477 | "이 튜토리얼에서, 비지도 학습 방식으로 다중 레이어 GraphSAGE 모델을 학습하는 방법을 GPU에 올라가지 않는 대규모 데이터셋의 링크 예측을 통해 배워 보았습니다. \n", 478 | "여기서 배운 이 방법은 어떤 사이즈의 그래프에 대해서도 확장될 수 있고, 단일 머신의 1개 GPU로도 작동합니다." 479 | ] 480 | }, 481 | { 482 | "cell_type": "markdown", 483 | "metadata": {}, 484 | "source": [ 485 | "## 다음은 무엇을 배우나요?\n", 486 | "\n", 487 | "다음 튜토리얼은 학습 절차를 단일 머신의 다중 GPU에 대해 scale-out하는 방법에 대해 배웁니다." 488 | ] 489 | } 490 | ], 491 | "metadata": { 492 | "kernelspec": { 493 | "display_name": "Python 3", 494 | "language": "python", 495 | "name": "python3" 496 | }, 497 | "language_info": { 498 | "codemirror_mode": { 499 | "name": "ipython", 500 | "version": 3 501 | }, 502 | "file_extension": ".py", 503 | "mimetype": "text/x-python", 504 | "name": "python", 505 | "nbconvert_exporter": "python", 506 | "pygments_lexer": "ipython3", 507 | "version": "3.8.3" 508 | } 509 | }, 510 | "nbformat": 4, 511 | "nbformat_minor": 4 512 | } 513 | -------------------------------------------------------------------------------- /large_graph/3_single_machine_multiple_GPU_training.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 다중 GPU를 사용한 GNN의 확률적(Storchastic) 학습 \n", 8 | "\n", 9 | "\n", 10 | "이번 튜토리얼에서는 Multi GPU 환경에서 노드 분류를 위한 다중 레이어 GraphSAGE 모델을 학습하는 방법을 배워보겠습니다. \n", 11 | "사용할 데이터셋은 OGB에서 제공하는 Amazon Copurchase Network으로, 240만 노드와 6100만 엣지를 포함하고 있으므로, 단일 GPU에는 올라가지 않습니다. \n", 12 | "\n", 13 | "\n", 14 | "이 튜토리얼은 다음 내용을 포함하고 있습니다. \n", 15 | "\n", 16 | "* `torch.nn.parallel.DistributedDataParallel` 메서드를 사용해 그래프 크기에 상관없이 GNN 모델을 단일 머신, 다중 GPU으로 학습하기.\n", 17 | "\n", 18 | "PyTorch `DistributedDataParallel` (혹은 짧게 말해 DDP)는 multi-GPU 학습의 일반적인 해결책입니다. \n", 19 | "DGL과 PyTorch DDP를 결합하는 것은 매우 쉬운데, 평범한 PyTorch 어플리케이션에서 적용하는 방법과 같이 하면 됩니다.\n", 20 | "\n", 21 | "* 데이터를 각 GPU에 대해 분할하기\n", 22 | "* PyTorch DDP를 사용해 모델 파라미터를 분배합니다\n", 23 | "* 이웃 샘플링 전략을 각자의 방법으로 커스터마이징합니다." 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 1, 29 | "metadata": {}, 30 | "outputs": [ 31 | { 32 | "name": "stderr", 33 | "output_type": "stream", 34 | "text": [ 35 | "Using backend: pytorch\n" 36 | ] 37 | } 38 | ], 39 | "source": [ 40 | "import numpy as np\n", 41 | "import dgl\n", 42 | "import torch\n", 43 | "import dgl.nn as dglnn\n", 44 | "import torch.nn as nn\n", 45 | "from torch.nn.parallel import DistributedDataParallel\n", 46 | "import torch.nn.functional as F\n", 47 | "import torch.multiprocessing as mp\n", 48 | "import sklearn.metrics\n", 49 | "import tqdm\n", 50 | "\n", 51 | "import utils" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "## 데이터셋 로드하기\n", 59 | "\n", 60 | "아래 코드는 첫번째 튜토리얼에서 복사되었습니다." 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 2, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "def load_data():\n", 70 | " import pickle\n", 71 | "\n", 72 | " with open('data.pkl', 'rb') as f:\n", 73 | " data = pickle.load(f)\n", 74 | " graph, node_features, node_labels, train_nids, valid_nids, test_nids = data\n", 75 | " utils.prepare_mp(graph)\n", 76 | " \n", 77 | " num_features = node_features.shape[1]\n", 78 | " num_classes = (node_labels.max() + 1).item()\n", 79 | " \n", 80 | " return graph, node_features, node_labels, train_nids, valid_nids, test_nids, num_features, num_classes" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "## 이웃 샘플링 커스터마이징하기\n", 88 | "\n", 89 | "이전 튜토리얼에서, `NodeDataLoader`와 `MultiLayerNeighborSampler`를 사용하는 방법을 배워 보았습니다. \n", 90 | "사실, `MultiLayerNeighborSampler`를 우리 마음대로 정한 샘플링 전략으로 대체할 수 있습니다. \n", 91 | "\n", 92 | "커스터마이징은 간단합니다. \n", 93 | "각 GNN 레이어에 대해, message passing에서 포함되는 엣지를 그래프로 지정해주면 됩니다. \n", 94 | "이 그래프는 기존 그래프와 같은 노드를 갖게 됩니다. \n", 95 | "\n", 96 | "예를 들어, `MultiLayerNeighborSampler`는 아래와 같이 구현됩니다." 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 3, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "class MultiLayerNeighborSampler(dgl.dataloading.BlockSampler):\n", 106 | " def __init__(self, fanouts):\n", 107 | " super().__init__(len(fanouts), return_eids=False)\n", 108 | " self.fanouts = fanouts\n", 109 | " \n", 110 | " def sample_frontier(self, layer_id, g, seed_nodes):\n", 111 | " fanout = self.fanouts[layer_id]\n", 112 | " return dgl.sampling.sample_neighbors(g, seed_nodes, fanout)" 113 | ] 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "metadata": {}, 118 | "source": [ 119 | "## Distributed Data Parallel (DDP)를 위한 데이터 로더 정의하기\n", 120 | "\n", 121 | "PyTorch DDP에서, 각 worker process는 정수값인 *rank*로 할당됩니다. \n", 122 | "이 rank는 worker process가 데이터셋의 어떤 파티션을 처리할지를 나타냅니다. \n", 123 | "\n", 124 | "따라서 데이터 로더 관점에서의 단일 GPU 경우와 다중 GPU 학습 간 유일한 차이점은, \n", 125 | "데이터 로더가 노드의 일부 파티션에 대해서만 iterate한다는 점입니다.\n" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 4, 131 | "metadata": {}, 132 | "outputs": [], 133 | "source": [ 134 | "def create_dataloader(rank, world_size, graph, nids):\n", 135 | " partition_size = len(nids) // world_size\n", 136 | " partition_offset = partition_size * rank\n", 137 | " nids = nids[partition_offset:partition_offset+partition_size]\n", 138 | " \n", 139 | " sampler = MultiLayerNeighborSampler([4, 4, 4])\n", 140 | " dataloader = dgl.dataloading.NodeDataLoader(\n", 141 | " graph, nids, sampler,\n", 142 | " batch_size=1024,\n", 143 | " shuffle=True,\n", 144 | " drop_last=False,\n", 145 | " num_workers=0\n", 146 | " )\n", 147 | " \n", 148 | " return dataloader" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "## 모델 정의하기\n", 156 | "\n", 157 | "모델 구현은 첫번째 튜토리얼에서 본 것과 정확히 동일합니다." 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": 5, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "class SAGE(nn.Module):\n", 167 | " def __init__(self, in_feats, n_hidden, n_classes, n_layers):\n", 168 | " super().__init__()\n", 169 | " self.n_layers = n_layers\n", 170 | " self.n_hidden = n_hidden\n", 171 | " self.n_classes = n_classes\n", 172 | " self.layers = nn.ModuleList()\n", 173 | " self.layers.append(dglnn.SAGEConv(in_feats, n_hidden, 'mean'))\n", 174 | " for i in range(1, n_layers - 1):\n", 175 | " self.layers.append(dglnn.SAGEConv(n_hidden, n_hidden, 'mean'))\n", 176 | " self.layers.append(dglnn.SAGEConv(n_hidden, n_classes, 'mean'))\n", 177 | " \n", 178 | " def forward(self, bipartites, x):\n", 179 | " for l, (layer, bipartite) in enumerate(zip(self.layers, bipartites)):\n", 180 | " x = layer(bipartite, x)\n", 181 | " if l != self.n_layers - 1:\n", 182 | " x = F.relu(x)\n", 183 | " return x" 184 | ] 185 | }, 186 | { 187 | "cell_type": "markdown", 188 | "metadata": {}, 189 | "source": [ 190 | "## 모델을 여러 GPU에 분배하기\n", 191 | "\n", 192 | "PyTorch DDP는 모델의 분산과 가중치의 synchronization을 관리해 줍니다. \n", 193 | "DGL에서는, 모델을 단순히 `torch.nn.parallel.DistributedDataParallel`으로 감싸 줌으로써 이 PyTorch DDP의 이점을 그대로 누릴 수 있습니다.\n", 194 | "\n", 195 | "분산 학습에서 추천되는 방식은 한 GPU에 학습 process를 하나만 가져가는 것입니다. \n", 196 | "이로써, 모델 instantiation 중에 process rank를 지정해줄 수도 있게 되는데, 이 rank가 GPU ID와 동일해지게 됩니다." 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 6, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "def init_model(rank, in_feats, n_hidden, n_classes, n_layers):\n", 206 | " model = SAGE(in_feats, n_hidden, n_classes, n_layers).to(rank)\n", 207 | " return DistributedDataParallel(model, device_ids=[rank], output_device=rank)" 208 | ] 209 | }, 210 | { 211 | "cell_type": "markdown", 212 | "metadata": {}, 213 | "source": [ 214 | "## 1개 process를 위한 학습 루프\n", 215 | "\n", 216 | "학습 루프는 다른 PyTorch DDP 어플리케이션과 똑같이 생겼습니다." 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": 7, 222 | "metadata": {}, 223 | "outputs": [], 224 | "source": [ 225 | "@utils.fix_openmp\n", 226 | "def train(rank, world_size, data):\n", 227 | " # data is the output of load_data\n", 228 | " torch.distributed.init_process_group(\n", 229 | " backend='nccl',\n", 230 | " init_method='tcp://127.0.0.1:12345',\n", 231 | " world_size=world_size,\n", 232 | " rank=rank)\n", 233 | " torch.cuda.set_device(rank)\n", 234 | " \n", 235 | " graph, node_features, node_labels, train_nids, valid_nids, test_nids, num_features, num_classes = data\n", 236 | " \n", 237 | " train_dataloader = create_dataloader(rank, world_size, graph, train_nids)\n", 238 | " # We only use one worker for validation\n", 239 | " valid_dataloader = create_dataloader(0, 1, graph, valid_nids)\n", 240 | " \n", 241 | " model = init_model(rank, num_features, 128, num_classes, 3)\n", 242 | " opt = torch.optim.Adam(model.parameters())\n", 243 | " torch.distributed.barrier()\n", 244 | " \n", 245 | " best_accuracy = 0\n", 246 | " best_model_path = 'model.pt'\n", 247 | " for epoch in range(10):\n", 248 | " model.train()\n", 249 | "\n", 250 | " for step, (input_nodes, output_nodes, bipartites) in enumerate(train_dataloader):\n", 251 | " bipartites = [b.to(rank) for b in bipartites]\n", 252 | " inputs = node_features[input_nodes].cuda()\n", 253 | " labels = node_labels[output_nodes].cuda()\n", 254 | " predictions = model(bipartites, inputs)\n", 255 | "\n", 256 | " loss = F.cross_entropy(predictions, labels)\n", 257 | " opt.zero_grad()\n", 258 | " loss.backward()\n", 259 | " opt.step()\n", 260 | "\n", 261 | " accuracy = sklearn.metrics.accuracy_score(labels.cpu().numpy(), predictions.argmax(1).detach().cpu().numpy())\n", 262 | "\n", 263 | " if rank == 0 and step % 10 == 0:\n", 264 | " print('Epoch {:05d} Step {:05d} Loss {:.04f}'.format(epoch, step, loss.item()))\n", 265 | "\n", 266 | " torch.distributed.barrier()\n", 267 | " \n", 268 | " if rank == 0:\n", 269 | " model.eval()\n", 270 | " predictions = []\n", 271 | " labels = []\n", 272 | " with torch.no_grad():\n", 273 | " for input_nodes, output_nodes, bipartites in valid_dataloader:\n", 274 | " bipartites = [b.to(rank) for b in bipartites]\n", 275 | " inputs = node_features[input_nodes].cuda()\n", 276 | " labels.append(node_labels[output_nodes].numpy())\n", 277 | " predictions.append(model.module(bipartites, inputs).argmax(1).cpu().numpy())\n", 278 | " predictions = np.concatenate(predictions)\n", 279 | " labels = np.concatenate(labels)\n", 280 | " accuracy = sklearn.metrics.accuracy_score(labels, predictions)\n", 281 | " print('Epoch {} Validation Accuracy {}'.format(epoch, accuracy))\n", 282 | " if best_accuracy < accuracy:\n", 283 | " best_accuracy = accuracy\n", 284 | " torch.save(model.module.state_dict(), best_model_path)\n", 285 | " \n", 286 | " torch.distributed.barrier()" 287 | ] 288 | }, 289 | { 290 | "cell_type": "code", 291 | "execution_count": 8, 292 | "metadata": {}, 293 | "outputs": [ 294 | { 295 | "name": "stdout", 296 | "output_type": "stream", 297 | "text": [ 298 | "Epoch 00000 Step 00000 Loss 5.7553\n", 299 | "Epoch 00000 Step 00010 Loss 2.6858\n", 300 | "Epoch 00000 Step 00020 Loss 2.1455\n", 301 | "Epoch 00000 Step 00030 Loss 1.7148\n", 302 | "Epoch 00000 Step 00040 Loss 1.6470\n", 303 | "Epoch 0 Validation Accuracy 0.7247158151717824\n", 304 | "Epoch 00001 Step 00000 Loss 1.3390\n", 305 | "Epoch 00001 Step 00010 Loss 1.3108\n", 306 | "Epoch 00001 Step 00020 Loss 1.3176\n", 307 | "Epoch 00001 Step 00030 Loss 1.4312\n", 308 | "Epoch 00001 Step 00040 Loss 1.1797\n", 309 | "Epoch 1 Validation Accuracy 0.7972687739999491\n", 310 | "Epoch 00002 Step 00000 Loss 1.0574\n", 311 | "Epoch 00002 Step 00010 Loss 1.1461\n", 312 | "Epoch 00002 Step 00020 Loss 1.0746\n", 313 | "Epoch 00002 Step 00030 Loss 1.0027\n", 314 | "Epoch 00002 Step 00040 Loss 0.9308\n", 315 | "Epoch 2 Validation Accuracy 0.8152480736464665\n", 316 | "Epoch 00003 Step 00000 Loss 0.9768\n", 317 | "Epoch 00003 Step 00010 Loss 1.0767\n", 318 | "Epoch 00003 Step 00020 Loss 0.9237\n", 319 | "Epoch 00003 Step 00030 Loss 1.0979\n", 320 | "Epoch 00003 Step 00040 Loss 0.8528\n", 321 | "Epoch 3 Validation Accuracy 0.83111664928922\n", 322 | "Epoch 00004 Step 00000 Loss 0.9134\n", 323 | "Epoch 00004 Step 00010 Loss 0.9284\n", 324 | "Epoch 00004 Step 00020 Loss 0.8158\n", 325 | "Epoch 00004 Step 00030 Loss 0.9542\n", 326 | "Epoch 00004 Step 00040 Loss 0.9215\n", 327 | "Epoch 4 Validation Accuracy 0.839508684484907\n", 328 | "Epoch 00005 Step 00000 Loss 0.9607\n", 329 | "Epoch 00005 Step 00010 Loss 0.9081\n", 330 | "Epoch 00005 Step 00020 Loss 0.8607\n", 331 | "Epoch 00005 Step 00030 Loss 0.8400\n", 332 | "Epoch 00005 Step 00040 Loss 0.8883\n", 333 | "Epoch 5 Validation Accuracy 0.8434249675762276\n", 334 | "Epoch 00006 Step 00000 Loss 0.7871\n", 335 | "Epoch 00006 Step 00010 Loss 0.9050\n", 336 | "Epoch 00006 Step 00020 Loss 0.8587\n", 337 | "Epoch 00006 Step 00030 Loss 0.7345\n", 338 | "Epoch 00006 Step 00040 Loss 0.7846\n", 339 | "Epoch 6 Validation Accuracy 0.8497317091778348\n", 340 | "Epoch 00007 Step 00000 Loss 0.7165\n", 341 | "Epoch 00007 Step 00010 Loss 0.8370\n", 342 | "Epoch 00007 Step 00020 Loss 0.8072\n", 343 | "Epoch 00007 Step 00030 Loss 0.7852\n", 344 | "Epoch 00007 Step 00040 Loss 0.8651\n", 345 | "Epoch 7 Validation Accuracy 0.853012232027058\n", 346 | "Epoch 00008 Step 00000 Loss 0.8609\n", 347 | "Epoch 00008 Step 00010 Loss 0.6784\n", 348 | "Epoch 00008 Step 00020 Loss 0.7328\n", 349 | "Epoch 00008 Step 00030 Loss 0.8150\n", 350 | "Epoch 00008 Step 00040 Loss 0.8347\n", 351 | "Epoch 8 Validation Accuracy 0.852732497520535\n", 352 | "Epoch 00009 Step 00000 Loss 0.7051\n", 353 | "Epoch 00009 Step 00010 Loss 0.7738\n", 354 | "Epoch 00009 Step 00020 Loss 0.8157\n", 355 | "Epoch 00009 Step 00030 Loss 0.7437\n", 356 | "Epoch 00009 Step 00040 Loss 0.7249\n", 357 | "Epoch 9 Validation Accuracy 0.8549703735727182\n" 358 | ] 359 | } 360 | ], 361 | "source": [ 362 | "if __name__ == '__main__':\n", 363 | " procs = []\n", 364 | " data = load_data()\n", 365 | " for proc_id in range(4): # 4 gpus\n", 366 | " p = mp.Process(target=train, args=(proc_id, 4, data))\n", 367 | " p.start()\n", 368 | " procs.append(p)\n", 369 | " for p in procs:\n", 370 | " p.join()" 371 | ] 372 | }, 373 | { 374 | "cell_type": "markdown", 375 | "metadata": {}, 376 | "source": [ 377 | "## 결론\n", 378 | "\n", 379 | "이 튜토리얼에서, GPU에 올라가지 않는 대규모 데이터에서 노드 분류를 위한 다중 레이어 GraphSAGE 모델을 학습하는 방법을 배웠습니다. \n", 380 | "여기서 배운 이 방법은 어떤 사이즈의 그래프에서든 확장될 수 있으며, \n", 381 | "단일 머신의 *몇 개의 GPU 에서든* 작동합니다." 382 | ] 383 | }, 384 | { 385 | "cell_type": "markdown", 386 | "metadata": {}, 387 | "source": [ 388 | "## 추가 자료: DDP로 학습할 때의 주의점\n", 389 | "\n", 390 | "DDP 코드를 작성할 때, 이 두가지 에러를 겪을 수 있습니다. \n", 391 | "\n", 392 | "* `Cannot re-initialize CUDA in forked subprocess` \n", 393 | "\n", 394 | " 이는 `mp.Process`를 사용해 subprocess를 만들기 전에 CUDA context를 초기화 해서 발생합니다.\n", 395 | " 해결책은 다음과 같습니다. \n", 396 | " \n", 397 | " * `mp.Process`를 호출하기 전에, CUDA context를 초기화할 수 있는 모든 가능한 코드를 제거합니다. \n", 398 | " 예를 들어, `mp.Process`를 호출하기 전에 GPU의 갯수를 `torch.cuda.device_count()`로 확인할 수 없습니다. \n", 399 | " 왜냐하면, 갯수를 확인하는 `torch.cuda.device_count()`는 CUDA context를 초기화하기 때문입니다. \n", 400 | " \n", 401 | " CUDA context가 초기화 되었는지의 여부를 `torch.cuda.is_initialized()`로 확인해볼 수 있습니다.\n", 402 | " \n", 403 | " * `mp.Process`로 forking하지 마시고, `torch.multiprocessing.spawn()`를 사용해 process를 생성하세요. \n", 404 | " (전자 방식의) 불리점은, 파이썬이 이 방법으로 생성된 모든 process에 대해 그래프 storage를 복제한다는 점입니다. \n", 405 | " 메모리 소비량이 선형적으로 증가하게 되지요.\n", 406 | " \n", 407 | "* 학습 프로세스가 미니배치 iteration중에 멈춤\n", 408 | " 이 원인은 다음과 같습니다. [lasting bug in the interaction between GNU OpenMP and `fork`](https://github.com/pytorch/pytorch/issues/17199) \n", 409 | " 다른 해결책은, `mp.Process`의 목표 함수를 데코레이터 `utils.fix_openmp`를 사용해 감싸는 것입니다. \n", 410 | " 이 방식은 이 튜토리얼에서 구현되어 있습니다." 411 | ] 412 | } 413 | ], 414 | "metadata": { 415 | "kernelspec": { 416 | "display_name": "Python 3", 417 | "language": "python", 418 | "name": "python3" 419 | }, 420 | "language_info": { 421 | "codemirror_mode": { 422 | "name": "ipython", 423 | "version": 3 424 | }, 425 | "file_extension": ".py", 426 | "mimetype": "text/x-python", 427 | "name": "python", 428 | "nbconvert_exporter": "python", 429 | "pygments_lexer": "ipython3", 430 | "version": "3.8.3" 431 | } 432 | }, 433 | "nbformat": 4, 434 | "nbformat_minor": 4 435 | } 436 | -------------------------------------------------------------------------------- /large_graph/assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/large_graph/assets/1.png -------------------------------------------------------------------------------- /large_graph/assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/large_graph/assets/2.png -------------------------------------------------------------------------------- /large_graph/assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/large_graph/assets/3.png -------------------------------------------------------------------------------- /large_graph/assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/large_graph/assets/4.png -------------------------------------------------------------------------------- /large_graph/assets/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/large_graph/assets/5.png -------------------------------------------------------------------------------- /large_graph/assets/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/large_graph/assets/6.png -------------------------------------------------------------------------------- /large_graph/assets/anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/large_graph/assets/anim.gif -------------------------------------------------------------------------------- /large_graph/assets/bipartite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/large_graph/assets/bipartite.png -------------------------------------------------------------------------------- /large_graph/assets/seed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/large_graph/assets/seed.png -------------------------------------------------------------------------------- /large_graph/sampling.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myeonghak/DGL-tutorial/bcc3c4a4d943c9d5bafd528a3d2cb896a4ce6496/large_graph/sampling.pptx -------------------------------------------------------------------------------- /large_graph/utils.py: -------------------------------------------------------------------------------- 1 | import torch.multiprocessing as mp 2 | from _thread import start_new_thread 3 | from functools import wraps 4 | import traceback 5 | 6 | def prepare_mp(graph): 7 | graph.in_degrees(0) 8 | graph.out_degrees(0) 9 | graph.find_edges([0]) 10 | 11 | def fix_openmp(func): 12 | """ 13 | Wraps a process entry point to make it work with OpenMP. 14 | """ 15 | @wraps(func) 16 | def decorated_function(*args, **kwargs): 17 | queue = mp.Queue() 18 | def _queue_result(): 19 | exception, trace, res = None, None, None 20 | try: 21 | res = func(*args, **kwargs) 22 | except Exception as e: 23 | exception = e 24 | trace = traceback.format_exc() 25 | queue.put((res, exception, trace)) 26 | 27 | start_new_thread(_queue_result, ()) 28 | result, exception, trace = queue.get() 29 | if exception is None: 30 | return result 31 | else: 32 | assert isinstance(exception, Exception) 33 | raise exception.__class__(trace) 34 | return decorated_function 35 | --------------------------------------------------------------------------------