chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Graphbolt Quickstart Tutorial
|
||||
|
||||
Graphbolt provides all you need to create a dataloader to train a Graph Neural Networks.
|
||||
|
||||
## Examples
|
||||
|
||||
- The [node_classification.py](https://github.com/dmlc/dgl/blob/master/examples/graphbolt/quickstart/node_classification.py)
|
||||
shows how to create a Graphbolt dataloader to train a 2 layer Graph Convolutional Networks node
|
||||
classification model.
|
||||
- The [link_prediction.py](https://github.com/dmlc/dgl/blob/master/examples/graphbolt/quickstart/link_prediction.py)
|
||||
shows how to create a Graphbolt dataloader to train a 2 layer GraphSage link prediction model.
|
||||
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
This example shows how to create a GraphBolt dataloader to sample and train a
|
||||
link prediction model with the Cora dataset.
|
||||
|
||||
Disclaimer: Please note that the test edges are not excluded from the original
|
||||
graph in the dataset, which could lead to data leakage. We are ignoring this
|
||||
issue for this example because we are focused on demonstrating usability.
|
||||
"""
|
||||
|
||||
import dgl.graphbolt as gb
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from dgl.nn import SAGEConv
|
||||
from torcheval.metrics import BinaryAUROC
|
||||
|
||||
|
||||
############################################################################
|
||||
# (HIGHLIGHT) Create a single process dataloader with dgl graphbolt package.
|
||||
############################################################################
|
||||
def create_dataloader(dataset, device, is_train=True):
|
||||
# The second of two tasks in the dataset is link prediction.
|
||||
task = dataset.tasks[1]
|
||||
itemset = task.train_set if is_train else task.test_set
|
||||
|
||||
# Sample seed edges from the itemset.
|
||||
datapipe = gb.ItemSampler(itemset, batch_size=256)
|
||||
|
||||
# Copy the mini-batch to the designated device for sampling and training.
|
||||
datapipe = datapipe.copy_to(device)
|
||||
|
||||
if is_train:
|
||||
# Sample negative edges for the seed edges.
|
||||
datapipe = datapipe.sample_uniform_negative(
|
||||
dataset.graph, negative_ratio=1
|
||||
)
|
||||
|
||||
# Sample neighbors for the seed nodes.
|
||||
datapipe = datapipe.sample_neighbor(dataset.graph, fanouts=[4, 2])
|
||||
|
||||
# Exclude seed edges from the subgraph.
|
||||
datapipe = datapipe.transform(gb.exclude_seed_edges)
|
||||
|
||||
else:
|
||||
# Sample neighbors for the seed nodes.
|
||||
datapipe = datapipe.sample_neighbor(dataset.graph, fanouts=[-1, -1])
|
||||
|
||||
# Fetch features for sampled nodes.
|
||||
datapipe = datapipe.fetch_feature(
|
||||
dataset.feature, node_feature_keys=["feat"]
|
||||
)
|
||||
|
||||
# Initiate the dataloader for the datapipe.
|
||||
return gb.DataLoader(datapipe)
|
||||
|
||||
|
||||
class GraphSAGE(nn.Module):
|
||||
def __init__(self, in_size, hidden_size=16):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList()
|
||||
self.layers.append(SAGEConv(in_size, hidden_size, "mean"))
|
||||
self.layers.append(SAGEConv(hidden_size, hidden_size, "mean"))
|
||||
self.predictor = nn.Sequential(
|
||||
nn.Linear(hidden_size, hidden_size),
|
||||
nn.ReLU(),
|
||||
nn.Linear(hidden_size, 1),
|
||||
)
|
||||
|
||||
def forward(self, blocks, x):
|
||||
hidden_x = x
|
||||
for layer_idx, (layer, block) in enumerate(zip(self.layers, blocks)):
|
||||
hidden_x = layer(block, hidden_x)
|
||||
is_last_layer = layer_idx == len(self.layers) - 1
|
||||
if not is_last_layer:
|
||||
hidden_x = F.relu(hidden_x)
|
||||
return hidden_x
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def evaluate(model, dataset, device):
|
||||
model.eval()
|
||||
dataloader = create_dataloader(dataset, device, is_train=False)
|
||||
|
||||
logits = []
|
||||
labels = []
|
||||
for step, data in enumerate(dataloader):
|
||||
# Get node pairs with labels for loss calculation.
|
||||
compacted_seeds = data.compacted_seeds.T
|
||||
label = data.labels
|
||||
|
||||
# The features of sampled nodes.
|
||||
x = data.node_features["feat"]
|
||||
|
||||
# Forward.
|
||||
y = model(data.blocks, x)
|
||||
logit = (
|
||||
model.predictor(
|
||||
y[compacted_seeds[0].long()] * y[compacted_seeds[1].long()]
|
||||
)
|
||||
.squeeze()
|
||||
.detach()
|
||||
)
|
||||
|
||||
logits.append(logit)
|
||||
labels.append(label)
|
||||
|
||||
logits = torch.cat(logits, dim=0)
|
||||
labels = torch.cat(labels, dim=0)
|
||||
|
||||
# Compute the AUROC score.
|
||||
metric = BinaryAUROC()
|
||||
metric.update(logits, labels)
|
||||
score = metric.compute().item()
|
||||
print(f"AUC: {score:.3f}")
|
||||
|
||||
|
||||
def train(model, dataset, device):
|
||||
dataloader = create_dataloader(dataset, device)
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)
|
||||
|
||||
for epoch in range(10):
|
||||
model.train()
|
||||
total_loss = 0
|
||||
########################################################################
|
||||
# (HIGHLIGHT) Iterate over the dataloader and train the model with all
|
||||
# mini-batches.
|
||||
########################################################################
|
||||
for step, data in enumerate(dataloader):
|
||||
# Get node pairs with labels for loss calculation.
|
||||
compacted_seeds = data.compacted_seeds.T
|
||||
labels = data.labels
|
||||
|
||||
# The features of sampled nodes.
|
||||
x = data.node_features["feat"]
|
||||
|
||||
# Forward.
|
||||
y = model(data.blocks, x)
|
||||
logits = model.predictor(
|
||||
y[compacted_seeds[0].long()] * y[compacted_seeds[1].long()]
|
||||
).squeeze()
|
||||
|
||||
# Compute loss.
|
||||
loss = F.binary_cross_entropy_with_logits(logits, labels.float())
|
||||
|
||||
# Backward.
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
total_loss += loss.item()
|
||||
|
||||
print(f"Epoch {epoch:03d} | Loss {total_loss / (step + 1):.3f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
||||
print(f"Training in {device} mode.")
|
||||
|
||||
# Load and preprocess dataset.
|
||||
print("Loading data...")
|
||||
dataset = gb.BuiltinDataset("cora").load()
|
||||
|
||||
# If a CUDA device is selected, we pin the graph and the features so that
|
||||
# the GPU can access them.
|
||||
if device == torch.device("cuda:0"):
|
||||
dataset.graph.pin_memory_()
|
||||
dataset.feature.pin_memory_()
|
||||
|
||||
in_size = dataset.feature.size("node", None, "feat")[0]
|
||||
model = GraphSAGE(in_size).to(device)
|
||||
|
||||
# Model training.
|
||||
print("Training...")
|
||||
train(model, dataset, device)
|
||||
|
||||
# Test the model.
|
||||
print("Testing...")
|
||||
evaluate(model, dataset, device)
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
This example shows how to create a GraphBolt dataloader to sample and train a
|
||||
node classification model with the Cora dataset.
|
||||
"""
|
||||
import dgl.graphbolt as gb
|
||||
import dgl.nn as dglnn
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torchmetrics.functional as MF
|
||||
|
||||
|
||||
############################################################################
|
||||
# (HIGHLIGHT) Create a single process dataloader with dgl graphbolt package.
|
||||
############################################################################
|
||||
def create_dataloader(dataset, itemset, device):
|
||||
# Sample seed nodes from the itemset.
|
||||
datapipe = gb.ItemSampler(itemset, batch_size=16)
|
||||
|
||||
# Copy the mini-batch to the designated device for sampling and training.
|
||||
datapipe = datapipe.copy_to(device)
|
||||
|
||||
# Sample neighbors for the seed nodes.
|
||||
datapipe = datapipe.sample_neighbor(dataset.graph, fanouts=[4, 2])
|
||||
|
||||
# Fetch features for sampled nodes.
|
||||
datapipe = datapipe.fetch_feature(
|
||||
dataset.feature, node_feature_keys=["feat"]
|
||||
)
|
||||
|
||||
# Initiate the dataloader for the datapipe.
|
||||
return gb.DataLoader(datapipe)
|
||||
|
||||
|
||||
class GCN(nn.Module):
|
||||
def __init__(self, in_size, out_size, hidden_size=16):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList()
|
||||
self.layers.append(dglnn.GraphConv(in_size, hidden_size))
|
||||
self.layers.append(dglnn.GraphConv(hidden_size, out_size))
|
||||
|
||||
def forward(self, blocks, x):
|
||||
hidden_x = x
|
||||
for layer_idx, (layer, block) in enumerate(zip(self.layers, blocks)):
|
||||
hidden_x = layer(block, hidden_x)
|
||||
is_last_layer = layer_idx == len(self.layers) - 1
|
||||
if not is_last_layer:
|
||||
hidden_x = F.relu(hidden_x)
|
||||
return hidden_x
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def evaluate(model, dataset, itemset, device):
|
||||
model.eval()
|
||||
y = []
|
||||
y_hats = []
|
||||
dataloader = create_dataloader(dataset, itemset, device)
|
||||
|
||||
for step, data in enumerate(dataloader):
|
||||
x = data.node_features["feat"]
|
||||
y.append(data.labels)
|
||||
y_hats.append(model(data.blocks, x))
|
||||
|
||||
return MF.accuracy(
|
||||
torch.cat(y_hats),
|
||||
torch.cat(y),
|
||||
task="multiclass",
|
||||
num_classes=dataset.tasks[0].metadata["num_classes"],
|
||||
)
|
||||
|
||||
|
||||
def train(model, dataset, device):
|
||||
# The first of two tasks in the dataset is node classification.
|
||||
task = dataset.tasks[0]
|
||||
dataloader = create_dataloader(dataset, task.train_set, device)
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)
|
||||
|
||||
for epoch in range(10):
|
||||
model.train()
|
||||
total_loss = 0
|
||||
########################################################################
|
||||
# (HIGHLIGHT) Iterate over the dataloader and train the model with all
|
||||
# mini-batches.
|
||||
########################################################################
|
||||
for step, data in enumerate(dataloader):
|
||||
# The features of sampled nodes.
|
||||
x = data.node_features["feat"]
|
||||
|
||||
# The ground truth labels of the seed nodes.
|
||||
y = data.labels
|
||||
|
||||
# Forward.
|
||||
y_hat = model(data.blocks, x)
|
||||
|
||||
# Compute loss.
|
||||
loss = F.cross_entropy(y_hat, y)
|
||||
|
||||
# Backward.
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
total_loss += loss.item()
|
||||
|
||||
# Evaluate the model.
|
||||
val_acc = evaluate(model, dataset, task.validation_set, device)
|
||||
test_acc = evaluate(model, dataset, task.test_set, device)
|
||||
print(
|
||||
f"Epoch {epoch:03d} | Loss {total_loss / (step + 1):.3f} | "
|
||||
f"Val Acc {val_acc.item():.3f} | Test Acc {test_acc.item():.3f}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
||||
print(f"Training in {device} mode.")
|
||||
|
||||
# Load and preprocess dataset.
|
||||
print("Loading data...")
|
||||
dataset = gb.BuiltinDataset("cora").load()
|
||||
|
||||
# If a CUDA device is selected, we pin the graph and the features so that
|
||||
# the GPU can access them.
|
||||
if device == torch.device("cuda:0"):
|
||||
dataset.graph.pin_memory_()
|
||||
dataset.feature.pin_memory_()
|
||||
|
||||
in_size = dataset.feature.size("node", None, "feat")[0]
|
||||
out_size = dataset.tasks[0].metadata["num_classes"]
|
||||
model = GCN(in_size, out_size).to(device)
|
||||
|
||||
# Model training.
|
||||
print("Training...")
|
||||
train(model, dataset, device)
|
||||
Reference in New Issue
Block a user