【联邦学习高级应用】第1章 垂直联邦学习(VFL)基础与形式化定义

001 A Comprehensive Survey of Privacy-preserving Federated Learning(便于寻找:FedAvg、垂直联邦学习的基本步骤) 这是我看的第一篇关于联邦学习的论文,综述文章,让我对联邦学习有了初步的了解。 A Comprehensive Survey of Privacy-preserving Federated Learning: A Taxonomy, Review, and Future Directionshttps://doi.org/10.1145/3460427 下图用表格的形式将联邦学习中隐私保护相关论文进行了罗列 ...... 阅读详情

目录

第1章 垂直联邦学习(VFL)基础与形式化定义

1.1 垂直联邦学习(VFL)基础与形式化定义

1.1.1 水平联邦、垂直联邦与联邦迁移学习的形式化区分

1.1.2 VFL的数学建模:特征空间划分与标签持有方定义

1.1.3 样本不对齐问题:ID-Mismatch的数学表述与影响分析

1.1.4 国外主流框架对比:FATE-VFL、PyVertical、FedTree架构解析

1.2 隐私保护集合求交(PSI)协议体系

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.3.1 实体解析(Entity Resolution)在VFL中的技术实现

1.3.2 模糊匹配与隐私保护记录链接(PPRL):Soundex与Bloom Filter方案

1.3.3 时序数据对齐:基于时间窗口的样本关联策略

1.3.4 特征异构处理:Embedding空间对齐与特征哈希技术

1.3.5 对齐质量评估:Precision-Recall在隐私约束下的度量方法

1.4 安全训练协议与梯度保护机制

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实现结合布隆过滤器压缩,可在保护隐私的同时降低通信开销。协议执行完毕后,各参与方仅保留交集样本的数据记录,并依据统一顺序重新排列,确保后续训练过程中样本索引的一致性。

PyVertical 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框架架构

框架对比层面,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反推原始值;同时支持近似匹配,容忍一定程度的拼写错误与格式差异。

Bloom Filter PPRL流程

以下代码实现了基于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 保护个体隐私;安全多方计算协议,使各方在加密状态下联合计算评估指标;采样评估,仅对随机抽样的子集进行人工标注与评估,降低隐私风险。

Precision-Recall评估

以下代码实现了隐私约束下的对齐质量评估系统,支持安全聚合、差分隐私与采样评估:

"""
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系统奠定了理论与工程基础。

水平联邦学习、垂直联邦学习和联邦迁移学习 这两家公司服务的用户可能有重叠部分,但它们的数据类型完全不同。想象不同的公司(比如几家银行)都拥有类似类型的客户数据,比如用户的年龄、职业和消费记录。每个公司都有一部分不同的客户,但他们的用户数据格式和特征是相同的。这种情况下,数据是在“横向”分布的——每个客户端拥有的都是来自。大学有学生的学术数据,但没有患者的病历信息。:适用于用户群体不同、数据类型不同的场景,比如医院和大学合作开发一个预测学生健康风险的模型。:适用于企业合作分析同一组用户的数据,比如联合分析用户的消费能力和购买偏好。 阅读详情

相关推荐

隐语 垂直联邦学习

4.拆分并发送梯度,进行反向传播更新Fuse模型,更新Base模型。数据处理(离散值特征不能直接用到神经网络)和特征上的缩放。支持灵活的自定义模型结构且内置了部分拆分模型。2.前向传播:安全传输给有Label的一方。内置多种通信/计算优化和安全加强方法。3.融合,继续前向传播,计算Loss。1.数据对齐:保证用户是相同的。扰动 保证数据不会被反向猜测。数据加载对齐(各方持有几列)指定account效率。自定义数据预处理流程。

huang8666的博客 646

联邦学习

课程主要就联邦学习介绍,联邦学习算法原理,联邦学习算法分类,联邦学习算法实现与优化和联邦学习应用案例,配套企业项目案例展开。由浅入深,引领学员浸泡逐步掌握企业级智能的技巧。

联邦学习 Federated Learning 基本介绍

联邦学习(FL)是一种在边缘设备利用本地数据进行模型训练,再将模型参数传回服务器聚合的一种训练方法,降低了传统集中式机器学习方法带来的一些系统隐私风险和成本。

Jiayyyiii的博客 2416

纵向联邦学习

一、纵向联邦学习的定义 纵向联邦学习(VFL)一般是适用于数据集上具有相同的样本空间、不同的特征空间的参与方所组成的联邦学习场景,纵向联邦学习也可以理解为按特征划分的联邦学习。 举个例子,我们假设有两家公司A和B想要协同地训练一个机器学习模型,每一家公司都拥有各自的数据。例如保险公司与银行合作,根据同一用户的购买历史与消费习惯,为该用户提供定制化的服务;医院与制药公司合作,通过利用同类患者的医疗记录,从而治疗患者的慢性疾病,并降低患者未来住院治疗的风险。

武天旭的博客 1万+

深入浅出横向联邦学习、纵向联邦学习、联邦迁移学习

深入浅出解析横向联邦学习(Horizontal Federated Learning)、纵向联邦学习(Vertical Federated Learning)和联邦迁移学习(Federated Transfer Learning)有多个机构(比如几家不同的银行,或者几家医院)都拥有一些数据,他们希望联合起来训练一个更强大的机器学习模型,但出于隐私保护或法规要求,他们不能直接把数据共享出来。联邦学习就是为了解决这个问题而提出的。它的核心思想是“数据不动模型动”,即数据保留在本地,参与方共同训练模型。

frostmelody 全网同名,大家多多关注呀~ 持续分享优质内容! 2467

【联邦学习高级应用】第1章 垂直联邦学习(VFL)基础与形式化定义 二

在这种架构下,协调方的角色弱化为轻量级的目录服务(Directory Service),仅负责初始的身份认证与网络发现(Network Discovery),后续的梯度交换与模型聚合直接在参与方之间完成。Multi-Krum算法是其中的代表性方法:给定n个参与方的更新,算法计算每对更新之间的欧氏距离,为每个更新选择距离最近的n-f-2个邻居(f为假设的拜占庭节点数),计算这些距离的和作为该更新的得分,最终选择得分最低的更新作为聚合基础。前向传播时,各参与方计算至切分层(Cut Layer)的激活值并传输;

VectorShift的博客 73

【信息科学与工程学】【产品体系】第二十篇 CDN网络 01

CDN技术体系全景解析​1. 业务类型与随机过程建模​​类型​​随机过程描述​​测度空间​​突发流量冲击​复合泊松过程X_i为请求强度)​区域热点聚集​空间霍克斯过程​分布式共振​耦合振子模型相空间​2. PRD核心指标(泛函约束)​​:节点队列状态向量A:网络拓扑关联矩阵(图论邻接矩阵):随机需求过程(Levy过程驱动)时间状态特征与代数结构​1. 系统状态流形​graph LRS[状态空间 S] -->|微分同胚| M[特征流形 M]

weixin_49199313的博客 4118

人工智能技术白皮书 —— 你的AI入门手册

深入定义状态 (State, S)、动作 (Action, A)、奖励 (Reward, R) 以及状态转移概率 (Transition Probability, P),构建一个完整的决策模型。强化学习是关于“决策”的科学,它让智能体(Agent)在与环境的持续互动中,通过试错和回报,学习一套能获得最大长期利益的最优行为策略。学习它,就是学习如何驱动机器的“想象力”。:一个迷宫地图,智能体(小机器人)在每个交叉口(状态)选择方向(动作),根据路径上的宝藏(奖励)或陷阱,更新自己的寻宝地图(策略)。

VectorShift的博客 1396

隐私计算实训营:联邦学习在垂直场景的开发实践

纵向联邦学习的参与方拥有相同样本空间、不同特征空间的数据,通过共有样本数据进行安全联合建模,在金融、广告等领域拥有广泛的应用场景。和横向联邦学习相比,纵向联邦学习的参与方之间需要协同完成数据求交集、模型联合训练和模型联合推理。并且,参与方越多,纵向联邦学习系统的复杂度就越高。下面以企业A和企业B两方为例来介绍纵向联邦学习的基本架构和流程。假设企业A有特征数据和标签数据,可以独立建模;企业B有特征数据,缺乏标签数据,因此无法独立建模。由于隐私法规和行业规范等原因,两个企业之间的数据无法直接互通。

TianxiaZhu824的博客 1278

垂直联邦学习:跨行业数据协作的新模式

垂直联邦学习:跨行业数据协作的新模式 关键词:垂直联邦学习, 跨行业数据协作, 联合学习, 联邦学习, 隐私保护, 数据共享, 模型训练 1. 背景介绍 1.1 问题由来 在

欢迎来到我的优快云空间!这里聚焦AI大模型应用实战,分享前沿技术、实战案例与开发经验。 773

水平和垂直联合深度学习功能可用于车辆重新识别

水平和垂直联合深度学习功能可用于车辆重新识别

白话搞懂【联邦学习】

联邦学习(Federated Learning)近几年很火,本文拟用**最通俗**语言解释联邦学习的来龙去脉。

全栈O-Jay的博客 8467

交通流预测爬坑记(一):交通流数据集,原始数据

常用的交通流数据集,持续更新~~

K_first的博客 4万+

读懂联邦学习中的安全与隐私问题(全)

该篇是2021年发表在《Future Generation Computer Systems》期刊上的一篇综述论文,主要介绍了联邦学习中可能面对的所有安全与隐私威胁问题,比较详细和具体的进行了综述,是篇还不错的文章,建议读原文。以下是个人的读书笔记,结合自己的理解,只截取部分关键或重要的部分。 先附上原文下载链接:查阅or下载 (长文警告,本篇大约1.8w个字,谨慎阅读!) 文章目录预览《A survey on security and privacy of federated learning》联邦学习的

cryptocxf的博客 2万+

2026 具身智能 VLA/WAM 全栈学习与训练路线:从动作建模、SFT、RL 到世界模型的 RTX PRO 6000 96GB 单卡实战指南

我会把这张表做成“”而不是只列论文名:先核对每个模型截至的公开代码、训练目标和数据接口,再单独给你一套。对于没有公开、只有推理代码或训练细节不完整的模型,我会明确标出来,避免把“论文里提到”误写成“可复现”。目前有一个重要更新:。截至今天 NVIDIA 官方开源主线已经是换成 Cosmos-Reason2-2B/Qwen3-VL 架构,动作端仍是连续路线,而且官方明确给出单卡微调时默认只训练,峰值显存约。所以你的 96GB 单卡对 GR00T SFT 非常宽裕。可以。下面我按,把它整理成一张真正面向“

u013250861的博客 448

2. Netty使用JSerialComm进行串口读取(二)

现在开始处理这些问题 第一个问题:串口读取超时异常一路传播到最后也没有处理。 ​ 第二个问题:即使处理了该异常,程序仍然退出,这不能满足一些实际使用要求需求。该问题与设置很长的读取超时时间没关系,无论多长到时间后总会退出。

s3c2410的博客 2463

【学习笔记】联邦学习

联邦学习是一种在保护数据隐私的前提下,允许多个参与方协作训练机器学习模型的框架。它主要有三种类型:水平联邦学习(Horizontal Federated Learning)、垂直联邦学习(Vertical Federated Learning)和联邦迁移学习(Federated Transfer Learning)。这三种方法都旨在在不共享原始数据的情况下,通过协作训练来提高机器学习模型的性能,同时保护数据的隐私和安全。

信息安全专业研究生在读,记录学习内容 1025

【实战技巧】matplotlib中文标签显示问题的3种高效解决方案

本文详细介绍了解决matplotlib中文标签显示问题的3种高效方法,包括修改字体设置、临时指定字体和修改后端配置。针对不同操作系统和环境提供了实用技巧,帮助开发者快速解决中文显示为方框的常见问题,提升数据可视化效果。

weixin_30855099的博客 172

010 Large-Scale Kernel Method for Vertical Federated Learning(垂直联邦学习大规模核方法)

面向垂直联邦学习的大规模核方法

Uzz_yuzaizai的博客 602

YOLOv8 | 损失函数改进,最新提出的Shape-IoU,有效涨点

作为检测器定位分支的重要组成,边框回归损失在目标检测任务中发挥巨大作用。现有的边框回归方法,通常考虑了GT框与预测框之间的几何关系,通过使用边框间的相对位置与相对形状等计算损失,而忽略了边框其自身的形状与尺度等固有属性对边框回归的影响。为了弥补现有研究的不足,本文提出聚焦边框自身形状与尺度的边框回归方法。首先我们对边框回归特性进行分析,得出边框自身形状因素与尺度因素会对回归结果产生影响。

kay_545 1915

2026.2.23:AgentScope框架实战<一>:安装并使用agentscope

本文介绍了AgentScope框架的安装与使用实践。通过PyPI安装agentscope[full]后,演示了如何创建ReActAgent代理:1)配置DashScope聊天模型;2)注册自定义工具函数(查询位置和天气);3)初始化代理并处理用户请求。示例展示了代理如何响应复合查询(如同时获取北京位置和天气信息),输出结果显示代理成功执行了工具调用并返回结构化响应。该框架支持Python 3.13环境,提供内存管理、工具集成等功能,适用于构建智能代理系统。

拥抱具身智能,昇腾Ascend C 512

联邦学习学习记录

例如,两个区域的银行可能各自区域的用户群非常不同,且用户交集非常小。例如,同城有一家银行,另一家电子商务公司,他们的用户集可能包含该地区的大多数居民,因此他们的用户空间交集很大。但是, 由于银行记录了用户的收支行为和信用评级,而电子商务保留了用户的浏览和购买历史,所以他们的特征空间有很大不同。由于他们的用户组不同,系统使用基于加密的用户ID对齐技术 来确认双方的普通用户,而不需要A和B暴露各自的数据。Step 3:A和B分别计算加密梯度和添加额外掩码,B还计算加密损失,A和B向C发送加密的值;

weixin_52326703的博客 480

数据分片的三种方式:哈希、一致性哈希、按照数据范围分片

数据分片就是按照一定的规则,将数据集划分成相互独立、正交的数据子集,然后将数据子集分布到不同的节点上。通过设计合理的数据分片规则,可将系统中的数据分布在不同的物理数据库中,达到提升应用系统数据处理速度的目的。三种常用的数据分片方式:1. 哈希分片2. 一致性哈希分片(Consistent Hash)3. 按照数据范围分片(Range Based)数据分片的应用场景:1. 数据库扩展2. 分布式缓存对集群部署多个缓存节点,提高缓存的处理能力。

wjianwei666的专栏 1135

横向联邦学习(Horizontal Federated Learning)VS 纵向联邦学习(Vertical Federated Learning)

1.横向联邦学习:适用于参与者的数据特征维度重叠较多的情形: 目的:增加训练数据的数据量 例如甲医院有患者:A,B,C;乙医院有患者C,D,F这六位患者,甲乙医院都有患者的身高,体重,血压,心跳等信息,那么进行联邦学习一个判断患者健康状况的模型时,就可以使用横向联邦学习: 示例: 2.纵向联邦学习:适用于参与者数据ID特征重叠较多的情形: 目的:增加参与者数据的数据维度 例如甲医院...

Xiao_Bai_Ke的博客 1万+

基于加速科技ST2500的SN74HC00N芯片测试计划

实际上随着工艺的进步,器件内部和引脚间的绝缘氧化膜越来越薄,导致漏电流发生的概率更大,另外制造过程中的工艺缺陷导致的桥接,异物或封装过程中造成的芯片划伤、引裂,都会造成IC的漏电流偏大,有部分产品可能会表现出漏电流偏大,但功能正常,此类产品具有潜在的可靠性问题,漏电流测试的目的就是把具有此类缺陷的产品筛选出来,避免其流到最终产品造成更大的损失。漏电流的测试方法相对简单,就是根据产品手册或测试规范对被测引脚施加额定的电压,然后测量其输入或流出的电流是否符合相应的设计规范。74HC00引脚功能如表所示。

weixin_68288412的博客 1440

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

上一篇: 【机器学习系列】一文搞懂联邦学习
下一篇: 【联邦学习高级应用】第1章 垂直联邦学习(VFL)基础与形式化定义 二
VectorShift
博客等级 码龄1年 1840粉丝 1109原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VectorShift

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值