全同态加密(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)
编码使用规范嵌入(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)
加法噪声增长慢,乘法需注意。
噪声管理:挑战与解决方案
噪声是 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
prod.relinearize()
无 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)