├── 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 |
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 |
58 | image pull이 잘 이루어 졌다면, 아래와 같은 내용을 확인해 볼 수 있습니다.
59 |
60 |
61 | ```
62 | docker images
63 | ```
64 |
65 |
66 |
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 | ""
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개의 엣지 타입만을 가지고 있는 그래프)의 srcdata
와 dstdata
는 ndata
와 동일합니다. \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 | "\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 | ""
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개의 엣지 타입만을 가지고 있는 그래프)의 srcdata
와 dstdata
는 ndata
와 동일합니다. \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 | "\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 | "\n",
276 | "\n",
277 | "그 이웃의 노드 피처를 통합할 필요가 있습니다. 아래의 녹색 노드를 보세요.\n",
278 | "\n",
279 | ""
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 | "\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 | ""
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 | ""
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 | ""
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 | ""
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 | ""
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 | ""
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 | ""
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 |
--------------------------------------------------------------------------------