Files
2026-07-13 13:35:51 +08:00

209 lines
8.2 KiB
Python

# referenced the following implementation: https://github.com/BarclayII/dgl/blob/ladies/examples/pytorch/ladies/ladies2.py
import dgl
import dgl.function as fn
import torch
def find_indices_in(a, b):
b_sorted, indices = torch.sort(b)
sorted_indices = torch.searchsorted(b_sorted, a)
sorted_indices[sorted_indices >= indices.shape[0]] = 0
return indices[sorted_indices]
def union(*arrays):
return torch.unique(torch.cat(arrays))
def normalized_edata(g, weight=None):
with g.local_scope():
if weight is None:
weight = "W"
g.edata[weight] = torch.ones(g.number_of_edges(), device=g.device)
g.update_all(fn.copy_e(weight, weight), fn.sum(weight, "v"))
g.apply_edges(lambda edges: {"w": 1 / edges.dst["v"]})
return g.edata["w"]
class LadiesSampler(dgl.dataloading.BlockSampler):
def __init__(
self,
nodes_per_layer,
importance_sampling=True,
weight="w",
out_weight="edge_weights",
replace=False,
):
super().__init__()
self.nodes_per_layer = nodes_per_layer
self.importance_sampling = importance_sampling
self.edge_weight = weight
self.output_weight = out_weight
self.replace = replace
def compute_prob(self, g, seed_nodes, weight, num):
"""
g : the whole graph
seed_nodes : the output nodes for the current layer
weight : the weight of the edges
return : the unnormalized probability of the candidate nodes, as well as the subgraph
containing all the edges from the candidate nodes to the output nodes.
"""
insg = dgl.in_subgraph(g, seed_nodes)
insg = dgl.compact_graphs(insg, seed_nodes)
if self.importance_sampling:
out_frontier = dgl.reverse(insg, copy_edata=True)
weight = weight[out_frontier.edata[dgl.EID].long()]
prob = dgl.ops.copy_e_sum(out_frontier, weight**2)
# prob = torch.sqrt(prob)
else:
prob = torch.ones(insg.num_nodes())
prob[insg.out_degrees() == 0] = 0
return prob, insg
def select_neighbors(self, prob, num):
"""
seed_nodes : output nodes
cand_nodes : candidate nodes. Must contain all output nodes in @seed_nodes
prob : unnormalized probability of each candidate node
num : number of neighbors to sample
return : the set of input nodes in terms of their indices in @cand_nodes, and also the indices of
seed nodes in the selected nodes.
"""
# The returned nodes should be a union of seed_nodes plus @num nodes from cand_nodes.
# Because compute_prob returns a compacted subgraph and a list of probabilities,
# we need to find the corresponding local IDs of the resulting union in the subgraph
# so that we can compute the edge weights of the block.
# This is why we need a find_indices_in() function.
neighbor_nodes_idx = torch.multinomial(
prob, min(num, prob.shape[0]), replacement=self.replace
)
return neighbor_nodes_idx
def generate_block(self, insg, neighbor_nodes_idx, seed_nodes, P_sg, W_sg):
"""
insg : the subgraph yielded by compute_prob()
neighbor_nodes_idx : the sampled nodes from the subgraph @insg, yielded by select_neighbors()
seed_nodes_local_idx : the indices of seed nodes in the selected neighbor nodes, also yielded
by select_neighbors()
P_sg : unnormalized probability of each node being sampled, yielded by compute_prob()
W_sg : edge weights of @insg
return : the block.
"""
seed_nodes_idx = find_indices_in(seed_nodes, insg.ndata[dgl.NID])
u_nodes = union(neighbor_nodes_idx, seed_nodes_idx)
sg = insg.subgraph(u_nodes.type(insg.idtype))
u, v = sg.edges()
lu = sg.ndata[dgl.NID][u.long()]
s = find_indices_in(lu, neighbor_nodes_idx)
eg = dgl.edge_subgraph(
sg, lu == neighbor_nodes_idx[s], relabel_nodes=False
)
eg.ndata[dgl.NID] = sg.ndata[dgl.NID][: eg.num_nodes()]
eg.edata[dgl.EID] = sg.edata[dgl.EID][eg.edata[dgl.EID].long()]
sg = eg
nids = insg.ndata[dgl.NID][sg.ndata[dgl.NID].long()]
P = P_sg[u_nodes.long()]
W = W_sg[sg.edata[dgl.EID].long()]
W_tilde = dgl.ops.e_div_u(sg, W, P)
W_tilde_sum = dgl.ops.copy_e_sum(sg, W_tilde)
d = sg.in_degrees()
W_tilde = dgl.ops.e_mul_v(sg, W_tilde, d / W_tilde_sum)
block = dgl.to_block(sg, seed_nodes_idx.type(sg.idtype))
block.edata[self.output_weight] = W_tilde
# correct node ID mapping
block.srcdata[dgl.NID] = nids[block.srcdata[dgl.NID].long()]
block.dstdata[dgl.NID] = nids[block.dstdata[dgl.NID].long()]
sg_eids = insg.edata[dgl.EID][sg.edata[dgl.EID].long()]
block.edata[dgl.EID] = sg_eids[block.edata[dgl.EID].long()]
return block
def sample_blocks(self, g, seed_nodes, exclude_eids=None):
output_nodes = seed_nodes
blocks = []
for block_id in reversed(range(len(self.nodes_per_layer))):
num_nodes_to_sample = self.nodes_per_layer[block_id]
W = g.edata[self.edge_weight]
prob, insg = self.compute_prob(
g, seed_nodes, W, num_nodes_to_sample
)
neighbor_nodes_idx = self.select_neighbors(
prob, num_nodes_to_sample
)
block = self.generate_block(
insg,
neighbor_nodes_idx.type(g.idtype),
seed_nodes.type(g.idtype),
prob,
W[insg.edata[dgl.EID].long()],
)
seed_nodes = block.srcdata[dgl.NID]
blocks.insert(0, block)
return seed_nodes, output_nodes, blocks
class PoissonLadiesSampler(LadiesSampler):
def __init__(
self,
nodes_per_layer,
importance_sampling=True,
weight="w",
out_weight="edge_weights",
skip=False,
):
super().__init__(
nodes_per_layer, importance_sampling, weight, out_weight
)
self.eps = 0.9999
self.skip = skip
def compute_prob(self, g, seed_nodes, weight, num):
"""
g : the whole graph
seed_nodes : the output nodes for the current layer
weight : the weight of the edges
return : the unnormalized probability of the candidate nodes, as well as the subgraph
containing all the edges from the candidate nodes to the output nodes.
"""
prob, insg = super().compute_prob(g, seed_nodes, weight, num)
one = torch.ones_like(prob)
if prob.shape[0] <= num:
return one, insg
c = 1.0
for i in range(50):
S = torch.sum(torch.minimum(prob * c, one).to(torch.float64)).item()
if min(S, num) / max(S, num) >= self.eps:
break
else:
c *= num / S
if self.skip:
skip_nodes = find_indices_in(seed_nodes, insg.ndata[dgl.NID])
prob[skip_nodes] = float("inf")
return torch.minimum(prob * c, one), insg
def select_neighbors(self, prob, num):
"""
seed_nodes : output nodes
cand_nodes : candidate nodes. Must contain all output nodes in @seed_nodes
prob : unnormalized probability of each candidate node
num : number of neighbors to sample
return : the set of input nodes in terms of their indices in @cand_nodes, and also the indices of
seed nodes in the selected nodes.
"""
# The returned nodes should be a union of seed_nodes plus @num nodes from cand_nodes.
# Because compute_prob returns a compacted subgraph and a list of probabilities,
# we need to find the corresponding local IDs of the resulting union in the subgraph
# so that we can compute the edge weights of the block.
# This is why we need a find_indices_in() function.
neighbor_nodes_idx = torch.arange(prob.shape[0], device=prob.device)[
torch.bernoulli(prob) == 1
]
return neighbor_nodes_idx