該文章翻譯自CKKS EXPLAINED, PART 2: FULL ENCODING AND DECODING,主要介紹CKKS方案中是如何編碼和解碼的(如何將複數向量轉成整數多項式,以及如何求逆運算)
介紹
在前一篇文章《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])
我希望你們喜歡這篇關於將複數向量編碼成多項式進行同態加密的小介紹。我們將在下面的文章中進一步深入探討這一點,敬請期待!