介绍
在前一篇文章《CKKS:第1部分,普通编码和解码》中,我们了解到,要在CKKS方案中实现加密复数向量的计算,必须首先构建一个编码和解码,将复数向量转换为多项式。
这个编/解码步骤是必要的,因为加密、解密和其他机制在多项式环上工作。因此,有必要找到一种将复数向量转换成多项式的方法。
我们还了解到,通过使用标准嵌入σ,即通过在\(X^N+1\)的根上计算多项式来解码多项式,我们能够在\(ℂ^N —>ℂ[X] /(X^N+1)\)。然而,因为我们希望我们的编码器输出多项式\(ℤ[X] /(X^N+1)\),为了利用多项式整数环的结构,我们需要修改前一个文章中的普通编码器,以便能够输出“右环的多项式”。(不太懂,应该是能够输出不带i的多项式)
因此,在本文中,我们将探讨如何实现原始论文《Homomorphic Encryption for Arithmetic of Approximate Numbers》中使用的编码和解码,这将是我们从头开始实现CKK的第一步。
CKKS编码
与前一篇文章的不同之处在于,编码多项式的明文空间现在是\(R=Z\left[ X \right]/X^{N}+1\)而不是\(\mbox{C}\left[ X \right]/X^{N}+1\),所以编码值多项式的系数必须是整数系数,然而当我们把一个向量编码成\(C^N\)时,我们已经了解到它的编码不一定是整数系数(有的是复数系数)。
为了解决这个问题,让我们来看看标准嵌入σ在R上的图像。
因为多项式R上是整数系数,即实数系数,我们在复数根上计算它们,其中一半是另一半的共轭项(参见上一章),我们有\(\sigma \left( R \right)\in H=z\in \mbox{C}^{N}:z_{j}=\neg z_{-j}\)。
像上一章的M=8:
从上面的照片看,\(\omega ^{1}=-\omega ^{7}\; and\; \omega ^{3}=-\omega ^{5}\),一般来说,我们用\(X^N+1\)的根计算一个多项式,对于任何多项式\(m\left( x \right)\in R,m\left( \xi ^{j} \right)=-m\left( \xi ^{-j} \right)=m\left( -\xi ^{-j} \right)\),因此,\(σ(R)\)上的任何元素实际上是在一个N/2的空间中,而不是N。因此,如果我们在CKKS中编码向量时使用大小为N/2的复数向量,我们需要通过复制其共轭根的来扩展它的另一半。
这个操作需要将\(ℍ\)投射到\(ℂ^{N/2}\),在CKKS论文中该操作称为π。请注意,这也定义了同构。
现在我们可以从\(z∈ℂ^{N/2}\)开始,用\(π^{−1}\)展开(注意π是映射,\(π^{−1}\)是扩展),我们可以得到\(π^{−1}(z)∈ℍ\).
我们面临的一个问题是,我们不能直接使用\(σ: R=ℤ[X]/(X^N+1)→σ(R)⊆ℍ\),因为ℍ 不一定在σ(R)中. σ确实定义了同构,但仅从R到σ(R). 为了证明σ(R)不等于ℍ, 你可以注意到R是可数的(??)因此σ(R) 也是,但是ℍ 不是,因为它与ℂ同构。
这个细节很重要,因为这意味着我们必须找到一种在σ(R)上的映射\(π^{−1}(z)\),为此,我们将使用一种称为“coordinate-wise random rounding, 坐标随机舍入”的技术,该技术在 A Toolkit for Ring-LWE Cryptography中定义。这种舍入技术允许将实数x舍入到⌊x⌋ 或⌊x⌋+1,我们将不深入讨论这个算法的细节,尽管我们将实现它。
想法很简单,有一个正交基ℤ:\(1,X,....,X^{N−1}\),假设σ是同构的,σ(R) 有一个正交基: \(β=(b1,b2,…,bN)=(σ(1),σ(X),...,σ(X^{N−1}))\). 因此,对于任何z∈ℍ, 我们将简单地将其投射到β上:$$z=\sum_{i=1}^{N}{z_{i}b_{i},z_{i}=\frac{<z,b_{i}>}{\left| \left| b_{i} \right| \right|^{2}}}$$
因为基要么是正交的,要么不是正交的,所以\(z_{I} =\frac{<z,b_{i}>}{\left| \left| b_{i} \right| \right|^{2}}\), 请注意,我们在这里使用的是hermitian积(厄米乘积):\(<x,y>=\sum_{i=1}^{N}{x_{i}\left( -y_{i} \right)}\), 厄米乘积给出了真正的输出,因为我们它是在ℍ上, 你可以通过计算来证明,或者注意到,你可以在ℍ 和\(ℝ^N\)之间找到同构关系,所以在ℍ上的内积将是实际的输出。
最后,一旦我们有了\(z_i\),我们只需要使用“coordinate-wise random rounding, 坐标随机舍入”将它们随机舍入到更高或更低的最接近整数。这样我们就得到了一个多项式,它的基坐标为整数\((σ(1),σ(X),...,σ(X^N)−1) )\),因此该多项式将属于σ(R) 。
一旦我们有了映射关系σ(R), 我们可以用\(σ^{−1}\)的输出,这正是我们想要的!
最后一个细节:因为舍入可能会破坏一些重要的数字,我们实际上需要在编码中乘以Δ>0,在解码中除以Δ以保持1/Δ的精度。要了解其工作原理,请假设您想要将x=1.4四舍五入,但不想将其四舍五入到最接近的整数,而是要将其四舍五入到最接近的0.25倍,以保持一定的精度。然后,您需要设置刻度Δ=4,其精度为1Δ=0.25。的确,现在当我们\(\left\lfloor \Delta x \right\rfloor=\left\lfloor 4\cdot 1.4 \right\rfloor=\left\lfloor 5.6 \right\rfloor=6\)一旦我们将其除以相同的Δ,我们得到1.5,这实际上是x=1.4的最接近倍数0.25。
所以最后的编码过程是:
以\(z∈ℂ^{N/2}\)为例
将其扩展到\(π^{-1}∈H\);
将其乘以Δ以保证精度
映射:\(\left\lfloor \Delta \pi ^{-1}\left( z \right) \right\rfloor_{\sigma \left( R \right)}\in \sigma \left( R \right)\)
使用σ:\(m\left( x \right)=\sigma ^{-1}\left( \left\lfloor \Delta \pi ^{-1}\left( z \right) \right\rfloor_{\sigma \left( R \right)} \right)\in R\)对其进行编码
解码过程要简单得多,从多项式m(X)我们只得到\(z=π∘σ(Δ^{−1}.m)\)。
实现
现在我们终于看到了完整的CKKS编码和解码是如何工作的,让我们来实现它吧!我们将使用之前用于Vanilla编码器和解码器的代码。代码可以在这里。
在本文的其余部分中,让我们重构并构建我们在上一篇文章中创建的CKKSEncoder类。在笔记本电脑环境中,我们不需要每次添加或更改方法时都重新定义类,而只需使用Fastai的fastcore包中的patch_to。这使我们能够对已经定义的对象进行修补。使用patch_to纯粹是为了方便,您可以使用添加的方法在每个单元重新定义CKKSEncoder。
# !pip3 install fastcore
from fastcore.foundation import patch_to
@patch_to(CKKSEncoder)
def pi(self, z: np.array) -> np.array:
"""Projects a vector of H into C^{N/2}."""
N = self.M // 4
return z[:N]
@patch_to(CKKSEncoder)
def pi_inverse(self, z: np.array) -> np.array:
"""Expands a vector of C^{N/2} by expanding it with its
complex conjugate."""
z_conjugate = z[::-1]
z_conjugate = [np.conjugate(x) for x in z_conjugate]
return np.concatenate([z, z_conjugate])
# We can now initialize our encoder with the added methods
encoder = CKKSEncoder(M)
z = np.array([0,1])
encoder.pi_inverse(z)
输出:array([0, 1, 1, 0])
@patch_to(CKKSEncoder)
def create_sigma_R_basis(self):
"""Creates the basis (sigma(1), sigma(X), ..., sigma(X** N-1))."""
self.sigma_R_basis = np.array(self.vandermonde(self.xi, self.M)).T
@patch_to(CKKSEncoder)
def __init__(self, M):
"""Initialize with the basis"""
self.xi = np.exp(2 * np.pi * 1j / M)
self.M = M
self.create_sigma_R_basis()
encoder = CKKSEncoder(M)
我们现在可以看看基:\(\sigma \left( 1 \right),\sigma \left( X \right),\sigma \left( X^{2} \right),\sigma \left( X^{3} \right)\)
encoder.sigma_R_basis
\(array([[ 1.00000000e+00+0.j, 1.00000000e+00+0.j,1.00000000e+00+0.j, 1.00000000e+00+0.j],[ 7.07106781e-01+0.70710678j, -7.07106781e-01+0.70710678j, -7.07106781e-01-0.70710678j, 7.07106781e-01-0.70710678j],[ 2.22044605e-16+1.j, -4.44089210e-16-1.j, 1.11022302e-15+1.j, -1.38777878e-15-1.j], [-7.07106781e-01+0.70710678j, 7.07106781e-01+0.70710678j,7.07106781e-01-0.70710678j, -7.07106781e-01-0.70710678j]])\)
这里我们将检查ℤ(σ(1)、σ(X)、σ(X2)、σ(X3))的元素是否被编码为整数多项式。
# Here we simply take a vector whose coordinates are (1,1,1,1) in the lattice basis
coordinates = [1,1,1,1]
b = np.matmul(encoder.sigma_R_basis.T, coordinates)
b
\(array([1.+2.41421356j, 1.+0.41421356j, 1.-0.41421356j, 1.-2.41421356j])\)
现在我们可以检查它是否编码为整数多项式。
p = encoder.sigma_inverse(b)
p
\(x↦(1+2.220446049250313e-16j)+((1+0j))x+((0.9999999999999998+2.7755575615628716e-17j))x^2+((1+2.220446049250313e-16j))x^3\)
@patch_to(CKKSEncoder)
def compute_basis_coordinates(self, z):
"""Computes the coordinates of a vector with respect to the orthogonal lattice basis."""
output = np.array([np.real(np.vdot(z, b) / np.vdot(b,b)) for b in self.sigma_R_basis])
return output
def round_coordinates(coordinates):
"""Gives the integral rest."""
coordinates = coordinates - np.floor(coordinates)
return coordinates
def coordinate_wise_random_rounding(coordinates):
"""Rounds coordinates randonmly."""
r = round_coordinates(coordinates)
f = np.array([np.random.choice([c, c-1], 1, p=[1-c, c]) for c in r]).reshape(-1)
rounded_coordinates = coordinates - f
rounded_coordinates = [int(coeff) for coeff in rounded_coordinates]
return rounded_coordinates
@patch_to(CKKSEncoder)
def sigma_R_discretization(self, z):
"""Projects a vector on the lattice using coordinate wise random rounding."""
coordinates = self.compute_basis_coordinates(z)
rounded_coordinates = coordinate_wise_random_rounding(coordinates)
y = np.matmul(self.sigma_R_basis.T, rounded_coordinates)
return y
encoder = CKKSEncoder(M)
最后,因为在舍入步骤中可能会损失精度,所以我们使用刻度参数Δ来达到固定的精度水平。
@patch_to(CKKSEncoder)
def __init__(self, M:int, scale:float):
"""Initializes with scale."""
self.xi = np.exp(2 * np.pi * 1j / M)
self.M = M
self.create_sigma_R_basis()
self.scale = scale
@patch_to(CKKSEncoder)
def encode(self, z: np.array) -> Polynomial:
"""Encodes a vector by expanding it first to H,
scale it, project it on the lattice of sigma(R), and performs
sigma inverse.
"""
pi_z = self.pi_inverse(z)
scaled_pi_z = self.scale * pi_z
rounded_scale_pi_zi = self.sigma_R_discretization(scaled_pi_z)
p = self.sigma_inverse(rounded_scale_pi_zi)
# We round it afterwards due to numerical imprecision
coef = np.round(np.real(p.coef)).astype(int)
p = Polynomial(coef)
return p
@patch_to(CKKSEncoder)
def decode(self, p: Polynomial) -> np.array:
"""Decodes a polynomial by removing the scale,
evaluating on the roots, and project it on C^(N/2)"""
rescaled_p = p / self.scale
z = self.sigma(rescaled_p)
pi_z = self.pi(z)
return pi_z
scale = 64
encoder = CKKSEncoder(M, scale)
我们现在可以立刻看到它,CKKS使用的完整编码器:
z = np.array([3 +4j, 2 - 1j])
z
输出:array([3.+4.j, 2.-1.j])
现在我们有一个整数多项式作为我们的编码。
p = encoder.encode(z)
p
\(x↦160.0+90.0x+160.0x^2+45.0x^3\)
而且它实际上解码得很好!
encoder.decode(p)
array([2.99718446+3.99155337j, 2.00281554-1.00844663j])
我希望你们喜欢这篇关于将复数向量编码成多项式进行同态加密的小介绍。我们将在下面的文章中进一步深入探讨这一点,敬请期待!