如何使用 Julia 語言實現「同態加密+機器學習」?

選自JuliaComputing

機器之心編譯

參與:李詩萌、Geek AI

最近,「區塊鏈」、「聯邦學習」等概念受到了空前的關注。而在這些概念背後,少不了一項技術的影子——「同態加密」。本文介紹了使用 Julia 語言進行基於同態加密數據機器學習的全過程,對於入門者具有極大的參考價值。


注意:本文討論了最前沿的密碼學技術,旨在提供一種利用「Julia Computing」進行研究的視角。請不要將文中的任何示例用於生產應用程序。在使用密碼學之前一定要諮詢專業的密碼學專家。


程序包:https://github.com/JuliaComputing/ToyFHE.jl相關代碼:https://github.com/JuliaComputing/ToyFHE.jl/blob/master/examples/encrypted_mnist/infer.jl


引言


假設你開發了一個酷炫的新機器學習模型,現在你想將部署該模型,為用戶提供服務。應該怎麼做呢?最簡單的方法可能是直接把模型發佈給用戶,然後讓他們使用自己的數據在本地運行這個模型。但這種方法存在一些問題:


  • 機器學習模型一般都很大,而用戶的設備實際上可能沒有足夠的存儲空間或算力來運行模型
  • 機器學習模型一般都會頻繁地更新,你可能不會想在網絡上頻繁傳輸這麼大的模型
  • 開發機器學習模型需要大量時間和計算資源,你可能會想通過向使用該模型的用戶收費來收回成本


接下來,常用的解決方案是將模型作為應用程序接口(API)在雲上公開。在過去幾年間,這些「機器學習即服務」產品如雨後春筍般湧現,每個主要的雲平臺都會為企業級開發者提供這樣的服務。
但這類產品的潛在用戶所面對的困境也是顯而易見的——處理用戶數據的遠程服務器可能並不可信。這樣就會存在明確的倫理和法律的分歧,從而限制這種解決方案的有效範圍。在受監管的產業(尤其是醫療業和金融業)中,一般是不允許將病患或金融數據發送給第三方進行處理的。我們可以做得更好嗎?


事實證明,我們可以!最近,密碼學方面取得的突破可以在無需進行解密的情況下,直接計算加密數據。在我們的例子中,用戶可以將加密數據(例如圖像)傳遞給雲 API,以此運行機器學習模型,並返回加密的答案。整個過程中都沒有解密用戶數據,尤其是雲服務商既不能訪問原始圖像,也不能解碼計算得到的預測值。這是怎麼做到的呢?本文通過構建一個進行加密圖像的手寫識別(來自 MNIST 數據集)的機器學習模型為大家揭秘背後的原理。


同態加密(Homomorphic Encryption,HE)的一般解釋


一般而言,對加密數據進行計算的能力被稱為「安全計算」,這是一個相當大的研究領域,針對大量不同的場景要用不同的密碼學方法和技術解決問題。在本例中,我們將關注所謂的「同態加密」技術。在同態加密系統中,我們一般要進行以下操作:

pub_key,eval_key,priv_key=keygen() 
encrypted=encrypt(pub_key,plaintext)
decrypted=decrypt(priv_key,encrypted)
encrypted′=eval(eval_key,f,encrypted)


前三步非常直觀,之前使用過任何非對稱加密技術的人都會對此感到很熟悉(就像通過安全傳輸層協議(TLS)連接到本文)。最後一步才是神奇之處。它使用加密數據評估了 f,並返回了另一個與基於加密值評估 f 的結果對應的加密值。這一性質正是我們將這種技術稱為「同態加密」的原因。評估操作與下面的加密操作等價:


f(decrypt(priv_key,encrypted))==decrypt(priv_key,eval(eval_key,f,encrypted))


(同樣地,可以基於加密值評估任意的同態 f)


支持哪些函數 f 取決於加密方案和支持的運算。如果只支持一種函數 f(比如 f=+),我們可以將這種加密方案稱為「部分同態」。如果 f 是可以建立任意電路的完整的門的集合,如果電路大小有限,稱之為「有限同態」(Somewhat Homomorphic Encryption, SHE);如果電路大小不受限制,稱之為「全同態」(Fully Homomorphic Encryption, FHE)。一般可以通過自助法(bootstrapping),將「有限」同態轉換為「全」同態,但這個問題已經超過了本文所討論的內容。


全同態加密是最近的研究,Craig Gentry 在 2009 年發表了第一個可行(但不實際)的方。現在陸續出現了一些更新也更實際的 FHE 方案。更重要的是,還有一些可以高效地實現這一方案的軟件包。最常用的兩個軟件包是 Microsoft SEAL和 PALISADE。此外,我最近還開源了這些算法的 Julia 實現(https://github.com/JuliaComputing/ToyFHE.jl)。出於我們的目的,我們將使用後者中實現的 CKKS 加密。


高級 CKKS


CKKS(以 Cheon-Kim-Kim-Song 的名字命名,他在 2016 年的論文「Homomorphic Encryption for Arithmetic of Approximate Numbers」提出)是一種同態加密方案,可以對以下基本操作進行同態評估:


  • 長度為 n 的複數向量的對應元素相加
  • 長度為 n 的複數向量的對應元素相乘
  • 向量中元素的旋轉(通過循環移位實現)
  • 向量元素的複共軛


這裡的參數 n 取決於需要的安全性和準確性,該值一般都比較高。在本例中,n=4096(值越高越安全,但是計算開銷也更大,時間複雜度大致會縮放為 nlog^n)。


此外,用 CKKS 計算是有噪聲的。因此,計算結果一般都只是近似值,而且要注意確保評估結果足夠準確,不會影響結果的正確性。


也就是說,對機器學習程序包的開發者而言,這些限制並不罕見。像 GPU 這樣有特殊用途的加速器,也可以處理數字向量。同樣,許多開發者會因算法選擇的影響、多線程等原因,認為浮點數噪聲太多(我要強調的是,有一個關鍵的區別是,浮點算法本身是確定性的,儘管因為實現的複雜性,它有時不會展現出這種確定性,但 CKKS 原語的噪聲真的很多,但這也許可以讓用戶意識到噪聲並沒有第一次出現時那麼可怕)。


考慮到這一點,我們再看看如何在 Julia 中執行這些運算(注意:這裡有一些非常不安全的參數選擇,這些操作的目的是說明這個庫在交互式解釋器(REPL)中的用法)。


julia>usingToyFHE
#Let'splaywith8elementvectors
julia>N=8;
#Choosesomeparameters-we'lltalkaboutitlater
julia>ℛ=NegacyclicRing(2N,(40,40,*40*))
ℤ₁₃₂₉₂₂₇₉₉₇₅₆₈₀₈₁₄₅₇₄₀₂₇₀₁₂₀₇₁₀₄₂₄₈₂₅₇/(x¹⁶+1)
#We'lluseCKKS julia>params=CKKSParams(ℛ)
CKKSparameters
#Weneedtopickascalingfactorforanumbers-againwe'lltalkaboutthatlater
julia>Tscale=FixedRational{2^40}
FixedRational{1099511627776,T}whereT
#Let'sstartwithaplainVectorofzeros
julia>plain=CKKSEncoding{Tscale}(zero(ℛ))
8-elementCKKSEncoding{FixedRational{1099511627776,T}whereT}withindices0:7:
0.0+0.0im
0.0+0.0im
0.0+0.0im
0.0+0.0im
0.0+0.0im
0.0+0.0im
0.0+0.0im
0.0+0.0im
#Ok,we'rereadytogetstarted,butfirstwe'llneedsomekeys
julia>kp=keygen(params)
CKKSkeypair
julia>kp.priv
CKKSprivatekey
julia>kp.pub
CKKSpublickey
#Alright,let'sencryptsomethings:
julia>foreach(i->plain[i]=i+1,0:7);plain
8-elementCKKSEncoding{FixedRational{1099511627776,T}whereT}withindices0:7:
1.0+0.0im
2.0+0.0im
3.0+0.0im
4.0+0.0im
5.0+0.0im
6.0+0.0im
7.0+0.0im
8.0+0.0im
julia>c=encrypt(kp.pub,plain)
CKKSciphertext(length2,encodingCKKSEncoding{FixedRational{1099511627776,T}whereT})
#Anddecryptitagain
julia>decrypt(kp.priv,c)
8-elementCKKSEncoding{FixedRational{1099511627776,T}whereT}withindices0:7:
0.9999999999995506-2.7335193113350057e-16im

1.9999999999989408-3.885780586188048e-16im
3.000000000000205+1.6772825551165524e-16im
4.000000000000538-3.885780586188048e-16im
4.999999999998865+8.382500573679615e-17im
6.000000000000185+4.996003610813204e-16im
7.000000000001043-2.0024593503998215e-16im
8.000000000000673+4.996003610813204e-16im
#Notethatwehadsomenoise.Let'sgothroughalltheprimitiveoperationswe'llneed:
julia>decrypt(kp.priv,c+c)
8-elementCKKSEncoding{FixedRational{1099511627776,T}whereT}withindices0:7:
1.9999999999991012-5.467038622670011e-16im
3.9999999999978817-7.771561172376096e-16im
6.00000000000041+3.354565110233105e-16im
8.000000000001076-7.771561172376096e-16im
9.99999999999773+1.676500114735923e-16im
12.00000000000037+9.992007221626409e-16im
14.000000000002085-4.004918700799643e-16im
16.000000000001346+9.992007221626409e-16im
julia>csq=c*c
CKKSciphertext(length3,encodingCKKSEncoding{FixedRational{1208925819614629174706176,T}whereT})
julia>decrypt(kp.priv,csq)8-elementCKKSEncoding{FixedRational{1208925819614629174706176,T}whereT}withindices0:7:
0.9999999999991012-2.350516767363621e-15im
3.9999999999957616-5.773159728050814e-15im
9.000000000001226-2.534464540987068e-15im
16.000000000004306-2.220446049250313e-15im
24.99999999998865+2.0903753311370056e-15im
36.00000000000222+4.884981308350689e-15im
49.000000000014595+1.0182491378134327e-15im
64.00000000001077+4.884981308350689e-15im


這很簡單!敏銳的讀者可能已經注意到了 csq 和之前的密文看起來有點不同。尤其是,它是「長度為 3」的密文,範圍也更大。要說明它們是什麼,以及它們是做什麼用的有點太過複雜。我只想說,在進一步計算之前,我們要得讓這些值降下來,否則我們會盡密文中的「空間」。幸運的是,有一種方法可以解決這兩個問題:


#Togetbackdowntolength2,weneedto`keyswitch`(aka
#relinerarize),whichrequiresanevaluationkey.Generating
#thisrequirestheprivatekey.Inarealapplicationwewould
#havegeneratedthisupfrontandsentitalongwiththeencrypted
#data,butsincewehavetheprivatekey,wecanjustdoitnow.
julia>ek=keygen(EvalMultKey,kp.priv)
CKKSmultiplicationkey
julia>csq_length2=keyswitch(ek,csq)
CKKSciphertext(length2,encodingCKKSEncoding{FixedRational{1208925819614629174706176,T}whereT})
#Gettingthescalebackdownisdoneusingmodswitching.
julia>csq_smaller=modswitch(csq_length2)
CKKSciphertext(length2,encodingCKKSEncoding{FixedRational{1.099511626783e12,T}whereT})
#Anditstilldecryptscorrectly(thoughnotewe'velostsomeprecision)
julia>decrypt(kp.priv,csq_smaller)
8-elementCKKSEncoding{FixedRational{1.099511626783e12,T}whereT}withindices0:7:
0.9999999999802469-5.005163520332181e-11im
3.9999999999957723-1.0468514951188039e-11im
8.999999999998249-4.7588542623100616e-12im
16.000000000023014-1.0413447889166631e-11im
24.999999999955193-6.187833723406491e-12im
36.000000000002345+1.860733715346631e-13im
49.00000000001647-1.442396043149794e-12im
63.999999999988695-1.0722489563648028e-10im


此外,modswitching(模轉換:modulus switching 的簡寫)減少了密文模的大小,所以我們不能無限地這麼做下去。(用上文提到的術語來說,我們在這裡使用的是 SHE 方案):

julia>ℛ#Remembertheringweinitiallycreated
ℤ₁₃₂₉₂₂₇₉₉₇₅₆₈₀₈₁₄₅₇₄₀₂₇₀₁₂₀₇₁₀₄₂₄₈₂₅₇/(x¹⁶+1)

julia>ToyFHE.ring(csq_smaller)#Itshrunk!
ℤ₁₂₀₈₉₂₅₈₂₀₁₄₄₅₉₃₇₇₉₃₃₁₅₅₃/(x¹⁶+1)


我們要做的最後一步運算是:旋轉。就像上文的密鑰轉換(KeySwitching),在這裡也需要評估密鑰(也稱為伽羅瓦(galois)密鑰):


julia>gk=keygen(GaloisKey,kp.priv;steps=2)
CKKSgaloiskey(element25)
julia>decrypt(circshift(c,gk))
decrypt(kp,circshift(c,gk))
8-elementCKKSEncoding{FixedRational{1099511627776,T}whereT}withindices0:7:
7.000000000001042+5.68459112632516e-16im
8.000000000000673+5.551115123125783e-17im
0.999999999999551-2.308655353580721e-16im
1.9999999999989408+2.7755575615628914e-16im
3.000000000000205-6.009767921608429e-16im
4.000000000000538+5.551115123125783e-17im
4.999999999998865+4.133860996136768e-17im
6.000000000000185-1.6653345369377348e-16im
#Andlet'scomparetodoingthesameontheplaintext
julia>circshift(plain,2)
8-elementOffsetArray(::Array{Complex{Float64},1},0:7)witheltypeComplex{Float64}withindices0:7:
7.0+0.0im
8.0+0.0im
1.0+0.0im
2.0+0.0im
3.0+0.0im
4.0+0.0im
5.0+0.0im
6.0+0.0im


好了,我們已經瞭解了同態加密庫的基本用法。在思考如何用這些原語進行神經網絡推斷之前,我們先觀察並訓練我們需要使用的神經網絡。


機器學習模型


如果你不熟悉機器學習或 Flux.jl 機器學習庫,我建議你先快速閱讀一下 Flux.jl 文檔或我們在 JuliaAcademy 上發佈的免費機器學習介紹課程,因為我們只會討論在加密數據上運行模型所做的更改。


我們將以 Flux 模型空間中卷積神經網絡的例子為出發點。在這個模型中,訓練循環、數據預處理等操作都不變,只是輕微地調整模型。我們要用的模型是:


functionreshape_and_vcat(x)
lety=reshape(x,64,4,size(x,4))
vcat((y[:,i,:]fori=axes(y,2))...)
end
end
model=Chain(
#Firstconvolution,operatingupona28x28image
Conv((7,7),1=>4,stride=(3,3),x->x.^2),
reshape_and_vcat,
Dense(256,64,x->x.^2),
Dense(64,10),
)


該模型與「安全外包矩陣的計算及其在神經網絡上與應用」(Secure Outsourced Matrix Computation and Application to Neural Networks)文中所用的模型基本相同,它們用相同的加密方案演示了相同的模型,但有兩個區別:(1)他們加密了模型而我們(為簡單起見)沒有對模型加密;(2)我們在每一層之後都有偏置向量(這也是 Flux 的默認行為),我不確定這種行為對本文評估的模型是否是這樣。也許是因為(2),我們模型的準確率才略高(98.6% vs 98.1%),但這也可能僅僅是因為超參數的差異。


「x.^2」激活函數也是一個不尋常的特徵(對那些有機器學習背景的人來說)。這裡更常用的選擇可能是「tanh」、「relu」或者其他更高級的函數。然而,儘管這些函數(尤其是 relu)可以更容易地評估明文值,但評估加密數據的計算開銷則相當大(基本上是評估多項式近似值)。幸運的是,「x.^2」可以很好地滿足我們的目的。


其餘的訓練循環基本上是相同的。我們從模型中刪除了「softmax」,取而代之的是「logitcrossentropy」損失函數(當然也可以保留它,在客戶端解密後再評估「softmax」)。訓練模型的完整代碼見 GitHub,在近期發佈的 GPU 上只需要幾分鐘就可以完成訓練。
代碼地址:https://github.com/JuliaComputing/ToyFHE.jl/blob/master/examples/encrypted_mnist/train.jl
高效地計算


好了,現在已經明確了我們需要做什麼,接下來看看我們要做哪些運算:


  • 卷積
  • 元素平方
  • 矩陣乘法


我們在上文中已經看到了,元素平方操作是很簡單的,所以我們按順序處理剩下的兩個問題。在整個過程中,假設批處理大小(batch size)為 64(你可能注意到了,我們有策略地選擇模型參數和批處理大小,從而充分利用 4096 元素向量的優勢,這是我們從實際的參數選擇中得到的)。


卷積


讓我們回顧一下卷積是如何工作的。首先,取原始輸入數組中的一些窗口(本例中為 7*7),窗口中的每個元素跟卷積掩模的元素相乘。然後移動窗口(本例中步長為 3,所以將窗口移動 3 個元素)。重複這個過程(用相同的卷積掩模)。下面的動畫說明了以(2,2)的步長進行 3*3 卷積的過程(藍色數組是輸入,綠色數組是輸出)。


如何使用 Julia 語言實現「同態加密+機器學習」?


另外,我們將卷積分成 4 個不同的「通道」(這意味著用不同的卷積掩模,將卷積又重複了 3 次)


好了,現在我們已經知道了要做什麼,接下來考慮一下該如何實現。幸運的是,卷積是我們模型中的第一步運算。因此,可以在加密數據之前(無需模型權重)先在客戶端上預處理,來節省一些工作。具體而言,我們將執行以下操作:


  • 預先計算每個卷積窗口(即從原始圖像中提取 7*7 的窗口),從每個輸入圖像中得到 64 個 7*7 的矩陣(注意要在步長為 2 的情況下得到 7*7 的窗口,要評估 28*28 的輸入圖像的話,要計算 8*8 的卷積窗口)
  • 將每個窗口中的相同位置收集到一個向量中,即對每張圖來說,都會有包含 64 個元素的向量,或當批處理大小為 64 時,會得到 64*64 的元素向量(即,共有 49 個 64*64 的矩陣)
  • 加密


然後卷積就變成了整個矩陣和適當掩碼元素的標量乘法,對這 49 個元素求和,得到了卷積的結果。這個方案是這樣實現的(在明文上):


functionpublic_preprocess(batch)
ka=OffsetArray(0:7,0:7)
#Createfeatureextractedmatrix
I=[[batch[i′*3.+(1:7),j′*3.+(1:7),1,k]fori′=ka,j′=ka]fork=1:64]
#Reshapeintotheciphertext
Iᵢⱼ=[[I[k][l...][i,j]fork=1:64,l=product(ka,ka)]fori=1:7,j=1:7]
end
Iᵢⱼ=public_preprocess(batch)
#Evaluatetheconvolution
weights=model.layers[1].weight
conv_weights=reverse(reverse(weights,dims=1),dims=2)
conved=[sum(Iᵢⱼ[i,j]*conv_weights[i,j,1,channel]fori=1:7,j=1:7)forchannel=1:4]
conved=map(((x,b),)->x.+b,zip(conved,model.layers[1].bias))


這樣的實現(對維度重新排序的模)給出了相同的答案,但是用了這樣的操作:


model*.*layers[*1*](batch)


加入加密操作後,我們得到:


Iᵢⱼ=public_preprocess(batch)
C_Iᵢⱼ=map(Iᵢⱼ)doIij
plain=CKKSEncoding{Tscale}(zero(plaintext_space(ckks_params)))
plain.=OffsetArray(vec(Iij),0:(N÷2-1))
encrypt(kp,plain)
end
weights=model.layers[1].weight
conv_weights=reverse(reverse(weights,dims=1),dims=2)
conved3=[sum(C_Iᵢⱼ[i,j]*conv_weights[i,j,1,channel]fori=1:7,j=1:7)forchannel=1:4]
conved2=map(((x,b),)->x.+b,zip(conved3,model.layers[1].bias))
conved1=map(ToyFHE.modswitch,conved2)


注意,由於權重是公開的,所以不需要密鑰轉換,因此沒有擴展密文的長度。


矩陣乘法


接下來看看矩陣乘法是如何實現的。我們利用這樣的事實——可以旋轉向量中的元素,來重排序乘法索引。特別是,要考慮向量中矩陣元素的行優先排序。然後,如果以行大小的倍數移動向量,就可以得到列旋轉的效果,這可以提供充足的原語來實現矩陣乘法(至少是方陣)。我們不妨試一下:


functionmatmul_square_reordered(weights,x)
sum(1:size(weights,1))dok
#WerotatethecolumnsoftheLHSandtakethediagonal
weight_diag=diag(circshift(weights,(0,(k-1))))
#WerotatetherowsoftheRHS
x_rotated=circshift(x,(k-1,0))
#Wedoanelementwise,broadcastmultiply
weight_diag.*x_rotated
end
end
functionmatmul_reorderd(weights,x)
sum(partition(1:256,64))dorange
matmul_square_reordered(weights[:,range],x[range,:])
end
end
fc1_weights=model.layers[3].W
x=rand(Float64,256,64)
@assert(fc1_weights*x)≈matmul_reorderd(fc1_weights,x)


當然,對於一般的矩陣乘法,我們可能需要更好的方法,但是在本例中,現在這種程度就已經足夠了。


優化代碼


至此,我們設法將所有內容整合在一起,而且也確實奏效了。這裡提供了代碼作為參考(省略了參數選擇等設置):


ek=keygen(EvalMultKey,kp.priv)
gk=keygen(GaloisKey,kp.priv;steps=64)

Iᵢⱼ=public_preprocess(batch)
C_Iᵢⱼ=map(Iᵢⱼ)doIij
plain=CKKSEncoding{Tscale}(zero(plaintext_space(ckks_params)))
plain.=OffsetArray(vec(Iij),0:(N÷2-1))
encrypt(kp,plain)
end
weights=model.layers[1].weight
conv_weights=reverse(reverse(weights,dims=1),dims=2)
conved3=[sum(C_Iᵢⱼ[i,j]*conv_weights[i,j,1,channel]fori=1:7,j=1:7)forchannel=1:4]
conved2=map(((x,b),)->x.+b,zip(conved3,model.layers[1].bias))
conved1=map(ToyFHE.modswitch,conved2)
Csqed1=map(x->x*x,conved1)
Csqed1=map(x->keyswitch(ek,x),Csqed1)
Csqed1=map(ToyFHE.modswitch,Csqed1)
functionencrypted_matmul(gk,weights,x::ToyFHE.CipherText)
result=repeat(diag(weights),inner=64).*x
rotated=x
fork=2:64
rotated=ToyFHE.rotate(gk,rotated)
result+=repeat(diag(circshift(weights,(0,(k-1)))),inner=64).*rotated
end
result
end
fq1_weights=model.layers[3].W
Cfq1=sum(enumerate(partition(1:256,64)))do(i,range)
encrypted_matmul(gk,fq1_weights[:,range],Csqed1[i])
end
Cfq1=Cfq1.+OffsetArray(repeat(model.layers[3].b,inner=64),0:4095)
Cfq1=modswitch(Cfq1)
Csqed2=Cfq1*Cfq1
Csqed2=keyswitch(ek,Csqed2)
Csqed2=modswitch(Csqed2)
functionnaive_rectangular_matmul(gk,weights,x)
@assertsize(weights,1)<size> weights=vcat(weights,zeros(eltype(weights),size(weights,2)-size(weights,1),size(weights,2)))
encrypted_matmul(gk,weights,x)
end
fq2_weights=model.layers[4].W
Cresult=naive_rectangular_matmul(gk,fq2_weights,Csqed2)Cresult=Cresult.+OffsetArray(repeat(vcat(model.layers[4].b,
zeros(54)),inner=64),0:4095)
/<size>


雖然代碼看起來不是很清晰,但是如果你已經進行到這一步了,那你就應該理解這個流程中的每一步。


現在,把注意力轉移到可以讓這一切更好理解的抽象上。我們先跳出密碼學和機器學習領域,考慮編程語言設計的問題。Julia 可以實現強大的抽象,我們可以利用這一點構建一些抽象。例如,可以將整個卷積提取過程封裝為自定義數組類型:


usingBlockArrays
"""
ExplodedConvArray{T,Dims,Storage}<:abstractarray>Representsaan`nxmx1xb`arrayofimages,butrearrangedintoa
seriesofconvolutionwindows.Evaluatingaconvolutioncompatible
with`Dims`onthisarrayisachievablethroughasequenceof
scalarmultiplicationsandsumsontheunderlingstorage.
"""
structExplodedConvArray{T,Dims,Storage}<:abstractarray> #sx*symatrixofb*(dx*dy)matricesofextractedelements
#where(sx,sy)=kernel_size(Dims)
#(dx,dy)=output_size(DenseConvDims(...))
cdims::Dims
x::Matrix{Storage}
functionExplodedConvArray{T,Dims,Storage}(cdims::Dims,storage::Matrix{Storage})where{T,Dims,Storage}
@assertall(==(size(storage[1])),size.(storage))
new{T,Dims,Storage}(cdims,storage)
end
end
Base.size(ex::ExplodedConvArray)=(NNlib.input_size(ex.cdims)...,1,size(ex.x[1],1))
functionExplodedConvArray{T}(cdims,batch::AbstractArray{T,4})where{T}
x,y=NNlib.output_size(cdims)
kx,ky=NNlib.kernel_size(cdims)
stridex,stridey=NNlib.stride(cdims)
kax=OffsetArray(0:x-1,0:x-1)
kay=OffsetArray(0:x-1,0:x-1)
I=[[batch[i′*stridex.+(1:kx),j′*stridey.+(1:ky),1,k]fori′=kax,j′=kay]fork=1:size(batch,4)]
Iᵢⱼ=[[I[k][l...][i,j]fork=1:size(batch,4),l=product(kax,kay)]for(i,j)inproduct(1:kx,1:ky)] ExplodedConvArray{T,typeof(cdims),eltype(Iᵢⱼ)}(cdims,Iᵢⱼ)
end
functionNNlib.conv(x::ExplodedConvArray{<:any>weights::AbstractArray{<:any> blocks=reshape([
Base.ReshapedArray(sum(x.x[i,j]*weights[i,j,1,channel]fori=1:7,j=1:7),(NNlib.output_size(cdims)...,1,size(x,4)),())forchannel=1:4],(1,1,4,1))
BlockArrays._BlockArray(blocks,BlockArrays.BlockSizes([8],[8],[1,1,1,1],[64]))

end


注意,如原始代碼所示,這裡用 BlockArrays 將 8*8*4*64 的數組表示成 4 個 8*8*1*64 的數組。所以現在,我們已經得到了第一個步驟更好的表徵(至少是在未加密數組上):


julia>cdims=DenseConvDims(batch,model.layers[1].weight;stride=(3,3),padding=(0,0,0,0),dilation=(1,1))
DenseConvDims:(28,28,1)*(7,7)->(8,8,4),stride:(3,3)pad:(0,0,0,0),dil:(1,1),flip:false
julia>a=ExplodedConvArray{eltype(batch)}(cdims,batch);
julia>model(a)
10×64Array{Float32,2}:
[snip]如何將這種表徵帶入加密的世界呢?我們需要做兩件事:

如何將這種表徵帶入加密的世界呢?我們需要做兩件事:


1. 我們想以這樣的方式加密結構體(ExplodedConvArray),以致於對每個字段(field)都能得到一個密文。然後,通過查詢該函數在原始結構上執行的操作,在加密的結構體上進行運算,並直接進行相同的同態操作。

2. 我們希望攔截某些在加密的上下文中以不同方式執行的操作。


幸運的是 Julia 提供了可以同時執行這兩個操作的抽象:使用 Cassette.jl 機制的編譯器插件。它是如何起作用的,以及如何使用它,都有些複雜,本文中不再深入介紹這部分內容。簡言之,你可以定義上下文(即「Excrypted」,然後定義在這樣的上下文中,運算是如何起作用的規則)。例如,第二個要求可以寫成:


所有這一切的最終結果是,用戶可以以最少的手工工作,寫完整個內容:


當然,就算經過了以上處理,代碼也不是最優的。加密系統的參數(例如 ℛ 環,什麼時候模轉換,什麼時候密鑰轉換等)表現出了在答案的準確性、安全性以及性能之間的取捨,而且參數很大程度上取決於正在運行的代碼。一般來說,人們希望編譯器能分析將要運行的加密代碼,為給定的安全等級和所需精度提出參數建議,然後用戶以最少的人工操作來生成代碼。


結語


對於任何系統來說,安全地自動執行任意計算都是一項艱鉅的任務,但 Julia 的元編程功能和友好的語法都讓它成為合適的開發平臺。RAMPARTS 系統已經做了一些嘗試,將簡單的 Julia 代碼編譯到 PALISADE FHE 庫中。「Julia Computing」正在與 RAMPARTS 背後的專家在 Verona 平臺上合作,最近已經發布了下一代版本。在過去的一年中,同態加密系統的性能才達到能以實際可用的速度評估有趣計算的程度。一扇嶄新的大門就此打開。隨著算法、軟件和硬件的進步,同態加密必然會成為保護數百萬用戶隱私的主流技術。


RAMPARTS 論文:https://eprint.iacr.org/2019/988.pdf

報告:https://www.youtube.com/watch?v=_KLlMg6jKQg


如果你想更深入地瞭解這一切是如何工作的,我已經試著確保了 ToyFHE 庫的可讀性。這裡還有一些文檔,希望這些文檔能幫助你進一步理解涉及到的密碼學內容。當然,還有很多工作要做。如果你對這類工作感興趣,或者有有趣的應用程序,請隨時與我們聯繫。


ToyFHE 庫:https://github.com/JuliaComputing/ToyFHE.jl其他參考文檔:https://juliacomputing.github.io/ToyFHE.jl/dev/man/background/


原文鏈接:

https://juliacomputing.com/blog/2019/11/22/encrypted-machine-learning.html


分享到:


相關文章: