0


同态加密和SEAL库的介绍(九)CKKS 参数心得 1

写在前面:

    前面几篇有官方的说明和示例做支撑,相信能给大家比较多的参考价值。但是由于没能对同态加密有更深入的了解,所以在我具体使用的时候出现各种问题。本篇是针对这些问题做的一些测试,由结论产生的了些个人的推测,希望对大家有帮助。

一、引入和参数配置

    上篇性能测试中,官方提到了一句:“**不推荐在CKKS中使用BFVDefault素数**。然而,对于性能测试,BFVDefault素数已经足够好了”。在 CKKS 中也提到了 “**以特定方式选择 coeff_modulus 可能非常重要**。”
     足以可见参数设置的重要性,当然三个参数关系中:**poly_modulus_degree 限制了 coeff_modulus 的上限,scale 要和 coeff_modulus 相适应**。故接下来针对 coeff_modulus 进行具体测试和说明。

1.1 参数说明

    先说上限的问题,poly_modulus_degree 的配置会确定 coeff_modulus 配置的上限(即几个位置加起来的比特数总和)。当然,**poly_modulus_degree 的选择会影响槽的数量和带来较大性能差异**,所以在设计之初就得确定,影响也是最大的。基本根据算法的设置就能选定一个性能最优的,并不需要过多抉择。

    poly_modulus_degree 选定后,虽然能确定 coeff_modulus 的上限,但是具体怎么设置是个问题,咱们先看通过 Default 函数自动生成的效果:

    虽然不确定函数内部依据何种原则生成的,但是都是顶着上限在生成的(可能会浪费),而且每位都是基本相同(这个有问题)。故所以官方不建议用此,那咱们接下来具体实验。

1.2 参数配置

输入准备:(后面测试中,主要改变的也是 coeff_modulus 和 scale )

EncryptionParameters parms(scheme_type::ckks);
size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);
parms.set_coeff_modulus(CoeffModulus::Create(poly_modulus_degree, { 60,40,60 }));
double scale = pow(2.0, 40);
SEALContext context(parms);

KeyGenerator keygen(context);
auto secret_key = keygen.secret_key();
PublicKey public_key;
keygen.create_public_key(public_key);
Encryptor encryptor(context, public_key);
Evaluator evaluator(context);
Decryptor decryptor(context, secret_key);
CKKSEncoder encoder(context);
size_t slot_count = encoder.slot_count();

1.3 输入编码和加密

    测试会从编码一个数组,然后备用几个常数来依次做乘法(加法基本没影响,且不同深度的乘法影响较大),观察其密文容量、模数链位置和 scale 的变化。
vector<double> the_input;
the_input.reserve(slot_count);
for (size_t i = 0; i < slot_count; i++){
    the_input.push_back((double)i);
}
std::cout << "Print the Input vector: " << endl;
Plaintext the_input_plain;
encoder.encode(the_input, scale, the_input_plain);
Ciphertext the_input_enc;
encryptor.encrypt(the_input_plain, the_input_enc);

Plaintext the_constant_plain_1, the_constant_plain_2, the_constant_plain_3;
encoder.encode(3.14, scale, the_constant_plain_1);
encoder.encode(3.14, scale, the_constant_plain_2);
encoder.encode(3.14, scale, the_constant_plain_3);

1.4 连乘要注意的问题

    之前说过算加法的时候,参数要匹配,后来发现乘法也需要,即 **param_id 和 scale的确切值 要相同**!
     补充:**之前在BFV的时候,可以通过 decryptor.invariant_noise_budget() 来查看噪声预算,实测这个函数在 CKKS 里面用不了**,所以很多时候虽然可以解密不报错,但是结果是错误的,故参数的设置要自己注意!
Ciphertext the_input_enc;
encryptor.encrypt(the_input_plain, the_input_enc);

evaluator.multiply_plain_inplace(the_input_enc, the_constant_plain_1);
evaluator.rescale_to_next_inplace(the_input_enc);

evaluator.multiply_plain_inplace(the_input_enc, the_constant_plain_2);

代码设计如上(中间输出的代码省略了),运行结果如下:

    **可以发现,乘法后 param_id 没变,但是 scale 翻倍了;Rescale 后,param_id 左移,scale 恢复了。**这时候进行第二次乘法会报错,因为一开始编码的 the_constant_plain_2 的 param_id 是 初始的2,scale 也是标准的 2^40 (rescale 后的 the_input_enc 只是近似,确切值不同),**故会报错说不匹配**。

这里修改匹配就行:

  1. 把 明文 的 param_id 向下调,和密文一样即可;但是不能调密文的,因为不能向上;
  2. 模数互相调整都行,因为密文也是近似于 2^40 次方,调整不会有大影响(但是你数字本身太大影响就能看出来了!)
evaluator.mod_switch_to_inplace(the_constant_plain_2, the_input_enc.parms_id());
the_constant_plain_2.scale() = the_input_enc.scale();

   ** 如果第二次不是乘法,是加法。那这里就有另一种情况了,即如果不进行 rescale 呢?**(因为如果一开始就打算只乘一次),那 param_id 其实是一样的,不用调整。但是明文要注意!**明文编码的时候用的是 2^40 次方,如果现在强行改成 2^80 次方就会出错!
     **这时候我试过一个方法,即直接在明文编码的时候就用 2^80 次方即可,这里只需再改一下确切值,就没问题了。(注意,是乘完了再加一次!当然情况比较特殊,大家做个参考即可)
encoder.encode(3.14, pow(scale,2), the_constant_plain_2);

Ciphertext the_input_enc;
encryptor.encrypt(the_input_plain, the_input_enc);

evaluator.multiply_plain_inplace(the_input_enc, the_constant_plain_1);

the_constant_plain_2.scale() = the_input_enc.scale();
evaluator.add_plain_inplace(the_input_enc, the_constant_plain_2);

     输出如上,是正确的。**因为 rescale 本身也是需要时间代价的**,**但是 scale 翻倍对解密是没有影响的**,故如果过程类似的可以参考下。**当然通过上面也能说明,加法是不改变容量、param_id 和 scale 的。**

    这里补充一下:这里没有输出密文的大小(size),因为经过测试过**明文乘法和加法并不会改变密文大小**,所以不在此处进行讨论(**下一篇密文乘法的时候会讨论**)。这里对容量进行了输出,虽然不清楚具体含义,但是 Rescale 会引起容量的变化,放在这里便于大家对比。

二、Coeff_modulus 和 Scale 的关系

    按照之前的解释,**coeff_modulus 的最后一位是用来生成密钥的,要大于等于其他的值;第一位是用来解密的,也是要适当的比较大;中间的要和 scale 相近。**所以之前例子中,官方采用{ 60, 40, 60} 这种配置。

咱们先看和 scale 的关系,将中间和 scale 设置成不相同的,观察变化:
scale = 30,coeff_modulus = 180 (50 + 40 + 40 + 50) bits

    这里我们发现,因为都是拿 2^30 编码的,所以乘完正常翻倍至 60 bits;**但是因为中间设置的是 40,这里 rescale 后直接变成 20 了**(即 60 - 40 = 20)!**直接解密发现虽然近似值差不多,但是明显误差变大了,即 scale 影响了结果的精度!
     故最好将中间的数设置为和 scale 一样,这样才能稳定中间结果**!

三、Scale 对精度的影响

    示例中只是简单提到了大约位数,但是无法推测出具体的精确程度。故做实验对比下:

scale = 20,coeff_modulus = 120 (40 + 20 + 20 + 40) bits

    第二位的精确结果是 9.8596,第三位是 19.7192,最后一位是 40375.062,可以发现**数字越大,误差也就越大**。**说明精度不是到具体位数的,而是跟数字本身有关的**!

scale = 40,coeff_modulus = 200 (60 + 40 + 40 + 60) bits

     可以发现,**哪怕是最后一位的 40375.062,也得到了精确结果**。(但是后面的测试会发现,**模数链是跟乘法深度匹配的,故如果scale大了,就无法进行深度的计算了,需要一定的取舍**)

四、加密参数对深度明文乘法的影响

** 模数链限制了乘法的深度,因为当 param_id 处于最底的时候,再 rescale 就会报错**。这个理解没什么问题,但是后续在实验的时候发现了一个现象,模数处于底部的时候,是一个比较特殊的状态。

4.1 模数底部的特殊性

scale = 20,coeff_modulus = 120 (40 + 20 + 20 + 40) bits

这里想去乘第三次的时候,会报错说:scale out of bounds !(第二次结果也有误)

    因为我是把密文的 scale 赋值给明文的,所以第三次乘法的 scale 应该是  ![(20.4605)^2](https://latex.csdn.net/eq?%2820.4605%29%5E2),**确实这超过了 coeffee_modulus 的第一位 40,但是第二次乘法结果的 scale 也超过**了,为什么没问题?

所以我猜测是因为此时已经处于了模数链的最底层,所以比较特殊?验证不是底层的情况:
scale = 20,coeff_modulus = 140 (40 + 20 + 20 + 20 + 40) bits

    果然,因为**模数练处于底层的特殊性。**当然此时结果是错的,**第二位的精确结果是30.9591**,按照之前结论,此时确实精度不够,但是不会报错,故先探究报错原因。

通过多次尝试发现:

scale = 20,coeff_modulus = 121 (41 + 20 + 20 + 40) bits

    即只要第 **coeff_modulus 第一位大于乘法结果的 scale 即可**。**虽然可以运行,但是解密结果是错误的**!故继续尝试:**coeff_modulus = 140 (50 + 20 + 20 + 50) bits 仍然错误!**

一直尝试到:**coeff_modulus = **160 (60 + 20 + 20 + 60) bits(最大只到60):

发现能解密了,但是精度根本不够,不过探究出了报错的原因。


4.2 提高精度的参数设置

接下来拉高精度:
** coeff_modulus = 200 (60 + 40 + 40 + 60),报错 scale out of bounds** !证明刚才的结论是正确的,即模数链底层比较特殊,这里的第一位 60 达不到要求
那么按照要求极限设置:** coeff_modulus = 178 (60 + 29 + 29 + 60) bits (29+29 < 60):**

果然没有报错,但是解密结果感人。当然此时也能总结出规律,加模数链


scale = 30,coeff_modulus = 190 (50 + 30 + 30 + 30 + 50) bits

    第三位的精确值是61.918288 ,后面第一位的精确值是:126715.7763,看得出来也差不多,但是再往后就不行了。故这种配置就是针对这个算法最优的。

有趣的是,当我继续想拉高精度的时候,即尝试了:

  1. scale = 40,coeff_modulus = 200 (40 + 40 + 40 + 40 + 40) bits :****报错!scale out of bounds
  2. scale = 40,coeff_modulus = 218 (49 + 40 + 40 + 40 + 49) bits: 拉满了218,不报错,但是精度不如上面的 190 (50 + 30 + 30 + 30 + 50) bits

故模数链得够长,而且第一个数和最后一个数也得够大!


4.3 本例总结

上面进行了 三次乘法 和 两次 Rescale:
如果模数链只给四位数(最后的为密钥的特殊素数,其他三位是给密文用的
),需要注意最后的大小问题,不然会报错 scale out of bounds(补充:此时不能再进行第三次 Rescale,会报错)此时精度也不理想。
如果模数链给五位数,则精度会提升,但是第一和最后位的大小要尽量大于其他。[ 即:190 (50 + 30 + 30 + 30 + 50) 的精度要大于 218 (49 + 40 + 40 + 40 + 49) ]。(此时可以进行第三次Rescale 再解密,但是我尝试过精度差不多,所以意义可能不大)
另外,
上面提到的精度,均与数字大小有关,并不是指可以精确到的位数


五、本篇总结

本篇探究了如下情况:

  1. 连乘时,要注意的参数匹配问题;

  2. Coeff_modulus 的中间数 和 Scale 的关系;

  3. Scale 对精度的影响;

  4. 模数链长度对乘法深度,准确说对 Rescale 次数 的影响;

  5. 报错 scale out of bounds 的具体情况;

  6. 想提高精度,比较合适的参数设置;

     叠个甲:很多结果都是测试得出的结论,不一定准确,毕竟不是看源码分析而来的。但是具有一定的参考价值,也算是帮大家踩过坑了。
      下一篇打算继续探究 **密文深度乘法情况 和 参数设置对内存占用的影响**。
    

本文转载自: https://blog.csdn.net/WaitMrAnt/article/details/141091136
版权归原作者 Mr.Ants 所有, 如有侵权,请联系我们删除。

“同态加密和SEAL库的介绍(九)CKKS 参数心得 1”的评论:

还没有评论