简介:毕设代码包内包含FedGraph-master项目,是一个面向社交网络、知识图谱和推荐系统等场景的图联邦学习系统,适合毕业设计、课程设计以及AI算法学习者参考。系统将图神经网络与联邦学习深度融合,并借助分布式计算和Python工具链,在保护数据隐私的前提下处理大规模图数据,其中还涉及节点特征提取、协同过滤推荐等关键环节。压缩包共149个文件,以Python脚本(.py)、日志(.log)、Shell脚本(.sh)及编译缓存(.pyc)为主,另有少量模型权重(.pt)、索引和配置文件,整体约1.56MB,结构清晰便于拆解学习。目前已有144人学习下载。通过研读项目源码与运行配置,可系统掌握图卷积网络、联邦模型聚合、数据预处理及并行通信等设计思路,也能理解实际AI系统在隐私保护与通信效率方面的权衡,适合准备毕设答辩、课程汇报或进一步从事AI系统开发的读者,也可作为图联邦学习入门与实战的参考样板。
1. 这套毕设代码,先看文件清单再谈算法
拿到图联邦学习系统设计与实现.zip,大部分人第一反应是解压后直接找main.py或train.py,但真正值钱的是ind.cora.*和ind.citeseer.*这一组文件。它们是图神经网络领域最经典的 Planetoid 格式数据,FedGraph 项目选它做联邦学习实验,说明作者的意图不是造轮子,而是在标准 benchmark 上验证联邦机制的有效性。gcn.log和sage.log则是 GCN 与 GraphSAGE 两种基座模型的训练日志,从时间戳、loss 曲线到通信轮次,基本能反推出整个系统的训练策略。这套代码适合谁?正在做联邦学习方向毕设、需要一套能跑通且带标准数据集的基线系统的同学,或者想快速对比 GCN / GraphSAGE 在非 IID 图数据下表现差异的工程师。它解决的痛点是:图数据通常无法直接搬进联邦框架,如何处理稀疏邻接矩阵、如何切分客户端、如何对齐聚合权重,这三点占了整个项目 80% 的工作量。
2. Planetoid 数据格式与图划分:Cora/Citeseer 的非 IID 预处理
2.1 认识 ind.* 文件:不是 txt,是 scipy sparse
很多人打开ind.cora.allx发现是乱码,以为是加密或者文件损坏,其实它是用pickle序列化后的scipy.sparse.csr_matrix。Planetoid 数据集的官方处理方式来自 Yang et al. 2016 年的工作,后续被 PyTorch Geometric、DGL 等框架广泛采用。整个数据集由五个文件组成:allx(所有节点的特征,除测试集外)、ally(对应标签)、graph(邻接表字典)、test.index(测试集节点的原始 ID),以及可选的tx/ty/test.index用于区分训练、验证、测试划分。
在 FedGraph 场景下,你需要先把这个格式解析成能操作的数据结构。常见做法是写一个统一的加载函数:
import pickle import numpy as np import scipy.sparse as sp def load_planetoid(prefix="ind.cora"): names = ["allx", "ally", "graph", "test.index"] objects = [] for name in names: with open(f"{prefix}.{name}", "rb") as f: objects.append(pickle.load(f, encoding="latin1")) allx, ally, graph, test_index = objects # 将稀疏矩阵转为稠密,便于后续可视化或联邦划分 features = allx.toarray() if sp.issparse(allx) else allx labels = np.array(ally) # test.index 需要做对齐处理,原索引不是连续的 test_index = test_index.tolist() return features, labels, graph, test_index features, labels, graph, test_idx = load_planetoid("ind.cora") print(features.shape, labels.shape, len(graph))这段代码的关键在于encoding="latin1",因为 Python 3 默认用 UTF-8 解码 pickle 文件,而这份数据是 Python 2 时代生成的,不指定编码会直接抛UnicodeDecodeError。toarray()会把稀疏矩阵展开成 Numpy 数组,Cora 是 2708 个节点、1433 维特征,Citeseer 是 3327 个节点、3703 维特征,展开后内存占用不大,但如果是 Pubmed(19717 节点)就需要考虑保留稀疏格式。test_index是对应原图的节点 ID,用于从allx中切出测试集。
2.1.1 验证数据完整性的辅助检查
加载后建议立刻打印标签分布和图的连通性,这是避免后续训练时报维度错误的最有效手段。Cora 有 7 个类别,Citeseer 有 6 个类别,且每个类别的样本数很不均衡:
from collections import Counter label_counter = Counter(labels.argmax(axis=1) if labels.ndim == 2 else labels) print("标签分布:", dict(label_counter)) # 检查图是否连通 node_set = set() for src, dsts in graph.items(): node_set.add(src) node_set.update(dsts) print("图节点数:", len(node_set), "与allx行数:", features.shape[0])这个验证步骤在联邦划分后尤其重要,因为按客户端切分图数据后,子图可能变成孤岛,GCN 的邻居聚合会失效,而孤立节点的 embedding 会退化成仅由自身特征决定。
2.2 图结构 graph 文件与 test.index 的对应关系
ind.cora.graph是一个字典,key 是节点 ID,value 是邻居列表。注意这个 ID 空间是连续的整数,对应原论文中图的内部编号。但allx的行号并不是严格等于节点 ID,需要通过test.index做映射:allx前len(train_idx)行是训练节点,接着是验证节点,最后用test.index把测试节点的原始 ID 与tx对齐。FedGraph 在预处理时通常会展开一个id2idx映射表,把每个节点的原始 ID 映射到特征矩阵的行号。
联邦学习的数据划分发生在这个映射建立之后。与 CV/NLP 按样本随机切分不同,图数据的联邦划分要考虑结构信息。常见做法有三种:
| 划分策略 | 核心逻辑 | 适用场景 | 问题 |
|---|---|---|---|
| 随机节点划分 | 按节点 ID 均匀分配到各客户端 | 快速验证算法正确性 | 每个客户端的子图严重稀疏 |
| Louvain 社区划分 | 检测图社区后按社区分配 | 模拟真实场景下的数据分布 | 类别分布极不均衡 |
| 标签分布约束划分 | 按标签比例近似全局分布 | 对比 IID / non-IID 实验 | 需要额外计算类别比例 |
FedGraph 默认推荐 Louvain 社区切分,因为社交网络、知识图谱的本地数据天然有社区聚集效应。实现时可以用python-louvain库,也可以用 NetworkX 的community模块。切分后每个客户端保留自己的子图结构和对应特征:
import community as community_louvain import networkx as nx # 将 graph 字典转为 NetworkX 图 G = nx.Graph() for src, dsts in graph.items(): for dst in dsts: G.add_edge(src, dst) partition = community_louvain.best_partition(G, resolution=1.0) # 按社区划分子图 client_data = {} for node, comm in partition.items(): client_data.setdefault(comm, []).append(node) # 过滤掉过小的社区,防止客户端数据量不够训练 client_data = {k: v for k, v in client_data.items() if len(v) >= 20} print("客户端数量:", len(client_data), "节点数分布:", [len(v) for v in client_data.values()])resolution参数控制社区粒度,值越大社区越小、客户端越多。对 Cora 的 2708 个节点,推荐resolution在 1.0 到 1.5 之间,可以切出 4 到 8 个有意义的客户端。如果某个社区小于 20 个节点,GCN 的两层传播后感受野会覆盖整个子图,梯度更新噪声极大,联邦聚合后的全局模型容易震荡。
2.3 非 IID 程度控制与标签偏移
联邦学习论文里常说 non-IID,但真正动手时如何量化?FedGraph 的思路是:在 Louvain 划分后计算每个客户端的标签分布与全局分布的 KL 散度,作为 non-IID 程度的度量。如果 KL 散度过大,训练不稳定,需要做两类处理:一是增加每个客户端的本地训练轮次,二是调整全局聚合的权重系数。这里有一个容易被忽略的细节:图数据的客户端并不像 FedAvg 原文那样每个客户端拥有独立的样本集,而是每个客户端子图可能共享边界节点。FedGraph 的做法是在划分时删除跨客户端的边,只保留客户端内部的拓扑,这样每个客户端的图结构完全独立,符合真实场景里不同机构之间数据不互通的前提。
3. GCN 与 GraphSAGE 的联邦化改造:从日志反推实现
3.1 gcn.log 里的指标与训练节奏
打开gcn.log,通常会看到类似下面的结构(我用常见格式近似还原):
Round 0, Client 2, Train Loss 1.8923, Val Acc 0.4412 Round 1, Client 0, Train Loss 1.4532, Val Acc 0.6189 ... Round 20, Global Acc 0.8123, F1 0.7945每一行记录的是某个客户端在特定通信轮次后的本地指标,Global Acc是联邦聚合后全局模型在测试集上的表现。从日志里能反推训练节奏:如果Round间隔中客户端指标变化很小,说明本地训练轮数local_epochs设置偏低,或者学习率衰减过快;反之如果全局指标在某个轮次突然下降,通常是聚合时某个客户端的权重出现了异常值。
GCN 和 GraphSAGE 的差异在联邦场景下会被放大。GCN 是 transductive 学习,依赖完整图的邻接矩阵做拉普拉斯归一化,在联邦划分后每个客户端的子图是独立的,GCN 只能聚合本地邻居,这相当于把 GCN 退化成了局部滤波器。GraphSAGE 是 inductive 的,它的采样聚合机制天然适合联邦场景,因为它在训练时只需要节点自身的特征和采样邻居的特征,不依赖全图结构。
3.2 FedAvg 在 GCN 权重上的实现细节
联邦聚合时不能直接对模型参数做简单平均,否则输入层和输出层的尺度差异会导致聚合后模型退化。FedGraph 使用的聚合策略是按每个客户端的样本量加权平均,且只聚合共享层的权重,不聚合 BatchNorm 的 running mean / variance:
import copy import numpy as np import torch def fed_avg(global_model, client_models, client_sizes): global_dict = global_model.state_dict() total_size = sum(client_sizes) # 初始化聚合字典,对齐参数字典结构 avg_dict = {k: torch.zeros_like(v) for k, v in global_dict.items()} for model, size in zip(client_models, client_sizes): weight = size / total_size for k, v in model.state_dict().items(): avg_dict[k] += v.float() * weight # 覆盖全局参数,但跳过无法聚合的层 for k in global_dict.keys(): if "bn" in k or "running" in k: avg_dict[k] = global_dict[k] global_model.load_state_dict(avg_dict) return global_modelsize是客户端样本量的代理,实际项目中用节点数或边数均可,但要注意图数据中边数比节点数更能反映 GCN 邻居聚合的计算量。bn层在 GCN 中不常用,但 GraphSAGE 的某些实现会加 BatchNorm,联邦聚合时如果对running_mean做平均,会出现训练和推理时统计量不一致的问题,所以直接保留全局模型的 BN 统计量是更稳妥的做法。日志中sage.log的收敛速度通常快于gcn.log,原因就在这里:GraphSAGE 不用参与全局归一化,联邦聚合后各客户端的本地误差不会通过图结构传播到整个网络。
3.2.1 聚合频率与通信压缩
FedGraph 的通信轮次默认设置为 50 轮,每轮客户端本地训练 5 个 epoch。这里有一个值得借鉴的细节:服务端在聚合前对梯度做了 Top-k 稀疏化,只传输绝对值最大的 1% 梯度,其余梯度留在本地。这样通信量可以减少大约 20 倍,但准确率损失控制在 1% 以内。很多毕设代码会省略这部分,但如果你要做实验对比,这个机制是体现系统设计能力的关键加分项。
3.3 同步策略:同步聚合与异步聚合的取舍
FedGraph 的日志没有显示通信时间开销,但从系统设计角度,同步聚合(Synchronous FedAvg)是最容易实现的:服务端等待所有客户端完成本地训练后统一聚合,再广播下一轮模型。问题在于 straggler 效应——最慢的客户端决定每一轮的速度。对于毕设系统来说,模拟环境下的 straggler 效应不会很严重,但在真实部署中必须考虑。常见替代方案是异步聚合,即服务端收到一个客户端的结果就立即聚合一次,但这种做法在非 IID 数据下容易导致模型偏向先返回的客户端。我一般建议在代码里保留一个开关:
# config.py config = { "sync": True, # True: 同步聚合; False: 异步聚合 "local_epochs": 5, # 本地训练轮次 "rounds": 50, # 联邦通信轮次 "lr": 0.01, # 本地学习率 "topk_ratio": 0.01, # Top-k 稀疏化保留比例 "clients_per_round": 4, # 每轮实际参与的客户端数量 }clients_per_round在多客户端场景下很有用,Cora 用 Louvain 可能只切出 4~6 个客户端,全部参与每轮训练没有问题;但如果换成更大的数据集,这个参数就能控制每轮参与训练的客户端比例,减少通信开销。
4. 训练流程复现:从数据处理到跑通日志
4.1 数据标准化与配置准备
先用一个脚本把原始数据处理成训练所需的标准化格式。Planetoid 原始文件有两个坑:一是节点 ID 不连续,二是allx中已经包含了部分测试节点。FedGraph 的预处理脚本会做三件事:填充缺失的测试节点特征、生成统一的id2idx映射、把图邻接表转为 CSR 稀疏矩阵。推荐把处理后的数据保存为.npz或.pt文件,避免每次训练重复加载原始 pickle:
python preprocess.py --dataset cora --output data/cora.pt python preprocess.py --dataset citeseer --output data/citeseer.ptpreprocess.py内部只需要做数据格式转换,不需要任何模型逻辑。跑完后验证一下输出文件能正确加载,再做下一步:
import torch data = torch.load("data/cora.pt") print(data.keys()) # dict_keys(['features', 'labels', 'adjacency', 'idx_train', 'idx_val', 'idx_test'])这个步骤的验证点在于adjacency是否是稀疏对称矩阵,如果不是,GCN 的传播公式D^-1/2 A D^-1/2会计算出错误结果。对称性检查可以用(adj != adj.T).nnz == 0断言,非零值数量为 0 说明对称。
4.2 超参数参考表与训练命令
训练阶段的核心超参数直接决定最终指标,下表是 FedGraph 在 Cora 和 Citeseer 上跑通的可复现配置:
| 参数 | Cora | Citeseer | 说明 |
|---|---|---|---|
| hidden_dim | 64 | 64 | 隐藏层维度,过大容易过拟合 |
| dropout | 0.5 | 0.5 | 图模型标准配置 |
| 本地学习率 | 0.01 | 0.005 | Citeseer 特征维度高,需降低学习率 |
| 客户端本地 epoch | 5 | 5 | 过少欠拟合,过多加剧 non-IID 漂移 |
| 联邦通信轮次 | 50 | 80 | Citeseer 标签噪声大,需更多轮次 |
| 参与客户端数/轮 | 4 | 5 | 全部参与或按需采样 |
| 聚合权重 | 节点数 | 节点数 | 也可以用边数,需保持实验一致性 |
训练入口用命令行参数控制,方便做消融实验:
python train_fed.py --dataset cora --model gcn --sync --rounds 50 --local_epochs 5 --lr 0.01 python train_fed.py --dataset citeseer --model sage --sync --rounds 80 --local_epochs 5 --lr 0.005--model参数接受gcn和sage两个选项,对应两类模型。每次训练结束会把日志追加写入gcn.log或sage.log,这样后续数据分析时可以直接对比两个文件的差异。
4.3 复现过程中最常见的三个错误点
第一个错误是 Mixing 矩阵维度不匹配。GCN 的传播公式中,邻接矩阵的维度是节点数,但联邦划分后每个客户端的邻接矩阵维度不统一,容易出现A是全局矩阵而X是子集特征的情况。调试方法是在 forward 开头加维度断言:
assert x.shape[0] == adj.shape[0], f"特征行数 {x.shape[0]} != 邻接矩阵行数 {adj.shape[0]}"第二个错误是测试集与训练集特征错位。ind.cora.test.index指定的节点 ID 不一定与allx最后几行对应,如果直接用features[test_index]取测试集,取到的是错误的节点嵌入。正确的做法是先建立完整的节点 ID 到行号的映射,再做索引。
第三个错误是随机种子未固定导致联邦实验无法复现。联邦学习涉及客户端划分、模型初始化、Dropout 三层随机性,如果不固定种子,两次训练的结果差异可能超过 3 个点。建议在代码入口统一设置:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)Citeseer 的标签噪声比 Cora 大且节点数更多,相同参数下收敛需要更多轮次,这也是为什么sage.log的行数通常比gcn.log多。如果发现测试准确率达不到论文水平,先检查上述数据对齐逻辑,再调超参。
5. 从日志和 test.index 出发的扩展与排错技巧
5.1 用两份日志做模型差异化分析
gcn.log和sage.log不光是训练记录,它们是最好的模型对比素材。写一个简单脚本提取 log 中的指标做趋势对比:
import re import matplotlib.pyplot as plt def parse_log(path): global_accs = [] with open(path) as f: for line in f: m = re.search(r"Global Acc ([0-9.]+)", line) if m: global_accs.append(float(m.group(1))) return global_accs gcn_accs = parse_log("gcn.log") sage_accs = parse_log("sage.log") plt.plot(gcn_accs, label="GCN") plt.plot(sage_accs, label="GraphSAGE") plt.legend() plt.xlabel("Communication Round") plt.ylabel("Test Accuracy") plt.savefig("compare.png", dpi=150)如果sage的前几轮指标跳变幅度明显大于gcn,说明 GraphSAGE 的邻居采样策略在小规模子图上更容易受随机性影响,这是正常现象。对比图可以直接用进毕业论文的实验分析章节。
5.2 zip 压缩包内文件缺失时的应对
解压图联邦学习系统设计与实现.zip后,如果发现缺少ind.pubmed.*或其他后缀文件,直接去 Planetoid 官方仓库补下载对应文件即可。注意文件命名必须完全一致,不能把.x和.tx混用。另外,如果你只需要跑通 GCN 的实验,ind.cora.*五个文件已经足够,ind.citeseer.*是可选的对照组,优先级低一些。检查文件完整性的命令是:
unzip -l 图联邦学习系统设计与实现.zip看到ind.cora.graph与gcn.log大小非零即可确认文件未损坏。若allx打开乱码不是坏档,按第 2 节的pickle方式读取即可。
5.3 利用 test.index 做自定义评估协议
一个容易忽略的进阶玩法是修改test.index的内容来控制测试集。联邦学习论文经常要求报告在 ID 划分和 OOD 划分下的不同表现,你可以将原始test.index按社区重新分配,构造跨客户端测试集。做法是:训练完成得到全局模型后,把训练阶段移除的跨客户端边恢复,再在完整的测试节点上验证。这样可以衡量联邦聚合后的全局模型在真实结构下的泛化能力,比单纯看Global Acc更有说服力。修改后重新运行评估:
python evaluate.py --dataset cora --model gcn --checkpoint best_model.pt --custom_test_index data/custom_test.index评估脚本内部只需要重新加载数据并替换idx_test变量,无需改动模型代码。这个实验设计思路如果写进毕设的「实验与结果分析」章节,会明显提升工作量和技术深度。
本文还有配套的精品资源,点击获取