281 lines
9.6 KiB
Python
281 lines
9.6 KiB
Python
"""
|
|
This script demonstrates node classification with GraphSAGE on large graphs,
|
|
merging GraphBolt (GB) and PyTorch Geometric (PyG). GraphBolt efficiently
|
|
manages data loading for large datasets, crucial for mini-batch processing.
|
|
Post data loading, PyG's user-friendly framework takes over for training,
|
|
showcasing seamless integration with GraphBolt. This combination offers an
|
|
efficient alternative to traditional Deep Graph Library (DGL) methods,
|
|
highlighting adaptability and scalability in handling large-scale graph data
|
|
for diverse real-world applications.
|
|
|
|
Key Features:
|
|
- Implements the GraphSAGE model, a scalable GNN, for node classification on
|
|
large graphs.
|
|
- Utilizes GraphBolt, an efficient framework for large-scale graph data processing.
|
|
- Integrates with PyTorch Geometric for building and training the GraphSAGE model.
|
|
- The script is well-documented, providing clear explanations at each step.
|
|
|
|
This flowchart describes the main functional sequence of the provided example.
|
|
main:
|
|
|
|
main
|
|
│
|
|
├───> Load and preprocess dataset (GraphBolt)
|
|
│ │
|
|
│ └───> Utilize GraphBolt's BuiltinDataset for dataset handling
|
|
│
|
|
├───> Instantiate the SAGE model (PyTorch Geometric)
|
|
│ │
|
|
│ └───> Define the GraphSAGE model architecture
|
|
│
|
|
├───> Train the model
|
|
│ │
|
|
│ ├───> Mini-Batch Processing with GraphBolt
|
|
│ │ │
|
|
│ │ └───> Efficient handling of mini-batches using GraphBolt's utilities
|
|
│ │
|
|
│ └───> Training Loop
|
|
│ │
|
|
│ ├───> Forward and backward passes
|
|
│ │
|
|
│ ├───> Convert GraphBolt MiniBatch to PyG Data
|
|
│ │
|
|
│ └───> Parameters optimization
|
|
│
|
|
└───> Evaluate the model
|
|
│
|
|
└───> Performance assessment on validation and test datasets
|
|
│
|
|
└───> Accuracy and other relevant metrics calculation
|
|
|
|
|
|
"""
|
|
|
|
import argparse
|
|
|
|
import dgl.graphbolt as gb
|
|
import torch
|
|
import torch.nn.functional as F
|
|
import torchmetrics.functional as MF
|
|
from torch_geometric.nn import SAGEConv
|
|
from tqdm import tqdm
|
|
|
|
|
|
class GraphSAGE(torch.nn.Module):
|
|
#####################################################################
|
|
# (HIGHLIGHT) Define the GraphSAGE model architecture.
|
|
#
|
|
# - This class inherits from `torch.nn.Module`.
|
|
# - Two convolutional layers are created using the SAGEConv class from PyG.
|
|
# - 'in_size', 'hidden_size', 'out_size' are the sizes of
|
|
# the input, hidden, and output features, respectively.
|
|
# - The forward method defines the computation performed at every call.
|
|
# - It's adopted from the official PyG example which can be found at
|
|
# https://github.com/pyg-team/pytorch_geometric/blob/master/examples/ogbn_products_sage.py
|
|
#####################################################################
|
|
def __init__(self, in_size, hidden_size, out_size):
|
|
super(GraphSAGE, self).__init__()
|
|
self.layers = torch.nn.ModuleList()
|
|
self.layers.append(SAGEConv(in_size, hidden_size))
|
|
self.layers.append(SAGEConv(hidden_size, hidden_size))
|
|
self.layers.append(SAGEConv(hidden_size, out_size))
|
|
|
|
def forward(self, x, edge_index):
|
|
for i, layer in enumerate(self.layers):
|
|
x = layer(x, edge_index)
|
|
if i != len(self.layers) - 1:
|
|
x = x.relu()
|
|
x = F.dropout(x, p=0.5, training=self.training)
|
|
return x
|
|
|
|
def inference(self, dataloader, x_all, device):
|
|
"""Conduct layer-wise inference to get all the node embeddings."""
|
|
for i, layer in tqdm(enumerate(self.layers), "inference"):
|
|
xs = []
|
|
for minibatch in dataloader:
|
|
# Call `to_pyg_data` to convert GB Minibatch to PyG Data.
|
|
pyg_data = minibatch.to_pyg_data()
|
|
n_id = pyg_data.n_id.to("cpu")
|
|
x = x_all[n_id].to(device)
|
|
edge_index = pyg_data.edge_index
|
|
x = layer(x, edge_index)
|
|
x = x[: pyg_data.batch_size]
|
|
if i != len(self.layers) - 1:
|
|
x = x.relu()
|
|
xs.append(x.cpu())
|
|
x_all = torch.cat(xs, dim=0)
|
|
return x_all
|
|
|
|
|
|
def create_dataloader(
|
|
dataset_set, graph, feature, batch_size, fanout, device, job
|
|
):
|
|
# Initialize an ItemSampler to sample mini-batches from the dataset.
|
|
datapipe = gb.ItemSampler(
|
|
dataset_set,
|
|
batch_size=batch_size,
|
|
shuffle=(job == "train"),
|
|
drop_last=(job == "train"),
|
|
)
|
|
# Sample neighbors for each node in the mini-batch.
|
|
datapipe = datapipe.sample_neighbor(
|
|
graph, fanout if job != "infer" else [-1]
|
|
)
|
|
# Copy the data to the specified device.
|
|
datapipe = datapipe.copy_to(device=device)
|
|
# Fetch node features for the sampled subgraph.
|
|
datapipe = datapipe.fetch_feature(feature, node_feature_keys=["feat"])
|
|
# Create and return a DataLoader to handle data loading.
|
|
dataloader = gb.DataLoader(datapipe, num_workers=0)
|
|
|
|
return dataloader
|
|
|
|
|
|
def train(model, dataloader, optimizer):
|
|
model.train() # Set the model to training mode
|
|
total_loss = 0 # Accumulator for the total loss
|
|
total_correct = 0 # Accumulator for the total number of correct predictions
|
|
total_samples = 0 # Accumulator for the total number of samples processed
|
|
num_batches = 0 # Counter for the number of mini-batches processed
|
|
|
|
for _, minibatch in tqdm(enumerate(dataloader), "training"):
|
|
#####################################################################
|
|
# (HIGHLIGHT) Convert GraphBolt MiniBatch to PyG Data class.
|
|
#
|
|
# Call `MiniBatch.to_pyg_data()` and it will return a PyG Data class
|
|
# with necessary data and information.
|
|
#####################################################################
|
|
pyg_data = minibatch.to_pyg_data()
|
|
|
|
optimizer.zero_grad()
|
|
out = model(pyg_data.x, pyg_data.edge_index)[: pyg_data.y.shape[0]]
|
|
y = pyg_data.y
|
|
loss = F.cross_entropy(out, y)
|
|
loss.backward()
|
|
optimizer.step()
|
|
|
|
total_loss += float(loss)
|
|
total_correct += int(out.argmax(dim=-1).eq(y).sum())
|
|
total_samples += y.shape[0]
|
|
num_batches += 1
|
|
avg_loss = total_loss / num_batches
|
|
avg_accuracy = total_correct / total_samples
|
|
return avg_loss, avg_accuracy
|
|
|
|
|
|
@torch.no_grad()
|
|
def evaluate(model, dataloader, num_classes):
|
|
model.eval()
|
|
y_hats = []
|
|
ys = []
|
|
for _, minibatch in tqdm(enumerate(dataloader), "evaluating"):
|
|
pyg_data = minibatch.to_pyg_data()
|
|
out = model(pyg_data.x, pyg_data.edge_index)[: pyg_data.y.shape[0]]
|
|
y = pyg_data.y
|
|
y_hats.append(out)
|
|
ys.append(y)
|
|
|
|
return MF.accuracy(
|
|
torch.cat(y_hats),
|
|
torch.cat(ys),
|
|
task="multiclass",
|
|
num_classes=num_classes,
|
|
)
|
|
|
|
|
|
@torch.no_grad()
|
|
def layerwise_infer(
|
|
model, infer_dataloader, test_set, feature, num_classes, device
|
|
):
|
|
model.eval()
|
|
features = feature.read("node", None, "feat")
|
|
pred = model.inference(infer_dataloader, features, device)
|
|
pred = pred[test_set._items[0]]
|
|
label = test_set._items[1].to(pred.device)
|
|
|
|
return MF.accuracy(
|
|
pred,
|
|
label,
|
|
task="multiclass",
|
|
num_classes=num_classes,
|
|
)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Which dataset are you going to use?"
|
|
)
|
|
parser.add_argument(
|
|
"--dataset",
|
|
type=str,
|
|
default="ogbn-products",
|
|
help='Name of the dataset to use (e.g., "ogbn-products", "ogbn-arxiv")',
|
|
)
|
|
parser.add_argument(
|
|
"--epochs", type=int, default=10, help="Number of training epochs."
|
|
)
|
|
parser.add_argument(
|
|
"--batch-size", type=int, default=1024, help="Batch size for training."
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
dataset_name = args.dataset
|
|
dataset = gb.BuiltinDataset(dataset_name).load()
|
|
graph = dataset.graph
|
|
feature = dataset.feature.pin_memory_()
|
|
train_set = dataset.tasks[0].train_set
|
|
valid_set = dataset.tasks[0].validation_set
|
|
test_set = dataset.tasks[0].test_set
|
|
all_nodes_set = dataset.all_nodes_set
|
|
num_classes = dataset.tasks[0].metadata["num_classes"]
|
|
|
|
train_dataloader = create_dataloader(
|
|
train_set,
|
|
graph,
|
|
feature,
|
|
args.batch_size,
|
|
[5, 10, 15],
|
|
device,
|
|
job="train",
|
|
)
|
|
valid_dataloader = create_dataloader(
|
|
valid_set,
|
|
graph,
|
|
feature,
|
|
args.batch_size,
|
|
[5, 10, 15],
|
|
device,
|
|
job="evaluate",
|
|
)
|
|
infer_dataloader = create_dataloader(
|
|
all_nodes_set,
|
|
graph,
|
|
feature,
|
|
4 * args.batch_size,
|
|
[-1],
|
|
device,
|
|
job="infer",
|
|
)
|
|
in_channels = feature.size("node", None, "feat")[0]
|
|
hidden_channels = 256
|
|
model = GraphSAGE(in_channels, hidden_channels, num_classes).to(device)
|
|
optimizer = torch.optim.Adam(model.parameters(), lr=0.003)
|
|
for epoch in range(args.epochs):
|
|
train_loss, train_accuracy = train(model, train_dataloader, optimizer)
|
|
|
|
valid_accuracy = evaluate(model, valid_dataloader, num_classes)
|
|
print(
|
|
f"Epoch {epoch}, Train Loss: {train_loss:.4f}, "
|
|
f"Train Accuracy: {train_accuracy:.4f}, "
|
|
f"Valid Accuracy: {valid_accuracy:.4f}"
|
|
)
|
|
test_accuracy = layerwise_infer(
|
|
model, infer_dataloader, test_set, feature, num_classes, device
|
|
)
|
|
print(f"Test Accuracy: {test_accuracy:.4f}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|