CKKS Part2: CKKS的编码和解码
该文章翻译自CKKS EXPLAINED, PART 2: FULL ENCODING AND DECODING,主要介绍CKKS方案中是如何编码和解码的(如何将复数向量转成整数多项式,以及如何求逆运算)
介绍
在前一篇文章《CKKS:第1部分,普通编码和解码》中,我们了解到,要在CKKS方案中实现加密复数向量的计算,必须首先构建一个编码和解码,将复数向量转换为多项式。
这个编/解码步骤是必要的,因为加密、解密和其他机制在多项式环上工作。因此,有必要找到一种将复数向量转换成多项式的方法。
我们还了解到,通过使用标准嵌入
因此,在本文中,我们将探讨如何实现原始论文《Homomorphic Encryption for Arithmetic of Approximate Numbers》中使用的编码和解码,这将是我们从头开始实现CKK的第一步。
CKKS编码
与前一篇文章的不同之处在于,编码多项式的明文空间现在是
为了解决这个问题,让我们来看看标准嵌入
因为在
像上一章的M=8:
从上面的照片看,
这个操作需要将(
现在我们可以从
我们面临的一个问题是,我们不能直接使用
这一段不太理解。
这个细节很重要,因为这意味着我们必须找到一种在
这里使用的单位根和普通编码中使用的不同。
想法很简单,有一个正交基
因为基要么是正交的,要么不是正交的,所以
最后,一旦我们有了
一旦我们有了映射关系
最后一个细节:因为舍入可能会破坏一些重要的数字,我们实际上需要在编码中乘以
要了解其工作原理,请假设您想要将
所以最后的编码过程是:
1、以
2、利用映射
3、将其乘以
4、映射到
5、使用
解码过程要简单得多,从多项式m(X)我们只得到
实现
现在我们终于看到了完整的CKKS编码和解码是如何工作的,让我们来实现它吧!我们将使用之前用于Vanilla编码器和解码器的代码。
强烈推荐源代码:这里。
(1)这是上一篇介绍的普通编码和解密:
import numpy as np
from numpy.polynomial import Polynomial
class CKKSEncoder:
"""Basic CKKS encoder to encode complex vectors into polynomials."""
def __init__(self, M: int):
"""Initialization of the encoder for M a power of 2.
xi, which is an M-th root of unity will, be used as a basis for our computations.
"""
self.xi = np.exp(2 * np.pi * 1j / M)
self.M = M
@staticmethod
def vandermonde(xi: np.complex128, M: int) -> np.array:
"""Computes the Vandermonde matrix from a m-th root of unity."""
N = M //2
matrix = []
# We will generate each row of the matrix
for i in range(N):
# For each row we select a different root
root = xi ** (2 * i + 1)
row = []
# Then we store its powers
for j in range(N):
row.append(root ** j)
matrix.append(row)
return matrix
def sigma_inverse(self, b: np.array) -> Polynomial:
"""Encodes the vector b in a polynomial using an M-th root of unity."""
# First we create the Vandermonde matrix
A = CKKSEncoder.vandermonde(self.xi, M)
# Then we solve the system
coeffs = np.linalg.solve(A, b)
# Finally we output the polynomial
p = Polynomial(coeffs)
return p
def sigma(self, p: Polynomial) -> np.array:
"""Decodes a polynomial by applying it to the M-th roots of unity."""
outputs = []
N = self.M //2
# We simply apply the polynomial on the roots
for i in range(N):
root = self.xi ** (2 * i + 1)
output = p(root)
outputs.append(output)
return np.array(outputs)
在notebook 环境中,在上面类的基础上重构。我们不需要每次添加或更改方法时都重新定义类,而只需使用Fastai的
(2)引入包
!pip3 install fastcore
from fastcore.foundation import patch_to
(3)
@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])
M = 8
# 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])
(4)生成单位根的次幂
@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)
encoder.sigma_R_basis
输出:
下面测试
# 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
输出:
p = encoder.sigma_inverse(b)
p
输出:
可以见还是复数多项式。
(5)重新定义
compute_basis_coordinates(self, z):计算
round_coordinates(coordinates):计算出小数部分
coordinate_wise_random_rounding(coordinates):
@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) #计算z_i
rounded_coordinates = coordinate_wise_random_rounding(coordinates) #舍入取整
y = np.matmul(self.sigma_R_basis.T, rounded_coordinates) #范德蒙矩阵*向量
return y
encoder = CKKSEncoder(M)
最后,因为在舍入步骤中可能会损失精度,所以我们使用刻度参数
(6)加入精度控制
@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使用的完整编码器
(7)引用fastcore的完整代码
#引入包
!pip3 install fastcore
import numpy as np
from numpy.polynomial import Polynomial
from fastcore.foundation import patch_to
#CKKSEncoder类
class CKKSEncoder:
"""Basic CKKS encoder to encode complex vectors into polynomials."""
def __init__(self, M: int):
"""Initialization of the encoder for M a power of 2.
xi, which is an M-th root of unity will, be used as a basis for our computations.
"""
self.xi = np.exp(2 * np.pi * 1j / M)
self.M = M
@staticmethod
def vandermonde(xi: np.complex128, M: int) -> np.array:
"""Computes the Vandermonde matrix from a m-th root of unity."""
N = M //2
matrix = []
# We will generate each row of the matrix
for i in range(N):
# For each row we select a different root
root = xi ** (2 * i + 1)
row = []
# Then we store its powers
for j in range(N):
row.append(root ** j)
matrix.append(row)
return matrix
def sigma_inverse(self, b: np.array) -> Polynomial:
"""Encodes the vector b in a polynomial using an M-th root of unity."""
# First we create the Vandermonde matrix
A = CKKSEncoder.vandermonde(self.xi, M)
# Then we solve the system
coeffs = np.linalg.solve(A, b)
# Finally we output the polynomial
p = Polynomial(coeffs)
return p
def sigma(self, p: Polynomial) -> np.array:
"""Decodes a polynomial by applying it to the M-th roots of unity."""
outputs = []
N = self.M //2
# We simply apply the polynomial on the roots
for i in range(N):
root = self.xi ** (2 * i + 1)
output = p(root)
outputs.append(output)
return np.array(outputs)
#\pi 映射和 \pi逆映射
@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])
#生成本原根
@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 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)
print("coordinates",coordinates)
rounded_coordinates = coordinate_wise_random_rounding(coordinates)
print("rounded_coordinates",rounded_coordinates)
y = np.matmul(self.sigma_R_basis.T, rounded_coordinates)
return y
#编码和解码
@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)
print("pi_z:",pi_z)
scaled_pi_z = self.scale * pi_z
print("scaled_pi_z:",scaled_pi_z)
rounded_scale_pi_zi = self.sigma_R_discretization(scaled_pi_z)
print("rounded_scale_pi_zi:",rounded_scale_pi_zi)
p = self.sigma_inverse(rounded_scale_pi_zi)
print("p:",p)
# We round it afterwards due to numerical imprecision
coef = np.round(np.real(p.coef)).astype(int)
print("coef:",coef)
p = Polynomial(coef)
print("p:",p)
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
print("rescaled_p:",rescaled_p)
z = self.sigma(rescaled_p)
print("z:",z)
pi_z = self.pi(z)
return pi_z
scale = 64
M=8
encoder = CKKSEncoder(M, scale)
#测试
z = np.array([3 +4j, 2 - 1j])
print("明文:",z)
p = encoder.encode(z)
print("编码后:",p)
d = encoder.decode(p)
print("解码后:",d)
输出:
明文: [3.+4.j 2.-1.j]
pi_z: [3.+4.j 2.-1.j 2.+1.j 3.-4.j]
scaled_pi_z: [192.+256.j 128. -64.j 128. +64.j 192.-256.j]
coordinates: [160. 90.50966799 160. 45.254834 ]
rounded_coordinates: [160, 90, 160, 46]
rounded_scale_pi_zi: [191.11269837+256.16652224j 128.88730163 -63.83347776j
128.88730163 +63.83347776j 191.11269837-256.16652224j]
p: (160-2.842170943040401e-14j) + (90.00000000000001+0j)·x¹ +(160+8.881784197001252e-16j)·x² +
(46.000000000000014+1.0658141036401503e-14j)·x³
coef: [160 90 160 46]
p: 160.0 + 90.0·x¹ + 160.0·x² + 46.0·x³
编码后: 160.0 + 90.0·x¹ + 160.0·x² + 46.0·x³
rescaled_p: 2.5 + 1.40625·x¹ + 2.5·x² + 0.71875·x³
z: [2.98613591+4.00260191j 2.01386409-0.99739809j 2.01386409+0.99739809j
2.98613591-4.00260191j]
解码后: [2.98613591+4.00260191j 2.01386409-0.99739809j]
这里可以看到:在sigma_R_discretization中就求出了整数多项式的系数
(8)不引用的完整代码
from numpy.polynomial import Polynomial
import numpy as np
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
class CKKSEncoder:
"""Basic CKKS encoder to encode complex vectors into polynomials."""
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
@staticmethod
def vandermonde(xi: np.complex128, M: int) -> np.array:
"""Computes the Vandermonde matrix from a m-th root of unity."""
N = M //2
matrix = []
# We will generate each row of the matrix
for i in range(N):
# For each row we select a different root
root = xi ** (2 * i + 1)
row = []
# Then we store its powers
for j in range(N):
row.append(root ** j)
matrix.append(row)
return matrix
def sigma_inverse(self, b: np.array) -> Polynomial:
"""Encodes the vector b in a polynomial using an M-th root of unity."""
# First we create the Vandermonde matrix
A = CKKSEncoder.vandermonde(self.xi, M)
# Then we solve the system
coeffs = np.linalg.solve(A, b)
# Finally we output the polynomial
p = Polynomial(coeffs)
return p
def sigma(self, p: Polynomial) -> np.array:
"""Decodes a polynomial by applying it to the M-th roots of unity."""
outputs = []
N = self.M //2
# We simply apply the polynomial on the roots
for i in range(N):
root = self.xi ** (2 * i + 1)
output = p(root)
outputs.append(output)
return np.array(outputs)
def pi(self, z: np.array) -> np.array:
"""Projects a vector of H into C^{N/2}."""
N = self.M // 4
return z[:N]
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])
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
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 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
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
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
#测试
z = np.array([3 +4j, 2 - 1j])
print("明文:",z)
p = encoder.encode(z)
print("编码后:",p)
d = encoder.decode(p)
print("解密后:",d)
输出:
明文: [3.+4.j 2.-1.j]
pi_z: [3.+4.j 2.-1.j 2.+1.j 3.-4.j]
scaled_pi_z: [192.+256.j 128. -64.j 128. +64.j 192.-256.j]
coordinates [160. 90.50966799 160. 45.254834 ]
rounded_coordinates [160, 91, 160, 46]
rounded_scale_pi_zi: [191.81980515+256.87362902j 128.18019485 -63.12637098j
128.18019485 +63.12637098j 191.81980515-256.87362902j]
p: (160-1.4210854715202004e-14j) + (91-7.105427357601002e-15j)·x¹ +
(160+7.993605777301127e-15j)·x² +
(46.00000000000001+1.4210854715202004e-14j)·x³
coef: [160 91 160 46]
p: 160.0 + 91.0·x¹ + 160.0·x² + 46.0·x³
编码后: 160.0 + 91.0·x¹ + 160.0·x² + 46.0·x³
rescaled_p: 2.5 + 1.421875·x¹ + 2.5·x² + 0.71875·x³
z: [2.99718446+4.01365045j 2.00281554-0.98634955j 2.00281554+0.98634955j
2.99718446-4.01365045j]
解密后: [2.99718446+4.01365045j 2.00281554-0.98634955j]
我希望你们喜欢这篇关于将复数向量编码成多项式进行同态加密的小介绍。我们将在下面的文章中进一步深入探讨这一点,敬请期待!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步