chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:35:51 +08:00
commit c36a561cd8
2172 changed files with 455595 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
# DGL Implementation of the Node2vec
This DGL example implements the graph embedding model proposed in the paper
[node2vec: Scalable Feature Learning for Networks](https://arxiv.org/abs/1607.00653)
The author's codes of implementation is in [Node2vec](https://github.com/aditya-grover/node2vec)
Example implementor
----------------------
This example was implemented by [Smile](https://github.com/Smilexuhc) during his intern work at the AWS Shanghai AI Lab.
The graph dataset used in this example
---------------------------------------
cora
- NumNodes: 2708
- NumEdges: 10556
ogbn-products
- NumNodes: 2449029
- NumEdges: 61859140
Dependencies
--------------------------------
- python 3.6+
- Pytorch 1.5.0+
- ogb
How to run example files
--------------------------------
To train a node2vec model:
```shell script
python main.py --task="train"
```
To time node2vec random walks:
```shell script
python main.py --task="time" --runs=10
```
Performance
-------------------------
**Setting:** `walk_length=50, p=0.25, q=4.0`
| Dataset | DGL | PyG |
| -------- | :---------: | :---------: |
| cora | 0.0092s | 0.0179s |
| products | 66.22s | 77.65s |
Note that the number in table are the average results of multiple trials.
For cora, we run 50 trials. For ogbn-products, we run 10 trials.
+62
View File
@@ -0,0 +1,62 @@
import time
from dgl.sampling import node2vec_random_walk
from model import Node2vecModel
from utils import load_graph, parse_arguments
def time_randomwalk(graph, args):
"""
Test cost time of random walk
"""
start_time = time.time()
# default setting for testing
params = {"p": 0.25, "q": 4, "walk_length": 50}
for i in range(args.runs):
node2vec_random_walk(graph, graph.nodes(), **params)
end_time = time.time()
cost_time_avg = (end_time - start_time) / args.runs
print(
"Run dataset {} {} trials, mean run time: {:.3f}s".format(
args.dataset, args.runs, cost_time_avg
)
)
def train_node2vec(graph, eval_set, args):
"""
Train node2vec model
"""
trainer = Node2vecModel(
graph,
embedding_dim=args.embedding_dim,
walk_length=args.walk_length,
p=args.p,
q=args.q,
num_walks=args.num_walks,
eval_set=eval_set,
eval_steps=1,
device=args.device,
)
trainer.train(
epochs=args.epochs, batch_size=args.batch_size, learning_rate=0.01
)
if __name__ == "__main__":
args = parse_arguments()
graph, eval_set = load_graph(args.dataset)
if args.task == "train":
print("Perform training node2vec model")
train_node2vec(graph, eval_set, args)
elif args.task == "time":
print("Timing random walks")
time_randomwalk(graph, args)
else:
raise ValueError("Task type error!")
+351
View File
@@ -0,0 +1,351 @@
import torch
import torch.nn as nn
from dgl.sampling import node2vec_random_walk
from sklearn.linear_model import LogisticRegression
from torch.utils.data import DataLoader
class Node2vec(nn.Module):
"""Node2vec model from paper node2vec: Scalable Feature Learning for Networks <https://arxiv.org/abs/1607.00653>
Attributes
----------
g: DGLGraph
The graph.
embedding_dim: int
Dimension of node embedding.
walk_length: int
Length of each trace.
p: float
Likelihood of immediately revisiting a node in the walk. Same notation as in the paper.
q: float
Control parameter to interpolate between breadth-first strategy and depth-first strategy.
Same notation as in the paper.
num_walks: int
Number of random walks for each node. Default: 10.
window_size: int
Maximum distance between the center node and predicted node. Default: 5.
num_negatives: int
The number of negative samples for each positive sample. Default: 5.
use_sparse: bool
If set to True, use PyTorch's sparse embedding and optimizer. Default: ``True``.
weight_name : str, optional
The name of the edge feature tensor on the graph storing the (unnormalized)
probabilities associated with each edge for choosing the next node.
The feature tensor must be non-negative and the sum of the probabilities
must be positive for the outbound edges of all nodes (although they don't have
to sum up to one). The result will be undefined otherwise.
If omitted, DGL assumes that the neighbors are picked uniformly.
"""
def __init__(
self,
g,
embedding_dim,
walk_length,
p,
q,
num_walks=10,
window_size=5,
num_negatives=5,
use_sparse=True,
weight_name=None,
):
super(Node2vec, self).__init__()
assert walk_length >= window_size
self.g = g
self.embedding_dim = embedding_dim
self.walk_length = walk_length
self.p = p
self.q = q
self.num_walks = num_walks
self.window_size = window_size
self.num_negatives = num_negatives
self.N = self.g.num_nodes()
if weight_name is not None:
self.prob = weight_name
else:
self.prob = None
self.embedding = nn.Embedding(self.N, embedding_dim, sparse=use_sparse)
def reset_parameters(self):
self.embedding.reset_parameters()
def sample(self, batch):
"""
Generate positive and negative samples.
Positive samples are generated from random walk
Negative samples are generated from random sampling
"""
if not isinstance(batch, torch.Tensor):
batch = torch.tensor(batch)
batch = batch.repeat(self.num_walks)
# positive
pos_traces = node2vec_random_walk(
self.g, batch, self.p, self.q, self.walk_length, self.prob
)
pos_traces = pos_traces.unfold(1, self.window_size, 1) # rolling window
pos_traces = pos_traces.contiguous().view(-1, self.window_size)
# negative
neg_batch = batch.repeat(self.num_negatives)
neg_traces = torch.randint(
self.N, (neg_batch.size(0), self.walk_length)
)
neg_traces = torch.cat([neg_batch.view(-1, 1), neg_traces], dim=-1)
neg_traces = neg_traces.unfold(1, self.window_size, 1) # rolling window
neg_traces = neg_traces.contiguous().view(-1, self.window_size)
return pos_traces, neg_traces
def forward(self, nodes=None):
"""
Returns the embeddings of the input nodes
Parameters
----------
nodes: Tensor, optional
Input nodes, if set `None`, will return all the node embedding.
Returns
-------
Tensor
Node embedding
"""
emb = self.embedding.weight
if nodes is None:
return emb
else:
return emb[nodes]
def loss(self, pos_trace, neg_trace):
"""
Computes the loss given positive and negative random walks.
Parameters
----------
pos_trace: Tensor
positive random walk trace
neg_trace: Tensor
negative random walk trace
"""
e = 1e-15
# Positive
pos_start, pos_rest = (
pos_trace[:, 0],
pos_trace[:, 1:].contiguous(),
) # start node and following trace
w_start = self.embedding(pos_start).unsqueeze(dim=1)
w_rest = self.embedding(pos_rest)
pos_out = (w_start * w_rest).sum(dim=-1).view(-1)
# Negative
neg_start, neg_rest = neg_trace[:, 0], neg_trace[:, 1:].contiguous()
w_start = self.embedding(neg_start).unsqueeze(dim=1)
w_rest = self.embedding(neg_rest)
neg_out = (w_start * w_rest).sum(dim=-1).view(-1)
# compute loss
pos_loss = -torch.log(torch.sigmoid(pos_out) + e).mean()
neg_loss = -torch.log(1 - torch.sigmoid(neg_out) + e).mean()
return pos_loss + neg_loss
def loader(self, batch_size):
"""
Parameters
----------
batch_size: int
batch size
Returns
-------
DataLoader
Node2vec training data loader
"""
return DataLoader(
torch.arange(self.N),
batch_size=batch_size,
shuffle=True,
collate_fn=self.sample,
)
@torch.no_grad()
def evaluate(self, x_train, y_train, x_val, y_val):
"""
Evaluate the quality of embedding vector via a downstream classification task with logistic regression.
"""
x_train = self.forward(x_train)
x_val = self.forward(x_val)
x_train, y_train = x_train.cpu().numpy(), y_train.cpu().numpy()
x_val, y_val = x_val.cpu().numpy(), y_val.cpu().numpy()
lr = LogisticRegression(
solver="lbfgs", multi_class="auto", max_iter=150
).fit(x_train, y_train)
return lr.score(x_val, y_val)
class Node2vecModel(object):
"""
Wrapper of the ``Node2Vec`` class with a ``train`` method.
Attributes
----------
g: DGLGraph
The graph.
embedding_dim: int
Dimension of node embedding.
walk_length: int
Length of each trace.
p: float
Likelihood of immediately revisiting a node in the walk.
q: float
Control parameter to interpolate between breadth-first strategy and depth-first strategy.
num_walks: int
Number of random walks for each node. Default: 10.
window_size: int
Maximum distance between the center node and predicted node. Default: 5.
num_negatives: int
The number of negative samples for each positive sample. Default: 5.
use_sparse: bool
If set to True, uses PyTorch's sparse embedding and optimizer. Default: ``True``.
weight_name : str, optional
The name of the edge feature tensor on the graph storing the (unnormalized)
probabilities associated with each edge for choosing the next node.
The feature tensor must be non-negative and the sum of the probabilities
must be positive for the outbound edges of all nodes (although they don't have
to sum up to one). The result will be undefined otherwise.
If omitted, DGL assumes that the neighbors are picked uniformly. Default: ``None``.
eval_set: list of tuples (Tensor, Tensor)
[(nodes_train,y_train),(nodes_val,y_val)]
If omitted, model will not be evaluated. Default: ``None``.
eval_steps: int
Interval steps of evaluation.
if set <= 0, model will not be evaluated. Default: ``None``.
device: str
device, default 'cpu'.
"""
def __init__(
self,
g,
embedding_dim,
walk_length,
p=1.0,
q=1.0,
num_walks=1,
window_size=5,
num_negatives=5,
use_sparse=True,
weight_name=None,
eval_set=None,
eval_steps=-1,
device="cpu",
):
self.model = Node2vec(
g,
embedding_dim,
walk_length,
p,
q,
num_walks,
window_size,
num_negatives,
use_sparse,
weight_name,
)
self.g = g
self.use_sparse = use_sparse
self.eval_steps = eval_steps
self.eval_set = eval_set
if device == "cpu":
self.device = device
else:
self.device = "cuda" if torch.cuda.is_available() else "cpu"
def _train_step(self, model, loader, optimizer, device):
model.train()
total_loss = 0
for pos_traces, neg_traces in loader:
pos_traces, neg_traces = pos_traces.to(device), neg_traces.to(
device
)
optimizer.zero_grad()
loss = model.loss(pos_traces, neg_traces)
loss.backward()
optimizer.step()
total_loss += loss.item()
return total_loss / len(loader)
@torch.no_grad()
def _evaluate_step(self):
nodes_train, y_train = self.eval_set[0]
nodes_val, y_val = self.eval_set[1]
acc = self.model.evaluate(nodes_train, y_train, nodes_val, y_val)
return acc
def train(self, epochs, batch_size, learning_rate=0.01):
"""
Parameters
----------
epochs: int
num of train epoch
batch_size: int
batch size
learning_rate: float
learning rate. Default 0.01.
"""
self.model = self.model.to(self.device)
loader = self.model.loader(batch_size)
if self.use_sparse:
optimizer = torch.optim.SparseAdam(
list(self.model.parameters()), lr=learning_rate
)
else:
optimizer = torch.optim.Adam(
self.model.parameters(), lr=learning_rate
)
for i in range(epochs):
loss = self._train_step(self.model, loader, optimizer, self.device)
if self.eval_steps > 0:
if epochs % self.eval_steps == 0:
acc = self._evaluate_step()
print(
"Epoch: {}, Train Loss: {:.4f}, Val Acc: {:.4f}".format(
i, loss, acc
)
)
def embedding(self, nodes=None):
"""
Returns the embeddings of the input nodes
Parameters
----------
nodes: Tensor, optional
Input nodes, if set `None`, will return all the node embedding.
Returns
-------
Tensor
Node embedding.
"""
return self.model(nodes)
+64
View File
@@ -0,0 +1,64 @@
import argparse
from ogb.linkproppred import *
from ogb.nodeproppred import *
from dgl.data import CitationGraphDataset
def load_graph(name):
cite_graphs = ["cora", "citeseer", "pubmed"]
if name in cite_graphs:
dataset = CitationGraphDataset(name)
graph = dataset[0]
nodes = graph.nodes()
y = graph.ndata["label"]
train_mask = graph.ndata["train_mask"]
val_mask = graph.ndata["test_mask"]
nodes_train, y_train = nodes[train_mask], y[train_mask]
nodes_val, y_val = nodes[val_mask], y[val_mask]
eval_set = [(nodes_train, y_train), (nodes_val, y_val)]
elif name.startswith("ogbn"):
dataset = DglNodePropPredDataset(name)
graph, y = dataset[0]
split_nodes = dataset.get_idx_split()
nodes = graph.nodes()
train_idx = split_nodes["train"]
val_idx = split_nodes["valid"]
nodes_train, y_train = nodes[train_idx], y[train_idx]
nodes_val, y_val = nodes[val_idx], y[val_idx]
eval_set = [(nodes_train, y_train), (nodes_val, y_val)]
else:
raise ValueError("Dataset name error!")
return graph, eval_set
def parse_arguments():
"""
Parse arguments
"""
parser = argparse.ArgumentParser(description="Node2vec")
parser.add_argument("--dataset", type=str, default="cora")
# 'train' for training node2vec model, 'time' for testing speed of random walk
parser.add_argument("--task", type=str, default="train")
parser.add_argument("--runs", type=int, default=10)
parser.add_argument("--device", type=str, default="cpu")
parser.add_argument("--embedding_dim", type=int, default=128)
parser.add_argument("--walk_length", type=int, default=50)
parser.add_argument("--p", type=float, default=0.25)
parser.add_argument("--q", type=float, default=4.0)
parser.add_argument("--num_walks", type=int, default=10)
parser.add_argument("--epochs", type=int, default=100)
parser.add_argument("--batch_size", type=int, default=128)
args = parser.parse_args()
return args