本节摘要:节点级任务输出每个节点的定性(分类)、定量(回归)或分组(聚类)结论。本节覆盖半监督训练的掩码机制、类别不平衡的处置、无标签图聚类的双路线,并在引文网络上完成从数据到评估的完整闭环,回答"为什么极少量标签能带动全图表示"这个半监督核心问题。
风控工位的日常画面是这样的:账号关系网铺满整面屏幕,红色标签零星闪烁——那是已被人工确认的黑产账号,其余全部无标注。工单要求给全网账号输出风险等级。这就是节点级分类的标准现场:图是全量可见的,标签是极度稀缺的,任务是为每个节点给出结论。承接第三章的装备与第二章的表示,本节把"节点定性"的完整流程走通:先讲清半监督训练为何在图上格外有效,再处理类别不平衡与无标签两类变体,最后完整训练并审视决策边界。节点回归(预测连续值,如路段流量)与节点聚类(无标签分团)共用同一套表示底座,差异只在损失函数。
图上半监督训练的形式出奇简单:模型对全图做前向,但损失只计算带标签节点——训练掩码是唯一的开关。它之所以有效,秘密在消息传递的"标签扩散"效应:带标签节点作为强信号源,其表示经层层聚合渗入邻居、邻居的邻居;反向传播时,梯度同样沿边回传,无标注节点的表示被间接塑形。这等价于让结构先验(同社区的节点倾向同类)参与学习,而无需显式的标签传播算法。类别不平衡是这个现场的常规状态——黑产账号占比可能只有百分之一的量级——若不处置,模型会把全部账号判为正常也能拿到很高的准确率。处置手段按代价排序:损失加权(少数类样本的梯度乘以更大系数)最轻;焦点损失把权重动态集中在难样本上;过采样少数类节点或图结构的重加权更重;评估侧则必须放弃裸准确率,改用受试者曲线下面积、召回率与精确率的调和均值等抗不平衡指标(第七章系统盘点)。
import torch import torch.nn.functional as F from torch_geometric.datasets import Planetoid from torch_geometric.nn import GCNConv data = Planetoid(root="/tmp/Cora", name="Cora")[0] class Net(torch.nn.Module): def __init__(self): super().__init__() self.c1 = GCNConv(data.num_features, 16) self.c2 = GCNConv(16, data.num_classes) def forward(self): return self.c2(self.c1(data.x, data.edge_index).relu(), data.edge_index) model, opt = Net(), torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) for epoch in range(200): opt.zero_grad() # 半监督核心:前向用全图,损失只用训练掩码内的节点 loss = F.cross_entropy(model()[data.train_mask], data.y[data.train_mask]) loss.backward(); opt.step() out = model() for name, mask in [("train", data.train_mask), ("val", data.val_mask), ("test", data.test_mask)]: acc = (out.argmax(1)[mask] == data.y[mask]).float().mean() print(f"{name} acc: {acc:.4f}") # 训练掩码内只有每类极少样本,测试精度却能到八成以上—— # 结构先验通过消息传递把稀缺标签"扩散"成了全图知识
这段训练值得盯住的就是那一行掩码索引:前向传播吃的是整张图(结构与全部特征),监督信号只有掩码内的涓涓细流。对比表格数据的分类器(没有结构可用,同样标签量下精度会惨不忍睹),半监督红利完全来自"边把同类节点织在一起"这个先验。
把损失加权落到代码上,并验证评估指标的变化。
# 模拟不平衡:把训练掩码里的多数类保留、少数类大量丢弃 import torch y = data.y.clone() train_idx = data.train_mask.nonzero().flatten() # 假设类别 0 为多数类,构造不平衡版本 keep = [i for i in train_idx.tolist() if y[i] == 0 or torch.rand(1) < 0.15] imbal_mask = torch.zeros_like(data.train_mask); imbal_mask[keep] = True # 类别权重:与类频成反比 counts = torch.bincount(y[imbal_mask], minlength=data.num_classes).float() weights = counts.sum() / (counts + 1e-6) / data.num_classes opt = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) for epoch in range(100): opt.zero_grad() loss = F.cross_entropy(model()[imbal_mask], y[imbal_mask], weight=weights) # 加权损失 loss.backward(); opt.step() from sklearn.metrics import f1_score pred = model().argmax(1)[data.test_mask].numpy() true = y[data.test_mask].numpy() print("宏平均 F1(抗不平衡指标):", round(f1_score(true, pred, average="macro"), 4)) # 对比不加权版本:多数类吸走梯度,宏平均 F1 明显偏低——加权把话语权还给少数类
标签完全没有时,节点定性退化为图聚类。路线一是"先嵌入再聚类":用图神经网络的自监督训练(第六章的对比学习方法)产出节点表示,再跑常规聚类算法——灵活通用,是当前主流。路线二是"图结构直接聚类":谱聚类把拉普拉斯特征向量当嵌入用(第二章的谱域知识再就业),或用模块度优化类方法直接在图上找社区——无需特征、可解释,适合纯结构网络。两条路线的共同评估难题是没有标签可对照,常用模块度、轮廓系数或人工抽检做代理指标。业务上"团伙发现"多为聚类任务:没人知道有几个团伙、谁属于哪个团伙,聚类结果本身就是要交付的情报。
# 路线一演示:谱嵌入 + KMeans(sklearn 风格伪代码,逻辑完整可跑) import numpy as np from sklearn.cluster import KMeans # 用第二章的拉普拉斯特征向量作为嵌入(取最小的几个非零特征值对应向量) from torch_geometric.utils import to_dense_adj, degree A = to_dense_adj(data.edge_index)[0].numpy() L = np.diag(degree(data.edge_index[0]).numpy()) - A vals, vecs = np.linalg.eigh(L) emb = vecs[:, 1:17] # 跳过平凡的第一维 labels = KMeans(n_clusters=7, n_init=10).fit_predict(emb) from sklearn.metrics import adjusted_rand_score print("聚类与真实标签的兰德指数:", round(adjusted_rand_score(data.y.numpy(), labels), 4)) # 无任何标签参与,仅凭谱结构就能恢复大半社区划分——结构证据的独立价值
个体定性之外,业务里更常见的问题是"这两账号之间会不会发生关系"。下一节处理边级结案:关系线索的预测与评估。