Class reshuffling balances graph-training batches by redistributing majority-class nodes rather than generating synthetic data. It suits imbalanced node classification when minority detection matters and SMOTE does not improve performance. section 4.4.2
A balanced sampler follows this pipeline:
- Separate majority and minority node indices. section 4.4.2
- Choose equal class counts, using the smaller class by default or half the requested batch size. section 4.4.2
- Randomly sample both classes, combine their indices, extract the corresponding subgraph, and re-index nodes consistently. section 4.4.2
Fraud-review node classification
For 100 nodes containing 76 majority-class and 24 minority-class examples, reshuffling creates training batches with equal class portions. A GATv2 model using this approach achieved mean validation F1 0.809, precision 0.878, recall 0.781, and AUC 0.914. section 4.4.2
Figure 4.12 Illustration of the reshuffling method using an example of 100 data points, with 76 in the majority class and 24 in the minority class. When creating batches for training, each batch is made to contain equal portions of the majority and minority classes.
Visualizes how unequal class totals become equal-class training batches. section 4.4.2
Listing 4.9 BalancedNodeSampler class
class BalancedNodeSampler(BaseSampler):
def __init__(self, data, num_samples=None):
super().__init__()
self.data = data
self.num_samples = num_samples #1
def sample_from_nodes(self, index, **kwargs):
majority_indices = torch.\
where(self.data.y == 0)[0] #2
minority_indices = torch.\
where(self.data.y == 1)[0] #3
if self.num_samples is None:
batch_size = min(len(majority_indices),\
len(minority_indices)) #4
else:
batch_size = self.num_samples // 2
majority_sample = majority_indices[torch.randperm\
(len(majority_indices))[:batch_size]] #5
minority_sample = minority_indices[torch.randint\
(len(minority_indices), (batch_size,))]
batch_indices = torch.cat\
((majority_sample, minority_sample)) #6
mask = torch.zeros(self.data.num_nodes, dtype=torch.bool)
mask[batch_indices] = True #7
row, col = self.data.edge_index
mask_edges = mask[row] & mask[col] #8
sub_row = row[mask_edges]
sub_col = col[mask_edges]
new_index = torch.full((self.data.num_nodes,), -1, dtype=torch.long)
new_index[batch_indices] = \
torch.arange(batch_indices.size(0)) #9
sub_row = new_index[sub_row]
sub_col = new_index[sub_col]
return SamplerOutput(
node=batch_indices,
row=sub_row,
col=sub_col,
edge=None,
num_sampled_nodes=[len(batch_indices)],
metadata=(batch_indices, None)
)
Shows the PyTorch sampler structure for selecting and combining majority- and minority-class nodes. section 4.4.2
