CKKS Part1:普通编码和解码
这篇文章,翻译于:【CKKS EXPLAINED: PART 1, VANILLA ENCODING AND DECODING】
主要介绍为CKKS中编码/解码做铺垫,讲一些基础知识
介绍
同态加密是一个很有前途的领域,它允许对密文进行计算。下面这篇优秀的文章《什么是同态加密》对同态加密是什么以及这一研究领域的利害关系进行了广泛的解释。
在本系列文章中,我们将深入研究Cheon-Kim-Song(CKKS)方案,该方案首次在论文《Homomorphic Encryption for Arithmetic of Approximate Numbers》中讨论。CKKS允许我们对复数向量(也就是实数)进行计算。我们的想法是,我们将用Python实现CKKS,然后通过使用这些加密原语,我们可以探索如何执行复杂的操作,如线性回归、神经网络等。
上图提供了CKKS的主要流程。我们可以看到,消息m是一个向量,我们希望对其执行某些计算,它首先被编码为明文多项式$p(X)$,然后使用公钥进行加密。
CKKS使用多项式,因为与向量计算相比,它们在安全性和效率之间提供了良好的折衷。【也就是使用RLWE和LWE之间区别,RLWE更加安全和高效】
一旦消息m被加密为c(一对多项式),CKKS就提供了几个可以对其执行的操作,例如加法、乘法和旋转。
如果我们用f表示函数,f是同态运算【加法和乘法】的组合,那么用私钥去解密 c'=f(c),然后解码,我们将得到m=f(m)。
实现同态加密方案的核心思想是在编码、解码、加密和解密上具有同态属性,这样,对密文的操作将被正确地解密和解码,并提供输出,就像直接对明文进行操作一样。
因此,在本文中,我们将看到如何实现编码和解码,在后面的文章中,我们将继续实现加密和解密,以获得同态加密方案。
预备知识
建议掌握线性代数和环理论的基本知识,以便更好地理解CKKS是如何实现的。您可以通过以下链接了解这些主题:
- 线性代数导论为线性代数提供了良好的基础。
- 环理论(数学113)是学习环理论的好资源。
具体到本文中,我们将依赖以下概念:
- 分圆多项式(Cyclotomic polynomials)是一种具有优良性质的多项式,当用作多项式模时,它具有高效的计算。
- 用于编码和解码的标准嵌入(canonical embedding)。它们具有很好的同构性质,即向量和多项式之间的一对一同态对应。
- 范德蒙矩阵(Vandermonde matrices)是一类特殊的矩阵,我们将用它来求标准嵌入的逆。
如果你想运行该项目的代码,你可以在这里找到它。
CKKS编码
CKKS利用整型多项式环的丰富结构实现其明文和密文空间。尽管如此,数据更多地以向量的形式出现,而不是以多项式的形式出现。
因此,我们需要将输入的复数向量\(z\in \mbox{C}^{\frac{N}{2}}\)编码成一个多项式\(m\left( X \right)=\frac{Z\left[ X \right]}{X^{N}+1}\)。
用N表示多项式模的次数,其中N是二的次幂。把\(\Phi _{M}=X^{N}+1\)(其中M=2N)叫做M次分圆多项式。明文空间是多项式环\(R=\frac{Z\left[ X \right]}{X^{N}+1}\)。用\(\xi _{M}=e^{\frac{2i\pi }{M}}\)表示M次单位根。
为了理解如何将向量编码为多项式,以及对多项式执行计算是如何反映在向量上的,我们将首先用一个普通的示例进行实验,我们将一个复数向量\(z\in \mbox{C}^N\)编码为一个复数多项式\(m\left( X \right)=\frac{C\left[ X \right]}{X^{N}+1}\)。
然后介绍CKKS编码:将一个复数向量\(z\in \mbox{C}^{N/2}\)编码为一个整数多项式\(m\left( X \right)=\frac{Z\left[ X \right]}{X^{N}+1}\)。
普通编码
这里我们将简单介绍将一个复数向量\(z\in \mbox{C}^N\)编码为一个复数多项式\(m\left( X \right)=\frac{C\left[ X \right]}{X^{N}+1}\)的情况。
为此,我们使用标准嵌入\(\sigma :\frac{\mbox{C}\left[ X \right]}{X^{N}+1}—>\mbox{C}^{N}\),对其进行解码和编码。
想法很简单,将多项式\(m(X)\)解码为一个向量\(z\) ,即用分圆多项式\(\Phi _{M}=X^{N}+1\)的根\(\xi ,\xi ^{3},...,\xi ^{2N-1}\)去计算该多项式。
所以为了解码多项式\(m(X)\),我们定义\(\sigma \left( m \right)=m\left( m\left( \xi \right),m\left( \xi ^{3} \right),...,m\left( \xi ^{2N-1} \right) \right)\),这里\(σ\)定义了一个同构,这意味着它是一个双射同态,因此任何向量都将唯一地编码到其相应的多项式中,反之亦然。【意思就是,只需把这些根带入多项式,就能得到对应的向量值,这就是解码!】。
麻烦的是如何把向量\(z\in \mbox{C}^N\)编码成多项式\(m\left( X \right)=\frac{C\left[ X \right]}{X^{N}+1}\),这意味着要求\(σ^{-1}\),因此问题就是要找到一个多项式\(m\left( X \right)=\sum_{}^{}{\alpha _{i}X^{i}\in \frac{\mbox{C}\left[ X \right]}{\left( X^{N}+1 \right)}}\),给出一个向量\(z\in \mbox{C}^{N}\),使得\(\sigma \left( m \right)=m\left( m\left( \xi \right),m\left( \xi ^{3} \right),...,m\left( \xi ^{2N-1} \right) \right)=(z_1,...,z_N)\)。
进一步研究这个问题,我们最终得到了以下系统:\(\sum_{}^{}{\alpha _{j}\left( \zeta ^{2i-1} \right)^{j}=z_{i}},i=1,...,N\)。
这可以看作是一个线性方程:\(\Alpha \alpha =z\),其中A是\(\left( \zeta ^{2i-1} \right)_{i=1,...,N}\)的范德蒙矩阵,$\alpha $是多项式的系数,z是需要编码的向量。
所以我们有\(\alpha =\Alpha ^{-1}z\),\(\sigma ^{-1}\left( z \right)=\sum_{}^{}{\alpha _{i}X^{i}\in \frac{\mbox{C}\left[ X \right]}{X^{N}+1}}\)。
举例
强力推荐使用:https://colab.research.google.com/drive/1C2WlzTh-28GUxobvIQK6Nj5GdfunAlH2?usp=sharing
现在让我们来看一个例子,以便更好地理解。
假定\(M=8,N=\frac{M}{2}=4,\Phi _{8}\left( X \right)=X^{4}+1,\omega =e^{\frac{2i\pi }{8}}=e^{\frac{i\pi }{4}}\)
我们的目标是对以下向量进行编码:[1,2,3,4]和[−1,−2,−3,−4] ;对它们进行解码;对它们编码后的多项式进行加法和乘法;然后对其计算结果进行解码。
正如我们所见,为了解码多项式,我们只需要根据M次单位根的幂来计算它。我们这里选择\(\xi _{8}=\omega =e^{\frac{i\pi }{4}}\)。
一旦我们有了\(\xi\)和 \(M\),我们就可以定义\(σ\)及其逆,分别进行解码和编码。
实现
1、现在我们使用Python实现普通的编码和解码:
import numpy as np
# First we set the parameters
M = 8
N = M //2
# We set xi, which will be used in our computations(计算M次单位根)
xi = np.exp(2 * np.pi * 1j / M)
xi
输出:(0.7071067811865476+0.7071067811865475j)
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(计算M次单位根).
"""
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.(根据M次单位根计算范德蒙矩阵)"""
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(只需将M次单位根的次幂代入多项式即可)
for i in range(N):
root = self.xi ** (2 * i + 1)
output = p(root)
outputs.append(output)
return np.array(outputs)
2、让我们先对一个实数向量进行编码:
# First we initialize our encoder(新建一个对象)
encoder = CKKSEncoder(M)
b = np.array([1, 2, 3, 4])
b
array([1, 2, 3, 4])
现在对该向量进行编码:
p = encoder.sigma_inverse(b)
p
输出:\(x↦(2.5+4.440892098500626e^{-16}j)+((-4.996003610813204e^{-16}+0.7071067811865479j))x+((-3.4694469519536176e^{-16}+0.5000000000000003j))x^2+((-8.326672684688674e^{-16}+0.7071067811865472j))x^3\)
3、现在让我们看看如何从多项式中提取我们最初得到的向量(解码):
b_reconstructed = encoder.sigma(p)
b_reconstructed
输出:\(array([1.-1.11022302e^{-16}j, 2.-4.71844785e^{-16}j, 3.+2.77555756e^{-17}j, 4.+2.22044605e^{-16}j])\)
我们可以看到解码值和初始向量非常接近。
np.linalg.norm(b_reconstructed - b)//误差范数
输出:\(6.944442800358888e^{-16}\)
如前所述,不是随机选择的\(σ\)来编码和解码的,但它有很多很好的特性。其中,σ是同构的,因此多项式的加法和乘法将导致向量的加法和乘法。
\(σ\)的同态性质是由于:\(X^{N}+1=0\; and\; \zeta ^{N}+1=0\)
4、我们现在可以开始对几个向量进行编码,看看如何对它们执行同态运算并对其进行解码。
m1 = np.array([1, 2, 3, 4])
m2 = np.array([1, -2, 3, -4])
p1 = encoder.sigma_inverse(m1)
p2 = encoder.sigma_inverse(m2)
输出:\(p1=(2.5+4.440892098500626e^{-16}j)+((-4.996003610813204e^{-16}+0.7071067811865479j))x+((-3.4694469519536176e^{-16}+0.5000000000000003j))x^2+((-8.326672684688674e^{-16}+0.7071067811865472j))x^3\)
\(p2=(-0.4999999999999997-3.3306690738754696e^{-16}j)+((-0.7071067811865472-9.43689570931383e^{-16}j))x+((2.4563684419831585e^{-15}-2.5j))x^2+((0.7071067811865475+1.8318679906315083e^{-15}j))x^3\)
我们可以看到,加法非常简单:
p_add = p1 + p2
p_add
输出:\(x↦(2.0000000000000004+1.1102230246251565e^{-16}j)+((-0.7071067811865477+0.707106781186547j))x+((2.1094237467877966e^{-15}-1.9999999999999996j))x^2+((0.7071067811865466+0.707106781186549j))x^3\)
正如预期的那样,我们看到p1+p2正确解码为[2,0,6,0]。
encoder.sigma(p_add)
输出:\(array([2.0000000e^{+00}+3.25176795e^{-17}j, 4.4408921e^{-16}-4.44089210e^{-16}j, 6.0000000e^{+00}+1.11022302e^{-16}j, 4.4408921e^{-16}+3.33066907e^{-16}j])\)
5、因为在进行乘法运算时,我们可能会得到阶数大于N的项,我们需要使用\(X^N+1\)进行模运算。
要执行乘法,我们首先需要定义我们将使用的多项式模。
poly_modulo = Polynomial([1,0,0,0,1])
poly_modulo
输出:\(x↦1.0+0.0x+0.0x^2+0.0x^3+1.0x^4\)
现在我们可以进行乘法运算了。
p_mult = p1 * p2 % poly_modulo
p
输出:\(x↦(2.5+4.440892098500626e^{-16}j)+((-4.996003610813204e^{-16}+0.7071067811865479j))x+((-3.4694469519536176e^{-16}+0.5000000000000003j))x^2+((-8.326672684688674e^{-16}+0.7071067811865472j))x^3\)
6、最后,如果我们解码它,我们可以看到我们得到了预期的结果。
encoder.sigma(p_mult)
输出:\(array([ 1.-8.67361738e^{-16}j, -4.+6.86950496e^{-16}j, 9.+6.86950496e^{-16}j, -16.-9.08301212e^{-15}j])\)
因此,我们可以看到,我们的简单编码和解码正常,因为它具有同态特性,并且是向量和多项式之间的一一映射。
虽然这是一个很大的进步,但我们实际上撒谎了,因为如果你之前注意到,当我们编码时\(σ^{-1}\),多项式有复系数。因此,虽然编码和解码确实是同态的,而且是一对一的,但它们所处的域是相同的(复数域)\(C^N→ℂ[X] /(X^N+1)\)。因为我们真的希望多项式属于(整数域)\(ℤ[X] /(X^N+1)\),为了使用整数多项式环的所有属性,我们需要确保编码输出具有整数系数而不是复数系数的多项式。
举一个比较具体的例子:
所以我希望你们喜欢这篇关于将复数编码成多项式进行同态加密的小介绍。我们将在下一篇文章中看到如何实现CKKS中使用的实际编码和解码,敬请期待!