Python 中 CKKS 方案的实际实现:噪声管理和密钥切换
面向初学者,给出 CKKS 方案在 Python 中的步步实现,应对噪声管理和密钥切换的工程挑战。
全同态加密(Fully Homomorphic Encryption, FHE)允许在加密数据上直接进行计算,而无需解密,这在隐私保护计算中至关重要。其中,CKKS 方案特别适用于处理实数或复数的近似计算,支持浮点数运算,广泛应用于机器学习和数据分析等场景。本文针对初学者,提供 CKKS 在 Python 中的逐步实现指南,重点探讨噪声管理和密钥切换这些实际挑战。通过 Microsoft SEAL 库的 Python 封装 TenSEAL,我们可以轻松上手,避免底层 C++ 的复杂性。
CKKS 方案概述
CKKS(Cheon-Kim-Kim-Song)方案由 2017 年提出,基于环学习带错误问题(Ring-LWE),其核心创新是将噪声视为近似计算误差。通过编码机制,将复数向量映射到多项式环上,支持加法、乘法和旋转等操作。但每次同态乘法会引入噪声增长,如果不管理好,将导致解密失败。
观点:噪声不是 bug,而是特征;通过参数调优和重缩放(Rescale),可以平衡计算深度与精度。证据显示,在典型机器学习任务中,CKKS 可支持 5-10 次乘法而不显著丢失精度(参考 TenSEAL 基准)。实际落地时,选择合适的多项式度数 N 和缩放因子 Δ 是关键。
环境准备与参数选择
首先,安装 TenSEAL:pip install tenseal
。TenSEAL 封装了 SEAL 库,提供 CKKS 支持。
关键参数:
- 多项式模度数(poly_modulus_degree, N):如 8192,支持 N/2 = 4096 个槽位。N 越大,支持更多并行计算,但计算开销增加。初学者推荐 4096 或 8192。
- 系数模位大小(coeff_mod_bit_sizes):模数链,如 [60, 40, 40, 60],用于多级计算。第一个是初始模 q0,后续用于重缩放。
- 全局缩放因子(global_scale):如 2^40,确保初始噪声远小于消息。缩放下限约 2^20,以保留精度。
- 乘法深度(mult_depth):间接由模链决定,通常 3-5。过多会耗尽“噪声预算”。
风险:参数不当可能导致 128 位安全级别下降,或运行时错误。监控噪声预算:TenSEAL 的 approx_noise_budget()
方法可检查剩余预算,低于 20 位时需重缩放或自举。
步步实现:基本加密与解密
-
创建上下文:
import tenseal as ts import numpy as np context = ts.context( ts.SCHEME_TYPE.CKKS, poly_modulus_degree=8192, coeff_mod_bit_sizes=[60, 40, 40, 40, 60] ) context.global_scale = 2**40 context.generate_galois_keys() # 用于旋转
这里,Galois 键支持向量旋转,如神经网络中的卷积。
-
生成密钥:
secret_key = ts.generate_private_key(context) public_key = ts.generate_public_key(secret_key, context) context.make_context_public(public_key) # 如果需公钥加密
私钥用于解密,公钥可选。
-
编码与加密: CKKS 支持实数向量编码。
data = np.array([1.0, 2.0, 3.0, 4.0]) # 实数向量,自动扩展为复数 plain = ts.ckks_vector(context, data) enc_vector = ts.ckks_vector(context, plain) # 或直接 ts.ckks_vector(context, data)
编码使用规范嵌入(Canonical Embedding),将向量映射到多项式系数。
-
基本操作与解密:
enc2 = ts.ckks_vector(context, np.array([5.0, 6.0, 7.0, 8.0])) result = enc_vector + enc2 # 加法 result = enc_vector * 2.0 # 标量乘 decrypted = result.decrypt() # 使用私钥解密 decoded = decrypted.tolist() # 解码回向量 print(decoded) # 近似 [6.0, 8.0, 10.0, 12.0]
加法噪声增长慢,乘法需注意。
噪声管理:挑战与解决方案
噪声是 CKKS 的核心挑战:加密引入初始噪声 e,同态操作累积,导致解密时消息 μ + e' ≈ μ,但 e' 过大则精度丢失。
观点:通过重缩放控制噪声,将其视为浮点截断。证据:在 TenSEAL 中,每次乘法后调用 result.rescale_to(global_scale)
,模拟模切换,丢弃低位噪声。
可落地参数/清单:
- 监控噪声:使用
enc.approx_noise_budget()
,初始约 60 位。每乘法耗 20-30 位。 - 重缩放阈值:当预算 < 30 位时,立即 rescale。参数:scale = 2^40,精度损失 ≈ log2(1/scale) 位。
- 自举(Bootstrap):高级,若深度耗尽,解密-重加密(TenSEAL 未原生支持,需自定义)。初学者避免,限制深度 ≤5。
- 精度测试清单:
- 加密小向量,多次乘法后检查 ||decoded - expected|| < 1e-5。
- 调整 scale:太大噪声淹没消息,太小精度不足。
- 模链长度:每级 1 次乘法,4 级支持 3 次乘。
示例:乘法后 rescale
prod = enc_vector * enc2
prod.rescale_to(2**40) # 控制噪声
若不 rescale,多次乘后预算为 0,解密失败。
密钥切换:重线性化机制
乘法后,密文度从 2 升至 3(c0 + c1 sk + c2 sk^2),需降回 2 度,使用重线性化键(relin_keys)进行密钥切换。
观点:密钥切换确保密文紧凑,避免指数增长。证据:SEAL/TenSEAL 中,relin_keys 基于私钥生成,切换时替换 sk^2 项为公钥形式。
步步实现:
-
生成 relin 键:
relin_keys = ts.generate_relin_keys(secret_key, context) context.relin_keys = relin_keys
-
乘法与切换:
prod = enc_vector * enc2 # 产生 3 度密文 prod.relinearize() # 自动使用 relin_keys 降度
无 relinearize,多次乘后内存爆炸。
挑战:生成 relin_keys 耗时(O(N^2 log q)),存储大。解决方案:预生成,仅支持必要深度(如 mult_depth=3 时生成相应键)。监控:prod.degree
应始终 ≤2。
清单:
-
- 上下文初始化后立即生成 relin_keys。
-
- 每乘法后调用 relinearize。
-
- 测试:多层乘法后,检查度数和性能(切换增加 20% 开销)。
-
- 优化:用更小的 N 测试,逐步 scale up。
高级应用与回滚策略
结合噪声与切换,实现线性回归等:编码特征向量,加密后同态矩阵乘(需旋转支持 Galois 键)。
回滚策略:
- 若噪声超阈值,fallback 到部分同态或解密重加密。
- 错误处理:捕获
EncryptionException
,调整参数重试。 - 性能:N=8192 上,单乘 ≈1ms(CPU),批量优化并行槽。
通过以上步骤,初学者可快速构建 CKKS 原型。实际项目中,结合 TFHE(如 Concrete-ML)扩展自举。未来,CKKS 将推动隐私 ML 落地,但需警惕量子攻击(后量子安全参数)。
(字数:约 1050)