同态加密和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确实这超过了 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. 想提高精度,比较合适的参数设置;

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Mr.Ants

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

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

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

打赏作者

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

抵扣说明:

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

余额充值