简介:基于DAG区块链的联邦学习框架,面向去中心化与个性化模型训练,可作为计算机、数学、电子信息等专业课程设计、期末大作业及毕业论文的代码范本。框架用Tangle有向无环图替代中心化参数服务器,结合异步交互与恶意节点识别,解决传统联邦学习中的服务器信任与模型同质化问题,同时支持客户端按局部数据定制个性化模型。资源包共77个文件,含46个Python源文件、25个pyc编译文件、3个Notebook分析示例、1个YAML环境配置及1个项目说明,压缩包仅1.17MB,目录按核心算法、分析工具、实验组件分层。已有221人学习;运行run.py即可自动获取数据集,结果保存至experiments目录,便于复现。源码具体实现tip选择、Tangle交易存储与解析、恶意节点及数据毒化模拟、图聚类等模块,Notebook分步演示关键流程,便于深入理解DAG融合联邦学习的算法设计与实现细节,也适合在此基础上扩展个性化聚合策略或进行对比研究。
1. 联邦学习不再依赖中心服务器:DAG 账本到底改了什么
联邦学习跑在中心参数服务器上时,最怕的不是模型收敛慢,而是服务器一挂、所有客户端白练;就算服务器不挂,全局模型和本地数据分布差异一大,平均后的权重也常常两头不讨好。把区块链引用进来做去中心化是个常见解法,但链式账本在联邦学习里又很尴尬:区块必须排队出块,节点一多就阻塞。这个基于 DAG(有向无环图)的联邦学习框架,把链式结构换成了 DAG 账本,多个交易可以并行引用、同时确认,异步联邦学习跑得起来,每个客户端的更新还能保留自己的个性。项目以 Python 源码完整实现,从核心节点、交易存储到毒化攻击都有现成代码。想做毕业设计、课程设计,或者想研究去中心化联邦学习算法的人,可以直接拿它当实验台。
2. 框架结构拆解:从文件布局看懂 Tangle、Tip 选择和交易存储
2.1 为什么选 DAG 而不是单链
传统区块链把交易按时间排成一条链,每个区块只能指向前一个区块,所有节点必须对「谁先谁后」达成一致后才能继续出块。联邦学习每个通信轮次有大量客户端同时提交参数更新,如果用单链,每一轮更新都要排队等确认,网络规模稍大就会出现吞吐瓶颈;更麻烦的是,恶意节点可以通过控制出块节奏拖垮整个系统。
DAG 方案(项目里用的是 Tangle 风格模型)允许新交易直接引用一个或多个已存在的交易作为父交易,只要不形成环,就可以持续并行写入。区块的「顺序」由引用关系间接确定:一笔交易被越多后来的交易引用,它的累积权重就越高,最终被确认只是时间问题。这种结构天然适合联邦学习:各个客户端不再等全局同步,提交的参数只要被其他节点看到并引用,就完成了「一次异步共识」。
反直觉的一点在于,DAG 对共识的定义不再是「所有节点看到同一个全序」,而是「大多数诚实节点最终会看到同样的前缀」。项目里把这套逻辑拆成了 tangle 数据结构、tip 选择策略和交易存储三部分,后面会逐个讲到。tip_selector_factory.py的存在也说明,选择父交易的策略被设计成可插拔的,并不是写死的单一算法。
2.2 源码目录:核心节点、实验室、分析模块各管什么
解压后顶层是code_20105目录,下面按照core/、lab/、analysis/、models/组织。先看整体布局,再深入代码时就不会迷路。
| 路径 | 职责 | 关键入口 |
|---|---|---|
run.py | 实验入口,控制数据集、通信轮数和攻击参数 | python dagfl/run.py |
environment.yml | conda 环境定义 | conda env create -f environment.yml |
core/node.py | 联邦学习节点:本地训练、发起交易 | 节点生命周期入口 |
core/tangle.py | DAG 账本:维护交易图、tip 集合 | add_transaction() |
core/tip_selection.py | 选择父交易的策略 | select_parent_tips() |
core/transaction.py | 交易结构:payload、父引用、时间戳 | 交易构造与序列化 |
core/transaction_store.py | 全网交易存储 | 按节点或时间维度检索 |
core/malicious_node.py | 恶意节点行为包装 | 投毒参数替换 |
core/poison_type.py | 攻击类型常量 | label_flip等枚举 |
lab/lab.py | 多轮实验调度器 | 创建节点、推进轮次 |
lab/lab_transaction_store.py | 实验室独立存储 | 模拟各客户端私有视野 |
analysis/tangle_analysator.py | 结果分析与可视化 | 读取 experiments 数据 |
models/与models/args.py | 模型定义与全局参数 | 数据集路径、轮数、种子 |
这里我想特别提一下lab/lab_transaction_store.py。主transaction_store.py可以看成全局账本,所有交易都在里面;而实验室环境里每个节点可能只维护自己的交易存储。这个设计不是冗余,是为了模拟真实异步网络里「客户端看不到全部交易」的情况。个性化之所以能成立,前提就是各节点视野不同——如果所有节点永远共享同一个全局账本,那最后还是会退化成中心化广播。
2.3 Tip 选择就是 DAG 的「共识代理」
Tangle 里没有独立的出块节点,也没有共识委员会,真正决定「哪笔交易先被确认」的是 tip 选择算法。所谓 tip,就是当前还没有被任何后续交易引用的交易。新交易到达时,先从 tip 集合里挑父节点;被选中的 tip 获得一次直接引用,间接引用它的一整棵子树都跟着增加权重。
项目中core/tip_selection.py的核心逻辑可以简化为加权随机游走:多个游走者从某个初始交易出发,按累积权重随机往下走,最终停在某个 tip 上,这个 tip 就被选为父交易。下面是我在本地读代码时梳理出的示意结构:
# core/tip_selection.py 的核心逻辑示意 def select_parent_tips(tangle, rng, walkers=3): tips = tangle.get_tips() # 尚未被引用的交易 if len(tips) <= 1: return tips total = 0.0 weights = {} for tip in tips: w = tangle.cumulative_weight(tip) # 间接引用数 + 自身权重 weights[tip] = w total += w selected = [] for _ in range(walkers): r = rng.random() * total # 按权重采样 acc = 0.0 for tip, w in weights.items(): acc += w if acc >= r and tip not in selected: selected.append(tip) break return selectedwalkers对应并行游走的粒子数,数值越大,选择结果越倾向于权重高的交易,收敛更快,但也更容易让权重已经很高的分支继续膨胀;数值小,新分支才有机会被引用,探索性更强。真实实现里还会加入时间衰减和节点信誉之类的修正项,但最底层的逻辑就是这种「按引用权重投骰子」。调这个参数,几乎等于直接调整个网络的收敛速度。
2.4 从交易到本地存储:数据是怎么流起来的
一次完整的更新链路大致是这样的:节点完成本地训练后,把模型参数序列化成一个交易;然后调用 tip 选择算法,选一个或两个父交易;交易被放到tangle.py的图结构里校验,再写入transaction_store.py。后续节点拉取父交易时,能看到上游的参数更新,等于间接完成了「聚合」。
这个流程里最容易忽略的是交易校验。DAG 允许并行写入,但必须保证父引用不存在环,否则图结构会被污染。tangle.py里通常会维护一个已见交易集合,新交易的父引用必须在集合里,或者能通过递归回溯找到,否则会被直接拒绝。实际项目中遇到「某些交易永远不被确认」,十有八九是父引用丢失或者形成了环,排查时先看这一层。
3. 本地跑通实验:conda 环境、run.py 参数和 experiments 结果解读
3.1 用 conda 一次建好运行环境
项目自带environment.yml,这是最省事的安装路径。先进入解压后的项目根目录,创建环境并激活:
cd dagfl conda env create -f environment.yml conda activate dagfl cd .. python dagfl/run.py注意这里的cd dagfl进入的是资源解压后的工程根目录,environment.yml就在里面。创建完环境后要回到上层目录再执行python dagfl/run.py,这是刻意为之:代码里模块导入路径是基于dagfl这个包来定位的,直接在根目录执行反而会报ModuleNotFoundError。我第一次跑的时候自作聪明直接python run.py,结果 import 阶段就卡住了,老老实实按 README 的两段式来,一次通过。
environment.yml里通常会列好 numpy、pandas、matplotlib 这一组科学计算基础库,以及项目依赖的 Python 版本。如果你机器上已经有 conda,装环境时看到长时间停在Solving environment,不要急着关,那是 conda 在解析依赖组合;真要卡死超过十分钟,再考虑单独建一个干净的空环境手动装包:
conda create -n dagfl python=3.9 -y conda activate dagfl conda install numpy pandas matplotlib -y这里把 Python 版本显式写成 3.9,是因为我在项目里看到了cpython-39.pyc的缓存文件,说明作者大概率是在 Python 3.9 下开发的。你用 3.8 或 3.10 也可能跑,但遇到奇怪的语法报错时,先回头检查版本。
3.2 run.py 里的参数怎么改
run.py是唯一需要关注的入口。程序会自动下载所需数据集,并通过models/args.py里的全局参数控制行为。下面是我整理出的核心参数及含义:
# run.py / args.py 中常见参数的简化示例 DATASET = "femnist" # 数据集:默认 femnist,可换其他分类数据 DATA_DIR = "./data" # 数据集存放目录,不存在则自动下载 COMM_ROUNDS = 100 # 联邦通信轮数,决定训练总时长 SAMPLE_RATIO = 0.6 # 每轮参与客户端的采样比例 SEED = 42 # 全局随机种子,复现实验结果必须固定 POISON_TYPE = None # None / label_flip / random_noise / backdoor MALICIOUS_RATIO = 0.0 # 恶意节点占比,0 表示全诚实 OUT_DIR = "experiments" # 结果输出目录SAMPLE_RATIO是联邦学习的经典调参项:每轮只随机抽 60% 客户端参与,能显著降低通信开销,但方差会变大,收敛曲线更抖。想验证极端同步场景,可以把它调到 1.0;想模拟设备掉线频繁的真实环境,调到 0.3 左右更有参考价值。
POISON_TYPE和MALICIOUS_RATIO是攻击仿真开关。POISON_TYPE=None, MALICIOUS_RATIO=0.0就是干净基线;把POISON_TYPE设成label_flip、MALICIOUS_RATIO设成 0.2,就能看到两成客户端投毒时全局收敛会被拖成什么样。这两个参数组合起来,基本就是论文实验里最常出现的对比矩阵。
SEED是我每次跑实验都会强制写死的参数。Tangle 的 tip 选择本身带随机游走,不固定种子的话,同一个参数跑两遍结果完全对不上,后面做对比实验根本没法解释差异。
3.3 experiments 输出怎么看
跑完一轮后,结果会落在experiments目录下。我一般先看有没有生成交易图相关的 JSON 或 CSV 文件,再从中抽训练指标。你可以在analysis/tangle_analysator.py基础上快速写一段读取逻辑:
# analysis/tangle_analysator.py 使用示意 import json with open("experiments/run_metrics.json", "r", encoding="utf-8") as f: run = json.load(f) acc = run["accuracy_curve"] # 每轮准确率列表 print(f"rounds: {len(acc)}") print(f"last 10 acc: {acc[-10:]}")这段代码的用途是快速确认训练是否收敛:如果最后 10 轮准确率还在大幅震荡,先检查SAMPLE_RATIO是不是太低,或者SEED没固定。accuracy_curve这种键名是我在自己实践中常用的命名,真实项目里可能是acc或metrics,打开文件看一眼 keys 再改。
4. 个性化与恶意节点:个人化模型如何在去中心化网络里存活
4.1 个性化不是「全局模型再微调」
中心化联邦学习的个性化,通常是在拿到全局模型之后,用本地数据再微调几轮。但这个框架里没有中央服务器,所以个性化的实现路径完全不同:每个节点在本地训练自己的模型,然后把自己的参数做成交易发到 Tangle;别的节点引用这笔交易,不等于直接拿到「全局模型」,而是拿到「某条分支上的视野」。视野不同,参数自然不同,这就是个性化的底层来源。
core/node.py里本地训练的核心可以抽象成下面这段:
# core/node.py 本地训练示意 def local_step(self, data_loader, local_epochs): """在本地数据上训练若干轮,返回更新后的参数。""" for epoch in range(local_epochs): for batch_x, batch_y in data_loader: self.model.train_on_batch(batch_x, batch_y) return self.model.get_parameters()local_epochs是很有讲究的参数:调大,本地模型会离全局趋势更远,个性化更强,但可能过拟合;调小,模型更依赖网络里其他节点的信息,收敛更稳。实际联邦学习场景里,客户端数据越异构,local_epochs越应该配大一点。
4.2 poison_type.py 与 malicious_node.py 的仿真设计
去中心化系统必须面对恶意节点。项目里core/poison_type.py定义攻击类型,core/malicious_node.py负责把普通节点包装成恶意节点。常见的攻击类型在代码里通常是这样定义的:
# core/poison_type.py 攻击类型定义示意 POISON_LABEL_FLIP = "label_flip" # 本地标签翻转后再训练 POISON_RANDOM = "random_noise" # 直接提交随机权重 POISON_BACKDOOR = "backdoor" # 给训练样本加后门图案malicious_node.py的典型行为是:正常完成训练,但在生成交易前把参数替换成攻击向量,再丢给 tip 选择器。比如随机噪声攻击会让模型权重完全脱离正常分布,而标签翻转攻击是「反向梯度」式的,破坏力更强。这个项目把它们做成了枚举常量,意味着你对比实验时不需要改节点代码,只要在run.py里切换POISON_TYPE就行。
我看项目里还有poisoning-debug.ipynb,说明作者当初也在用 notebook 逐轮调试投毒效果。这个 notebook 的价值不仅仅是看结果,更重要的是它能让你单步观察恶意交易进入 Tangle 后,被其他节点引用的路径——这在纯命令行黑盒运行下很难看清。
4.3 用 notebook 验证攻击效果
把恶意节点比例调高后,最直观的现象是准确率曲线不再平滑。我一般会跑三组对照:干净基线、random_noise攻击、label_flip攻击,然后把曲线画在同一张图里:
# 对照实验可视化示意 import matplotlib.pyplot as plt import pandas as pd for name in ["clean", "random_noise", "label_flip"]: df = pd.read_csv(f"experiments/{name}/metrics.csv") plt.plot(df["round"], df["acc"], label=name) plt.xlabel("round") plt.ylabel("accuracy") plt.legend() plt.savefig("experiments/poison_compare.png")如果random_noise攻击下准确率掉得比label_flip还多,先别急着下「随机噪声更强」的结论——检查恶意参数是否在交易序列化阶段被截断,以及MALICIOUS_RATIO是否真的生效了。这类攻击实验最容易翻车的地方不在算法,而在参数传递链路。
5. 避坑清单:把环境、数据下载和随机种子问题一次排干净
5.1 conda 长时间卡在 Solving environment
现象:执行conda env create -f environment.yml后,终端十几分钟停在Solving environment,偶尔直接报依赖冲突。
原因:默认软件源在大依赖组合下解析得很慢;另外项目里可能固定了某些包版本,和当前 conda 版本或 Python 版本已经不兼容,解析器反复回溯才导致卡顿。
解决:先换国内常见的 conda 镜像源,或者干脆跳过environment.yml,手动建一个干净的 3.9 环境,只装最基础的几件套:
conda create -n dagfl python=3.9 -y conda activate dagfl conda install numpy pandas matplotlib -y装完如果某段代码报缺包,再单独pip install缺的那个,这样反而比解析整个 yml 快得多。
5.2 数据集自动下载「玄学失败」
现象:第一次运行run.py,终端显示正在下载数据集,下一秒抛出 HTTPS 连接错误或 HTTP 403。
原因:自动下载走的服务器在你所在网络下不稳定,或者下载工具的 User-Agent 被服务端拒了。
解决:用浏览器手动把数据集下载到本地,然后改run.py里的DATA_DIR指向本地路径,同时把自动下载的开关关掉。常见做法是:
DATASET = "femnist" DATA_DIR = "/path/to/local/femnist" # 手动下载并解压后的目录 SKIP_DOWNLOAD = True # 跳过自动下载能跑实验才是第一位的,没必要在这件事上跟网络较劲。
5.3 直接运行 run.py 报 ModuleNotFoundError
现象:在项目根目录下执行python run.py,抛ModuleNotFoundError: No module named 'dagfl'或core。
原因:工程的包名是dagfl,代码里用from core import ...或from dagfl.core import ...这类绝对导入。当前工作目录不对时,Python 找不到包。
解决:严格按 README 的两段式执行:先cd dagfl进入工程目录安装环境,再cd ..回到上一级,最后用python dagfl/run.py运行。这个命令从父目录切入,dagfl就变成了一个可导入的包。
5.4 同样参数跑两遍,结果对不上
现象:固定了所有参数,实验跑两遍,准确率曲线完全不同,甚至好坏的结论都会反转。
原因:SEED没有真正传进 tip 选择的随机游走过程。Python 的random.seed()只影响全局随机模块,如果代码里用了 NumPy 的np.random,两者各自独立,必须分别设置。
解决:在run.py开头同时固定两套随机源:
import random import numpy as np SEED = 42 random.seed(SEED) np.random.seed(SEED)如果tip_selection.py内部自己创建了random.Random()实例,还需要把这个实例的种子暴露成参数,而不能只在入口 seed 一次。这也是调 DAG 类项目最常见的暗坑:图结构对随机性高度敏感,种子没锁死,其他参数全白调。
5.5 跑长实验内存越占越多
现象:通信轮数设到 100 以上,进程跑到中途变慢,内存占用持续上涨,最后被系统杀掉。
原因:Tangle 图里每笔交易都被保留在内存中,交易越多,图遍历和累积权重计算的开销越大。
解决:先在COMM_ROUNDS=20下跑通验证链路,再把轮数拉长。长时间实验要定期把历史交易序列化到磁盘,并从活跃图里裁剪掉已经确认很久的旧交易。项目自带analysis/tangle_analysator.py,本来就要分析交易图,数据落盘后正好可以复用它。
6. 进阶改造:自定义 Tip 选择策略和实验指标
6.1 接入自己的 Tip 选择算法
项目的tip_selector_factory.py把策略做成了工厂模式,这给了很大的改造空间。最简单的玩法是继承现有 selector,改一小段逻辑。比如我希望网络更偏向「节点本地近期状态」,可以写一个优先选择本节点最近交易的策略:
# 自定义策略:优先引用本节点最近的交易 from core.tip_selection import BaseTipSelector class RecentLocalSelector(BaseTipSelector): def select(self, tangle, node_id): tips = tangle.get_tips() local = [t for t in tips if t.origin_node == node_id] if local: return [max(local, key=lambda t: t.timestamp)] return tips[:1]在tip_selector_factory.py里注册这个类,run.py就能通过参数切换。改完策略记得跑一组干净基线和一组新策略的对照,看收敛轮数是否有实质变化。
6.2 把对比实验脚本化
固定 seed、换攻击类型、换客户端采样比例,这套流程完全可以写成 shell 循环,一次跑完整组实验:
for attack in none label_flip random_noise; do python dagfl/run.py --poison_type $attack --seed 42 --out_dir experiments/$attack done跑完后再用一个汇总脚本把所有metrics.csv合并成一张表,比在 notebook 里手动改参数重跑要省事得多。我第一次跑这个框架时,整整一周都在调随机游走参数,最后发现影响最大的其实是 seed 没固定,两个分支的对比图根本没法解释。从那以后我每次跑对比实验都强制走一遍:固定 SEED、用独立子目录存结果、跑完立刻导 CSV 归档。框架本身把账本、交易、投毒节点这些最底层的东西都写好了,你要做的是在这上面加自己的策略。希望帮到你。
本文还有配套的精品资源,点击获取