
简介面向毕业设计、课程设计与软件工程实践的联邦学习安全聚合项目基于TensorFlow与同态加密技术重点解决模型聚合阶段的数据隐私泄露风险可作为隐私保护机器学习方向的学习范本。源码包共55个文件约2.06MB以Python脚本与模型文件model/modle为主体另含批处理启动脚本、配置文件、密钥证书及README运行说明目录结构清晰便于直接对照代码与说明学习。当前已有221人学习下载。项目完整展示了联邦学习框架搭建、同态加密库调用、模型并行与数据并行策略以及秘密共享等安全聚合算法的实现细节运行说明覆盖环境配置、参数设置、训练循环与结果评估等关键步骤并提供了服务端/客户端启动脚本适合希望深入理解隐私保护机器学习、动手复现安全聚合流程的开发者。1. 同态加密联邦学习到底在解决什么问题在联邦学习流程中客户端把梯度上传给中心服务器服务器在内存中聚合后再下发全局参数。数据没有离开本地但梯度以明文形式出现在服务器里的那一瞬间就是系统最大的破绽。梯度泄露攻击已能还原出高保真训练样本“诚实但好奇”的服务器和被攻破的服务器都能从中提取敏感信息。同态加密把“梯度可见”这个环节去掉客户端本地加密服务器在密文上求和只有聚合完成后才由持私钥的一方解密。服务器自始至终看不到原始梯度。以 TensorFlow 做训练框架、以 Paillier 这类加法同态加密做安全聚合是当前联邦学习工程落地里最常见的一套搭配标题里这份源码系统也遵守了这条主线。接下来按威胁模型、选型、最小实现、参数排错、隐私增强五部分展开。前两节给后面的代码和配置建立依据后三节可以当作实施清单直接用。2. 梯度即隐私联邦学习的聚合风险与同态加密选型2.1 安全聚合的威胁模型与边界在安全聚合设计里攻击者通常按四种身份建模诚实但好奇的服务器严格按协议执行聚合但会留存每轮明文梯度做成员推断或属性推断被攻破的服务器攻击者拿到内存、磁盘或容器权限明文梯度直接落网恶意客户端提交伪造梯度参与聚合或在收到全局模型后反推其他客户端的梯度贡献网络嗅探者在不安全的链路上抓包直接获得通信内容。同态加密解决第 1、2、4 类的机密性问题因为梯度在客户端本地就完成加密链路和服务器内存里物理上不存在明文。对第 3 类恶意客户端同态加密无能为力——恶意客户端提交什么它就被加密成什么。抗拜占庭的鲁棒聚合算法比如 Krum、Trimmed Mean 需要在加密之外单独接入。所以这套系统的总体架构是同态加密保护机密性鲁棒聚合保护完整性TLS 保护传输层三者叠加才称得上完整的安全聚合。这也是标题里“安全聚合”四个字最容易引起误解的地方它不是某个单点加密算法而是一组互补机制的集合。开始选型之前先把自己要防的攻击者身份说清楚否则后续参数和流程设计都会失真。2.2 加法同态够用吗Paillier 与 CKKS 的取舍联邦学习最经典的 FedAvg 聚合公式可以写成w_{t1} w_t - lr * (1 / K) * Σ Δw_k其中 Δw_k 是第 k 个客户端的梯度更新服务端要做的事是求和后取平均。去掉减法和学习率系数核心运算是加法这正好落在加法同态的射程内密文相加对应明文相加。因此标题这一类以 TensorFlow 为前端、聚合逻辑只有平均和加权平均的工程实现优先选 Paillier 而不是全同态方案。维度PaillierCKKS同态能力加法同态加法和乘法浮点近似数值类型整数浮点复数精度精确近似有截断误差密文膨胀率约 24 倍数十倍依赖安全参数工程成本低python-paillier 可用高依赖 SEAL/OpenFHE典型聚合算法FedAvg、FedSGDFedProx、带正则/动量的聚合选定方案之后真正耗时间的是数值编码。Paillier 工作在整数域CKKS 虽然能直接处理浮点但参数配置复杂对内存和 CPU 的要求高一个量级。2.2.1 Paillier 的缩放与取整TensorFlow 默认梯度是 float32Paillier 只能加密整数。常见做法是选一个缩放因子 precision把梯度乘以 1/precision 后取整解密后除以同一个因子恢复浮点值。缩放因子直接决定量化误差也会在后面影响明文溢出判断所以它既是加密参数也是训练超参的一部分。2.2.2 CKKS 的适用场景如果聚合逻辑需要在密文状态做乘法例如计算梯度方差或者 FedProx 里对权重偏离加正则项Paillier 就不够用了。此时才值得升级到 CKKS代价是密文体积膨胀到极高旋转和重线性化操作非常吃内存服务端聚合速度会明显下降。把这些成本放在一起比较选型结论很直接聚合只有平均就选 Paillier有复杂聚合逻辑再考虑 CKKS。2.3 差分隐私不能替代同态加密的两个原因第一差分隐私通过注入噪声隐藏个体贡献噪声进入最终模型后会造成精度损失并且和隐私预算 ε 强相关同态加密不引入统计噪声解密结果和明文聚合在数学上等价只差浮点缩放误差。第二差分隐私保护的是“某个样本是否参与了训练”这类统计性信息同态加密保护的是“这次上传的梯度具体值是多少”两者隐私语义完全不同。一个常见误用是把两者当作同一件事加了同态加密之后全局模型仍然以明文下发给各方参与方从最终模型反推训练数据的攻击依然成立。防这一类泄露需要差分隐私或数据消毒来兜底而不是加密本身。3. 在 TensorFlow 中落地安全聚合的最小实现3.1 系统目录与模块职责一套可运行的联邦安全聚合系统至少包含客户端、服务端、配置三部分。按源码包的形式组织比较常见的目录结构是fedsec-aggregator/ ├── client/ │ ├── model.py # TensorFlow 模型定义与训练循环 │ ├── encrypt.py # 梯度展平、缩放、加密 │ └── client_main.py # 客户端入口 ├── server/ │ ├── aggregator.py # 密文聚合与轮次管理 │ ├── decrypt.py # 解密、反缩放、重构模型 │ └── server_main.py # 服务端入口 ├── config/ │ ├── he_config.yaml # 同态加密参数密钥位数、precision │ └── fl_config.yaml # 联邦学习超参轮次、客户端数、批次大小 └── utils/ └── serialize.py # 密文序列化与网络传输封装模块职责如下表模块职责对外接口client/encrypt.py本地加密梯度更新encrypt_client_update(grads, pub_key)server/aggregator.py聚合多个客户端的密文secure_aggregate(encrypted_updates)server/decrypt.py私钥解密并恢复更新decrypt_and_reconstruct(...)config/*.yaml配置密钥与训练参数yaml.safe_load()加载3.2 客户端侧从 TensorFlow 梯度到 Paillier 密文TensorFlow 训练产生的梯度是tf.Tensor加密前要先取出数值并展平成一维 float64 数组。有一个关键约束加密过程必须在 eager 模式下执行不能放进tf.function图模式里python-paillier 的加密操作无法进入 TensorFlow 图执行环境。import numpy as np from phe import paillier def encrypt_client_update(grads, pub_key, precision1e-6): 将 TensorFlow 梯度列表加密为 Paillier 密文列表。 grads: tf.GradientTape 计算得到的梯度列表元素为 tf.Tensor pub_key: paillier.PaillierPublicKey训练开始前由服务端广播 precision: 浮点缩放精度决定密文可分辨的最小梯度步长 返回: encrypted_updates: 每个元素是 list[EncryptedNumber] grad_shapes: 每个参数的原始 shape解密后用于重构 encrypted_updates [] grad_shapes [] for g in grads: if g is None: continue # float64 展平避免 float32 位数不够引入额外量化误差 flat g.numpy().astype(np.float64).flatten() # 放大到整数域并四舍五入Paillier 只能加密整数消息 scaled np.round(flat / precision) # 逐元素加密得到 EncryptedNumber 对象 encrypted [pub_key.encrypt(float(v)) for v in scaled] encrypted_updates.append(encrypted) grad_shapes.append(g.shape) return encrypted_updates, grad_shapes逻辑说明precision1e-6表示把梯度放大 10^6 倍。假设梯度值是 0.0001234缩放后取整得到 123解密后再乘以 1e-6 还原为 0.000123。astype(np.float64)是为了保证缩放后的大整数不会被 float32 的 7 位有效数字截断。参数说明precision 取值必须结合训练数值范围。它太小会把明文推到接近 Paillier 上限太大又会让小梯度直接变成 0导致这一轮更新丢失。另外在加密前必须检查梯度是否有限if not np.all(np.isfinite(flat)): raise ValueError(gradient contains NaN/Inf, abort this round)这一步成本几乎为零但能避免 NaN 被转换成极端整数后污染整轮聚合结果是客户端加密前必做的校验。3.3 服务端侧密文聚合与解密所有客户端完成本地训练后把密文列表上传到服务端。服务端只有公钥只能对密文做加法def secure_aggregate(encrypted_updates): 多个客户端的密文更新逐位累加。 encrypted_updates: list[list[EncryptedNumber]] 每个子列表来自一个客户端长度必须一致。 client_count len(encrypted_updates) if client_count 0: raise RuntimeError(no encrypted updates received) aggregate_len len(encrypted_updates[0]) aggregated [] for i in range(aggregate_len): cipher_sum encrypted_updates[0][i] for c in range(1, client_count): cipher_sum cipher_sum encrypted_updates[c][i] aggregated.append(cipher_sum) return aggregated逻辑说明python-paillier 重载了运算符底层执行的是密文乘法模 n^2 的运算因为 Paillier 的数学性质是E(m1) * E(m2) mod n^2 E(m1 m2)。密文加法满足交换律和结合律客户端上传顺序不影响最终结果。聚合完成后由持有私钥的一方解密def decrypt_and_reconstruct(aggregated, priv_key, shapes, precision1e-6, client_count3): 将聚合密文解密并还原为平均梯度。 raw np.array([priv_key.decrypt(c) for c in aggregated], dtypenp.float64) # 先除客户端数再乘回精度避免整数截断误差 raw raw / client_count * precision restored [] offset 0 for shape in shapes: size int(np.prod(shape)) restored.append(raw[offset:offset size].reshape(shape)) offset size return restored注意这里必须先做浮点除法再乘回精度。如果反过来先乘后除整数除法会直接把小梯度截断成 0哪怕 precision 设得合理也救不回来。3.4 运行的最小命令序列以本地多进程模拟多客户端为例安装和启动命令如下conda create -n fedsec python3.9 -y conda activate fedsec pip install tensorflow phe pyyaml python server/server_main.py --config config/he_config.yaml python client/client_main.py \ --server-addr 127.0.0.1:50051 \ --rounds 5 \ --batch-size 32命令说明先启动服务端再启动客户端。--rounds是联邦学习轮数--batch-size会直接决定梯度幅度分布间接影响 Paillier 缩放精度改动后需要同步检查he_config.yaml里的 precision。服务端启动时若配置了生成密钥对会在内存中生成公私钥并把公钥广播给所有客户端私钥只在解密阶段使用不要落盘也不要写进任何配置文件。3.5 密钥复用与重放防护同态加密的密钥不需要每轮更换同一个公钥可以复用到训练会话结束。但复用的代价是重放攻击攻击者截获一轮有效密文在后续轮次重复提交相当于反复放大该批梯度的影响。常见做法是在明文中拼接轮次编号和客户端 ID 后整体加密服务端解密后使用这些元信息校验合法性。由于只有服务端持有私钥这些元信息不会暴露给其他客户端。4. 参数边界、网络开销与三个高频坑位4.1 precision 与明文上限的联动Paillier 的明文空间是模 n 的整数域n 是两个大素数的乘积。2048 位密钥下 n 约 2^20483072 位下约 2^3072理论明文上限非常大。工程上真正要关注的是 precision、梯度尺度和密钥位数三者组合后的净效应。一个容易踩的具体场景LSTM 或 Transformer 这类模型在长序列上容易出现较大中间值某个梯度元素达到 500precision 设为 1e-6 时缩放后是 5e8这是 9 位数完全安全。但如果把 precision 调到 1e-9同样梯度的缩放结果变成 5e11仍在上限之内但 float64 转整数时的尾数误差开始放大。反过来precision 设成 1e-2梯度 0.003 缩放后直接变成 0整轮更新丢失。精度选型的实用做法是先在明文状态统计一轮梯度最大绝对值max_abs然后按precision 10^(-floor(log10(max_abs)) - 1)设定留出至少一个数量级的余量。固定批次大小是前提批次从 32 突然调到 512梯度范数也会跟着跳一个量级原设置的边界安全就不成立了。提示precision 一旦确定整个训练会话内所有客户端必须使用同一个值。建议把 precision 作为公共配置随公钥一起广播避免客户端各自理解出现偏差。4.2 密文膨胀率与通信瓶颈Paillier 的密文是模 n^2 下的整数2048 位密钥时每个密文约 512 字节。一个 100 万参数的模型明文梯度大约 4 MB加密后约 300 到 400 MB。这类数据在广域网上完全不可接受所以系统一定会在加密前做维度压缩。常见手段是 Top-K 稀疏化和低精度量化配合使用def sparse_and_encrypt(grad_flat, pub_key, keep_ratio0.01, precision1e-6): 只加密 Top-K 个梯度元素其余位置显式置零。 k max(1, int(len(grad_flat) * keep_ratio)) indices np.argsort(np.abs(grad_flat))[-k:] sparse_flat np.zeros_like(grad_flat) sparse_flat[indices] grad_flat[indices] scaled np.round(sparse_flat / precision) encrypted [pub_key.encrypt(float(v)) for v in scaled] return encrypted, indices代码逻辑keep_ratio0.01只保留绝对值最大的 1% 梯度其余位置补零。补零操作在加密前完成密文数量和通信量直接降到原来的 1%服务端加法不受影响零位置求和仍然是零。由于 Top-K 选择本身可能泄露梯度分布信息对隐私要求更严格的系统会把索引集合也一并加密或者改用固定的随机掩码代替动态选择。4.3 三个高频坑位与对应解法第一个坑tf.function 图模式下直接加密报错。TensorFlow 2.x 大量使用 Eager 模式但不少团队会把训练循环包进tf.function提性能。python-paillier 的加密方法会创建 Python 对象并调用 GMP 的 C 扩展在 AutoGraph 转写过程中直接抛异常。解法是把训练和加密拆成两个阶段训练在图模式内完成加密在退出图模式、进入网络传输之前执行。第二个坑梯度中的 NaN 或 Inf 在加密后不可见。NaN 转整数后变成-9223372036854775808加密解密都能“成功”但聚合结果被污染模型发散后很难定位问题。检查点必须放在客户端加密前就是前面代码里np.all(np.isfinite(flat))的那一步。第三个坑客户端掉线导致密文序列长度不一致。联邦训练中客户端掉线是常态。服务端如果按第一个客户端的长度做聚合缺失位置按 0 补齐但上层逻辑并不知道哪个客户端缺失。工程解法是服务端先做元信息核对客户端在密文 payload 前附加一个明文清单包含参数个数和各层 shape 的哈希哈希不匹配直接判定本轮失败不进入聚合流程。这也是服务端逻辑里最容易忽略的边界条件。5. 用差分隐私补上同态加密盖不住的泄漏面5.1 解密后的模型本身还在泄露信息同态加密隐藏了聚合过程中的梯度但每轮结束后的全局模型仍然以明文下发给参与方。服务端如果积累了多轮明文模型可以做两类攻击成员推断攻击判断某条样本是否在训练集中属性推断攻击猜测训练数据中某个敏感属性的取值。这两类攻击都不需要看到某一方的梯度模型参数和输出分布本身携带了统计性信息。同态加密对这个泄漏面没有任何防护能力因为它保护的是聚合过程不是模型本身。另一个容易被忽略的路径是梯度稀疏化的选择逻辑。Top-K 稀疏化选出的索引集合如果明文传输攻击者可以从索引的聚集模式推断训练数据分布。在意这个泄露面的系统需要把索引集合也纳入加密范围或者改掉动态 Top-K使用与数据无关的固定随机掩码。5.2 本地差分隐私在加密之前注入噪声把差分隐私和同态加密叠加时我习惯先在客户端本地做一次梯度裁剪加噪声再做同态加密。优点是噪声在本地注入服务端即使解密也拿不到原始梯度噪声不经过密文通道不会增加密文体积。一个可以直接抄的参数组合裁剪阈值 C1.0噪声乘子 σ1.1。def clip_and_noise(flat_grad, clip_norm1.0, noise_scale1.1): L2 裁剪 高斯噪声返回可输入加密管线的 float64 数组。 clip_norm: L2 范数裁剪阈值决定单条梯度的敏感度上限 noise_scale: 高斯噪声标准差与裁剪阈值共同决定隐私预算 l2_norm np.linalg.norm(flat_grad) if l2_norm clip_norm: flat_grad flat_grad * (clip_norm / l2_norm) noise np.random.normal(0, noise_scale, sizeflat_grad.shape) return flat_grad noise参数说明裁剪阈值 1.0 加上噪声尺度 1.1在 batch size 32 到 128 的图像分类任务上通常能把精度损失控制在 1% 以内成员推断攻击成功率会被压到接近随机猜测的 0.5。这两组参数必须一起调固定裁剪阈值从 σ0.5 逐步上调同时记录验证集准确率和攻击成功率两条曲线取准确率可接受且攻击成功率接近 0.5 的最小 σ。如果发现梯度裁剪后 L2 范数长期贴在上限边界说明裁剪阈值设小了把 clip_norm 上调到 2.0 再跑一轮直到裁剪生效的比例低于 20%。本文还有配套的精品资源点击获取