Menggunakan data terenkripsi untuk pembelajaran mesin tanpa mendekripsi
Artikel ini membahas teknik kriptografi canggih. Ini hanya gambaran umum dari penelitian yang dilakukan oleh Julia Computing. Jangan gunakan contoh yang diberikan di sini dalam aplikasi komersial. Selalu berkonsultasi dengan kriptografi sebelum menerapkan kriptografi.
Di sini Anda dapat mengunduh paket yang mengimplementasikan semua keajaiban, dan di
sini adalah kode yang dibahas dalam artikel.
Pendahuluan
Katakanlah Anda baru saja mengembangkan model pembelajaran mesin baru yang keren (tentu saja, menggunakan
Flux.jl ). Dan sekarang Anda ingin mulai menyebarkannya untuk pengguna Anda. Bagaimana Anda akan melakukan ini? Mungkin cara termudah adalah memberikan model kepada pengguna dan membiarkannya berjalan secara lokal pada data mereka. Tetapi pendekatan ini memiliki kelemahan:
- Model pembelajaran mesin berukuran besar, dan komputer pengguna mungkin tidak memiliki cukup sumber daya komputasi atau disk.
- Model pembelajaran mesin sering diperbarui, dan mungkin tidak nyaman bagi Anda untuk secara teratur mengirim data dalam jumlah besar melalui jaringan.
- Pengembangan model memakan waktu dan membutuhkan banyak sumber daya komputasi. Dan Anda mungkin ingin kompensasi untuk ini dalam bentuk biaya untuk menggunakan model Anda.
Kemudian mereka biasanya ingat bahwa model dapat disediakan di cloud melalui API. Selama beberapa tahun terakhir, banyak layanan seperti itu telah muncul, masing-masing platform cloud besar menawarkan layanan serupa dengan pengembang perusahaan. Tetapi pengguna potensial dihadapkan pada dilema yang jelas: sekarang data mereka diproses pada server jauh, yang mungkin tidak dapat dipercaya. Ini memiliki implikasi etis dan hukum yang jelas yang membatasi penggunaan layanan tersebut. Dalam industri yang diatur, terutama layanan kesehatan dan keuangan, seringkali tidak mungkin untuk mengirim data pasien dan klien ke pihak ketiga untuk diproses.
Ada opsi lain?
Ternyata ada! Penemuan terbaru dalam kriptografi memungkinkan komputasi dengan data
tanpa mendekodekannya . Misalnya, pengguna mengirim data terenkripsi (katakanlah, gambar) ke cloud API yang meluncurkan model pembelajaran mesin, dan kemudian mengirim respons terenkripsi. Tanpa tahap data didekripsi, penyedia cloud tidak mendapatkan akses ke gambar sumber dan tidak dapat mendekripsi perkiraan ramalan. Bagaimana ini mungkin? Mari kita cari tahu contoh membuat layanan untuk pengenalan tulisan tangan pada gambar yang dienkripsi dari dataset MNIST.
Tentang enkripsi homomorfik
Kemampuan untuk melakukan perhitungan dengan data terenkripsi biasanya disebut sebagai "komputasi aman". Ini adalah area yang luas untuk penelitian, dengan banyak pendekatan untuk kriptografi tergantung pada semua jenis skenario aplikasi. Kami akan fokus pada teknik yang disebut "enkripsi homomorfik". Dalam sistem seperti itu, operasi berikut biasanya tersedia untuk kami:
pub_key, eval_key, priv_key = keygen()
encrypted = encrypt(pub_key, plaintext)
decrypted = decrypt(priv_key, encrypted)
encryptedβ² = eval(eval_key, f, encrypted)
Tiga operasi pertama sederhana dan akrab bagi semua orang yang telah menggunakan algoritma enkripsi asimetris apa pun (misalnya, jika Anda terhubung melalui TLS). Semua keajaiban terjadi dalam operasi terakhir. Selama enkripsi, ia mengevaluasi fungsi
f
dan mengembalikan nilai terenkripsi lain yang dihitung sesuai dengan hasil evaluasi
f
pada nilai terenkripsi. Fitur ini memberi pendekatannya namanya. Penilaian terkait dengan operasi enkripsi:
f(decrypt(priv_key, encrypted)) == decrypt(priv_key, eval(eval_key, f, encrypted))
Demikian pula, menggunakan nilai terenkripsi, kita dapat mengevaluasi homomorfisme sewenang-wenang
f
.
Fungsi mana
f
didukung tergantung pada skema kriptografi dan operasi yang didukung. Jika hanya satu
f
didukung (misalnya,
f = +
), maka rangkaian disebut "sebagian homomorfik". Jika
f
dapat berupa serangkaian gateway yang lengkap, berdasarkan skema yang dapat dibuat sewenang-wenang, maka untuk skema ukuran terbatas ini disebut jenis lain dari perhitungan homomorfik sebagian - "agak homomorfik", dan untuk ukuran tak terbatas - perhitungan "sepenuhnya homomorfik". Anda dapat mengubah "dalam beberapa cara" menjadi enkripsi yang sepenuhnya homomorfik menggunakan teknik bootstrap, tetapi ini di luar ruang lingkup artikel kami. Enkripsi sepenuhnya homomorfik adalah penemuan yang relatif baru, skema kerja pertama (meskipun tidak praktis) diterbitkan oleh
Craig Gentry pada tahun 2009 . Ada beberapa skema yang kemudian (dan praktis) sepenuhnya homomorfis. Ada juga paket perangkat lunak yang secara kualitatif mengimplementasikan skema ini. Paling sering mereka menggunakan
Microsoft SEAL dan
PALISADE . Selain itu, saya baru-baru ini membuka kode implementasi untuk algoritma
Pure Julia ini . Untuk artikel ini, kami akan menggunakan enkripsi CKKS yang diterapkan di dalamnya.
Ikhtisar CKS
CKKS (dengan nama penulis
karya ilmiah Cheon-Kim-Kim-Song, yang mengusulkan algoritma pada 2016) adalah skema enkripsi homomorfik yang memungkinkan evaluasi homomorfik dari operasi primitif berikut:
- Penambahan elemen dari panjang
n
vektor bilangan kompleks.
- Penggandaan elemen-bijaksana dari panjang
n
vektor kompleks.
- Putar elemen (dalam konteks
circshift
) dalam vektor.
- Pasangan terintegrasi elemen vektor.
Parameter
n
tergantung pada tingkat keamanan dan akurasi yang diinginkan, dan biasanya cukup tinggi. Dalam contoh kita, itu akan sama dengan 4096 (nilai yang lebih tinggi meningkatkan keamanan, tetapi juga lebih sulit dalam perhitungan, skalanya kira-kira seperti
n log n
).
Selain itu, perhitungan menggunakan CKKS
berisik . Oleh karena itu, hasilnya adalah perkiraan, dan harus diperhatikan bahwa hasilnya dievaluasi dengan akurasi yang cukup agar tidak mempengaruhi kebenaran hasil.
Di sisi lain, pembatasan seperti itu tidak biasa bagi pengembang paket pembelajaran mesin. Akselerator khusus seperti GPU juga biasanya beroperasi dengan vektor angka. Selain itu, bagi banyak pengembang, angka floating point kadang-kadang terlihat berisik karena pengaruh algoritma pemilihan, multithreading, dan sebagainya. Saya ingin menekankan bahwa perbedaan utama di sini adalah bahwa perhitungan aritmatika dengan angka floating point pada awalnya bersifat deterministik, bahkan jika ini tidak jelas karena kompleksitas implementasinya, meskipun primitif CKKS benar-benar berisik. Tapi mungkin ini memungkinkan pengguna untuk memahami bahwa suara itu tidak seseram kelihatannya.
Sekarang mari kita lihat bagaimana Anda dapat melakukan operasi ini di Julia (catatan: parameter yang sangat tidak aman dipilih, dengan operasi ini kami hanya menggambarkan penggunaan perpustakaan di REPL).
julia> using ToyFHE
Sangat sederhana! Pembaca yang penuh perhatian mungkin memperhatikan bahwa CSQ sedikit berbeda dari ciphertext sebelumnya. Secara khusus, ciphertext memiliki "panjang 3" dan skalanya jauh lebih besar. Penjelasan tentang apa ini dan apa yang dibutuhkan berada di luar cakupan artikel ini. Cukuplah untuk mengatakan bahwa kita perlu menurunkan nilai sebelum melanjutkan dengan perhitungan, jika tidak, "tempat" akan berakhir pada ciphertext. Untungnya, kita dapat mengurangi masing-masing dari dua nilai yang meningkat:
Selain itu, modswitching (kependekan dari modulus switching, switching modul) mengurangi ukuran modul ciphertext, sehingga kami tidak dapat terus melakukan ini tanpa batas waktu (kami menggunakan skema enkripsi yang agak homomorfik):
julia> β
Kami membahas dasar-dasar penggunaan perpustakaan HE. Tetapi sebelum beralih menggunakan primitif ini untuk menghitung perkiraan jaringan saraf, mari kita lihat proses mempelajarinya.
Model pembelajaran mesin
Jika Anda tidak terbiasa dengan pembelajaran mesin atau pustaka Flux.jl, maka saya sarankan menjalankan singkat melalui
dokumentasi Flux.jl atau melihat
pengantar gratis
untuk pembelajaran mesin , karena kami hanya akan membahas perubahan dalam menerapkan model untuk data terenkripsi.
Mari kita mulai dengan menggunakan jaringan saraf convolutional
dari kebun binatang Flux . Kami akan melakukan siklus pelatihan yang sama, dengan persiapan data dan sebagainya, hanya menyiapkan model sedikit. Ini dia:
function reshape_and_vcat(x) let y=reshape(x, 64, 4, size(x, 4)) vcat((y[:,i,:] for i=axes(y,2))...) end end model = Chain(
Ini adalah model yang sama seperti dalam karya
"Secure Outsourced Matrix Computation and Application to Neural Networks" , yang menggunakan skema kriptografi yang sama dengan dua perbedaan: 1) demi kesederhanaan, kami tidak mengenkripsi model itu sendiri, dan 2) setelah setiap lapisan yang kami miliki Vektor Bayesian digunakan (dalam Flux ini dilakukan secara default), saya tidak yakin apa itu dalam karya yang disebutkan. Mungkin, karena poin kedua, akurasi pada set uji model kami ternyata sedikit lebih tinggi (98,6% berbanding 98,1%), tetapi perbedaan hiperparametrik juga bisa menjadi alasannya.
Unusual (bagi mereka yang memiliki pengalaman dalam pembelajaran mesin) adalah aktivasi fungsi. Paling sering dalam kasus seperti itu mereka menggunakan
tanh
,
relu
atau sesuatu yang lebih fantastis. Tetapi meskipun fungsi-fungsi ini (terutama
relu
) mudah dihitung untuk nilai teks biasa, namun, mereka mungkin membutuhkan banyak sumber daya komputasi untuk mengevaluasi mereka dalam bentuk terenkripsi (kami biasanya memperkirakan perkiraan polinomial). Untungnya, dalam hal ini
x.^2
berfungsi dengan baik.
Sisa siklus belajar tetap sama. Kami menghapus
softmax
dari model untuk kehilangan-fungsi
logitcrossentropy
(Anda dapat meninggalkannya dan mengevaluasi softmax setelah dekripsi pada klien). Kode lengkap untuk melatih model terletak
pada GitHub , kode ini berjalan dalam beberapa menit pada kartu video baru.
Operasi yang efektif
Sekarang kita tahu operasi apa yang perlu kita lakukan:
- Koagulasi.
- Elemen kuadrat.
- Perkalian matriks.
Dengan mengkuadratkan semuanya sederhana, kami telah memeriksanya di atas, jadi kami akan mempertimbangkan dua operasi lainnya. Kami berasumsi bahwa panjang paket data adalah 64 (Anda mungkin memperhatikan bahwa parameter model dan ukuran paket dipilih untuk memanfaatkan vektor elemen-4096 yang kami peroleh sebagai hasil dari pilihan parameter yang realistis).
Koagulasi
Ingat bagaimana koagulasi bekerja. Ambil jendela (dalam kasus kami 7x7) dari array input asli, dan setiap elemen jendela dikalikan dengan elemen masker konvolusi. Kemudian kami memindahkan jendela ke beberapa langkah (dalam kasus kami, langkahnya adalah 3, yaitu, kami memindahkan 3 elemen) dan mengulangi prosesnya (dengan topeng konvolusi yang sama). Animasi proses (
sumber ) untuk konvolusi 3x3 dengan langkah
(2, 2)
ditunjukkan di bawah ini (array-input biru, output-hijau):
Selain itu, kami melakukan konvolusi dalam empat "saluran" yang berbeda (yaitu, kami mengulangi konvolusi 3 kali lebih banyak dengan masker yang berbeda).
Sekarang kita tahu apa yang harus dilakukan, masih harus mengerti caranya. Kami beruntung konvolusi adalah operasi pertama dalam model kami. Akibatnya, untuk menghemat sumber daya, kami dapat melakukan pra-proses data pada klien, dan kemudian mengenkripsi mereka (tanpa menggunakan bobot). Mari kita lakukan ini:
- Pertama, kami menghitung setiap jendela konvolusi (yaitu, sampel 7x7 dari gambar sumber), yang memberi kami 64 matriks 7x7 untuk setiap gambar input. Perhatikan bahwa untuk jendela 7x7 dengan kelipatan 2, akan ada jendela konvolusi 8x8 untuk mengevaluasi gambar input 28x28.
- Mari kita kumpulkan dalam satu vektor posisi yang sama di setiap jendela. Yaitu, untuk setiap gambar kita akan memiliki vektor 64-elemen, atau vektor 64x64 elemen untuk paket ukuran 64 (total 49 matriks 64 64 x 64).
- Kami akan mengenkripsi.
Kemudian koagulasi berubah menjadi perkalian skalar dari seluruh matriks dengan elemen topeng yang sesuai. Dan menyimpulkan semua 49 elemen, kita mendapatkan hasil lipat. Beginilah implementasi dari strategi ini (dalam teks biasa):
function public_preprocess(batch) ka = OffsetArray(0:7, 0:7)
Ini (modul untuk mengubah dimensi) (modulo - mengubah urutan ukuran) memberikan jawaban yang sama dengan model operasi.
model.layers[1](batch)
.
Tambahkan operasi enkripsi:
Iα΅’β±Ό = public_preprocess(batch) C_Iα΅’β±Ό = map(Iα΅’β±Ό) do Iij 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] for i=1:7, j=1:7) for channel = 1:4] conved2 = map(((x,b),)->x .+ b, zip(conved3, model.layers[1].bias)) conved1 = map(ToyFHE.modswitch, conved2)
Harap dicatat bahwa keyswitch tidak diperlukan di sini karena bobot bersifat publik. Jadi kami tidak menambah panjang ciphertext.
Perkalian matriks
Beralih ke perkalian matriks, kita dapat menggunakan rotasi elemen dalam vektor untuk mengubah urutan indeks perkalian. Pertimbangkan penempatan elemen matriks dalam vektor. Jika kita menggeser vektor dengan kelipatan ukuran baris, kita mendapatkan efek rotasi kolom, yang merupakan primitif yang cukup untuk menerapkan perkalian matriks (setidaknya matriks kuadrat). Mari kita coba:
function matmul_square_reordered(weights, x) sum(1:size(weights, 1)) do k
Tentu saja, untuk perkalian matriks umum, sesuatu yang lebih rumit diperlukan, tetapi untuk sekarang ini sudah cukup.
Memperbaiki teknik
Sekarang semua komponen pekerjaan teknik kami. Ini adalah keseluruhan kode (kecuali untuk pengaturan opsi pemilihan dan hal-hal serupa):
ek = keygen(EvalMultKey, kp.priv) gk = keygen(GaloisKey, kp.priv; steps=64) Iα΅’β±Ό = public_preprocess(batch) C_Iα΅’β±Ό = map(Iα΅’β±Ό) do Iij 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] for i=1:7, j=1:7) for 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, conved1) Csqed1 = map(x->keyswitch(ek, x), Csqed1) Csqed1 = map(ToyFHE.modswitch, Csqed1) function encrypted_matmul(gk, weights, x::ToyFHE.CipherText) result = repeat(diag(weights), inner=64).*x rotated = x for k = 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) function naive_rectangular_matmul(gk, weights, x) @assert size(weights, 1) < size(weights, 2) 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)
Itu tidak terlihat terlalu rapi, tetapi jika Anda melakukan semua ini, Anda harus memahami setiap langkah.
Sekarang mari kita pikirkan abstraksi apa yang dapat menyederhanakan hidup kita. Kami meninggalkan bidang kartografi dan pembelajaran mesin dan beralih ke arsitektur bahasa pemrograman, jadi mari manfaatkan fakta bahwa Julia memungkinkan Anda untuk menggunakan dan membuat abstraksi yang kuat. Misalnya, Anda dapat merangkum seluruh proses ekstraksi konvolusi ke dalam tipe array Anda:
using BlockArrays """ ExplodedConvArray{T, Dims, Storage} <: AbstractArray{T, 4} Represents a an `nxmx1xb` array of images, but rearranged into a series of convolution windows. Evaluating a convolution compatible with `Dims` on this array is achievable through a sequence of scalar multiplications and sums on the underling storage. """ struct ExplodedConvArray{T, Dims, Storage} <: AbstractArray{T, 4}
Di sini kita kembali menggunakan
BlockArrays
untuk mewakili array
8x8x4x64
sebagai empat array
8x8x1x64
seperti dalam kode sumber. Sekarang presentasi tahap pertama telah menjadi jauh lebih indah, paling tidak dengan array yang tidak terenkripsi:
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Γ64 Array{Float32,2}: [snip]
Sekarang bagaimana kita menghubungkan ini dengan enkripsi? Untuk melakukan ini, Anda perlu:
- Enkripsi strukturnya (
ExplodedConvArray
) sehingga kita mendapatkan ciphertext untuk setiap bidang. Operasi dengan struktur terenkripsi akan memverifikasi apa fungsi akan lakukan dengan struktur asli, dan melakukan hal yang sama secara homomorfis.
- Mencegat operasi tertentu untuk melakukannya secara berbeda dalam konteks terenkripsi.
Untungnya, Julia memberi kita abstraksi untuk ini: sebuah plugin kompiler yang menggunakan mekanisme
Cassette.jl . Saya tidak akan memberi tahu Anda apa itu dan bagaimana cara kerjanya, saya akan singkat mengatakan bahwa itu dapat menentukan konteks, misalnya,
Encrypted
, dan kemudian mendefinisikan aturan bagaimana operasi harus bekerja dalam konteks ini. Misalnya, Anda dapat menulis ini untuk persyaratan kedua:
Akibatnya, pengguna akan dapat menulis semua hal di atas dengan jumlah minimum pekerjaan manual:
kp = keygen(ckks_params) ek = keygen(EvalMultKey, kp.priv) gk = keygen(GaloisKey, kp.priv; steps=64)
, . ( β, modswitch, keyswitch ..) , . , , , , .
Kesimpulan
β . Julia . RAMPARTS (
paper ,
JuliaCon talk ) : Julia- - PALISADE. Julia Computing RAMPARTS Verona,
. , . . , , .
,
ToyFHE .
, , , .