Semi-supervised learning trains with a small labeled subset and uses the resulting model to predict labels for unlabeled examples. In a graph, this supports classification when most nodes lack labels. section 2.3
A Political Books example retains 20% of book labels and predicts political orientation for the rest. Nodes represent books, edges represent co-purchases, and labels are grouped as right, left, or neutral. section 2.3 section 2.3.1
A practical workflow separates known labels from hidden ones, then trains using only the known subset:
- Encode node labels and randomly select 20% of node indices as labeled; Boolean masks mark labeled and unlabeled nodes. section 2.3.1
- Filter embeddings and labels with the labeled mask. Keep each embedding aligned with its node and label. section 2.3.1
- Train a random forest on embeddings as tabular features, or train a GCN using graph structure and node features. In the GNN loop, mask outputs before computing loss so only labeled nodes contribute. section 2.3 section 2.3.3
Listing 2.5 Preprocessing for semi-supervised problem
labels = []
for node, data in gml_graph.nodes(data=True): #1
if data['value'] == 'c':
labels.append('right')
elif data['value'] == 'l':
labels.append('left')
else:
labels.append('neutral')
labels = np.array(labels)
random.seed(52) #2
indices = list(range(len(labels))) #3
labelled_percentage = 0.2 #4
labelled_indices = random.sample(indices, \
int(labelled_percentage * len(labels))) #5
labelled_mask = np.zeros(len(labels), dtype=bool) #6
unlabelled_mask = np.ones(len(labels), dtype=bool)
labelled_mask[labelled_indices] = True #7
unlabelled_mask[labelled_indices] = False
labelled_labels = labels[labelled_mask] #8
unlabelled_labels = labels[unlabelled_mask]
label_mapping = {'left': 0, 'right': 1, 'neutral': 2} #9
numeric_labels = np.array([label_mapping[label] for label in labels])
See the preprocessing code for label encoding, random selection, and creation of labeled and unlabeled masks. section 2.3.1
Listing 2.9 Training loop
for epoch in range(3000): #1
optimizer.zero_grad()
_, out = model(data.x, data.edge_index) #2
out_masked = out[data.train_mask] #3
loss = loss_fn(out_masked, train_labels) #4
loss.backward()
optimizer.step()
if epoch % 10 == 0: #5
print(f'Epoch {epoch}, Log Loss: {loss.item()}')
See the training loop applying a mask to model outputs before calculating loss and updating parameters. section 2.3.3




