Commit 47f91959 authored by Donghan Yu's avatar Donghan Yu
Browse files

public

parents
# Created by https://www.gitignore.io/api/python,macos
# Edit at https://www.gitignore.io/?templates=python,macos
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don’t work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# End of https://www.gitignore.io/api/python,macos
MIT License
Copyright (c) 2020 Donghan Yu, Ruohong Zhang, Zhengbao Jiang, Yuexin Wu, Yiming Yang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Code for [Graph-Revised Convolutional Network](https://arxiv.org/abs/1911.07123) ([ECML-PKDD 2020](https://ecmlpkdd2020.net/))
## Requirements
```
python >= 3.6.0
pytorch = 1.5.0
tqdm
itermplot
```
The code is based on [pyg](https://github.com/rusty1s/pytorch_geometric). Please see [instructions](https://pytorch-geometric.readthedocs.io/en/latest/notes/installation.html) for its installation.
*dataprocess.py* is used for data spliting, edge sampling, and data loader.
## Reproduce Results
### Run our model GRCN under fixed train/val/test split
```
./run_fixed.sh 1(GPU No.) GRCN Cora(dataset: Cora, CiteSeer, PubMed) --sparse
```
To save the log result, add `--save` in the command.
You can change the parameters of *run_fixed.sh* and *config/*.
### Run our model GRCN under random train/val/test split
```
./run_random.sh 1(GPU No.) GRCN Cora(dataset: Cora, CiteSeer, PubMed, CoraFull, Computers, CS) --sparse
```
When running on PubMed dataset, add `--keep_train_num`.
To save the log result, add `--save` in the command.
You can change the parameters of *run_random.sh* and *config/*.
### Results
Our model achieves the following performance on :
#### semi-supervised node classification ([fixed split](https://arxiv.org/pdf/1603.08861.pdf))
| Model | Cora | CiteSeer | PubMed |
|-----------|----------|----------|----------|
| GCN | 81.4±0.5 | 70.9±0.5 | 79.0±0.3 |
| GAT | 83.2±0.7 | 72.6±0.6 | 78.8±0.3 |
| LDS | 84.0±0.4 | 74.8±0.5 | N/A |
| GLCN | 81.8±0.6 | 70.8±0.5 | 78.8±0.4 |
| Fast-GRCN | 83.6±0.4 | 72.9±0.6 | 79.0±0.2 |
| GRCN | 84.2±0.4 | 73.6±0.5 | 79.0±0.2 |
#### semi-supervised node classification (random splits)
| Model | Cora | CiteSeer | PubMed |
|-----------|----------|----------|----------|
| GCN | 81.2±1.9 | 69.8±1.9 | 77.7±2.9 |
| GAT | 81.7±1.9 | 68.8±1.8 | 77.7±3.2 |
| LDS | 81.6±1.0 | 71.0±0.9 | N/A |
| GLCN | 81.4±1.9 | 69.8±1.8 | 77.2±3.2 |
| Fast-GRCN | 83.8±1.6 | 72.3±1.4 | 77.6±3.2 |
| GRCN | 83.7±1.7 | 72.6±1.3 | 77.9±0.2 |
'''
Complete the graph structrue
Be careful that the completed graph should be symmetric
Be careful that you need to renormalize the completed adjacency matrix
'''
import torch
from tqdm import tqdm
import torch.nn.functional as F
from torch_geometric.nn import GCNConv
from sklearn.metrics.pairwise import *
from collections import Counter
import numpy as np
import random
EOS = 1e-10
# construct adj matrix from edge_index
def convert_edge2adj(edge_index, num_nodes):
# float type
mat = torch.zeros((num_nodes, num_nodes))
for i in range(edge_index.shape[1]):
x, y = edge_index[:, i]
mat[x, y] = mat[y, x] = 1
return mat
# generate normalized random walk adjacency matrix
def normalize(adj):
inv_sqrt_degree = 1. / torch.sqrt(adj.sum(dim=1, keepdim=False))
inv_sqrt_degree[inv_sqrt_degree == float("Inf")] = 0
return inv_sqrt_degree[:, None] * adj * inv_sqrt_degree[None, :]
# transform flat index to adjacency edge index for input of GraphConv
def flat_index2adj_index(indices, num_nodes):
size = len(indices)
ret_list = []
for num, index in enumerate(indices):
# TODO: check whether the transformation is correct or not
ret_list.append([index / num_nodes, index % num_nodes])
ret_indices = torch.tensor(ret_list)
return ret_indices.t().contiguous()
def sparse_complete(true_adj, pred_adj, k):
flat_true_adj = true_adj.view(-1)
# remove diagonal entries
flat_pred_adj = (pred_adj).view(-1)
sorted, indices = torch.topk(flat_pred_adj, k)
two_d_indices = flat_index2adj_index(indices, pred_adj.shape[0])
return 1.0 * flat_true_adj[indices].sum() / k, two_d_indices
def _complete_acc(true_adj, pred_adj):
flat_true_adj = true_adj.view(-1)
pos_index = (flat_true_adj == 1).nonzero().view(-1)
neg_index = (flat_true_adj == 0).nonzero().view(-1)
pos_num = pos_index.shape[0]
neg_num = neg_index.shape[0]
sample_neg_num = pos_num * 5
neg_sample = neg_index[torch.randperm(neg_num)[:sample_neg_num]]
flat_pred_adj = (pred_adj).view(-1)
total_index = torch.cat([pos_index, neg_sample])
sorted, indices = torch.topk(flat_pred_adj[total_index], pos_num)
return 1.0 * flat_true_adj[total_index[indices]].sum() / pos_num
def cal_similarity_graph(node_features):
similarity_graph = torch.mm(node_features, node_features.t())
return similarity_graph
class Complete(object):
def __init__(self, sampled_edge_index, data, model, device, args):
super(Complete, self).__init__()
self.sampled_edge_index = sampled_edge_index.to(device)
self.data = data.to(device)
self.device = device
self.model = model
self.dense = args.dense
self.reduce = args.reduce
# pre-preprocessing of the edge set as normalized adjacency graphs, for the ease of edge completion
if self.dense:
self.adj_full = convert_edge2adj(data.edge_index, data.num_nodes).to(device)
self.adj_part = convert_edge2adj(sampled_edge_index, data.num_nodes).to(device)
self.loop_adj_part = torch.eye(self.adj_part.shape[0]).to(device) + self.adj_part
self.norm_adj_part = normalize(self.loop_adj_part)
else:
loop_edge_index = torch.stack([torch.arange(data.num_nodes), torch.arange(data.num_nodes)])
self.loop_adj_part = torch.cat([sampled_edge_index, loop_edge_index], dim=1)
def complete_graph(self, method, compl_param):
self.model.eval()
if method in ["GCN", "SGC", "GAT"]:
if self.dense:
return 0., self.norm_adj_part
else:
return 0., self.sampled_edge_index
elif method in ["GAT_official", "GAT_dense"]:
if self.dense:
return 0., self.norm_adj_part
else:
return 0., self.loop_adj_part
else:
exit("wrong model")
import torch
import torch.nn.functional as F
params_random={
"nhid": 64, # number of hidden units per layer
"dropout": 0.5,
"F": torch.relu,
"F_graph": torch.tanh,
"lr": 5e-3, # learning rate for node classification
"wd": 5e-3, # weight decay for node classification, default 5e-3
"lr_graph": 1e-3, # learning rate for graph modification
"epochs": 300, # epoch number for model training
"log_epoch": 10,
"normalize": True,
"batch_size": 20000,
"layertype": "diag"
}
import torch
import torch.nn.functional as F
params_fixed={
"nhid": 32, # number of hidden units per layer
"dropout": 0.5,
"F": torch.relu,
"F_graph": torch.tanh,
"lr": 5e-2, # learning rate for node classification
"wd": 5e-3, # weight decay for node classification
"lr_graph": 1e-3, # learning rate for graph modification
"epochs": 200, # epoch number for model training
"log_epoch": 1,
"normalize": False,
"batch_size": 5000,
"layertype": "diag"
}
params_random={
"nhid": 32, # number of hidden units per layer
"dropout": 0.5,
"F": torch.relu,
"F_graph": torch.tanh,
"lr": 5e-3, # learning rate for node classification
"wd": 5e-3, # weight decay for node classification
"lr_graph": 1e-3, # learning rate for graph modification
"epochs": 300, # epoch number for model training
"log_epoch": 1,
"normalize": False,
"batch_size": 5000,
"layertype": "diag"
}
import torch
import torch.nn.functional as F
params_random={
"nhid": 64, # number of hidden units per layer
"dropout": 0.5,
"F": torch.relu,
"F_graph": torch.tanh,
"lr": 5e-3, # learning rate for node classification
"wd": 5e-3, # weight decay for node classification, default 5e-3
"lr_graph": 1e-3, # learning rate for graph modification
"epochs": 300, # epoch number for model training
"log_epoch": 10,
"normalize": True,
"batch_size": 20000,
"layertype": "diag"
}
import torch
import torch.nn.functional as F
params_fixed={
"nhid": 32, # number of hidden units per layer
"dropout": 0.5,
"F": torch.relu,
"F_graph": torch.tanh,
"lr": 5e-3, # learning rate for node classification
"wd": 5e-3, # weight decay for node classification
"lr_graph": 1e-3, # learning rate for graph modification
"epochs": 300, # epoch number for model training
"log_epoch": 1,
"normalize": True,
"batch_size": 3000,
"layertype": "diag"
}
params_random={
"nhid": 32, # number of hidden units per layer
"dropout": 0.5,
"F": torch.relu,
"F_graph": torch.tanh,
"lr": 5e-3, # learning rate for node classification
"wd": 5e-3, # weight decay for node classification
"lr_graph": 1e-3, # learning rate for graph modification
"epochs": 300, # epoch number for model training
"log_epoch": 1,
"normalize": True,
"batch_size": 3000,
"layertype": "diag"
}
import torch
import torch.nn.functional as F
params_random={
"nhid": 64, # number of hidden units per layer
"dropout": 0.5,
"F": torch.relu,
"F_graph": "identity",
"lr": 5e-3, # learning rate for node classification
"wd": 5e-3, # weight decay for node classification
"lr_graph": 5e-3, # learning rate for graph modification
"epochs": 300, # epoch number for model training
"log_epoch": 10,
"normalize": True,
"batch_size": 20000,
"layertype": "diag"
}
import torch
import torch.nn.functional as F
params_fixed={
"nhid": 32, # number of hidden units per layer
"dropout": 0.5,
"F": torch.relu,
"F_graph": torch.tanh,
"lr": 5e-3, # learning rate for node classification
"wd": 5e-3, # weight decay for node classification
"lr_graph": 0, # learning rate for graph modification
"epochs": 300, # epoch number for model training
"log_epoch": 1,
"normalize": False,
"batch_size": 20000,
"layertype": "dense"
}
params_random={
"nhid": 32, # number of hidden units per layer
"dropout": 0.5,
"F": torch.relu,
"F_graph": torch.tanh,
"lr": 5e-3, # learning rate for node classification
"wd": 5e-3, # weight decay for node classification
"lr_graph": 1e-3, # learning rate for graph modification
"epochs": 300, # epoch number for model training
"log_epoch": 10,
"normalize": True,
"batch_size": 20000,
"layertype": "diag"
}
import numpy as np
import torch
from collections import defaultdict
from torch_scatter import scatter_add
class Data(object):
def __init__(self, x, y, adj, train_mask, val_mask, test_mask):
self.x = x
self.y = y
self.num_nodes = x.shape[0]
self.adj = adj
self.train_mask = train_mask
self.val_mask = val_mask
self.test_mask = test_mask
def to(self, device):
self.x = self.x.to(device)
self.y = self.y.to(device)
self.adj = self.adj.to(device)
# sample partial edges
def sample_edge(edge_index, ratio, seed=123):
# sample from half side of the symmetric adjacency matrix
half_edge_index = []
for i in range(edge_index.shape[1]):
if edge_index[0, i] < edge_index[1, i]:
half_edge_index.append(edge_index[:, i].view(2,-1))
half_edge_index = torch.cat(half_edge_index, dim=1)
np.random.seed(seed)
num_edge = half_edge_index.shape[1]
samples = np.random.choice(num_edge, size=int(ratio*num_edge), replace=False)
sampled_edge_index = half_edge_index[:, samples]
sampled_edge_index = torch.cat([sampled_edge_index, sampled_edge_index[torch.LongTensor([1,0])]], dim=1)
return sampled_edge_index
def random_split(data, data_seed, args):
if data_seed == -1:
print("fixed split")
return 0
if args.dataset in ["Cora", "CiteSeer", "PubMed"]:
if not args.keep_train_num:
num_nodes = data.train_mask.shape[0]
train_num = torch.sum(data.train_mask)
val_num = torch.sum(data.val_mask)
test_num = torch.sum(data.test_mask)
print(train_num, val_num, test_num)
data.train_mask = torch.zeros(num_nodes).type(torch.uint8)
data.val_mask = torch.zeros(num_nodes).type(torch.uint8)
data.test_mask = torch.zeros(num_nodes).type(torch.uint8)
inds = np.arange(num_nodes)
np.random.seed(data_seed)
np.random.shuffle(inds)
inds = torch.tensor(inds)
data.train_mask[inds[:train_num]] = 1
data.val_mask[inds[train_num:train_num + val_num]] = 1
data.test_mask[inds[train_num + val_num:train_num + val_num + test_num]] = 1
else:
index = data.y
num_nodes = data.y.shape[0]
val_num = torch.sum(data.val_mask)
test_num = torch.sum(data.test_mask)
input = torch.ones(num_nodes)
out = scatter_add(input, index)
train_node_per_class = args.train_num
data.train_mask = torch.zeros(num_nodes).type(torch.uint8)
data.val_mask = torch.zeros(num_nodes).type(torch.uint8)
data.test_mask = torch.zeros(num_nodes).type(torch.uint8)
inds = np.arange(num_nodes)
np.random.seed(data_seed)
np.random.shuffle(inds)
inds = torch.tensor(inds)
data_y_shuffle = data.y[inds]
for i in range(out.shape[0]):
inds_i = inds[data_y_shuffle==i]
data.train_mask[inds_i[:train_node_per_class]] = 1
val_test_inds = np.arange(num_nodes)[data.train_mask.numpy() == 0]
np.random.shuffle(val_test_inds)
val_test_inds = torch.tensor(val_test_inds)
data.val_mask[val_test_inds[:val_num]] = 1
data.test_mask[val_test_inds[val_num:val_num + test_num]] = 1
elif args.dataset in ["CoraFull", "Computers", "Photo", "CS"]:
index = data.y
num_nodes = data.y.shape[0]
input = torch.ones(num_nodes)
out = scatter_add(input, index)
train_node_per_class = args.train_num
val_node_per_class = 30
data.train_mask = torch.zeros(num_nodes).type(torch.uint8)
data.val_mask = torch.zeros(num_nodes).type(torch.uint8)
data.test_mask = torch.zeros(num_nodes).type(torch.uint8)
inds = np.arange(num_nodes)
np.random.seed(data_seed)
np.random.shuffle(inds)
inds = torch.tensor(inds)
data_y_shuffle = data.y[inds]
for i in range(out.shape[0]):
if out[i] <= train_node_per_class + val_node_per_class:
continue
inds_i = inds[data_y_shuffle==i]
data.train_mask[inds_i[:train_node_per_class]] = 1
data.val_mask[inds_i[train_node_per_class:train_node_per_class+val_node_per_class]] = 1
data.test_mask[inds_i[train_node_per_class+val_node_per_class:]] = 1
def dataloader(data, Adj, batch_size, sparse=False, shuffle=False):
inds = np.arange(data.num_nodes)
if shuffle:
np.random.shuffle(inds)
inds = torch.tensor(inds)
batch_data_list = []
start = 0
while start < data.num_nodes:
end = min(start + batch_size, data.num_nodes)
batch_inds = inds[start:end]
if not sparse:
batch_data = Data(x=data.x[batch_inds], y=data.y[batch_inds],
adj=Adj[batch_inds][:, batch_inds],
train_mask=data.train_mask[batch_inds],
val_mask=data.val_mask[batch_inds],
test_mask=data.test_mask[batch_inds])