SEAL/native/examples/4_ckks_basics.cpp
print_example_banner("Example: CKKS Basics");
在这个例子中,我们演示了一个多项式函数的求值
PI * x^3 + 0.4*x + 1
在加密的浮点输入数据x为一组4096等距点在区间[0,1]。这个例子演示了CKKS方案的许多主要特性,但也说明了使用它的挑战。
我们首先建立CKKS方案。
EncryptionParameters parms(scheme_type::CKKS);
我们在’
2_encoders.cpp中看到CKKS中的乘法导致了规模在密文中成长。任何密文的尺度都不能太接近coeff_modulus的总大小,否则密文就会耗尽空间来存储放大的明文。CKKS方案提供了一个“rescale”功能,可以减少scale,并稳定scale扩展。Rescaling是一种模转换操作(回忆一下3_levels.cpp’)。在转换模量时,它会从coeff_modulo中移除最后一个素数,但作为副作用,它会通过移除的素数缩小了密文。通常,我们想要对尺度如何变化有完美的控制,这就是为什么在CKKS方案中,为coeff_moduls使用精心挑选的素数更为常见。
更精确地说,假设CKKS密文中的scale为S,则当前coeff_modulus(密文)中的最后一个素数是P. Rescaling下一层将scale更改为S/P,并从coeff_modulo中删除素数P,和模量切换中通常的做法一样。质数的数量限制了重新计算的次数,从而限制了计算的乘法深度。
自由选择初始规模是有可能的。一个好的策略是在coeff_modul_be中设置初始标度S和素数P_i彼此非常接近。如果密文在乘法之前有scale S,乘法之后是S^2,rescaling之后是 S^2 / P_i。如果所有P_i接近S,然后S^2/P_i又接近S。通过这种方法,我们可以在整个计算过程中使scale稳定在接近S。一般来说,对于深度D的电路,我们需要缩放D倍,即我们需要把D 素数从系数模数中去掉。一旦在coeff_modules中只剩下一个素数,剩下的素数必须比S大几个位,以保留明文的小数点前值。
因此,一般好的策略是选择CKKS方案的参数如下:
(1)选择一个60位素数作为coeff_modules中的第一个素数。这将在解密时提供最高的精度;
(2)选择另一个60位素数作为coeff_modules的最后一个元素,因为它将被用作特殊素数,并且应该与其他素数中最大的素数一样大;
(3)选择中间素数,彼此之间相近。
我们使用CoefModulusf::Create
来生成适当大小的素数。注意,我们的coeff_modules是200位,这低于我们的多项式模数: coeff_modulus_degree::MaxBitCount(8192
)返回218`。
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, 40, 60 }));
我们选择初始规模为2^40。在最后一层,小数点前留下60-40=20位精度,小数点后留下足够(大约10-20位)的精度。因为我们的中间质数是40位(事实上,它们非常接近2的40),所以我们可以像上面描述的那样实现规模稳定。
double scale = pow(2.0, 40);
auto context = SEALContext::Create(parms);
print_parameters(context);
cout << endl;
KeyGenerator keygen(context);
auto public_key = keygen.public_key();
auto secret_key = keygen.secret_key();
auto relin_keys = keygen.relin_keys();
Encryptor encryptor(context, public_key);
Evaluator evaluator(context);
Decryptor decryptor(context, secret_key);
CKKSEncoder encoder(context);
size_t slot_count = encoder.slot_count();
cout << "Number of slots: " << slot_count << endl;
vector<double> input;
input.reserve(slot_count);
double curr_point = 0;
double step_size = 1.0 / (static_cast<double>(slot_count) - 1);
for (size_t i = 0; i < slot_count; i++, curr_point += step_size)
{
input.push_back(curr_point);
}
cout << "Input vector: " << endl;
print_vector(input, 3, 7);
cout << "Evaluating polynomial PI*x^3 + 0.4x + 1 ..." << endl;
/*
We create plaintexts for PI, 0.4, and 1 using an overload of CKKSEncoder::encode
that encodes the given floating-point value to every slot in the vector.
*/
Plaintext plain_coeff3, plain_coeff1, plain_coeff0;
encoder.encode(3.14159265, scale, plain_coeff3);
encoder.encode(0.4, scale, plain_coeff1);
encoder.encode(1.0, scale, plain_coeff0);
Plaintext x_plain;
print_line(__LINE__);
cout << "Encode input vectors." << endl;
encoder.encode(input, scale, x_plain);
Ciphertext x1_encrypted;
encryptor.encrypt(x_plain, x1_encrypted);
/*
To compute x^3 we first compute x^2 and relinearize. However, the scale has
now grown to 2^80.
*/
Ciphertext x3_encrypted;
print_line(__LINE__);
cout << "Compute x^2 and relinearize:" << endl;
evaluator.square(x1_encrypted, x3_encrypted);
evaluator.relinearize_inplace(x3_encrypted, relin_keys);
cout << " + Scale of x^2 before rescale: " << log2(x3_encrypted.scale())
<< " bits" << endl;
现在rescale;除了模数切换之外,规模还减少了一个因子,该因子等于被切换掉的素数(40位素数)。因此,新的scale应该接近2的40次方。但是注意,scale不等于2^40:这是因为40位素数只接近2的40次方。
print_line(__LINE__);
cout << "Rescale x^2." << endl;
evaluator.rescale_to_next_inplace(x3_encrypted);
cout << " + Scale of x^2 after rescale: " << log2(x3_encrypted.scale())
<< " bits" << endl;
现在x3_encryption与x1_encryption处于不同的级别,这阻止我们将它们相乘来计算x^3。 我们可以简单地将
x1_encrypted切换到模数转换链中的下一个参数。然而,由于我们仍然需要将x^3 项 乘以PI(plain_coeff3),
所以我们先计算PI*x,然后将它与x^2相乘,得到PI *x^3。最后,我们计算PI *x 并将其从2^80缩放到接近
2^40的值。
print_line(__LINE__);
cout << "Compute and rescale PI*x." << endl;
Ciphertext x1_encrypted_coeff3;
evaluator.multiply_plain(x1_encrypted, plain_coeff3, x1_encrypted_coeff3);
cout << " + Scale of PI*x before rescale: " << log2(x1_encrypted_coeff3.scale())
<< " bits" << endl;
evaluator.rescale_to_next_inplace(x1_encrypted_coeff3);
cout << " + Scale of PI*x after rescale: " << log2(x1_encrypted_coeff3.scale())
<< " bits" << endl;
因为x3_encrypted和x1_encrypted_coeff3具有相同的scale和使用相同的加密参数,所以我们可以将它们相乘。我们将结果写入x3_encrypted,relinearize和rescale。再次注意scale是接近2^40,但不完全是2的40次方due to yet another scaling by a prime。我们已经到了模切换链的最后一个level。
print_line(__LINE__);
cout << "Compute, relinearize, and rescale (PI*x)*x^2." << endl;
evaluator.multiply_inplace(x3_encrypted, x1_encrypted_coeff3);
evaluator.relinearize_inplace(x3_encrypted, relin_keys);
cout << " + Scale of PI*x^3 before rescale: " << log2(x3_encrypted.scale())
<< " bits" << endl;
evaluator.rescale_to_next_inplace(x3_encrypted);
cout << " + Scale of PI*x^3 after rescale: " << log2(x3_encrypted.scale())
<< " bits" << endl;
接下来计算1次项。所有这些都需要一个带有plain_coeff1的multiply_plain。我们使用结果覆盖x1_encryption。
print_line(__LINE__);
cout << "Compute and rescale 0.4*x." << endl;
evaluator.multiply_plain_inplace(x1_encrypted, plain_coeff1);
cout << " + Scale of 0.4*x before rescale: " << log2(x1_encrypted.scale())
<< " bits" << endl;
evaluator.rescale_to_next_inplace(x1_encrypted);
cout << " + Scale of 0.4*x after rescale: " << log2(x1_encrypted.scale())
<< " bits" << endl;
现在我们希望计算所有三项的和。但是,有一个严重的问题:这三个术语所使用的加密参数是不同的,这是由于模量从rescaling而来的。
加密的加法和减法要求输入的level相同,并且加密参数(parms_id)匹配。如果不匹配,Evaluate将抛出异常。
cout << endl;
print_line(__LINE__);
cout << "Parameters used by all three terms are different." << endl;
cout << " + Modulus chain index for x3_encrypted: "
<< context->get_context_data(x3_encrypted.parms_id())->chain_index() << endl;
cout << " + Modulus chain index for x1_encrypted: "
<< context->get_context_data(x1_encrypted.parms_id())->chain_index() << endl;
cout << " + Modulus chain index for plain_coeff0: "
<< context->get_context_data(plain_coeff0.parms_id())->chain_index() << endl;
cout << endl;
让我们仔细考虑一下在这一点上的scales是多少。我们把系数中的质数表示为P_0, P_1, P_2, P_3,按这个顺序。P_3作为特殊的模量,不涉及重调scaling。经过以上规模的计算,密文的scale为:
- Product x^2 has scale 2^80 and is at level 2; - Product PI*x has scale 2^80 and is at level 2; - We rescaled both down to scale 2^80/P_2 and level 1; - Product PI*x^3 has scale (2^80/P_2)^2; - We rescaled it down to scale (2^80/P_2)^2/P_1 and level 0; - Product 0.4*x has scale 2^80; - We rescaled it down to scale 2^80/P_2 and level 1; - The contant term 1 has scale 2^40 and is at level 2.
虽然这三项的比例大约是2^40,但它们的确切值是不同的,因此不能相加。
print_line(__LINE__);
cout << "The exact scales of all three terms are different:" << endl;
ios old_fmt(nullptr);
old_fmt.copyfmt(cout);
cout << fixed << setprecision(10);
cout << " + Exact scale in PI*x^3: " << x3_encrypted.scale() << endl;
cout << " + Exact scale in 0.4*x: " << x1_encrypted.scale() << endl;
cout << " + Exact scale in 1: " << plain_coeff0.scale() << endl;
cout << endl;
cout.copyfmt(old_fmt);
> 有很多方法可以解决这个问题。因为P_2和P_1非常接近2的40次方,我们可以简单地“撒谎”到Microsoft SEAL,
并设置相同的比例。例如,将*x^3的比例改为2^40就意味着将*x^3的值乘以2^120/(P_2^2*P_1),这非常接近于1。
>
> 这应该不会导致任何明显的错误。
>
> 另一个选项是用scale 2^80/P_2对1进行编码,用0.4*x进行乘法运算,最后进行缩放。在这种情况下,我们还需要
确保使用适当的加密参数(parms_id)对1进行编码。
>
> 在本例中,我们将使用第一***种(最简单的)方法***,简单地将PI*x^3和0.4*x的范围更改为2^40。
print_line(__LINE__);
cout << "Normalize scales to 2^40." << endl;
x3_encrypted.scale() = pow(2.0, 40);
x1_encrypted.scale() = pow(2.0, 40);
我们还有一个加密参数不匹配的问题。这是很容易修复,使用传统的模切换(没有重新缩放)。CKKS支持模切换,就像BFV方案一样,允许我们在根本不需要时切换部分系数模量。
print_line(__LINE__);
cout << "Normalize encryption parameters to the lowest level." << endl;
parms_id_type last_parms_id = x3_encrypted.parms_id();
evaluator.mod_switch_to_inplace(x1_encrypted, last_parms_id);
evaluator.mod_switch_to_inplace(plain_coeff0, last_parms_id);
/*三个密文兼容,可加了
All three ciphertexts are now compatible and can be added.
*/
print_line(__LINE__);
cout << "Compute PI*x^3 + 0.4*x + 1." << endl;
Ciphertext encrypted_result;
evaluator.add(x3_encrypted, x1_encrypted, encrypted_result);
//计算结果存入 encrypted_result
evaluator.add_plain_inplace(encrypted_result, plain_coeff0);
/*
First print the true result.
*/
Plaintext plain_result; //明文结果
print_line(__LINE__);
cout << "Decrypt and decode PI*x^3 + 0.4x + 1." << endl;
cout << " + Expected result 希望打印出:" << endl;
vector<double> true_result;
for (size_t i = 0; i < input.size(); i++)
{
double x = input[i];
true_result.push_back((3.14159265 * x * x + 0.4)* x + 1);
}
print_vector(true_result, 3, 7);
/* 真正解密、解码、打印 看看如何
Decrypt, decode, and print the result.
*/
decryptor.decrypt(encrypted_result, plain_result); //解密
vector<double> result;
encoder.decode(plain_result, result); //解码
cout << " + Computed result ...... Correct." << endl;
print_vector(result, 3, 7);
虽然我们没有在这些例子中显示任何复数的计算,但是CKKSEncoder可以让我们很容易地做到这一点。复数的加法和乘法就像人们所期望的那样。