CKKS EXPLAINED, PART 5: RESCALING
Introduction
在之前的 CKKS 解释系列文章的第四部分《乘法和重线性化》中,我们了解了 CKKS 中的密文乘法是如何工作的,为什么我们需要对输出进行重线性化以保持密文大小不变,以及如何执行重线性化操作。
然而,正如我们将要看到的,我们还需要进行一种称为重新缩放的最终操作,以管理噪声并避免溢出。这将是本系列的最后一个理论文章,在下一篇也是最后一篇文章中,我们将使用 Python 实现所有内容!
为了理解这个过程是如何工作的,首先1我们将从高层次的角度来看,然后再深入了解其详细工作原理。
High level view of the modulus chain
到目前为止,我们已经深入研究了CKKS的细节,但是在这里我们将退后一步。CKKS使用所谓的级别(levels)进行工作,这意味着在噪声增加到无法正确解密输出之前,只允许进行有限次数的乘法操作。
您可以将其想象为一个汽车油箱。最初,油箱是满的,但随着进行越来越多的操作,油箱会逐渐耗尽,直到没有油了,您将无法再进行任何操作。同样,级别化同态加密方案也是如此:您首先具有一定数量的“油”,但是随着进行乘法操作,您的“油”会越来越少,直到耗尽,您将无法执行任何操作。
下图说明了这一点。当您开始时,您拥有初始的满油箱。但随着进行乘法和重新缩放操作,您将失去级别(level),这相当于消耗了一部分“油”。因此,如果您从L个级别开始,表示为 q L , q L − 1 , … , q 1 q_L, q_{L-1}, \ldots, q_1 qL,qL−1,…,q1,处于级别 q l q_l ql意味着您还剩下 l l l次乘法操作,执行一次乘法将级别从 q l q_l ql降低到 q l − 1 q_{l-1} ql−1。
当您的“汽油”耗尽时,在现实生活中您可以加油以便继续前行。这个操作被称为自举(bootstrapping),我们在本文中不会涉及到。因此,如果我们假设没有机会给油箱加油,使用级别化同态加密方案时需要考虑一个有趣的方面:您需要预先知道要执行的乘法次数!
事实上,就像在现实生活中一样,如果您计划前往较远的地方,您需要的汽油数量会比您只在附近行驶时多。在这里也是如此,根据您需要执行的乘法次数的多少,您需要调整油箱的大小。但是油箱越大,计算的负担就越重,参数的安全性也越低。事实上,就像在现实生活中一样,如果您需要更大的油箱,它会更重,使事情变慢,并且会降低安全性。
我们不会详细介绍所有细节,但是需要知道 CKKS 方案的难度是基于比率 N q \frac Nq qN,其中 N N N 是我们多项式的次数,即我们向量的大小, q q q 是系数模数,即我们的油箱大小。
因此,我们需要的乘法次数越多,油箱越大,因此我们的参数变得不那么安全。为了保持相同的安全级别,我们需要增加 N N N,这将增加我们操作的计算成本。
下图来自 Microsoft Private AI Bootcamp,显示了在使用 CKKS 时必须考虑的权衡。为了保证 128 位的安全性,我们必须增加多项式的次数,即使我们不需要额外的插槽,因为模增加可能会使我们的参数不安全。
在我们继续更深入的理论部分之前,让我们总结一下关键要点:
- 重新缩放和噪声管理可以看作是管理一个汽车油箱,您从一个初始预算开始,随着使用次数的增加而逐渐减少。如果油耗尽,您将无法再继续操作。
- 您需要事先知道要执行多少次乘法,这将决定油箱的大小,并影响您将使用的多项式次数的大小。
Context
现在我们来看一下更详细的原因和工作原理。
如果您还记得第二部分关于编码的内容,如果我们有一个初始的数值向量 z z z,在编码过程中会乘以一个尺度 Δ \Delta Δ 来保持一定的精度。
因此,明文 μ \mu μ 和密文 c c c 中包含的基本值是 Δ ⋅ z \Delta \cdot z Δ⋅z。当我们将两个密文 c c c 和 c ′ c' c′ 相乘时,结果中包含了 z ⋅ z ′ ⋅ Δ 2 z \cdot z' \cdot \Delta^2 z⋅z′⋅Δ2。因此,它包含了尺度的平方,这可能导致在几次乘法后发生溢出,因为尺度可能呈指数级增长。此外,正如我们之前所看到的,每次乘法后噪声都会增加。
因此,重新缩放操作的目的实际上是保持尺度恒定,并减少密文中存在的噪声。
Vanilla solution
那么我们如何解决这个问题呢?为此,我们需要看一下如何定义 q q q。请记住,参数 q q q 被用作多项式环 R q = Z q [ X ] / ( X N + 1 ) R_q = \mathbb{Z}_q[X]/(X^N+1) Rq=Zq[X]/(XN+1) 中系数的模数。
如高层次视图所述, q q q 将被用作一个我们会逐步耗尽的“油箱”,用于我们的操作。
假设我们需要执行 L L L 次乘法,使用尺度 Δ \Delta Δ,那么我们将定义 q q q 如下:
q = Δ L ⋅ q 0 q = \Delta^L \cdot q_0 q=ΔL⋅q0
其中 q 0 ≥ Δ q_0 \geq \Delta q0≥Δ,它将决定我们希望在小数部分之前有多少位。实际上,如果我们假设我们希望小数部分具有 30 位的精度,并且整数部分具有 10 位的精度,我们将设置:
Δ = 2 30 \Delta = 2^{30} Δ=230, q 0 = 2 bits integer ⋅ 2 bits decimal = 2 10 + 30 = 2 40 q_0 = 2^{\text{{bits integer}}} \cdot 2^{\text{{bits decimal}}} = 2^{10+30} = 2^{40} q0=2bits integer⋅2bits decimal=210+30=240。
一旦我们确定了整数部分和小数部分的精度要求,选择了要执行的乘法次数 L L L,并相应地设置了 q q q,定义重新缩放操作就非常简单了:我们只需对密文进行除法和四舍五入即可。
确实,假设我们在给定的级别 l l l上,模数为 q l q_l ql。我们有一个密文 c ∈ R q l 2 c \in \mathcal{R}_{q_l^2} c∈Rql2。然后我们可以定义从级别 l l l到 l − 1 l-1 l−1的重新缩放操作为:
R S l → l − 1 ( c ) = ⌊ q l − 1 q l ⋅ c ⌉ m o d q l − 1 = ⌊ Δ − 1 ⋅ c ⌉ m o d q l − 1 RS_{l \to l-1}(c) = \left\lfloor \frac{q_{l-1}}{q_l} \cdot c \right\rceil \bmod q_{l-1} = \left\lfloor \Delta^{-1} \cdot c \right\rceil \bmod q_{l-1} RSl→l−1(c)=⌊qlql−1⋅c⌉modql−1=⌊Δ−1⋅c⌉modql−1,
其中 q l = Δ l ⋅ q 0 q_l = \Delta^l \cdot q_0 ql=Δl⋅q0。
通过这样做,我们成功做到了两件事:
- 在解密两个密文 c , c ′ c, c' c,c′的乘积后,底层值为 Δ ⋅ z , Δ ⋅ z ′ \Delta \cdot z, \Delta \cdot z' Δ⋅z,Δ⋅z′,应用重新缩放后,我们得到了 Δ ⋅ z ⋅ z ′ \Delta \cdot z \cdot z' Δ⋅z⋅z′。因此,只要我们在每次乘法后进行重新缩放,尺度就会保持恒定。
- 噪声被减少,因为我们不仅除以底层明文值,还除以解密的噪声部分,记为 μ + e \mu+e μ+e。因此,重新缩放也用于噪声的减少。
example
假设我们在级别 l l l上,模数为 q l q_l ql,而我们有一个密文 c c c,它的值范围在 [ 0 , q l 2 ) [0, q_l^2) [0,ql2)之间。我们想要将密文 c c c重新缩放到级别 l − 1 l-1 l−1上,其中模数为 q l − 1 q_{l-1} ql−1。
让我们考虑一个具体的数字,如 c = 2500 c = 2500 c=2500,并假设 q l = 10000 q_l = 10000 ql=10000和 q l − 1 = 1000 q_{l-1} = 1000 ql−1=1000。
根据重新缩放操作的定义,我们有:
R S l → l − 1 ( c ) = ⌊ q l − 1 q l ⋅ c ⌉ m o d q l − 1 RS_{l \to l-1}(c) = \left\lfloor \frac{q_{l-1}}{q_l} \cdot c \right\rceil \bmod q_{l-1} RSl→l−1(c)=⌊qlql−1⋅c⌉modql−1
代入我们的值,我们得到:
R S l → l − 1 ( 2500 ) = ⌊ 1000 10000 ⋅ 2500 ⌉ m o d 1000 RS_{l \to l-1}(2500) = \left\lfloor \frac{1000}{10000} \cdot 2500 \right\rceil \bmod 1000 RSl→l−1(2500)=⌊100001000⋅2500⌉mod1000
计算结果为:
R S l → l − 1 ( 2500 ) = ⌊ 0.1 ⋅ 2500 ⌉ m o d 1000 = ⌊ 250 ⌉ m o d 1000 = 250 RS_{l \to l-1}(2500) = \left\lfloor 0.1 \cdot 2500 \right\rceil \bmod 1000 = \left\lfloor 250 \right\rceil \bmod 1000 = 250 RSl→l−1(2500)=⌊0.1⋅2500⌉mod1000=⌊250⌉mod1000=250
所以,通过重新缩放操作,我们将密文 c = 2500 c = 2500 c=2500从级别 l l l重新缩放到级别 l − 1 l-1 l−1,得到的结果为 c ′ = 250 c' = 250 c′=250。
这样做的好处是,我们可以在较高的级别上进行计算,然后通过重新缩放操作将结果转换为较低级别,以便继续进行后续计算。同时,重新缩放操作还有助于减少噪声,因为除以明文值和噪声部分。这有助于维持加密计算的精度和安全性。
因此,将所有内容综合起来,在CKKS中执行乘法需要完成三件事:
- 计算乘积: c m u l t = C M u l t ( c , c ′ ) = ( d 0 , d 1 , d 2 ) c_{mult} = CMult(c, c') = (d_0, d_1, d_2) cmult=CMult(c,c′)=(d0,d1,d2)。
- 进行线性化: c r e l i n = R e l i n ( ( d 0 , d 1 , d 2 ) , e v k ) c_{relin} = Relin((d_0, d_1, d_2), evk) crelin=Relin((d0,d1,d2),evk)。
- 进行重新缩放: c r s = R S l → l − 1 ( c ) c_{rs}= RS_{l \to l-1}(c) crs=RSl→l−1(c)。
完成所有这些步骤后,使用秘密密钥进行解密将提供正确的结果,我们就完成了!嗯,几乎完成了,因为还有最后一个细节需要讨论。
Chinese remainder theorem
我们已经看到我们拥有了所需的一切,但是有一个技术问题:计算是在巨大的数字上进行的!实际上,我们的操作是在巨大的模数 q l = Δ l ⋅ q 0 ql = \Delta^l \cdot q_0 ql=Δl⋅q0下进行的。例如,假设我们希望小数部分有30位精度,整数部分有10位精度,并且进行10次乘法。那么我们有 q L = Δ L ⋅ q 0 = 2 30 ⋅ 10 + 40 = 2 340 q_L = \Delta^L \cdot q_0 = 2^{30 \cdot 10 + 40} = 2^{340} qL=ΔL⋅q0=230⋅10+40=2340!
因为我们有时会处理巨大的多项式,比如均匀采样的多项式,一些计算无法适应常规的64位系统,因此我们必须找到一种解决方法。
这就是中国剩余定理的用武之地!该定理指出,如果我们有 L L L个互质的数 p 1 , … , p L p_1, \ldots, p_L p1,…,pL, p = ∏ l = 1 L p l p = \prod_{l=1}^{L} p_l p=∏l=1Lpl是它们的乘积,那么映射
Z / p Z → Z / p 1 Z × ⋯ × Z / p L Z : x ( m o d p ) ↦ ( x ( m o d p 1 ) , … , x ( m o d p L ) ) \mathbb{Z}/p\mathbb{Z}\to\mathbb{Z}/p_1\mathbb{Z}\times\cdots\times\mathbb{Z}/p_L\mathbb{Z}:x(\mathrm{~mod~}p)\mapsto(x(\mathrm{~mod~}p_1),\ldots,x(\mathrm{~mod~}p_L)) Z/pZ→Z/p1Z×⋯×Z/pLZ:x( mod p)↦(x( mod p1),…,x( mod pL))
是一个环同构,也就是说,如果您想在“大”的环 Z / p Z \mathbb{Z}/p\mathbb{Z} Z/pZ上进行算术运算,您可以在“小”的环 Z / p l Z \mathbb{Z}/p_l\mathbb{Z} Z/plZ上独立进行,这样就不会遇到超过64位的计算问题。
因此,在实践中,我们首先选择 p 1 , … , p L , q 0 p_1, \ldots, p_L, q_0 p1,…,pL,q0是素数,其中每个 p l ≈ Δ p_l \approx \Delta pl≈Δ, q 0 q_0 q0是一个大于 Δ \Delta Δ的素数,取决于所需的整数精度,然后设置 q L = ∏ l = 1 L p l ⋅ q 0 q_L = \prod_{l=1}^{L} p_l \cdot q_0 qL=∏l=1Lpl⋅q0。
这样,我们可以使用中国剩余定理,并通过上面描述的小技巧来进行具有大模数的算术运算。重新缩放操作必须稍作修改:
R S l → l − 1 ( c ) = ⌊ q l − 1 q l c ⌉ ( m o d q l − 1 ) = ⌊ p l − 1 c ⌉ ( m o d q l − 1 ) RS_{l\to l-1}(c)=\lfloor\frac{q_{l-1}}{q_l}c\rceil(\mathrm{mod~}q_{l-1})=\lfloor p_l^{-1}c\rceil(\mathrm{mod~}q_{l-1}) RSl→l−1(c)=⌊qlql−1c⌉(mod ql−1)=⌊pl−1c⌉(mod ql−1)。
因此,在本文中,我们已经了解了重新缩放是什么,为什么我们需要它,以及如何在实践中实现它。在下一篇也是最后一篇文章中,我们将把所有内容整合起来,在Python中编写一个类似CKKS的同态加密方案!