目录
1.1.3 样本不对齐问题:ID-Mismatch的数学表述与影响分析
1.1.4 国外主流框架对比:FATE-VFL、PyVertical、FedTree架构解析
1.2.1 隐私保护集合求交(PSI)的密码学基础:OT扩展与Diffie-Hellman密钥交换
1.2.2 基于不经意传输(OT)的PSI协议实现:KKRT16优化方案
1.2.3 基于同态加密的PSI:Paillier与BFV方案的性能对比
1.2.4 恶意安全模型下的PSI:OPRF与VOLE-based PSI协议
1.2.5 大规模场景下的PSI优化:分桶并行与硬件加速(Intel IPP/AVX-512)
1.3.1 实体解析(Entity Resolution)在VFL中的技术实现
1.3.2 模糊匹配与隐私保护记录链接(PPRL):Soundex与Bloom Filter方案
1.3.4 特征异构处理:Embedding空间对齐与特征哈希技术
1.3.5 对齐质量评估:Precision-Recall在隐私约束下的度量方法
1.4.1 分裂学习(Split Learning)与VFL的融合架构
1.4.2 安全矩阵乘法协议:Beaver Triples与SPDZ2k实现
1.4.3 梯度泄露攻击(DLG/IG)在VFL中的防御:梯度压缩与稀疏化
1.4.4 基于秘密分享的分层模型训练:ABY3与SecureML协议优化
1.4.5 通信压缩技术:Top-k稀疏化、Sketching与量化编码
第1章 垂直联邦学习(VFL)基础与形式化定义
1.1 垂直联邦学习(VFL)基础与形式化定义
1.1.1 水平联邦、垂直联邦与联邦迁移学习的形式化区分
联邦学习按照数据分布特征可划分为三种基本范式,其区分依据源于参与方数据集在样本空间与特征空间两个维度上的重叠程度。
水平联邦学习(Horizontal Federated Learning, HFL)适用于各参与方拥有相同样本特征但覆盖不同用户群体的场景。在此架构下,各参与方的本地数据集共享特征空间,而样本空间互不重叠。典型应用包括多个移动设备基于各自用户数据协同训练输入法预测模型,或不同地区医院基于相同检查指标但不同患者群体训练疾病诊断模型。训练过程中,各参与方在本地完成模型参数计算后,仅将梯度或参数更新上传至中央服务器进行聚合,原始数据始终保留在本地设备。
垂直联邦学习(Vertical Federated Learning, VFL)则针对参与方拥有相同用户群体但特征互补的场景。该范式下,各参与方共享样本空间,但特征空间存在显著差异。例如,银行与电商平台联合建模时,双方可能拥有同一批客户的不同维度信息:银行掌握资金流水与信用记录,电商平台掌握消费偏好与行为轨迹。VFL的核心挑战在于如何在保护各方原始特征隐私的前提下,实现跨域特征的协同建模。
联邦迁移学习(Federated Transfer Learning, FTL)处理的是样本空间与特征空间均仅有少量重叠的更为复杂场景。当两个数据集在特征空间和样本空间上均存在有限交集时,FTL通过迁移学习技术将源域知识迁移至目标域。具体而言,参与方首先基于重叠样本与特征训练初始模型,随后利用该模型对非重叠样本进行预测或微调。FTL适用于业务性质相似但数据分布差异较大的跨机构协作,如跨国金融机构间的风控模型共建,或不同医疗体系间的诊断知识迁移。

1.1.2 VFL的数学建模:特征空间划分与标签持有方定义
垂直联邦学习的系统架构通常包含两类核心角色:标签持有方(Host)与特征提供方(Guest)。标签持有方拥有目标变量,负责模型训练过程中的损失计算与梯度回传;特征提供方仅拥有特征数据,通过本地计算提取特征表示并上传中间结果。
在特征空间划分层面,VFL采用纵向切分策略。假设总特征空间由多个互不相交的子空间构成,每个参与方持有一个或多个子空间。训练过程中,各参与方首先对本地特征进行前向计算,生成低维度的中间表示(通常称为嵌入向量或隐藏层输出)。这些中间表示被传输至标签持有方进行聚合,形成完整的特征表示后输入后续网络层完成预测。反向传播时,标签持有方将梯度逐层回传至各特征提供方,各方基于本地梯度独立更新模型参数。
该架构的关键设计在于分割神经网络(Split Neural Network, SplitNN)的应用。分割点通常设置在各参与方特征提取网络的输出层,将模型划分为底部网络(本地特征提取)与顶部网络(全局预测聚合)。底部网络保留在数据持有方本地,顶部网络部署于标签持有方或可信第三方服务器。这种分层设计确保了原始特征数据不出域,同时允许各方协同训练端到端模型。

1.1.3 样本不对齐问题:ID-Mismatch的数学表述与影响分析
垂直联邦学习的前提条件是各参与方拥有共同的用户群体,即样本ID存在交集。然而在实际应用中,参与方的用户集合往往并不完全一致,由此产生样本不对齐(ID-Mismatch)问题。该问题表现为:部分用户仅存在于某一参与方的数据集中,而在其他参与方无对应记录;或同一用户在不同系统中的标识符不一致。
样本不对齐对VFL系统的影响体现在三个层面。在数据层面,非交集样本无法参与联合建模,导致有效训练数据量减少,模型泛化能力受损。在通信层面,若未进行预先对齐,各方在训练过程中可能因样本索引错位而产生错误的梯度计算,导致模型收敛异常。在隐私层面,直接交换用户ID列表以确定交集会泄露参与方的用户隐私信息,违背联邦学习的隐私保护初衷。
解决样本不对齐问题的核心技术是隐私集合求交(Private Set Intersection, PSI)。PSI协议允许两个或多个参与方在不泄露各自完整用户集合的前提下,仅计算出集合的交集部分。基于Diffie-Hellman密钥交换的PSI实现结合布隆过滤器压缩,可在保护隐私的同时降低通信开销。协议执行完毕后,各参与方仅保留交集样本的数据记录,并依据统一顺序重新排列,确保后续训练过程中样本索引的一致性。

1.1.4 国外主流框架对比:FATE-VFL、PyVertical、FedTree架构解析
当前国际上支持垂直联邦学习的开源框架主要包括FATE、PyVertical与FedTree,三者在架构设计、模型支持与安全机制方面各具特色。
FATE(Federated AI Technology Enabler)由微众银行主导开发,是目前功能最为完善的联邦学习框架之一。其VFL模块采用异构特征分箱(Hetero Feature Binning)机制处理离散特征,支持基于同态加密的梯度安全聚合。FATE的架构设计强调企业级部署,提供可视化工作流编排、多方安全计算协议栈与完善的权限管理。在垂直场景下,FATE支持逻辑回归、梯度提升决策树(GBDT)与深度神经网络等多种模型,并通过Hetero-NN组件实现分割神经网络的灵活配置。其特征分箱模块采用分位数分箱或等宽分箱策略,结合Paillier同态加密实现跨方特征WOE值与IV值的联合计算。
PyVertical由OpenMined社区开发,是首个专门针对垂直联邦学习的开源框架。该框架基于PySyft构建,采用分割神经网络架构实现特征垂直分布场景下的深度学习。PyVertical的核心创新在于将PSI协议与SplitNN训练流程无缝集成:训练前,数据科学家与各数据拥有方执行PSI确定共同样本;训练中,各方通过本地网络提取特征嵌入并上传,数据科学家负责聚合与损失计算。PyVertical支持多头部(Multi-headed)网络结构,允许两个及以上数据拥有方同时参与特征提取,适用于医疗影像多模态融合等场景。框架采用诚实但好奇(Honest-but-Curious)安全模型,假设各方遵守协议但可能试图从接收到的信息中推断敏感数据。
FedTree专注于树模型在联邦学习场景下的训练,由新加坡国立大学研究团队开发。该系统同时支持水平与垂直联邦的GBDT训练,在VFL场景下采用直方图聚合机制。FedTree的核心设计是基于直方图的知识传递:各参与方在本地构建特征直方图,通过安全聚合协议上传至服务器进行全局最优分裂点计算。为保护标签隐私,FedTree采用加法同态加密对梯度直方图进行加密,确保无标签方无法直接获取标签信息。系统提供命令行接口与Python接口,支持独立模拟与分布式部署两种运行模式,并集成CUDA加速以提升同态加密运算效率。

框架对比层面,FATE提供最为完整的工业化解决方案,涵盖特征工程、模型训练、评估部署全流程,但系统复杂度较高;PyVertical专注于深度学习场景,架构轻量且与PyTorch生态兼容,适合学术研究与小规模概念验证;FedTree则在树模型联邦训练领域具有专业优势,其直方图聚合机制在保障隐私的同时实现了较高的训练效率。三者均支持基于同态加密或安全多方计算的隐私保护机制,但在通信效率、可扩展性与模型类型支持方面存在差异,实际选型需结合具体业务场景、数据规模与安全需求综合评估。
1.2 隐私保护集合求交(PSI)协议体系
1.2.1 隐私保护集合求交(PSI)的密码学基础:OT扩展与Diffie-Hellman密钥交换
隐私保护集合求交(Private Set Intersection, PSI)允许两方在不泄露各自集合元素的前提下计算交集。其密码学基础主要建立在两类原语之上:不经意传输(Oblivious Transfer, OT)及其扩展协议,以及Diffie-Hellman密钥交换机制。
不经意传输扩展(OT Extension)解决了基础OT协议的高通信开销问题。基础1-out-of-2 OT协议中,接收方选择性地获取发送方两个消息中的一个,而发送方无法获知接收方的选择比特。Ishai等人提出的OT扩展技术允许通过执行少量基础OT实例(通常基于公钥加密)来生成大量有效OT实例,将计算复杂度从线性降低至对数级别。KK13协议在此基础上实现了1-out-of-n OT扩展,使单个OT实例可支持任意大的选择空间,消除了OT执行次数与元素位宽的直接关联。
Diffie-Hellman密钥交换在PSI中的应用体现为ECDH-PSI协议。该协议利用椭圆曲线离散对数问题的困难性,双方各自对集合元素进行哈希后映射到椭圆曲线点,执行标量乘法后交换结果。通过比较交换后的曲线点坐标,双方可在不暴露原始元素的情况下确定交集。该方案的优势在于概念简洁、实现直接,且可通过标准椭圆曲线库(如Curve25519、Secp256k1或国密SM2)获得成熟的密码学保障。
实例与代码实现
以下实现展示基于Curve25519的ECDH-PSI协议核心逻辑,包含完整的密钥派生、点乘法优化及交集计算流程:
#!/usr/bin/env python3
"""
ECDH-PSI Protocol Implementation
基于Curve25519的隐私保护集合求交协议实现
脚本内容:
- 椭圆曲线点运算优化
- 双盲哈希机制
- 并行交集计算
- 内存高效的数据结构
使用方式:
1. 准备两方数据集(Sender/Receiver)
2. 分别执行密钥生成与元素盲化
3. 交换盲化后的集合点坐标
4. 本地计算交集
依赖:cryptography, numpy, multiprocessing
"""
import os
import hashlib
import numpy as np
from typing import Set, Tuple, List, Dict
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from multiprocessing import Pool, cpu_count
from collections import defaultdict
import struct
class CurvePoint:
"""优化后的曲线点表示,使用32字节紧凑存储"""
__slots__ = ['x_coord', 'compressed']
def __init__(self, x_coord: bytes, compressed: bool = True):
self.x_coord = x_coord[:32] # Curve25519点压缩表示
self.compressed = compressed
def __hash__(self):
return hash(self.x_coord)
def __eq__(self, other):
return isinstance(other, CurvePoint) and self.x_coord == other.x_coord
class ECDHPSI:
"""
ECDH-PSI协议实现
安全参数:Curve25519,128位安全级别
"""
def __init__(self, curve_name: str = "X25519"):
self.private_key = X25519PrivateKey.generate()
self.public_key = self.private_key.public_key()
self._precomputed_powers = {} # 预计算优化
def _hash_to_curve(self, element: str) -> bytes:
"""
确定性哈希到曲线点
使用SHA3-256避免长度扩展攻击,输出32字节标量
"""
# 域分离:添加协议标识前缀防止跨协议攻击
domain_prefix = b"ECDH-PSI-v1"
hasher = hashlib.sha3_256()
hasher.update(domain_prefix)
hasher.update(element.encode('utf-8'))
# 转换为曲线标量(Clamp处理确保合法私钥)
scalar = bytearray(hasher.digest())
scalar[0] &= 248 # 清除低3位
scalar[31] &= 127 # 清除高位
scalar[31] |= 64 # 设置第6位
return bytes(scalar)
def blind_element(self, element: str) -> CurvePoint:
"""
元素盲化:Hash(element) * private_key
本地执行标量乘法,不泄露原始元素
"""
scalar = self._hash_to_curve(element)
# 加载为私钥对象执行乘法
temp_priv = X25519PrivateKey.from_private_bytes(scalar)
temp_pub = temp_priv.public_key()
# 执行DH计算:scalar * G * private_key
shared = self.private_key.exchange(temp_pub)
return CurvePoint(shared)
def blind_set_parallel(self, elements: List[str], num_workers: int = None) -> List[CurvePoint]:
"""
并行集合盲化优化
利用多核CPU加速大规模数据集处理
"""
if num_workers is None:
num_workers = min(cpu_count(), 8)
# 分块处理以平衡负载
chunk_size = max(1, len(elements) // num_workers)
chunks = [elements[i:i + chunk_size] for i in range(0, len(elements), chunk_size)]
with Pool(num_workers) as pool:
results = pool.map(self._blind_chunk, chunks)
# 扁平化结果
return [point for sublist in results for point in sublist]
def _blind_chunk(self, chunk: List[str]) -> List[CurvePoint]:
"""工作进程执行的块处理函数"""
return [self.blind_element(e) for e in chunk]
def compute_intersection(self, local_blinded: Set[CurvePoint],
remote_blinded: Set[CurvePoint]) -> Set[CurvePoint]:
"""
本地交集计算
双方交换盲化点后,Receiver执行第二次盲化并比较
"""
# 对于Receiver:需要对remote_blinded再次盲化
# 此处假设当前实例为Receiver角色
return local_blinded.intersection(remote_blinded)
def serialize_points(self, points: Set[CurvePoint]) -> bytes:
"""紧凑序列化用于网络传输"""
count = len(points)
header = struct.pack('!I', count) # 4字节大端序长度
body = b''.join([p.x_coord for p in points])
return header + body
def deserialize_points(self, data: bytes) -> Set[CurvePoint]:
"""反序列化接收到的点集合"""
count = struct.unpack('!I', data[:4])[0]
points = set()
offset = 4
for _ in range(count):
x_coord = data[offset:offset+32]
points.add(CurvePoint(x_coord))
offset += 32
return points
# 使用示例与性能测试
def demo_ecdh_psi():
"""完整协议流程演示"""
# 初始化双方
sender = ECDHPSI()
receiver = ECDHPSI()
# 构造测试数据集(模拟联邦学习ID对齐场景)
sender_ids = [f"user_{i:06d}" for i in range(100000) if i % 3 == 0]
receiver_ids = [f"user_{i:06d}" for i in range(100000) if i % 5 == 0]
print(f"Sender集合大小: {len(sender_ids)}")
print(f"Receiver集合大小: {len(receiver_ids)}")
# 阶段1:本地盲化
print("执行本地盲化...")
sender_blinded = sender.blind_set_parallel(sender_ids)
receiver_blinded = receiver.blind_set_parallel(receiver_ids)
# 阶段2:交换公钥与盲化集合(模拟网络传输)
sender_pub = sender.public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
receiver_pub = receiver.public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
# 阶段3:Receiver对Sender的盲化集合进行二次盲化
# 实际协议中,Sender也应对Receiver集合执行相同操作
print("执行二次盲化与交集计算...")
# 模拟交集(实际需完整协议轮次)
expected_intersection = set(sender_ids).intersection(set(receiver_ids))
print(f"预期交集大小: {len(expected_intersection)}")
if __name__ == "__main__":
demo_ecdh_psi()
1.2.2 基于不经意传输(OT)的PSI协议实现:KKRT16优化方案
KKRT16协议(Kolesnikov-Kumaresan-Rosulek-Trieu, CCS 2016)代表了OT扩展PSI的里程碑式优化。该协议通过三项核心技术突破,实现了局域网环境下每秒处理数百万元素的高吞吐率:首先,将IKNP OT扩展中的纠错码替换为伪随机编码,实现了1-out-of-infinity OT扩展,使OT实例数量与元素位宽解耦;其次,提出批量相关密钥OPRF(BaRK-OPRF)构造,允许发送方使用单个密钥种子派生多个相关OPRF实例;第三,引入三路无存储Cuckoo哈希(3-Hash Stash-less Cuckoo Hashing),将集合映射至哈希表时消除stash溢出处理的开销,同时降低通信复杂度。
协议执行流程分为四个阶段:基础OT阶段使用Naor-Pinkas或类似协议生成初始OT关联;OT扩展阶段通过伪随机编码矩阵扩展至所需数量;OPRF评估阶段利用Cuckoo哈希表结构对双方集合进行不经意伪随机函数评估;最终交集通过比较OPRF输出值的哈希碰撞确定。该方案在220 规模数据集上可实现3.8秒完成计算,224 规模数据集可在1分钟内处理完毕,成为后续PSI协议的基准对比方案。
实例与代码实现
以下实现展示KKRT16协议的核心组件,包括BaRK-OPRF构造、Cuckoo哈希表优化及OT扩展矩阵运算:
#!/usr/bin/env python3
"""
KKRT16 PSI Protocol Implementation
基于OT扩展与BaRK-OPRF的高性能隐私集合求交
脚本内容:
- BaRK-OPRF批量伪随机函数评估
- 三路无存储Cuckoo哈希表构造
- IKNP OT扩展协议实现
- 矩阵转置优化(AVX-512模拟)
使用方式:
1. 配置协议参数(集合大小、统计安全性参数)
2. 初始化Sender与Receiver角色
3. 执行OT扩展与OPRF评估
4. 输出交集结果
依赖:numpy, bitarray, hashlib, typing
"""
import numpy as np
import hashlib
import struct
from typing import List, Tuple, Set, Dict, Optional
from bitarray import bitarray
from bitarray.util import int2ba, ba2int
import secrets
from dataclasses import dataclass
from enum import Enum
class Role(Enum):
SENDER = 1
RECEIVER = 2
@dataclass
class OTExtensionParams:
"""OT扩展协议参数配置"""
kappa: int = 128 # 计算安全参数
s: int = 40 # 统计安全参数
base_ot_count: int = 128 # 基础OT数量
code_length: int = 512 # 伪随机编码长度
class PseudorandomCode:
"""
伪随机编码实现(替代传统纠错码)
使用AES-NI优化的多轮哈希构造
"""
def __init__(self, input_len: int, output_len: int):
self.input_len = input_len
self.output_len = output_len
self._key_schedule = None
def encode(self, input_bits: bitarray, seed: bytes) -> bitarray:
"""
伪随机编码:将短输入扩展为长输出
基于 Davies-Meyer 构造:E_seed(x) ^ x
"""
# 分块处理,每块128位(AES块大小)
num_blocks = (self.output_len + 127) // 128
output = bitarray()
for i in range(num_blocks):
# 构造 tweak:seed || block_index
tweak = seed + struct.pack('!Q', i)
# 使用SHA3-256模拟AES(生产环境应使用硬件AES-NI)
hasher = hashlib.sha3_256()
hasher.update(tweak)
hasher.update(input_bits.tobytes())
block = hasher.digest()[:16] # 取前128位
output.frombytes(block)
return output[:self.output_len]
class CuckooHashTable:
"""
三路无存储Cuckoo哈希表
优化版本:无stash设计,通过参数调整保证高负载因子
"""
def __init__(self, num_items: int, stash_size: int = 0):
self.num_items = num_items
# 表大小设置为1.2倍元素数量,确保无stash插入成功率高
self.table_size = int(1.2 * num_items) + 1
self.stash_size = stash_size # KKRT16中通常为0
self.num_hashes = 3 # 三路哈希
# 预计算哈希函数种子
self.hash_seeds = [secrets.token_bytes(32) for _ in range(self.num_hashes)]
self.table: Dict[int, Optional[str]] = {}
self.stash: List[str] = []
def _hash(self, item: str, func_idx: int) -> int:
"""第func_idx个哈希函数"""
hasher = hashlib.blake2b(key=self.hash_seeds[func_idx], digest_size=8)
hasher.update(item.encode())
return int.from_bytes(hasher.digest(), 'big') % self.table_size
def insert(self, items: List[str]) -> bool:
"""
批量插入,使用BFS优化置换路径搜索
返回是否全部成功插入(无stash溢出)
"""
for item in items:
if not self._insert_single(item):
return False
return True
def _insert_single(self, item: str, max_iter: int = 100) -> bool:
"""单个元素插入,支持踢出(kicking)机制"""
current = item
for _ in range(max_iter):
# 尝试三个位置
for i in range(self.num_hashes):
pos = self._hash(current, i)
if pos not in self.table or self.table[pos] is None:
self.table[pos] = current
return True
# 踢出现有元素
if i == self.num_hashes - 1:
kicked = self.table[pos]
self.table[pos] = current
current = kicked
# 放入stash(如果启用)
if len(self.stash) < self.stash_size:
self.stash.append(current)
return True
return False
def get_positions(self, item: str) -> List[int]:
"""获取元素在三路哈希中的所有可能位置"""
return [self._hash(item, i) for i in range(self.num_hashes)]
class BarkOPRF:
"""
批量相关密钥OPRF(BaRK-OPRF)实现
核心优化:通过相关密钥减少通信,批量处理提升吞吐
"""
def __init__(self, params: OTExtensionParams):
self.params = params
self.code = PseudorandomCode(params.kappa, params.code_length)
def receiver_evaluate(self, inputs: List[str],
base_ot_choices: bitarray,
base_ot_received: List[bytes]) -> Dict[str, bytes]:
"""
Receiver端OPRF评估
基于OT扩展矩阵的列选择构造
"""
assert len(base_ot_choices) == self.params.base_ot_count
assert len(base_ot_received) == self.params.base_ot_count
results = {}
num_inputs = len(inputs)
# 构造选择比特矩阵Q(num_inputs × base_ot_count)
# 每行对应一个输入的编码
for idx, item in enumerate(inputs):
# 将输入编码为选择向量
item_bits = self._item_to_bits(item, self.params.base_ot_count)
# 计算Q行:与基础OT选择比特异或
q_row = bitarray()
for i in range(self.params.base_ot_count):
q_row.append(item_bits[i] ^ base_ot_choices[i])
# 派生OPRF输出:Hash(Q_row, item)
oprf_output = self._derive_output(q_row, item, base_ot_received)
results[item] = oprf_output
return results
def sender_evaluate(self, inputs: List[str],
base_ot_secrets: List[Tuple[bytes, bytes]]) -> Dict[str, bytes]:
"""
Sender端OPRF评估
持有Delta密钥种子,可计算所有可能行的PRF值
"""
results = {}
# 生成Delta(相关密钥种子)
delta = secrets.token_bytes(32)
for item in inputs:
# 计算所有2^kappa可能性的PRF(优化:仅计算实际需要的)
item_bits = self._item_to_bits(item, self.params.base_ot_count)
# 实际实现中应使用多项式插值或PaXoS优化
oprf_output = self._sender_derive(item, item_bits, delta, base_ot_secrets)
results[item] = oprf_output
return results
def _item_to_bits(self, item: str, length: int) -> bitarray:
"""将字符串项目映射为固定长度比特串"""
hasher = hashlib.sha256()
hasher.update(item.encode())
digest = hasher.digest()
bits = bitarray()
bits.frombytes(digest)
return bits[:length]
def _derive_output(self, q_row: bitarray, item: str,
ot_received: List[bytes]) -> bytes:
"""从Q行和基础OT接收值派生OPRF输出"""
# 矩阵点积模拟:Q_row · OT_received
hasher = hashlib.sha3_256()
hasher.update(q_row.tobytes())
hasher.update(item.encode())
# 混合OT接收值(模拟密钥派生)
for i, bit in enumerate(q_row):
if bit:
hasher.update(ot_received[i])
return hasher.digest()
def _sender_derive(self, item: str, item_bits: bitarray,
delta: bytes, base_secrets: List[Tuple[bytes, bytes]]) -> bytes:
"""Sender端派生,利用Delta计算相关密钥"""
hasher = hashlib.sha3_256()
hasher.update(delta)
hasher.update(item.encode())
# 根据item_bits选择T矩阵行
for i, bit in enumerate(item_bits):
chosen = base_secrets[i][1] if bit else base_secrets[i][0]
hasher.update(chosen)
return hasher.digest()
class KKRT16PSI:
"""
KKRT16 PSI协议完整实现
整合OT扩展、Cuckoo哈希与BaRK-OPRF
"""
def __init__(self, role: Role, params: Optional[OTExtensionParams] = None):
self.role = role
self.params = params or OTExtensionParams()
self.oprf = BarkOPRF(self.params)
self.cuckoo: Optional[CuckooHashTable] = None
def setup_cuckoo(self, items: List[str]):
"""初始化Cuckoo哈希表用于集合存储"""
self.cuckoo = CuckooHashTable(len(items))
success = self.cuckoo.insert(items)
if not success:
raise RuntimeError("Cuckoo哈希插入失败,需调整表大小参数")
def execute_psi(self, local_items: List[str],
remote_oprf_outputs: Optional[Dict[str, bytes]] = None) -> Set[str]:
"""
执行PSI协议核心逻辑
实际实现需包含网络通信层协调
"""
self.setup_cuckoo(local_items)
if self.role == Role.RECEIVER:
# Receiver:执行OPRF评估,发送盲化值
# 模拟基础OT结果(实际应从Sender获取)
mock_choices = bitarray(self.params.base_ot_count)
mock_choices.setall(0)
mock_received = [secrets.token_bytes(32) for _ in range(self.params.base_ot_count)]
oprf_results = self.oprf.receiver_evaluate(local_items, mock_choices, mock_received)
# 实际协议中:发送OPRF输出给Sender比较
return set(oprf_results.keys())
else: # SENDER
# Sender:评估所有Cuckoo位置的OPRF值
mock_secrets = [(secrets.token_bytes(32), secrets.token_bytes(32))
for _ in range(self.params.base_ot_count)]
oprf_results = self.oprf.sender_evaluate(local_items, mock_secrets)
# 比较Receiver发来的OPRF值
if remote_oprf_outputs:
intersection = set()
local_set = set(oprf_results.values())
for item, oprf_val in remote_oprf_outputs.items():
if oprf_val in local_set:
intersection.add(item)
return intersection
return set()
def demo_kkrt16():
"""KKRT16协议性能演示"""
params = OTExtensionParams(kappa=128, s=40, base_ot_count=128)
# 构造大规模测试集
size = 10000
sender_items = [f"entity_{secrets.token_hex(8)}" for _ in range(size)]
receiver_items = [f"entity_{secrets.token_hex(8)}" for _ in range(size)]
# 构造30%交集
intersection_size = 3000
common = [f"common_{i:08d}" for i in range(intersection_size)]
sender_items[:intersection_size] = common
receiver_items[:intersection_size] = common
print(f"数据集规模: {size}")
print(f"预期交集: {intersection_size}")
# 初始化协议实例
sender = KKRT16PSI(Role.SENDER, params)
receiver = KKRT16PSI(Role.RECEIVER, params)
# 执行协议(模拟)
print("执行Cuckoo哈希构造...")
sender.setup_cuckoo(sender_items)
receiver.setup_cuckoo(receiver_items)
print("协议初始化完成,实际网络轮次需配合通信层")
if __name__ == "__main__":
demo_kkrt16()
1.2.3 基于同态加密的PSI:Paillier与BFV方案的性能对比
同态加密(Homomorphic Encryption, HE)为PSI提供了无需交互的替代路径,特别适用于通信受限或需要后续密态计算的场景。Paillier方案作为部分同态加密(PHE)代表,支持无限次密文加法与单次明文乘法,其安全性基于决策性合数剩余假设,在密钥生成与加密效率上具有显著优势。BFV(Brakerski-Fan-Vercauteren)方案属于层级全同态加密(Leveled FHE),支持有限次数的密文乘加混合运算,基于环上容错学习(RLWE)问题,提供语义安全与电路隐私保护。
性能维度对比显示,Paillier在加法运算与密钥生成阶段具有最低延迟,适合简单求交后的聚合统计;BFV在批量处理与SIMD编码(通过明文打包技术)场景下展现更高吞吐,但单次加密开销显著高于Paillier。OpenFHE库的最新评估表明,在32位整数运算场景下,BFV的加密延迟约270ms(AWS t3.medium),解密约16ms,而Paillier的加密操作更快但密文膨胀率更高。对于联邦学习中的ID对齐任务,Paillier适用于小规模、低延迟需求;BFV适用于需要后续密态特征聚合的复杂流程。
实例与代码实现
以下实现对比Paillier与BFV在PSI场景下的核心操作性能,包含完整的密钥管理、密文打包与批量交集计算:
#!/usr/bin/env python3
"""
Homomorphic Encryption PSI Comparison
Paillier vs BFV方案在隐私集合求交中的性能对比
脚本内容:
- Paillier部分同态加密实现(phe库)
- BFV层级全同态加密实现(ten seal/openfhe模拟)
- 密文打包(Batching)优化技术
- 批量交集计算性能基准测试
使用方式:
1. 安装依赖:pip install phe tenseal
2. 配置安全参数(密钥长度、多项式模数)
3. 执行对比测试:python he_psi_comparison.py
4. 分析时延、吞吐与密文膨胀指标
依赖:phe, tenseal, numpy, time, psutil
"""
import time
import numpy as np
import psutil
import os
from typing import List, Tuple, Dict, Set
from phe import paillier
from phe.util import invert, powmod, getprimeover, isqrt
import tenseal as ts
import hashlib
import struct
from dataclasses import dataclass
from collections import defaultdict
@dataclass
class HEPerformanceMetrics:
"""同态加密性能指标"""
scheme: str
keygen_time_ms: float
encrypt_time_ms: float
decrypt_time_ms: float
add_time_ms: float
multiply_time_ms: float
ciphertext_size_bytes: int
plaintext_size_bytes: int
memory_peak_mb: float
class PaillierPSI:
"""
Paillier-based PSI实现
利用加法同态性:E(a) * E(b) = E(a+b),通过密文比较实现交集检测
"""
def __init__(self, key_size: int = 2048):
self.key_size = key_size
self.public_key = None
self.private_key = None
self._keygen_time = 0
def generate_keys(self) -> Tuple[paillier.PaillierPublicKey, paillier.PaillierPrivateKey]:
"""密钥生成,记录时延"""
start = time.perf_counter()
self.public_key, self.private_key = paillier.generate_paillier_keypair(n_length=self.key_size)
self._keygen_time = (time.perf_counter() - start) * 1000
return self.public_key, self.private_key
def hash_to_scalar(self, item: str, max_val: int) -> int:
"""将字符串映射为Paillier明文空间内的整数"""
hasher = hashlib.sha256()
hasher.update(item.encode())
digest = int.from_bytes(hasher.digest(), 'big')
return digest % max_val
def encrypt_set(self, items: List[str]) -> List[paillier.EncryptedNumber]:
"""
批量加密集合元素
优化:预计算随机数以加速加密
"""
if not self.public_key:
raise RuntimeError("Keys not generated")
n = self.public_key.n
max_val = n // 2 # 确保在合法明文空间内
encrypted = []
for item in items:
scalar = self.hash_to_scalar(item, max_val)
# Paillier加密:c = g^m * r^n mod n^2
ctxt = self.public_key.encrypt(scalar)
encrypted.append(ctxt)
return encrypted
def compute_encrypted_intersection_indicator(self,
local_ctxt: List[paillier.EncryptedNumber],
remote_ctxt: List[paillier.EncryptedNumber]) -> List[int]:
"""
同态交集指示计算
通过密文减法后解密判断是否为0(相同元素)
实际协议需配合零知识证明防止泄露
"""
indicators = []
for lc in local_ctxt:
for rc in remote_ctxt:
# 同态减法:E(a) * E(b)^{-1} = E(a-b)
diff = lc - rc # phe库重载了减法
decrypted = self.private_key.decrypt(diff)
# 解密后为0表示元素相等
indicators.append(1 if decrypted == 0 else 0)
return indicators
class BFVPSI:
"""
BFV-based PSI实现
利用SIMD批量编码与层级同态运算
支持密文打包:单个密文可编码多个明文槽(slot)
"""
def __init__(self, poly_modulus_degree: int = 8192,
plain_modulus: int = 1032193,
coeff_mod_bit_sizes: List[int] = None):
self.poly_modulus_degree = poly_modulus_degree
self.plain_modulus = plain_modulus
self.coeff_mod_bit_sizes = coeff_mod_bit_sizes or [40, 20, 40]
self.context = None
self.public_key = None
self.secret_key = None
self._keygen_time = 0
def generate_context(self):
"""生成TenSEAL BFV上下文"""
start = time.perf_counter()
self.context = ts.context(
ts.SCHEME_TYPE.BFV,
poly_modulus_degree=self.poly_modulus_degree,
plain_modulus=self.plain_modulus,
coeff_mod_bit_sizes=self.coeff_mod_bit_sizes
)
self.context.generate_galois_keys()
self.context.generate_relin_keys()
self._keygen_time = (time.perf_counter() - start) * 1000
return self.context
def encode_and_encrypt(self, items: List[str], batch_size: int = None) -> List[ts.BFVVector]:
"""
批量编码与加密
利用BFV的SIMD特性:poly_modulus_degree/2 个槽位
"""
if not self.context:
raise RuntimeError("Context not initialized")
slots = self.poly_modulus_degree // 2
if batch_size is None:
batch_size = slots
# 将字符串映射为模plain_modulus的整数向量
def item_to_vector(items_batch: List[str]) -> List[int]:
vec = []
for item in items_batch:
hasher = hashlib.sha256()
hasher.update(item.encode())
val = int.from_bytes(hasher.digest()[:4], 'big') % self.plain_modulus
vec.append(val)
# 填充至slots长度
vec.extend([0] * (slots - len(vec)))
return vec
encrypted_batches = []
for i in range(0, len(items), batch_size):
batch = items[i:i+batch_size]
vec = item_to_vector(batch)
# 编码为明文多项式并加密
ptxt = ts.bfv_vector(self.context, vec)
encrypted_batches.append(ptxt)
return encrypted_batches
def compute_intersection_homomorphically(self,
local_batches: List[ts.BFVVector],
remote_batches: List[ts.BFVVector]) -> List[List[int]]:
"""
BFV同态交集计算
通过密文等值测试多项式实现(简化版)
实际使用需构造Fermat小定理等值测试电路
"""
results = []
for lb, rb in zip(local_batches, remote_batches):
# 同态减法
diff = lb - rb
# 解密后检查零值(实际协议中不应直接解密)
decrypted = diff.decrypt()
# 标记零值位置为交集
indicators = [1 if x == 0 else 0 for x in decrypted]
results.append(indicators)
return results
class HEPerformanceBenchmark:
"""
同态加密PSI方案性能基准测试
对比Paillier与BFV在关键指标上的差异
"""
def __init__(self, test_sizes: List[int] = [100, 1000, 10000]):
self.test_sizes = test_sizes
self.results: List[HEPerformanceMetrics] = []
def benchmark_paillier(self, size: int) -> HEPerformanceMetrics:
"""Paillier性能测试"""
process = psutil.Process(os.getpid())
mem_before = process.memory_info().rss / 1024 / 1024
psi = PaillierPSI(key_size=2048)
# 密钥生成
pk, sk = psi.generate_keys()
# 构造测试数据
items_a = [f"user_{i:08d}" for i in range(size)]
items_b = [f"user_{i:08d}" for i in range(size//2, size + size//2)]
# 加密时延
t_start = time.perf_counter()
ctxt_a = psi.encrypt_set(items_a)
encrypt_time = (time.perf_counter() - t_start) * 1000 / size
# 解密时延(抽样)
t_start = time.perf_counter()
_ = [sk.decrypt(c) for c in ctxt_a[:10]]
decrypt_time = (time.perf_counter() - t_start) * 1000 / 10
# 加法时延
t_start = time.perf_counter()
_ = ctxt_a[0] + ctxt_a[1]
add_time = (time.perf_counter() - t_start) * 1000
# 密文大小
sample_ctxt = ctxt_a[0].ciphertext(be_secure=False)
ctxt_size = len(str(sample_ctxt).encode())
mem_after = process.memory_info().rss / 1024 / 1024
return HEPerformanceMetrics(
scheme="Paillier-2048",
keygen_time_ms=psi._keygen_time,
encrypt_time_ms=encrypt_time,
decrypt_time_ms=decrypt_time,
add_time_ms=add_time,
multiply_time_ms=-1, # Paillier不支持密文乘
ciphertext_size_bytes=ctxt_size,
plaintext_size_bytes=8,
memory_peak_mb=mem_after - mem_before
)
def benchmark_bfv(self, size: int) -> HEPerformanceMetrics:
"""BFV性能测试"""
process = psutil.Process(os.getpid())
mem_before = process.memory_info().rss / 1024 / 1024
# 使用较小参数以加速测试
psi = BFVPSI(poly_modulus_degree=4096, plain_modulus=1032193)
ctx = psi.generate_context()
# 构造测试数据(利用SIMD批量处理)
batch_size = 2048 # 4096/2 slots
items = [f"entity_{i:08d}" for i in range(size)]
# 加密时延(批量)
t_start = time.perf_counter()
batches = psi.encode_and_encrypt(items, batch_size)
encrypt_time = (time.perf_counter() - t_start) * 1000 / size
# 解密时延
t_start = time.perf_counter()
_ = [b.decrypt() for b in batches[:2]]
decrypt_time = (time.perf_counter() - t_start) * 1000 / (2 * batch_size)
# 加法时延(批量)
t_start = time.perf_counter()
_ = batches[0] + batches[1] if len(batches) > 1 else batches[0] + batches[0]
add_time = (time.perf_counter() - t_start) * 1000
# 密文大小估计(序列化后)
sample_bytes = batches[0].serialize() if hasattr(batches[0], 'serialize') else b'0'
ctxt_size = len(sample_bytes)
mem_after = process.memory_info().rss / 1024 / 1024
return HEPerformanceMetrics(
scheme="BFV-4096",
keygen_time_ms=psi._keygen_time,
encrypt_time_ms=encrypt_time,
decrypt_time_ms=decrypt_time,
add_time_ms=add_time,
multiply_time_ms=-1, # 测试环境可能不支持
ciphertext_size_bytes=ctxt_size,
plaintext_size_bytes=4,
memory_peak_mb=mem_after - mem_before
)
def run_full_benchmark(self):
"""执行完整对比测试"""
print("=" * 80)
print("同态加密PSI方案性能对比测试")
print("=" * 80)
for size in self.test_sizes:
print(f"\n测试规模: {size} 元素")
print("-" * 40)
# Paillier测试
print("测试Paillier方案...")
paillier_metrics = self.benchmark_paillier(size)
self.results.append(paillier_metrics)
self._print_metrics(paillier_metrics)
# BFV测试
print("测试BFV方案...")
bfv_metrics = self.benchmark_bfv(size)
self.results.append(bfv_metrics)
self._print_metrics(bfv_metrics)
# 对比分析
self._compare_metrics(paillier_metrics, bfv_metrics)
def _print_metrics(self, m: HEPerformanceMetrics):
"""打印性能指标"""
print(f"\n方案: {m.scheme}")
print(f" 密钥生成: {m.keygen_time_ms:.2f} ms")
print(f" 单元素加密: {m.encrypt_time_ms:.2f} ms")
print(f" 单元素解密: {m.decrypt_time_ms:.2f} ms")
print(f" 密文加法: {m.add_time_ms:.2f} ms")
print(f" 密文膨胀率: {m.ciphertext_size_bytes / m.plaintext_size_bytes:.1f}x")
print(f" 内存峰值: {m.memory_peak_mb:.2f} MB")
def _compare_metrics(self, p: HEPerformanceMetrics, b: HEPerformanceMetrics):
"""对比分析"""
print(f"\n对比分析:")
print(f" 加密速度比 (Paillier/BFV): {p.encrypt_time_ms/b.encrypt_time_ms:.2f}x")
print(f" 密文膨胀比 (Paillier/BFV): {(p.ciphertext_size_bytes/p.plaintext_size_bytes)/(b.ciphertext_size_bytes/b.plaintext_size_bytes):.2f}x")
def demo_he_comparison():
"""演示入口"""
benchmark = HEPerformanceBenchmark(test_sizes=[1000, 5000])
benchmark.run_full_benchmark()
if __name__ == "__main__":
demo_he_comparison()
1.2.4 恶意安全模型下的PSI:OPRF与VOLE-based PSI协议
半诚实安全模型假设参与方严格遵守协议但试图从视图中推断额外信息,而恶意安全模型需抵御任意偏离协议规范的攻击者,包括输入操纵、消息篡改与提前终止。OPRF-based PSI在恶意模型下的核心挑战是确保发送方无法区分接收方真实输入与随机猜测,同时防止接收方通过重放攻击推断发送方集合。
VOLE(Vector Oblivious Linear Evaluation)作为新型密码学原语,为恶意安全PSI提供了高效路径。VOLE允许发送方获得向量B 与标量Δ ,接收方获得向量A 与C ,满足关系C=Δ⋅A+B 。基于VOLE的OPRF构造利用PaXoS(Point-and-Permute XOR-Sets)数据结构解决线性系统,实现恶意安全下的伪随机函数评估。Rindal与Schoppmann提出的VOLE-PSI协议在220 规模数据集上,恶意安全版本仅需6.2秒与59MB通信,较半诚实版本 overhead 低于15%,达到450比特/元素的通信效率,为当前最优公开结果。
恶意安全增强技术包括:一致性检查确保VOLE关联参数的有效性;可编程OPRF(OPPRF)支持接收方编程特定输入点的输出值,用于Circuit-PSI扩展;以及基于LPN(Learning Parity with Noise)的伪随机相关生成器,降低VOLE生成阶段的计算开销。
实例与代码实现
以下实现展示基于VOLE的恶意安全OPRF与PSI协议核心逻辑,包含PaXoS数据结构、一致性验证与OPPRF扩展:
#!/usr/bin/env python3
"""
Malicious-Secure VOLE-based PSI Implementation
基于Vector-OLE与PaXoS的恶意安全PSI协议
脚本内容:
- VOLE相关生成与一致性验证
- PaXoS(Point-and-Permute XOR-Sets)数据结构
- 恶意安全OPRF评估
- Circuit-PSI扩展(OPPRF)
使用方式:
1. 配置有限域参数(素数模数、向量维度)
2. 初始化VOLE生成器(基于LPN或OT)
3. 执行PaXoS编码与OPRF评估
4. 验证恶意行为检测机制
依赖:numpy, galois(有限域运算), hashlib, py_ecc(椭圆曲线)
"""
import numpy as np
import hashlib
import secrets
from typing import List, Tuple, Dict, Set, Optional, Callable
from dataclasses import dataclass
from abc import ABC, abstractmethod
import struct
# 使用Python原生大整数模拟有限域运算(生产环境应使用galois库)
class FiniteField:
"""素域GF(P)运算封装"""
def __init__(self, prime: int):
self.p = prime
def add(self, a: int, b: int) -> int:
return (a + b) % self.p
def sub(self, a: int, b: int) -> int:
return (a - b) % self.p
def mul(self, a: int, b: int) -> int:
return (a * b) % self.p
def inv(self, a: int) -> int:
"""费马小定理求逆"""
return pow(a, self.p - 2, self.p)
@dataclass
class VOLECorrelation:
"""VOLE关联参数"""
Delta: int # 发送方持有的标量密钥
A: List[int] # 接收方持有的向量
B: List[int] # 发送方持有的向量
C: List[int] # 接收方持有的向量(满足 C = Delta * A + B)
field: FiniteField
class PaXoS:
"""
PaXoS(Point-and-Permute XOR-Sets)数据结构
用于高效求解稀疏线性系统,支持恶意安全验证
"""
def __init__(self, field: FiniteField, num_keys: int,
num_columns: int, hash_funcs: int = 3):
self.field = field
self.num_keys = num_keys
self.num_columns = num_columns
self.hash_funcs = hash_funcs
self.matrix = [[0] * num_columns for _ in range(num_keys)]
self.values = [0] * num_keys
def encode(self, keys: List[str], values: List[int]) -> List[int]:
"""
编码阶段:求解线性系统 M * P = V
返回参数向量P,使得对于每个key,Decode(P, key) = value
"""
# 构造稀疏矩阵(基于哈希位置)
for i, key in enumerate(keys):
positions = self._get_positions(key)
for pos in positions:
self.matrix[i][pos % self.num_columns] = self.field.add(
self.matrix[i][pos % self.num_columns], 1
)
self.values[i] = values[i]
# 使用高斯消元或迭代法求解(简化版)
# 实际实现应使用Wiedemann算法或类似稀疏求解器
P = self._solve_sparse_system()
return P
def decode(self, P: List[int], key: str) -> int:
"""解码:计算行向量与P的点积"""
positions = self._get_positions(key)
result = 0
for pos in positions:
result = self.field.add(result, P[pos % self.num_columns])
return result
def _get_positions(self, key: str) -> List[int]:
"""多哈希函数位置计算"""
positions = []
for i in range(self.hash_funcs):
hasher = hashlib.blake2b(key=str(i).encode(), digest_size=8)
hasher.update(key.encode())
pos = int.from_bytes(hasher.digest(), 'big') % self.num_columns
positions.append(pos)
return positions
def _solve_sparse_system(self) -> List[int]:
"""简化的高斯消元(实际应使用优化算法)"""
# 返回随机解(演示用)
return [secrets.randbelow(self.field.p) for _ in range(self.num_columns)]
class MaliciousOPRF:
"""
恶意安全OPRF实现
基于VOLE与PaXoS,包含一致性验证机制
"""
def __init__(self, field: FiniteField, vole: VOLECorrelation):
self.field = field
self.vole = vole
self.paxos = PaXoS(field, num_keys=1024, num_columns=2048)
def receiver_encode(self, inputs: List[str]) -> Tuple[List[int], List[int], bytes]:
"""
接收方编码阶段
返回:A'向量(调整后的PaXoS参数)、C向量、一致性证明
"""
# 对输入进行哈希映射到域元素
hashed_inputs = [self._hash_to_field(x) for x in inputs]
# 使用PaXoS编码输入
D = self.paxos.encode(inputs, hashed_inputs) # D为PaXoS参数
# 调整A向量:A = D + A'
A_prime = [self.field.sub(d, a) for d, a in zip(D, self.vole.A[:len(D)])]
# 计算一致性证明(Fiat-Shamir变换)
proof = self._compute_consistency_proof(A_prime, self.vole.C)
return A_prime, self.vole.C[:len(D)], proof
def sender_evaluate(self, A_prime: List[int], proof: bytes) -> Dict[str, int]:
"""
发送方评估阶段
验证一致性证明后计算OPRF输出
"""
# 验证VOLE一致性
if not self._verify_consistency(A_prime, proof):
raise SecurityException("Consistency check failed - possible malicious behavior")
# 重构K向量:K = B + A * Delta
A = [self.field.add(ap, a) for ap, a in zip(A_prime, self.vole.A[:len(A_prime)])]
K = [self.field.add(b, self.field.mul(a, self.vole.Delta))
for b, a in zip(self.vole.B[:len(A)], A)]
# 计算OPRF输出:F(x) = H(Decode(K, x) - Delta * H^F(x))
results = {}
# 实际应遍历发送方集合计算
return results
def _hash_to_field(self, x: str) -> int:
"""随机预言机映射到有限域"""
hasher = hashlib.sha3_256()
hasher.update(x.encode())
digest = int.from_bytes(hasher.digest(), 'big')
return digest % self.field.p
def _compute_consistency_proof(self, A_prime: List[int], C: List[int]) -> bytes:
"""计算零知识一致性证明(简化版)"""
hasher = hashlib.sha3_256()
for a, c in zip(A_prime, C):
hasher.update(struct.pack('!Q', a))
hasher.update(struct.pack('!Q', c))
return hasher.digest()
def _verify_consistency(self, A_prime: List[int], proof: bytes) -> bool:
"""验证证明(实际应使用Sigma协议或SNARKs)"""
computed = self._compute_consistency_proof(A_prime, self.vole.C[:len(A_prime)])
return computed == proof
class CircuitPSI:
"""
Circuit-PSI扩展
输出秘密共享的交集,支持后续MPC计算
"""
def __init__(self, oprf: MaliciousOPRF):
self.oprf = oprf
def compute_secret_shared_intersection(self,
sender_set: List[str],
receiver_set: List[str]) -> Tuple[List[int], List[int]]:
"""
计算秘密共享的交集
输出为两方分别持有的加法共享值
"""
# 执行OPRF评估
# 使用Cuckoo哈希与PaXoS编程OPRF输出
# 最终交集项的共享值为(0,0),非交集为随机值
# 模拟共享输出
sender_share = [secrets.randbelow(self.oprf.field.p) for _ in range(len(sender_set))]
receiver_share = [secrets.randbelow(self.oprf.field.p) for _ in range(len(receiver_set))]
return sender_share, receiver_share
class SecurityException(Exception):
"""恶意行为检测异常"""
pass
class VOLEGenerator:
"""
VOLE关联生成器
基于LPN假设的伪随机相关生成(简化实现)
"""
def __init__(self, field: FiniteField, vector_len: int):
self.field = field
self.vector_len = vector_len
def generate(self) -> VOLECorrelation:
"""生成VOLE关联参数"""
Delta = secrets.randbelow(self.field.p)
A = [secrets.randbelow(self.field.p) for _ in range(self.vector_len)]
B = [secrets.randbelow(self.field.p) for _ in range(self.vector_len)]
C = [self.field.add(self.field.mul(a, Delta), b) for a, b in zip(A, B)]
return VOLECorrelation(Delta, A, B, C, self.field)
def demo_malicious_psi():
"""恶意安全PSI演示"""
# 初始化有限域(使用大素数)
prime = (1 << 61) - 1 # 梅森素数,适合快速模运算
field = FiniteField(prime)
print("初始化VOLE生成器...")
vole_gen = VOLEGenerator(field, vector_len=2048)
vole = vole_gen.generate()
print("构造恶意安全OPRF...")
oprf = MaliciousOPRF(field, vole)
# 构造测试集合
sender_items = [f"malicious_test_{i}" for i in range(1000)]
receiver_items = [f"malicious_test_{i}" for i in range(500, 1500)]
print("执行接收方编码...")
A_prime, C, proof = oprf.receiver_encode(receiver_items)
print("执行发送方评估与验证...")
try:
# 正常执行
result = oprf.sender_evaluate(A_prime, proof)
print("一致性验证通过")
except SecurityException as e:
print(f"检测到恶意行为: {e}")
# 模拟篡改攻击
print("\n模拟篡改攻击...")
tampered_A = [x + 1 for x in A_prime] # 篡改A'
try:
oprf.sender_evaluate(tampered_A, proof)
except SecurityException:
print("成功检测并阻止篡改攻击")
if __name__ == "__main__":
demo_malicious_psi()
1.2.5 大规模场景下的PSI优化:分桶并行与硬件加速(Intel IPP/AVX-512)
联邦学习场景常涉及千万级样本的ID对齐,对PSI协议的计算与通信效率提出极高要求。分桶并行策略通过将大集合哈希映射至多个小桶,使各桶独立执行PSI子协议,实现水平扩展。优化要点包括:基于统计分布的桶大小均衡,避免长尾延迟;局部敏感哈希(LSH)预处理提升相似元素聚合效率;以及跨桶负载均衡调度,最大化多核CPU利用率。
硬件加速层面,Intel Integrated Performance Primitives(IPP)库提供经AVX-512指令集优化的密码学原语,包括大整数模幂运算、AES-NI加速对称加密与CRC32哈希。AVX-512的512位寄存器宽度允许单指令处理8个64位整数或16个32位整数,配合掩码操作与融合乘加(FMA)指令,可将OT扩展中的矩阵转置、有限域运算等瓶颈操作提速3-5倍。具体优化包括:使用VPALIGNR与VPERM2I128实现128位块级矩阵转置;利用VPCLMULQDQ加速Galois域乘法;以及通过VPGATHERDD实现不规则内存访问向量化。
实例与代码实现
以下实现展示分桶并行PSI架构与AVX-512优化的密码学原语模拟(使用NumPy向量化作为AVX-512抽象),包含负载均衡调度与硬件加速感知的数据流:
#!/usr/bin/env python3
"""
Large-Scale PSI Optimization with Bucketing and Hardware Acceleration
分桶并行与AVX-512硬件加速的大规模PSI优化
脚本内容:
- 一致性哈希分桶策略
- 多进程并行桶处理
- AVX-512向量化运算模拟(NumPy)
- Intel IPP密码学原语接口
- 内存池与零拷贝优化
使用方式:
1. 配置分桶参数(桶数量、负载因子阈值)
2. 检测CPU特性(AVX-512F/CD/VL/BW/DQ)
3. 执行分桶哈希与并行PSI
4. 合并桶级结果并验证
依赖:numpy, multiprocessing, cpuinfo, psutil, hashlib
"""
import numpy as np
import multiprocessing as mp
from multiprocessing import Pool, shared_memory
from typing import List, Dict, Tuple, Set, Callable, Optional
from dataclasses import dataclass
from collections import defaultdict
import hashlib
import struct
import time
import os
import psutil
from cpuinfo import get_cpu_info
import secrets
from concurrent.futures import ProcessPoolExecutor, as_completed
@dataclass
class BucketConfig:
"""分桶配置参数"""
num_buckets: int = 256 # 桶数量,通常为2的幂
target_load_factor: float = 1.2 # 目标负载因子
max_bucket_size: int = 10000 # 单桶最大元素数
use_avx512: bool = True # 启用AVX-512优化
num_workers: int = None # 并行工作进程数
class ConsistentHashBucketer:
"""
一致性哈希分桶器
最小化集合变化时的桶间数据迁移
"""
def __init__(self, config: BucketConfig):
self.config = config
self.ring_size = 2**32 # 哈希环大小
self.virtual_nodes = 150 # 每个物理桶的虚拟节点数
self.ring: Dict[int, int] = {} # 哈希环:位置 -> 桶ID
# 构建哈希环
for bucket_id in range(config.num_buckets):
for replica in range(self.virtual_nodes):
key = f"bucket_{bucket_id}_replica_{replica}"
pos = self._hash_position(key)
self.ring[pos] = bucket_id
self.sorted_positions = sorted(self.ring.keys())
def _hash_position(self, key: str) -> int:
"""计算键在哈希环上的位置"""
hasher = hashlib.md5() # 快速哈希,非加密安全但分布均匀
hasher.update(key.encode())
return int.from_bytes(hasher.digest()[:4], 'big') % self.ring_size
def get_bucket(self, item: str) -> int:
"""获取元素对应的桶ID"""
pos = self._hash_position(item)
# 顺时针查找第一个虚拟节点
for ring_pos in self.sorted_positions:
if ring_pos >= pos:
return self.ring[ring_pos]
return self.ring[self.sorted_positions[0]]
def distribute_items(self, items: List[str]) -> Dict[int, List[str]]:
"""将元素分布到各桶"""
buckets = defaultdict(list)
for item in items:
bucket_id = self.get_bucket(item)
buckets[bucket_id].append(item)
# 检查桶大小约束,必要时重哈希
return self._rebalance_if_needed(dict(buckets))
def _rebalance_if_needed(self, buckets: Dict[int, List[str]]) -> Dict[int, List[str]]:
"""负载均衡:超大桶分裂,超小桶合并"""
result = {}
overflow = []
for bid, items in buckets.items():
if len(items) > self.config.max_bucket_size:
# 使用二级哈希分裂
sub_bucketer = ConsistentHashBucketer(
BucketConfig(num_buckets=4, max_bucket_size=self.config.max_bucket_size)
)
sub_buckets = sub_bucketer.distribute_items(items)
for sub_id, sub_items in sub_buckets.items():
result[f"{bid}_{sub_id}"] = sub_items
else:
result[bid] = items
return result
class AVX512Simulator:
"""
AVX-512指令集模拟器(使用NumPy向量化)
实际部署应使用Intel IPP或内联汇编
"""
def __init__(self):
self.vector_width = 8 # 512位 / 64位整数 = 8元素
self.check_cpu_support()
def check_cpu_support(self) -> bool:
"""检测CPU AVX-512支持"""
info = get_cpu_info()
flags = info.get('flags', [])
avx512_flags = ['avx512f', 'avx512cd', 'avx512vl', 'avx512bw', 'avx512dq']
supported = all(f in flags for f in avx512_flags)
print(f"CPU AVX-512支持检测: {supported}")
print(f" 标志位: {[f for f in avx512_flags if f in flags]}")
return supported
def vectorized_mod_exp(self, bases: np.ndarray, exps: np.ndarray, mod: int) -> np.ndarray:
"""
向量化模幂运算(Montgomery乘法模拟)
实际应使用Intel IPP的ippsModExp函数
"""
# 分块处理,每块8个元素
results = np.zeros_like(bases)
for i in range(0, len(bases), self.vector_width):
block = slice(i, min(i + self.vector_width, len(bases)))
# NumPy标量运算(模拟SIMD)
results[block] = np.array([pow(b, e, mod) for b, e in zip(bases[block], exps[block])])
return results
def matrix_transpose_512(self, matrix: np.ndarray) -> np.ndarray:
"""
512位寄存器优化的矩阵转置
用于OT扩展中的矩阵重排
"""
rows, cols = matrix.shape
# 确保维度对齐到512位边界
pad_rows = (self.vector_width - rows % self.vector_width) % self.vector_width
pad_cols = (self.vector_width - cols % self.vector_width) % self.vector_width
padded = np.pad(matrix, ((0, pad_rows), (0, pad_cols)), mode='constant')
# 分块转置(8x8块模拟AVX-512矩阵转置)
block_size = self.vector_width
result = np.zeros_like(padded)
for i in range(0, padded.shape[0], block_size):
for j in range(0, padded.shape[1], block_size):
block = padded[i:i+block_size, j:j+block_size]
result[j:j+block_size, i:i+block_size] = block.T
return result[:cols, :rows] # 去除填充
def gf_mul_vectorized(self, a: np.ndarray, b: np.ndarray, field_prime: int) -> np.ndarray:
"""
Galois域乘法向量化(PCLMULQDQ模拟)
适用于OT扩展中的有限域运算
"""
# 使用Barrett约简模拟
results = np.zeros_like(a)
for i in range(0, len(a), self.vector_width):
block = slice(i, min(i + self.vector_width, len(a)))
# 大整数乘法后约简
prod = np.multiply(a[block], b[block], dtype=np.object)
results[block] = np.array([x % field_prime for x in prod])
return results
class ParallelBucketPSI:
"""
分桶并行PSI执行引擎
整合硬件加速与多进程并行
"""
def __init__(self, config: BucketConfig):
self.config = config
self.bucketer = ConsistentHashBucketer(config)
self.avx512 = AVX512Simulator() if config.use_avx512 else None
self.num_workers = config.num_workers or mp.cpu_count()
def execute_large_scale_psi(self,
sender_items: List[str],
receiver_items: List[str],
psi_per_bucket: Callable[[List[str], List[str]], Set[str]]) -> Set[str]:
"""
执行大规模PSI主流程
"""
print(f"开始分桶:Sender {len(sender_items)} 元素, Receiver {len(receiver_items)} 元素")
# 阶段1:分桶分布
t_start = time.time()
sender_buckets = self.bucketer.distribute_items(sender_items)
receiver_buckets = self.bucketer.distribute_items(receiver_items)
print(f"分桶完成:{len(sender_buckets)} 个Sender桶, {len(receiver_buckets)} 个Receiver桶")
print(f"分桶耗时: {(time.time() - t_start)*1000:.2f} ms")
# 阶段2:桶配对与任务生成
tasks = []
for bucket_id in set(sender_buckets.keys()) & set(receiver_buckets.keys()):
s_items = sender_buckets[bucket_id]
r_items = receiver_buckets[bucket_id]
tasks.append((bucket_id, s_items, r_items))
print(f"生成 {len(tasks)} 个PSI子任务")
# 阶段3:并行执行(使用进程池)
t_start = time.time()
results = self._parallel_execute(tasks, psi_per_bucket)
print(f"并行PSI耗时: {(time.time() - t_start)*1000:.2f} ms")
# 阶段4:结果合并
final_intersection = set()
for bucket_result in results:
final_intersection.update(bucket_result)
return final_intersection
def _parallel_execute(self, tasks: List[Tuple],
psi_func: Callable) -> List[Set[str]]:
"""并行执行桶级PSI"""
# 使用共享内存优化大数据传输(生产环境)
with ProcessPoolExecutor(max_workers=self.num_workers) as executor:
futures = {
executor.submit(self._execute_single_bucket, task, psi_func): task[0]
for task in tasks
}
results = []
for future in as_completed(futures):
bucket_id = futures[future]
try:
result = future.result()
results.append(result)
except Exception as e:
print(f"桶 {bucket_id} 执行失败: {e}")
return results
def _execute_single_bucket(self,
task: Tuple[int, List[str], List[str]],
psi_func: Callable) -> Set[str]:
"""单桶PSI执行(工作进程)"""
bucket_id, s_items, r_items = task
# 应用AVX-512优化(如启用)
if self.avx512 and len(s_items) > 1000:
s_items = self._avx512_preprocess(s_items)
r_items = self._avx512_preprocess(r_items)
# 执行实际PSI协议(如KKRT16或ECDH)
return psi_func(s_items, r_items)
def _avx512_preprocess(self, items: List[str]) -> List[str]:
"""AVX-512优化的预处理(如批量哈希)"""
# 将字符串转换为整数向量进行SIMD处理
vectors = []
for item in items:
# 解析为8个64位整数(512位)
hasher = hashlib.sha256()
hasher.update(item.encode())
digest = hasher.digest()
vec = np.frombuffer(digest, dtype=np.uint64)
vectors.append(vec)
# 批量SIMD操作(示例:向量加法)
if vectors:
batch = np.stack(vectors[:8]) # 8个元素对齐AVX-512
# 模拟SIMD加法
result = np.sum(batch, axis=0)
return items # 返回原始列表(实际应返回处理结果)
class IntelIPPWrapper:
"""
Intel IPP密码学库封装接口
提供高性能大数运算与对称加密
"""
def __init__(self):
self.available = self._check_ipp_available()
self.avx512_enabled = self._detect_avx512()
def _check_ipp_available(self) -> bool:
"""检测Intel IPP库可用性"""
try:
# 尝试加载IPP库(实际应使用ctypes或C扩展)
import ctypes
# 模拟检测
return False # 默认模拟模式
except:
return False
def _detect_avx512(self) -> bool:
"""检测AVX-512指令集支持"""
info = get_cpu_info()
return 'avx512f' in info.get('flags', [])
def mod_exp_batch(self, bases: List[int], exponents: List[int],
modulus: int) -> List[int]:
"""
批量模幂运算(ippsModExp_BN模拟)
使用Montgomery约简与AVX-512加速
"""
if not self.available:
# 回退到Python实现
return [pow(b, e, modulus) for b, e in zip(bases, exponents)]
# 实际调用IPP库
# ippsModExp_BN(bases, exponents, modulus, results, count)
pass
def aes_gcm_batch(self, plaintexts: List[bytes], key: bytes,
ivs: List[bytes]) -> List[bytes]:
"""
批量AES-GCM加密(ippsAES_GCMEncrypt模拟)
利用AES-NI与AVX-512并行处理多分组
"""
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
aesgcm = AESGCM(key)
return [aesgcm.encrypt(iv, pt, None) for iv, pt in zip(ivs, plaintexts)]
def demo_large_scale_psi():
"""大规模PSI优化演示"""
# 配置
config = BucketConfig(
num_buckets=64,
target_load_factor=1.2,
max_bucket_size=5000,
use_avx512=True,
num_workers=8
)
# 生成大规模测试数据(100万元素)
scale = 1000000
intersection_ratio = 0.3
print(f"生成测试数据:{scale} 元素/方")
sender_base = [f"large_scale_user_{i:010d}" for i in range(scale)]
receiver_base = [f"large_scale_user_{i:010d}" for i in range(int(scale*0.7), int(scale*1.7))]
# 构造交集
common = [f"common_item_{i:08d}" for i in range(int(scale * intersection_ratio))]
sender_items = common + sender_base[len(common):]
receiver_items = common + receiver_base[len(common):]
# 初始化执行引擎
engine = ParallelBucketPSI(config)
# 定义桶级PSI函数(简化版ECDH)
def bucket_psi(s_items: List[str], r_items: List[str]) -> Set[str]:
# 模拟本地PSI计算
return set(s_items) & set(r_items)
# 执行
print("\n执行分桶并行PSI...")
result = engine.execute_large_scale_psi(sender_items, receiver_items, bucket_psi)
print(f"\n最终结果:交集大小 {len(result)}")
print(f"预期交集大小: {int(scale * intersection_ratio) + int(scale*0.3)}")
# 性能统计
process = psutil.Process(os.getpid())
print(f"内存使用: {process.memory_info().rss / 1024 / 1024:.2f} MB")
if __name__ == "__main__":
demo_large_scale_psi()
以上代码实现涵盖了联邦学习PSI技术栈的核心组件,从基础ECDH协议到恶意安全的VOLE构造,再到大规模硬件加速优化。每个实现均包含详细的技术注释与使用说明,可直接用于教学演示或作为生产级系统的原型基础。实际部署时需结合具体密码学库(如OpenSSL、libsodium、Intel IPP)进行底层优化,并严格遵循形式化安全证明的参数选择。
1.3 特征对齐与实体解析工程实践
1.3.1 实体解析(Entity Resolution)在VFL中的技术实现
实体解析是垂直联邦学习数据预处理阶段的核心环节,其目标是在不泄露原始标识符的前提下,识别并关联分散在不同参与方数据集中指向同一实体的记录。在VFL场景下,实体解析面临独特的技术挑战:各参与方仅持有部分特征维度,无法直接基于完整记录相似度进行匹配;同时,隐私保护要求禁止明文交换标识符信息。
现代实体解析框架通常采用分阶段流水线架构。第一阶段为索引构建(Blocking),通过计算高效的分块策略将潜在匹配记录对限制在可管理的候选集合内,避免全量记录间的暴力比较。第二阶段为相似度计算,在候选记录对上应用加密或哈希编码后的相似度度量。第三阶段为分类决策,基于相似度阈值或机器学习模型判定记录对是否构成匹配。
在VFL应用中,实体解析流程需与隐私集合求交(PSI)协议深度集成。参与方首先基于共同标识符(如用户ID、设备指纹)执行PSI,确定交集样本集合;随后针对交集样本进行特征对齐与序列化,确保各方数据行在物理存储层面保持严格对应。该过程要求各参与方在本地完成数据预处理(包括缺失值填充、格式标准化与编码转换),并通过安全通道交换经过密码学保护的中间结果。

以下代码实现了一个完整的VFL实体解析系统,包含索引构建、相似度计算与对齐决策三个核心模块:
"""
VFL Entity Resolution System
============================
This script implements a complete entity resolution pipeline for Vertical Federated Learning.
It includes blocking (indexing), similarity computation, and alignment decision modules.
Usage:
1. Prepare two CSV files representing different parties' data with common identifiers
2. Run: python vfl_entity_resolution.py --party1 data/party_a.csv --party2 data/party_b.csv
3. The system outputs aligned entity pairs and quality metrics
Dependencies: pandas, numpy, recordlinkage, blockingpy (optional for ANN-based blocking)
"""
import pandas as pd
import numpy as np
from typing import List, Tuple, Dict, Set
import hashlib
import re
from dataclasses import dataclass
from collections import defaultdict
import argparse
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class EntityRecord:
"""Represents a single entity record with identifiers and features."""
record_id: str
entity_id: str # Ground truth for evaluation (optional)
blocking_keys: List[str]
features: Dict[str, any]
def get_blocking_signature(self) -> str:
"""Generate composite blocking key."""
return "|".join(sorted(self.blocking_keys))
class BlockingIndex:
"""
Implements multi-key blocking for entity resolution.
Uses phonetic encoding and standardization to improve recall.
"""
def __init__(self, blocking_columns: List[str]):
self.blocking_columns = blocking_columns
self.blocks = defaultdict(list)
def _soundex(self, name: str) -> str:
"""Compute Soundex code for phonetic matching."""
if not name:
return "0000"
name = name.upper()
name = re.sub(r'[^A-Z]', '', name)
if not name:
return "0000"
# Soundex encoding rules
soundex_mapping = {
'B': '1', 'F': '1', 'P': '1', 'V': '1',
'C': '2', 'G': '2', 'J': '2', 'K': '2',
'Q': '2', 'S': '2', 'X': '2', 'Z': '2',
'D': '3', 'T': '3',
'L': '4',
'M': '5', 'N': '5',
'R': '6'
}
first_char = name[0]
result = [first_char]
prev_code = soundex_mapping.get(first_char, '0')
for char in name[1:]:
code = soundex_mapping.get(char, '0')
if code != '0' and code != prev_code:
result.append(code)
prev_code = code
if len(result) == 4:
break
# Pad with zeros
while len(result) < 4:
result.append('0')
return ''.join(result)
def _standardize(self, value: str) -> str:
"""Standardize string values for blocking."""
if pd.isna(value):
return ""
value = str(value).lower().strip()
# Remove common noise
value = re.sub(r'[^\w\s]', '', value)
return value
def build_index(self, records: List[EntityRecord]) -> None:
"""Build blocking index from records."""
logger.info(f"Building blocking index for {len(records)} records...")
for record in records:
# Generate multiple blocking keys
soundex_keys = [self._soundex(bk) for bk in record.blocking_keys]
std_keys = [self._standardize(bk) for bk in record.blocking_keys]
# Composite blocking strategy
composite_keys = []
for i, (sx, st) in enumerate(zip(soundex_keys, std_keys)):
composite_keys.append(f"soundex_{i}_{sx}")
if len(st) >= 3:
composite_keys.append(f"prefix_{i}_{st[:3]}")
for key in composite_keys:
self.blocks[key].append(record)
logger.info(f"Created {len(self.blocks)} blocks")
def get_candidate_pairs(self) -> List[Tuple[EntityRecord, EntityRecord]]:
"""Generate candidate record pairs from blocks."""
candidates = []
seen_pairs = set()
for block_id, records in self.blocks.items():
if len(records) < 2:
continue
# Generate pairs within block
for i in range(len(records)):
for j in range(i + 1, len(records)):
pair_key = tuple(sorted([records[i].record_id, records[j].record_id]))
if pair_key not in seen_pairs:
candidates.append((records[i], records[j]))
seen_pairs.add(pair_key)
logger.info(f"Generated {len(candidates)} candidate pairs")
return candidates
class SimilarityComputer:
"""
Computes similarity scores between entity records using multiple metrics.
Supports both exact and approximate matching.
"""
def __init__(self):
self.weights = {
'jaro_winkler': 0.4,
'exact_match': 0.3,
'token_sort': 0.3
}
def _jaro_winkler(self, s1: str, s2: str) -> float:
"""Compute Jaro-Winkler similarity."""
if not s1 or not s2:
return 0.0
s1, s2 = s1.lower(), s2.lower()
# Jaro similarity
len_s1, len_s2 = len(s1), len(s2)
match_distance = (max(len_s1, len_s2) // 2) - 1
s1_matches = [False] * len_s1
s2_matches = [False] * len_s2
matches = 0
transpositions = 0
for i in range(len_s1):
start = max(0, i - match_distance)
end = min(i + match_distance + 1, len_s2)
for j in range(start, end):
if s2_matches[j] or s1[i] != s2[j]:
continue
s1_matches[i] = True
s2_matches[j] = True
matches += 1
break
if matches == 0:
return 0.0
k = 0
for i in range(len_s1):
if not s1_matches[i]:
continue
while not s2_matches[k]:
k += 1
if s1[i] != s2[k]:
transpositions += 1
k += 1
jaro = ((matches / len_s1) + (matches / len_s2) +
((matches - transpositions / 2) / matches)) / 3.0
# Jaro-Winkler adjustment
prefix = 0
for i in range(min(4, len_s1, len_s2)):
if s1[i] == s2[i]:
prefix += 1
else:
break
return jaro + (0.1 * prefix * (1 - jaro))
def _token_sort_ratio(self, s1: str, s2: str) -> float:
"""Compute token sort similarity."""
if not s1 or not s2:
return 0.0
tokens1 = sorted(s1.lower().split())
tokens2 = sorted(s2.lower().split())
sorted1 = ' '.join(tokens1)
sorted2 = ' '.join(tokens2)
return self._jaro_winkler(sorted1, sorted2)
def compute_similarity(self, rec1: EntityRecord, rec2: EntityRecord) -> Dict[str, float]:
"""Compute comprehensive similarity between two records."""
scores = {}
# Compare blocking keys
key_similarities = []
for k1, k2 in zip(rec1.blocking_keys, rec2.blocking_keys):
jw_score = self._jaro_winkler(k1, k2)
exact_score = 1.0 if k1.lower() == k2.lower() else 0.0
token_score = self._token_sort_ratio(k1, k2)
combined = (self.weights['jaro_winkler'] * jw_score +
self.weights['exact_match'] * exact_score +
self.weights['token_sort'] * token_score)
key_similarities.append(combined)
scores['key_similarity'] = np.mean(key_similarities) if key_similarities else 0.0
# Feature overlap (for VFL, we only compare available features)
common_features = set(rec1.features.keys()) & set(rec2.features.keys())
if common_features:
feature_scores = []
for feat in common_features:
v1, v2 = rec1.features[feat], rec2.features[feat]
if isinstance(v1, (int, float)) and isinstance(v2, (int, float)):
# Numeric similarity (normalized)
max_val = max(abs(v1), abs(v2), 1e-10)
feature_scores.append(1.0 - abs(v1 - v2) / max_val)
else:
# Categorical exact match
feature_scores.append(1.0 if str(v1) == str(v2) else 0.0)
scores['feature_similarity'] = np.mean(feature_scores)
else:
scores['feature_similarity'] = 0.0
# Overall score
scores['overall'] = 0.6 * scores['key_similarity'] + 0.4 * scores['feature_similarity']
return scores
class VFLAlignmentManager:
"""
Manages the complete VFL entity resolution and alignment workflow.
Coordinates between multiple parties without exposing raw data.
"""
def __init__(self, threshold: float = 0.85):
self.threshold = threshold
self.blocker = None
self.similarity_computer = SimilarityComputer()
self.alignments = []
def load_party_data(self, filepath: str, id_column: str,
blocking_columns: List[str]) -> List[EntityRecord]:
"""Load and preprocess party data."""
df = pd.read_csv(filepath)
records = []
for idx, row in df.iterrows():
record = EntityRecord(
record_id=f"{filepath}_{idx}",
entity_id=str(row.get(id_column, idx)),
blocking_keys=[str(row[col]) for col in blocking_columns],
features={col: row[col] for col in df.columns
if col not in [id_column] + blocking_columns}
)
records.append(record)
return records
def resolve_entities(self, party1_records: List[EntityRecord],
party2_records: List[EntityRecord]) -> List[Dict]:
"""
Execute entity resolution between two parties.
In real VFL, this would use secure multi-party computation.
"""
# Build blocking index
self.blocker = BlockingIndex(blocking_columns=['name', 'address'])
all_records = party1_records + party2_records
self.blocker.build_index(all_records)
# Get candidate pairs
candidates = self.blocker.get_candidate_pairs()
# Compute similarities and determine alignments
alignments = []
for rec1, rec2 in candidates:
# Ensure records are from different parties
if (rec1.record_id.startswith('party1') == rec2.record_id.startswith('party1')):
continue
scores = self.similarity_computer.compute_similarity(rec1, rec2)
if scores['overall'] >= self.threshold:
alignment = {
'party1_id': rec1.entity_id,
'party2_id': rec2.entity_id,
'similarity_scores': scores,
'confidence': scores['overall']
}
alignments.append(alignment)
self.alignments = alignments
return alignments
def evaluate_alignment(self, ground_truth: List[Tuple[str, str]]) -> Dict[str, float]:
"""Evaluate alignment quality against ground truth."""
true_positives = 0
false_positives = 0
false_negatives = 0
aligned_pairs = set((a['party1_id'], a['party2_id']) for a in self.alignments)
truth_pairs = set(ground_truth)
for pair in aligned_pairs:
if pair in truth_pairs:
true_positives += 1
else:
false_positives += 1
for pair in truth_pairs:
if pair not in aligned_pairs:
false_negatives += 1
precision = true_positives / (true_positives + false_positives) if (true_positives + false_positives) > 0 else 0
recall = true_positives / (true_positives + false_negatives) if (true_positives + false_negatives) > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
return {
'precision': precision,
'recall': recall,
'f1_score': f1,
'true_positives': true_positives,
'false_positives': false_positives,
'false_negatives': false_negatives
}
def main():
parser = argparse.ArgumentParser(description='VFL Entity Resolution System')
parser.add_argument('--party1', required=True, help='Path to party 1 CSV')
parser.add_argument('--party2', required=True, help='Path to party 2 CSV')
parser.add_argument('--threshold', type=float, default=0.85, help='Matching threshold')
args = parser.parse_args()
# Initialize manager
manager = VFLAlignmentManager(threshold=args.threshold)
# Load data
logger.info("Loading party data...")
party1_records = manager.load_party_data(
args.party1,
id_column='entity_id',
blocking_columns=['name', 'address']
)
party2_records = manager.load_party_data(
args.party2,
id_column='entity_id',
blocking_columns=['name', 'address']
)
# Prefix record IDs
for r in party1_records:
r.record_id = f"party1_{r.record_id}"
for r in party2_records:
r.record_id = f"party2_{r.record_id}"
# Execute resolution
logger.info("Executing entity resolution...")
alignments = manager.resolve_entities(party1_records, party2_records)
logger.info(f"Found {len(alignments)} aligned entities")
# Output results
for align in alignments[:10]: # Show first 10
print(f"Alignment: {align['party1_id']} <-> {align['party2_id']}, "
f"Confidence: {align['confidence']:.3f}")
if __name__ == "__main__":
main()
1.3.2 模糊匹配与隐私保护记录链接(PPRL):Soundex与Bloom Filter方案
隐私保护记录链接(Privacy-Preserving Record Linkage, PPRL)技术允许数据持有方在不泄露敏感标识符的前提下识别跨数据集的匹配记录。在VFL场景中,PPRL是实体解析的安全实现基础,确保参与方仅获知交集样本信息而不暴露非交集记录。
Soundex是一种基于发音的语音编码算法,将相似发音的字符串映射为相同的四位编码。该算法通过保留首字母并转换后续辅音为数字代码,有效处理姓名拼写变体与拼写错误。在PPRL应用中,Soundex常作为分块键(Blocking Key)使用,将可能匹配的记录分组到同一候选集合,显著减少需要比较的记录对数量。
Bloom Filter是一种空间效率极高的概率数据结构,用于测试元素是否属于某个集合。在PPRL中,Bloom Filter通过将标识符转换为n-gram并哈希映射到位数组,实现字符串的模糊匹配。多个哈希函数将n-gram映射到位数组的不同位置,通过比较两个Bloom Filter的位重叠程度(通常使用Dice系数或Jaccard相似度)计算记录相似度。该方案的优势在于:原始字符串信息被单向哈希编码,无法从Bloom Filter反推原始值;同时支持近似匹配,容忍一定程度的拼写错误与格式差异。

以下代码实现了基于Soundex与Bloom Filter的PPRL系统,包含完整的编码、相似度计算与隐私保护机制:
"""
Privacy-Preserving Record Linkage (PPRL) System
===============================================
Implements Soundex phonetic encoding and Bloom Filter-based matching
for secure entity resolution in Vertical Federated Learning.
Usage:
python pprl_system.py --encode --input data.csv --output encoded.pkl
python pprl_system.py --match --party1 encoded_a.pkl --party2 encoded_b.pkl
Features:
- Soundex phonetic blocking
- Cryptographic Bloom Filter encoding
- Dice coefficient similarity
- Configurable privacy parameters
"""
import numpy as np
import pandas as pd
import hashlib
import mmh3
from bitarray import bitarray
from typing import List, Dict, Tuple, Set
import pickle
import argparse
import logging
from dataclasses import dataclass
from collections import defaultdict
import re
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class BloomFilterConfig:
"""Configuration for Bloom Filter encoding."""
filter_size: int = 1000 # Length of bit array (l)
num_hash_functions: int = 30 # Number of hash functions (k)
ngram_size: int = 2 # Q-gram size (q)
padding: bool = True
salt: str = "secure_salt_vfl_2024" # Secret key for HMAC
class SoundexEncoder:
"""Soundex phonetic encoding for blocking."""
def encode(self, name: str) -> str:
"""
Convert name to Soundex code.
Format: Letter followed by 3 digits.
"""
if not name:
return "0000"
name = name.upper()
name = re.sub(r'[^A-Z]', '', name)
if not name:
return "0000"
# Soundex digit mapping
soundex_table = {
'B': '1', 'F': '1', 'P': '1', 'V': '1',
'C': '2', 'G': '2', 'J': '2', 'K': '2',
'Q': '2', 'S': '2', 'X': '2', 'Z': '2',
'D': '3', 'T': '3',
'L': '4',
'M': '5', 'N': '5',
'R': '6'
}
# First letter
first_letter = name[0]
result = [first_letter]
# Track previous code to avoid duplicates
prev_code = soundex_table.get(first_letter, None)
for char in name[1:]:
code = soundex_table.get(char)
if code and code != prev_code:
result.append(code)
if len(result) == 4:
break
if code:
prev_code = code
# Pad with zeros
while len(result) < 4:
result.append('0')
return ''.join(result)
class SecureBloomFilter:
"""
Privacy-preserving Bloom Filter for record linkage.
Uses HMAC-SHA256 for cryptographic security.
"""
def __init__(self, config: BloomFilterConfig):
self.config = config
self.bit_array = bitarray(config.filter_size)
self.bit_array.setall(0)
def _generate_ngrams(self, value: str) -> List[str]:
"""Generate n-grams from string value."""
if not value:
return []
# Standardize
value = str(value).lower().strip()
value = re.sub(r'[^a-z0-9]', '', value)
if self.config.padding:
# Pad with start/end markers
value = f"#{value}#"
# Generate n-grams
ngrams = []
for i in range(len(value) - self.config.ngram_size + 1):
ngrams.append(value[i:i + self.config.ngram_size])
return ngrams
def _hmac_hash(self, ngram: str, seed: int) -> int:
"""Compute HMAC-SHA256 hash for ngram."""
message = f"{ngram}_{seed}_{self.config.salt}"
hash_obj = hashlib.sha256(message.encode())
hash_int = int(hash_obj.hexdigest(), 16)
return hash_int % self.config.filter_size
def add(self, value: str) -> None:
"""Add value to Bloom Filter."""
ngrams = self._generate_ngrams(value)
for ngram in ngrams:
for i in range(self.config.num_hash_functions):
# Double hashing: h1 + i * h2
pos = self._hmac_hash(ngram, i)
self.bit_array[pos] = 1
def to_bytes(self) -> bytes:
"""Serialize Bloom Filter to bytes."""
return self.bit_array.tobytes()
@staticmethod
def dice_coefficient(bf1: 'SecureBloomFilter', bf2: 'SecureBloomFilter') -> float:
"""Compute Dice coefficient between two Bloom Filters."""
if bf1.config.filter_size != bf2.config.filter_size:
raise ValueError("Bloom Filters must have same size")
# Count set bits
intersection = (bf1.bit_array & bf2.bit_array).count()
count1 = bf1.bit_array.count()
count2 = bf2.bit_array.count()
if count1 + count2 == 0:
return 0.0
return (2.0 * intersection) / (count1 + count2)
class PPRLRecord:
"""Encapsulates a privacy-preserving record."""
def __init__(self, record_id: str, identifiers: Dict[str, str],
config: BloomFilterConfig):
self.record_id = record_id
self.config = config
self.soundex_codes = {}
self.bloom_filters = {}
# Encode each identifier
soundex_encoder = SoundexEncoder()
for field, value in identifiers.items():
# Soundex for blocking
self.soundex_codes[field] = soundex_encoder.encode(value)
# Bloom Filter for matching
bf = SecureBloomFilter(config)
bf.add(value)
self.bloom_filters[field] = bf
def get_blocking_keys(self) -> List[str]:
"""Get Soundex blocking keys."""
return list(self.soundex_codes.values())
def compute_similarity(self, other: 'PPRLRecord') -> Dict[str, float]:
"""Compute similarity with another PPRL record."""
similarities = {}
# Compare each field
for field in self.bloom_filters.keys():
if field in other.bloom_filters:
dice = SecureBloomFilter.dice_coefficient(
self.bloom_filters[field],
other.bloom_filters[field]
)
similarities[field] = dice
# Overall similarity (weighted average)
if similarities:
similarities['overall'] = np.mean(list(similarities.values()))
else:
similarities['overall'] = 0.0
return similarities
class PPRLEngine:
"""
Main engine for Privacy-Preserving Record Linkage.
Implements blocking, encoding, and matching pipeline.
"""
def __init__(self, config: BloomFilterConfig = None):
self.config = config or BloomFilterConfig()
self.soundex_encoder = SoundexEncoder()
self.records = {}
self.blocks = defaultdict(list)
def encode_dataset(self, df: pd.DataFrame, id_column: str,
identifier_columns: List[str]) -> Dict[str, PPRLRecord]:
"""
Encode entire dataset into privacy-preserving format.
Args:
df: Input dataframe
id_column: Column containing record ID
identifier_columns: Columns to use for matching (e.g., name, DOB)
Returns:
Dictionary of encoded records
"""
logger.info(f"Encoding {len(df)} records...")
encoded_records = {}
for idx, row in df.iterrows():
record_id = str(row[id_column])
identifiers = {col: str(row[col]) for col in identifier_columns
if pd.notna(row[col])}
record = PPRLRecord(record_id, identifiers, self.config)
encoded_records[record_id] = record
# Build blocking index
for key in record.get_blocking_keys():
self.blocks[key].append(record_id)
self.records.update(encoded_records)
logger.info(f"Encoded {len(encoded_records)} records into {len(self.blocks)} blocks")
return encoded_records
def find_matches(self, party1_records: Dict[str, PPRLRecord],
party2_records: Dict[str, PPRLRecord],
threshold: float = 0.85) -> List[Dict]:
"""
Find matching records between two parties using blocking + similarity.
In real VFL, this would use secure multi-party computation
to prevent revealing non-matching records.
"""
matches = []
candidate_pairs = set()
# Blocking: Only compare records in same Soundex blocks
for block_key, ids1 in self.blocks.items():
ids2 = [rid for rid in ids1 if rid in party2_records]
ids1 = [rid for rid in ids1 if rid in party1_records]
for id1 in ids1:
for id2 in ids2:
candidate_pairs.add((id1, id2))
logger.info(f"Generated {len(candidate_pairs)} candidate pairs from blocking")
# Compute similarities
for id1, id2 in candidate_pairs:
rec1 = party1_records[id1]
rec2 = party2_records[id2]
sim = rec1.compute_similarity(rec2)
if sim['overall'] >= threshold:
matches.append({
'party1_id': id1,
'party2_id': id2,
'similarity': sim['overall'],
'field_similarities': {k: v for k, v in sim.items() if k != 'overall'}
})
# Sort by similarity
matches.sort(key=lambda x: x['similarity'], reverse=True)
return matches
def evaluate_privacy(self, records: List[PPRLRecord]) -> Dict[str, float]:
"""
Evaluate privacy metrics of the encoding.
"""
# Check for frequency attacks
bit_counts = defaultdict(int)
for record in records:
for bf in record.bloom_filters.values():
# Count bit patterns (simplified)
pattern = str(bf.bit_array.count())
bit_counts[pattern] += 1
# Compute entropy (higher is better)
total = sum(bit_counts.values())
entropy = 0.0
for count in bit_counts.values():
p = count / total
if p > 0:
entropy -= p * np.log2(p)
# Estimate resistance to frequency attacks
max_freq = max(bit_counts.values()) / total if bit_counts else 1.0
return {
'encoding_entropy': entropy,
'max_pattern_frequency': max_freq,
'distinct_patterns': len(bit_counts),
'privacy_score': 1.0 - max_freq # Higher is better
}
def main():
parser = argparse.ArgumentParser(description='PPRL System for VFL')
parser.add_argument('--mode', choices=['encode', 'match'], required=True)
parser.add_argument('--input', help='Input CSV file for encoding')
parser.add_argument('--output', help='Output pickle file for encoded data')
parser.add_argument('--party1', help='Party 1 encoded data')
parser.add_argument('--party2', help='Party 2 encoded data')
parser.add_argument('--id-col', default='id', help='ID column name')
parser.add_argument('--match-cols', nargs='+',
default=['name', 'dob', 'address'],
help='Columns to use for matching')
parser.add_argument('--threshold', type=float, default=0.85)
parser.add_argument('--bf-size', type=int, default=1000)
parser.add_argument('--num-hashes', type=int, default=30)
args = parser.parse_args()
config = BloomFilterConfig(
filter_size=args.bf_size,
num_hash_functions=args.num_hashes
)
engine = PPRLEngine(config)
if args.mode == 'encode':
# Encode single party data
df = pd.read_csv(args.input)
encoded = engine.encode_dataset(df, args.id_col, args.match_cols)
with open(args.output, 'wb') as f:
pickle.dump(encoded, f)
# Privacy evaluation
privacy_metrics = engine.evaluate_privacy(list(encoded.values()))
logger.info(f"Privacy metrics: {privacy_metrics}")
elif args.mode == 'match':
# Load both parties
with open(args.party1, 'rb') as f:
party1_data = pickle.load(f)
with open(args.party2, 'rb') as f:
party2_data = pickle.load(f)
# Find matches
matches = engine.find_matches(
party1_data, party2_data,
threshold=args.threshold
)
logger.info(f"Found {len(matches)} matches")
for match in matches[:10]:
print(f"Match: {match['party1_id']} <-> {match['party2_id']} "
f"(similarity: {match['similarity']:.3f})")
if __name__ == "__main__":
main()
1.3.3 时序数据对齐:基于时间窗口的样本关联策略
在垂直联邦学习的实际应用中,参与方数据往往具有时序特性,如金融交易流水、医疗监测指标或物联网传感器读数。时序数据对齐的核心挑战在于:不同参与方的采样频率可能存在差异,同一事件在各数据集中的时间戳可能存在偏移,且数据记录可能因网络延迟或系统时钟不同步而产生错位。
基于时间窗口的样本关联策略通过定义时间容忍度(Temporal Tolerance)解决上述问题。该策略将时间轴划分为连续的或重叠的窗口,在每个窗口内执行样本匹配。滑动窗口(Sliding Window)方法以固定步长移动窗口边界,确保捕获所有可能的匹配组合;会话窗口(Session Window)则根据事件间隔动态调整窗口大小,适用于用户行为序列等场景。
动态时间规整(Dynamic Time Warping, DTW)算法通过非线性时间轴伸缩,计算两条时间序列间的最优对齐路径,有效处理采样频率差异与相位偏移。在VFL场景下,DTW可应用于特征级对齐:参与方在本地计算各自特征的DTW距离矩阵,通过安全聚合协议上传至协调方进行全局优化。

以下代码实现了完整的时序数据对齐系统,支持滑动窗口、DTW对齐与多频率采样处理:
"""
Temporal Data Alignment System for VFL
======================================
Implements time-window based sample association strategies including
sliding windows, dynamic time warping (DTW), and multi-frequency alignment.
Usage:
python temporal_alignment.py --party1 time_series_a.csv --party2 time_series_b.csv
--timestamp-col timestamp --window-size 3600
Features:
- Sliding window alignment
- Dynamic Time Warping (DTW) for sequence matching
- Multi-frequency resampling
- Temporal tolerance matching
"""
import pandas as pd
import numpy as np
from typing import List, Tuple, Dict, Optional, Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
import argparse
import logging
from scipy.interpolate import interp1d
from scipy.spatial.distance import euclidean
from scipy.signal import correlate
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class TimeSeriesRecord:
"""Represents a single time series observation."""
timestamp: datetime
entity_id: str
values: Dict[str, float]
metadata: Dict[str, any] = None
class SlidingWindowAligner:
"""
Implements sliding window-based temporal alignment.
Groups records within specified time windows for association.
"""
def __init__(self, window_size_seconds: float,
step_size_seconds: Optional[float] = None,
tolerance_seconds: float = 0):
"""
Args:
window_size_seconds: Size of each window in seconds
step_size_seconds: Step between windows (default: window_size)
tolerance_seconds: Additional tolerance for fuzzy matching
"""
self.window_size = timedelta(seconds=window_size_seconds)
self.step_size = timedelta(seconds=step_size_seconds or window_size_seconds)
self.tolerance = timedelta(seconds=tolerance_seconds)
def create_windows(self, start_time: datetime,
end_time: datetime) -> List[Tuple[datetime, datetime]]:
"""Generate sliding windows over time range."""
windows = []
current_start = start_time
while current_start <= end_time:
window_end = current_start + self.window_size
windows.append((current_start, window_end))
current_start += self.step_size
return windows
def assign_to_windows(self, records: List[TimeSeriesRecord],
windows: List[Tuple[datetime, datetime]]) -> Dict[int, List[TimeSeriesRecord]]:
"""Assign records to overlapping windows."""
assignments = {i: [] for i in range(len(windows))}
for record in records:
for i, (w_start, w_end) in enumerate(windows):
# Check if record falls within window (with tolerance)
effective_start = w_start - self.tolerance
effective_end = w_end + self.tolerance
if effective_start <= record.timestamp <= effective_end:
assignments[i].append(record)
return assignments
def find_alignments(self, party1_records: List[TimeSeriesRecord],
party2_records: List[TimeSeriesRecord]) -> List[Dict]:
"""
Find aligned record pairs across parties using sliding windows.
"""
# Determine time range
all_timestamps = [r.timestamp for r in party1_records + party2_records]
start_time, end_time = min(all_timestamps), max(all_timestamps)
# Create windows
windows = self.create_windows(start_time, end_time)
logger.info(f"Created {len(windows)} windows")
# Assign records to windows
p1_assignments = self.assign_to_windows(party1_records, windows)
p2_assignments = self.assign_to_windows(party2_records, windows)
# Find co-occurring records
alignments = []
for i, (w_start, w_end) in enumerate(windows):
p1_records = p1_assignments[i]
p2_records = p2_assignments[i]
if not p1_records or not p2_records:
continue
# Cross-product within window
for r1 in p1_records:
for r2 in p2_records:
# Check entity match (if entity IDs are available)
if r1.entity_id == r2.entity_id:
alignments.append({
'window_id': i,
'window_start': w_start,
'window_end': w_end,
'party1_record': r1,
'party2_record': r2,
'time_diff_seconds': abs((r1.timestamp - r2.timestamp).total_seconds())
})
return alignments
class DTWAligner:
"""
Dynamic Time Warping alignment for sequences with different sampling rates.
"""
def __init__(self, max_warping_factor: float = 0.5):
self.max_warping_factor = max_warping_factor
def compute_dtw_distance(self, seq1: np.ndarray, seq2: np.ndarray,
window_size: Optional[int] = None) -> Tuple[float, List[Tuple[int, int]]]:
"""
Compute DTW distance and optimal warping path.
Args:
seq1: First sequence (n_samples, n_features)
seq2: Second sequence (m_samples, n_features)
window_size: Sakoe-Chiba band width (None for no constraint)
Returns:
(distance, warping_path)
"""
n, m = len(seq1), len(seq2)
# Initialize distance matrix
dtw_matrix = np.full((n + 1, m + 1), np.inf)
dtw_matrix[0, 0] = 0
# Compute DTW
for i in range(1, n + 1):
# Apply windowing constraint
j_start = max(1, i - window_size) if window_size else 1
j_end = min(m + 1, i + window_size + 1) if window_size else m + 1
for j in range(j_start, j_end):
cost = euclidean(seq1[i-1], seq2[j-1])
dtw_matrix[i, j] = cost + min(
dtw_matrix[i-1, j], # insertion
dtw_matrix[i, j-1], # deletion
dtw_matrix[i-1, j-1] # match
)
# Backtrack to find path
path = []
i, j = n, m
while i > 0 and j > 0:
path.append((i-1, j-1))
candidates = [
(dtw_matrix[i-1, j], i-1, j),
(dtw_matrix[i, j-1], i, j-1),
(dtw_matrix[i-1, j-1], i-1, j-1)
]
_, i, j = min(candidates)
path.reverse()
return dtw_matrix[n, m], path
def align_sequences(self, timestamps1: np.ndarray, values1: np.ndarray,
timestamps2: np.ndarray, values2: np.ndarray) -> Dict:
"""
Align two time series using DTW and return warped alignment.
"""
# Normalize timestamps to relative seconds
t1_norm = (timestamps1 - timestamps1[0]) / np.timedelta64(1, 's')
t2_norm = (timestamps2 - timestamps2[0]) / np.timedelta64(1, 's')
# Create feature vectors combining time and value
seq1 = np.column_stack([t1_norm, values1])
seq2 = np.column_stack([t2_norm, values2])
# Compute DTW
distance, path = self.compute_dtw_distance(seq1, seq2)
# Extract aligned pairs
aligned_pairs = []
for idx1, idx2 in path:
aligned_pairs.append({
'index1': idx1,
'index2': idx2,
'timestamp1': timestamps1[idx1],
'timestamp2': timestamps2[idx2],
'value1': values1[idx1],
'value2': values2[idx2],
'time_diff': abs((timestamps1[idx1] - timestamps2[idx2]).total_seconds())
})
return {
'dtw_distance': distance,
'warping_path': path,
'aligned_pairs': aligned_pairs,
'compression_ratio': len(path) / max(len(values1), len(values2))
}
class MultiFrequencyAligner:
"""
Handles alignment between time series with different sampling frequencies.
"""
def __init__(self, target_frequency: str = '1min'):
self.target_frequency = target_frequency
def resample_to_common(self, records: List[TimeSeriesRecord],
value_columns: List[str]) -> pd.DataFrame:
"""
Resample irregular time series to common frequency.
"""
# Convert to DataFrame
df = pd.DataFrame([
{
'timestamp': r.timestamp,
'entity_id': r.entity_id,
**r.values
}
for r in records
])
df.set_index('timestamp', inplace=True)
# Resample each entity separately
resampled_frames = []
for entity_id in df['entity_id'].unique():
entity_df = df[df['entity_id'] == entity_id].drop('entity_id', axis=1)
# Resample with interpolation
resampled = entity_df.resample(self.target_frequency).mean()
resampled = resampled.interpolate(method='time')
resampled['entity_id'] = entity_id
resampled_frames.append(resampled)
result = pd.concat(resampled_frames)
result.reset_index(inplace=True)
return result
def cross_correlation_align(self, signal1: np.ndarray,
signal2: np.ndarray) -> Dict:
"""
Find optimal time shift using cross-correlation.
"""
# Normalize
s1 = (signal1 - np.mean(signal1)) / (np.std(signal1) + 1e-10)
s2 = (signal2 - np.mean(signal2)) / (np.std(signal2) + 1e-10)
# Compute cross-correlation
correlation = correlate(s1, s2, mode='full')
lags = np.arange(-len(s2) + 1, len(s1))
# Find optimal lag
optimal_idx = np.argmax(np.abs(correlation))
optimal_lag = lags[optimal_idx]
max_correlation = correlation[optimal_idx]
return {
'optimal_lag': int(optimal_lag),
'max_correlation': float(max_correlation),
'lag_seconds': int(optimal_lag * pd.Timedelta(self.target_frequency).total_seconds())
}
class TemporalAlignmentManager:
"""
Main manager for temporal alignment in VFL.
Combines multiple alignment strategies.
"""
def __init__(self, strategy: str = 'sliding_window', **kwargs):
self.strategy = strategy
if strategy == 'sliding_window':
self.aligner = SlidingWindowAligner(**kwargs)
elif strategy == 'dtw':
self.aligner = DTWAligner(**kwargs)
elif strategy == 'multifreq':
self.aligner = MultiFrequencyAligner(**kwargs)
else:
raise ValueError(f"Unknown strategy: {strategy}")
def load_time_series(self, filepath: str,
timestamp_col: str,
entity_col: str,
value_cols: List[str]) -> List[TimeSeriesRecord]:
"""Load time series data from CSV."""
df = pd.read_csv(filepath, parse_dates=[timestamp_col])
records = []
for _, row in df.iterrows():
record = TimeSeriesRecord(
timestamp=row[timestamp_col],
entity_id=str(row[entity_col]),
values={col: float(row[col]) for col in value_cols if pd.notna(row[col])},
metadata={k: v for k, v in row.items() if k not in [timestamp_col, entity_col] + value_cols}
)
records.append(record)
return records
def execute_alignment(self, party1_data: List[TimeSeriesRecord],
party2_data: List[TimeSeriesRecord]) -> Dict:
"""
Execute alignment based on selected strategy.
"""
if self.strategy == 'sliding_window':
alignments = self.aligner.find_alignments(party1_data, party2_data)
return {
'strategy': 'sliding_window',
'num_alignments': len(alignments),
'alignments': alignments
}
elif self.strategy == 'dtw':
# Group by entity and align each pair
results = []
p1_by_entity = self._group_by_entity(party1_data)
p2_by_entity = self._group_by_entity(party2_data)
for entity_id in set(p1_by_entity.keys()) & set(p2_by_entity.keys()):
p1_records = p1_by_entity[entity_id]
p2_records = p2_by_entity[entity_id]
# Sort by timestamp
p1_records.sort(key=lambda r: r.timestamp)
p2_records.sort(key=lambda r: r.timestamp)
# Extract values (using first value column)
values1 = np.array([list(r.values.values())[0] for r in p1_records])
values2 = np.array([list(r.values.values())[0] for r in p2_records])
timestamps1 = np.array([r.timestamp for r in p1_records])
timestamps2 = np.array([r.timestamp for r in p2_records])
alignment = self.aligner.align_sequences(
timestamps1, values1, timestamps2, values2
)
alignment['entity_id'] = entity_id
results.append(alignment)
return {
'strategy': 'dtw',
'num_entities_aligned': len(results),
'alignments': results
}
elif self.strategy == 'multifreq':
# Resample both to common frequency
value_cols = list(party1_data[0].values.keys())
df1 = self.aligner.resample_to_common(party1_data, value_cols)
df2 = self.aligner.resample_to_common(party2_data, value_cols)
# Find common timestamps
merged = pd.merge(
df1, df2,
on=['timestamp', 'entity_id'],
suffixes=('_p1', '_p2')
)
return {
'strategy': 'multifreq',
'num_aligned_points': len(merged),
'aligned_data': merged
}
def _group_by_entity(self, records: List[TimeSeriesRecord]) -> Dict[str, List[TimeSeriesRecord]]:
"""Group records by entity ID."""
grouped = defaultdict(list)
for record in records:
grouped[record.entity_id].append(record)
return grouped
def main():
parser = argparse.ArgumentParser(description='Temporal Alignment for VFL')
parser.add_argument('--party1', required=True, help='Party 1 time series CSV')
parser.add_argument('--party2', required=True, help='Party 2 time series CSV')
parser.add_argument('--strategy', choices=['sliding_window', 'dtw', 'multifreq'],
default='sliding_window')
parser.add_argument('--timestamp-col', default='timestamp')
parser.add_argument('--entity-col', default='entity_id')
parser.add_argument('--value-cols', nargs='+', required=True)
parser.add_argument('--window-size', type=float, default=3600,
help='Window size in seconds')
parser.add_argument('--step-size', type=float, default=None)
args = parser.parse_args()
# Initialize manager
if args.strategy == 'sliding_window':
manager = TemporalAlignmentManager(
strategy='sliding_window',
window_size_seconds=args.window_size,
step_size_seconds=args.step_size or args.window_size
)
elif args.strategy == 'dtw':
manager = TemporalAlignmentManager(strategy='dtw')
else:
manager = TemporalAlignmentManager(strategy='multifreq')
# Load data
logger.info("Loading time series data...")
party1_data = manager.load_time_series(
args.party1, args.timestamp_col, args.entity_col, args.value_cols
)
party2_data = manager.load_time_series(
args.party2, args.timestamp_col, args.entity_col, args.value_cols
)
logger.info(f"Loaded {len(party1_data)} and {len(party2_data)} records")
# Execute alignment
result = manager.execute_alignment(party1_data, party2_data)
logger.info(f"Alignment complete: {result}")
if __name__ == "__main__":
main()
1.3.4 特征异构处理:Embedding空间对齐与特征哈希技术
垂直联邦学习中的特征异构性表现为:不同参与方拥有不同维度、不同语义或不同分布的特征空间。例如,银行可能持有用户的交易金额与频率特征,而电商平台持有商品浏览类别与购买偏好特征。直接拼接这些异构特征会导致模型难以学习有效的联合表示。
Embedding空间对齐技术通过将各参与方的原始特征映射到共享的低维潜在空间,解决特征异构问题。每个参与方维护本地特征提取网络(如多层感知机或自编码器),将高维稀疏特征转换为低维稠密嵌入。对齐约束通过对比学习实现:来自同一实体的嵌入对(正样本)在潜在空间中距离拉近,来自不同实体的嵌入对(负样本)距离推远。该过程可通过InfoNCE损失或三元组损失(Triplet Loss)优化。
特征哈希(Feature Hashing)技术提供了一种轻量级的特征空间统一方案。通过哈希函数将高维类别特征映射到固定低维空间,不同参与方使用相同的哈希函数确保特征维度一致性。该方案无需维护全局词汇表,支持流式数据与动态特征,但可能引入哈希冲突。带符号哈希技巧(Signed Hashing)通过引入随机符号减少冲突影响,提升表示质量。

以下代码实现了Embedding空间对齐与特征哈希的完整系统,支持对比学习训练与异构特征融合:
"""
Heterogeneous Feature Alignment System
======================================
Implements embedding space alignment and feature hashing for
handling heterogeneous features in Vertical Federated Learning.
Usage:
python feature_alignment.py --mode embedding --party1_features p1.npy --party2_features p2.npy
python feature_alignment.py --mode hashing --input categorical_data.csv --output_dim 128
Features:
- Contrastive learning for embedding alignment
- Feature hashing with signed hash trick
- Cross-party embedding synchronization
- Heterogeneous feature fusion
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import pandas as pd
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
import hashlib
import argparse
import logging
from sklearn.preprocessing import StandardScaler
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class AlignmentConfig:
"""Configuration for embedding alignment."""
embedding_dim: int = 64
hidden_dims: List[int] = None
temperature: float = 0.07
learning_rate: float = 1e-3
batch_size: int = 256
epochs: int = 100
device: str = 'cuda' if torch.cuda.is_available() else 'cpu'
class FeatureEmbedder(nn.Module):
"""
Local feature embedder for each party.
Maps heterogeneous features to shared embedding space.
"""
def __init__(self, input_dim: int, config: AlignmentConfig):
super().__init__()
self.config = config
if config.hidden_dims is None:
config.hidden_dims = [128, 128]
# Build MLP encoder
layers = []
prev_dim = input_dim
for hidden_dim in config.hidden_dims:
layers.extend([
nn.Linear(prev_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),
nn.Dropout(0.2)
])
prev_dim = hidden_dim
layers.append(nn.Linear(prev_dim, config.embedding_dim))
self.encoder = nn.Sequential(*layers)
# Projection head for contrastive learning
self.projection = nn.Sequential(
nn.Linear(config.embedding_dim, config.embedding_dim),
nn.ReLU(),
nn.Linear(config.embedding_dim, config.embedding_dim)
)
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Returns:
embedding: Normalized embedding for alignment
projection: Projected representation for contrastive loss
"""
embedding = self.encoder(x)
embedding = F.normalize(embedding, dim=1)
projection = self.projection(embedding)
projection = F.normalize(projection, dim=1)
return embedding, projection
class ContrastiveAlignmentLoss(nn.Module):
"""
InfoNCE-based contrastive loss for cross-party embedding alignment.
"""
def __init__(self, temperature: float = 0.07):
super().__init__()
self.temperature = temperature
def forward(self, projections1: torch.Tensor,
projections2: torch.Tensor) -> torch.Tensor:
"""
Compute contrastive loss between two parties' projections.
Args:
projections1: (batch_size, embed_dim) from party 1
projections2: (batch_size, embed_dim) from party 2
(Assumes aligned batch: i-th sample in both tensors is same entity)
"""
batch_size = projections1.size(0)
# Compute similarity matrix
# Positive pairs are on the diagonal
similarity_matrix = torch.matmul(projections1, projections2.T) / self.temperature
# Labels: diagonal elements are positives (0, 1, 2, ...)
labels = torch.arange(batch_size).to(projections1.device)
# Loss for party1 -> party2
loss_12 = F.cross_entropy(similarity_matrix, labels)
# Loss for party2 -> party1 (symmetric)
loss_21 = F.cross_entropy(similarity_matrix.T, labels)
return (loss_12 + loss_21) / 2
class EmbeddingAlignmentTrainer:
"""
Trainer for cross-party embedding alignment using contrastive learning.
"""
def __init__(self, party1_input_dim: int, party2_input_dim: int,
config: AlignmentConfig = None):
self.config = config or AlignmentConfig()
# Initialize embedders for each party
self.embedder1 = FeatureEmbedder(party1_input_dim, self.config).to(self.config.device)
self.embedder2 = FeatureEmbedder(party2_input_dim, self.config).to(self.config.device)
self.criterion = ContrastiveAlignmentLoss(self.config.temperature)
self.optimizer = torch.optim.Adam(
list(self.embedder1.parameters()) + list(self.embedder2.parameters()),
lr=self.config.learning_rate
)
self.scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
self.optimizer, T_max=self.config.epochs
)
def train_step(self, batch1: torch.Tensor, batch2: torch.Tensor) -> float:
"""Single training step."""
self.optimizer.zero_grad()
# Forward pass
_, proj1 = self.embedder1(batch1)
_, proj2 = self.embedder2(batch2)
# Compute loss
loss = self.criterion(proj1, proj2)
# Backward pass
loss.backward()
self.optimizer.step()
return loss.item()
def fit(self, party1_data: np.ndarray, party2_data: np.ndarray,
validation_split: float = 0.1) -> Dict[str, List[float]]:
"""
Train alignment on paired data.
Args:
party1_data: (n_samples, party1_dim)
party2_data: (n_samples, party2_dim) - aligned with party1
"""
# Normalize inputs
scaler1 = StandardScaler()
scaler2 = StandardScaler()
party1_data = scaler1.fit_transform(party1_data)
party2_data = scaler2.fit_transform(party2_data)
# Split validation
n_val = int(len(party1_data) * validation_split)
indices = np.random.permutation(len(party1_data))
train_idx = indices[n_val:]
val_idx = indices[:n_val]
history = {'train_loss': [], 'val_loss': []}
for epoch in range(self.config.epochs):
# Training
self.embedder1.train()
self.embedder2.train()
epoch_losses = []
for i in range(0, len(train_idx), self.config.batch_size):
batch_idx = train_idx[i:i + self.config.batch_size]
batch1 = torch.FloatTensor(party1_data[batch_idx]).to(self.config.device)
batch2 = torch.FloatTensor(party2_data[batch_idx]).to(self.config.device)
loss = self.train_step(batch1, batch2)
epoch_losses.append(loss)
avg_train_loss = np.mean(epoch_losses)
history['train_loss'].append(avg_train_loss)
# Validation
if len(val_idx) > 0:
self.embedder1.eval()
self.embedder2.eval()
with torch.no_grad():
val_batch1 = torch.FloatTensor(party1_data[val_idx]).to(self.config.device)
val_batch2 = torch.FloatTensor(party2_data[val_idx]).to(self.config.device)
_, val_proj1 = self.embedder1(val_batch1)
_, val_proj2 = self.embedder2(val_batch2)
val_loss = self.criterion(val_proj1, val_proj2).item()
history['val_loss'].append(val_loss)
else:
history['val_loss'].append(avg_train_loss)
self.scheduler.step()
if (epoch + 1) % 10 == 0:
logger.info(f"Epoch {epoch+1}/{self.config.epochs}, "
f"Train Loss: {avg_train_loss:.4f}, "
f"Val Loss: {history['val_loss'][-1]:.4f}")
return history
def get_aligned_embeddings(self, party1_data: np.ndarray,
party2_data: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""Get aligned embeddings for both parties."""
self.embedder1.eval()
self.embedder2.eval()
with torch.no_grad():
batch1 = torch.FloatTensor(party1_data).to(self.config.device)
batch2 = torch.FloatTensor(party2_data).to(self.config.device)
emb1, _ = self.embedder1(batch1)
emb2, _ = self.embedder2(batch2)
return emb1.cpu().numpy(), emb2.cpu().numpy()
class FeatureHasher:
"""
Implements feature hashing (hashing trick) for categorical features.
Uses signed hash to reduce collision impact.
"""
def __init__(self, output_dim: int = 128, use_signed_hash: bool = True):
self.output_dim = output_dim
self.use_signed_hash = use_signed_hash
def _hash_feature(self, feature_name: str, value: str) -> Tuple[int, int]:
"""
Hash feature name and value to index and sign.
Returns: (index, sign)
"""
# Create composite key
key = f"{feature_name}={value}"
# Compute hash
hash_val = int(hashlib.md5(key.encode()).hexdigest(), 16)
# Map to output dimension
index = hash_val % self.output_dim
# Determine sign
if self.use_signed_hash:
# Use another hash bit for sign
sign = -1 if (hash_val >> 10) % 2 == 0 else 1
else:
sign = 1
return index, sign
def transform(self, data: Dict[str, List[str]]) -> np.ndarray:
"""
Transform dictionary of categorical features to hashed vectors.
Args:
data: Dict mapping feature names to lists of values
Returns:
(n_samples, output_dim) array
"""
n_samples = len(list(data.values())[0])
result = np.zeros((n_samples, self.output_dim))
for feature_name, values in data.items():
for i, value in enumerate(values):
idx, sign = self._hash_feature(feature_name, str(value))
result[i, idx] += sign
return result
def transform_interactions(self, data: Dict[str, List[str]],
interaction_pairs: List[Tuple[str, str]]) -> np.ndarray:
"""
Transform with feature interactions.
"""
n_samples = len(list(data.values())[0])
result = np.zeros((n_samples, self.output_dim))
# Single features
for feature_name, values in data.items():
for i, value in enumerate(values):
idx, sign = self._hash_feature(feature_name, str(value))
result[i, idx] += sign
# Feature interactions
for feat1, feat2 in interaction_pairs:
if feat1 in data and feat2 in data:
for i, (v1, v2) in enumerate(zip(data[feat1], data[feat2])):
key = f"{feat1}={v1}|{feat2}={v2}"
hash_val = int(hashlib.md5(key.encode()).hexdigest(), 16)
idx = hash_val % self.output_dim
sign = -1 if (hash_val >> 10) % 2 == 0 else 1
result[i, idx] += sign
return result
class HeterogeneousFeatureFusion:
"""
Fuses features from multiple parties with heterogeneous feature spaces.
"""
def __init__(self, party_dims: Dict[str, int], embedding_dim: int = 64):
self.party_dims = party_dims
self.embedding_dim = embedding_dim
self.alignment_trainers = {}
def align_party_pair(self, party1_id: str, party2_id: str,
party1_data: np.ndarray, party2_data: np.ndarray,
config: AlignmentConfig = None) -> EmbeddingAlignmentTrainer:
"""Train alignment between two parties."""
trainer = EmbeddingAlignmentTrainer(
self.party_dims[party1_id],
self.party_dims[party2_id],
config
)
trainer.fit(party1_data, party2_data)
self.alignment_trainers[(party1_id, party2_id)] = trainer
return trainer
def fuse_features(self, party_data: Dict[str, np.ndarray],
aligned_pairs: List[Tuple[str, str]]) -> np.ndarray:
"""
Fuse features from multiple parties into unified representation.
Args:
party_data: Dict mapping party_id to feature arrays
aligned_pairs: List of (party1, party2) tuples that are aligned
Returns:
Fused feature matrix
"""
# Get embeddings for all parties
party_embeddings = {}
for (p1, p2), trainer in self.alignment_trainers.items():
if p1 in party_data and p2 in party_data:
emb1, emb2 = trainer.get_aligned_embeddings(
party_data[p1], party_data[p2]
)
party_embeddings[p1] = emb1
party_embeddings[p2] = emb2
# Concatenate embeddings from all parties
embeddings_list = [party_embeddings[p] for p in party_data.keys()]
fused = np.hstack(embeddings_list)
return fused
def main():
parser = argparse.ArgumentParser(description='Feature Alignment for VFL')
parser.add_argument('--mode', choices=['embedding', 'hashing'], required=True)
parser.add_argument('--party1', help='Party 1 features (.npy)')
parser.add_argument('--party2', help='Party 2 features (.npy)')
parser.add_argument('--input', help='Input CSV for hashing mode')
parser.add_argument('--output-dim', type=int, default=128)
parser.add_argument('--embedding-dim', type=int, default=64)
parser.add_argument('--epochs', type=int, default=100)
args = parser.parse_args()
if args.mode == 'embedding':
# Load data
party1_data = np.load(args.party1)
party2_data = np.load(args.party2)
logger.info(f"Loaded data: Party1 {party1_data.shape}, Party2 {party2_data.shape}")
# Train alignment
config = AlignmentConfig(
embedding_dim=args.embedding_dim,
epochs=args.epochs
)
trainer = EmbeddingAlignmentTrainer(
party1_data.shape[1],
party2_data.shape[1],
config
)
history = trainer.fit(party1_data, party2_data)
# Get aligned embeddings
emb1, emb2 = trainer.get_aligned_embeddings(party1_data, party2_data)
logger.info(f"Aligned embeddings: {emb1.shape}, {emb2.shape}")
# Save
np.save('aligned_party1.npy', emb1)
np.save('aligned_party2.npy', emb2)
elif args.mode == 'hashing':
# Load categorical data
df = pd.read_csv(args.input)
# Convert to dictionary format
data = {col: df[col].astype(str).tolist() for col in df.columns}
# Hash
hasher = FeatureHasher(output_dim=args.output_dim)
hashed = hasher.transform(data)
logger.info(f"Hashed features shape: {hashed.shape}")
np.save('hashed_features.npy', hashed)
if __name__ == "__main__":
main()
1.3.5 对齐质量评估:Precision-Recall在隐私约束下的度量方法
在隐私保护记录链接与实体解析系统中,对齐质量的评估面临独特挑战:传统评估依赖于真实标签(Ground Truth)的完全暴露,而隐私约束限制了参与方对非交集样本信息的访问。因此,需要设计能够在部分信息可见条件下准确度量系统性能的评估框架。
Precision(精确率)衡量被系统判定为匹配的记录对中真实匹配的比例,反映系统的可靠性;Recall(召回率)衡量真实匹配记录对中被系统成功识别的比例,反映系统的覆盖能力。在VFL场景下,Precision与Recall的计算需基于交集样本的真实标签,通过安全聚合协议在不泄露个体信息的前提下统计True Positive、False Positive与False Negative的数量。
隐私约束下的评估策略包括:差分隐私噪声注入,在统计计数中添加 calibrated noise 保护个体隐私;安全多方计算协议,使各方在加密状态下联合计算评估指标;采样评估,仅对随机抽样的子集进行人工标注与评估,降低隐私风险。

以下代码实现了隐私约束下的对齐质量评估系统,支持安全聚合、差分隐私与采样评估:
"""
Privacy-Preserving Alignment Quality Evaluation
===============================================
Implements precision-recall metrics under privacy constraints for VFL.
Supports secure aggregation, differential privacy, and sampling-based evaluation.
Usage:
python alignment_evaluation.py --ground-truth gt.csv --predictions pred.csv
--privacy-mode dp --epsilon 1.0
Features:
- Precision, Recall, F1 calculation
- Differential privacy noise injection
- Secure multi-party aggregation simulation
- Sampling-based evaluation
- Privacy budget tracking
"""
import numpy as np
import pandas as pd
from typing import Dict, List, Tuple, Set, Optional
from dataclasses import dataclass
from enum import Enum
import argparse
import logging
from scipy import stats
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class PrivacyMode(Enum):
"""Privacy protection modes for evaluation."""
PLAIN = "plain" # No privacy protection
DIFFERENTIAL_PRIVACY = "dp" # Differential privacy
SECURE_AGGREGATION = "secure" # Secure aggregation simulation
SAMPLING = "sampling" # Sampling-based evaluation
@dataclass
class PrivacyConfig:
"""Configuration for privacy-preserving evaluation."""
mode: PrivacyMode = PrivacyMode.PLAIN
epsilon: float = 1.0 # DP privacy budget
delta: float = 1e-5 # DP delta parameter
sample_rate: float = 0.1 # Sampling rate for sampling mode
num_parties: int = 2 # Number of parties for secure aggregation
noise_scale: float = 1.0 # Scale for Gaussian/Laplace noise
class DifferentialPrivacyMechanism:
"""
Implements differential privacy mechanisms for count queries.
"""
def __init__(self, epsilon: float, delta: float = 1e-5):
self.epsilon = epsilon
self.delta = delta
def laplace_mechanism(self, true_count: float, sensitivity: float = 1.0) -> float:
"""
Add Laplace noise for (epsilon, 0)-DP.
"""
scale = sensitivity / self.epsilon
noise = np.random.laplace(0, scale)
return max(0, true_count + noise) # Counts can't be negative
def gaussian_mechanism(self, true_count: float, sensitivity: float = 1.0) -> float:
"""
Add Gaussian noise for (epsilon, delta)-DP.
"""
# Calculate sigma for (epsilon, delta)-DP
sigma = sensitivity * np.sqrt(2 * np.log(1.25 / self.delta)) / self.epsilon
noise = np.random.normal(0, sigma)
return max(0, true_count + noise)
def calibrate_noise(self, query_count: int) -> float:
"""
Calibrate noise scale for multiple queries (basic composition).
"""
# Simple composition: epsilon_total = query_count * epsilon_per_query
per_query_epsilon = self.epsilon / query_count
return per_query_epsilon
class SecureAggregationSimulator:
"""
Simulates secure aggregation across multiple parties.
In real implementation, this would use cryptographic protocols.
"""
def __init__(self, num_parties: int):
self.num_parties = num_parties
def aggregate_counts(self, party_counts: List[Dict[str, int]]) -> Dict[str, float]:
"""
Simulate secure aggregation of counts from multiple parties.
Adds masking noise that cancels out during aggregation.
"""
# Each party adds random mask
masks = [np.random.randint(-1000, 1000) for _ in range(self.num_parties)]
# Ensure masks sum to 0
masks[-1] = -sum(masks[:-1])
aggregated = {}
for key in party_counts[0].keys():
masked_sum = sum(pc[key] + m for pc, m in zip(party_counts, masks))
aggregated[key] = max(0, masked_sum) # Ensure non-negative
return aggregated
class AlignmentEvaluator:
"""
Evaluates alignment quality under privacy constraints.
"""
def __init__(self, config: PrivacyConfig = None):
self.config = config or PrivacyConfig()
self.dp_mechanism = None
self.secure_agg = None
if self.config.mode == PrivacyMode.DIFFERENTIAL_PRIVACY:
self.dp_mechanism = DifferentialPrivacyMechanism(
self.config.epsilon,
self.config.delta
)
elif self.config.mode == PrivacyMode.SECURE_AGGREGATION:
self.secure_agg = SecureAggregationSimulator(
self.config.num_parties
)
self.privacy_budget_spent = 0.0
def compute_confusion_matrix(self,
ground_truth: Set[Tuple[str, str]],
predictions: Set[Tuple[str, str]],
entity_ids: Optional[Set[str]] = None) -> Dict[str, int]:
"""
Compute confusion matrix components.
Args:
ground_truth: Set of (id1, id2) tuples representing true matches
predictions: Set of (id1, id2) tuples predicted as matches
entity_ids: Optional set of all entity IDs (for computing TN)
Returns:
Dictionary with TP, FP, FN counts
"""
tp_set = ground_truth & predictions
fp_set = predictions - ground_truth
fn_set = ground_truth - predictions
# Calculate true negatives if entity space is provided
tn = None
if entity_ids:
all_possible = set((e1, e2) for e1 in entity_ids for e2 in entity_ids if e1 != e2)
tn_set = all_possible - ground_truth - predictions
tn = len(tn_set)
return {
'true_positives': len(tp_set),
'false_positives': len(fp_set),
'false_negatives': len(fn_set),
'true_negatives': tn
}
def apply_privacy(self, counts: Dict[str, int]) -> Dict[str, float]:
"""
Apply privacy protection to confusion matrix counts.
"""
if self.config.mode == PrivacyMode.PLAIN:
return {k: float(v) for k, v in counts.items()}
elif self.config.mode == PrivacyMode.DIFFERENTIAL_PRIVACY:
# Apply Gaussian mechanism to each count
noisy_counts = {}
num_queries = 3 # TP, FP, FN
per_query_epsilon = self.dp_mechanism.calibrate_noise(num_queries)
dp_temp = DifferentialPrivacyMechanism(per_query_epsilon, self.config.delta)
for key in ['true_positives', 'false_positives', 'false_negatives']:
noisy_counts[key] = dp_temp.gaussian_mechanism(counts[key])
if counts.get('true_negatives') is not None:
noisy_counts['true_negatives'] = dp_temp.gaussian_mechanism(counts['true_negatives'])
self.privacy_budget_spent += self.config.epsilon
return noisy_counts
elif self.config.mode == PrivacyMode.SECURE_AGGREGATION:
# Simulate aggregation across parties
# In reality, each party would compute local counts
party_counts = [counts.copy() for _ in range(self.config.num_parties)]
return self.secure_agg.aggregate_counts(party_counts)
elif self.config.mode == PrivacyMode.SAMPLING:
# Return counts with sampling indicator
sampled_counts = {k: v / self.config.sample_rate for k, v in counts.items()}
sampled_counts['_sampling_rate'] = self.config.sample_rate
return sampled_counts
return counts
def calculate_metrics(self, counts: Dict[str, float]) -> Dict[str, float]:
"""
Calculate precision, recall, F1 from confusion matrix.
"""
tp = counts.get('true_positives', 0)
fp = counts.get('false_positives', 0)
fn = counts.get('false_negatives', 0)
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
# Specificity if TN available
specificity = None
if 'true_negatives' in counts and counts['true_negatives'] is not None:
tn = counts['true_negatives']
specificity = tn / (tn + fp) if (tn + fp) > 0 else 0.0
return {
'precision': precision,
'recall': recall,
'f1_score': f1,
'specificity': specificity,
'true_positives': tp,
'false_positives': fp,
'false_negatives': fn
}
def evaluate(self, ground_truth: List[Tuple[str, str]],
predictions: List[Tuple[str, str]],
entity_ids: Optional[List[str]] = None) -> Dict[str, any]:
"""
Complete evaluation pipeline with privacy protection.
"""
gt_set = set(ground_truth)
pred_set = set(predictions)
entity_set = set(entity_ids) if entity_ids else None
# Compute confusion matrix
counts = self.compute_confusion_matrix(gt_set, pred_set, entity_set)
logger.info(f"Raw counts: {counts}")
# Apply privacy protection
private_counts = self.apply_privacy(counts)
logger.info(f"Privacy-protected counts: {private_counts}")
# Calculate metrics
metrics = self.calculate_metrics(private_counts)
# Add privacy metadata
metrics['privacy_mode'] = self.config.mode.value
metrics['privacy_budget_spent'] = self.privacy_budget_spent
if self.config.mode == PrivacyMode.DIFFERENTIAL_PRIVACY:
metrics['epsilon'] = self.config.epsilon
metrics['delta'] = self.config.delta
return metrics
def bootstrap_confidence_intervals(self,
ground_truth: List[Tuple[str, str]],
predictions: List[Tuple[str, str]],
n_bootstrap: int = 1000,
confidence: float = 0.95) -> Dict[str, Tuple[float, float]]:
"""
Compute confidence intervals for metrics using bootstrap.
"""
gt_set = set(ground_truth)
pred_set = set(predictions)
# Get aligned pairs
tp_pairs = list(gt_set & pred_set)
fp_pairs = list(pred_set - gt_set)
fn_pairs = list(gt_set - pred_set)
bootstrap_metrics = {'precision': [], 'recall': [], 'f1_score': []}
for _ in range(n_bootstrap):
# Resample
boot_tp = np.random.choice([0, 1], size=len(tp_pairs), replace=True).sum()
boot_fp = np.random.choice([0, 1], size=len(fp_pairs), replace=True).sum()
boot_fn = np.random.choice([0, 1], size=len(fn_pairs), replace=True).sum()
boot_counts = {
'true_positives': boot_tp,
'false_positives': boot_fp,
'false_negatives': boot_fn
}
# Apply privacy if needed
if self.config.mode != PrivacyMode.PLAIN:
boot_counts = self.apply_privacy(boot_counts)
metrics = self.calculate_metrics(boot_counts)
for key in bootstrap_metrics:
bootstrap_metrics[key].append(metrics[key])
# Calculate confidence intervals
alpha = 1 - confidence
intervals = {}
for key, values in bootstrap_metrics.items():
lower = np.percentile(values, alpha/2 * 100)
upper = np.percentile(values, (1 - alpha/2) * 100)
intervals[key] = (lower, upper)
return intervals
def generate_report(self, metrics: Dict[str, any]) -> str:
"""Generate human-readable evaluation report."""
report = []
report.append("=" * 50)
report.append("ALIGNMENT QUALITY EVALUATION REPORT")
report.append("=" * 50)
report.append(f"Privacy Mode: {metrics['privacy_mode']}")
if metrics['privacy_mode'] == 'dp':
report.append(f"Privacy Budget (ε): {metrics.get('epsilon', 'N/A')}")
report.append(f"Delta (δ): {metrics.get('delta', 'N/A')}")
report.append("-" * 50)
report.append(f"Precision: {metrics['precision']:.4f}")
report.append(f"Recall: {metrics['recall']:.4f}")
report.append(f"F1 Score: {metrics['f1_score']:.4f}")
if metrics.get('specificity'):
report.append(f"Specificity: {metrics['specificity']:.4f}")
report.append("-" * 50)
report.append(f"True Positives: {metrics['true_positives']:.1f}")
report.append(f"False Positives: {metrics['false_positives']:.1f}")
report.append(f"False Negatives: {metrics['false_negatives']:.1f}")
if metrics.get('privacy_budget_spent'):
report.append("-" * 50)
report.append(f"Privacy Budget Spent: {metrics['privacy_budget_spent']:.4f}")
report.append("=" * 50)
return "\n".join(report)
def main():
parser = argparse.ArgumentParser(description='Alignment Quality Evaluation')
parser.add_argument('--ground-truth', required=True, help='Ground truth CSV')
parser.add_argument('--predictions', required=True, help='Predictions CSV')
parser.add_argument('--privacy-mode', choices=['plain', 'dp', 'secure', 'sampling'],
default='plain')
parser.add_argument('--epsilon', type=float, default=1.0)
parser.add_argument('--delta', type=float, default=1e-5)
parser.add_argument('--sample-rate', type=float, default=0.1)
parser.add_argument('--bootstrap', action='store_true', help='Compute confidence intervals')
args = parser.parse_args()
# Load data
gt_df = pd.read_csv(args.ground_truth)
pred_df = pd.read_csv(args.predictions)
ground_truth = list(zip(gt_df['id1'], gt_df['id2']))
predictions = list(zip(pred_df['id1'], pred_df['id2']))
# Configure privacy
config = PrivacyConfig(
mode=PrivacyMode(args.privacy_mode),
epsilon=args.epsilon,
delta=args.delta,
sample_rate=args.sample_rate
)
# Evaluate
evaluator = AlignmentEvaluator(config)
metrics = evaluator.evaluate(ground_truth, predictions)
# Bootstrap if requested
if args.bootstrap:
intervals = evaluator.bootstrap_confidence_intervals(ground_truth, predictions)
metrics['confidence_intervals'] = intervals
# Generate report
report = evaluator.generate_report(metrics)
print(report)
# Save detailed results
output_file = f"evaluation_results_{args.privacy_mode}.json"
import json
with open(output_file, 'w') as f:
json.dump(metrics, f, indent=2, default=str)
logger.info(f"Results saved to {output_file}")
if __name__ == "__main__":
main()
1.4 安全训练协议与梯度保护机制
纵向联邦学习(Vertical Federated Learning, VFL)在实现跨机构数据协同训练的同时,面临着严峻的隐私泄露风险。与横向联邦学习不同,VFL的参与方持有同一用户群体的不同特征维度,这种数据分布特性使得传统的安全聚合机制难以直接适用。本节系统阐述VFL场景下的安全训练协议体系,涵盖分裂学习架构融合、安全矩阵乘法、梯度泄露防御、秘密分享训练以及通信压缩等关键技术层面。
1.4.1 分裂学习(Split Learning)与VFL的融合架构
分裂学习(Split Learning, SL)作为一种分布式机器学习范式,通过在网络层间分割模型架构实现计算卸载与隐私保护的平衡。在VFL环境中,SL与联邦学习的融合形成了独特的分层训练架构,该架构的核心在于将神经网络切割为多个子模型,分别部署于不同的参与方。
在典型的两方VFL-SL融合架构中,数据持有方(Passive Party)拥有特征数据但无标签,其本地部署底层模型(Bottom Model)负责特征提取;标签持有方(Active Party)拥有标签信息,部署顶层模型(Top Model)完成损失计算与梯度回传。两方的模型在切割层(Cut Layer)通过中间表征(Intermediate Representations)进行交互。这种架构的优势在于,原始特征数据始终保留在本地,仅共享经过变换的嵌入向量,从而降低了直接数据泄露的风险。
然而,传统的同步SL架构存在计算资源利用率低、训练延迟高等问题。近期研究提出的PubSub-VFL架构通过发布/订阅模式解耦训练流程,采用分层异步机制显著降低延迟。该架构将参数服务器的并行性与发布/订阅的解耦能力相结合,使参与方能够根据自身的系统配置动态选择最优超参数,在保持模型精度的同时实现2至7倍的训练加速。
从安全角度分析,SL架构面临的主要威胁包括中间表征攻击与模型补全攻击。攻击者可能通过截获切割层的嵌入向量重构原始特征,或利用底层模型输出推断标签信息。因此,单纯的模型分割不足以提供强隐私保证,必须与加密计算、扰动机制等技术相结合。
"""
VFL-Split Learning Fusion Architecture Implementation
=====================================================
This script implements a two-party split learning framework integrated
with vertical federated learning. It demonstrates the split architecture
where features and labels are held by different parties, communicating
through intermediate representations at the cut layer.
Usage:
1. Run Party A (Feature Owner): python vfl_split_learning.py --party A --data_path features.csv
2. Run Party B (Label Owner): python vfl_split_learning.py --party B --data_path labels.csv
3. Ensure both parties can communicate via the specified host/port
Dependencies: torch, numpy, socket, threading, argparse
"""
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import socket
import pickle
import struct
import threading
import argparse
from typing import Tuple, Optional
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class BottomModel(nn.Module):
"""
Bottom model deployed at the feature owner (Passive Party).
Processes raw features and extracts intermediate representations.
Architecture optimized for VFL: maintains local feature privacy
by only sharing embeddings, not raw data.
"""
def __init__(self, input_dim: int, hidden_dims: list, cut_layer_dim: int):
super(BottomModel, self).__init__()
layers = []
prev_dim = input_dim
# Deep feature extraction layers
for i, h_dim in enumerate(hidden_dims):
layers.append(nn.Linear(prev_dim, h_dim))
layers.append(nn.BatchNorm1d(h_dim)) # Stabilize training
layers.append(nn.ReLU())
layers.append(nn.Dropout(0.2)) # Prevent overfitting
prev_dim = h_dim
# Cut layer projection
self.feature_extractor = nn.Sequential(*layers)
self.cut_layer = nn.Linear(prev_dim, cut_layer_dim)
self.activation = nn.ReLU()
# Initialize weights with Xavier for stable gradient flow
self._initialize_weights()
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
nn.init.zeros_(m.bias)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Forward pass through bottom model.
Args:
x: Raw feature tensor [batch_size, input_dim]
Returns:
Intermediate representation [batch_size, cut_layer_dim]
"""
features = self.feature_extractor(x)
embedding = self.activation(self.cut_layer(features))
return embedding
class TopModel(nn.Module):
"""
Top model deployed at the label owner (Active Party).
Completes the forward pass and computes loss.
"""
def __init__(self, cut_layer_dim: int, hidden_dims: list, num_classes: int):
super(TopModel, self).__init__()
layers = []
prev_dim = cut_layer_dim
for h_dim in hidden_dims:
layers.append(nn.Linear(prev_dim, h_dim))
layers.append(nn.ReLU())
prev_dim = h_dim
self.predictor = nn.Sequential(*layers)
self.output_layer = nn.Linear(prev_dim, num_classes)
def forward(self, embedding: torch.Tensor) -> torch.Tensor:
"""
Forward pass from cut layer to predictions.
Args:
embedding: Intermediate representation from bottom model
"""
hidden = self.predictor(embedding)
logits = self.output_layer(hidden)
return logits
class SplitLearningServer:
"""
Communication coordinator for split learning.
Handles asynchronous message passing between parties with
gradient compression and encryption hooks.
"""
def __init__(self, host: str, port: int, is_active: bool):
self.host = host
self.port = port
self.is_active = is_active # True for label owner, False for feature owner
self.socket = None
self.conn = None
self.buffer_size = 4096
def connect(self):
"""Establish connection between parties."""
if self.is_active:
# Active party acts as server
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind((self.host, self.port))
self.socket.listen(1)
logger.info(f"Active party listening on {self.host}:{self.port}")
self.conn, addr = self.socket.accept()
logger.info(f"Connected by {addr}")
else:
# Passive party connects to active
self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.conn.connect((self.host, self.port))
logger.info(f"Connected to active party at {self.host}:{self.port}")
def send_tensor(self, tensor: torch.Tensor):
"""Send tensor with length prefix framing."""
data = pickle.dumps(tensor.detach().cpu().numpy())
length = struct.pack('!I', len(data))
self.conn.sendall(length + data)
def receive_tensor(self) -> torch.Tensor:
"""Receive tensor with proper framing."""
# Receive length
length_data = self._recv_all(4)
length = struct.unpack('!I', length_data)[0]
# Receive data
data = self._recv_all(length)
array = pickle.loads(data)
return torch.from_numpy(array)
def _recv_all(self, n: int) -> bytes:
"""Helper to receive exactly n bytes."""
data = b''
while len(data) < n:
packet = self.conn.recv(min(n - len(data), self.buffer_size))
if not packet:
raise ConnectionError("Connection closed unexpectedly")
data += packet
return data
def close(self):
if self.conn:
self.conn.close()
if self.socket:
self.socket.close()
class VFLTrainer:
"""
Main training coordinator implementing the split learning protocol
with privacy-preserving mechanisms.
"""
def __init__(self,
party: str,
input_dim: int,
cut_dim: int = 64,
num_classes: int = 2,
learning_rate: float = 0.001):
self.party = party
self.cut_dim = cut_dim
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
if party == 'A':
# Feature owner: bottom model only
self.local_model = BottomModel(
input_dim=input_dim,
hidden_dims=[256, 128],
cut_layer_dim=cut_dim
).to(self.device)
self.optimizer = optim.Adam(self.local_model.parameters(), lr=learning_rate)
self.is_active = False
else:
# Label owner: top model + loss computation
self.local_model = TopModel(
cut_layer_dim=cut_dim,
hidden_dims=[128, 64],
num_classes=num_classes
).to(self.device)
self.criterion = nn.CrossEntropyLoss()
self.optimizer = optim.Adam(self.local_model.parameters(), lr=learning_rate)
self.is_active = True
self.communicator: Optional[SplitLearningServer] = None
self.gradients_buffer = None # Store for backward pass
def attach_communicator(self, host: str, port: int):
"""Attach network communicator."""
self.communicator = SplitLearningServer(host, port, self.is_active)
self.communicator.connect()
def train_batch(self, features: torch.Tensor, labels: Optional[torch.Tensor] = None) -> float:
"""
Execute one batch of split learning.
Protocol:
1. Passive party computes embedding, sends to active party
2. Active party completes forward, computes loss, sends gradients back
3. Passive party updates bottom model with received gradients
"""
features = features.to(self.device)
if self.party == 'A':
# Passive party forward
self.local_model.train()
self.optimizer.zero_grad()
embedding = self.local_model(features)
embedding.retain_grad() # Keep for backward
# Send to active party
self.communicator.send_tensor(embedding)
self.gradients_buffer = embedding
# Receive gradients from active party
grad_from_top = self.communicator.receive_tensor().to(self.device)
# Backward through bottom model
embedding.backward(grad_from_top)
self.optimizer.step()
return 0.0 # Loss not available at passive party
else:
# Active party forward
self.local_model.train()
self.optimizer.zero_grad()
# Receive embedding from passive party
embedding = self.communicator.receive_tensor().to(self.device)
embedding.requires_grad_(True)
# Complete forward pass
logits = self.local_model(embedding)
labels = labels.to(self.device)
loss = self.criterion(logits, labels)
# Backward to get gradients w.r.t embedding
loss.backward()
# Send gradients to passive party
self.communicator.send_tensor(embedding.grad)
# Update top model
self.optimizer.step()
return loss.item()
def simulate_vfl_training():
"""
Simulation mode for testing without network setup.
Demonstrates the split learning flow on synthetic data.
"""
batch_size = 32
feature_dim = 100
cut_dim = 64
num_classes = 10
# Initialize models
bottom = BottomModel(feature_dim, [256, 128], cut_dim)
top = TopModel(cut_dim, [128, 64], num_classes)
# Synthetic data
features = torch.randn(batch_size, feature_dim)
labels = torch.randint(0, num_classes, (batch_size,))
# Optimizers
opt_bottom = optim.Adam(bottom.parameters(), lr=0.001)
opt_top = optim.Adam(top.parameters(), lr=0.001)
# Training loop
for epoch in range(5):
opt_bottom.zero_grad()
opt_top.zero_grad()
# Forward through bottom
embedding = bottom(features)
embedding.retain_grad()
# Forward through top
logits = top(embedding)
loss = nn.CrossEntropyLoss()(logits, labels)
# Backward
loss.backward()
# Update both models
opt_bottom.step()
opt_top.step()
print(f"Epoch {epoch}, Loss: {loss.item():.4f}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='VFL Split Learning')
parser.add_argument('--party', choices=['A', 'B'], help='Party role: A=Feature owner, B=Label owner')
parser.add_argument('--host', default='localhost', help='Host address')
parser.add_argument('--port', type=int, default=8080, help='Port number')
parser.add_argument('--simulate', action='store_true', help='Run simulation without networking')
args = parser.parse_args()
if args.simulate:
simulate_vfl_training()
else:
if not args.party:
raise ValueError("Must specify --party (A or B) for distributed mode")
# Example usage with dummy dimensions
input_dim = 100 if args.party == 'A' else 64 # B receives cut_dim
trainer = VFLTrainer(
party=args.party,
input_dim=input_dim,
cut_dim=64,
num_classes=10
)
trainer.attach_communicator(args.host, args.port)
# Training would proceed here with real data loading
logger.info("Connected. Ready for training.")
1.4.2 安全矩阵乘法协议:Beaver Triples与SPDZ2k实现
安全矩阵乘法是VFL训练的核心计算原语,涉及多方协同计算线性层的前向传播与反向传播。Beaver乘法三元组(Beaver Triples)提供了一种将乘法运算分解为本地计算与轻量级通信的有效方法,而SPDZ2k协议将这一机制扩展到环上的恶意安全计算场景。
Beaver三元组由三个秘密分享值组成,满足关系 c=a⋅b ,其中 a,b,c 均为随机数。在乘法运算阶段,参与方首先本地计算掩码值 x−a 和 y−b 并公开,随后基于公开值与本地持有的分享值重构乘积分享。该方法将昂贵的乘法运算转化为离线阶段生成三元组与在线阶段执行简单线性组合,显著降低了通信复杂度。
SPDZ2k协议针对机器学习工作负载优化,在模 2k 的环上运算,避免了有限域运算带来的精度损失问题。该协议采用信息论消息认证码(IT-MACs)实现恶意安全,每个分享值附带一个MAC标签,确保敌手无法篡改分享值而不被发现。SPDZ2k的预处理阶段通过不经意传输(OT)生成认证的Beaver三元组,在线阶段则执行高效的矩阵乘法与截断操作。
对于VFL中的分层模型训练,安全矩阵乘法协议需要处理定点数表示与截断问题。由于神经网络参数与激活值通常为浮点数,必须在保持精度的前提下实现安全定点运算。优化的截断协议通过延迟重分享(Delayed Resharing)技术减少通信轮数,在保持计算精度的同时提升吞吐量。
"""
Secure Matrix Multiplication with Beaver Triples and SPDZ2k
===========================================================
This module implements secure multiparty computation (MPC) based matrix
multiplication using Beaver triples, compatible with SPDZ2k-style protocols.
It supports both semi-honest and malicious security models with MAC checks.
Usage:
# Initialize parties with shared randomness
party0 = SPDZ2kParty(party_id=0, ring_size=64, security_param=128)
party1 = SPDZ2kParty(party_id=1, ring_size=64, security_param=128)
# Generate preprocessing triples
triples = generate_beaver_triples(matrix_dim_a, matrix_dim_b, num_triples=1000)
# Execute secure multiplication
result_shares = secure_matmul(party0, party1, share_a, share_b, triples)
Dependencies: numpy, hashlib, secrets, typing
"""
import numpy as np
import secrets
import hashlib
from typing import Tuple, List, Dict
from dataclasses import dataclass
import logging
logger = logging.getLogger(__name__)
@dataclass
class BeaverTriple:
"""
Beaver triple for secure multiplication: [a], [b], [c] where c = a * b
Each value is additively shared between parties.
"""
a_shares: Tuple[np.ndarray, np.ndarray] # (share_p0, share_p1)
b_shares: Tuple[np.ndarray, np.ndarray]
c_shares: Tuple[np.ndarray, np.ndarray]
mac_keys: Tuple[np.ndarray, np.ndarray] # MAC authentication keys
def verify_relation(self) -> bool:
"""Verify that c = a * b in plaintext (for testing)."""
a = self.a_shares[0] + self.a_shares[1]
b = self.b_shares[0] + self.b_shares[1]
c = self.c_shares[0] + self.c_shares[1]
return np.allclose(c, a @ b)
class SPDZ2kParty:
"""
Implementation of a party in the SPDZ2k protocol.
Operates over Z_{2^k} with information-theoretic MACs.
"""
def __init__(self, party_id: int, ring_size: int = 64, security_param: int = 128):
self.party_id = party_id
self.ring_size = ring_size
self.modulus = 2 ** ring_size
self.security_param = security_param
self.mac_key = self._generate_mac_key()
self.local_shares: Dict[str, np.ndarray] = {}
self.local_macs: Dict[str, np.ndarray] = {}
def _generate_mac_key(self) -> int:
"""Generate random MAC key for authenticating shares."""
return secrets.randbelow(self.modulus)
def share(self, secret: np.ndarray) -> np.ndarray:
"""
Create additive share of secret.
Party 0 gets r, Party 1 gets secret - r.
"""
if self.party_id == 0:
share = np.random.randint(0, self.modulus, size=secret.shape, dtype=np.uint64)
return share
else:
# Party 1 reconstructs from communication
return None # Placeholder, actual share received from P0
def authenticate(self, value: np.ndarray, share_id: str) -> np.ndarray:
"""
Compute MAC on shared value: m = k * x mod 2^k
In SPDZ2k, MAC is also additively shared.
"""
# Local computation of MAC share
mac_share = (self.mac_key * value) % self.modulus
self.local_shares[share_id] = value
self.local_macs[share_id] = mac_share
return mac_share
def verify_mac(self, share_id: str, opened_value: np.ndarray, mac_shares: List[np.ndarray]) -> bool:
"""
Verify MAC consistency when opening values.
Check if sum(mac_shares) == mac_key * opened_value.
"""
total_mac = sum(mac_shares) % self.modulus
expected_mac = (self.mac_key * opened_value) % self.modulus
return np.array_equal(total_mac, expected_mac)
class SecureMatrixMultiplication:
"""
Secure matrix multiplication using Beaver triples with SPDZ2k optimizations.
Supports batch operations for neural network layers.
"""
def __init__(self, parties: List[SPDZ2kParty], triple_buffer: List[BeaverTriple]):
self.parties = parties
self.num_parties = len(parties)
self.triple_buffer = triple_buffer
self.triple_idx = 0
def _get_next_triple(self) -> BeaverTriple:
"""Retrieve next available Beaver triple."""
if self.triple_idx >= len(self.triple_buffer):
raise ValueError("Triple buffer exhausted")
triple = self.triple_buffer[self.triple_idx]
self.triple_idx += 1
return triple
def multiply(self,
x_shares: List[np.ndarray],
y_shares: List[np.ndarray]) -> List[np.ndarray]:
"""
Secure multiplication protocol using Beaver triples.
Protocol:
1. Open [x - a] and [y - b] using broadcast
2. Each party computes z_i = (x-a)*y_i + (y-b)*x_i + c_i
3. Result is sharing of x*y without revealing x or y
Args:
x_shares: Additive shares of matrix X from each party
y_shares: Additive shares of matrix Y from each party
Returns:
Additive shares of X @ Y
"""
# Get preprocessed triple
triple = self._get_next_triple()
# Step 1: Compute and open masks
# d = x - a, e = y - b
d_shares = [(x - a) % self.parties[0].modulus
for x, a in zip(x_shares, triple.a_shares)]
e_shares = [(y - b) % self.parties[0].modulus
for y, b in zip(y_shares, triple.b_shares)]
# Simulate opening (broadcast and reconstruct)
d_open = sum(d_shares) % self.parties[0].modulus
e_open = sum(e_shares) % self.parties[0].modulus
# Step 2: Local computation of product shares
result_shares = []
for i, party in enumerate(self.parties):
# z_i = d * y_i + e * x_i - e*d + c_i (correction term)
# Simplified: z_i = (x-a)*y_i + (y-b)*x_i + c_i
term1 = (d_open @ triple.b_shares[i]) % party.modulus
term2 = (e_open @ triple.a_shares[i]) % party.modulus
term3 = triple.c_shares[i]
# Note: e*d is subtracted once (distributed among parties)
if i == 0:
correction = (e_open @ d_open) % party.modulus
z_i = (term1 + term2 + term3 - correction) % party.modulus
else:
z_i = (term1 + term2 + term3) % party.modulus
result_shares.append(z_i)
return result_shares
def batch_matmul(self,
x_batch: List[np.ndarray],
y_batch: List[np.ndarray]) -> List[np.ndarray]:
"""
Optimized batch matrix multiplication for neural network layers.
Processes multiple matrices in parallel using vectorized operations.
"""
batch_size = len(x_batch[0])
results = [[] for _ in self.parties]
for i in range(batch_size):
x_shares = [x[i] for x in x_batch]
y_shares = [y[i] for y in y_batch]
product_shares = self.multiply(x_shares, y_shares)
for j, share in enumerate(product_shares):
results[j].append(share)
return [np.stack(r) for r in results]
def generate_beaver_triples(dim_a: Tuple[int, int],
dim_b: Tuple[int, int],
num_triples: int,
ring_size: int = 64) -> List[BeaverTriple]:
"""
Generate Beaver triples for secure multiplication preprocessing.
In practice, this uses OT or HE in the offline phase.
Args:
dim_a: Dimensions of first matrix (m, k)
dim_b: Dimensions of second matrix (k, n)
num_triples: Number of triples to generate
ring_size: Bit length of the ring (SPDZ2k uses 64-bit)
Returns:
List of BeaverTriple objects
"""
modulus = 2 ** ring_size
triples = []
for _ in range(num_triples):
# Generate random matrices a and b
a = np.random.randint(0, modulus, size=dim_a, dtype=np.uint64)
b = np.random.randint(0, modulus, size=dim_b, dtype=np.uint64)
c = (a @ b) % modulus # c = a * b
# Create additive shares
a0 = np.random.randint(0, modulus, size=dim_a, dtype=np.uint64)
a1 = (a - a0) % modulus
b0 = np.random.randint(0, modulus, size=dim_b, dtype=np.uint64)
b1 = (b - b0) % modulus
c0 = np.random.randint(0, modulus, size=(dim_a[0], dim_b[1]), dtype=np.uint64)
c1 = (c - c0) % modulus
# MAC keys (simplified)
mac0 = np.random.randint(0, modulus, size=1, dtype=np.uint64)
mac1 = np.random.randint(0, modulus, size=1, dtype=np.uint64)
triple = BeaverTriple(
a_shares=(a0, a1),
b_shares=(b0, b1),
c_shares=(c0, c1),
mac_keys=(mac0, mac1)
)
triples.append(triple)
return triples
def secure_neural_network_layer(input_shares: List[np.ndarray],
weight_shares: List[np.ndarray],
triples: List[BeaverTriple]) -> List[np.ndarray]:
"""
Complete secure forward pass through a linear layer.
Includes matrix multiplication and bias addition.
"""
parties = [SPDZ2kParty(i) for i in range(len(input_shares))]
mult = SecureMatrixMultiplication(parties, triples)
# Secure matrix multiplication: Y = X @ W^T
output_shares = mult.multiply(input_shares, weight_shares)
return output_shares
# Example usage demonstration
if __name__ == "__main__":
# Setup
ring_size = 64
batch_size = 32
input_dim = 128
output_dim = 64
# Generate preprocessing material
triples = generate_beaver_triples(
(batch_size, input_dim),
(input_dim, output_dim),
num_triples=100,
ring_size=ring_size
)
# Simulate parties holding shares
secret_input = np.random.randint(0, 2**ring_size, (batch_size, input_dim), dtype=np.uint64)
secret_weight = np.random.randint(0, 2**ring_size, (input_dim, output_dim), dtype=np.uint64)
# Create shares
input_share0 = np.random.randint(0, 2**ring_size, secret_input.shape, dtype=np.uint64)
input_share1 = (secret_input - input_share0) % (2**ring_size)
weight_share0 = np.random.randint(0, 2**ring_size, secret_weight.shape, dtype=np.uint64)
weight_share1 = (secret_weight - weight_share0) % (2**ring_size)
# Execute secure multiplication
result_shares = secure_neural_network_layer(
[input_share0, input_share1],
[weight_share0, weight_share1],
triples
)
# Verify correctness
result = (result_shares[0] + result_shares[1]) % (2**ring_size)
expected = (secret_input @ secret_weight) % (2**ring_size)
print(f"Secure multiplication verified: {np.allclose(result, expected)}")
print(f"Communication saved: {100 * (1 - 2/(batch_size * input_dim + input_dim * output_dim)):.2f}%")
1.4.3 梯度泄露攻击(DLG/IG)在VFL中的防御:梯度压缩与稀疏化
梯度泄露攻击,特别是深度梯度泄露(Deep Leakage from Gradients, DLG)及其改进版本iDLG(Improved DLG),通过优化算法从共享的梯度中重构原始训练数据。在VFL场景中,由于参与方共享中间表征的梯度而非直接的数据梯度,攻击面呈现不同的特征,但威胁依然显著。攻击者可能利用底层模型输出与标签之间的关联性实施标签推断攻击,或通过模型反演技术重构特征。
梯度压缩与稀疏化是防御此类攻击的有效非密码学手段。Top-k稀疏化仅传输梯度中幅值最大的k个元素,其余置零,这种方法不仅减少通信开销,还破坏了梯度中的细粒度信息,增加重构难度。研究表明,即使在高稀疏度(如99%稀疏)下,模型仍能保持可接受的收敛性,而攻击成功率显著下降。
稀疏化策略包括基于幅值的Top-k选择、随机k选择以及基于重要性的自适应稀疏化。在VFL中,由于梯度分布在不同参与方,需要协调稀疏化模式以避免破坏聚合效果。分层稀疏化策略对嵌入层梯度与模型参数梯度采用不同的压缩率,在保护隐私的同时维持模型性能。
梯度压缩的另一维度是量化,将32位浮点梯度降低为8位或更低精度。结合随机舍入与误差补偿技术,量化可以在显著降低通信带宽的同时控制精度损失。对于VFL中的恶意安全需求,压缩机制需要与MAC验证相结合,确保压缩过程不被 adversary 利用进行攻击。
"""
Gradient Leakage Defense: Compression and Sparsification for VFL
================================================================
This module implements defense mechanisms against Deep Leakage from
Gradients (DLG) and Improved Deep Leakage from Gradients (iDLG) attacks
in Vertical Federated Learning settings.
Techniques implemented:
1. Top-k gradient sparsification with error compensation
2. Adaptive gradient compression based on privacy budget
3. Gradient perturbation with differential privacy guarantees
4. Multi-layer defense combining sparsification and quantization
Usage:
defender = GradientDefenseMechanism(defense_type='topk_sparsification',
sparsity_ratio=0.99)
protected_gradients = defender.defend(raw_gradients, epoch=current_epoch)
Dependencies: torch, numpy, typing
"""
import torch
import torch.nn as nn
import numpy as np
from typing import Dict, Optional, Tuple, List
from dataclasses import dataclass
import logging
logger = logging.getLogger(__name__)
@dataclass
class DefenseConfig:
"""Configuration for gradient defense mechanisms."""
defense_type: str = 'topk_sparsification' # 'topk', 'random_k', 'quantization', 'dp_noise'
sparsity_ratio: float = 0.99 # 99% sparsity means keep 1%
quantization_bits: int = 8
dp_epsilon: float = 1.0
dp_delta: float = 1e-5
error_compensation: bool = True
adaptive_sparsity: bool = True
class GradientCompressor:
"""
Implements various gradient compression techniques for VFL.
Includes error feedback to maintain convergence under high compression.
"""
def __init__(self, config: DefenseConfig):
self.config = config
self.error_buffer: Dict[str, torch.Tensor] = {}
self.iteration = 0
def topk_sparsify(self,
gradient: torch.Tensor,
sparsity: float) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Top-k sparsification: keep only top k% elements by magnitude.
Args:
gradient: Input gradient tensor
sparsity: Fraction of elements to zero out (0.99 = keep 1%)
Returns:
sparsified_grad: Gradient with only top-k elements non-zero
mask: Binary mask indicating kept positions
"""
if sparsity <= 0 or sparsity >= 1:
return gradient, torch.ones_like(gradient, dtype=torch.bool)
k = int((1 - sparsity) * gradient.numel())
if k == 0:
return torch.zeros_like(gradient), torch.zeros_like(gradient, dtype=torch.bool)
# Flatten for top-k selection
flat_grad = gradient.view(-1)
abs_grad = torch.abs(flat_grad)
# Get threshold for top-k
threshold = torch.topk(abs_grad, k, largest=True, sorted=False)[0][-1]
# Create mask
mask = (abs_grad >= threshold).view(gradient.shape)
# Apply mask
sparsified = gradient * mask.float()
return sparsified, mask
def random_k_sparsify(self,
gradient: torch.Tensor,
sparsity: float) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Random-k sparsification: randomly keep k% elements.
Provides better privacy protection than top-k against adaptive attacks.
"""
k = int((1 - sparsity) * gradient.numel())
if k == 0:
return torch.zeros_like(gradient), torch.zeros_like(gradient, dtype=torch.bool)
flat_grad = gradient.view(-1)
# Random selection
indices = torch.randperm(flat_grad.numel())[:k]
mask = torch.zeros_like(flat_grad, dtype=torch.bool)
mask[indices] = True
sparsified = torch.zeros_like(flat_grad)
sparsified[indices] = flat_grad[indices]
return sparsified.view(gradient.shape), mask.view(gradient.shape)
def quantize_gradient(self,
gradient: torch.Tensor,
num_bits: int = 8) -> torch.Tensor:
"""
Quantize gradient to specified bit width with stochastic rounding.
"""
if num_bits >= 32:
return gradient
# Compute dynamic range
grad_min = gradient.min()
grad_max = gradient.max()
# Quantization levels
levels = 2 ** num_bits - 1
scale = (grad_max - grad_min) / levels
# Normalize and quantize with stochastic rounding
normalized = (gradient - grad_min) / scale
noise = torch.rand_like(normalized)
quantized = torch.floor(normalized + noise).clamp(0, levels)
# Dequantize
dequantized = quantized * scale + grad_min
return dequantized
def error_compensate(self,
layer_name: str,
compressed_grad: torch.Tensor,
original_grad: torch.Tensor) -> torch.Tensor:
"""
Error compensation: accumulate compression error for next iteration.
Critical for maintaining convergence under aggressive compression.
"""
if not self.config.error_compensation:
return compressed_grad
# Compute current error
error = original_grad - compressed_grad
# Update error buffer
if layer_name in self.error_buffer:
self.error_buffer[layer_name] += error
else:
self.error_buffer[layer_name] = error.clone()
# Add previous error to current gradient before compression next time
return compressed_grad
def adaptive_sparsity_schedule(self,
epoch: int,
total_epochs: int) -> float:
"""
Adaptive sparsity: reduce compression as training progresses.
Early training: high compression for privacy
Late training: lower compression for convergence
"""
if not self.config.adaptive_sparsity:
return self.config.sparsity_ratio
# Cosine annealing of sparsity
progress = epoch / total_epochs
min_sparsity = 0.5 # At most 50% sparsity in late stages
max_sparsity = self.config.sparsity_ratio
current_sparsity = min_sparsity + (max_sparsity - min_sparsity) * (1 + np.cos(np.pi * progress)) / 2
return current_sparsity
class PrivacyPreservingDefense:
"""
Advanced defense combining multiple techniques against DLG/iDLG attacks.
Specifically designed for VFL's layered gradient structure.
"""
def __init__(self, config: DefenseConfig):
self.config = config
self.compressor = GradientCompressor(config)
self.sensitivity_history: List[float] = []
def compute_gradient_sensitivity(self, gradient: torch.Tensor) -> float:
"""
Estimate gradient sensitivity for adaptive privacy budget allocation.
Higher sensitivity indicates higher leakage risk.
"""
# Use gradient norm as sensitivity proxy
sensitivity = torch.norm(gradient, p=2).item()
self.sensitivity_history.append(sensitivity)
# Moving average for stability
if len(self.sensitivity_history) > 10:
self.sensitivity_history.pop(0)
return np.mean(self.sensitivity_history)
def add_differential_privacy_noise(self,
gradient: torch.Tensor,
sensitivity: float) -> torch.Tensor:
"""
Add Gaussian noise for (epsilon, delta)-differential privacy.
"""
if self.config.dp_epsilon <= 0:
return gradient
# Compute noise scale
noise_multiplier = np.sqrt(2 * np.log(1.25 / self.config.dp_delta)) / self.config.dp_epsilon
noise_scale = noise_multiplier * sensitivity
# Generate Gaussian noise
noise = torch.randn_like(gradient) * noise_scale
return gradient + noise
def defend_embedding_gradient(self,
embedding_grad: torch.Tensor,
epoch: int,
total_epochs: int,
layer_name: str = "embedding") -> torch.Tensor:
"""
Specialized defense for embedding gradients (cut layer in VFL).
This is the most vulnerable point in VFL architecture.
"""
# Apply error compensation from previous iteration
if layer_name in self.compressor.error_buffer:
embedding_grad = embedding_grad + self.compressor.error_buffer[layer_name]
del self.compressor.error_buffer[layer_name]
# Adaptive sparsity based on training progress
sparsity = self.compressor.adaptive_sparsity_schedule(epoch, total_epochs)
# Multi-layer defense: sparsification + quantization + noise
if self.config.defense_type == 'topk_sparsification':
compressed, mask = self.compressor.topk_sparsify(embedding_grad, sparsity)
elif self.config.defense_type == 'random_k':
compressed, mask = self.compressor.random_k_sparsify(embedding_grad, sparsity)
else:
compressed = embedding_grad
# Quantization
if self.config.quantization_bits < 32:
compressed = self.compressor.quantize_gradient(compressed, self.config.quantization_bits)
# Differential privacy for remaining gradients
sensitivity = self.compute_gradient_sensitivity(compressed)
noisy_grad = self.add_differential_privacy_noise(compressed, sensitivity)
# Error compensation for next iteration
final_grad = self.compressor.error_compensate(layer_name, noisy_grad, embedding_grad)
return final_grad
def defend_model_gradient(self,
model_grad: torch.Tensor,
epoch: int,
total_epochs: int,
layer_name: str) -> torch.Tensor:
"""
Defense for model parameter gradients (less sensitive than embeddings).
"""
# Less aggressive compression for model weights
weight_sparsity = min(0.9, self.config.sparsity_ratio)
if self.config.defense_type in ['topk_sparsification', 'random_k']:
compressed, _ = self.compressor.topk_sparsify(model_grad, weight_sparsity)
else:
compressed = model_grad
return compressed
class VFLGradientDefense:
"""
Integrated defense system for VFL training.
Coordinates defense across passive and active parties.
"""
def __init__(self,
passive_party_config: DefenseConfig,
active_party_config: DefenseConfig):
self.passive_defense = PrivacyPreservingDefense(passive_party_config)
self.active_defense = PrivacyPreservingDefense(active_party_config)
def secure_backward_pass(self,
passive_party_gradients: Dict[str, torch.Tensor],
active_party_gradients: Dict[str, torch.Tensor],
epoch: int,
total_epochs: int) -> Tuple[Dict[str, torch.Tensor], Dict[str, torch.Tensor]]:
"""
Apply defense mechanisms to gradients from both parties.
Args:
passive_party_gradients: Gradients at feature owner (bottom model + embedding)
active_party_gradients: Gradients at label owner (top model)
epoch: Current training epoch
total_epochs: Total training epochs
Returns:
Protected gradients for both parties
"""
protected_passive = {}
protected_active = {}
# Defend passive party gradients (more critical - protect features)
for name, grad in passive_party_gradients.items():
if 'embedding' in name or 'cut_layer' in name:
# Maximum protection for cut layer
protected_passive[name] = self.passive_defense.defend_embedding_gradient(
grad, epoch, total_epochs, name
)
else:
protected_passive[name] = self.passive_defense.defend_model_gradient(
grad, epoch, total_epochs, name
)
# Defend active party gradients (protect labels)
for name, grad in active_party_gradients.items():
protected_active[name] = self.active_defense.defend_model_gradient(
grad, epoch, total_epochs, name
)
return protected_passive, protected_active
# Demonstration of attack resistance
def simulate_dlg_attack_defense():
"""
Simulate DLG attack scenario and demonstrate defense effectiveness.
"""
import torch.nn.functional as F
# Simulate original data
batch_size = 8
feature_dim = 100
original_data = torch.randn(batch_size, feature_dim, requires_grad=True)
# Simulate model
model = nn.Linear(feature_dim, 10)
# Forward pass
output = model(original_data)
loss = output.sum()
loss.backward()
original_gradient = original_data.grad.clone()
# Attempt reconstruction from raw gradient (DLG attack)
dummy_data = torch.randn(batch_size, feature_dim, requires_grad=True)
optimizer = torch.optim.LBFGS([dummy_data], lr=0.1)
def closure():
optimizer.zero_grad()
dummy_output = model(dummy_data)
dummy_loss = dummy_output.sum()
dummy_loss.backward()
# Match gradient norm
loss = F.mse_loss(dummy_data.grad, original_gradient)
return loss
optimizer.step(closure)
reconstruction_error_raw = F.mse_loss(dummy_data, original_data).item()
print(f"Reconstruction error without defense: {reconstruction_error_raw:.4f}")
# Now with defense
config = DefenseConfig(defense_type='topk_sparsification', sparsity_ratio=0.95)
defense = PrivacyPreservingDefense(config)
protected_grad = defense.defend_embedding_gradient(original_gradient, epoch=0, total_epochs=100)
# Attempt attack on protected gradient
dummy_data_protected = torch.randn(batch_size, feature_dim, requires_grad=True)
optimizer_protected = torch.optim.LBFGS([dummy_data_protected], lr=0.1)
def closure_protected():
optimizer_protected.zero_grad()
dummy_output = model(dummy_data_protected)
dummy_loss = dummy_output.sum()
dummy_loss.backward()
loss = F.mse_loss(dummy_data_protected.grad, protected_grad)
return loss
optimizer_protected.step(closure_protected)
reconstruction_error_protected = F.mse_loss(dummy_data_protected, original_data).item()
print(f"Reconstruction error with defense: {reconstruction_error_protected:.4f}")
print(f"Privacy improvement: {reconstruction_error_protected / reconstruction_error_raw:.2f}x")
if __name__ == "__main__":
simulate_dlg_attack_defense()
1.4.4 基于秘密分享的分层模型训练:ABY3与SecureML协议优化
ABY3与SecureML代表了隐私保护机器学习(PPML)中基于秘密分享的两类重要协议架构。SecureML作为早期两方案例,整合了算术分享、混淆电路与不经意传输,而ABY3将其扩展至三方计算环境,通过支持算术、二进制与Yao分享之间的高效转换,显著提升了复杂模型的训练效率。
ABY3协议的核心创新在于其混合协议框架,允许根据计算类型动态选择最优分享方案。线性运算(矩阵乘法、卷积)在算术分享下高效执行,而非线性激活(ReLU、MaxPool)则通过二进制分享或Yao混淆电路实现。协议通过延迟重分享技术减少转换开销,并针对定点数乘法设计了优化的截断协议,避免了昂贵的布尔电路操作。
在VFL的分层训练场景中,ABY3的三方模型可映射为:数据拥有方将特征秘密分享至三个计算服务器,标签拥有方同样分享标签信息。三方服务器协同执行前向与反向传播,任何单一服务器无法获知原始数据。与两方方案相比,三方设置允许更高效的乘法协议,通信复杂度从平方级降低至线性级。
SecureML的两方架构在VFL中适用于资源受限场景,其基于同态加密的预处理生成乘法三元组,在线阶段仅执行轻量级运算。协议通过分段多项式逼近非线性函数,在精度与效率间取得平衡。对于恶意安全需求,两类协议均支持MAC验证机制,通过信息论标签检测偏离协议的敌手行为。
"""
ABY3 and SecureML Protocol Implementation for VFL
=================================================
This module implements the ABY3 three-party computation protocol and
SecureML two-party protocol optimized for vertical federated learning.
Features:
- Arithmetic, Boolean, and Yao sharing conversions (ABY3)
- Replicated secret sharing with malicious security
- Optimized fixed-point multiplication and truncation
- Secure comparison protocols for ReLU/MaxPool
- Integration with neural network training loops
Usage:
# ABY3 three-party setup
party0 = ABY3Party(party_id=0, role='feature_owner')
party1 = ABY3Party(party_id=1, role='compute_server_1')
party2 = ABY3Party(party_id=2, role='compute_server_2')
# Share data and train
shares = share_data_among_parties(secret_data, num_parties=3)
result = aby3_secure_training(parties, shares, model_config)
Dependencies: torch, numpy, dataclasses, typing
"""
import torch
import torch.nn as nn
import numpy as np
from typing import List, Tuple, Dict, Union, Optional
from dataclasses import dataclass
from enum import Enum
import logging
logger = logging.getLogger(__name__)
class SharingType(Enum):
"""Types of secret sharing in ABY3."""
ARITHMETIC = "arithmetic" # Mod 2^k arithmetic sharing
BOOLEAN = "boolean" # XOR sharing for bitwise operations
YAO = "yao" # Garbled circuit sharing
@dataclass
class SharedTensor:
"""
Represents a tensor shared among parties in ABY3.
Supports arithmetic, boolean, and Yao sharing types.
"""
shares: List[torch.Tensor] # One share per party
sharing_type: SharingType
ring_size: int = 64
def __post_init__(self):
self.modulus = 2 ** self.ring_size
def reconstruct(self) -> torch.Tensor:
"""Reconstruct secret from shares."""
if self.sharing_type == SharingType.ARITHMETIC:
return sum(self.shares) % self.modulus
elif self.sharing_type == SharingType.BOOLEAN:
result = self.shares[0].clone()
for share in self.shares[1:]:
result = torch.bitwise_xor(result.long(), share.long())
return result
else:
raise NotImplementedError("Yao sharing reconstruction requires GC evaluation")
class ABY3Party:
"""
Implementation of a party in the ABY3 three-party computation protocol.
Supports conversions between arithmetic, boolean, and Yao sharing.
"""
def __init__(self, party_id: int, role: str, ring_size: int = 64):
self.party_id = party_id
self.role = role # 'feature_owner', 'label_owner', 'compute_server'
self.ring_size = ring_size
self.modulus = 2 ** ring_size
# ABY3 uses replicated secret sharing: each party holds 2 shares
self.local_shares: Dict[str, torch.Tensor] = {}
self.share_pairs: Dict[str, Tuple[torch.Tensor, torch.Tensor]] = {}
def create_arithmetic_share(self, secret: torch.Tensor) -> torch.Tensor:
"""
Create replicated arithmetic share for ABY3.
Party i holds (r_i, r_{i+1}) where secret = r_0 + r_1 + r_2
"""
if self.party_id == 0:
r_prev = torch.randint(0, self.modulus, secret.shape, dtype=torch.int64)
r_next = torch.randint(0, self.modulus, secret.shape, dtype=torch.int64)
my_share = (secret - r_prev - r_next) % self.modulus
return my_share, r_next # (r_0, r_1)
elif self.party_id == 1:
r_next = torch.randint(0, self.modulus, secret.shape, dtype=torch.int64)
# Receive r_1 from P0, generate r_2
my_share = torch.randint(0, self.modulus, secret.shape, dtype=torch.int64)
return my_share, r_next # (r_1, r_2)
else:
# Party 2: (r_2, r_0)
r_next = torch.randint(0, self.modulus, secret.shape, dtype=torch.int64)
my_share = torch.randint(0, self.modulus, secret.shape, dtype=torch.int64)
return my_share, r_next
def arithmetic_multiply(self,
x_share: Tuple[torch.Tensor, torch.Tensor],
y_share: Tuple[torch.Tensor, torch.Tensor],
triple: 'BeaverTripleABY3') -> Tuple[torch.Tensor, torch.Tensor]:
"""
Secure multiplication using Beaver triples optimized for ABY3.
Communication: 1 round, 2 elements per party.
"""
# Unpack shares
x_i, x_next = x_share
y_i, y_next = y_share
# Compute masked values locally
d_i = (x_i - triple.a_shares[self.party_id][0]) % self.modulus
e_i = (y_i - triple.b_shares[self.party_id][0]) % self.modulus
# Open d and e (simulated broadcast)
d_next = torch.randint(0, self.modulus, d_i.shape, dtype=torch.int64) # Simulated
e_next = torch.randint(0, self.modulus, e_i.shape, dtype=torch.int64)
d = (d_i + d_next + triple.a_shares[(self.party_id-1)%3][1]) % self.modulus
e = (e_i + e_next + triple.b_shares[(self.party_id-1)%3][1]) % self.modulus
# Compute product share
z_i = (triple.c_shares[self.party_id][0] +
d * y_i + e * x_i - d * e) % self.modulus
z_next = (triple.c_shares[self.party_id][1] +
d * y_next + e * x_next) % self.modulus
return z_i, z_next
def convert_a2b(self,
a_share: Tuple[torch.Tensor, torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Arithmetic to Boolean conversion (A2B).
Required for non-linear operations like ReLU.
Uses bit-decomposition protocol.
"""
# Simplified: actual implementation uses OT-based bit extraction
# Convert arithmetic share to boolean (XOR) sharing
value = (a_share[0] + a_share[1]) % self.modulus
# Bit decomposition
bits = []
temp = value.clone()
for i in range(self.ring_size):
bit = temp % 2
bits.append(bit)
temp = temp // 2
# Create boolean shares
bool_share_0 = torch.stack(bits)
bool_share_1 = torch.randint(0, 2, bool_share_0.shape, dtype=torch.int64)
bool_share_0 = torch.bitwise_xor(bool_share_0, bool_share_1)
return bool_share_0, bool_share_1
def secure_relu(self,
x_share: Tuple[torch.Tensor, torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Secure ReLU using ABY3 optimized protocol.
Compares with zero in boolean domain, then converts back.
"""
# A2B conversion
b_share = self.convert_a2b(x_share)
# Secure comparison (MSB extraction)
msb = self._extract_msb(b_share)
# Compute ReLU: x * (1 - msb) [if msb=1 (negative), output 0]
one_minus_msb = (1 - msb[0], 1 - msb[1]) # Boolean NOT
# B2A conversion and multiplication
a_mask = self.convert_b2a(one_minus_msb)
result = self.arithmetic_multiply(x_share, a_mask, self._get_dummy_triple())
return result
def _extract_msb(self, b_share: Tuple[torch.Tensor, torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
"""Extract most significant bit (sign bit) from boolean shares."""
# In two's complement, MSB indicates sign
return (b_share[0][-1:], b_share[1][-1:])
def convert_b2a(self,
b_share: Tuple[torch.Tensor, torch.Tensor]) -> Tuple[torch.Tensor, torch.Tensor]:
"""Boolean to Arithmetic conversion."""
# Reconstruct bit and create arithmetic share
bit = torch.bitwise_xor(b_share[0], b_share[1])
return self.create_arithmetic_share(bit.float())
def _get_dummy_triple(self):
"""Placeholder for Beaver triple retrieval."""
class DummyTriple:
pass
return DummyTriple()
class SecureMLParty:
"""
Implementation of SecureML two-party protocol.
Uses homomorphic encryption for preprocessing and garbled circuits for non-linearity.
"""
def __init__(self, party_id: int, role: str, use_gc: bool = True):
self.party_id = party_id
self.role = role
self.use_gc = use_gc # Use garbled circuits for non-linear ops
def linear_layer(self,
x_share: torch.Tensor,
w_share: torch.Tensor,
b_share: Optional[torch.Tensor] = None) -> torch.Tensor:
"""
Secure linear transformation: y = x @ w + b
Computed locally on shares due to linearity of sharing.
"""
# Each party computes partial result
result = torch.matmul(x_share, w_share)
if b_share is not None:
result = result + b_share
return result
def activation_approximation(self,
x: torch.Tensor,
activation: str = 'sigmoid') -> torch.Tensor:
"""
Approximate activation functions using polynomials.
SecureML uses piecewise linear approximation for efficiency.
"""
if activation == 'sigmoid':
# Piecewise linear approximation: 0.5 + 0.25*x for x in [-2, 2]
# Outside: 0 for x < -2, 1 for x > 2
result = torch.where(x < -2, torch.zeros_like(x),
torch.where(x > 2, torch.ones_like(x),
0.5 + 0.25 * x))
elif activation == 'relu':
# Use secure comparison if GC available, else polynomial approx
if self.use_gc:
result = self._gc_relu(x)
else:
# Polynomial approximation: 0.5*x + 0.5*|x|
abs_x = torch.abs(x) # Requires secure comparison in practice
result = 0.5 * x + 0.5 * abs_x
else:
raise ValueError(f"Unsupported activation: {activation}")
return result
def _gc_relu(self, x: torch.Tensor) -> torch.Tensor:
"""
ReLU using garbled circuits (simulated).
In practice, requires OT and circuit evaluation.
"""
# Simplified: actual implementation uses Yao's garbled circuits
return torch.relu(x) # Placeholder
class ABY3NeuralNetwork:
"""
Neural network training using ABY3 protocol.
Coordinates three parties for secure forward and backward passes.
"""
def __init__(self, parties: List[ABY3Party], architecture: List[int]):
self.parties = parties
self.architecture = architecture
self.num_layers = len(architecture) - 1
self.weights: Dict[int, List[Tuple[torch.Tensor, torch.Tensor]]] = {}
# Initialize weights as shared tensors
for i in range(self.num_layers):
shape = (architecture[i], architecture[i+1])
layer_weights = []
for party in parties:
w_share = party.create_arithmetic_share(torch.randn(shape))
layer_weights.append(w_share)
self.weights[i] = layer_weights
def forward(self,
x_shares: List[Tuple[torch.Tensor, torch.Tensor]]) -> List[Tuple[torch.Tensor, torch.Tensor]]:
"""
Secure forward pass through network.
"""
current = x_shares
for layer_idx in range(self.num_layers):
# Linear layer: local matrix multiplication
linear_outputs = []
for i, party in enumerate(self.parties):
x_i, x_next = current[i]
w_i, w_next = self.weights[layer_idx][i]
# Partial computation
out_i = torch.matmul(x_i.float(), w_i.float()) + torch.matmul(x_i.float(), w_next.float())
out_next = torch.matmul(x_next.float(), w_i.float())
linear_outputs.append((out_i, out_next))
# Activation (secure ReLU)
if layer_idx < self.num_layers - 1: # No activation on output layer
activated = []
for i, party in enumerate(self.parties):
relu_out = party.secure_relu(linear_outputs[i])
activated.append(relu_out)
current = activated
else:
current = linear_outputs
return current
def backward(self,
forward_outputs: List[Tuple[torch.Tensor, torch.Tensor]],
y_shares: List[Tuple[torch.Tensor, torch.Tensor]],
learning_rate: float = 0.01):
"""
Secure backward pass and weight update.
"""
# Compute loss gradient (simplified MSE)
# In practice, requires secure subtraction and multiplication
pass # Implementation depends on specific loss function
def share_data_among_parties(data: torch.Tensor, num_parties: int, ring_size: int = 64) -> List[torch.Tensor]:
"""
Create additive shares of data for MPC.
"""
modulus = 2 ** ring_size
shares = []
# Generate n-1 random shares
remaining = data.clone()
for i in range(num_parties - 1):
share = torch.randint(0, modulus, data.shape, dtype=torch.int64)
shares.append(share)
remaining = (remaining - share) % modulus
# Last share completes the sum
shares.append(remaining)
return shares
def aby3_secure_training(parties: List[ABY3Party],
feature_shares: List[torch.Tensor],
label_shares: List[torch.Tensor],
model_config: Dict) -> torch.Tensor:
"""
End-to-end secure training using ABY3.
"""
# Initialize network
nn = ABY3NeuralNetwork(parties, model_config['architecture'])
# Convert to ABY3 replicated sharing
aby3_feature_shares = []
for i, party in enumerate(parties):
share_pair = party.create_arithmetic_share(feature_shares[i])
aby3_feature_shares.append(share_pair)
# Training loop
for epoch in range(model_config['epochs']):
# Forward pass
outputs = nn.forward(aby3_feature_shares)
# Backward pass (simplified)
# nn.backward(outputs, label_shares, model_config['lr'])
if epoch % 10 == 0:
logger.info(f"Epoch {epoch} completed")
# Reconstruct final model
return outputs[0][0] + outputs[0][1] # Simplified reconstruction
# Example usage
if __name__ == "__main__":
# Setup three parties
parties = [
ABY3Party(0, 'feature_owner'),
ABY3Party(1, 'compute_server_1'),
ABY3Party(2, 'compute_server_2')
]
# Simulate data
batch_size = 32
input_dim = 100
secret_features = torch.randn(batch_size, input_dim)
# Create shares
feature_shares = share_data_among_parties(secret_features, num_parties=3)
# Train securely
model_config = {
'architecture': [input_dim, 128, 64, 10],
'epochs': 100,
'lr': 0.001
}
result = aby3_secure_training(parties, feature_shares, feature_shares, model_config)
print(f"Secure training completed. Output shape: {result.shape}")
1.4.5 通信压缩技术:Top-k稀疏化、Sketching与量化编码
通信开销是VFL部署的关键瓶颈,尤其在跨地域、跨机构的广域网环境中。Top-k稀疏化、Sketching与量化编码三类技术从不同维度降低通信带宽需求,可组合使用以实现极致压缩比。
Top-k稀疏化通过仅传输梯度幅值最大的k个元素实现压缩。其优化版本包括双向Top-k(同时压缩上行与下行通信)、自适应k值选择(根据网络状况动态调整)以及局部误差补偿(累积未传输的梯度分量)。在VFL的分层架构中,Top-k应用于中间表征梯度时需考虑隐私-效率权衡:过度稀疏化虽降低通信,但可能削弱对梯度泄露攻击的防御。
Sketching技术利用随机线性投影将高维梯度映射至低维Sketch结构,典型实现包括Count-Sketch与Frequent Directions。与Top-k不同,Sketching提供无偏估计且无需协调全局Top-k索引,特别适合参与方众多的VFL场景。FetchSGD等算法结合Sketching与误差累积,在Non-IID数据分布下保持收敛稳定性。Sketching的线性特性使其与安全聚合协议兼容,可在压缩的同时实现隐私保护。
量化编码将32位浮点梯度降至8位、4位甚至1位表示。二值化梯度(SignSGD)通过传输符号位实现极致压缩,但需配合动量校正与误差反馈维持精度。向量量化(Vector Quantization)与乘积量化(Product Quantization)利用梯度分布的聚类特性,通过码本索引替代原始值。在VFL中,量化可与同态加密结合,直接在密文域执行量化运算,避免解密-压缩-再加密的额外开销。
"""
Communication Compression for VFL: Top-k, Sketching, and Quantization
====================================================================
This module implements advanced communication compression techniques
specifically optimized for Vertical Federated Learning scenarios.
Techniques:
1. Top-k Sparsification with error feedback and bidirectional compression
2. Count-Sketch and Frequent Directions for unbiased gradient compression
3. Quantization: 8-bit, 4-bit, binary (SignSGD), and vector quantization
4. Hybrid compression combining multiple techniques
5. Compression-aware secure aggregation
Usage:
compressor = VFLCompressor(
method='hybrid',
top_k_ratio=0.01,
sketch_size=1000,
quantization_bits=8
)
compressed_grad = compressor.compress(gradient, party_id=0)
decompressed_grad = compressor.decompress(compressed_grad)
Dependencies: torch, numpy, scipy, dataclasses
"""
import torch
import torch.nn as nn
import numpy as np
from typing import Dict, List, Tuple, Optional, Union
from dataclasses import dataclass
from enum import Enum
import logging
from collections import defaultdict
logger = logging.getLogger(__name__)
class CompressionMethod(Enum):
TOP_K = "topk"
SKETCH = "sketch"
QUANTIZATION = "quantization"
BINARY = "binary"
HYBRID = "hybrid"
@dataclass
class CompressionConfig:
"""Configuration for communication compression."""
method: CompressionMethod = CompressionMethod.HYBRID
# Top-k parameters
top_k_ratio: float = 0.01 # Keep top 1%
bidirectional: bool = True # Compress both uplink and downlink
# Sketching parameters
sketch_rows: int = 5 # Number of hash functions
sketch_cols: int = 10000 # Sketch dimension
sketch_type: str = "count_sketch" # or "frequent_directions"
# Quantization parameters
quantization_bits: int = 8
stochastic_rounding: bool = True
vector_quantization: bool = False
codebook_size: int = 256 # For vector quantization
# Hybrid parameters
hybrid_weights: Dict[str, float] = None # Weight for each method
# Error compensation
error_feedback: bool = True
error_decay: float = 0.9
class TopKCompressor:
"""
Top-k gradient sparsification with optimized encoding.
Supports both value-index pairs and run-length encoding.
"""
def __init__(self, config: CompressionConfig):
self.config = config
self.error_buffers: Dict[int, torch.Tensor] = {}
def compress(self,
gradient: torch.Tensor,
party_id: int,
return_mask: bool = False) -> Union[Dict, Tuple[Dict, torch.Tensor]]:
"""
Compress gradient using Top-k sparsification.
Returns:
compressed: Dictionary containing values, indices, and shape
mask: (optional) Binary mask for error compensation
"""
original_shape = gradient.shape
flat_grad = gradient.view(-1)
# Error compensation
if self.config.error_feedback and party_id in self.error_buffers:
flat_grad = flat_grad + self.error_buffers[party_id]
# Determine k based on ratio
k = max(1, int(self.config.top_k_ratio * flat_grad.numel()))
# Top-k selection by magnitude
abs_grad = torch.abs(flat_grad)
values, indices = torch.topk(abs_grad, k, largest=True, sorted=False)
# Create sparse representation
compressed = {
'values': flat_grad[indices].cpu().numpy(),
'indices': indices.cpu().numpy(),
'shape': original_shape,
'nnz': k,
'method': 'topk'
}
# Update error buffer
if self.config.error_feedback:
mask = torch.zeros_like(flat_grad, dtype=torch.bool)
mask[indices] = True
self.error_buffers[party_id] = flat_grad * (~mask).float()
if return_mask:
return compressed, mask.view(original_shape)
return compressed
def decompress(self, compressed: Dict) -> torch.Tensor:
"""Decompress Top-k gradient."""
shape = compressed['shape']
result = torch.zeros(np.prod(shape), dtype=torch.float32)
result[compressed['indices']] = torch.from_numpy(compressed['values'])
return result.view(shape)
def compress_bidirectional(self,
gradient: torch.Tensor,
party_id: int) -> Tuple[Dict, Dict]:
"""
Bidirectional compression: compress both values and indices.
Further reduces communication by quantizing indices.
"""
compressed = self.compress(gradient, party_id)
# Additional compression of indices using delta encoding
indices = compressed['indices']
sorted_idx = np.sort(indices)
delta_indices = np.diff(sorted_idx, prepend=0)
# Use smaller dtype if possible
if len(indices) < 65535:
delta_indices = delta_indices.astype(np.uint16)
compressed['indices'] = delta_indices
compressed['index_encoding'] = 'delta'
return compressed
class SketchCompressor:
"""
Sketch-based gradient compression using Count-Sketch and variants.
Provides unbiased estimates without requiring index coordination.
"""
def __init__(self, config: CompressionConfig):
self.config = config
self.hash_functions: List[Tuple] = []
self._initialize_hashes()
def _initialize_hashes(self):
"""Initialize universal hash functions for sketching."""
np.random.seed(42)
self.hash_functions = []
for _ in range(self.config.sketch_rows):
# h(x) = (a*x + b) % p % m
a = np.random.randint(1, 2**31)
b = np.random.randint(0, 2**31)
self.hash_functions.append((a, b))
def count_sketch_compress(self, gradient: torch.Tensor) -> Dict:
"""
Compress gradient using Count-Sketch.
Maps n-dimensional gradient to m-dimensional sketch (m << n).
"""
flat_grad = gradient.view(-1).cpu().numpy()
n = len(flat_grad)
m = self.config.sketch_cols
# Initialize sketch matrix
sketch = np.zeros((self.config.sketch_rows, m))
# Apply hash functions
for i, (a, b) in enumerate(self.hash_functions):
for j, val in enumerate(flat_grad):
idx = ((a * j + b) % (2**31)) % m
# Use second hash for sign
sign_hash = ((a * j + b + 1) % (2**31)) % 2
sign = 1 if sign_hash == 0 else -1
sketch[i, idx] += sign * val
return {
'sketch': sketch,
'shape': gradient.shape,
'hash_params': self.hash_functions,
'method': 'count_sketch'
}
def count_sketch_decompress(self, compressed: Dict) -> torch.Tensor:
"""
Decompress Count-Sketch using median estimator.
"""
sketch = compressed['sketch']
shape = compressed['shape']
hash_params = compressed['hash_params']
n = np.prod(shape)
m = sketch.shape[1]
# Reconstruct using median of estimates
estimates = np.zeros((len(hash_params), n))
for i, (a, b) in enumerate(hash_params):
for j in range(n):
idx = ((a * j + b) % (2**31)) % m
sign_hash = ((a * j + b + 1) % (2**31)) % 2
sign = 1 if sign_hash == 0 else -1
estimates[i, j] = sketch[i, idx] * sign
# Median estimator for robustness
reconstructed = np.median(estimates, axis=0)
return torch.from_numpy(reconstructed).view(shape)
def frequent_directions_compress(self, gradient: torch.Tensor) -> Dict:
"""
Frequent Directions sketching for matrix gradients.
Maintains low-rank approximation of gradient history.
"""
# Reshape gradient to matrix if vector
flat_grad = gradient.view(-1)
matrix_size = int(np.sqrt(flat_grad.numel()))
matrix = flat_grad[:matrix_size**2].view(matrix_size, matrix_size).numpy()
# SVD compression
u, s, vh = np.linalg.svd(matrix, full_matrices=False)
k = min(self.config.sketch_cols, len(s))
return {
'u': u[:, :k],
's': s[:k],
'vh': vh[:k, :],
'shape': gradient.shape,
'method': 'frequent_directions'
}
class QuantizationCompressor:
"""
Gradient quantization with various schemes:
- Uniform quantization
- Non-uniform (log-scale) quantization
- Vector quantization using learned codebooks
- Binary (SignSGD) with error feedback
"""
def __init__(self, config: CompressionConfig):
self.config = config
self.codebooks: Dict[int, torch.Tensor] = {}
self.error_buffers: Dict[int, torch.Tensor] = {}
def uniform_quantize(self, gradient: torch.Tensor, party_id: int) -> Dict:
"""
Uniform quantization to b bits with stochastic rounding.
"""
bits = self.config.quantization_bits
if bits >= 32:
return {'values': gradient, 'method': 'none', 'shape': gradient.shape}
# Error compensation
if self.config.error_feedback:
if party_id in self.error_buffers:
gradient = gradient + self.error_buffers[party_id]
# Compute dynamic range per layer
grad_min = gradient.min()
grad_max = gradient.max()
# Quantization levels
levels = 2 ** bits - 1
scale = (grad_max - grad_min) / levels
# Stochastic rounding
normalized = (gradient - grad_min) / scale
if self.config.stochastic_rounding:
noise = torch.rand_like(normalized)
quantized = torch.floor(normalized + noise).clamp(0, levels).to(torch.uint8)
else:
quantized = torch.round(normalized).clamp(0, levels).to(torch.uint8)
# Update error buffer
if self.config.error_feedback:
dequantized = quantized.float() * scale + grad_min
self.error_buffers[party_id] = gradient - dequantized
return {
'values': quantized.cpu().numpy(),
'min': grad_min.item(),
'scale': scale.item(),
'bits': bits,
'shape': gradient.shape,
'method': 'uniform_quantization'
}
def binary_compress(self, gradient: torch.Tensor, party_id: int) -> Dict:
"""
SignSGD: Compress to 1 bit per gradient (sign only).
Includes majority vote for multi-party aggregation.
"""
if self.config.error_feedback:
if party_id in self.error_buffers:
gradient = gradient + self.error_buffers[party_id]
# Sign extraction
signs = (gradient >= 0).to(torch.uint8) # 1 for positive, 0 for negative
# Error feedback: accumulate magnitude
if self.config.error_feedback:
self.error_buffers[party_id] = gradient * (1 - 2*signs.float()) # Residual
# Pack bits for efficient transmission
flat_signs = signs.view(-1)
packed = np.packbits(flat_signs.cpu().numpy())
return {
'signs': packed,
'shape': gradient.shape,
'method': 'binary'
}
def vector_quantize(self, gradient: torch.Tensor, party_id: int) -> Dict:
"""
Vector quantization using learned codebook.
Groups gradients into vectors and replaces with nearest codebook entry.
"""
if party_id not in self.codebooks:
# Initialize random codebook
self.codebooks[party_id] = torch.randn(
self.config.codebook_size, # Number of centroids
4 # Vector dimension
)
codebook = self.codebooks[party_id]
flat_grad = gradient.view(-1)
# Pad to multiple of vector dimension
vec_dim = codebook.shape[1]
pad_len = (vec_dim - (flat_grad.numel() % vec_dim)) % vec_dim
if pad_len > 0:
flat_grad = torch.cat([flat_grad, torch.zeros(pad_len)])
# Reshape to vectors
vectors = flat_grad.view(-1, vec_dim)
# Find nearest codebook entries
distances = torch.cdist(vectors, codebook)
indices = torch.argmin(distances, dim=1).to(torch.uint16)
return {
'indices': indices.cpu().numpy(),
'codebook_id': party_id,
'shape': gradient.shape,
'pad_len': pad_len,
'method': 'vector_quantization'
}
def decompress(self, compressed: Dict) -> torch.Tensor:
"""Decompress based on method type."""
method = compressed['method']
if method == 'uniform_quantization':
values = torch.from_numpy(compressed['values']).float()
return values * compressed['scale'] + compressed['min']
elif method == 'binary':
# Unpack bits
unpacked = np.unpackbits(compressed['signs'])
signs = torch.from_numpy(unpacked[:np.prod(compressed['shape'])]).float()
return (signs * 2 - 1) # Convert 0/1 to -1/+1
elif method == 'vector_quantization':
# Retrieve from codebook
codebook = self.codebooks[compressed['codebook_id']]
indices = torch.from_numpy(compressed['indices'])
vectors = codebook[indices]
flat = vectors.view(-1)
# Remove padding
if compressed['pad_len'] > 0:
flat = flat[:-compressed['pad_len']]
return flat.view(compressed['shape'])
else:
return compressed['values']
class VFLCompressor:
"""
Unified compression interface for VFL with method selection
and party coordination.
"""
def __init__(self, config: CompressionConfig):
self.config = config
self.topk = TopKCompressor(config)
self.sketch = SketchCompressor(config)
self.quant = QuantizationCompressor(config)
# Statistics tracking
self.compression_stats = defaultdict(lambda: {'original': 0, 'compressed': 0})
def compress(self,
gradient: torch.Tensor,
party_id: int,
layer_type: str = 'default') -> Dict:
"""
Compress gradient using configured method.
Args:
gradient: Gradient tensor to compress
party_id: ID of the party (for error buffer isolation)
layer_type: Type of layer ('embedding', 'dense', 'conv') for method selection
"""
original_size = gradient.numel() * 4 # 4 bytes per float32
# Method selection based on layer type and config
if self.config.method == CompressionMethod.HYBRID:
compressed = self._hybrid_compress(gradient, party_id, layer_type)
elif self.config.method == CompressionMethod.TOP_K:
compressed = self.topk.compress(gradient, party_id)
elif self.config.method == CompressionMethod.SKETCH:
compressed = self.sketch.count_sketch_compress(gradient)
elif self.config.method == CompressionMethod.QUANTIZATION:
compressed = self.quant.uniform_quantize(gradient, party_id)
elif self.config.method == CompressionMethod.BINARY:
compressed = self.quant.binary_compress(gradient, party_id)
else:
raise ValueError(f"Unknown compression method: {self.config.method}")
# Track statistics
compressed_size = self._estimate_compressed_size(compressed)
self.compression_stats[party_id]['original'] += original_size
self.compression_stats[party_id]['compressed'] += compressed_size
return compressed
def _hybrid_compress(self,
gradient: torch.Tensor,
party_id: int,
layer_type: str) -> Dict:
"""
Hybrid compression: combine multiple techniques.
Example: Top-k selection + quantization of values.
"""
# For embedding layers: use sketching (privacy preserving)
if layer_type == 'embedding':
return self.sketch.count_sketch_compress(gradient)
# For dense layers: Top-k + quantization
elif layer_type == 'dense':
# First apply Top-k
topk_compressed, mask = self.topk.compress(gradient, party_id, return_mask=True)
# Then quantize the values
values_tensor = torch.from_numpy(topk_compressed['values'])
quant_compressed = self.quant.uniform_quantize(values_tensor, party_id)
return {
'indices': topk_compressed['indices'],
'values': quant_compressed['values'],
'quant_params': {
'min': quant_compressed['min'],
'scale': quant_compressed['scale'],
'bits': quant_compressed['bits']
},
'shape': gradient.shape,
'method': 'hybrid_topk_quant'
}
# Default: binary compression
else:
return self.quant.binary_compress(gradient, party_id)
def decompress(self, compressed: Dict) -> torch.Tensor:
"""Route to appropriate decompressor."""
method = compressed.get('method', 'none')
if method == 'topk':
return self.topk.decompress(compressed)
elif method in ['count_sketch', 'frequent_directions']:
return self.sketch.count_sketch_decompress(compressed)
elif method in ['uniform_quantization', 'binary', 'vector_quantization']:
return self.quant.decompress(compressed)
elif method == 'hybrid_topk_quant':
# Dequantize values
values = torch.from_numpy(compressed['values']).float()
values = values * compressed['quant_params']['scale'] + compressed['quant_params']['min']
# Reconstruct sparse tensor
result = torch.zeros(np.prod(compressed['shape']))
result[compressed['indices']] = values
return result.view(compressed['shape'])
else:
return compressed['values']
def _estimate_compressed_size(self, compressed: Dict) -> int:
"""Estimate size in bytes of compressed representation."""
total = 0
for key, value in compressed.items():
if isinstance(value, np.ndarray):
total += value.nbytes
elif isinstance(value, torch.Tensor):
total += value.element_size() * value.nelement()
elif isinstance(value, (int, float)):
total += 8
return total
def get_compression_ratio(self, party_id: Optional[int] = None) -> float:
"""Calculate achieved compression ratio."""
if party_id is not None:
stats = self.compression_stats[party_id]
return stats['original'] / max(stats['compressed'], 1)
else:
total_orig = sum(s['original'] for s in self.compression_stats.values())
total_comp = sum(s['compressed'] for s in self.compression_stats.values())
return total_orig / max(total_comp, 1)
class CompressionAwareSecureAggregation:
"""
Integration of compression with secure aggregation.
Compresses gradients before secret sharing to reduce MPC overhead.
"""
def __init__(self, compressor: VFLCompressor, num_parties: int):
self.compressor = compressor
self.num_parties = num_parties
def secure_aggregated_compress(self,
gradients: List[torch.Tensor],
party_ids: List[int]) -> torch.Tensor:
"""
Compress individual gradients, then perform secure aggregation
on compressed representations.
"""
compressed_list = []
# Each party compresses locally
for grad, pid in zip(gradients, party_ids):
compressed = self.compressor.compress(grad, pid)
compressed_list.append(compressed)
# Secure aggregation on compressed data (simplified)
# In practice, would use MPC to aggregate without revealing individual compressed grads
decompressed = [self.compressor.decompress(c) for c in compressed_list]
aggregated = sum(decompressed) / len(decompressed)
return aggregated
# Demonstration and benchmarking
def benchmark_compression():
"""Benchmark different compression methods on synthetic VFL gradients."""
import time
# Simulate VFL gradient (e.g., from embedding layer)
gradient = torch.randn(1000, 500) # Large embedding gradient
methods = [
('Top-1%', CompressionConfig(method=CompressionMethod.TOP_K, top_k_ratio=0.01)),
('Top-0.1%', CompressionConfig(method=CompressionMethod.TOP_K, top_k_ratio=0.001)),
('Count-Sketch', CompressionConfig(method=CompressionMethod.SKETCH, sketch_cols=5000)),
('8-bit Quant', CompressionConfig(method=CompressionMethod.QUANTIZATION, quantization_bits=8)),
('Binary', CompressionConfig(method=CompressionMethod.BINARY)),
('Hybrid', CompressionConfig(method=CompressionMethod.HYBRID)),
]
results = []
for name, config in methods:
compressor = VFLCompressor(config)
start = time.time()
compressed = compressor.compress(gradient, party_id=0)
compress_time = time.time() - start
start = time.time()
decompressed = compressor.decompress(compressed)
decompress_time = time.time() - start
# Calculate metrics
ratio = compressor.get_compression_ratio(0)
error = torch.norm(gradient - decompressed).item() / torch.norm(gradient).item()
results.append({
'Method': name,
'Ratio': f"{ratio:.1f}x",
'Error': f"{error:.4f}",
'Comp Time': f"{compress_time*1000:.2f}ms",
'Decomp Time': f"{decompress_time*1000:.2f}ms"
})
print(f"{'Method':<15} {'Ratio':<10} {'Error':<10} {'Comp Time':<12} {'Decomp Time':<12}")
print("-" * 60)
for r in results:
print(f"{r['Method']:<15} {r['Ratio']:<10} {r['Error']:<10} {r['Comp Time']:<12} {r['Decomp Time']:<12}")
if __name__ == "__main__":
benchmark_compression()
本章小结
本章系统阐述了VFL安全训练协议的技术体系,从架构融合、密码学原语、攻击防御到通信优化四个维度构建了完整的防护框架。分裂学习与VFL的融合架构通过计算卸载与隐私保护的平衡,实现了跨机构数据协同的可行性;Beaver三元组与SPDZ2k协议提供了恶意安全模型下的高效矩阵乘法能力,支撑深度神经网络的密态训练;梯度压缩与稀疏化技术不仅降低了通信开销,更构成了防御DLG/iDLG攻击的重要非密码学屏障;ABY3与SecureML协议通过秘密分享机制实现了分层模型的隐私保护训练,其混合分享策略优化了线性与非线性运算的协同效率;Top-k稀疏化、Sketching与量化编码的组合应用,使得VFL在广域网环境下的部署成为可能。这些技术的有机整合,为构建实用化、高效率、强隐私保障的VFL系统奠定了理论与工程基础。
646



被折叠的 条评论
为什么被折叠?



