├── requirements.txt ├── imgs ├── pytorch_KR.png └── pytorch_logo_2018.svg ├── 4_Spatial_Graph_Convolution ├── imgs │ ├── GAT.png │ ├── a_ij.png │ ├── e_ij.png │ ├── multi_head.png │ ├── citeseer_attention_test.png │ └── citeseer_attention_train.png ├── models.py ├── test.py ├── opts.py ├── layers.py ├── train.py ├── README.md └── utils.py ├── 2_Understanding_Graphs ├── figures │ ├── norm_adj.png │ └── isolated_nodes.png ├── load_planetoid.py ├── README.md └── preprocess_planetoid.py ├── 3_Spectral_Graph_Convolution ├── imgs │ ├── benz.png │ ├── bond.png │ ├── ecfp.png │ ├── gcn_web.png │ ├── pytorch.png │ ├── caffeine.png │ ├── norm_adj.png │ ├── result_mol.png │ ├── structure.png │ ├── cora_gcn_test.png │ └── cora_gcn_train.png ├── opts.py ├── test.py ├── models.py ├── layers.py ├── train.py ├── molecule_utils.py ├── utils.py ├── forward_mol.py └── README.md ├── 1_Going_Beyond_Euclidean_Data ├── figures │ ├── 2d.jpg │ ├── mesh.jpg │ ├── voice.jpg │ ├── voxel.jpg │ ├── 3d_earth.jpg │ ├── augment.png │ ├── distance.png │ ├── point-cloud.png │ ├── point_cloud.jpg │ ├── brain_functions.jpeg │ ├── brain_functions.jpg │ └── social_network.png └── README.md ├── Dockerfile ├── LICENSE ├── .gitignore └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | networkx 2 | scipy 3 | -------------------------------------------------------------------------------- /imgs/pytorch_KR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/imgs/pytorch_KR.png -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/imgs/GAT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/4_Spatial_Graph_Convolution/imgs/GAT.png -------------------------------------------------------------------------------- /2_Understanding_Graphs/figures/norm_adj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/2_Understanding_Graphs/figures/norm_adj.png -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/benz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/benz.png -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/bond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/bond.png -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/ecfp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/ecfp.png -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/imgs/a_ij.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/4_Spatial_Graph_Convolution/imgs/a_ij.png -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/imgs/e_ij.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/4_Spatial_Graph_Convolution/imgs/e_ij.png -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/2d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/2d.jpg -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/gcn_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/gcn_web.png -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/pytorch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/pytorch.png -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/mesh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/mesh.jpg -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/voice.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/voice.jpg -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/voxel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/voxel.jpg -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/caffeine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/caffeine.png -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/norm_adj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/norm_adj.png -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/result_mol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/result_mol.png -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/structure.png -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/imgs/multi_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/4_Spatial_Graph_Convolution/imgs/multi_head.png -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/3d_earth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/3d_earth.jpg -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/augment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/augment.png -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/distance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/distance.png -------------------------------------------------------------------------------- /2_Understanding_Graphs/figures/isolated_nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/2_Understanding_Graphs/figures/isolated_nodes.png -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/point-cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/point-cloud.png -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/point_cloud.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/point_cloud.jpg -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/cora_gcn_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/cora_gcn_test.png -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/imgs/cora_gcn_train.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/3_Spectral_Graph_Convolution/imgs/cora_gcn_train.png -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/brain_functions.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/brain_functions.jpeg -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/brain_functions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/brain_functions.jpg -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/figures/social_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/1_Going_Beyond_Euclidean_Data/figures/social_network.png -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/imgs/citeseer_attention_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/4_Spatial_Graph_Convolution/imgs/citeseer_attention_test.png -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/imgs/citeseer_attention_train.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmsookim/graph-tutorial.pytorch/HEAD/4_Spatial_Graph_Convolution/imgs/citeseer_attention_train.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #FROM pytorch/pytorch:1.0-cuda10.0-cudnn7-runtime 2 | FROM pytorch/pytorch:0.4.1-cuda9-cudnn7-runtime 3 | 4 | COPY . /root/example 5 | WORKDIR /root/example 6 | RUN pip install pip -U && pip install -r requirements.txt 7 | 8 | # Update to Torch 1.0 9 | RUN conda install pytorch torchvision -y -c pytorch 10 | 11 | # Planetoid dataset 12 | RUN git clone https://github.com/kimiyoung/planetoid.git 13 | RUN mkdir ../Data 14 | RUN mv ./planetoid/data ../Data/Planetoid/ 15 | RUN rm -rf planetoid 16 | RUN conda install -y -c rdkit rdkit 17 | RUN conda install -y -c rdkit nox 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bumsoo Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # t7 files 107 | *.t7 108 | -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/models.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | from layers import GraphAttention 5 | 6 | class GAT(nn.Module): 7 | def __init__(self, nfeat, nhid, nclass, dropout, nheads, nouts, alpha): 8 | super(GAT, self).__init__() 9 | self.dropout = dropout 10 | 11 | self.attentions = [GraphAttention(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)] # concat 12 | for i, attention in enumerate(self.attentions): 13 | # Each attention will be GraphAttention(nfeat, nhid, dropout, concat=True) 14 | self.add_module('attention_1_{}'.format(i), attention) 15 | 16 | ''' 17 | < Inductive Learning > 18 | self.attentions_2 = [GraphAttention(nhid * nheads, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)] # concat 19 | for i, attention in enumerate(self.attentions_2): 20 | # Each attention will be GraphAttention(nfeat, nhid, dropout, concat=True) 21 | self.add_module('attention_2_{}'.format(i), attention) 22 | ''' 23 | self.out_att = [GraphAttention(nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False) for _ in range(nouts)] 24 | for i, attention in enumerate(self.out_att): 25 | self.add_module('attention_out_{}'.format(i), attention) 26 | 27 | def forward(self, x, adj): 28 | x = F.dropout(x, self.dropout, training=self.training) # in_dropout 29 | x = torch.cat([att(x, adj) for att in self.attentions], dim=1) # concat 30 | #x = F.dropout(x, self.dropout, training=self.training) 31 | ''' 32 | < Inductive learning > 33 | res = x 34 | x = F.dropout(x, self.dropout, training=self.training) # in_dropout 35 | x = torch.cat([att(x, adj) for att in self.attentions_2], dim=1) 36 | x = x + res 37 | ''' 38 | x = torch.mean(torch.stack([att(x, adj) for att in self.out_att], dim=1), dim=1) # avg (for pubmed) 39 | x = F.elu(x) 40 | 41 | return F.log_softmax(x, dim=1) 42 | -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/opts.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import torch 4 | from pathlib import Path 5 | 6 | class BaseOptions(): 7 | def __init__(self): 8 | self.parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 9 | self.initialized = False 10 | 11 | def initialize(self): 12 | self.parser.add_argument('--dataroot', type=str, default=os.path.join(Path.home(), "Data", "Planetoid"), help='path') 13 | self.parser.add_argument('--dataset', type=str, default='pubmed', help='[cora | citeseer | pubmed]') 14 | self.parser.add_argument('--num_hidden', type=int, default=32, help='number of features') 15 | self.parser.add_argument('--dropout', type=float, default=0.5, help='dropout') 16 | self.parser.add_argument('--weight_decay', type=float, default=5e-4, help='weight decay') 17 | self.parser.add_argument('--init_type', type=str, default='uniform', help='[uniform | xavier]') 18 | self.parser.add_argument('--model', type=str, default='basic', help='[basic]') 19 | 20 | def parse(self): 21 | if not self.initialized: 22 | self.initialize() 23 | 24 | self.opt = self.parser.parse_args() 25 | self.opt.isTrain = self.isTrain 26 | args = vars(self.opt) 27 | 28 | return self.opt 29 | 30 | class TrainOptions(BaseOptions): 31 | # Override 32 | def initialize(self): 33 | BaseOptions.initialize(self) 34 | self.parser.add_argument('--lr', type=float, default=1e-3, help='initial learning rate') 35 | self.parser.add_argument('--optimizer', type=str, default='adam', help='[sgd | adam]') 36 | self.parser.add_argument('--epoch', type=int, default=5000, help='number of training epochs') 37 | self.parser.add_argument('--lr_decay_epoch', type=int, default=1000, help='multiply by a gamma every set iter') 38 | self.parser.add_argument('--alpha', type=float, default=0.2, help='Alpha value for the leaky_relu') 39 | self.isTrain = True 40 | 41 | class TestOptions(BaseOptions): 42 | def initialize(self): 43 | BaseOptions.initialize(self) 44 | self.isTrain = False 45 | -------------------------------------------------------------------------------- /imgs/pytorch_logo_2018.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 18 | 20 | 21 | 24 | 26 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/test.py: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Author : Bumsoo Kim, 2018 3 | # Github : https://github.com/meliketoy/graph-tutorial.pytorch 4 | # 5 | # Korea University, Data-Mining Lab 6 | # Graph Convolutional Neural Network 7 | # 8 | # Description : test.py 9 | # The main code for testing Graph Attention Networks. 10 | # *********************************************************** 11 | 12 | import time 13 | import os 14 | import numpy as np 15 | import torch 16 | import torch.nn.functional as F 17 | import torch.optim as optim 18 | 19 | from utils import * 20 | from models import GAT 21 | from opts import TestOptions 22 | 23 | """ 24 | N : number of nodes 25 | D : number of features per node 26 | E : number of classes 27 | 28 | @ input : 29 | - adjacency matrix (N x N) 30 | - feature matrix (N x D) 31 | - label matrix (N x E) 32 | 33 | @ dataset : 34 | - citeseer 35 | - cora 36 | - pubmed 37 | """ 38 | opt = TestOptions().parse() 39 | 40 | adj, features, labels, idx_train, idx_val, idx_test = load_data(path=opt.dataroot, dataset=opt.dataset) 41 | use_gpu = torch.cuda.is_available() 42 | 43 | print("\n[STEP 2] : Obtain (adjacency, feature, label) matrix") 44 | print("| Adjacency matrix : {}".format(adj.shape)) 45 | print("| Feature matrix : {}".format(features.shape)) 46 | print("| Label matrix : {}".format(labels.shape)) 47 | 48 | load_model = torch.load(os.path.join('checkpoint', opt.dataset, '%s.t7' %(opt.model))) 49 | model = load_model['model'].cpu() 50 | acc_val = load_model['acc'] 51 | 52 | if use_gpu: 53 | _, features, adj, labels, idx_test = \ 54 | list(map(lambda x: x.cuda(), [model, features, adj, labels, idx_test])) 55 | 56 | def test(): 57 | print("\n[STEP 4] : Testing") 58 | 59 | model.eval() 60 | output = model(features, adj) 61 | 62 | print(output[idx_test].shape) 63 | print(labels[idx_test].shape) 64 | 65 | acc_test = accuracy(output[idx_test], labels[idx_test]) 66 | print("| Validation acc : {}%".format(acc_val.data.cpu().numpy() * 100)) 67 | print("| Test acc : {}%\n".format(acc_test.data.cpu().numpy() * 100)) 68 | 69 | if __name__ == "__main__": 70 | test() 71 | -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/test.py: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Author : Bumsoo Kim, 2018 3 | # Github : https://github.com/meliketoy/graph-tutorial.pytorch 4 | # 5 | # Korea University, Data-Mining Lab 6 | # Graph Convolutional Neural Network 7 | # 8 | # Description : test.py 9 | # The main code for testing Graph Convolutional Networks. 10 | # *********************************************************** 11 | 12 | import time 13 | import os 14 | import numpy as np 15 | import torch 16 | import torch.nn.functional as F 17 | import torch.optim as optim 18 | 19 | from utils import * 20 | from models import GCN 21 | from opts import TestOptions 22 | 23 | """ 24 | N : number of nodes 25 | D : number of features per node 26 | E : number of classes 27 | 28 | @ input : 29 | - adjacency matrix (N x N) 30 | - feature matrix (N x D) 31 | - label matrix (N x E) 32 | 33 | @ dataset : 34 | - citeseer 35 | - cora 36 | - pubmed 37 | """ 38 | opt = TestOptions().parse() 39 | 40 | adj, features, labels, idx_train, idx_val, idx_test = load_data(path=opt.dataroot, dataset=opt.dataset) 41 | use_gpu = torch.cuda.is_available() 42 | 43 | print("\n[STEP 2] : Obtain (adjacency, feature, label) matrix") 44 | print("| Adjacency matrix : {}".format(adj.shape)) 45 | print("| Feature matrix : {}".format(features.shape)) 46 | print("| Label matrix : {}".format(labels.shape)) 47 | 48 | load_model = torch.load(os.path.join('checkpoint', opt.dataset, '%s.t7' %(opt.model))) 49 | model = load_model['model'].cpu() 50 | acc_val = load_model['acc'] 51 | 52 | if use_gpu: 53 | _, features, adj, labels, idx_test = \ 54 | list(map(lambda x: x.cuda(), [model, features, adj, labels, idx_test])) 55 | 56 | def test(): 57 | print("\n[STEP 4] : Testing") 58 | 59 | model.eval() 60 | output = model(features, adj) 61 | 62 | print(output[idx_test].shape) 63 | print(labels[idx_test].shape) 64 | 65 | acc_test = accuracy(output[idx_test], labels[idx_test]) 66 | print("| Validation acc : {}%".format(acc_val.data.cpu().numpy() * 100)) 67 | print("| Test acc : {}%\n".format(acc_test.data.cpu().numpy() * 100)) 68 | 69 | if __name__ == "__main__": 70 | test() 71 | -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/opts.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import torch 4 | from pathlib import Path 5 | 6 | class BaseOptions(): 7 | def __init__(self): 8 | self.parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 9 | self.initialized = False 10 | 11 | def initialize(self): 12 | self.parser.add_argument('--dataroot', type=str, default=os.path.join(Path.home(), "Data", "Planetoid"), help='path') 13 | self.parser.add_argument('--dataset', type=str, default='cora', help='[cora | citeseer | pubmed]') 14 | self.parser.add_argument('--num_hidden', type=int, default=8, help='number of features') 15 | self.parser.add_argument('--dropout', type=float, default=0.6, help='dropout') 16 | self.parser.add_argument('--weight_decay', type=float, default=5e-4, help='weight decay') 17 | self.parser.add_argument('--init_type', type=str, default='xavier', help='[uniform | xavier]') 18 | self.parser.add_argument('--model', type=str, default='attention', help='[attention]') 19 | 20 | def parse(self): 21 | if not self.initialized: 22 | self.initialize() 23 | 24 | self.opt = self.parser.parse_args() 25 | self.opt.isTrain = self.isTrain 26 | args = vars(self.opt) 27 | 28 | return self.opt 29 | 30 | class TrainOptions(BaseOptions): 31 | # Override 32 | def initialize(self): 33 | BaseOptions.initialize(self) 34 | self.parser.add_argument('--lr', type=float, default=5e-3, help='initial learning rate') 35 | self.parser.add_argument('--optimizer', type=str, default='adam', help='[sgd | adam]') 36 | self.parser.add_argument('--epoch', type=int, default=800, help='number of training epochs') 37 | self.parser.add_argument('--tolerance', type=int, default=100, help='multiply by a gamma every set iter') 38 | self.parser.add_argument('--nb_heads', type=int, default=8, help='number of input head attentions') 39 | self.parser.add_argument('--nb_outs', type=int, default=1, help='number of output head attentions') 40 | self.parser.add_argument('--alpha', type=float, default=0.2, help='Alpha value for the leaky_relu') 41 | self.isTrain = True 42 | 43 | class TestOptions(BaseOptions): 44 | def initialize(self): 45 | BaseOptions.initialize(self) 46 | self.isTrain = False 47 | -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/models.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | from layers import GraphConvolution, GraphAttention 5 | 6 | class GCN(nn.Module): 7 | def __init__(self, nfeat, nhid, nclass, dropout, init): 8 | super(GCN, self).__init__() 9 | 10 | self.gc1 = GraphConvolution(nfeat, nhid, init=init) 11 | self.gc2 = GraphConvolution(nhid, nclass, init=init) 12 | self.dropout = dropout 13 | 14 | def bottleneck(self, path1, path2, path3, adj, in_x): 15 | return F.relu(path3(F.relu(path2(F.relu(path1(in_x, adj)), adj)), adj)) 16 | 17 | def forward(self, x, adj): 18 | x = F.dropout(F.relu(self.gc1(x, adj)), self.dropout, training=self.training) 19 | x = self.gc2(x, adj) 20 | 21 | return F.log_softmax(x, dim=1) 22 | 23 | class GCN_drop_in(nn.Module): 24 | def __init__(self, nfeat, nhid, nclass, dropout, init): 25 | super(GCN_drop_in, self).__init__() 26 | 27 | self.gc1 = GraphConvolution(nfeat, nhid, init=init) 28 | self.gc2 = GraphConvolution(nhid, nclass, init=init) 29 | self.dropout = dropout 30 | 31 | def bottleneck(self, path1, path2, path3, adj, in_x): 32 | return F.relu(path3(F.relu(path2(F.relu(path1(in_x, adj)), adj)), adj)) 33 | 34 | def forward(self, x, adj): 35 | x = F.dropout(x, self.dropout, training=self.training) 36 | x = F.dropout(F.relu(self.gc1(x, adj)), self.dropout, training=self.training) 37 | x = self.gc2(x, adj) 38 | 39 | return F.log_softmax(x, dim=1) 40 | 41 | class GAT(nn.Module): 42 | def __init__(self, nfeat, nhid, nclass, dropout, alpha, nheads): 43 | super(GAT, self).__init__() 44 | self.dropout = dropout 45 | 46 | self.attentions = [GraphAttention(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)] 47 | for i, attention in enumerate(self.attentions): 48 | self.add_module('attention_{}'.format(i), attention) 49 | 50 | self.out_att = GraphAttention(nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False) 51 | 52 | def forward(self, x, adj): 53 | x = F.dropout(x, self.dropout, training=self.training) 54 | x = torch.cat([att(x, adj) for att in self.attentions], dim=1) 55 | x = F.dropout(x, self.dropout, training=self.training) 56 | x = F.elu(self.out_att(x, adj)) 57 | return F.log_softmax(x, dim=1) 58 | -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/layers.py: -------------------------------------------------------------------------------- 1 | import math 2 | import torch 3 | import numpy as np 4 | import torch.nn as nn 5 | import torch.nn.functional as F 6 | from torch.nn.parameter import Parameter 7 | from torch.nn.modules.module import Module 8 | 9 | class GraphAttention(nn.Module): 10 | """ 11 | Simple GAT layer, similar to https://arxiv.org/abs/1710.10903 12 | """ 13 | 14 | def __init__(self, in_features, out_features, dropout, alpha, concat=True): 15 | super(GraphAttention, self).__init__() 16 | self.dropout = dropout 17 | self.in_features = in_features 18 | self.out_features = out_features 19 | self.alpha = alpha 20 | self.concat = concat 21 | 22 | # Glorot Initialization 23 | self.W = nn.Parameter(nn.init.xavier_uniform_(torch.Tensor(in_features, out_features).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor), gain=np.sqrt(2.0)), requires_grad=True) 24 | self.a1 = nn.Parameter(nn.init.xavier_uniform_(torch.Tensor(out_features, 1).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor), gain=np.sqrt(2.0)), requires_grad=True) 25 | self.a2 = nn.Parameter(nn.init.xavier_uniform_(torch.Tensor(out_features, 1).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor), gain=np.sqrt(2.0)), requires_grad=True) 26 | 27 | self.leakyrelu = nn.LeakyReLU(self.alpha) 28 | 29 | def forward(self, input, adj): 30 | h = torch.mm(input, self.W) 31 | N = h.size()[0] 32 | 33 | f_1 = torch.matmul(h, self.a1) 34 | f_2 = torch.matmul(h, self.a2) 35 | #e = self.leakyrelu(f_1 + f_2.transpose(0,1)) 36 | print(f_1.shape) 37 | print(f_2.shape) 38 | print(f_2.transpose(0,1).shape) 39 | print((f_1 +f_2.transpose(0,1)).shape) 40 | e = self.leakyrelu(torch.cat((f_1, f_2))) 41 | 42 | zero_vec = -9e15*torch.ones_like(e) 43 | attention = torch.where(adj > 0, e, zero_vec) 44 | attention = F.softmax(attention, dim=1) # normalized attention weights 45 | attention = F.dropout(attention, self.dropout, training=self.training) 46 | h_prime = torch.matmul(attention, h) 47 | 48 | if self.concat: 49 | return F.relu(h_prime) 50 | else: 51 | return h_prime 52 | 53 | def __repr__(self): 54 | return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.out_features) + ')' 55 | -------------------------------------------------------------------------------- /2_Understanding_Graphs/load_planetoid.py: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Author : Bumsoo Kim, 2018 3 | # Github : https://github.com/meliketoy/graph-tutorial.pytorch 4 | # 5 | # Korea University, Data-Mining Lab 6 | # Basic Tutorial for Non-Euclidean Graph Representation Learning 7 | # 8 | # Description : load_planetoid.py 9 | # Code for uploading planetoid dataset 10 | # *********************************************************** 11 | 12 | import pickle as pkl 13 | import argparse 14 | import os 15 | from pathlib import Path 16 | 17 | def read_data(path, dataset): 18 | print("\n[STEP 1]: Upload {} dataset.".format(dataset)) 19 | 20 | names = ['x', 'y', 'tx', 'ty', 'allx', 'ally', 'graph'] 21 | objects = [] 22 | 23 | for idx, name in enumerate(names): 24 | with open("{}/ind.{}.{}".format(path, dataset, name), 'rb') as f: 25 | objects.append(pkl.load(f, encoding='latin1')) 26 | # 데이터 피클들은 python2 형식으로 저장한 상태여서, encoding='latin1'을 반드시 추가해줘야 합니다. 27 | 28 | x, y, tx, ty, allx, ally, graph = tuple(objects) 29 | """ 30 | ind.[:dataset].x => label이 존재하는 training 노드의 feature vectors (scipy.sparse.csr.csr_matrix) 31 | ind.[:dataset].y => 각 노드의 one-hot 으로 표현된 레이블 (numpy.ndarray) 32 | ind.[:dataset].allx => 모든 training 노드의 feature vectors (scipy.sparse.csr.csr_matrix) 33 | ind.[:dataset].ally => ind.dataset.allx 에 대한 모든 레이블 (numpy.ndarray) 34 | ind.[:dataset].graph => {index: [index of neighbor nodes]} (collections.defaultdict) 35 | 36 | ind.[:dataset].tx => test 노드의 feature vectors (scipy.sparse.csr.csr_matrix) 37 | ind.[:dataset].ty => test 노드의 one-hot 으로 표현된 레이블 (numpy.ndarray) 38 | ind.[:dataset].test.index => indices of test instances in graph, for the inductive setting 39 | """ 40 | 41 | return x, y, tx, ty, allx, ally, graph 42 | 43 | if __name__ == "__main__": 44 | # Argument 45 | parser = argparse.ArgumentParser(description='PyTorch KR Tutorial') 46 | parser.add_argument('--dataset', required=True, type=str, help='dataset') 47 | parser.add_argument('--data_path', 48 | default=os.path.join(Path.home(), "Data", "Planetoid"), type=str, help='data path') 49 | args = parser.parse_args() 50 | 51 | x, y, tx, ty, allx, ally, graph = read_data(path=args.data_path, dataset=args.dataset) 52 | print("Shape of \'x\' : %s" %str(x.todense().shape)) 53 | print("Shape of \'y\' : %s" %str(y.shape)) 54 | print("Shape of \'tx\' : %s" %str(tx.todense().shape)) 55 | print("Shape of \'ty\' : %s" %str(ty.shape)) 56 | print("Shape of \'allx\' : %s" %str(allx.todense().shape)) 57 | print("Shape of \'ally\' : %s" %str(ally.shape)) 58 | 59 | print("Graph Sample (node # 1022) : graph[1022] = %s" %str(graph[1022])) 60 | 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | --------------------------------------------------------------------- 4 | 5 | **Non-Euclidean Graph Representation 데이터**를 다루기 위한 PyTorch 튜토리얼 (PyTorch KR Tutorial Competition 2018 참가작) 6 | 7 | ## Table of Contents 8 | - [1. Going Beyond Euclidean Data : Graphs](./1_Going_Beyond_Euclidean_Data/) 9 | - [2. Understanding Graphs : Planetoid Dataset](./2_Understanding_Graphs/) 10 | - [3. Graph Node Classification : Spectral](./3_Spectral_Graph_Convolution/) 11 | - [4. Graph Node Classification : Spatial](./4_Spatial_Graph_Convolution/) 12 | 13 | ## Requirements 14 | 15 | - Install docker 16 | 설치 가이드 : [설치자료1](https://subicura.com/2017/01/19/docker-guide-for-beginners-2.html), [설치자료2](https://hiseon.me/2018/02/19/install-docker/) 17 | 18 | ```bash 19 | # 오래된 버전의 도커가 존재하는 경우, 오래된 버전의 도커 삭제 20 | $ sudo apt-get remove docker docker-engine docker.io 21 | 22 | # 도커에 필요한 패키지 설치 23 | $ sudo apt-get update && sudo apt-get install \ 24 | apt-transport-https \ 25 | ca-certificates \ 26 | curl \ 27 | software-properties-common 28 | 29 | # 도커의 공식 GPG 키와 저장소를 추가한다. 30 | $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 31 | $ sudo add-apt-repository \ 32 | "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ 33 | xenial \ 34 | stable" 35 | # change `lsb_release -cs` to `xenial` (https://stackoverflow.com/questions/41133455/docker-repository-does-not-have-a-release-file-on-running-apt-get-update-on-ubun) 36 | 37 | # 도커 패키지 검색 확인 38 | $ sudo apt-get update && sudo apt-cache search docker-ce 39 | # > docker-ce - Docker: the open-source application container engine 40 | 41 | # 도커 CE 에디션 설치 42 | $ sudo apt-get update && sudo apt-get install docker-ce 43 | 44 | # 도커 사용자계정 추가 45 | $ sudo usermod -aG docker $USER 46 | ``` 47 | 48 | - nvidia-docker 설치하기(GPU 환경) 49 | 50 | ```bash 51 | # pytorch 1.0 cuda 10 version 을 실행하기 위해서는 nvidia-driver 396 이상을 설치하여야 한다. 52 | sudo add-apt-repository ppa:graphics-drivers 53 | sudo apt-get update 54 | sudo apt-get install nvidia-396 55 | ``` 56 | 57 | ```bash 58 | # nvidia docker 설치 59 | $ curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | \ 60 | sudo apt-key add - 61 | 62 | $ distribution=$(. /etc/os-release;echo $ID$VERSION_ID) 63 | $ curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \ 64 | sudo tee /etc/apt/sources.list.d/nvidia-docker.list 65 | 66 | $ sudo apt-get update 67 | 68 | # nvidia-docker 설치 69 | $ sudo apt-get install -y nvidia-docker2 70 | ``` 71 | 72 | - Building your own image : Dockerfile -> Build 73 | 베이스 이미지를 받은 상태에서, 필요한 라이브러리를 그냥 설치하면 종료 이후 설치 내역은 보존되지 않습니다. 74 | 따라서, Dockerfile을 만들어서 build하여 image를 만들면, 실행 환경을 저장할 수 있습니다. 75 | 76 | ```bash 77 | # docker build [OPTIONS] PATH | URL | - 78 | $ docker build -t {image name} . # 현재 경로에 Dockerfile이 있으며, {image name} 이름의 Dockerfile을 빌드함. 79 | 80 | # 예시 : 이번 튜토리얼에서는 bumsoo 라는 이름으로 이미지를 빌드하여 사용하였습니다. 81 | $ docker build -t bumsoo . 82 | ``` 83 | 84 | - Pull docker image 85 | ```bash 86 | $ docker pull bumsoo-graph-tutorial 87 | 88 | # docker run -t {Docker Image} {시작 명령어} : interactive mode 로 진입 89 | $ docker run -it bumsoo /bin/bash 90 | 91 | # GPU 환경의 interactive mode 92 | $ nvidia-docker run -it bumsoo /bin/bash 93 | ``` 94 | 95 | ## How to Run? 96 | 97 | 실행하는 방법은 두 가지가 있습니다. 98 | 99 | 첫 번째는 docker interactive mode로 진입하여, 일반적인 python file을 실행하는 것입니다. 100 | 101 | 두 번째는, docker 경로를 주어 바로 실행하는 방법입니다. 102 | 103 | 각각의 실행 방법은 다음과 같습니다. 104 | 105 | - docker interactive mode 106 | ```bash 107 | $ nvidia-docker run -it bumsoo /bin/bash 108 | 109 | # bin/bash 실행 후 110 | > cd [:단원] 111 | > python [:파일명] 112 | 113 | # 예제 114 | > cd 2_Understanding_Graphs/ 115 | > python preprocess_planetoid.py --dataset cora --step normalize --mode pitfall 116 | ``` 117 | 118 | - docker run 119 | ```bash 120 | $ nvidia-docker run -it bumsoo python [:단원]/[:파일].py 121 | 122 | # Example 123 | $ nvidia-docker run -it bumsoo python 2_Understanding_Graphs/preprocess_planetoid.py --dataset cora --step normalize --mode pitfall 124 | ``` 125 | 126 | ## Author 127 | Bumsoo Kim, [@meliketoy](https://github.com/meliketoy) 128 | 129 | Korea University, DMIS(Data Mining & Information Systems) Lab 130 | -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/layers.py: -------------------------------------------------------------------------------- 1 | import math 2 | import torch 3 | import numpy as np 4 | import torch.nn as nn 5 | import torch.nn.functional as F 6 | from torch.nn.parameter import Parameter 7 | from torch.nn.modules.module import Module 8 | 9 | class GraphConvolution(Module): 10 | """ 11 | Simple GCN layer, similar to https://arxiv.org/abs/1609.02907 12 | """ 13 | 14 | def __init__(self, in_features, out_features, bias=True, init='xavier'): 15 | super(GraphConvolution, self).__init__() 16 | self.in_features = in_features 17 | self.out_features = out_features 18 | self.weight = Parameter(torch.FloatTensor(in_features, out_features)) 19 | if bias: 20 | self.bias = Parameter(torch.FloatTensor(out_features)) 21 | else: 22 | self.register_parameter('bias', None) 23 | if init == 'uniform': 24 | print("| Uniform Initialization") 25 | self.reset_parameters_uniform() 26 | elif init == 'xavier': 27 | print("| Xavier Initialization") 28 | self.reset_parameters_xavier() 29 | elif init == 'kaiming': 30 | print("| Kaiming Initialization") 31 | self.reset_parameters_kaiming() 32 | else: 33 | raise NotImplementedError 34 | 35 | def reset_parameters_uniform(self): 36 | stdv = 1. / math.sqrt(self.weight.size(1)) 37 | self.weight.data.uniform_(-stdv, stdv) 38 | if self.bias is not None: 39 | self.bias.data.uniform_(-stdv, stdv) 40 | 41 | def reset_parameters_xavier(self): 42 | nn.init.xavier_normal_(self.weight.data, gain=0.02) # Implement Xavier Uniform 43 | if self.bias is not None: 44 | nn.init.constant_(self.bias.data, 0.0) 45 | 46 | def reset_parameters_kaiming(self): 47 | nn.init.kaiming_normal_(self.weight.data, a=0, mode='fan_in') 48 | if self.bias is not None: 49 | nn.init.constant_(self.bias.data, 0.0) 50 | 51 | def forward(self, input, adj): 52 | support = torch.mm(input, self.weight) 53 | output = torch.spmm(adj, support) 54 | if self.bias is not None: 55 | return output + self.bias 56 | else: 57 | return output 58 | 59 | def __repr__(self): 60 | return self.__class__.__name__ + ' (' \ 61 | + str(self.in_features) + ' -> ' \ 62 | + str(self.out_features) + ')' 63 | 64 | 65 | class GraphAttention(nn.Module): 66 | """ 67 | Simple GAT layer, similar to https://arxiv.org/abs/1710.10903 68 | """ 69 | 70 | def __init__(self, in_features, out_features, dropout, alpha, concat=True): 71 | super(GraphAttention, self).__init__() 72 | self.dropout = dropout 73 | self.in_features = in_features 74 | self.out_features = out_features 75 | self.alpha = alpha 76 | self.concat = concat 77 | 78 | self.W = nn.Parameter(nn.init.xavier_normal_(torch.Tensor(in_features, out_features).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor), gain=np.sqrt(2.0)), requires_grad=True) 79 | self.a1 = nn.Parameter(nn.init.xavier_normal_(torch.Tensor(out_features, 1).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor), gain=np.sqrt(2.0)), requires_grad=True) 80 | self.a2 = nn.Parameter(nn.init.xavier_normal_(torch.Tensor(out_features, 1).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor), gain=np.sqrt(2.0)), requires_grad=True) 81 | 82 | self.leakyrelu = nn.LeakyReLU(self.alpha) 83 | 84 | def forward(self, input, adj): 85 | h = torch.mm(input, self.W) 86 | N = h.size()[0] 87 | 88 | f_1 = torch.matmul(h, self.a1) 89 | f_2 = torch.matmul(h, self.a2) 90 | e = self.leakyrelu(f_1 + f_2.transpose(0,1)) 91 | 92 | zero_vec = -9e15*torch.ones_like(e) 93 | attention = torch.where(adj > 0, e, zero_vec) 94 | attention = F.softmax(attention, dim=1) 95 | attention = F.dropout(attention, self.dropout, training=self.training) 96 | h_prime = torch.matmul(attention, h) 97 | 98 | if self.concat: 99 | return F.elu(h_prime) 100 | else: 101 | return h_prime 102 | 103 | def __repr__(self): 104 | return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.out_features) + ')' 105 | -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/train.py: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Author : Bumsoo Kim, 2018 3 | # Github : https://github.com/meliketoy/graph-tutorial.pytorch 4 | # 5 | # Korea University, Data-Mining Lab 6 | # Basic Tutorial for Non-Euclidean Graph Representation Learning 7 | # 8 | # Description : train.py 9 | # The main code for training classification networks. 10 | # *********************************************************** 11 | 12 | import time 13 | import random 14 | import os 15 | import sys 16 | import numpy as np 17 | import torch 18 | import torch.nn.functional as F 19 | import torch.optim as optim 20 | 21 | from torch.autograd import Variable 22 | from utils import * 23 | from models import GCN, GAT 24 | from opts import TrainOptions 25 | 26 | """ 27 | N : number of nodes 28 | D : number of features per node 29 | E : number of classes 30 | 31 | @ input : 32 | - adjacency matrix (N x N) 33 | - feature matrix (N x D) 34 | - label matrix (N x E) 35 | 36 | @ dataset : 37 | - citeseer 38 | - cora 39 | - pubmed 40 | """ 41 | opt = TrainOptions().parse() 42 | 43 | # Data upload 44 | adj, features, labels, idx_train, idx_val, idx_test = load_data(path=opt.dataroot, dataset=opt.dataset) 45 | use_gpu = torch.cuda.is_available() 46 | 47 | random.seed(42) 48 | np.random.seed(42) 49 | torch.manual_seed(42) 50 | if use_gpu: 51 | torch.cuda.manual_seed(42) 52 | 53 | model, optimizer = None, None 54 | best_acc = 0 55 | 56 | # Define the model and optimizer 57 | if (opt.model == 'basic'): 58 | print("| Constructing basic GCN model...") 59 | model = GCN( 60 | nfeat = features.shape[1], 61 | nhid = opt.num_hidden, 62 | nclass = labels.max().item() + 1, 63 | dropout = opt.dropout, 64 | init = opt.init_type 65 | ) 66 | else: 67 | raise NotImplementedError 68 | 69 | if (opt.optimizer == 'sgd'): 70 | optimizer = optim.SGD( 71 | model.parameters(), 72 | lr = opt.lr, 73 | weight_decay = opt.weight_decay, 74 | momentum = 0.9 75 | ) 76 | elif (opt.optimizer == 'adam'): 77 | optimizer = optim.Adam( 78 | model.parameters(), 79 | lr = opt.lr, 80 | weight_decay = opt.weight_decay 81 | ) 82 | else: 83 | raise NotImplementedError 84 | 85 | if use_gpu: 86 | model.cuda() 87 | features, adj, labels, idx_train, idx_val, idx_test = \ 88 | list(map(lambda x: x.cuda(), [features, adj, labels, idx_train, idx_val, idx_test])) 89 | 90 | features, adj, labels = list(map(lambda x : Variable(x), [features, adj, labels])) 91 | 92 | if not os.path.isdir('checkpoint'): 93 | os.mkdir('checkpoint') 94 | 95 | save_point = os.path.join('./checkpoint', opt.dataset) 96 | 97 | if not os.path.isdir(save_point): 98 | os.mkdir(save_point) 99 | 100 | def lr_scheduler(epoch, opt): 101 | return opt.lr * (0.5 ** int(epoch / opt.lr_decay_epoch)) 102 | 103 | # Train 104 | def train(epoch): 105 | global best_acc 106 | 107 | t = time.time() 108 | model.train() 109 | optimizer.lr = lr_scheduler(epoch, opt) 110 | optimizer.zero_grad() 111 | 112 | output = model(features, adj) 113 | loss_train = F.nll_loss(output[idx_train], labels[idx_train]) 114 | acc_train = accuracy(output[idx_train], labels[idx_train]) 115 | 116 | loss_train.backward() 117 | optimizer.step() 118 | 119 | # Validation for each epoch 120 | model.eval() 121 | output = model(features, adj) 122 | loss_val = F.nll_loss(output[idx_val], labels[idx_val]) 123 | acc_val = accuracy(output[idx_val], labels[idx_val]) 124 | 125 | if acc_val > best_acc: 126 | best_acc = acc_val 127 | state = { 128 | 'model': model, 129 | 'acc': best_acc, 130 | 'epoch': epoch, 131 | } 132 | 133 | torch.save(state, os.path.join(save_point, '%s.t7' %(opt.model))) 134 | 135 | sys.stdout.flush() 136 | sys.stdout.write('\r') 137 | sys.stdout.write("=> Training Epoch #{} : lr = {:.4f}".format(epoch, optimizer.lr)) 138 | sys.stdout.write(" | Training acc : {:6.2f}%".format(acc_train.data.cpu().numpy() * 100)) 139 | sys.stdout.write(" | Best acc : {:.2f}%". format(best_acc.data.cpu().numpy() * 100)) 140 | 141 | 142 | # Main code for training 143 | if __name__ == "__main__": 144 | print("\n[STEP 2] : Obtain (adjacency, feature, label) matrix") 145 | print("| Adjacency matrix : {}".format(adj.shape)) 146 | print("| Feature matrix : {}".format(features.shape)) 147 | print("| Label matrix : {}".format(labels.shape)) 148 | 149 | # Training 150 | print("\n[STEP 3] : Training") 151 | for epoch in range(1, opt.epoch+1): 152 | train(epoch) 153 | print("\n=> Training finished!") 154 | -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/train.py: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Author : Bumsoo Kim, 2018 3 | # Github : https://github.com/meliketoy/graph-tutorial.pytorch 4 | # 5 | # Korea University, Data-Mining Lab 6 | # Graph Convolutional Neural Network 7 | # 8 | # Description : train.py 9 | # The main code for training Graph Attention Networks. 10 | # *********************************************************** 11 | 12 | import time 13 | import random 14 | import os 15 | import sys 16 | import numpy as np 17 | import torch 18 | import torch.nn.functional as F 19 | import torch.optim as optim 20 | 21 | from torch.autograd import Variable 22 | from utils import * 23 | from models import GAT 24 | from opts import TrainOptions 25 | 26 | """ 27 | N : number of nodes 28 | D : number of features per node 29 | E : number of classes 30 | 31 | @ input : 32 | - adjacency matrix (N x N) 33 | - feature matrix (N x D) 34 | - label matrix (N x E) 35 | 36 | @ dataset : 37 | - citeseer 38 | - cora 39 | - pubmed 40 | """ 41 | opt = TrainOptions().parse() 42 | 43 | # Data upload 44 | adj, features, labels, idx_train, idx_val, idx_test = load_data(path=opt.dataroot, dataset=opt.dataset) 45 | use_gpu = torch.cuda.is_available() 46 | 47 | model, optimizer = None, None 48 | best_acc = 0 49 | early_stop = 0 50 | 51 | # Define the model and optimizer 52 | if (opt.model == 'attention'): 53 | print("| Constructing Graph Attention Network model...") 54 | model = GAT( 55 | nfeat = features.shape[1], 56 | nhid = opt.num_hidden, 57 | nclass = int(labels.max().item()) + 1, 58 | dropout = opt.dropout, 59 | nheads = opt.nb_heads, 60 | nouts = opt.nb_outs, 61 | alpha = opt.alpha 62 | ) 63 | else: 64 | raise NotImplementedError 65 | 66 | if (opt.optimizer == 'sgd'): 67 | optimizer = optim.SGD( 68 | model.parameters(), 69 | lr = opt.lr, 70 | weight_decay = opt.weight_decay, 71 | momentum = 0.9 72 | ) 73 | elif (opt.optimizer == 'adam'): 74 | optimizer = optim.Adam( 75 | model.parameters(), 76 | lr = opt.lr, 77 | weight_decay = opt.weight_decay 78 | ) 79 | else: 80 | raise NotImplementedError 81 | 82 | if use_gpu: 83 | model.cuda() 84 | features, adj, labels, idx_train, idx_val, idx_test = \ 85 | list(map(lambda x: x.cuda(), [features, adj, labels, idx_train, idx_val, idx_test])) 86 | 87 | features, adj, labels = list(map(lambda x : Variable(x), [features, adj, labels])) 88 | 89 | if not os.path.isdir('checkpoint'): 90 | os.mkdir('checkpoint') 91 | 92 | save_point = os.path.join('./checkpoint', opt.dataset) 93 | 94 | if not os.path.isdir(save_point): 95 | os.mkdir(save_point) 96 | 97 | # Train 98 | def train(epoch): 99 | global best_acc, early_stop 100 | 101 | t = time.time() 102 | model.train() 103 | optimizer.lr = opt.lr 104 | optimizer.zero_grad() 105 | 106 | output = model(features, adj) 107 | 108 | loss_train = F.nll_loss(output[idx_train], labels[idx_train]) 109 | acc_train = accuracy(output[idx_train], labels[idx_train]) 110 | 111 | loss_train.backward() 112 | optimizer.step() 113 | 114 | # Validation for each epoch 115 | model.eval() 116 | output = model(features, adj) 117 | loss_val = F.nll_loss(output[idx_val], labels[idx_val]) 118 | acc_val = accuracy(output[idx_val], labels[idx_val]) 119 | 120 | if acc_val > best_acc: 121 | best_acc = acc_val 122 | state = { 123 | 'model': model, 124 | 'acc': best_acc, 125 | 'epoch': epoch, 126 | } 127 | 128 | torch.save(state, os.path.join(save_point, '%s.t7' %(opt.model))) 129 | early_stop = 0 130 | else: 131 | early_stop += 1 132 | 133 | if (early_stop > 100): 134 | return True 135 | 136 | sys.stdout.flush() 137 | sys.stdout.write('\r') 138 | sys.stdout.write("=> Training Epoch #{} : lr = {:.4f}".format(epoch, optimizer.lr)) 139 | sys.stdout.write(" | Training acc : {:6.2f}%".format(acc_train.data.cpu().numpy() * 100)) 140 | sys.stdout.write(" | Best acc : {:.2f}%". format(best_acc.data.cpu().numpy() * 100)) 141 | return False 142 | 143 | # Main code for training 144 | if __name__ == "__main__": 145 | print("\n[STEP 2] : Obtain (adjacency, feature, label) matrix") 146 | print("| Adjacency matrix : {}".format(adj.shape)) 147 | print("| Feature matrix : {}".format(features.shape)) 148 | print("| Label matrix : {}".format(labels.shape)) 149 | 150 | # Training 151 | print("\n[STEP 3] : Training") 152 | for epoch in range(1, opt.epoch+1): 153 | stopped = train(epoch) 154 | if (stopped): 155 | break 156 | 157 | print("\n=> Training finished!") 158 | -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/README.md: -------------------------------------------------------------------------------- 1 | # Spatial Graph Convolutional Networks 2 | 3 | Non-Euclidean data를 해결하기 위한 두 번째 방법은, Spatial domain 내에서 해결하는 접근법입니다. 4 | 그 중 대표적인 것이 [Graph Attention Network](https://arxiv.org/pdf/1710.10903.pdf)입니다. 5 | 6 | ## Graph Attention Networks 7 | 8 |

9 | 10 | Graph Attention Networks는 spatial 하게 인접한 노드들에 attention weight를 부과하여 이를 통해 각 노드를 표현한다. 11 | 12 | 위 그림에서, 가장 먼저 shared linear transformation, parameterized weight matrix W 를 구해야 합니다. 13 | 14 | 서로 다른 노드와 자신과의 self-attention을 통해 우리는 다음과 같은 식을 얻습니다. 15 | 16 |

17 | 18 | 코드 상에서 이 식은 다음과 같습니다. 19 | 20 | ```bash 21 | h = torch.mm(input, self.W) 22 | 23 | # a1, a2 = (out_features x 1) 24 | f_1 = torch.matmul(h, self.a1) 25 | f_2 = torch.matmul(h, self.a2) 26 | e = f_1 + f_2.transpose(0,1) 27 | ``` 28 | 29 | 여기에 negative input slope 가 0.2 인 LeakyReLU non-linearity를 적용하고, normalize를 시켜줍니다. 30 | 31 | (normalize 방법으론 일반적으로 softmax를 이용합니다.) 32 | 33 |

34 | 35 | 코드 상에서 이 식은 다음과 같습니다. 36 | 37 | ```bash 38 | e = self.leakyrelu(e) 39 | attention = torch.where(adj > 0, e, zero_vec) 40 | attention = F.softmax(attention, dim=1) 41 | ``` 42 | 43 | 학습의 안정성을 위하여, 본 논문에서는 multi-head attention을 진행합니다. 44 | 45 | 마지막 layer 이전 모든 layer에서는 이 작업을 head 의 개수만큼 반복하여 concat 하여 주고, 46 | 47 | 마지막 layer에서는 head의 개수만큼 발생한 output의 average를 취해줍니다. 48 | 49 |

50 | 51 | 코드 상에서 이 식은 다음과 같습니다. 52 | 53 | ```bash 54 | self.attentions = [GraphAttention(nfeat, nhid, dropout=dropout, alpha=alpha, concat=True) for _ in range(nheads)] # concat 55 | self.out_att = [GraphAttention(nhid * nheads, nclass, dropout=dropout, alpha=alpha, concat=False) for _ in range(nouts)] 56 | 57 | # Output layer 가 아닌 layer의 경우 58 | x = torch.cat([att(x, adj) for att in self.attentions], dim=1) # concat 59 | 60 | # Output layer 의 경우 61 | x = torch.mean(torch.stack([att(x, adj) for att in self.out_att], dim=1), dim=1) # avg (for pubmed) 62 | ``` 63 | 64 | Normalize 는 Xavier Initialization 의 다른 이름인 'Glorot Initialization'을 사용했으며, 65 | 66 | Transductive task의 경우 67 | ```bash 68 | # cora, citeseer 69 | nvidia-docker run -it bumsoo python 4_Spatial_Graph_Convolution/train.py --dataset [:dataset] --weight_decay 5e-4 --dropout 0.6 --nb_heads 8 --nb_outs 1 70 | 71 | # pubmed (용량을 매우 많이 차지하므로 느리더라도 CPU 학습을 추천합니다.) 72 | docker run -it bumsoo python 4_Spatial_Graph_Convolution/train.py --dataset pubmed --weight_decay 1e-3 --dropout 0.6 --nb_heads 8 --nb_outs 8 73 | ``` 74 | 75 | ## Train Planetoid Network 76 | 77 | | dataset | classes | nodes | # of edge | 78 | |:-------:|:-------:|:-----:|:-----------:| 79 | | citeseer| 6 | 3,327 | 4,676 | 80 | | cora | 7 | 2,708 | 5,278 | 81 | | pubmed | 3 | 19,717| 44,327 | 82 | 83 | 84 | 이전 튜토리얼과 마찬가지로, [2_Understanding_Graphs](../2_Understanding_Graphs) 에서 다루었던 Planetoid의 데이터셋에 대해 학습을 해보겠습니다. 85 | 86 | 아래의 script를 실행시키면, 원하시는 데이터셋에 GCN 을 학습시키실 수 있습니다. 87 | 88 | [2_Understanding_Graphs](../2_Understanding_Graphs) 에서 설명한 것과 같이 Planetoid 데이터셋을 다운로드 받으신 후, [:dir to dataset] 에 대입하여 실행하시면 됩니다. (Dockerfile 에서 자동적으로 데이터를 받아 필요한 경로로 이동시켜줍니다) 89 | 90 | 기본 default 설정은 2_Understanding_Graphs 의 /home/[:user]/Data/Planetoid 디렉토리로 설정되어 있습니다. 91 | 92 | 이전 2번 튜토리얼 레포에서 보셨던 데이터의 전처리에 관한 사항은, [utils.py](utils.py) 에서 확인해보실 수 있습니다. 93 | 94 | ```bash 95 | # nvidia docker run -it bumsoo-graph-tutorial /bin/bash 실행 이후 96 | > python train.py --dataroot [:dir to dataset] --datset [:cora | citeseer | pubmed] 97 | 98 | # 바로 실행하는 경우 99 | $ nvidia-docker run -it bumsoo python 4_Spatial_Graph_Convolution/train.py --dataset pubmed --lr 0.01 --weight_decay 1e-3 --nb_heads 8 100 | $ nvidia-docker run -it bumsoo python 4_Spatial_Graph_Convolution/train.py --dataset [:else] --lr 5e-3 101 | ``` 102 | 103 | 위 코드를 실행하면, 아래와 같은 결과화면을 얻으실 수 있습니다. 104 | 105 | ![train_citeseer](./imgs/citeseer_attention_train.png) 106 | 107 | ## Test (Inference) Planetoid networks 108 | 109 | Training 과정을 모두 마치신 이후, 다음과 같은 코드를 통해 학습된 weight를 테스트셋에 적용해보실 수 있습니다. 110 | 111 | ```bash 112 | # nvidia docker run -it bumsoo-graph-tutorial /bin/bash 실행 이후 113 | > python test.py --dataroot [:dir to dataset] --dataset [:cora | citeseer | pubmed] 114 | 115 | # 바로 실행하는 경우 116 | $ nvidia-docker run -it bumsoo python 4_Spatial_Graph_Convolution/test.py --dataset [:dataset] 117 | ``` 118 | 119 | 위 코드를 실행하면, 아래와 같은 결과화면을 얻으실 수 있습니다. 120 | 121 | ![test_citeseer](./imgs/citeseer_attention_test.png) 122 | 123 | ## Result 124 | 125 | 800 epoch 후 학습된 최종 성능은 다음과 같습니다. 126 | 127 | GAT (recon) 이 본 repository의 코드로 학습 후, test data 에 적용한 결과입니다. 128 | 129 | | Method | Citeseer | Cora | Pubmed | 130 | |:------------|:---------|:-----|:-------| 131 | | GCN (rand) | 67.9 | 80.1 | 78.9 | 132 | | GCN (paper) | 70.3 | 81.5 | 79.0 | 133 | | GAT (paper) | 72.5 | 83.0 | 79.0 | 134 | | **GAT (recon)** | **72.2** | **82.2** | **78.6** | 135 | -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/molecule_utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import numpy as np 3 | import networkx as nx 4 | import scipy.sparse as sp 5 | import torch.nn.functional as F 6 | import torch 7 | from scipy import sparse 8 | from rdkit import Chem 9 | from scipy.sparse import csgraph 10 | 11 | class ContrastiveLoss(torch.nn.Module): 12 | """ 13 | Contrastive loss function. 14 | Based on: http://yann.lecun.com/exdb/publis/pdf/hadsell-chopra-lecun-06.pdf 15 | """ 16 | 17 | def __init__(self, margin=2.0): 18 | super(ContrastiveLoss, self).__init__() 19 | self.margin = margin 20 | 21 | def forward(self, output1, output2, label): 22 | euclidean_distance = F.pairwise_distance(output1, output2) 23 | loss_contrastive = torch.mean((1-label) * torch.pow(euclidean_distance, 2) + 24 | (label) * torch.pow(torch.clamp(self.margin - euclidean_distance, min=0.0), 2)) 25 | 26 | return loss_contrastive 27 | 28 | def parse_index_file(filename): 29 | index = [] 30 | 31 | for line in open(filename): 32 | index.append(int(line.strip())) 33 | 34 | return index 35 | 36 | def normalize(mx): 37 | """Row-normalize sparse matrix""" 38 | rowsum = np.array(mx.sum(1), dtype=float) 39 | r_inv = np.power(rowsum, -1).flatten() 40 | r_inv[np.isinf(r_inv)] = 0. 41 | r_mat_inv = sp.diags(r_inv) 42 | mx = r_mat_inv.dot(mx) 43 | 44 | return mx 45 | 46 | def normalize_adj(mx): 47 | """Row-normalize sparse matrix""" 48 | rowsum = np.array(mx.sum(1)) 49 | r_inv_sqrt = np.power(rowsum, -0.5).flatten() 50 | r_inv_sqrt[np.isinf(r_inv_sqrt)] = 0. 51 | r_mat_inv_sqrt = sp.diags(r_inv_sqrt) 52 | 53 | return mx.dot(r_mat_inv_sqrt).transpose().dot(r_mat_inv_sqrt).tocoo() 54 | 55 | def one_of_k_encoding(x, allowable_set): 56 | if x not in allowable_set: 57 | raise Exception("input {0} not in allowable set{1}:".format(x, allowable_set)) 58 | return list(map(lambda s: x==s, allowable_set)) 59 | 60 | def one_of_k_encoding_unk(x, allowable_set): 61 | if x not in allowable_set: 62 | x = allowable_set[-1] 63 | 64 | return list(map(lambda s: x==s, allowable_set)) 65 | 66 | def atom_features(atom): 67 | # atom (vertex) will have 62 dimensions 68 | return np.array(one_of_k_encoding_unk(atom.GetSymbol(), 69 | ['C', 'N', 'O', 'S', 'F', 'Si', 'P', 'Cl', 'Br', 'Mg', 'Na', 70 | 'Ca', 'Fe', 'As', 'Al', 'I', 'B', 'V', 'K', 'Tl', 'Yb', 71 | 'Sb', 'Sn', 'Ag', 'Pd', 'Co', 'Se', 'Ti', 'Zn', 'H', 72 | 'Li', 'Ge', 'Cu', 'Au', 'Ni', 'Cd', 'In', 'Mn', 'Zr', 73 | 'Cr', 'Pt', 'Hg', 'Pb', 'Unknown']) + 74 | one_of_k_encoding(atom.GetDegree(), [0, 1, 2, 3, 4, 5]) + 75 | one_of_k_encoding_unk(atom.GetTotalNumHs(), [0, 1, 2, 3, 4]) + 76 | one_of_k_encoding_unk(atom.GetImplicitValence(), [0, 1, 2, 3, 4, 5]) + 77 | [atom.GetIsAromatic()]) 78 | 79 | def bond_features(bond): 80 | bt = bond.GetBondType() 81 | 82 | # bond (edge) will have 6 dimensions 83 | return np.array([bt == Chem.rdchem.BondType.SINGLE, 84 | bt == Chem.rdchem.BondType.DOUBLE, 85 | bt == Chem.rdchem.BondType.TRIPLE, 86 | bt == Chem.rdchem.BondType.AROMATIC, 87 | bond.GetIsConjugated(), 88 | bond.IsInRing()]) 89 | 90 | # Obtain dim(atom features) through a toy chemical compound 'CC' 91 | def num_atom_features(): 92 | m = Chem.MolFromSmiles('CC') 93 | alist = m.GetAtoms() 94 | a = alist[0] 95 | return len(atom_features(a)) 96 | 97 | # Obtain dim(bond features) through a toy chemical compound 'CC' 98 | def num_bond_features(): 99 | simple_mol = Chem.MolFromSmiles('CC') 100 | Chem.SanitizeMol(simple_mol) 101 | return len(bond_features(simple_mol.GetBonds()[0])) 102 | 103 | # Create (feature, adj) from a mol 104 | def create_graph(mol): 105 | num_atom = max([atom.GetIdx() for atom in mol.GetAtoms()]) + 1 106 | adj = np.zeros((num_atom, num_atom, num_bond_features())) 107 | 108 | features = [atom_features(atom) * 1 for atom in mol.GetAtoms()] 109 | feature = np.stack(features, axis=0) 110 | 111 | edge_list = [(bond.GetBeginAtomIdx(), bond.GetEndAtomIdx(), bond_features(bond)) 112 | for bond in mol.GetBonds()] 113 | 114 | for edge in edge_list: 115 | v1, v2, f = edge 116 | f = f * 1 # Convert boolean to int 117 | adj[v1][v2] = f 118 | adj[v2][v1] = f 119 | 120 | # normalize feature 121 | sparse_features = sparse.csr_matrix(features) 122 | normed_features = normalize(sparse_features) 123 | features = np.array(normed_features.todense()) 124 | 125 | # normalize adj 126 | for layer in range(num_bond_features()): 127 | sparse_adj = sparse.csr_matrix(adj[:,:,layer]) 128 | normed_adj = normalize_adj(sparse_adj + sparse.eye(sparse_adj.shape[0])) 129 | adj[:,:,layer] = np.array(normed_adj.todense()) 130 | 131 | return adj, feature 132 | -------------------------------------------------------------------------------- /2_Understanding_Graphs/README.md: -------------------------------------------------------------------------------- 1 | ## Graph Structure 2 | 참고자료 : [slides](https://www.cl.cam.ac.uk/~pv273/slides/UCLGraph.pdf) 3 | 4 | ## Planetoid Dataset 5 | Planetoid 데이터셋은 graph 형식의 데이터를 다루는 테스크 중 일반적인 성능의 지표로 많이 사용되는 데이터셋입니다. 6 | Planetoid를 통해 evaluation을 한 논문은 다음과 같은 예시가 대표적입니다. 7 | 8 | - [Semi-Supervised Classification with Graph Convolutional Networks](https://arxiv.org/pdf/1609.02907.pdf) 9 | - [Graph Attention Networks](https://mila.quebec/wp-content/uploads/2018/07/d1ac95b60310f43bb5a0b8024522fbe08fb2a482.pdf) 10 | - [Topology Adaptive Graph Convolutional Networks](https://arxiv.org/pdf/1710.10370.pdf) 11 | - [Deeper Insights into Graph Convolutional Networks for Semi-Supervised Learning](https://arxiv.org/pdf/1801.07606.pdf) 12 | 13 | 튜토리얼에서 사용된 Planetoid는 아래 논문의 데이터셋을 참조하였습니다: 14 | [Revisiting Semi-Supervised Learning with Graph Embeddings](https://arxiv.org/abs/1603.08861). 15 | 16 | Planetoid 데이터셋은 3개의 데이터로 구성이 되어있습니다. ('pubmed', 'cora', 'citeseer') 17 | 18 | Each node in the dataset represents a document, and the edge represents the 'reference' relationship between the documents. 19 | 20 | Planetoid 데이터셋에서, 각 노드는 'document'를 의미하며, 각 edge는 'document'간 reference 관계를 나타냅니다. 21 | 예를 들어, 아래 그림과 같이 paper A 에서 paper B 를 reference 했다면, edge(A, B) = 1 입니다. 22 | 23 | [:예시 그림] 24 | 25 | ## Planetoid Dataset Download 26 | [Github planetoid repo](https://github.com/kimiyoung/planetoid) 를 다운로드 받은 후, 내부에 있는 data 폴더를 ~/Data/Planetoid 로 이동시켜줍니다. (이후 튜토리얼에서 환경설정은 모두 동일합니다.) 27 | 28 | ```bash 29 | $ git clone https://github.com/kimiyoung/planetoid.git 30 | $ mkdir ~/Data 31 | $ mv ./data ~/Data/Planetoid/ 32 | ``` 33 | 34 | ## [STEP 1] : Planetoid data 읽어보기 35 | 36 | 첫번째 단계로, Planetoid 의 세 개의 데이터셋(cora, pubmed, citeseer)을 읽어보겠습니다. 37 | docker 환경을 실행한 상태에서, 아래 코드를 돌리면 Planetoid 데이터를 읽을 수 있습니다. 38 | 39 | ```bash 40 | $ python load_planetoid.py --dataset cora 41 | $ python load_planetoid.py --dataset citeseer 42 | $ python load_planetoid.py --dataset pubmed 43 | ``` 44 | 45 | 데이터는 다음과 같은 두 가지 방식으로 학습할 수 있습니다. 46 | 47 | ### 전이학습 (Transductive learning) 48 | - x : 각 training 데이터 중, 레이블이 존재하는 instance에 대한 feature vector 49 | - y : 각 training 데이터에 대한 label 이 one-hot 방식으로 표현되어 있습니다. 50 | - graph : dict{index: [index of neighber nodes]}, 각 노드의 인접 노드는 list 형식으로 표현되어 있습니다. 51 | 52 | ### 추론 학습 (Inductive learning) 53 | - x : 각 training 데이터의 feature vector 54 | - y : 각 training 데이터에 대한 label 이 one-hot 방식으로 표현되어 있습니다. 55 | - allx : training 데이터 중, 레이블의 유무와 관련 없이 모든 instance에 대한 feature vector. 56 | - graph : dict{index: [index of neighber nodes]}, 각 노드의 인접 노드는 list 형식으로 표현되어 있습니다. 57 | 58 | ## [STEP 2] : Pre-processing 59 | 60 | Pre-processing 은 총 세 단계로 이루어진다. 61 | 62 | - train / test split 63 | - isolated node 검사 64 | - normalize 65 | 66 | ### train / val / test split 67 | 68 | Pre-processing 의 첫 번째 단게로, train / val / test split 을 해야합니다. 69 | 70 | validation set 은 따로 지정되있지 않으므로, 500개로 설정하여 실험을 진행합니다. 71 | 72 | ```bash 73 | $ python preprocess_planetoid.py --dataset [:dataset] --step split 74 | 75 | # Citeseer example 76 | $ python preprocess_planetoid.py --dataset citeseer --step split 77 | > [STEP 1]: Upload citeseer dataset. 78 | > | # of train set : 120 79 | > | # of validation set : 500 80 | > | # of test set : 1000 81 | ``` 82 | 83 | Pre-processing 의 두 번째 단계로, graph에 존재하는 isolated node를 검사해야 합니다. 84 | 85 | ```bash 86 | $ python preprocess_planetoid.py --dataset pubmed --step isolate 87 | > Isolated Nodes : [] 88 | 89 | $ python preprocess_planetoid.py --dataset cora --step isolate 90 | > Isolated Nodes : [] 91 | 92 | $ python preprocess_planetoid.py --dataset citeseer --step isolate 93 | > Isolated Nodes : [2407, 2489, 2553, 2682, 2781, 2953, 3042, 3063, 3212, 3214, 3250, 3292, 3305, 3306, 3309] 94 | ``` 95 | 96 | 세 개의 dataset 중, citeseer 데이터셋의 test 데이터에 다음과 같은 isolated node를 발견할 수 있습니다. 97 | 98 | ### Normalize 99 | 100 | Normalize 는 feature와 adjacency matrix 에 대해서 모두 Row normalize를 진행합니다. 101 | 102 | Feature vector에는, degree를 normalize 하기 위하여 row-wise normalization을 진행합니다. 103 | 104 | Adjacency Matrix에서는, 인접 노드의 개수에 따른 degree의 차이를 normalize 해주기 위하여 Symmetric Laplacian을 이용합니다. 이를 통해서, 한 노드와 인접 노드 간의 spectrum을 표현할 수 있습니다. 105 | 106 | Adjacency Matrix의 normalize는원 저자의 [paper](https://arxiv.org/pdf/1609.02907.pdf)에서 확인이 가능합니다. 107 | 108 | ![H](./figures/norm_adj.png) 109 | 110 | normalize 를 실행하고 결과를 확인하기 위해서는 아래의 코드를 실행하시면 됩니다. 111 | 112 | ```bash 113 | $ python preprocess_planetoid.py --dataset [:dataset] --step normalize 114 | ``` 115 | 116 | ## Pitfall 117 | 118 | 기존 논문 저자의 [repository](https://github.com/kimiyoung/planetoid) 에 공개된 데이터에는, 몇 가지 문제가 있습니다. 119 | 120 | - 중복된 edge 의 존재 (한 노드가 다른 노드를 두 번 이상 reference) 121 | - self citation의 존재 (self citation을 고려할지 그렇지 않을지에 대한 정의를 명확히 해야할 것 같습니다. 본 논문에서 제시된 edge 개수를 맞추려면 self citation을 고려해야하므로, 본 튜토리얼에서도 똑같이 적용하였습니다.) 122 | 123 | ```bash 124 | $ python preprocess_planetoid.py --dataset [:dataset] --step normalize --mode pitfall 125 | ``` 126 | 127 | | dataset | classes | nodes | # of redundant | # of self citation | reported edge | actual edge | 128 | |:-------:|:-------:|:-----:|:--------------:|:------------------:|:-------------:|:-----------:| 129 | | citeseer| 6 | 3,327 | 232 | 124 | 4,732 | 4,676 | 130 | | cora | 7 | 2,708 | 302 | 0 | 5,429 | 5,278 | 131 | | pubmed | 3 | 19,717| 25 | 3 | 44,338 | 44,327 | 132 | 133 | 기존의 구현은 [링크](https://github.com/kimiyoung/planetoid)의 repository에서 볼 수 있습니다. 134 | -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/utils.py: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Author : Bumsoo Kim, 2018 3 | # Github : https://github.com/meliketoy/graph-tutorial.pytorch 4 | # 5 | # Korea University, Data-Mining Lab 6 | # Basic Tutorial for Non-Euclidean Graph Representation Learning 7 | # 8 | # Description : utils.py 9 | # Code for uploading planetoid dataset 10 | # *********************************************************** 11 | 12 | import sys 13 | import numpy as np 14 | import pickle as pkl 15 | import networkx as nx 16 | import scipy.sparse as sp 17 | import torch 18 | from scipy.sparse import csgraph 19 | 20 | def parse_index_file(filename): 21 | index = [] 22 | 23 | for line in open(filename): 24 | index.append(int(line.strip())) 25 | 26 | return index 27 | 28 | def missing_elements(L): 29 | start, end = L[0], L[-1] 30 | return sorted(set(range(start, end+1)).difference(L)) 31 | 32 | def normalize_sparse_features(mx): 33 | """Row-normalize sparse matrix""" 34 | rowsum = np.array(mx.sum(1)) 35 | r_inv = np.power(rowsum, -1).flatten() 36 | r_inv[np.isinf(r_inv)] = 0. 37 | r_mat_inv = sp.diags(r_inv) 38 | mx = r_mat_inv.dot(mx) 39 | 40 | return mx 41 | 42 | def normalize_sparse_adj(mx): 43 | """Laplacian Normalization""" 44 | rowsum = np.array(mx.sum(1)) 45 | r_inv_sqrt = np.power(rowsum, -0.5).flatten() 46 | r_inv_sqrt[np.isinf(r_inv_sqrt)] = 0. 47 | r_mat_inv_sqrt = sp.diags(r_inv_sqrt) 48 | 49 | return mx.dot(r_mat_inv_sqrt).transpose().dot(r_mat_inv_sqrt).tocoo() 50 | 51 | def accuracy(output, labels): 52 | preds = output.max(1)[1].type_as(labels) 53 | correct = preds.eq(labels).double() 54 | correct = correct.sum() 55 | return correct / len(labels) 56 | 57 | def load_data(path="/home/bumsoo/Data/Planetoid", dataset="cora"): 58 | """ 59 | ind.[:dataset].x => the feature vectors of the training instances (scipy.sparse.csr.csr_matrix) 60 | ind.[:dataset].y => the one-hot labels of the labeled training instances (numpy.ndarray) 61 | ind.[:dataset].allx => the feature vectors of both labeled and unlabeled training instances (csr_matrix) 62 | ind.[:dataset].ally => the labels for instances in ind.dataset_str.allx (numpy.ndarray) 63 | ind.[:dataset].graph => the dict in the format {index: [index of neighbor nodes]} (collections.defaultdict) 64 | 65 | ind.[:dataset].tx => the feature vectors of the test instances (scipy.sparse.csr.csr_matrix) 66 | ind.[:dataset].ty => the one-hot labels of the test instances (numpy.ndarray) 67 | 68 | ind.[:dataset].test.index => indices of test instances in graph, for the inductive setting 69 | """ 70 | print("\n[STEP 1]: Upload {} dataset.".format(dataset)) 71 | 72 | names = ['x', 'y', 'tx', 'ty', 'allx', 'ally', 'graph'] 73 | objects = [] 74 | 75 | for i in range(len(names)): 76 | with open("{}/ind.{}.{}".format(path, dataset, names[i]), 'rb') as f: 77 | if (sys.version_info > (3,0)): 78 | objects.append(pkl.load(f, encoding='latin1')) # python3 compatibility 79 | else: 80 | objects.append(pkl.load(f)) # python2 81 | 82 | x, y, tx, ty, allx, ally, graph = tuple(objects) 83 | 84 | test_idx = parse_index_file("{}/ind.{}.test.index".format(path, dataset)) 85 | test_idx_range = np.sort(test_idx) 86 | 87 | if dataset == 'citeseer': 88 | #Citeseer dataset contains some isolated nodes in the graph 89 | test_idx_range_full = range(min(test_idx), max(test_idx)+1) 90 | tx_extended = sp.lil_matrix((len(test_idx_range_full), x.shape[1])) 91 | tx_extended[test_idx_range-min(test_idx_range), :] = tx 92 | tx = tx_extended 93 | 94 | ty_extended = np.zeros((len(test_idx_range_full), y.shape[1])) 95 | ty_extended[test_idx_range-min(test_idx_range), :] = ty 96 | ty = ty_extended 97 | 98 | # Feature & Adjacency Matrix 99 | features = sp.vstack((allx, tx)).tolil() 100 | features[test_idx, :] = features[test_idx_range, :] 101 | adj = nx.adjacency_matrix(nx.from_dict_of_lists(graph)) 102 | print("| # of nodes : {}".format(adj.shape[0])) 103 | print("| # of edges : {}".format(int(adj.sum().sum()/2 + adj.diagonal().sum()/2))) 104 | 105 | # Normalization 106 | features = normalize_sparse_features(features) 107 | adj = normalize_sparse_adj(adj + sp.eye(adj.shape[0])) # Input is A_hat 108 | print("| # of features : {}".format(features.shape[1])) 109 | print("| # of clases : {}".format(ally.shape[1])) 110 | 111 | features = torch.FloatTensor(np.array(features.todense())) 112 | sparse_mx = adj.tocoo().astype(np.float32) 113 | adj = torch.FloatTensor(np.array(adj.todense())) 114 | 115 | labels = np.vstack((ally, ty)) 116 | labels[test_idx, :] = labels[test_idx_range, :] 117 | 118 | if dataset == 'citeseer': 119 | save_label = np.where(labels)[1] 120 | labels = torch.LongTensor(np.where(labels)[1]) 121 | 122 | idx_train = range(len(y)) 123 | idx_val = range(len(y), len(y)+500) 124 | idx_test = test_idx_range.tolist() 125 | 126 | print("| # of train set : {}".format(len(idx_train))) 127 | print("| # of val set : {}".format(len(idx_val))) 128 | print("| # of test set : {}".format(len(idx_test))) 129 | 130 | idx_train, idx_val, idx_test = list(map(lambda x: torch.LongTensor(x), [idx_train, idx_val, idx_test])) 131 | 132 | if dataset == 'citeseer': 133 | L = np.sort(idx_test) 134 | missing = missing_elements(L) 135 | 136 | for element in missing: 137 | save_label = np.insert(save_label, element, 0) 138 | 139 | labels = torch.LongTensor(save_label) 140 | 141 | return adj, features, labels, idx_train, idx_val, idx_test 142 | -------------------------------------------------------------------------------- /4_Spatial_Graph_Convolution/utils.py: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Author : Bumsoo Kim, 2018 3 | # Github : https://github.com/meliketoy/graph-tutorial.pytorch 4 | # 5 | # Korea University, Data-Mining Lab 6 | # Basic Tutorial for Non-Euclidean Graph Representation Learning 7 | # 8 | # Description : utils.py 9 | # Code for uploading planetoid dataset 10 | # *********************************************************** 11 | 12 | import sys 13 | import numpy as np 14 | import pickle as pkl 15 | import networkx as nx 16 | import scipy.sparse as sp 17 | import torch 18 | from scipy.sparse import csgraph 19 | 20 | def parse_index_file(filename): 21 | index = [] 22 | 23 | for line in open(filename): 24 | index.append(int(line.strip())) 25 | 26 | return index 27 | 28 | def missing_elements(L): 29 | start, end = L[0], L[-1] 30 | return sorted(set(range(start, end+1)).difference(L)) 31 | 32 | def normalize_sparse_features(mx): 33 | """Row-normalize sparse matrix""" 34 | rowsum = np.array(mx.sum(1)) 35 | rowsum[rowsum == 0] = -9e15 36 | r_inv = np.power(rowsum, -1).flatten() 37 | r_inv[np.isinf(r_inv)] = 0. 38 | r_mat_inv = sp.diags(r_inv) 39 | mx = r_mat_inv.dot(mx) 40 | 41 | return mx 42 | 43 | def normalize_sparse_adj(mx): 44 | """Laplacian Normalization""" 45 | rowsum = np.array(mx.sum(1)) 46 | r_inv_sqrt = np.power(rowsum, -0.5).flatten() 47 | r_inv_sqrt[np.isinf(r_inv_sqrt)] = 0. 48 | r_mat_inv_sqrt = sp.diags(r_inv_sqrt) 49 | 50 | return mx.dot(r_mat_inv_sqrt).transpose().dot(r_mat_inv_sqrt).tocoo() 51 | 52 | def accuracy(output, labels): 53 | preds = output.max(1)[1].type_as(labels) 54 | correct = preds.eq(labels).double() 55 | correct = correct.sum() 56 | return correct / len(labels) 57 | 58 | def load_data(path="/home/bumsoo/Data/Planetoid", dataset="cora"): 59 | """ 60 | ind.[:dataset].x => the feature vectors of the training instances (scipy.sparse.csr.csr_matrix) 61 | ind.[:dataset].y => the one-hot labels of the labeled training instances (numpy.ndarray) 62 | ind.[:dataset].allx => the feature vectors of both labeled and unlabeled training instances (csr_matrix) 63 | ind.[:dataset].ally => the labels for instances in ind.dataset_str.allx (numpy.ndarray) 64 | ind.[:dataset].graph => the dict in the format {index: [index of neighbor nodes]} (collections.defaultdict) 65 | 66 | ind.[:dataset].tx => the feature vectors of the test instances (scipy.sparse.csr.csr_matrix) 67 | ind.[:dataset].ty => the one-hot labels of the test instances (numpy.ndarray) 68 | 69 | ind.[:dataset].test.index => indices of test instances in graph, for the inductive setting 70 | """ 71 | print("\n[STEP 1]: Upload {} dataset.".format(dataset)) 72 | 73 | names = ['x', 'y', 'tx', 'ty', 'allx', 'ally', 'graph'] 74 | objects = [] 75 | 76 | for i in range(len(names)): 77 | with open("{}/ind.{}.{}".format(path, dataset, names[i]), 'rb') as f: 78 | if (sys.version_info > (3,0)): 79 | objects.append(pkl.load(f, encoding='latin1')) # python3 compatibility 80 | else: 81 | objects.append(pkl.load(f)) # python2 82 | 83 | x, y, tx, ty, allx, ally, graph = tuple(objects) 84 | 85 | test_idx = parse_index_file("{}/ind.{}.test.index".format(path, dataset)) 86 | test_idx_range = np.sort(test_idx) 87 | 88 | if dataset == 'citeseer': 89 | #Citeseer dataset contains some isolated nodes in the graph 90 | test_idx_range_full = range(min(test_idx), max(test_idx)+1) 91 | tx_extended = sp.lil_matrix((len(test_idx_range_full), x.shape[1])) 92 | tx_extended[test_idx_range-min(test_idx_range), :] = tx 93 | tx = tx_extended 94 | 95 | ty_extended = np.zeros((len(test_idx_range_full), y.shape[1])) 96 | ty_extended[test_idx_range-min(test_idx_range), :] = ty 97 | ty = ty_extended 98 | 99 | # Feature & Adjacency Matrix 100 | features = sp.vstack((allx, tx)).tolil() 101 | features[test_idx, :] = features[test_idx_range, :] 102 | adj = nx.adjacency_matrix(nx.from_dict_of_lists(graph)) 103 | print("| # of nodes : {}".format(adj.shape[0])) 104 | print("| # of edges : {}".format(int(adj.sum().sum()/2 + adj.diagonal().sum()/2))) 105 | 106 | # Normalization 107 | features = normalize_sparse_features(features) 108 | adj = normalize_sparse_adj(adj + sp.eye(adj.shape[0])) # Input is A_hat 109 | print("| # of features : {}".format(features.shape[1])) 110 | print("| # of clases : {}".format(ally.shape[1])) 111 | 112 | features = torch.FloatTensor(np.array(features.todense())) 113 | sparse_mx = adj.tocoo().astype(np.float32) 114 | adj = torch.FloatTensor(np.array(adj.todense())) 115 | 116 | labels = np.vstack((ally, ty)) 117 | labels[test_idx, :] = labels[test_idx_range, :] 118 | 119 | if dataset == 'citeseer': 120 | save_label = np.where(labels)[1] 121 | labels = torch.LongTensor(np.where(labels)[1]) 122 | 123 | idx_train = range(len(y)) 124 | idx_val = range(len(y), len(y)+500) 125 | idx_test = test_idx_range.tolist() 126 | 127 | print("| # of train set : {}".format(len(idx_train))) 128 | print("| # of val set : {}".format(len(idx_val))) 129 | print("| # of test set : {}".format(len(idx_test))) 130 | 131 | idx_train, idx_val, idx_test = list(map(lambda x: torch.LongTensor(x), [idx_train, idx_val, idx_test])) 132 | 133 | if dataset == 'citeseer': 134 | L = np.sort(idx_test) 135 | missing = missing_elements(L) 136 | 137 | for element in missing: 138 | save_label = np.insert(save_label, element, 0) 139 | 140 | labels = torch.LongTensor(save_label) 141 | 142 | return adj, features, labels, idx_train, idx_val, idx_test 143 | -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/forward_mol.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | from rdkit import Chem 4 | import torch 5 | import torch.nn as nn 6 | import torch.nn.functional as F 7 | from torch.nn.parameter import Parameter 8 | from torch.nn.modules.module import Module 9 | from molecule_utils import atom_features, bond_features, num_atom_features, num_bond_features 10 | 11 | # Graph Convolution Layer 12 | class GraphConvolution(Module): 13 | def __init__(self, in_features, out_features, edge_features, bias=True, init='xavier'): 14 | super(GraphConvolution, self).__init__() 15 | self.in_features = in_features 16 | self.out_features = out_features 17 | self.weight = Parameter(nn.init.xavier_normal_(torch.Tensor(in_features, out_features).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor), gain=np.sqrt(2.0)), requires_grad=True) 18 | 19 | if bias: 20 | self.bias = Parameter(torch.Tensor(out_features).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor)) 21 | else: 22 | self.register_parameter('bias', None) 23 | 24 | if init=='uniform': 25 | #print("| Uniform Initialization") 26 | self.reset_parameters_uniform() 27 | elif init=='xavier': 28 | #print("| Xavier Initialization") 29 | self.reset_parameters_xavier() 30 | elif init=='kaiminig': 31 | #print("| Kaiming Initialization") 32 | self.reset_parameters_kaiming() 33 | else: 34 | raise NotImplementedError 35 | 36 | # Initializations 37 | def reset_parameters_uniform(self): 38 | stdv = 1. / math.sqrt(self.weight.size(1)) 39 | self.weight.data.uniform_(-stdv, stdv) 40 | if self.bias is not None: 41 | self.bias.data.uniform_(-stdv, stdv) 42 | 43 | def reset_parameters_xavier(self): 44 | nn.init.xavier_normal_(self.weight.data, gain=0.02) # Implement Xavier Uniform 45 | if self.bias is not None: 46 | nn.init.constant_(self.bias.data, 0.0) 47 | 48 | def reset_parameters_kaiming(self): 49 | nn.init.kaiming_normal_(self.weight.data, a=0, mode='fan_in') 50 | if self.bias is not None: 51 | nn.init.constant_(self.bias.data, 0.0) 52 | 53 | # Forward 54 | def forward(self, input, adj): 55 | support = torch.mm(input, self.weight) 56 | output = torch.mm(adj, support) 57 | if self.bias is not None: 58 | return output + self.bias 59 | else: 60 | return output 61 | 62 | # Graph Convolution Network Model 63 | class GCN(nn.Module): 64 | def __init__(self, nfeat, nhid, nclass, nedges, dropout, init): 65 | super(GCN, self).__init__() 66 | 67 | self.weight = Parameter(nn.init.xavier_normal_(torch.Tensor(nfeat, nhid).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor), gain=np.sqrt(2.0)), requires_grad=True) 68 | 69 | self.gc1 = [GraphConvolution(nhid, nhid, nedges, init=init) for _ in range(nedges)] 70 | for idx, gc1 in enumerate(self.gc1): 71 | self.add_module('GCN_1_%d' %idx, gc1) 72 | self.gc2 = [GraphConvolution(nhid, nclass, nedges, init=init) for _ in range(nedges)] 73 | for idx, gc2 in enumerate(self.gc2): 74 | self.add_module('GCN_2_%d' %idx, gc2) 75 | 76 | self.dropout = dropout 77 | self.adj_dropout = dropout 78 | 79 | def AGG(self, x1, x2, method='add'): 80 | res = None 81 | 82 | if method == 'add': 83 | res = x1 + x2 84 | elif method == 'max': 85 | res = max(x1, x2) 86 | elif method == 'min': 87 | res = min(x1, x2) 88 | else: 89 | raise NotImplementedError 90 | 91 | return res 92 | 93 | def forward(self, x, adj): 94 | # iterate through the edges 95 | prev = None 96 | in_x = torch.matmul(x,self.weight) 97 | 98 | for i in range(adj.shape[2]): 99 | x = F.leaky_relu(self.gc1[i](F.dropout(in_x,self.dropout,self.training), F.dropout(adj[:,:,i], self.adj_dropout, self.training)), negative_slope = 0.2, inplace = False) 100 | x = F.leaky_relu(self.gc2[i](F.dropout(x,self.dropout,self.training), F.dropout(adj[:,:,i], self.adj_dropout, self.training)), 0.2) 101 | 102 | if i == 0: 103 | prev = x 104 | else: 105 | prev = self.AGG(prev, x) 106 | 107 | return prev 108 | 109 | if __name__ == "__main__": 110 | print("Forward path for molecule GCN processing") 111 | 112 | # name = 1-benzylimidazole 113 | # pid = BRD-K32795028 114 | smiles = 'c1ccc(Cn2ccnc2)cc1' 115 | mol = Chem.MolFromSmiles(smiles) 116 | 117 | num_atom = max([atom.GetIdx() for atom in mol.GetAtoms()]) + 1 118 | adj = np.zeros((num_atom, num_atom, num_bond_features())) 119 | 120 | features = [atom_features(atom) * 1 for atom in mol.GetAtoms()] 121 | feature = np.stack(features, axis=0) 122 | 123 | edge_list = [(bond.GetBeginAtomIdx(), bond.GetEndAtomIdx(), bond_features(bond)) for bond in mol.GetBonds()] 124 | 125 | for edge in edge_list: 126 | v1, v2, f = edge 127 | f = f * 1 128 | adj[v1][v2] = f 129 | adj[v2][v1] = f 130 | 131 | adj, feature = list(map(lambda x : torch.FloatTensor(x), [adj, feature])) 132 | if torch.cuda.is_available(): 133 | adj, feature = list(map(lambda x : x.cuda(), [adj, feature])) 134 | 135 | print("Input Feature (# nodes x 62): " + str(feature.shape)) # node x 62 136 | print("Input adjacency (# nodes x # nodes x 6): " + str(adj.shape)) # node x node x 6 137 | 138 | model = GCN( 139 | nfeat = num_atom_features(), 140 | nhid = 100, 141 | nclass = 500, 142 | nedges = num_bond_features(), 143 | dropout = 0.5, 144 | init = 'xavier' 145 | ) 146 | 147 | output = model(feature, adj) 148 | print("Output (# of nodes x 500): " + str(output.shape)) 149 | 150 | max_lst, idx = ((torch.max(output, dim=0))) 151 | print("Max pool to a 500 dimensional vector : "+ str(max_lst.shape)) 152 | -------------------------------------------------------------------------------- /2_Understanding_Graphs/preprocess_planetoid.py: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Author : Bumsoo Kim, 2018 3 | # Github : https://github.com/meliketoy/graph-tutorial.pytorch 4 | # 5 | # Korea University, Data-Mining Lab 6 | # Basic Tutorial for Non-Euclidean Graph Representation Learning 7 | # 8 | # Description : preprocess.py 9 | # Code for preprocessing planetoid dataset 10 | # *********************************************************** 11 | 12 | import torch 13 | import pickle as pkl 14 | import argparse 15 | import os 16 | import numpy as np 17 | import scipy.sparse as sp 18 | import networkx as nx 19 | from pathlib import Path 20 | from load_planetoid import read_data 21 | 22 | def parse_index_file(filename): 23 | index = [] 24 | 25 | for line in open(filename): 26 | index.append(int(line.strip())) 27 | 28 | return index 29 | 30 | def missing_elements(L): 31 | start, end = L[0], L[-1] 32 | return sorted(set(range(start, end+1)).difference(L)) 33 | 34 | def preprocess_citeseer(tx, ty, test_idx): 35 | #Citeseer dataset contains some isolated nodes in the graph 36 | bef_tx = (tx.todense()) 37 | test_idx_range = np.asarray(test_idx, dtype=np.int64) 38 | test_idx_range_full = range(min(test_idx), max(test_idx)+1) 39 | tx_extended = sp.lil_matrix((len(test_idx_range_full), tx.shape[1])) 40 | tx_extended[test_idx_range-min(test_idx_range), :] = tx 41 | tx = tx_extended 42 | 43 | if (args.step == 'isolate'): 44 | print("Citeseer 전처리 이전 test shape : %s" %str(bef_tx.shape)) 45 | print("Citeseer 전처리 이후 test shape : %s" %str(tx.todense().shape)) 46 | 47 | ty_extended = np.zeros((len(test_idx_range_full), ty.shape[1])) 48 | ty_extended[test_idx_range-min(test_idx_range), :] = ty 49 | ty = ty_extended 50 | 51 | return tx, ty 52 | 53 | def normalize_sparse_feature(mx): 54 | """Row-normalize sparse matrix""" 55 | rowsum = np.array(mx.sum(1)) 56 | r_inv = np.power(rowsum, -1).flatten() 57 | r_inv[np.isinf(r_inv)] = 0. 58 | r_mat_inv = sp.diags(r_inv) 59 | mx = r_mat_inv.dot(mx) 60 | 61 | return mx 62 | 63 | def normalize_sparse_adj(mx): 64 | """Row-normalize sparse matrix""" 65 | rowsum = np.array(mx.sum(1)) # D_hat (Diagonal Matrix for Degrees) 66 | r_inv_sqrt = np.power(rowsum, -0.5).flatten() # D_hat^(-1/2) 67 | r_inv_sqrt[np.isinf(r_inv_sqrt)] = 0. 68 | r_mat_inv_sqrt = sp.diags(r_inv_sqrt) # list of diagonal of D_hat^(-1/2) 69 | 70 | return mx.dot(r_mat_inv_sqrt).transpose().dot(r_mat_inv_sqrt).tocoo() # D_hat^(-1/2) . A_hat . D_hat^(-1/2) 71 | 72 | def check_symmetric(a, tol=1e-8): 73 | return not False in (np.abs(a-a.T) < tol) 74 | 75 | def pitfall(path, dataset): 76 | x, y, tx, ty, allx, ally, graph = read_data(path=args.data_path, dataset=args.dataset) 77 | 78 | print("Redundant edges in the Graph!") 79 | # 데이터 그래프 내의 redundant한 edge가 존재합니다. 따라서 원 논문과 node 개수는 동일하나, 80 | # 엣지 개수는 다른 adjacency matrix가 출력됩니다. 81 | edges = 0 82 | idx = 0 83 | for key in graph: 84 | orig_lst = (graph[key]) 85 | set_lst = set(graph[key]) 86 | edges += len(orig_lst) 87 | 88 | if len(orig_lst) != len(set_lst): 89 | print(orig_lst) 90 | idx += (len(orig_lst) - len(set_lst)) 91 | 92 | print("Number of Redundant Edges : %d" %idx) 93 | print("Reported Edges : %d" %(edges/2)) 94 | 95 | # adj 96 | adj = nx.adjacency_matrix(nx.from_dict_of_lists(graph)) 97 | 98 | # edge from adj 99 | print("There also exists {} self inferences!!".format(adj.diagonal().sum())) 100 | # Self inference도 존재하므로 (원래 adjacency matrix의 diagonal의 합은 0이어야 합니다) 101 | act_edges = adj.sum().sum() + adj.diagonal().sum() # diagonal 은 한 번만 계산되므로 /2 이전 한 번 더 더해줍니다. 102 | print("Actual Edges in the Adjacency Matrix : %d" %(act_edges/2)) 103 | 104 | def preprocess_data(path, dataset): 105 | x, y, tx, ty, allx, ally, graph = read_data(path=args.data_path, dataset=args.dataset) 106 | 107 | test_idx = parse_index_file("{}/ind.{}.test.index".format(path, dataset)) 108 | test_idx_range = np.sort(test_idx) # test idx 를 오름차순으로 정리한다. 109 | 110 | idx_train = range(len(y)) 111 | idx_val = range(len(y), len(y)+500) 112 | idx_test = test_idx_range.tolist() 113 | 114 | # train / val / test split 115 | if (args.step == 'split'): 116 | print("| # of train set : {}".format(len(idx_train))) 117 | print("| # of validation set : {}".format(len(idx_val))) 118 | print("| # of test set : {}".format(len(idx_test))) 119 | 120 | L = np.sort(idx_test) 121 | missing = missing_elements(L) 122 | 123 | if (args.step == 'isolate'): 124 | print("Isolated Nodes : %s" %str(missing)) 125 | 126 | # citeseer 데이터 전처리 127 | if dataset == 'citeseer': 128 | tx, ty = preprocess_citeseer(tx, ty, test_idx) 129 | 130 | # feature, adj 131 | features = sp.vstack((allx, tx)).tolil() 132 | features[test_idx, :] = features[test_idx_range, :] 133 | adj = nx.adjacency_matrix(nx.from_dict_of_lists(graph)) 134 | 135 | print("| # of nodes : {}".format(adj.shape[0])) 136 | print("| # of edges : {}".format((adj.sum().sum() + adj.diagonal().sum())/2)) 137 | print("| # of features : {}".format(features.shape[1])) 138 | print("| # of clases : {}".format(ally.shape[1])) 139 | 140 | if args.step == 'normalize': 141 | bef_features = features 142 | bef_adj = adj 143 | 144 | features = normalize_sparse_feature(features) 145 | adj = normalize_sparse_adj(adj+sp.eye(adj.shape[0])) # input is A_hat 146 | 147 | print("Features example before normalization : ") 148 | print(bef_features[:2]) 149 | print("Features example after normalization : ") 150 | print(features[:2]) 151 | print("Adjacency matrix before normalization : ") 152 | print(bef_adj) 153 | print("Adjacency matrix after normalization : ") 154 | print(adj) 155 | 156 | features = torch.FloatTensor(np.array(features.todense())) 157 | sparse_mx = adj.tocoo().astype(np.float32) 158 | adj = torch.FloatTensor(np.array(adj.todense())) 159 | 160 | labels = np.vstack((ally, ty)) 161 | labels[test_idx, :] = labels[test_idx_range, :] 162 | 163 | if dataset == 'citeseer': 164 | save_label = np.where(labels)[1] 165 | for element in missing: 166 | save_label = np.insert(save_label, element, 0) # Missing (Isolated) Nodes 자리에 0 을 채운다. 167 | labels = torch.LongTensor(save_label) 168 | else: 169 | labels = torch.LongTensor(np.where(labels)[1]) 170 | 171 | idx_train, idx_val, idx_test = list(map(lambda x: torch.LongTensor(x), [idx_train, idx_val, idx_test])) 172 | 173 | return adj, features, labels, idx_train, idx_val, idx_test 174 | 175 | if __name__ == '__main__': 176 | # Argument 177 | parser = argparse.ArgumentParser(description='PyTorch KR Tutorial') 178 | parser.add_argument('--dataset', required=True, type=str, help='dataset') 179 | parser.add_argument('--data_path', 180 | default=os.path.join(Path.home(), "Data", "Planetoid"), type=str, help='data path') 181 | parser.add_argument('--step', required=True, type=str, help='[split | isolate | normalize]') 182 | parser.add_argument('--mode', default='process', type=str, help='[process | pitfall]') 183 | args = parser.parse_args() 184 | 185 | # Main 186 | if args.mode == 'process': 187 | adj, features, labels, idx_train, idx_val, idx_test = preprocess_data(path=args.data_path, dataset=args.dataset) 188 | elif args.mode == 'pitfall': 189 | pitfall(path=args.data_path, dataset=args.dataset) 190 | -------------------------------------------------------------------------------- /1_Going_Beyond_Euclidean_Data/README.md: -------------------------------------------------------------------------------- 1 | Geometric Deep Learning: Going Beyond Euclidean Data 2 | ------------------------------------------------------- 3 | 저자 논문 : [here](https://arxiv.org/pdf/1611.08097.pdf). 4 | 5 | # Euclidean vs Non-Euclidean 6 | 7 | ## 1.1 Euclidean 공간이란? 8 | 9 | Euclidean 공간(혹은 Euclidean Geometry)란, 수학적으로 유클리드가 연구했던 평면과 공간의 일반화된 표현입니다. 10 | 11 | 좁은 의미에서 유클리드 공간은, [피타고라스의 정의](https://ko.wikipedia.org/wiki/%ED%94%BC%ED%83%80%EA%B3%A0%EB%9D%BC%EC%8A%A4%EC%9D%98_%EC%A0%95%EB%A6%AC)에 의한 길이소의 제곱의 계수가 모두 양수인 공간을 이야기합니다. 12 | 넓은 의미에서 유클리드 공간은, **그리드(Grid)**로 표현이 가능한 모든 공간을 일컫습니다. 13 | 이 때 그리드(Grid)는, 시간과 공간적 개념을 모두 포함하며, 대표적인 예시로는 '2D 이미지', '3D Voxel', '음성' 데이터 등을 들 수 있습니다. 14 | 15 | | | | | 16 | |:---:|:---:|:---:| 17 | | **2D 이미지** | **3D Voxel** | **음성** | 18 | 19 | ## 1.2 Non Euclidean 공간이란? 20 | 21 | 문자 그대로 'Euclidean 공간이 아닌 공간'을 지칭하며, 대표적으로 두 가지를 들 수 있습니다. 22 | 23 | ### [1.2.1 Manifold](https://ko.wikipedia.org/wiki/%EB%8B%A4%EC%96%91%EC%B2%B4) 24 | 25 | Manifold란, 두 점 사이의 거리 혹은 유사도가 근거리에서는 유클리디안(Euclidean metric, 직선거리)를 따르지만 원거리에서는 그렇지 않은 공간을 일컫습니다. 26 | 27 | 이해가 쉬운 가장 간단한 예로는, 구의 표면(2차원 매니폴드)를 들 수 있습니다. 28 | 3차원 공간에서 A점과 B점 사이의 유클리디안 거리(얇은 실선)와 실제의 거리(geodesic distance, 굵은 실선)는 일치하지 않는 것을 볼 수 있습니다. 29 | 30 |

31 | 32 | 이러한 Manifold 형태를 가지는 데이터의 대표적인 예시로는 3D mesh 혹은 point cloud 형태를 들 수 있습니다. 33 | 34 | - 3D Mesh 35 | 36 | [Wikipedia] Material made of a network of wire or thread. 37 | 38 | 3D Mesh란, 다각형의 조합을 통해 3D 모델을 구성한 것을 의미한다. 39 | 40 | 매우 많은 개수의 다각형으로 실제에 가깝게 사물을 표현할 수 있으면서도, 적은 개수의 간단한 모양의 조합으로도 사물을 빠르게 표현할 수 있는 것이 가장 큰 장점이다. 41 | 42 | - Point cloud 43 | 44 | 점구름(point cloud)란, 3차원 좌표계에서 X, Y, Z 좌표로 정의되며 사물의 표면을 나타내는 데에 주로 사용된다. 45 | 3D scanner에서 주로 이용하는 3차원 표현 방식이다. 46 | 47 | 이런 point cloud는, surface reconstruction을 위해 앞서 소개한 mesh 로 변형하여 처리하기도 한다. 48 | 49 | |

|

| 50 | |:---:|:---:| 51 | | **3D Mesh** | **Point cloud** | 52 | 53 | ### [1.2.2 Graph](https://en.wikipedia.org/wiki/Graph_\(discrete_mathematics\)) 54 | 55 | Graph란, 일련의 노드의 집합 **V**와 연결(변)의 집합 **E**로 구성된 자료 구조의 일종입니다. 56 | 일반적으로 노드에는 데이터가, 엣지엔 노드와 노드 사이의 관계 정보가 포함되어 있습니다. 57 | 58 | 일상적으로 볼 수 있는 Graph형 데이터의 예시로는 Social network 혹은 Brain functional connectivity network등이 있습니다. 59 | 60 | |

|

| 61 | |:---:|:---:| 62 | | **Social Networks** | **Brain Functional Networks** | 63 | 64 | 용어 설명 : [출처](https://ratsgo.github.io/data%20structure&algorithm/2017/11/18/graph/) 65 | 66 | | 용어 | 설명 | 67 | |:------------:|:--------------------| 68 | | sparse graph | node의 개수 `>` edge의 개수 | 69 | | dense graph | node의 개수 `<` edge의 개수 | 70 | | adjacent | 임의의 두 node가 하나의 edge로 연결되어 있을 경우, 두 node는 서로 adjacent 하다 | 71 | | incident | 임의의 두 node가 하나의 edge로 연결되어 있을 경우, edge는 두 node에 incident 하다 | 72 | | degree | node에 연결된 edge의 개수 | 73 | 74 | - 20190107 Issue 75 | 76 | Sparse Graph와 Dense Graph는 위와 같은 정의 외에도, 77 | 78 | a) 79 | Sparse Graph : A sparse graph is a graph G = (V, E) in which |E| = O(|V|). 80 | Dense Graph : A dense graph is a graph G = (V, E) in which |E| = Θ(|V|2). 81 | 82 | b) 83 | A dense graph is a graph in which the number of edges is close to the maximal number of edges. 84 | A sparse graph is a graph in which the number of edges is close to the minimal number of edges. 85 | 86 | 와 같은 정의도 존재합니다. 87 | 88 | ------------------------------------------------------------------------------------------------------------ 89 | 90 | # Spatial vs Spectral 91 | 92 | 딥러닝, 특히 CNN(Convolutional Neural Networks)는 일정한 grid 로 정의할 수 있는 (길이, 시간 등) spatial한 성격의 데이터에 대해 뛰어난 성능을 드러내며 발전해왔습니다. 93 | 94 | 이러한 기존의 Euclidean data (이미지, 음향 등)에서는 두 가지 특징이 성립했습니다. 95 | 96 | - Grid structure 97 | - Translational Equivariance/Invariance 98 | 99 | 그렇다면, 각각 Grid structure 와 Translational Equivariance/Invariance는 어떤 성질이길래 CNN과 같은 알고리즘이 성공적으로 적용될 수 있었을까요? 100 | 101 | ## 2.1 Grid structure 102 | 103 | Grid based metric은 input 크기와 무관하게 적은 parameter의 개수로 이를 학습하는 것을 가능하게 합니다. 104 | 105 | 즉, 이미지의 grid structure는 CNN이 사용하는 아주 작은 크기의 filter만으로도 방대한 이미지의 특징을 빠르게 파악할 수 있도록 해줍니다. 106 | 107 | ## 2.2 'Translational Equivariance/Invariance'란? 108 | 109 | Translational Equivariance/Invariance를 알아보기 위해, 먼저 이미지 처리의 경우를 살펴봅시다. 110 | 111 | 이미지 I 가 (x,y) 에서 가장 중요한 classifier feature인 최대값 m 을 가진다고 가정합시다. 이 때, classifier의 가장 흥미로운 특징 중 하나는, 이미지를 왜곡한 distorted image I' 에서도 m에 의해 마찬가지로 classification이 된다는 점입니다. 112 | 113 | 이러한 특징을 데이터의 Translational한 구조라고 하는데, Translational한 구조의 데이터는 모델의 weight sharing을 가능하게 만들어, 학습 시 매우 큰 이점을 줄 수 있습니다. 114 | 115 | Translational 구조에는 대표적으로 Equivariance와 Invariance를 들 수 있습니다. 116 | 117 | Translational Equivariance/Invariance란, 모든 벡터에 대해 translation (u,v)를 적용한다고 했을 때, translation된 새로운 이미지 I'의 최대값 m' 는 m과 동일하며(Equivariance), 최대값이 나타나는 자리 (x', y')는 (x-u, y-v)로 distortion에 대해 "equally" 변화한다는 것을 의미합니다(Invariance). 118 | 119 | | 용어 | 공식 | 설명 | 120 | |:---|:-----------------------|:---| 121 | | Translational Invariance | (x',y') = (x-u, y-v) | 변형에도 불구하고 같은 feature로 mapping 된다. | 122 | | Translational Equivariance | m' = m | 이미지에서의 변형식은 feature에서의 변형식과 대응된다. | 123 | 124 | 우리가 흔히 사용하는 2D convnet의 input인 image는, translation에 대해서는 equivariant하나, rotation에 대해서는 equivariant하지 않습니다. 125 | 126 | 따라서, 이것이 흔히 이미지 인식 학습 코드에 나타나있는 augmentation코드에 'rotation'이 자주 등장하는 이유라고 생각할 수 있습니다. 127 | 128 | 참고자료 : [참고자료1](https://www.slideshare.net/ssuser06e0c5/brief-intro-invariance-and-equivariance), 129 | [참고자료2](https://www.quora.com/What-is-the-difference-between-equivariance-and-invariance-in-Convolution-neural-networks) 130 | 131 | Invariance와 Equivariance한 성질을 부각하여 학습을 조금 더 효과적으로 하기 위하여 다음과 같은 방법이 활용됩니다. 132 | 133 | ### 2.2.1 Invariance 134 | 135 | CNN을 transformation-invariant하게 만들기 위해, training sample에 대한 data-augmentation을 수행합니다. 136 | 137 |

138 | 139 | 위에 나타난 일련의 augmentation을 통해서, 우리는 이미지가 변형됨에도 불구하고 같은 feature vector로 맵핑되도록 학습할 수 있게 됩니다. 140 | 141 | 이는 generalization 단계에서 기존의 이미지 인식 방식보다 월등히 높은 성능을 거둘 수 있도록 하는 데에 큰 역할을 차지하였습니다. 142 | 143 | ### 2.2.2 Equivariance 144 | 145 | - [Group Convnet](https://arxiv.org/pdf/1602.07576.pdf) 146 | - [Capsule Net](https://arxiv.org/pdf/1710.09829.pdf), [CNN의 한계와 CapsNet에 관한 설명](https://jayhey.github.io/deep%20learning/2017/11/28/CapsNet_1/) 147 | 148 | ---------------------------------------------------------------------------------------------------------------- 149 | 150 | ## 2.3 Non Euclidean Data : Geometric Deep Learning 151 | 152 | 그렇다면, 위의 두 조건이 충족되지 않는 ***Non-Euclidean data*** 에 대해서는 어떻게 학습을 할 수 있을까요? 153 | 154 | 대표적으로 두 가지의 접근법이 있어왔는데, 한가지는 Spatial한 접근법이고, 한 가지는 Spectral한 접근법입니다. 155 | 156 | ## 2.3.1 Spatial Domain 157 | 158 | 기존에 알고 있던 그리드로 표현할 수 있는 데이터들은 대부분 Spatial Domain 에서 처리가 가능합니다. 159 | 대표적인 Spatial Domain에서의 처리는 이미지 인식에 이미 널리 알려진 Convolutional Neural Network가 존재합니다. 160 | 161 | 또한, 그리드로 정의되어 있지 않는 데이터 역시 spatial domain에서 처리하고자 하는 시도들이 존재한다. 162 | 대표적인 것으로 [Graph Attention Network](https://arxiv.org/pdf/1710.10903.pdf) 를 들 수 있습니다. 163 | 164 | - Spatial 접근 예시 : [Spectral Networks and Deep Locally Connected Networks on Graphs](https://arxiv.org/pdf/1312.6203.pdf) 165 | 166 | 더 자세한 내용은 [4_Spatial_Graph_Convolution](../4_Spatial_Graph_Convolution)에서 다루겠습니다. 167 | 168 | ## 2.3.2 Spectral Domain 169 | 170 | Spatial Domain 내에서 일정한 grid 를 가지지 않아 처리하기가 복잡한 데이터를 다루는 방법 중의 하나는 이를 spectral domain으로 사영시키는 것이다. 171 | 172 | 이는 Fourier Transformation 을 통해 이루어진다. 173 | 174 | 그래프 구조에서는, Laplacian 변환을 통해 spectral space의 basis를 구하며, normalization과 결합하여 아래와 같은 형태로 표현되게 됩니다. 175 | 176 |

177 | 178 | - Spectral 접근 예시 : [Spectral CNN](http://www.cs.yale.edu/homes/spielman/561/) 179 | 180 | 더 자세한 내용은 [3_Spectral_Graph_Convolution](../3_Spectral_Graph_Convolution) 에서 다루겠습니다. 181 | -------------------------------------------------------------------------------- /3_Spectral_Graph_Convolution/README.md: -------------------------------------------------------------------------------- 1 | # Spectral Graph Convolutional Networks 2 | 3 | 기존의 Euclidean data 에서, Spatial CNN은 다음과 같은 조건 아래에서 뛰어난 성능을 보였습니다. 4 | 5 | - Grid structure 6 | - Translational Equivalance/Invariance 7 | 8 | 그러나, Spatial CNN은 위의 두 조건이 충족되지 않는 Non-Euclidean data에 대해서 학습이 어렵다는 한계점을 지니고 있습니다. 9 | 이를 해결하기 위한 첫 번째 방법은, Spatial domain에서 Spectral domain 으로 옮기는 접근법입니다. [Spectral CNN](http://www.cs.yale.edu/homes/spielman/561/), [Semi-Supervised Classification with Graph Convolutional Networks](https://arxiv.org/pdf/1609.02907.pdf) 10 | 11 | ## Graph Convolutional Networks 12 | 13 | Social Networks, Knowledge Graphs, Protein interaction networks, World Wide Web 등, 우리 주변의 많은 데이터들은 그래 프 구조를 지니고 있습니다. 14 | 대부분의 Graph Neural Network 모델들은 공통된 구조를 가지고 있고, 그래프의 모든 위치에 대해 공유된 필터를 사용한다는 점에 착안하여 CNN(Convolutional Neural Network)와 마찬가지로 이를 GCN(Graph Convolutional Networks)라고 합니다. 15 | 16 |

17 | 18 | Graph Convolutional Networks의 목표는, G=(V, E) (여기서, V는 Vertex, 즉 노드의 표현형이며, E는 Edge, 각 변 혹은 엣지의 표현형입니다.)로 표현되는 그래프 데이터에서 특정 시그널이나 feature를 잡는 함수를 학습하는 것입니다. 19 | 20 | **Input** 21 | - N x D 차원의 feature vector (N : Number of nodes, D : number of input features) 22 | - Graph의 구조를 반영할 수 있는 매트릭스 형태의 표현식; 일반적으로 adjacency matrix A 를 사용합니다. 23 | 24 | **Output** 25 | - N x F 차원의 feature 매트릭스 (N : Number of nodes, F : number of output features) 26 | 27 | 각 뉴럴 네트워크의 계층은 이런 input을 ReLU 혹은 pooling 등의 non-linear function ***f*** 를 적용합니다. 28 | 29 |

30 | 31 | ***f*** 함수를 어떻게 결정하고 parameter화 시키냐에 따라 ![H(0)](http://latex.codecogs.com/gif.latex?H%5E%7B%280%29%7D%3DX) 와 ![H(L)](http://latex.codecogs.com/gif.latex?H%5E%7B%28L%29%7D%3DZ), 로부터 원하는 특정 모델을 구상할 수 있게 됩니다. 32 | 33 | 이번 튜토리얼에서 사용할 GCN 구조는 아래와 같습니다. 34 | 35 |

36 | 37 | Non-linear activation function 으로는 ReLU (Rectified Linear Unit)를 사용하며, 이를 통해 아래의 식을 도출할 수 있습니다. 38 | 39 |

40 | 41 | ***A*** 와의 곱은 각 노드에 대해 자기 자신을 제외한(self connection이 존재하지 않는다는 가정 하에) 모든 인접 노드의 feature vector를 합하는 것을 의미합니다. 42 | 43 | 이와 같은 방식에선, 스스로의 feature 값을 참조할 수 없으므로, 이를 해결하기 위하여 ***A*** 를 사용하는 대신 ***A+I*** (A_hat) 을 사용하여 계산합니다. 44 | 45 | ***A*** 는 일반적으로 normalize가 되어있지 않은 상태이므로, ***A*** 와의 곱은 각 feature vector의 scale을 완전히 바꿔놓을 수 있게 됩니다. 46 | 47 | 따라서, 우리는 이전에 기술한 것과 마찬가지로 ***A*** 의 모든 열의 합이 1 이 될 수 있도록 row-wise normalize를 feature와 adjacency matrix에 각각 진행합니다. 48 | 49 | 이는 random walk 방식으로는 ![row sum](http://latex.codecogs.com/gif.latex?D%5E%7B-1%7DA)이 되며, 원 논문에서 사용한 방식으로는 50 | 51 |

52 | 53 | 가 됩니다. 54 | 55 | 이를 [코드](utils.py)에서 살펴보면 56 | 57 | ```bash 58 | # line 107 59 | adj = normalize_sparse_adj(adj + sp.eye(adj.shape[0])) # pass (A+I) (or A_hat) 60 | 61 | # line 42 62 | def normalize_sparse_adj(mx): 63 | """Laplacian Normalization""" 64 | rowsum = np.array(mx.sum(1)) # D_hat 65 | r_inv_sqrt = np.power(rowsum, -0.5).flatten() # D_hat^(-1/2) 66 | r_inv_sqrt[np.isinf(r_inv_sqrt)] = 0. 67 | r_mat_inv_sqrt = sp.diags(r_inv_sqrt) 68 | 69 | return mx.dot(r_mat_inv_sqrt).transpose().dot(r_mat_inv_sqrt).tocoo() # D_hat^(-1/2) x A_hat x D_hat^(-1/2) 70 | ``` 71 | 72 | 각 단계의 계산과정이 코드 상에 어디에 해당하는지는 [gcn.py](./gcn.py) 코드 내에 주석으로 삽입하였습니다. 73 | 74 | **최종 구현 :** 75 | 76 | 위의 모든 구현 이론을 종합하여 [Kipf & Welling](http://arxiv.org/abs/1609.02907) (ICLR 2017) 논문에서 소개한 Graph Convolutional Neural Network 를 구현하였습니다. 77 | 78 | 더 많은 세부 정보를 위해서는, [여기](https://tkipf.github.io/graph-convolutional-networks/)를 참조하시면 좋을 것 같습니다. 79 | 80 | ## Molecule Structure Processing 81 | 82 |

83 | 84 | Graph Structure 데이터의 가장 대표적인 예로는, 분자 구조가 존재할 수 있습니다. 85 | 86 | 이전 neural network 구조에서는, 분자 구조를 사용할 때는 ECFP (Extended Connectivity FingerPrint)를 사용하여 고정된 형식의 벡터 표현식을 이용해왔습니다. [예시자료](https://arxiv.org/pdf/1811.09714.pdf) 87 | 88 |

89 | 90 | 그러나, 이는 Graph 단위에서 특정 요소가 존재하는지의 여부에 대한 표현식이므로, 분명한 한계가 존재할 수 밖에 없습니다. 91 | 92 | 이와 같은 그래프 형태의, non-Euclidean graph 데이터의 구조를 Graph Convolution network를 통하여 학습할 수 있습니다. 93 | 94 | 본 튜토리얼에서는 이를 처리하여 고정된 형식의 벡터를 만드는 forward path를 소개하며, 이후 이를 활용하는 것은 95 | 96 | 마지막 layer의 vector를 연결하여 [classification](https://arxiv.org/pdf/1805.10988.pdf)을 하거나, [fingerprint](https://arxiv.org/pdf/1509.09292.pdf)를 만들거나, siamese network를 구상하여 [유사도를 측정](https://arxiv.org/pdf/1703.02161.pdf)할 수 있습니다. 97 | 98 | 그러나, 분자 구조에는 edge가 위처럼 2차원의 단순한 adjacency matrix에서 표현되지 못합니다. 99 | Edge에도 여러가지 type이 존재하기 때문입니다. 100 | 101 | ## Graphs with Mulitple Type Edges 102 | 103 | 분자 구조의 경우에는, Edge (Bond라고 표현합니다)가 여러가지 type을 가질 수 있습니다. 가장 대표적인 것으로는 single, double, triple, aromatic 등의 bond type이 있습니다. 104 | 105 |

106 | 107 | 이런 경우에는, 일반적으로 Aggregation 이라는 방법을 통해 데이터를 처리합니다. [reference](https://arxiv.org/pdf/1806.02473.pdf) 108 | 109 | 아래의 코드는, 튜토리얼 내에서 지정한 임의의 pid를 가진 molecule vector를 [RDkit](https://www.rdkit.org/)을 통해 graph 형태로 표현한 뒤, 이를 GCN forward path 에 대입하여 100차원의 feature vector를 생성하는 과정입니다. 110 | 111 | 사용된 화합물은 아래와 같이 생긴 1-benzylimidazole 입니다. 112 | 113 | 114 |

115 | 116 | ```bash 117 | # docker run -it bumsoo /bin/bash 실행된 환경 118 | $ python forward_mol.py 119 | 120 | # docker 외부 실행환경 121 | $ docker run -it bumsoo python 3_Spectral_Graph_Convolution/forward_mol.py 122 | ``` 123 | 124 | 위 코드를 실행시키면, 아래와 같은 결과를 얻을 수 있습니다. 125 | 126 | ![mol](./imgs/result_mol.png) 127 | 128 | 결과 화면을 통해, adj = (12 x 12 x 6), feat = (12 x 62) 짜리 그래프로부터 고정된 500 크기의 vector 로 forward가 됐음을 확인할 수 있습니다. 129 | 130 | ## Train Planetoid Network 131 | 132 | | dataset | classes | nodes | # of edge | 133 | |:-------:|:-------:|:-----:|:-----------:| 134 | | citeseer| 6 | 3,327 | 4,676 | 135 | | cora | 7 | 2,708 | 5,278 | 136 | | pubmed | 3 | 19,717| 44,327 | 137 | 138 | 139 | 이번에는, [2_Understanding_Graphs](../2_Understanding_Graphs) 에서 다루었던 Planetoid의 데이터셋에 대해 학습을 해보겠습니다. 140 | 141 | Planetoid는 node classification task 이며, document에 해당하는 각 노드가 주어진 142 | 143 | k 개의 class 중 어느 class 에 해당하는지 classification을 하면 되는 문제입니다. 144 | 145 | 아래의 script를 실행시키면, 원하시는 데이터셋에 GCN 을 학습시키실 수 있습니다. 146 | 147 | [2_Understanding_Graphs](../2_Understanding_Graphs) 에서 설명한 것과 같이 Planetoid 데이터셋을 다운로드 받으신 후, [:dir to dataset] 에 대입하여 실행하시면 됩니다. 148 | 149 | 기본 default 설정은 2_Understanding_Graphs 의 /home/[:user]/Data/Planetoid 디렉토리로 설정되어 있습니다. 150 | 151 | 이전 2번 튜토리얼 레포에서 보셨던 데이터의 전처리에 관한 사항은, [utils.py](utils.py) 에서 확인해보실 수 있습니다. 152 | 153 | ```bash 154 | # nvidia docker run -it bumsoo-graph-tutorial /bin/bash 실행 이후 155 | > python train.py --dataroot [:dir to dataset] --datset [:cora | citeseer | pubmed] 156 | 157 | # 바로 실행하는 경우 158 | $ nvidia-docker run -it bumsoo python 3_Spatial_Graph_Convolution/train.py --dataset [:dataset] 159 | ``` 160 | 161 | 위 코드를 실행하면 다음과 같은 결과 화면을 보실 수 있습니다. 162 | 163 | ![train_cora](./imgs/cora_gcn_train.png) 164 | 165 | ## Test (Inference) Planetoid networks 166 | 167 | Training 과정을 모두 마치신 이후, 다음과 같은 코드를 통해 학습된 weight를 테스트셋에 적용해보실 수 있습니다. 168 | 169 | ```bash 170 | # nvidia docker run -it bumsoo-graph-tutorial /bin/bash 실행 이후 171 | > python test.py --dataroot [:dir to dataset] --dataset [:cora | citeseer | pubmed] 172 | 173 | # 바로 실행하는 경우 174 | $ nvidia-docker run -it bumsoo python 3_Spatial_Graph_Convolution/test.py --dataset [:dataset] 175 | ``` 176 | 177 | 위 코드를 실행하면 다음과 같은 결과 화면을 보실 수 있습니다. 178 | 179 | ![test_cora](./imgs/cora_gcn_test.png) 180 | 181 | ## Result 182 | 183 | 최종 성능은 다음과 같습니다. GCN (recon) 이 본 repository의 코드로 학습 후, test data 에 적용한 결과입니다. 184 | 185 | | Method | Citeseer | Cora | Pubmed | 186 | |:------------|:---------|:-----|:-------| 187 | | GCN (rand) | 67.9 | 80.1 | 78.9 | 188 | | GCN (paper) | 70.3 | 81.5 | 79.0 | 189 | | **GCN (recon)** | **70.6** | **80.9** | **80.8** | 190 | --------------------------------------------------------------------------------