写在前面:
前面几篇有官方的说明和示例做支撑,相信能给大家比较多的参考价值。但是由于没能对同态加密有更深入的了解,所以在我具体使用的时候出现各种问题。本篇是针对这些问题做的一些测试,由结论产生的了些个人的推测,希望对大家有帮助。
一、引入和参数配置
上篇性能测试中,官方提到了一句:“不推荐在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 只是近似,确切值不同),故会报错说不匹配。
这里修改匹配就行:
- 把 明文 的 param_id 向下调,和密文一样即可;但是不能调密文的,因为不能向上;
- 模数互相调整都行,因为密文也是近似于 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 应该是 ,确实这超过了 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,看得出来也差不多,但是再往后就不行了。故这种配置就是针对这个算法最优的。
有趣的是,当我继续想拉高精度的时候,即尝试了:
- scale = 40,coeff_modulus = 200 (40 + 40 + 40 + 40 + 40) bits :
报错!scale out of bounds ! - 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 再解密,但是我尝试过精度差不多,所以意义可能不大)
另外,上面提到的精度,均与数字大小有关,并不是指可以精确到的位数。
五、本篇总结
本篇探究了如下情况:
- 连乘时,要注意的参数匹配问题;
- Coeff_modulus 的中间数 和 Scale 的关系;
- Scale 对精度的影响;
- 模数链长度对乘法深度,准确说对 Rescale 次数 的影响;
- 报错 scale out of bounds 的具体情况;
- 想提高精度,比较合适的参数设置;
叠个甲:很多结果都是测试得出的结论,不一定准确,毕竟不是看源码分析而来的。但是具有一定的参考价值,也算是帮大家踩过坑了。
下一篇打算继续探究 密文深度乘法情况 和 参数设置对内存占用的影响。