استخدام البيانات المشفرة للتعلم الآلي دون فك تشفيرها
تتناول هذه المقالة تقنيات التشفير المتقدمة. هذه مجرد نظرة عامة على الأبحاث التي أجرتها جوليا للحوسبة. لا تستخدم الأمثلة الواردة هنا في التطبيقات التجارية. استشر دائما مع cryptographers قبل تطبيق التشفير.
هنا يمكنك تنزيل الحزمة التي تنفذ كل السحر ،
وهنا هو الكود الذي تمت مناقشته في المقال.
مقدمة
لنفترض أنك طورت للتو نموذجًا جديدًا رائعًا للتعلم الآلي (بالطبع ، باستخدام
Flux.jl ). والآن تريد البدء في نشرها للمستخدمين. كيف ستفعل هذا؟ ربما تكون أسهل طريقة هي إعطاء النموذج للمستخدمين وتركه يعمل محليًا على بياناتهم. ولكن هذا النهج له عيوب:
- تعد نماذج التعلم الآلي كبيرة ، وقد لا تحتوي أجهزة الكمبيوتر الخاصة بالمستخدم على موارد كافية للحوسبة أو الأقراص.
- غالبًا ما يتم تحديث نماذج التعلم الآلي ، وقد لا يكون من المناسب لك إرسال كميات كبيرة من البيانات بانتظام عبر الشبكة.
- يستغرق تطوير النموذج وقتًا طويلاً ويتطلب قدرا كبيرا من موارد الحوسبة. وقد ترغب في الحصول على تعويض عن ذلك في شكل رسوم لاستخدام النموذج الخاص بك.
ثم يذكرون عادة أنه يمكن توفير النموذج في السحابة من خلال واجهة برمجة التطبيقات. على مدى السنوات القليلة الماضية ، ظهرت العديد من هذه الخدمات ؛ كل منصة سحابة كبيرة تقدم خدمات مماثلة لمطوري الشركات. لكن المستخدمين المحتملين يواجهون معضلة واضحة: الآن تتم معالجة بياناتهم على خادم بعيد ، والتي قد لا تكون جديرة بالثقة. وهذا له آثار أخلاقية وقانونية واضحة تحد من استخدام هذه الخدمات. في الصناعات الخاضعة للتنظيم ، لا سيما الرعاية الصحية والخدمات المالية ، غالبًا ما يكون من غير الممكن إرسال بيانات المريض والعميل إلى أطراف ثالثة للمعالجة.
أي خيارات أخرى؟
اتضح أن هناك! تسمح الاكتشافات الحديثة في التشفير بالحوسبة مع البيانات
دون فك تشفيرها . على سبيل المثال ، يرسل المستخدم بيانات مشفرة (على سبيل المثال ، صور) إلى API السحابية ، التي تطلق نموذجًا للتعلم الآلي ، ثم ترسل استجابة مشفرة. في أي مرحلة يتم فك تشفير البيانات ، لا يتمكن موفر السحابة من الوصول إلى الصور المصدر ولا يمكنه فك تشفير التوقعات المحسوبة. كيف هذا ممكن؟ دعونا نتعرف على مثال إنشاء خدمة للتعرف على خط اليد على الصور المشفرة من مجموعة بيانات MNIST.
حول تشفير homomorphic
يشار عادةً إلى القدرة على إجراء العمليات الحسابية باستخدام البيانات المشفرة باسم "الحوسبة الآمنة". هذه مساحة كبيرة للبحث ، مع العديد من الأساليب للتشفير اعتمادًا على جميع أنواع سيناريوهات التطبيق. سوف نركز على تقنية تسمى "تشفير متماثل". في مثل هذا النظام ، عادة ما تكون العمليات التالية متاحة لنا:
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 = +
) ، فإن الدائرة تسمى "متماثل جزئيًا". إذا كانت
f
يمكن أن تكون أي مجموعة كاملة من البوابات ، على أساسها يمكن إنشاء مخططات تعسفية ، فإن هذا بالنسبة لحجم محدود من المخطط يسمى نوعًا آخر من الحسابات المتجانسة جزئيًا - "شكل متماثل إلى حد ما" ، ولحساب غير محدود - "حساب متماثل تمامًا". يمكنك تحويل "بطريقة ما" إلى تشفير متماثل تمامًا باستخدام تقنية bootstrapping ، لكن هذا يتجاوز نطاق مقالتنا. تشفير متماثل الشكل بالكامل هو اكتشاف حديث نسبياً ، تم نشر أول مخطط عمل (وإن كان غير عملي) من قبل
كريج جينتري في عام 2009 . هناك عدد من المخططات المتجانسة تمامًا (والعملية). هناك أيضًا حزم برامج تنفذ هذه المخططات نوعيًا. في أغلب الأحيان يستخدمون
Microsoft SEAL و
PALISADE . بالإضافة إلى ذلك ، قمت مؤخرًا بفتح رمز التنفيذ لخوارزميات
Pure Julia . بالنسبة لهذه المقالة ، سوف نستخدم تشفير CKKS المطبق فيه.
نظرة عامة على CKS
CKKS (بأسماء مؤلفي
العمل العلمي Cheon-Kim-Kim-Kim-Song ، الذين اقترحوا الخوارزمية في عام 2016) هو نظام تشفير متماثل الشكل يسمح بتقييم التماثل الذاتي للعمليات البدائية التالية:
- الإضافة الأولية لأطوال موجات
n
للأعداد المركبة.
- الضرب الحكيم للعنصر من أطوار النواقل المعقدة.
- تدوير (في سياق
circshift
) عناصر في ناقل.
- الاقتران المتكامل لعناصر المتجهات.
تعتمد المعلمة
n
على المستوى المطلوب من الأمان والدقة ، وعادة ما تكون عالية جدًا. في المثال الخاص بنا ، سيكون مساويًا 4096 (تزيد القيمة الأعلى من الأمان ، ولكنها أيضًا أكثر صعوبة في العمليات الحسابية ، فهي تقاس تقريباً مثل
n log n
).
بالإضافة إلى ذلك ، الحسابات باستخدام CKKS
صاخبة . لذلك ، فإن النتائج تقريبية ، ويجب الحرص على تقييم النتائج بدقة كافية حتى لا تؤثر على صحة النتيجة.
من ناحية أخرى ، فإن هذه القيود ليست غير عادية بالنسبة لمطوري حزم التعلم الآلي. عادة ما تعمل المعجلات الخاصة مثل GPU مع ناقلات الأرقام. بالإضافة إلى ذلك ، بالنسبة إلى العديد من المطورين ، تبدو أرقام الفاصلة العائمة في بعض الأحيان صاخبة نظرًا لتأثير خوارزميات التحديد وتعدد مؤشرات الترابط وما إلى ذلك. أريد أن أؤكد أن الاختلاف الرئيسي هنا هو أن الحسابات الحسابية بأرقام الفاصلة العائمة هي حتمية في البداية ، حتى لو لم يكن هذا واضحًا بسبب تعقيد التنفيذ ، على الرغم من أن بدائل CKKS صاخبة بالفعل. ولكن ربما يتيح ذلك للمستخدمين فهم أن الضوضاء ليست مخيفة كما قد تبدو.
الآن ، لنرى كيف يمكنك تنفيذ هذه العمليات في جوليا (ملاحظة: يتم تحديد معلمات غير آمنة للغاية ، مع هذه العمليات نوضح فقط استخدام المكتبة في REPL).
julia> using ToyFHE
بسيط جدا! قد يلاحظ القارئ اليقظ أن CSQ يختلف قليلاً عن النص المشفر السابق. على وجه الخصوص ، النص المشفر له "طول 3" والمقياس أكبر بكثير. شرح ما هذا وما هو مطلوب هو خارج نطاق هذه المقالة. يكفي أن نقول أننا نحتاج إلى خفض القيم قبل المتابعة مع الحسابات ، وإلا فإن "المكان" سينتهي في النص المشفر. لحسن الحظ ، يمكننا تقليل كل من القيمتين المتزايدتين:
بالإضافة إلى ذلك ، يقلل modswitching (اختصار لتبديل المعامل ، تبديل الوحدة) من حجم الوحدة النمطية للنص المشفر ، بحيث لا يمكننا الاستمرار في القيام بذلك إلى أجل غير مسمى (نستخدم نظام تشفير متماثل إلى حد ما):
julia> ℛ
غطينا أساسيات استخدام مكتبة HE. ولكن قبل الانتقال إلى استخدام هذه العناصر الأولية لحساب توقعات الشبكة العصبية ، دعنا ننظر إلى عملية تعلمها.
نموذج التعلم الآلي
إذا لم تكن معتادًا على التعلم الآلي أو مكتبة Flux.jl ، فنوصيك بإجراء جولة قصيرة من خلال
وثائق Flux.jl أو الاطلاع على
مقدمة مجانية
للتعلم الآلي ، لأننا سنناقش فقط التغييرات في تطبيق النموذج على البيانات المشفرة.
لنبدأ باستخدام الشبكة العصبية التلافيفية
من حديقة حيوانات Flux . سنقوم بتنفيذ نفس دورة التدريب ، مع إعداد البيانات وما إلى ذلك ، مجرد إعداد النموذج قليلاً. ومن هنا:
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(
هذا هو نفس النموذج كما هو الحال في العمل
"تأمين مصفوفة الاستعانة بمصادر خارجية الآمنة والتطبيق على الشبكات العصبية" ، والذي يستخدم نفس نظام التشفير مع اختلافين: 1) من أجل البساطة ، نحن لم تشفر النموذج نفسه ، و 2) بعد كل طبقة لدينا تستخدم ناقلات بايزي (في Flux يتم ذلك افتراضيًا) ، لست متأكدًا مما كان عليه في العمل المذكور. ربما ، نظرًا للنقطة الثانية ، اتضح أن دقة مجموعة الاختبار الخاصة بنموذجنا أعلى قليلاً (98.6٪ مقابل 98.1٪) ، ولكن قد تكون الاختلافات في القياس الزائد هي السبب أيضًا.
غير عادية (بالنسبة لأولئك الذين لديهم خبرة في التعلم الآلي) هو
x.^2
تنشيط الوظائف. في أغلب الأحيان ، في مثل هذه الحالات ، يستخدمون
tanh
أو
relu
أو شيء أكثر خيالية. ولكن على الرغم من أن هذه الدالات (خاصة
relu
) يتم حسابها بسهولة لقيم النص العادية ، إلا أنها قد تتطلب الكثير من الموارد الحسابية لتقييمها في شكل مشفر (عادةً ما نقدر تقريب كثير الحدود). لحسن الحظ ، في هذه الحالة
x.^2
يعمل بشكل رائع.
بقيت بقية دورة التعلم كما هي. لقد أزلنا
softmax
من طراز
softmax
لخسارة الوظيفة (يمكنك تركه وتقييم softmax بعد فك التشفير على العميل). يكمن الرمز الكامل لتدريب النموذج
على GitHub ، ويتم تشغيله في بضع دقائق على أي بطاقة فيديو جديدة.
عمليات فعالة
الآن نحن نعرف العمليات التي نحتاج إلى تنفيذها:
- تخثر الدم.
- تربيع العنصر.
- مصفوفة الضرب.
مع ترميز كل شيء بسيط ، لقد درسناه بالفعل أعلاه ، لذلك سننظر في عمليتين أخريين. نحن نفترض أن طول حزمة البيانات هو 64 (قد تلاحظ أنه تم اختيار معلمات النموذج وحجم الحزمة للاستفادة من متجه 4096 عنصر الذي حصلنا عليه نتيجة لاختيار واقعي للمعلمات).
تجلط الدم
أذكر كيف يعمل التخثر. خذ نافذة (في حالتنا 7 × 7) من صفيف الإدخال الأصلي ، وضرب كل عنصر من عناصر النافذة بعنصر قناع الالتفاف. ثم ننقل النافذة إلى خطوة ما (في حالتنا ، الخطوة هي 3 ، أي ، ننقل 3 عناصر) ونكرر العملية (باستخدام نفس قناع الالتفاف). تظهر الرسوم المتحركة للعملية (
المصدر ) للالتفاف 3x3 مع الخطوة
(2, 2)
أدناه (الصفيف الأزرق - الإدخال ، الأخضر - الإخراج):
بالإضافة إلى ذلك ، نقوم بإجراء الإلتفاف في أربع "قنوات" مختلفة (أي نكرر الإلتواء 3 مرات مع أقنعة مختلفة).
الآن نحن نعرف ماذا نفعل ، يبقى أن نفهم كيف. نحن محظوظون لأن الالتواء هو أول عملية في نموذجنا. نتيجة لذلك ، من أجل توفير الموارد ، يمكننا معالجة البيانات على العميل مسبقًا ، ثم تشفيرها (دون استخدام الأوزان). دعونا نفعل هذا:
- أولاً ، نحسب كل نافذة ملفوف (أي ، عينة 7 × 7 من الصور المصدر) ، والتي تعطينا 64 مصفوفات 7 × 7 لكل صورة إدخال. لاحظ أنه بالنسبة لنافذة 7 × 7 بزيادات تبلغ 2 ، سيكون هناك 8x8 نوافذ ملتوية لتقييم صورة الإدخال 28 × 28.
- دعونا نجمع في نفس المتجه نفس المواقف في كل نافذة. بمعنى ، سيكون لكل متجه 64 عنصرًا ، أو متجهًا 64 × 64 عنصرًا لحزمة بحجم 64 (إجمالي 49 مصفوفة 64 × 64).
- سنقوم بتشفير.
ثم يتحول التخثر ببساطة إلى ضرب عددي للمصفوفة بأكملها مع عنصر القناع المقابل. ولخص العناصر الـ 49 لاحقًا ، نحصل على نتيجة قابلة للطي. إليك ما قد يبدو عليه تنفيذ هذه الاستراتيجية (بنص عادي):
function public_preprocess(batch) ka = OffsetArray(0:7, 0:7)
هذا (الوحدة النمطية لتغيير البعد) (modulo - تغيير ترتيب الأحجام) يعطي نفس الإجابة مثل
model.layers[1](batch)
العملية
model.layers[1](batch)
.
إضافة عمليات التشفير:
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)
يرجى ملاحظة أن keyswitch غير مطلوب هنا لأن الأوزان عامة. لذلك نحن لا زيادة طول النص المشفر.
مصفوفة الضرب
بالانتقال إلى ضرب المصفوفة ، يمكننا استخدام دوران العناصر في المتجه لتغيير ترتيب مؤشرات الضرب. النظر في وضع الحكمة الصف من عناصر المصفوفة في ناقلات. إذا قمنا بنقل المتجه بعدة مضاعفات من حجم الصف ، فسوف نحقق تأثير دوران العمود ، وهو بدائي كافٍ لتنفيذ ضرب المصفوفة (على الأقل مصفوفات مربعة). لنجرب:
function matmul_square_reordered(weights, x) sum(1:size(weights, 1)) do k
بالطبع ، بالنسبة لضرب المصفوفة العامة ، هناك حاجة إلى شيء أكثر تعقيدًا ، لكن هذا يكفي الآن.
تحسين التقنية
الآن جميع مكونات عملنا الفني. فيما يلي الرمز بالكامل (باستثناء إعداد خيارات التحديد والأشياء المشابهة):
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)
لا يبدو أنيقًا جدًا ، ولكن إذا فعلت كل هذا ، فيجب أن تفهم كل خطوة.
الآن دعنا نفكر فيما يمكن أن تبسطه التجريدات حياتنا. نحن نترك مجال رسم الخرائط والتعلم الآلي وننتقل إلى بنية لغة البرمجة ، لذلك دعونا نستفيد من حقيقة أن جوليا تسمح لك باستخدام وخلق تجريدات قوية. على سبيل المثال ، يمكنك تغليف العملية الكاملة لاستخراج التلفيف في نوع الصفيف الخاص بك:
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}
هنا استخدمنا مرة أخرى
BlockArrays
لتمثيل صفيف
8x8x1x64
صفائف
8x8x1x64
كما في التعليمات البرمجية المصدر. الآن أصبح تقديم المرحلة الأولى أكثر جمالا ، على الأقل مع صفائف غير مشفرة:
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]
الآن كيف يمكننا ربط هذا مع التشفير؟ للقيام بذلك ، تحتاج إلى:
- قم بتشفير البنية (
ExplodedConvArray
) حتى نحصل على النص المشفر لكل حقل. ستتحقق العمليات ذات البنية المشفرة مما ستفعله الوظيفة بالهيكل الأصلي ، وتفعل الشيء نفسه بشكل متجانس.
- اعتراض عمليات معينة من أجل تنفيذها بشكل مختلف في سياق مشفر.
لحسن الحظ ، توفر جوليا لنا تجريدًا لهذا: مكون إضافي لبرنامج التحويل البرمجي يستخدم آلية
Cassette.jl . لن أخبرك ما هو وكيف تعمل ، وسأقول بإيجاز أنه يمكن تحديد السياق ، على سبيل المثال ،
Encrypted
، ثم يحدد القواعد كيف ينبغي أن تعمل العمليات في هذا السياق. على سبيل المثال ، يمكنك كتابة هذا للشرط الثاني:
نتيجة لذلك ، سيتمكن المستخدم من كتابة كل ما سبق مع الحد الأدنى من العمل اليدوي:
kp = keygen(ckks_params) ek = keygen(EvalMultKey, kp.priv) gk = keygen(GaloisKey, kp.priv; steps=64)
, . ( ℛ, modswitch, keyswitch ..) , . , , , , .
استنتاج
— . Julia . RAMPARTS (
paper ,
JuliaCon talk ) : Julia- - PALISADE. Julia Computing RAMPARTS Verona,
. , . . , , .
,
ToyFHE .
, , , .