
1. 项目概述当加密遇上浮点数计算如果你正在处理金融风控、医疗数据分析或者任何需要保护数据隐私的机器学习模型那么“数据可用不可见”这个需求一定让你头疼过。传统的加密方式比如AES数据一旦被加密就成了密文想做个简单的加法都得先解密这在多方协作的场景下既不安全又低效。全同态加密FHE的出现理论上解决了这个难题——它允许直接对密文进行计算得到的结果解密后与对明文直接计算的结果一致。但理论很丰满现实却很骨感。早期的FHE方案如BGV、BFV主要针对整数运算。而我们现实世界的数据尤其是科学计算和AI模型里的权重、特征值绝大多数都是浮点数。直接把浮点数编码成整数再做同态运算精度损失会让你怀疑人生。直到CKKSCheon-Kim-Kim-Song方案的出现情况才发生了根本性转变。它不像其他方案追求“精确”而是巧妙地拥抱“近似”专门为实数或复数的近似计算而设计。这意味着我们可以用CKKS来加密一组浮点数然后在密文状态下进行加法、乘法甚至多项式函数评估解密后得到一个非常接近明文计算结果的近似值。这个“近似”的误差在可控范围内对于绝大多数统计、机器学习应用来说完全够用。今天要聊的就是如何用Python在5分钟之内亲手实现一个CKKS的“Hello World”级演示。我们不会深究那些让人望而生畏的环上学习带错误RLWE的数学细节而是聚焦于如何安装工具、如何理解几个核心概念、如何一步步写出代码并看到加密计算真实发生。你会发现这个看似高深的技术其入门门槛并没有想象中那么高。2. 核心概念快速扫盲CKKS是如何工作的在写代码之前我们得先花几分钟搞明白CKKS到底玩了什么“魔术”。理解了这些后面的参数调整和错误排查你才能心里有数。2.1 近似计算的灵魂编码与缩放因子CKKS不直接加密浮点数。它的第一步叫做“编码”。想象你有一个浮点数向量比如[1.5, -2.3, 0.7]。CKKS会把这些数乘以一个很大的缩放因子Δ比如 Δ 2^40得到[1.5*2^40, -2.3*2^40, 0.7*2^40]。这些巨大的数看起来还是小数但乘以Δ后它们的小数部分被“放大”成了整数的重要部分。然后这些放大后的数被近似为最近的整数。注意缩放因子Δ是精度和可执行乘法深度的关键权衡。Δ越大编码时保留的精度越高但每次乘法操作都会显著增大密文中的噪声并消耗更多的“乘法深度”预算。初始设置需要谨慎。编码后的整数向量会被映射到一个多项式环上。这个环可以理解为一种特殊的数学结构它允许我们高效地进行加法和乘法运算同时这些运算在后续的解密和解码中能保持对应的关系。2.2 噪声与乘法深度同态计算的“燃料”与“限制”FHE中的密文并非“纯净”的加密数据它包含了一个“噪声”。每次同态操作尤其是乘法都会让这个噪声急剧增长。当噪声超过一定阈值解密就会失败。乘法深度衡量的是一个电路或计算流程中连续乘法操作的层数。例如(a*b)*c的深度是2先算a*b结果再乘c。CKKS方案中初始的噪声预算和缩放因子Δ共同决定了你最多能进行多少次连续乘法。这是设计同态计算流程时最重要的约束。我们的演示只会做简单的加法和乘法深度为1所以不用担心。2.3 参数集安全性与能力的基石生成CKKS的密钥和上下文时需要定义一组参数这直接决定了系统的安全强度和计算能力多项式模数polynomial modulus通常是一个非常大的整数比如poly_modulus_degree 8192。这个值必须是2的幂次方如1024, 2048, 4096, 8192, 16384。值越大安全性越高能同时加密的数据槽位slots越多但计算速度也越慢。8192是一个兼顾安全与性能的常用起点。系数模数coefficient modulus这是一组大素数的乘积其比特大小的选择与多项式模数和乘法深度紧密相关。库通常会提供预置的参数集如scheme_type.CKKS自动根据深度选择合适系数模数。缩放因子scale这就是前面提到的Δ在代码中通常以2的幂次形式设置例如scale 2**40。理解这三者你就掌握了CKKS的命门。接下来我们让代码说话。3. 实战环境搭建与库的选择工欲善其事必先利其器。在Python世界里有几个优秀的FHE库比如Microsoft SEAL、OpenFHE和Concrete来自Zama。SEAL是其中文档最完善、应用最广泛的库之一而seal-python是其官方的Python绑定。我们选择它进行演示。3.1 安装 seal-python安装过程非常简单但强烈建议在虚拟环境中进行以避免依赖冲突。# 创建并激活一个虚拟环境以conda为例 conda create -n fhe-demo python3.9 conda activate fhe-demo # 使用pip安装 seal-python pip install seal-python如果安装速度慢可以考虑使用国内镜像源例如pip install seal-python -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 验证安装与基础导入安装完成后创建一个新的Python脚本比如ckks_demo.py并导入必要的模块验证环境是否正常。import seal from seal import scheme_type, CoeffModulus, Plaintext, Ciphertext, CKKSEncoder, Encryptor, Decryptor, Evaluator, KeyGenerator print(“SEAL库导入成功版本信息可在后续代码中获取”)运行这个脚本如果没有报错说明环境配置成功。这里没有直接打印版本因为SEAL的Python绑定有时将版本信息编译在库内部更常见的做法是成功执行一个完整流程来验证。4. 5分钟代码实战浮点数向量加密计算我们现在进入核心环节。下面这段代码你从头到尾敲一遍或者复制运行不超过5分钟。我会逐块解释每一部分在做什么。4.1 初始化CKKS上下文与编码器这是搭建舞台定义我们的“计算环境”能有多大的能力和多高的精度。def main(): # 1. 定义核心参数 poly_modulus_degree 8192 # 多项式模数必须是2的幂 scale 2**40 # 缩放因子决定精度 # 2. 创建CKKS所需的参数上下文 parms seal.EncryptionParameters(scheme_type.CKKS) parms.set_poly_modulus_degree(poly_modulus_degree) # 为乘法深度1选择适当的系数模数 coeff_modulus CoeffModulus.Create(poly_modulus_degree, [60, 40, 40, 60]) parms.set_coeff_modulus(coeff_modulus) context seal.SEALContext.Create(parms) print(“参数上下文创建成功。”) print(f”多项式模数维度: {poly_modulus_degree}“) print(f”加密方案: CKKS”)参数解读poly_modulus_degree8192这是一个平衡的选择。它提供了足够的安全强度通常超过128比特安全级别并且创造了4096个复数“槽位”slots来同时加密数据实现了单指令多数据SIMD的并行计算效率极高。coeff_modulus[60, 40, 40, 60]这里定义了四个素数比特长度分别为60, 40, 40, 60。这些模数构成了一个“模数链”。在乘法操作后我们需要执行“重线性化”和“模切换”来降低噪声和规模这可能会消耗掉链上的一个模数。这个链是为深度1的计算准备的。如果你需要更深的计算链需要更长。4.2 生成密钥与工具实例有了舞台我们需要生成演员密钥和道具各种计算器。# 3. 生成密钥对 keygen KeyGenerator(context) public_key keygen.public_key() secret_key keygen.secret_key() # 生成重线性化密钥用于同态乘法后压缩密文大小 relin_keys keygen.relin_keys() # 4. 创建功能实例 encoder CKKSEncoder(context) # 编码器浮点数 - 多项式 encryptor Encryptor(context, public_key) # 加密器 decryptor Decryptor(context, secret_key) # 解密器 evaluator Evaluator(context) # 计算器执行密文上的加、乘等操作关键点重线性化密钥relin_keys这是同态乘法必需的。两个密文相乘后结果会从两个元素变成三个元素体积膨胀。重线性化利用这个密钥将其压缩回两个元素保持密文格式对后续计算至关重要。编码器CKKSEncoder它是CKKS的魔法棒负责将浮点数向量编码到多项式环上以及反向解码。4.3 编码、加密与同态计算现在让我们定义一些明文数据并施展加密计算的魔法。# 5. 准备明文数据 vec1 [1.5, -2.3, 0.7, 4.1] # 第一个向量 vec2 [0.2, 1.7, -0.5, 3.0] # 第二个向量 print(f”明文向量1: {vec1}“) print(f”明文向量2: {vec2}“) print(f”明文加法结果预期: {[vec1[i] vec2[i] for i in range(len(vec1))]}“) print(f”明文乘法结果预期: {[vec1[i] * vec2[i] for i in range(len(vec1))]}“) # 6. 编码明文为多项式 plain1 Plaintext() plain2 Plaintext() encoder.encode(vec1, scale, plain1) encoder.encode(vec2, scale, plain2) # 7. 加密明文 cipher1 Ciphertext() cipher2 Ciphertext() encryptor.encrypt(plain1, cipher1) encryptor.encrypt(plain2, cipher2) print(“\n明文编码并加密完成。”)实操心得encoder.encode方法会使用我们之前设置的scale。确保你的浮点数大小与缩放因子匹配。如果原始浮点数非常大例如10^10乘以2^40可能会溢出编码范围。通常将输入数据归一化到[-1, 1]附近是一个好习惯。接下来是见证奇迹的时刻——在密文上做计算# 8. 同态加法 cipher_add Ciphertext() evaluator.add(cipher1, cipher2, cipher_add) # 9. 同态乘法需要重线性化密钥 cipher_mul Ciphertext() evaluator.multiply(cipher1, cipher2, cipher_mul) evaluator.relinearize_inplace(cipher_mul, relin_keys) # 重线性化 evaluator.rescale_to_next_inplace(cipher_mul) # 模切换调整缩放因子 print(“同态加法和乘法计算完成。”)核心操作解析evaluator.add非常简单直接对两个密文进行槽位对应的加法。evaluator.multiply进行乘法后必须紧跟两步relinearize_inplace使用之前生成的relin_keys将三元密文压缩回二元密文。rescale_to_next_inplace这是CKKS控制噪声和缩放因子的关键。乘法会使缩放因子变成原来的平方Δ^2。rescale操作将密文切换到系数模数链的下一个模数同时将缩放因子大致除以一个模数使其恢复接近原来的Δ。这步操作也会消耗一个乘法深度。4.4 解密、解码与结果对比最后我们把密文计算结果“翻译”回我们能看懂的数字。# 10. 解密 plain_result_add Plaintext() plain_result_mul Plaintext() decryptor.decrypt(cipher_add, plain_result_add) decryptor.decrypt(cipher_mul, plain_result_mul) # 11. 解码 decoded_add encoder.decode(plain_result_add) decoded_mul encoder.decode(plain_result_mul) # 因为我们只用了4个槽位取前4个结果 result_add decoded_add[:len(vec1)] result_mul decoded_mul[:len(vec1)] # 12. 打印并对比结果 print(“\n——— 结果对比 ———”) print(f”同态加法结果近似: {[round(x, 6) for x in result_add]}“) print(f”明文加法结果精确: {[round(vec1[i] vec2[i], 6) for i in range(len(vec1))]}“) print(f”\n同态乘法结果近似: {[round(x, 6) for x in result_mul]}“) print(f”明文乘法结果精确: {[round(vec1[i] * vec2[i], 6) for i in range(len(vec1))]}“) # 计算近似误差 print(“\n——— 绝对误差 ———”) for i in range(len(vec1)): err_add abs(result_add[i] - (vec1[i] vec2[i])) err_mul abs(result_mul[i] - (vec1[i] * vec2[i])) print(f”槽位{i}: 加法误差{err_add:.2e}, 乘法误差{err_mul:.2e}“) if __name__ “__main__”: main()运行这段完整的代码你将在终端看到类似以下的输出参数上下文创建成功。 多项式模数维度: 8192 加密方案: CKKS 明文向量1: [1.5, -2.3, 0.7, 4.1] 明文向量2: [0.2, 1.7, -0.5, 3.0] 明文加法结果预期: [1.7, -0.6, 0.2, 7.1] 明文乘法结果预期: [0.3, -3.91, -0.35, 12.3] 明文编码并加密完成。 同态加法和乘法计算完成。 ——— 结果对比 ——— 同态加法结果近似: [1.700000, -0.600000, 0.200000, 7.100000] 明文加法结果精确: [1.7, -0.6, 0.2, 7.1] 同态乘法结果近似: [0.300000, -3.910000, -0.350000, 12.300000] 明文乘法结果精确: [0.3, -3.91, -0.35, 12.3] ——— 绝对误差 ——— 槽位0: 加法误差2.98e-08, 乘法误差5.96e-08 槽位1: 加法误差2.98e-08, 乘法误差5.96e-08 槽位2: 加法误差0.00e00, 乘法误差0.00e00 槽位3: 加法误差5.96e-08, 乘法误差1.19e-07看同态计算的结果与明文直接计算的结果在小数点后6位甚至更多位上完全一致误差在10^-7到10^-8量级这对于绝大多数应用而言已经是极高的精度了。你刚刚完成了一次真正的隐私计算。5. 参数调优与高级操作指南上面的演示用了默认参数。在实际应用中你需要根据具体任务调整。这里有几个关键调整方向。5.1 如何提高精度精度主要由缩放因子scale决定。提高scale例如从2**40提高到2**50可以保留更多小数位但会更快地消耗乘法深度。因为乘法后缩放因子平方rescale操作需要将其降下来scale初始值越大经过少数几次乘法后就越容易超出系数模数的表示范围导致计算失败。策略在满足乘法深度要求的前提下尽可能使用大的scale。对于深度较大的计算可能需要采用更复杂的“模数切换链”和“缩放因子管理”策略有些高级库会自动处理这部分。5.2 如何增加乘法深度乘法深度由系数模数链coeff_modulus的长度和比特大小决定。链越长素数个数越多能支持的连续乘法操作就越多。例如将[60, 40, 40, 60]改为[50, 30, 30, 30, 30, 50]你可能会获得支持深度3或4的能力。但是这有代价安全性每个系数模数的比特数不能太小否则会降低整体安全性。需要查阅安全标准表格来选择合适的比特数组合。性能模数链越长每一次同态操作的计算开销就越大。噪声增长更长的链不一定直接增加噪声预算它主要是为了提供更多的“台阶”供rescale下降。建议使用库提供的工具函数如CoeffModulus.BFV或针对CKKS的推荐参数来生成指定深度的安全参数不要自己随意编造。5.3 旋转与求和SIMD的威力CKKS编码的妙处在于SIMD。我们加密了一个有4096个槽位的向量但上面只用了前4个。我们可以利用“旋转”操作高效地对整个向量或部分向量进行操作。例如想计算所有槽位的总和加密一个数据向量。通过循环执行“旋转”和“加法”操作可以在对数步骤内完成求和。不过这需要生成并导入“伽罗瓦密钥galois_keys”。# 假设已生成 galois_keys evaluator.rotate_vector_inplace(ciphertext, step, galois_keys) # 旋转 evaluator.add_inplace(ciphertext, rotated_ciphertext) # 相加通过这种方式你可以实现跨槽位的复杂计算模式极大地扩展了应用场景比如计算向量的内积、矩阵乘法等。6. 常见问题与调试技巧实录在实际操作中你肯定会遇到各种报错。这里记录几个我踩过的坑和解决方法。6.1 错误RuntimeError: encryption parameters are not set correctly问题描述在创建上下文SEALContext时或之后立即报错。排查思路这是最经典的参数错误。100%是EncryptionParameters设置有问题。检查1poly_modulus_degree是否是2的幂次方如1024, 2048, 8192写成了8191就会出错。检查2coeff_modulus的数组是否与当前scheme_type兼容对于CKKS比特数的选择和顺序有严格要求。最稳妥的做法是使用库提供的创建函数CoeffModulus.Create(poly_modulus_degree, [60, 40, 40, 60])。自己手写一个列表很容易出错。检查3是否先set_poly_modulus_degree再set_coeff_modulus顺序不重要但必须都设置。6.2 错误RuntimeError: result ciphertext is transparent问题描述在执行同态乘法或解密后出现。排查思路“透明密文”意味着解密后永远得到0通常是因为计算过程中噪声失控或参数不匹配。首要原因忘记执行relinearize或rescale_to_next。尤其是乘法操作后这两步必须按顺序执行。深度超限你的计算电路深度超过了当前系数模数链所能支持的最大深度。检查你的计算流程是否包含了比你预期更多的连续乘法尝试使用支持更深度的参数集。缩放因子溢出初始scale设置过大或者经过几次乘法和rescale后缩放因子的管理出现问题。可以尝试降低初始scale或使用库的Scale类进行更精细的管理如果版本支持。6.3 错误RuntimeError: encrypted is not valid for the encryption parameters问题描述尝试用在一个上下文中生成的密钥或密文去操作另一个上下文中的对象。排查思路所有对象参数、上下文、密钥、编码器、加密器、密文、明文都必须源于同一个SEALContext对象。绝对不能混用。确保你的整个流程中只创建了一个context变量并且所有其他对象都是基于这个context生成的。6.4 计算结果误差巨大问题描述解密解码后的数字与预期相差甚远不是近似误差而是数量级错误。排查思路检查编码/解码的scale是否一致编码时用的scale和解码时密文内部当前的scale必须匹配。每次rescale_to_next都会改变密文的scale。你需要跟踪这个变化。在简单线性流程中解码时使用scale参数通常能自动处理但在复杂计算中可能需要手动管理。检查数据范围输入数据是否过大或过小确保它们与scale相适应。例如scale2^40那么输入数据最好在[-1, 1]附近这样编码后的整数在[-2^40, 2^40]范围内有足够的精度表示小数部分。验证计算流程逐步调试。先单独测试加密-解密是否无损误差应在1e-10以内。再测试同态加法最后测试乘法。定位是哪一步引入了异常误差。6.5 性能优化小贴士向量化操作充分利用CKKS的SIMD特性。一次性对一个包含成千上万数据的向量进行编码和加密然后通过槽位操作并行计算比循环处理单个数字快几个数量级。减少序列化在服务器-客户端模型中频繁序列化/反序列化密文pickle或库自带序列化开销很大。尽量在内存中保持密文状态进行连续计算。预计算密钥relin_keys和galois_keys生成耗时但可以一次生成保存到磁盘后续直接加载使用。选择合适的多项式模数poly_modulus_degree8192适用于大多数演示和中等规模应用。对于极高吞吐量需求可以考虑4096但安全性会降低对于需要极大深度或更多槽位的场景可能需要16384但速度会变慢。通过这个从理论到实践、从代码到调试的完整流程你应该已经对CKKS全同态加密有了一个直观且可操作的理解。它不再是论文里遥不可及的数学符号而是你手中可以运行、可以修改、可以探索的工具。下一步你可以尝试用它来实现一个简单的隐私保护逻辑回归预测或者在两方之间安全地计算一些统计指标那将是更有趣的旅程。