十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

纵向联邦学习与标签差分隐私:乳腺癌数据集的隐私保护建模实践

纵向联邦学习与标签差分隐私:乳腺癌数据集的隐私保护建模实践 简介面向信息安全、人工智能及相关专业学生的密码学课程设计资源在 breast cancer 公开数据集上实现纵向联邦学习与标签差分隐私的联合方案覆盖模型构建、隐私参数 epsilon 与正则化参数 lambda 对准确率影响的实验分析适合用于毕设、课设或项目演示。资源共 16 个文件压缩包约 1.19MB包含 Python 源码main.py、model.py、utils.py、依赖与配置说明txt、json、可视化结果图png、jpg、Jupyter 教学笔记ipynb及密码学自选题报告 PDF结构清晰便于对照代码理解完整流程。目前已有 150 余人学习浏览。下载后可从 README 入手结合教程笔记快速复现实验并依据可视化的准确率变化图表进一步分析隐私保护强度与模型效用的权衡是理解联邦学习与差分隐私结合实践的直观参考。1. 为什么要在 breast cancer 数据集上同时做纵向联邦学习与标签差分隐私不少初学者把这道大作业误看成“两门选修课拼盘”纵向联邦学习解决两家医院不交换原始特征也能联合建模标签差分隐私解决病理标签不能被反推患者是否患病。但真正拼在一起才会发现联邦训练过程中的梯度与误差传递恰好就是标签反推最容易落地的攻击面。一道负责任的密码学大作业至少要让你看到这条攻击路径并给出一个可验证的防护闭环。本文围绕 breast cancer 数据集威斯康星乳腺癌二分类展开用两个参与方模拟纵向特征切分用随机响应实现标签差分隐私再用 Paillier 同态加密保护中间 logits最后给出校准和评估方法。适合要交课的本科生也适合想快速对比 VFL LDP 效果的一线算法工程师。2. 在 breast cancer 数据集上纵向切分数据两个参与方各拿一半特征2.1 纵向联邦学习的切分逻辑为什么是切列不是切行横向联邦切的是样本纵向联邦切的是特征。在 breast cancer 数据集中每一行是一位患者的病理测量记录30 个特征共同描述同一例肿瘤样本。假设其中两家机构各有不同检测手段A 机构记录了 mean radius、mean texture 等前半段形态学特征B 机构记录了 worst perimeter、worst area 等后半段指标而病理标签0 为良性1 为恶性只由 B 机构掌握。这就是典型的纵向联邦场景样本 ID 重叠特征被列方向切开。在实现前先明确角色被动方feature party不持有标签主动方label party持有标签两方共同计算一个逻辑回归模型。我们不会把任何一方的原始特征数组完整发送到另一方去双方只交换加密后的中间结果。下面先从最小原型开始把数据准备和切分步骤写清楚。2.2 环境准备conda 虚拟环境与最小依赖包为了不让各种库的版本互相干扰建议先创建一个干净的 Python 环境。下面的命令用 conda 创建名为vfl的环境并安装所需依赖conda create -n vfl python3.8 -y conda activate vfl pip install numpy pandas scikit-learn phe参数说明python3.8是一个兼容性较好的版本phe是python-paillier的安装名提供 Paillier 加法同态加密的公开密钥与私钥实现scikit-learn用来加载 breast cancer 数据和计算评估指标。如果机器上已有 Python 环境直接pip install也可以但用虚拟环境能避免后面训练循环里 NumPy 版本带来的隐性问题。2.3 用 load_breast_cancer 构造两个 party 的本地视图下面这段代码把原始数据集按特征列切成两份并模拟两个参与方各自持有一部分特征。这里直接用sklearn.datasets.load_breast_cancer()特征顺序固定为 scikit-learn 返回的原顺序前 15 列归 A后 15 列归 B。实际业务中如果两家机构使用不同数据库需要在样本 ID 对齐之后再做这一步这里用同一个 NumPy 数组模拟行对齐。import numpy as np from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler data load_breast_cancer() X, y data.data, data.target feature_names data.feature_names # 按特征列顺序做纵向切分 split 15 X_A X[:, :split] # party A 持有前 15 个特征 X_B X[:, split:] # party B 持有后 15 个特征 # 样本 ID 只是一个占位数组用于体现“行对齐” patient_id np.arange(X.shape[0])代码逻辑很简单X_A和X_B的行索引完全一致代表同一批患者列则被分开。注意patient_id在这里没有实际参与运算真实场景中不同机构需要通过安全的样本对齐协议比如基于哈希或密码学 PSI来找出共有患者而本文重点是建模部分。2.4 训练集测试集划分与局部标准化纵向联邦的训练集测试集划分必须在切分之后进行而且要保证 A 和 B 使用相同的样本 ID 集合。最稳妥的方式是先把索引划分好再分别从两边抽取。另外标准化不能使用合并后的全局统计数据否则等价于让双方交换了均值和方差构成统计信息泄露。正确的做法是各参与方只用自己的训练集计算均值和标准差。# 先用同样的 random_state 划分保证两边拿到的样本 ID 一致 X_A_train, X_A_test, X_B_train, X_B_test, y_train, y_test, id_train, id_test \ train_test_split(X_A, X_B, y, patient_id, test_size0.3, random_state42, stratifyy) # 分别 fit 和 transform避免跨方触碰对方数据 sc_A StandardScaler().fit(X_A_train) sc_B StandardScaler().fit(X_B_train) X_A_train_s sc_A.transform(X_A_train) X_A_test_s sc_A.transform(X_A_test) X_B_train_s sc_B.transform(X_B_train) X_B_test_s sc_B.transform(X_B_test) print(A 方训练集形状:, X_A_train_s.shape) print(B 方训练集形状:, X_B_train_s.shape) print(标签形状:, y_train.shape)参数说明stratifyy让训练集和测试集中良性与恶性的比例保持一致避免切分带来的类别分布偏移。random_state42固定随机种子保证 A、B 双方对同一批样本进行切分。标准化对象分别是sc_A和sc_B这符合纵向联邦“本地特征本地统计”的原则。如果把X_A和X_B堆叠起来再做StandardScaler就会引入跨机构隐私泄露这点在代码评审时常被单独考察。2.5 两个参与方的数据视图一览下面的表格直接看参与方持有情况方便对照后文中的训练协议参与方持有特征标签 y角色对外发送的信息party AX[:, :15]前 15 个形态学特征无被动方 / 特征方加密后的本地 logitsparty BX[:, 15:]后 15 个纹理与最差区域特征有主动方 / 标签方加噪后的误差信号 e这里一个容易踩的坑是千万不要把测试集也混入训练前的标准化。只能先 fit 训练集再用训练集得到的均值和方差去 transform 测试集否则测试集信息会以统计量形式进入训练导致评估结果虚高。下一节会对标签做差分隐私扰动翻转后的标签会直接影响训练所以数据准备阶段最好只做当前这一步。3. 标签差分隐私实现用随机响应翻转 y而不是给标签加高斯噪声3.1 为什么二分类标签不能直接加高斯或拉普拉斯噪声标签 y 是离散的 0/1 值如果给每个标签加上一个连续噪声得到的是“0.37”“1.24”这类没有任何监督意义的实数模型无法训练。因此标签差分隐私的常见做法是随机响应以一定概率把样本标签翻转到相反的类别这个翻转过程满足本地化差分隐私并且保留下一个合法的二分类标签。随机响应尤其适合纵向联邦因为翻转可以在标签方本地完成其他参与方即使拿到误差信号也无法区分原始标签和翻转后的标签。3.2 ε 与翻转概率的关系对于一个二分类标签随机响应的翻转概率设计为p_flip 1 / (1 exp(ε))当 ε 0 时p_flip 0.5标签完全随机隐私最强但数据不可用当 ε 趋向无穷大时p_flip趋向 0等价于不扰动。这里的 ε 是差分隐私预算直觉上预算越小噪声越大。比较实用的范围在 0.5 到 2 之间既能提供一定保护又不会让模型精度崩掉。3.3 Python 实现随机响应标签翻转下面这个函数接收原始标签数组和隐私预算 ε返回翻转后的标签与翻转概率import numpy as np def randomize_label(y, epsilon): 对二分类标签做随机响应翻转。 epsilon: 差分隐私预算越小翻转概率越大。 返回: y_random: 翻转后的标签 p_flip : 翻转概率训练后校准会用到 p_flip 1.0 / (1.0 np.exp(epsilon)) flip_mask np.random.random(len(y)) p_flip y_random np.where(flip_mask, 1 - y, y) return y_random, p_flip参数说明np.random.random(len(y))生成[0.0, 1.0)的均匀随机数小于p_flip的位置被翻转。np.where在flip_mask为 True 的位置把 0 变 1、1 变 0。注意这个函数只在训练集上调用测试集不能用翻转后的标签否则评估结果就不代表真实分类能力了。3.4 不同 ε 对应的翻转概率与数据可用性εp_flip 1/(1e^ε)标签保持率 1-p_flip直观影响0.10.4750.525接近一半标签被翻转模型几乎学不到有效信号0.50.3780.622有一定噪声需要更多 epoch 或更大样本1.00.2690.731约四分之一标签被翻转精度仍可接受2.00.1190.881隐私较弱模型精度接近未加噪10.00.0000450.999955几乎无扰动可以视为没有标签保护实际使用中我一般把 ε 当作超参数扫一遍而不是拍脑袋选 1.0。值得强调的一点是随机响应只在训练开始时执行一次不要在每次 epoch 重新翻转否则同一个样本在不同 epoch 会被随机赋给不同标签模型将很难收敛。每次训练运行设置同一个随机种子可以让结果可复现。在纵向联邦场景下执行randomize_label的必须是持有标签的 B 方。A 方不需要知道你到底翻了多少标签只需要从 B 方拿到包含噪声的误差信号。这正好把标签差分隐私和密码学保护串了起来差分隐私抵抗统计推断同态加密抵抗中间链路偷听。下一章把它们放进同一个训练循环。4. 纵向联邦学习训练循环从明文原型到 Paillier 加密聚合4.1 一次迭代的通信协议设计这里以逻辑回归为例模型整体可以写成z X_A w_A X_B w_B。在一个 batch 内通信协议按下面几步进行party A 用本地特征和本地权重计算logit_A X_A[idx] w_A。A 将logit_A加密后发给 BB 计算logit_B X_B[idx] w_B并与解密的logit_A相加得到全局线性输出z。B 计算预测概率p sigmoid(z)和误差e p - y_random。B 用e更新本地权重w_B同时把加噪后的误差发送给 AA 再更新w_A。这个协议把完整的逻辑回归拆成两个本地线性层中间只交换 logits 和误差。密码学的角色是保护第 2 步的 logits让 B 无法直接拿到X_A或w_A标签差分隐私的角色是保护第 3 步和第 4 步的标签信息让 A 即使拿到误差也无法还原出真实的y。这是一个教学级简化方案真正的生产系统会用更严格的安全多方计算协议来保护误差信号但这里的信息链路已经具备“密码学 标签差分隐私”双保险的形态。4.2 明文原型先把联邦逻辑回归跑通先不上密码学写一个朴素明文版本用于验证数据切分和标签翻转正确同时也作为后续加密版的性能基线。def sigmoid(z): z np.clip(z, -30, 30) return 1.0 / (1.0 np.exp(-z)) def vfl_train_plain(X_A, X_B, y, lr0.1, epochs20, batch_size32): 明文版本的纵向联邦逻辑回归训练。 仅在单个进程内模拟两个参与方不涉及加密。 rng np.random.default_rng(0) w_A np.zeros(X_A.shape[1]) w_B np.zeros(X_B.shape[1]) # 训练前只翻转一次标签 y_random, p_flip randomize_label(y, epsilon1.0) for epoch in range(epochs): perm rng.permutation(len(y_random)) total_loss 0.0 for i in range(0, len(perm), batch_size): idx perm[i:i batch_size] # 双方各自计算本地 logits logit_A X_A[idx] w_A logit_B X_B[idx] w_B z logit_A logit_B pred sigmoid(z) e pred - y_random[idx] # 分别更新两个参与方的权重 w_A - lr * (X_A[idx].T e) / len(idx) w_B - lr * (X_B[idx].T e) / len(idx) loss -np.mean(y_random[idx] * np.log(pred 1e-9) (1 - y_random[idx]) * np.log(1 - pred 1e-9)) total_loss loss if (epoch 1) % 5 0: print(fepoch {epoch 1}, loss {total_loss / (len(perm) // batch_size):.4f}, p_flip {p_flip:.3f}) return w_A, w_B参数说明lr0.1是逻辑回归常见的初始学习率如果发现 loss 震荡可以降到 0.05。batch_size32是为了模拟小批量通信实际纵向联邦中每次通信都需要加密和解密batch 太小会让通信开销占比过大太大又容易收敛变慢。代码中的注释明确标出哪些计算发生在 A、哪些发生在 B训练过程中并没有真正发送数据只是把双方本地结果相加这种设计便于后续替换成加密版本。4.3 用 Paillier 做安全聚合同态加法的核心代码Paillier 加密支持密文相加等于明文相加也就是E(m1) * E(m2) E(m1 m2)。在phe库中两个密文对象用运算符即可完成同态加。下面这段代码展示 party A 把本地 logits 加密发给 BB 同态加自己的 logits 后解密from phe import paillier # 生成密钥对n_length2048 是模数位数 public_key, private_key paillier.generate_paillier_keypair(n_length2048) # 模拟 party A 的本地 logits logit_A X_A_train_s[:8] w_A logit_B X_B_train_s[:8] w_B # party A 加密后发送给 B enc_logit_A [public_key.encrypt(x) for x in logit_A] # party B 收到密文与本地明文做同态加 enc_z [c b for c, b in zip(enc_logit_A, logit_B)] # party B 解密得到全局 logits z np.array([private_key.decrypt(c) for c in enc_z])这段代码的关键点有三个。第一public_key.encrypt(x)会在密文中加入随机性所以即使加密相同明文两次结果也不同这是 Paillier 的概率加密特性但解密结果仍然一致。第二c b中b是明文phe库允许密文加明文等价于同态加法。第三解密后的z只有 B 知道A 无法看到全局 logits因此中间链路只暴露部分加权信息不暴露原始特征矩阵。4.4 把误差信号加噪后传给 A纵向联邦与标签差分隐私的结合点在完整训练循环中B 更新完w_B后还需要把误差e发给 A否则 A 无法更新本地权重。但直接发送明文e会泄露标签信息因此这里在误差上叠加 Laplace 噪声再发给 A。这个操作与标签差分隐私的“输出扰动”理念一致也让密码学不再是孤立模块。# B 计算明文误差 pred sigmoid(z) e pred - y_random[:8] # B 更新自己的权重 w_B - 0.1 * (X_B_train_s[:8].T e) / 8 # B 给误差加 Laplace 噪声后发给 A noisy_e e np.random.laplace(loc0.0, scale0.05, sizee.shape) # A 用噪声化误差更新本地权重 w_A - 0.1 * (X_A_train_s[:8].T noisy_e) / 8参数说明np.random.laplace的scale控制噪声强度scale0.05意味着噪声标准差约为 0.05对训练影响较小如果希望隐私更强可以提高到 0.2。这里 A 拿到的不是精确梯度所以模型收敛速度会变慢在验证时需要加大 epoch 或稍调学习率。这种“误差扰动”与第 3 章的标签翻转形成了二级保护即使通信链路被窃听攻击者拿到的也只是翻转后的标签和额外噪声的叠加。4.5 纵向联邦训练的关键参数表下面是整套训练中我比较关注的一组参数适合作为大作业报告的参数说明表参数推荐值说明lr0.05 ~ 0.2梯度噪声较大时建议用较小学习率batch_size32 ~ 128越小加密次数越多越大越接近全批量下降epochs20 ~ 50breast cancer 数据只有 569 条样本太多容易过拟合ε1.0翻转概率约 26.9%隐私与效用的平衡点laplace_scale0.05 ~ 0.2控制误差信号中的额外噪声幅度n_length2048Paillier 密钥模数位数作业可临时降为 1024 提速注意不要把加密版的精度和明文版直接对比后要求完全一致。密码学保护与标签差分隐私都会带来效用损失重点是把损失控制在一个可接受范围内并画出隐私预算与 AUC 的关系曲线。5. 验证标签差分隐私与加密带来的收益AUC、校准和性能开销5.1 翻转标签导致的概率偏移怎么校准确用随机响应翻转标签训练出来的模型输出的概率是“翻转后标签为 1”的概率而不是真实患病概率。举个例子若p_flip 0.2模型输出p 0.8那么真实恶性概率实际上是0.8 * 0.8 0.2 * 0.2 0.68。如果直接用 0.5 作为阈值会高估风险。校准函数需要用到第 3 章随机响应函数返回的p_flipdef calibrate_prob(p, p_flip): 将翻转标签训练得到的概率还原为真实正类概率。 return p * (1 - p_flip) (1 - p) * p_flip逻辑说明p * (1 - p_flip)项表示“模型预测为 1 且标签没有被翻转”的贡献(1 - p) * p_flip项表示“模型预测为 0 但标签被翻转”的贡献。两者相加才是真实标签为 1 的概率。这个校正步骤在报告里非常重要它能直接证明你理解随机响应如何影响模型输出。5.2 用 ε 扫描代替单点手工调参与其只跑一个 ε 值不如写一个小脚本扫一遍观察隐私预算对 AUC 的边际影响。下面这段代码用测试集上的 ROC_AUC 来衡量模型效用from sklearn.metrics import roc_auc_score for eps in [0.1, 0.5, 1.0, 2.0, 5.0]: y_random, p_flip randomize_label(y_train, epsiloneps) w_A, w_B vfl_train_plain(X_A_train_s, X_B_train_s, y_random, lr0.1, epochs30, batch_size32) test_logit X_A_test_s w_A X_B_test_s w_B test_prob sigmoid(test_logit) test_prob_cal calibrate_prob(test_prob, p_flip) auc roc_auc_score(y_test, test_prob_cal) print(feps{eps:.1f}, flip{p_flip:.3f}, AUC{auc:.4f})扫描结果通常呈现一条向下凹的曲线ε 从 5 降到 1 时AUC 损失很小继续降到 0.1AUC 会显著下滑因为标签接近随机。把这条曲线画出来比单独给一个精度值更有说服力。若发现 AUC 在某一档突然崩塌说明该 ε 低于数据可用性的阈值后续报告可以依据这条曲线选择预算。5.3 加密性能的几个直接相关技巧Paillier 加密慢在模指数运算尤其是 2048 位密钥。实际作业里可以先小批量跑通正确性再对全量训练。若觉得速度太慢可以尝试把n_length降到 1024并同时修改训练 epoch 数来观察精度变化。另一个常用的优化是每轮只加密一次批量向量而不是对每条样本逐个加密这在phe里可以用列表推导式直接完成。还有一个细节n_length2048时密文尺寸大约是明文的 4 到 8 倍在记录通信开销时最好测量enc_logit_A的字节数而不是估计值。把这些数值写进文档就能把“密码学大作业”从简单的“调库跑通”提升到“有工程量化”的水准。本文还有配套的精品资源点击获取
返回列表