[[285696]] 最近、「ブロックチェーン」や「フェデレーテッドラーニング」などの概念がかつてないほど注目を集めています。これらの概念の背後には、「準同型暗号」という欠かせない技術があります。この記事では、準同型暗号化データに基づいて機械学習を実行するために Julia 言語を使用するプロセス全体を紹介します。これは初心者にとって非常に参考になります。 注: この記事では、最先端の暗号化技術について説明し、Julia Computing を使用した研究の視点を提供することを目的としています。この記事の例を本番アプリケーションに使用しないでください。暗号化を使用する前に、必ず専門の暗号化の専門家に相談してください。 パッケージ: https://github.com/JuliaComputing/ToyFHE.jl 関連コード: https://github.com/JuliaComputing/ToyFHE.jl/blob/master/examples/encrypted_mnist/infer.jl 導入 新しく優れた機械学習モデルを開発し、それを展開してユーザーに提供したいとします。何をすべきでしょうか?最も簡単な方法は、モデルをユーザーに直接公開し、ユーザーが自分のデータを使用してローカルでモデルを実行できるようにすることです。しかし、このアプローチにはいくつか問題があります。 - 機械学習モデルは一般的に大きく、ユーザーのデバイスにはモデルを実行するのに十分なストレージ容量や計算能力がない場合があります。
- 機械学習モデルは頻繁に更新されることが多いため、このような大規模なモデルをネットワーク経由で頻繁に転送することはおそらく望ましくありません。
- 機械学習モデルの開発には多くの時間とコンピューティング リソースが必要であり、モデルの使用に対してユーザーに料金を請求することでコストを回収したい場合があります。
次に、一般的な解決策は、モデルをクラウド上のアプリケーション プログラミング インターフェイス (API) として公開することです。こうした「サービスとしての機械学習」の提供はここ数年で急増しており、すべての主要クラウド プラットフォームがエンタープライズ開発者に提供しています。 しかし、このような製品の潜在的なユーザーが直面するジレンマは明らかです。ユーザーデータを処理するリモートサーバーが信頼できない可能性があるのです。これにより、明確な倫理的および法的境界が生まれ、そのような解決策の範囲が制限されることになります。規制産業(特に医療および金融)では、患者データや財務データを処理のために第三者に送信することは通常許可されていません。もっと良い方法はないでしょうか? 実は、できるんです!最近の暗号化技術の進歩により、暗号化されたデータを復号化することなく直接計算することが可能になりました。私たちの場合、ユーザーは暗号化されたデータ(画像など)をクラウド API に渡し、クラウド API は機械学習モデルを実行して暗号化された回答を返します。プロセス全体を通じてユーザーデータは復号化されず、特にクラウド サービス プロバイダーは元の画像にアクセスすることも、計算された予測値を復号化することもできません。これはどうやって行うのですか?この記事では、暗号化された画像 (MNIST データセットから) の手書き認識用の機械学習モデルを構築することで、その背後にある原理を明らかにします。 準同型暗号(HE)の一般的な説明 一般的に、暗号化されたデータに対して計算を実行する機能は「セキュア コンピューティング」と呼ばれ、さまざまなシナリオで問題を解決するためにさまざまな暗号化方法と技術を必要とする、かなり大規模な研究分野です。この例では、「準同型暗号化」と呼ばれる手法に焦点を当てます。準同型暗号化システムでは、通常、次の操作を実行します。 - pub_key、eval_key、priv_key = keygen()
- 暗号化 = encrypt(pub_key, plaintext)
- 復号化 = decrypt(priv_key, 暗号化)
- 暗号化された′ = eval(eval_key, f, 暗号化された)
最初の 3 つの手順は非常に直感的で、以前に非対称暗号化を使用したことがある人 (TLS 経由でこの記事に接続するなど) にとっては馴染みのあるものになるでしょう。最後のステップで魔法が起こります。暗号化されたデータを使用して f を評価し、暗号化された値に基づいて f を評価した結果に対応する別の暗号化された値を返します。この特性のため、この手法は「準同型暗号化」と呼ばれます。評価操作は、次の暗号化操作と同等です。 - f(復号化(priv_key, 暗号化)) == 復号化(priv_key, eval(eval_key, f, 暗号化))
(同様に、任意の準同型 f は暗号化された値に対して評価できます。) どの関数 f がサポートされるかは、暗号化スキームとサポートされる操作によって異なります。 1 つの関数 f のみがサポートされている場合 (たとえば、f=+)、この暗号化方式を「部分準同型」と呼ぶことができます。 f が任意の回路を構築できるゲートの完全なセットである場合、回路サイズが制限されている場合は「ある程度準同型暗号化 (SHE)」と呼ばれ、回路サイズが制限されていない場合は「完全に準同型な暗号化 (FHE)」と呼ばれます。一般に、ブートストラップによって「有限」準同型を「完全」準同型に変換することは可能ですが、この問題はこの記事の範囲を超えています。 完全準同型暗号化は比較的最近の研究分野であり、最初の実用的な(ただし非実用的)アプローチは 2009 年に Craig Gentry によって公開されました。現在、より新しく、より実用的な FHE スキームがいくつか登場しています。さらに、これを効率的に実行できるソフトウェア パッケージもあります。最もよく使用される 2 つのソフトウェア パッケージは、Microsoft SEAL と PALISADE です。さらに、最近、これらのアルゴリズムの Julia 実装をオープンソース化しました (https://github.com/JuliaComputing/ToyFHE.jl)。ここでは、後者に実装されている CKKS 暗号化を使用します。 上級CKKS CKKS(2016 年の論文「近似数の算術のための準同型暗号化」で提案した Cheon-Kim-Kim-Song にちなんで命名)は、次の基本演算を準同型的に評価できる準同型暗号化方式です。 - 長さnの複素ベクトルの対応する要素を追加する
- 長さnの複素ベクトルの対応する要素を乗算する
- ベクトル内の要素の回転(円シフトによって実装)
ベクトル要素の複素共役 ここでのパラメータ n は、必要なセキュリティと精度に依存し、通常は高くなります。この例では、n=4096 です (値が大きいほど安全ですが、計算コストも高くなり、時間の複雑さはおおよそ nlog^n に比例します)。 さらに、CKKS を使用した計算にはノイズが多くなります。したがって、計算された結果は一般に近似値に過ぎず、結果の正確性に影響を与えない程度に評価結果が正確であることを確認するように注意する必要があります。 とはいえ、これらの制限は機械学習パッケージの開発者にとっては珍しいことではありません。 GPU などの専用アクセラレータは、数値のベクトルを処理することもできます。同様に、多くの開発者は、アルゴリズムの選択、マルチスレッドなどにより、浮動小数点数はノイズが多すぎると主張するでしょう。(重要な違いは、浮動小数点演算自体は決定論的ですが、実装の複雑さによりその決定論性が示されない場合もありますが、CKKS プリミティブは非常にノイズが多いことです。しかし、これにより、ノイズは最初に思ったほど恐ろしいものではないとユーザーが気付くかもしれません)。 これを念頭に置いて、Julia でこれらの操作を実行する方法を見てみましょう (注: ここでは非常に安全でないパラメータ選択がいくつかあり、これらの操作の目的は、対話型インタープリター (REPL) でのこのライブラリの使用方法を説明することです)。 - julia> ToyFHE を使用
-
- # 8要素ベクトルで遊んでみよう
-
- ジュリア> N = 8 ;
-
- # パラメータをいくつか選択します - これについては後で説明します
-
- ジュリア> ℛ = NegacyclicRing(2N, ( 40 , 40 , * 40 *))
- ℤ₁₃₂₉₂₂₇₉₉₇₅₆₈₀₈₁₄₅₇₄₀₂₇₁₀₄₂₄₈₂₅₇/(x¹⁶ + 1 )
-
- # CKKSを使用します julia> params = CKKSParams(ℛ)
- CKKSパラメータ
-
- #数値のスケール係数を選択する必要があります。これについては後で説明します。
- ジュリア> Tscale = FixedRational{ 2 ^ 40 }
- FixedRational{ 1099511627776 ,T} ここでT
-
- # ゼロの単純なベクトルから始めましょう
- ジュリア> プレーン = CKKSEncoding{Tscale}(ゼロ(ℛ))
- 8要素CKKSEncoding{FixedRational{ 1099511627776 ,T} (T)、インデックス0 : 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
-
- #準備はできましたが、まずはキーが必要です
- ジュリア> kp = keygen(パラメータ)
- CKKS キーペア
-
- ジュリア> kp.priv
- CKKS秘密鍵
-
- ジュリア> kp.pub
- CKKS公開鍵
-
- # では、いくつか暗号化してみましょう:
- ジュリア> foreach(i->plain[i] = i+ 1 , 0 : 7 ); プレーン
- 8要素CKKSEncoding{FixedRational{ 1099511627776 ,T} (T)、インデックス0 : 7 :
- 1.0 + 0.0im
- 2.0 + 0.0im
- 3.0 + 0.0im
- 4.0 + 0.0イム
- 5.0 + 0.0im
- 6.0 + 0.0イム
- 7.0 + 0.0イム
- 8.0 + 0.0イム
-
- ジュリア> c = encrypt(kp.pub, plain)
- CKKS暗号文(長さ2 、エンコード
- CKKSEncoding{FixedRational{ 1099511627776 ,T} (ただしT})
-
- # そして再び復号化する
- ジュリア> 復号化(kp.priv, c)
- 8要素CKKSEncoding{FixedRational{ 1099511627776 ,T} (T)、インデックス0 : 7 :
- 0.99999999999995506 - 2 .7335193113350057e-16im
- 1.99999999999989408 - 3 .885780586188048e-16im
- 3.0000000000000205 + 1 .6772825551165524e-16im
- 4.000000000000538 - 3 .885780586188048e-16im
- 4.9999999999998865 + 8 .382500573679615e-17im
- 6.000000000000185 + 4 .996003610813204e-16im
- 7.000000000001043 - 2 .0024593503998215e-16im
- 8.0000000000000673 + 4.996003610813204e -16im
-
- # ノイズが少しあることに注意してください。必要なすべての基本操作を確認してみましょう。
-
- julia> 復号化(kp.priv, c+c)
- 8要素CKKSEncoding{FixedRational{ 1099511627776 ,T} (T)、インデックス0 : 7 :
- 1.99999999999991012 - 5 .467038622670011e-16im
- 3.99999999999978817 - 7 .771561172376096e-16im
- 6.00000000000041 + 3.354565110233105e -16im
- 8.000000000001076 - 7 .771561172376096e-16im
- 9.999999999999773 + 1 .676500114735923e-16im
- 12.000000000000037 + 9 .992007221626409e-16im
- 14.000000000002085 - 4 .004918700799643e-16im
- 16.000000000001346 + 9 .992007221626409e-16im
-
- ジュリア> csq = c*c
- CKKS暗号文(長さ3 、エンコードCKKSEncoding{FixedRational{ 1208925819614629174706176 ,T}、ただしT})
-
- julia> decrypt(kp.priv, csq) 8要素 CKKSEncoding{FixedRational{ 1208925819614629174706176 ,T} where T} のインデックスは0 : 7 :
- 0.9999999999991012 - 2 .350516767363621e-15im
- 3.99999999999957616 - 5 .773159728050814e-15im
- 9.000000000001226 - 2 .534464540987068e-15im
- 16.000000000004306 - 2 .220446049250313e-15im
- 24.999999999998865 + 2.0903753311370056e -15im
- 36.000000000000222 + 4.884981308350689e -15im
- 49.000000000014595 + 1 .0182491378134327e-15im
- 64.00000000001077 + 4.884981308350689e -15im
とても簡単です!賢明な読者は、csq が以前の暗号文とは少し異なっていることに気付いたかもしれません。特に長さ3の暗号文であり、範囲が広くなっています。これらが何であり、何に使用されるのかを説明するのは少し複雑すぎます。先に進んでこれらの値を減らす必要があるとだけ言っておきます。そうしないと、暗号文の「スペース」が足りなくなってしまいます。幸いなことに、両方の問題を解決する方法があります。 - # 長さ2に戻すには、`keyswitch`(別名
- # relinerarize) には評価キーが必要です。生成
- #これには秘密鍵が必要です。実際のアプリケーションでは
- #これを事前に生成し、暗号化された
- # データですが、秘密鍵を持っているので、今すぐ実行できます。
- ジュリア> ek = keygen(EvalMultKey, kp.priv)
- CKKS乗算キー
-
- ジュリア> csq_length2 = キースイッチ(ek, csq)
- CKKS暗号文(長さ2 、エンコード
- CKKSEncoding{FixedRational{ 1208925819614629174706176 ,T} (ただしT})
-
-
- # スケールを元に戻すには、modswitching を使用します。
- ジュリア> csq_smaller = modswitch(csq_length2)
-
- CKKS暗号文(長さ2 、エンコード
- CKKSEncoding{FixedRational{ 1 .099511626783e12,T} (ただしT})
-
-
- # それでも正しく復号化されます (ただし、精度が若干低下していることに注意してください)
- julia> 復号化(kp.priv、csq_smaller)
- 8要素CKKSEncoding{FixedRational{ 1 .099511626783e12,T} (T)、インデックス0 : 7 :
- 0.99999999999802469 - 5 .005163520332181e-11im
- 3.99999999999957723 - 1 .0468514951188039e-11im
- 8.9999999999998249 - 4 .7588542623100616e-12im
- 16.000000000023014 - 1 .0413447889166631e-11im
- 24.9999999999955193 - 6 .187833723406491e-12im
- 36.0000000000002345 + 1 .860733715346631e-13im
- 49.00000000001647 - 1 .442396043149794e-12im
- 63.9999999999988695 - 1 .0722489563648028e-10im
また、modswitching (modulus switch の略) は暗号文のモジュラスのサイズを縮小するため、これを無期限に実行することはできません。 (上記の用語を使用して、ここでは SHE スキームを使用します):- julia> ℛ # 最初に作ったリングを思い出してください
- ℤ₁₃₂₉₂₂₇₉₉₇₅₆₈₀₈₁₄₅₇₄₀₂₇₁₀₄₂₄₈₂₅₇/(x¹⁶ + 1 )
-
- julia> ToyFHE.ring(csq_smaller) # 縮みました!
- ℤ₁₂₀₈₉₂₅₈₂₀₁₄₄₅₉₃₇₇₉₃₃₁₅₅₃/(x¹⁶ + 1 )
最後に行う操作は回転です。上記の KeySwitching と同様に、ここでもキー (ガロア キーとも呼ばれます) を評価する必要があります。 - ジュリア> gk = keygen(GaloisKey, kp.priv; ステップ = 2 )
- CKKS ガロア鍵 (要素25 )
-
- ジュリア> 復号化(circshift(c, gk))
- 復号化(kp, circshift(c, gk))
- 8要素CKKSEncoding{FixedRational{ 1099511627776 ,T} (T)、インデックス0 : 7 :
- 7.000000000001042 + 5.68459112632516e -16im
- 8.0000000000000673 + 5.551115123125783e -17im
- 0.9999999999999551 - 2 .308655353580721e-16im
- 1.99999999999989408 + 2.7755575615628914e -16im
- 3.000000000000205 - 6 .009767921608429e-16im
- 4.000000000000538 + 5.551115123125783e -17im
- 4.9999999999998865 + 4.133860996136768e -17im
- 6.000000000000185 - 1 .6653345369377348e-16im
-
- # 平文で同じことをやってみたらどうだろう
- ジュリア> circshift(プレーン, 2 )
- 8要素の OffsetArray(::Array{Complex{Float64}, 1 }, 0 : 7 )、要素型は Complex{Float64}、インデックスは0 : 7 :
- 7.0 + 0.0イム
- 8.0 + 0.0イム
- 1.0 + 0.0im
- 2.0 + 0.0im
- 3.0 + 0.0im
- 4.0 + 0.0イム
- 5.0 + 0.0im
- 6.0 + 0.0イム
さて、準同型暗号化ライブラリの基本的な使い方を学びました。これらのプリミティブをニューラル ネットワーク推論にどのように使用するかを考える前に、まず使用する必要があるニューラル ネットワークを観察してトレーニングしましょう。 機械学習モデル 機械学習や Flux.jl 機械学習ライブラリを初めて使用する場合は、暗号化されたデータでモデルを実行するために行われた変更についてのみ説明するため、まず Flux.jl のドキュメントまたは JuliaAcademy の無料入門機械学習コースをざっと見ることをお勧めします。 まず、Flux モデル空間における畳み込みニューラル ネットワークの例から始めます。このモデルでは、トレーニング ループ、データの前処理、その他の操作は変更されず、モデルはわずかに調整されるだけです。使用するモデルは次のとおりです。 - 関数 reshape_and_vcat(x)
- y = reshape(x, 64 , 4 , size(x, 4 ))とします。
- vcat((y[:,i,:] 、 i=axes(y, 2 ))...)
- 終わり
- 終わり
-
- モデル = チェーン(
- # 最初の畳み込みは 28x28 の画像に対して行われます
- 変換(( 7 , 7 ), 1 => 4 , ストライド=( 3 , 3 ), x->x.^ 2 ),
- 再形成とvcat、
- 密( 256,64 , x->x.^ 2 ) 、
- 密( 64,10 ) 、
- )
このモデルは、論文「Secure Outsourced Matrix Computation and Application to Neural Networks」で使用されているモデルと基本的に同じです。彼らは同じ暗号化方式で同じモデルを示していますが、2 つの違いがあります。(1) 彼らはモデルを暗号化しますが、私たちは暗号化しません (簡単のため)。(2) 私たちは各レイヤーの後にバイアス ベクトルがあります (これは Flux のデフォルトの動作でもあります)。これは、この論文で評価されたモデルに当てはまるかどうかはわかりません。おそらく(2)のせいで、我々のモデルの精度はわずかに高くなっている(98.6%対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 効率的な計算 さて、何をする必要があるかがわかったので、どのような操作を行う必要があるかを確認しましょう。 要素ごとの二乗は簡単であることが上でわかりましたので、残りの 2 つの問題に順番に取り組んでいきます。全体を通して、バッチ サイズは 64 と想定します (実際のパラメータ選択から得られる 4096 要素のベクトルを活用するために、モデル パラメータとバッチ サイズを戦略的に選択していることに気付くかもしれません)。 畳み込み 畳み込みがどのように機能するかを確認しましょう。まず、元の入力配列のいくつかのウィンドウ (この場合は 7*7) を取得し、ウィンドウ内の各要素を畳み込みマスクの要素で乗算します。次に、ウィンドウをシフトします (この場合、ストライドは 3 なので、ウィンドウを 3 要素だけシフトします)。このプロセスを繰り返します(同じ畳み込みマスクを使用)。以下のアニメーションは、ストライド (2, 2) で 3*3 畳み込みを実行するプロセスを示しています (青い配列は入力、緑の配列は出力です)。
さらに、畳み込みを4つの異なる「チャネル」に分割します(つまり、畳み込みは異なる畳み込みマスクでさらに3回繰り返されます)。 さて、何をしたいのかがわかったので、それをどうやって行うかを考えてみましょう。幸いなことに、畳み込みは私たちのモデルの最初の演算です。したがって、データを暗号化する前にクライアント上でデータを前処理することで(モデルの重みなしで)、作業をいくらか節約することができます。具体的には、次のことを行います。 - 各畳み込みウィンドウを事前計算し(つまり、元の画像から 7*7 ウィンドウを抽出し)、各入力画像から 64 個の 7*7 行列を取得します(ストライド 2 の 7*7 ウィンドウを取得するには、28*28 の入力画像を評価するために 8*8 畳み込みウィンドウを計算する必要があることに注意してください)。
- 各ウィンドウの同じ位置をベクトルに収集します。つまり、各画像には 64 個の要素を含むベクトルが存在します。バッチ サイズが 64 の場合、64*64 要素のベクトル (つまり、合計 49 個の 64*64 行列) が得られます。
- 暗号化
畳み込みは、行列全体と適切なマスク要素のスカラー乗算になり、これらの 49 個の要素の合計が畳み込みの結果になります。このスキームは次のように実装されます (プレーンテキスト)。 - 関数 public_preprocess(バッチ)
- ka = オフセット配列( 0 : 7 , 0 : 7 )
- # 特徴抽出マトリックスを作成する
- I = [[batch[i′* 3 .+ ( 1 : 7 ), j′* 3 .+ ( 1 : 7 ), 1 , k] i′=ka、j′=kaの場合] k = 1 : 64の場合]
-
- # 暗号文に作り直す
- Iᵢⱼ = [[I[k][l...][i,j] 、 k = 1 : 64 、l = product(ka, ka)] 、 i = 1 : 7 、j = 1 : 7 ]
- 終わり
-
- Iᵢⱼ = public_preprocess(バッチ)
-
- # 畳み込みを評価する
- 重み = model.layers[ 1 ].weight
- conv_weights = 逆(逆(重み、ディメンション = 1 )、ディメンション = 2 )
- conved = [sum(Iᵢⱼ[i,j]*conv_weights[i,j, 1 ,channel] 、 i = 1 : 7 、j = 1 : 7 ) 、 channel = 1 : 4 ]
- 凸包 = map(((x,b),)->x .+ b, zip(凸包、モデル.レイヤー[ 1 ].バイアス))
この実装 (次元の順序変更を除く) では同じ答えが得られますが、次の操作を使用します。 - model*.*layers[* 1 *](バッチ)
暗号化操作を追加すると、次のようになります。 - Iᵢⱼ = public_preprocess(バッチ)
- C_Iᵢⱼ = map(Iᵢⱼ)を実行するIij
- プレーン = CKKSEncoding{Tscale}(ゼロ(プレーンテキストスペース(ckks_params)))
- プレーン .= OffsetArray(vec(Iij), 0 :(N ÷ 2 - 1 ))
- 暗号化(kp, プレーン)
- 終わり
-
- 重み = model.layers[ 1 ].weight
- conv_weights = 逆(逆(重み、ディメンション = 1 )、ディメンション = 2 )
- conved3 = [sum(C_Iᵢⱼ[i,j]*conv_weights[i,j, 1 ,channel] 、 i = 1 : 7 、j = 1 : 7の場合)、 channel = 1 : 4の場合]
- conved2 = map(((x,b),)->x .+ b, zip(conved3, model.layers[ 1 ].bias))
- conved1 = map(ToyFHE.modswitch、conved2)
重みは公開されているため、キーのローテーションは必要なく、暗号文の長さは延長されないことに注意してください。 行列の乗算 次に、行列乗算がどのように実装されるかを見てみましょう。ベクトルの要素を回転して乗算インデックスを並べ替えることができるという事実を利用します。特に、ベクトル内の行列要素の行優先の順序を考慮してください。次に、ベクトルを行サイズの倍数だけシフトすると、列回転の効果が得られ、行列乗算 (少なくとも正方行列の) を実装するのに十分なプリミティブが提供されます。これを試してみましょう: - 関数 matmul_square_reordered(重み, x)
- 合計( 1 :サイズ(重み、 1 )) kを実行します
- # 左辺の列を回転させて対角線を取ります
- weight_diag = diag(circshift(重み, ( 0 ,(k - 1 ))))
- # 右辺の行を回転します
- x_rotated = circshift(x, (k- 1 , 0 ))は回転します。
- #要素ごとにブロードキャスト乗算を実行します
- 重み_diag .* x_rotated
- 終わり
- 終わり
-
- 関数 matmul_reorderd(重み, x)
- sum(partition( 1 : 256 , 64 ))範囲を実行する
- matmul_square_reordered(重み[:, 範囲], x[範囲, :])
- 終わり
- 終わり
-
- fc1_weights = モデル.レイヤー[ 3 ].W
- x = ランド(Float64, 256 , 64 )
- @assert (fc1_weights*x) ≈ matmul_reorderd(fc1_weights, x)
もちろん、一般的な行列乗算にはもっと良い方法が必要になるかもしれませんが、この例では今のところこれで十分です。 コードの最適化 この時点で、私たちはすべてをまとめることができ、実際に機能しました。参考までに、以下のコードを示します (パラメータの選択やその他の設定は省略しています)。 - ek = keygen(EvalMultKey、kp.priv)
- gk = keygen(GaloisKey, kp.priv; ステップ = 64 )
-
- Iᵢⱼ = public_preprocess(バッチ)
- C_Iᵢⱼ = map(Iᵢⱼ)を実行するIij
- プレーン = CKKSEncoding{Tscale}(ゼロ(プレーンテキストスペース(ckks_params)))
- プレーン .= OffsetArray(vec(Iij), 0 :(N ÷ 2 - 1 ))
- 暗号化(kp, プレーン)
- 終わり
-
- 重み = model.layers[ 1 ].weight
- conv_weights = 逆(逆(重み、ディメンション = 1 )、ディメンション = 2 )
- conved3 = [sum(C_Iᵢⱼ[i,j]*conv_weights[i,j, 1 ,channel] 、 i = 1 : 7 、j = 1 : 7の場合)、 channel = 1 : 4の場合]
- conved2 = map(((x,b),)->x .+ b, zip(conved3, model.layers[ 1 ].bias))
- conved1 = map(ToyFHE.modswitch、conved2)
-
- Csqed1 = map(x->x*x, 変換1)
- Csqed1 = map(x->keyswitch(ek, x), Csqed1)
- Csqed1 = map(ToyFHE.modswitch、Csqed1)
-
- 関数encrypted_matmul(gk, weights, x::ToyFHE.CipherText)
- 結果 = repeat(diag(weights), inner= 64 ).*x
- 回転 = x
- k = 2の場合: 64
- 回転 = ToyFHE.rotate(gk, 回転)
- 結果 += repeat(diag(circshift(weights, ( 0 ,(k- 1 )))), inner= 64 ) .* 回転
- 終わり
- 結果
- 終わり
-
- fq1_weights = モデル.レイヤー[ 3 ].W
- Cfq1 = sum(enumerate(partition( 1 : 256 , 64 ))) do (i,range)
- 暗号化されたmatmul(gk, fq1_weights[:, 範囲], Csqed1[i])
- 終わり
-
- Cfq1 = Cfq1 .+ OffsetArray(repeat(model.layers[ 3 ].b, inner= 64 ), 0 : 4095 )
- Cfq1 = modswitch(Cfq1)
-
- Csqed2 = Cfq1*Cfq1
- Csqed2 = キースイッチ(ek, Csqed2)
- Csqed2 = modswitch(Csqed2)
-
- 関数 naive_rectangular_matmul(gk, 重み, x)
- @assertサイズ(重み, 1 ) < サイズ(重み, 2 )
- 重み = vcat(重み、ゼロ(eltype(重み)、サイズ(重み、 2 )-サイズ(重み、 1 )、サイズ(重み、 2 )))
- 暗号化されたmatmul(gk, 重み, x)
- 終わり
-
- fq2_weights = モデル.レイヤー[ 4 ].W
- Cresult = naive_rectangular_matmul(gk, fq2_weights, Csqed2)Cresult = Cresult .+ OffsetArray(repeat(vcat(model.layers[ 4 ].b,
- ゼロ( 54 )、内部= 64、0 : 4095 )
コードはあまり明確に見えないかもしれませんが、ここまで来たら、プロセスのすべてのステップを理解しているはずです。 さて、これらすべてをもう少し理解しやすくする抽象化に注目してみましょう。まず、暗号化と機械学習の分野を超えて、プログラミング言語の設計の問題について考えてみましょう。 Julia は強力な抽象化を可能にし、これを利用していくつかの抽象化を構築することができます。たとえば、畳み込み抽出プロセス全体をカスタム配列型としてカプセル化できます。 - BlockArraysを使用する
-
- 「」 「 」
- ExplodedConvArray{T, Dims, Storage} <: AbstractArray{T, 4 }
-
- 画像の`nxmx1xb`配列を表しますが、
- 畳み込みウィンドウのシリーズ。畳み込み互換の評価
- この配列の「Dims」は、次のシーケンスで実現できます。
- 基礎ストレージ上のスカラー乗算と合計。
- 「」 「 」
- 構造体ExplodedConvArray{T, Dims, Storage} <: AbstractArray{T, 4 }
- # 抽出された要素の b*(dx*dy) 行列の sx*sy 行列
- # ここで (sx, sy) = kernel_size(Dims)
- # (dx, dy) = output_size(DenseConvDims(...))
- cdims::ディメンション
- x::Matrix{ストレージ}
- 関数 ExplodedConvArray{T, Dims, Storage}(cdims::Dims, storage::Matrix{Storage}) ここで {T, Dims, Storage}
- @assert all(==(サイズ(ストレージ[ 1 ])), サイズ.(ストレージ))
- 新しい{T, Dims, Storage}(cdims, storage)
- 終わり
- 終わり
- Base.size(ex::ExplodedConvArray) = (NNlib.input_size(ex.cdims)..., 1 、サイズ(ex.x[ 1 ], 1 ))
-
- 関数ExplodedConvArray{T}(cdims, batch::AbstractArray{T, 4 }) ここで{T}
- x, y = NNlib.output_size(cdims)
- kx,ky = NNlib.kernel_size(cdims)
- stridex、stridey = NNlib.stride(cdims)
- kax = オフセット配列( 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] i′=kax, j′=kayの場合] k = 1 : size(batch, 4 )]
- Iᵢⱼ = [[I[k][l...][i,j]
- k = 1 :size(batch, 4 ), l = product(kax, kay)] (i,j) が product( 1 :kx, 1 :ky)]の場合
-
- ExplodedConvArray{T, typeof(cdims), eltype(Iᵢⱼ)}(cdims, Iᵢⱼ)
- 終わり
-
- 関数NNlib.conv(x::ExplodedConvArray{<:Any, Dims},
- weights::AbstractArray{<:Any, 4 }, cdims::Dims) ここで {Dims<:ConvDims}
- ブロック = 形状変更([
- Base.ReshapedArray(sum(xx[i,j]*weights[i,j, 1 ,channel] 、 i= 1 : 7 、j= 1 : 7 )、(NNlib.output_size(cdims)..., 1 ,size(x, 4 ) ) 、()) 、 channel = 1 : 4 ] 、 ( 1、1、4、1 ) )
- BlockArrays._BlockArray(ブロック、BlockArrays.BlockSizes([ 8 ], [ 8 ], [ 1 , 1 , 1 , 1 ], [ 64 ]))
- 終わり
元のコードに示されているように、ここでは 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 )、ストライド: ( 3 , 3 )、パッド: ( 0 , 0 , 0 , 0 )、dil: ( 1 , 1 )、反転: false
-
- julia> a = ExplodedConvArray{eltype(batch)}(cdims, batch);
- ジュリア> モデル(a)
- 10 × 64配列{Float32, 2 }:
- [snip] この特徴をどのように取り入れるか
この表現を暗号通貨の世界にどのように取り入れるのでしょうか?次の 2 つのことを行う必要があります。 1. 各フィールドの暗号文を取得するような方法で構造体 (ExplodedConvArray) を暗号化します。次に、関数が元の構造に対して実行した操作を照会することで、暗号化された構造に対して操作を行い、同じ準同型操作を直接実行します。 2. 暗号化のコンテキストで異なる方法で実行される特定の操作を傍受したいと考えています。 幸いなことに、Julia は Cassette.jl メカニズムを使用するコンパイラ プラグインという、両方を実行できる抽象化を提供します。その仕組みと使用方法は少し複雑なので、この記事では詳しく説明しません。つまり、コンテキスト (つまり、「暗号化済み」) を定義し、そのようなコンテキスト内で操作がどのように機能するかについてのルールを定義します。たとえば、2 番目の要件は次のように記述できます。 これらすべての結果として、ユーザーは最小限の手作業でコンテンツ全体を作成できるようになります。 もちろん、上記の処理を行った後でもコードは最適ではありません。暗号システムのパラメータ (例: ℛ リング、モジュロを切り替えるタイミング、キーを切り替えるタイミングなど) は、回答の精度、セキュリティ、パフォーマンスの間のトレードオフを表し、実行されるコードに大きく依存します。一般的には、コンパイラーが実行する暗号化コードを分析し、指定されたセキュリティ レベルと必要な精度のパラメーターを提案し、ユーザー側の手動作業を最小限に抑えてコードを生成することが望まれます。 結論 任意の計算を安全に自動化することはどのシステムにとっても難しい作業ですが、Julia のメタプログラミング機能と使いやすい構文により、Julia は適切な開発プラットフォームになります。 RAMPARTS システムでは、単純な Julia コードを PALISADE FHE ライブラリにコンパイルする試みがいくつか行われてきました。 Julia Computing は、Verona プラットフォームで RAMPARTS の専門家と協力し、最近次世代バージョンをリリースしました。準同型暗号システムのパフォーマンスが、興味深い計算を実際に使用可能な速度で評価できるレベルに達したのは、ここ 1 年だけです。新たな扉が開きました。アルゴリズム、ソフトウェア、ハードウェアの進歩により、準同型暗号化は必然的に何百万ものユーザーのプライバシーを保護する主流の技術になるでしょう。 RAMPARTS 論文: https://eprint.iacr.org/2019/988.pdf レポート: https://www.youtube.com/watch?v=_KLlMg6jKQg |