احتساب الإحصاءات الخاصة مع الحفاظ على الخصوصية باستخدام الشعاع

1. مقدمة

قد تعتقد أنّ الإحصاءات المجمَّعة لا تسرّب أي معلومات عن الأفراد الذين تتكوّن الإحصاءات من بياناتهم. ومع ذلك، هناك العديد من الطرق التي يمكن للمهاجم من خلالها الحصول على معلومات حساسة عن الأفراد في مجموعة بيانات من إحصاء مجمّع.

لحماية خصوصية الأفراد، ستتعرّف على كيفية إنشاء إحصاءات خاصة باستخدام عمليات تجميع تحافظ على الخصوصية من Privacy on Beam. الخصوصية في Beam هي إطار عمل للخصوصية التفاضلية يعمل مع Apache Beam.

ما المقصود بـ "خاص"؟

عند استخدام كلمة "خاص" في جميع أنحاء هذا الدرس التطبيقي، نعني أنّ الناتج يتم إنتاجه بطريقة لا تؤدي إلى تسريب أي معلومات خاصة عن الأفراد في البيانات. يمكننا إجراء ذلك باستخدام "الخصوصية التفاضلية"، وهي مفهوم قوي للخصوصية يهدف إلى إخفاء الهوية. إخفاء الهوية هو عملية تجميع البيانات من عدة مستخدمين لحماية خصوصية المستخدمين. تستخدم جميع طرق إخفاء الهوية التجميع، ولكن لا تؤدي جميع طرق التجميع إلى إخفاء الهوية. من ناحية أخرى، توفّر الخصوصية التفاضلية ضمانات قابلة للقياس بشأن تسريب المعلومات والخصوصية.

2. نظرة عامة على الخصوصية التفاضلية

لفهم الخصوصية التفاضلية بشكل أفضل، لنلقِ نظرة على مثال بسيط.

يعرض هذا الرسم البياني الشريطي مدى الازدحام في مطعم صغير في إحدى الأمسيات. يصل العديد من الضيوف في الساعة 7 مساءً، ويكون المطعم فارغًا تمامًا في الساعة 1 صباحًا:

a43dbf3e2c6de596.png

يبدو هذا مفيدًا!

ولكن هناك شرط. عند وصول ضيف جديد، يكشف الرسم البياني الشريطي عن هذه المعلومة على الفور. في الرسم البياني، من الواضح أنّ هناك ضيفًا جديدًا، وأنّه وصل في الساعة 1 صباحًا تقريبًا:

bda96729e700a9dd.png

وهذا ليس جيدًا من منظور الخصوصية. يجب ألا تكشف الإحصاءات المجهّلة الهوية عن مساهمات فردية. عند وضع هذين الرسمين البيانيين جنبًا إلى جنب، يصبح الفرق أكثر وضوحًا: يحتوي الرسم البياني الشريطي البرتقالي على ضيف إضافي وصل في الساعة 1 صباحًا تقريبًا:

d562ddf799288894.png

مرة أخرى، هذا ليس جيدًا. ماذا نفعل؟

سنقلّل دقة الرسوم البيانية الشريطية قليلاً من خلال إضافة ضوضاء عشوائية.

اطّلِع على الرسمَين البيانيَين الشريطيَين أدناه. وعلى الرغم من أنّها ليست دقيقة تمامًا، إلا أنّها لا تزال مفيدة، ولا تكشف عن مساهمات فردية. أحسنت.

838a0293cd4fcfe3.gif

تضيف الخصوصية التفاضلية القدر المناسب من التشويش العشوائي لإخفاء المساهمات الفردية.

كان تحليلنا مبسطًا إلى حد ما. يتطلّب تنفيذ الخصوصية التفاضلية بشكلٍ صحيح خطوات أكثر ويتضمّن عددًا من التفاصيل الدقيقة غير المتوقّعة. على غرار التشفير، قد لا يكون إنشاء تطبيقك الخاص للخصوصية التفاضلية فكرة جيدة. يمكنك استخدام Privacy on Beam بدلاً من تنفيذ الحلّ الخاص بك. لا تنشئ الخصوصية التفاضلية بنفسك.

في هذا الدرس التطبيقي حول الترميز، سنوضّح كيفية إجراء تحليل يحافظ على الخصوصية التفاضلية باستخدام Privacy on Beam.

3- تنزيل "الخصوصية على Beam"

لست بحاجة إلى تنزيل Privacy on Beam لتتمكّن من اتّباع الخطوات الواردة في الدرس العملي، لأنّه يمكنك العثور على جميع الرموز ذات الصلة والرسومات البيانية في هذا المستند. ومع ذلك، إذا أردت تنزيل الرمز لتشغيله بنفسك أو استخدام ميزة Privacy on Beam لاحقًا، يمكنك إجراء ذلك باتّباع الخطوات أدناه.

يُرجى العِلم أنّ هذا الدرس العملي مخصّص للإصدار 1.1.0 من المكتبة.

أولاً، نزِّل تطبيق Privacy on Beam:

https://github.com/google/differential-privacy/archive/refs/tags/v1.1.0.tar.gz

أو يمكنك استنساخ مستودع Github:

git clone --branch v1.1.0 https://github.com/google/differential-privacy.git

تتوفّر الخصوصية على Beam في الدليل ذي المستوى الأعلى privacy-on-beam/.

يمكنك العثور على الرمز البرمجي لهذا الدرس التطبيقي حول الترميز ومجموعة البيانات في الدليل privacy-on-beam/codelab/.

يجب أيضًا تثبيت Bazel على جهاز الكمبيوتر. يمكنك العثور على تعليمات التثبيت لنظام التشغيل على موقع Bazel الإلكتروني.

4. احتساب عدد الزيارات في الساعة

لنفترض أنّك تملك مطعمًا وتريد مشاركة بعض الإحصاءات حوله، مثل أوقات الزيارة الشائعة. لحسن الحظ، أنت على دراية بمفهومَي الخصوصية التفاضلية وإخفاء الهوية، لذا تريد إجراء ذلك بطريقة لا تؤدي إلى تسريب معلومات عن أي زائر فردي.

يمكنك العثور على الرمز البرمجي لهذا المثال في codelab/count.go.

لنبدأ بتحميل مجموعة بيانات وهمية تحتوي على زيارات إلى مطعمك في يوم اثنين معيّن. لا يهمّنا الرمز البرمجي لهذا الغرض في هذا الدرس التطبيقي حول الترميز، ولكن يمكنك الاطّلاع على الرمز البرمجي في codelab/main.go وcodelab/utils.go وcodelab/visit.go.

رقم تعريف الزائر

وقت الإدخال

الوقت المستغرَق (بالدقائق)

المبلغ الذي تم إنفاقه (باليورو)

1

‫9:30:00 صباحًا

26

24

2

‫11:54:00 صباحًا

53

17

3

1:05:00 م

81

33

ستنشئ أولاً رسمًا بيانيًا شريطيًا غير خاص لأوقات الزيارة إلى مطعمك باستخدام Beam في عينة التعليمات البرمجية أدناه. Scope هو تمثيل لمسار البيانات، ويتمّ إضافة كل عملية جديدة ننفّذها على البيانات إلى Scope. تأخذ CountVisitsPerHour Scope ومجموعة من الزيارات، ويتم تمثيلها على شكل PCollection في Beam. تستخرِج هذه الدالة ساعة كل زيارة من خلال تطبيق الدالة extractVisitHour على المجموعة. بعد ذلك، يتم احتساب عدد مرات تكرار كل ساعة وعرضه.

func CountVisitsPerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("CountVisitsPerHour")
    visitHours := beam.ParDo(s, extractVisitHourFn, col)
    visitsPerHour := stats.Count(s, visitHours)
    return visitsPerHour
}

func extractVisitHourFn(v Visit) int {
    return v.TimeEntered.Hour()
}

ينتج عن ذلك رسم بياني شريطي جميل (من خلال تنفيذ bazel run codelab -- --example="count" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/count.csv --output_chart_file=$(pwd)/count.png) في الدليل الحالي باسم count.png:

a179766795d4e64a.png

الخطوة التالية هي تحويل خط الأنابيب والرسم البياني الشريطي إلى رسم بياني خاص. وننفّذ ذلك على النحو التالي.

أولاً، اتّصِل بالرقم MakePrivateFromStruct على PCollection<V> للحصول على PrivatePCollection<V>. يجب أن يكون الإدخال PCollection مجموعة من البُنى. علينا إدخال PrivacySpec وidFieldPath كمدخلات إلى MakePrivateFromStruct.

spec := pbeam.NewPrivacySpec(epsilon, delta)
pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

PrivacySpec هي بنية تحتوي على مَعلمات الخصوصية التفاضلية (إبسيلون ودلتا) التي نريد استخدامها لإخفاء هوية البيانات. (لا داعي للقلق بشأنها في الوقت الحالي، وسنقدّم قسمًا اختياريًا لاحقًا إذا أردت معرفة المزيد عنها).

idFieldPath هو مسار حقل معرّف المستخدم داخل البنية (Visit في حالتنا). في هذه الحالة، يكون معرّف المستخدم للزوّار هو الحقل VisitorID من Visit.

بعد ذلك، نستدعي pbeam.Count() بدلاً من stats.Count()، وتتلقّى pbeam.Count() كمدخل بنية CountParams التي تتضمّن مَعلمات مثل MaxValue التي تؤثر في دقة الناتج.

visitsPerHour := pbeam.Count(s, visitHours, pbeam.CountParams{
    // Visitors can visit the restaurant once (one hour) a day
    MaxPartitionsContributed: 1,
    // Visitors can visit the restaurant once within an hour
    MaxValue:                 1,
})

وبالمثل، يحدّد MaxPartitionsContributed عدد ساعات الزيارة المختلفة التي يمكن للمستخدم المساهمة بها. نتوقّع أن يزور المستخدم المطعم مرة واحدة في اليوم على الأكثر (أو لا يهمّنا إذا زاره عدة مرات خلال اليوم)، لذا نضبط القيمة على 1 أيضًا. سنتناول هذه المَعلمات بمزيد من التفصيل في قسم اختياري.

يحدّ MaxValue من عدد المرات التي يمكن أن يساهم فيها مستخدم واحد في القيم التي نحسبها. في هذه الحالة تحديدًا، القيم التي نحسبها هي ساعات الزيارة، ونتوقّع أن يزور المستخدم المطعم مرة واحدة فقط (أو لا يهمّنا إذا زاره عدة مرات في الساعة)، لذا نضبط هذه المَعلمة على 1.

في النهاية، سيبدو الرمز البرمجي على النحو التالي:

func PrivateCountVisitsPerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateCountVisitsPerHour")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, delta)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    visitHours := pbeam.ParDo(s, extractVisitHourFn, pCol)
    visitsPerHour := pbeam.Count(s, visitHours, pbeam.CountParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed: 1,
        // Visitors can visit the restaurant once within an hour
        MaxValue:                 1,
    })
    return visitsPerHour
}

نرى رسمًا بيانيًا شريطيًا مشابهًا (count_dp.png) للإحصاءات المحافظة على الخصوصية (ينفّذ الأمر السابق كلاً من مسارات البيانات غير المحافظة على الخصوصية والمحافظة على الخصوصية):

d6a0ace1acd3c760.png

تهانينا! لقد حسبت إحصاءاتك الأولى التي تحافظ على الخصوصية التفاضلية.

قد يختلف الرسم البياني الشريطي الذي يظهر لك عند تشغيل الرمز عن هذا الرسم. حسنًا. بسبب التشويش في الخصوصية التفاضلية، ستحصل على رسم بياني شريطي مختلف في كل مرة تشغّل فيها الرمز، ولكن يمكنك ملاحظة أنّها متشابهة إلى حدّ ما مع الرسم البياني الشريطي الأصلي غير الخاص الذي كان لدينا.

يُرجى العِلم أنّه من المهم جدًا عدم إعادة تشغيل سلسلة المعالجة عدة مرات لضمان الخصوصية (على سبيل المثال، للحصول على رسم بياني شريطي أفضل). يتم توضيح سبب عدم إعادة تشغيل عمليات نقل البيانات في قسم "احتساب إحصاءات متعددة".

5- استخدام الأقسام العامة

في القسم السابق، ربما لاحظت أنّنا حذفنا جميع الزيارات (البيانات) لبعض الأقسام، أي الساعات.

d7fbc5d86d91e54a.png

ويرجع ذلك إلى اختيار الأقسام/تحديد الحدود، وهي خطوة مهمة لضمان توفّر ضمانات الخصوصية التفاضلية عندما يعتمد وجود أقسام الإخراج على بيانات المستخدم نفسها. في هذه الحالة، يمكن أن يؤدي مجرد توفّر قسم في الناتج إلى تسريب معلومات عن وجود مستخدم فردي في البيانات (راجِع منشور المدونة هذا للحصول على شرح حول سبب انتهاك ذلك للخصوصية). لمنع حدوث ذلك، لا تحتفظ ميزة "الخصوصية على Beam" إلا بالأقسام التي تضم عددًا كافيًا من المستخدمين.

عندما لا تعتمد قائمة أقسام الإخراج على بيانات المستخدم الخاصة، أي أنّها معلومات عامة، لا نحتاج إلى خطوة اختيار القسم هذه. هذا هو الحال في مثال المطعم: نحن نعرف ساعات عمل المطعم (من الساعة 9:00 صباحًا إلى الساعة 9:00 مساءً).

يمكنك العثور على الرمز البرمجي لهذا المثال في codelab/public_partitions.go.

سننشئ ببساطة PCollection من الساعات بين 9 و21 (باستثناء 21) ونُدخله إلى الحقل PublicPartitions من CountParams:

func PrivateCountVisitsPerHourWithPublicPartitions(s beam.Scope,
    col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateCountVisitsPerHourWithPublicPartitions")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    // Create a PCollection of output partitions, i.e. restaurant's work hours
    // (from 9 am till 9pm (exclusive)).
    hours := beam.CreateList(s, [12]int{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})

    visitHours := pbeam.ParDo(s, extractVisitHourFn, pCol)
    visitsPerHour := pbeam.Count(s, visitHours, pbeam.CountParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed: 1,
        // Visitors can visit the restaurant once within an hour
        MaxValue:                 1,
        // Visitors only visit during work hours
        PublicPartitions:         hours,
    })
    return visitsPerHour
}

يُرجى العِلم أنّه يمكن ضبط دلتا على 0 إذا كنت تستخدم الأقسام العامة و"ضوضاء لابلاس" (الإعداد التلقائي)، كما هو الحال أعلاه.

عند تشغيل مسار العرض مع الأقسام العامة (باستخدام bazel run codelab -- --example="public_partitions" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/public_partitions.csv --output_chart_file=$(pwd)/public_partitions.png)، نحصل على (public_partitions_dp.png):

7c950fbe99fec60a.png

كما تلاحظ، نحتفظ الآن بالأقسام 9 و10 و16 التي أزلناها سابقًا بدون أقسام عامة.

لا يتيح لك استخدام الأقسام العامة الاحتفاظ بالمزيد من الأقسام فحسب، بل يضيف أيضًا نصف مقدار التشويش تقريبًا إلى كل قسم مقارنةً بعدم استخدام الأقسام العامة بسبب عدم إنفاق أي ميزانية للخصوصية، أي إبسيلون ودلتا، على اختيار الأقسام. لهذا السبب، يكون الفرق بين عدد مرات الظهور غير المجمّعة وعدد مرات الظهور الخاصة أقل قليلاً مقارنةً بعملية التنفيذ السابقة.

هناك نقطتان مهمّتان يجب مراعاتهما عند استخدام الأقسام العامة:

  1. يجب توخّي الحذر عند استخلاص قائمة الأقسام من البيانات الأولية: إذا لم يتم ذلك بطريقة توفّر الخصوصية التفاضلية، مثلاً من خلال قراءة قائمة جميع الأقسام في بيانات المستخدم، لن توفّر عملية المعالجة ضمانات الخصوصية التفاضلية. راجِع القسم المتقدّم أدناه لمعرفة كيفية إجراء ذلك بطريقة تحافظ على الخصوصية التفاضلية.
  2. إذا لم تتوفّر بيانات (مثل الزيارات) لبعض الأقسام العامة، سيتم تطبيق التشويش على هذه الأقسام للحفاظ على الخصوصية التفاضلية. على سبيل المثال، إذا استخدمنا ساعات بين 0 و24 (بدلاً من 9 و21)، سيتم تشويش جميع الساعات وقد تعرض بعض الزيارات عندما لا تكون هناك أي زيارات.

(متقدّم) استخلاص الأقسام من البيانات

إذا كنت تُجري عمليات تجميع متعدّدة باستخدام قائمة الأقسام غير العامة نفسها في مسار الإعداد نفسه، يمكنك استخلاص قائمة الأقسام مرة واحدة باستخدام SelectPartitions() وتوفير الأقسام لكل عملية تجميع كإدخال PublicPartition. لا يوفّر هذا الإجراء الأمان من ناحية الخصوصية فحسب، بل يتيح لك أيضًا إضافة تشويش أقل بسبب استخدام ميزانية الخصوصية في اختيار الأقسام مرة واحدة فقط لجميع مراحل المعالجة.

6. احتساب متوسط مدة الإقامة

بعد أن عرفنا كيفية حساب عدد العناصر بطريقة تحافظ على الخصوصية التفاضلية، لنلقِ نظرة على كيفية حساب المتوسطات. على وجه التحديد، سنحسب الآن متوسط مدة إقامة الزوّار.

يمكنك العثور على الرمز البرمجي لهذا المثال في codelab/mean.go.

في العادة، لاحتساب متوسط مدة الإقامة غير الخاص، نستخدم stats.MeanPerKey() مع خطوة معالجة مسبقة تحوّل PCollection الزيارات الواردة إلى PCollection<K,V> حيث K هي ساعة الزيارة وV هي الوقت الذي قضاه الزائر في المطعم.

func MeanTimeSpent(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("MeanTimeSpent")
    hourToTimeSpent := beam.ParDo(s, extractVisitHourAndTimeSpentFn, col)
    meanTimeSpent := stats.MeanPerKey(s, hourToTimeSpent)
    return meanTimeSpent
}

func extractVisitHourAndTimeSpentFn(v Visit) (int, int) {
    return v.TimeEntered.Hour(), v.MinutesSpent
}

ينتج عن ذلك رسم بياني شريطي جميل (من خلال تنفيذ bazel run codelab -- --example="mean" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/mean.csv --output_chart_file=$(pwd)/mean.png) في الدليل الحالي باسم mean.png:

bc2df28bf94b3721.png

للحفاظ على الخصوصية التفاضلية، نحول PCollection مرة أخرى إلى PrivatePCollection ونستبدل stats.MeanPerKey() بـ pbeam.MeanPerKey(). على غرار Count، لدينا MeanParams التي تتضمّن بعض المَعلمات، مثل MinValue وMaxValue، التي تؤثّر في الدقة. يمثّل MinValue وMaxValue الحدود التي نضعها لمساهمة كل مستخدم في كل مفتاح.

meanTimeSpent := pbeam.MeanPerKey(s, hourToTimeSpent, pbeam.MeanParams{
    // Visitors can visit the restaurant once (one hour) a day
    MaxPartitionsContributed:     1,
    // Visitors can visit the restaurant once within an hour
    MaxContributionsPerPartition: 1,
    // Minimum time spent per user (in mins)
    MinValue:                     0,
    // Maximum time spent per user (in mins)
    MaxValue:                     60,
})

في هذه الحالة، يمثّل كل مفتاح ساعة، وتمثّل القيم الوقت الذي استغرقه الزوّار. نضبط قيمة MinValue على 0 لأنّنا لا نتوقّع أن يقضي الزوّار أقل من 0 دقيقة في المطعم. نضبط MaxValue على 60، ما يعني أنّه إذا استغرق الزائر أكثر من 60 دقيقة، سنتعامل معه كما لو أنّه استغرق 60 دقيقة.

في النهاية، سيبدو الرمز البرمجي على النحو التالي:

func PrivateMeanTimeSpent(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateMeanTimeSpent")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    // Create a PCollection of output partitions, i.e. restaurant's work hours
    // (from 9 am till 9pm (exclusive)).
    hours := beam.CreateList(s, [12]int{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})

    hourToTimeSpent := pbeam.ParDo(s, extractVisitHourAndTimeSpentFn, pCol)
    meanTimeSpent := pbeam.MeanPerKey(s, hourToTimeSpent, pbeam.MeanParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed:     1,
        // Visitors can visit the restaurant once within an hour
        MaxContributionsPerPartition: 1,
        // Minimum time spent per user (in mins)
        MinValue:                     0,
        // Maximum time spent per user (in mins)
        MaxValue:                     60,
        // Visitors only visit during work hours
        PublicPartitions:             hours,
    })
    return meanTimeSpent
}

نرى رسمًا بيانيًا شريطيًا مشابهًا (mean_dp.png) للإحصاءات المحافظة على الخصوصية (ينفّذ الأمر السابق كلاً من مسارات البيانات غير المحافظة على الخصوصية والمحافظة على الخصوصية):

e8ac6a9bf9792287.png

مرة أخرى، كما هو الحال مع عملية العدّ، بما أنّ هذه العملية تحافظ على الخصوصية التفاضلية، سنحصل على نتائج مختلفة في كل مرة ننفّذها. ولكن يمكنك ملاحظة أنّ مدة الإقامة المحسوبة باستخدام الخصوصية التفاضلية لا تختلف كثيرًا عن النتيجة الفعلية.

7. احتساب الإيرادات في الساعة

إحصائية أخرى مثيرة للاهتمام يمكننا الاطّلاع عليها هي الإيرادات في الساعة على مدار اليوم.

يمكنك العثور على الرمز البرمجي لهذا المثال في codelab/sum.go.

مرة أخرى، سنبدأ بالإصدار غير الخاص. بعد إجراء بعض المعالجة المسبقة على مجموعة البيانات التجريبية، يمكننا إنشاء PCollection<K,V> حيث K هو ساعة الزيارة وV هو المبلغ الذي أنفقه الزائر في المطعم: لحساب إجمالي الأرباح غير الخاصة في الساعة، يمكننا ببساطة جمع كل الأموال التي أنفقها الزوار من خلال استدعاء stats.SumPerKey():

func RevenuePerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("RevenuePerHour")
    hourToMoneySpent := beam.ParDo(s, extractVisitHourAndMoneySpentFn, col)
    revenues := stats.SumPerKey(s, hourToMoneySpent)
    return revenues
}

func extractVisitHourAndMoneySpentFn(v Visit) (int, int) {
    return v.TimeEntered.Hour(), v.MoneySpent
}

ينتج عن ذلك رسم بياني شريطي جميل (من خلال تنفيذ bazel run codelab -- --example="sum" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/sum.csv --output_chart_file=$(pwd)/sum.png) في الدليل الحالي باسم sum.png:

548619173fad0c9a.png

للحفاظ على الخصوصية التفاضلية، نحول PCollection مرة أخرى إلى PrivatePCollection ونستبدل stats.SumPerKey() بـ pbeam.SumPerKey(). على غرار Count وMeanPerKey، لدينا SumParams التي تتضمّن بعض المَعلمات، مثل MinValue وMaxValue، التي تؤثّر في الدقة.

revenues := pbeam.SumPerKey(s, hourToMoneySpent, pbeam.SumParams{
    // Visitors can visit the restaurant once (one hour) a day
    MaxPartitionsContributed: 1,
    // Minimum money spent per user (in euros)
    MinValue:                 0,
    // Maximum money spent per user (in euros)
    MaxValue:                 40,
})

في هذه الحالة، يمثّل MinValue وMaxValue الحدود التي نحدّدها للمبلغ الذي ينفقه كل زائر. نضبط قيمة MinValue على 0 لأنّنا لا نتوقّع أن ينفق الزوّار أقل من 0 يورو في المطعم. لقد ضبطنا قيمة MaxValue على 40، ما يعني أنّه إذا أنفق أحد الزوّار أكثر من 40 يورو، سنتعامل معه كما لو أنّه أنفق 40 يورو.

في النهاية، سيبدو الرمز البرمجي على النحو التالي:

func PrivateRevenuePerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateRevenuePerHour")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    // Create a PCollection of output partitions, i.e. restaurant's work hours
    // (from 9 am till 9pm (exclusive)).
    hours := beam.CreateList(s, [12]int{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})

    hourToMoneySpent := pbeam.ParDo(s, extractVisitHourAndMoneySpentFn, pCol)
    revenues := pbeam.SumPerKey(s, hourToMoneySpent, pbeam.SumParams{
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed: 1,
        // Minimum money spent per user (in euros)
        MinValue:                 0,
        // Maximum money spent per user (in euros)
        MaxValue:                 40,
        // Visitors only visit during work hours
        PublicPartitions:         hours,
    })
    return revenues
}

نرى رسمًا بيانيًا شريطيًا مشابهًا (sum_dp.png) للإحصاءات المحافظة على الخصوصية (ينفّذ الأمر السابق كلاً من مسارات البيانات غير المحافظة على الخصوصية والمحافظة على الخصوصية):

46c375e874f3e7c4.png

مرة أخرى، كما هو الحال مع عملية الاحتساب وعملية حساب المتوسط، بما أنّ هذه العملية تحافظ على الخصوصية التفاضلية، سنحصل على نتائج مختلفة في كل مرة ننفّذها. ولكن يمكنك ملاحظة أنّ النتيجة التي تمّت معالجتها باستخدام الخصوصية التفاضلية قريبة جدًا من الأرباح الفعلية في الساعة.

8. احتساب إحصاءات متعددة

في معظم الأحيان، قد يهمّك احتساب إحصاءات متعدّدة على البيانات الأساسية نفسها، على غرار ما فعلته مع العدد والمتوسط والمجموع. عادةً ما يكون ذلك أكثر وضوحًا وأسهل في تنفيذه في مسار بيانات Beam واحد وفي ملف ثنائي واحد. يمكنك إجراء ذلك باستخدام ميزة "الخصوصية على Beam" أيضًا. يمكنك كتابة مسار معالجة واحد لتنفيذ عمليات التحويل والحسابات واستخدام PrivacySpec واحد لمسار المعالجة بأكمله.

لا يقتصر الأمر على سهولة تنفيذ ذلك باستخدام PrivacySpec واحد، بل إنّه أفضل أيضًا من ناحية الخصوصية. إذا كنت تتذكّر مَعلمتَي إبسيلون ودلتا اللتين نقدّمهما إلى PrivacySpec، فهما تمثّلان ما يُعرف باسم ميزانية الخصوصية، وهي مقياس لمقدار الخصوصية التي تفصح عنها للمستخدمين في البيانات الأساسية.

من المهم تذكُّر أنّ ميزانية الخصوصية هي ميزانية إضافية: إذا شغّلت مسار بيانات بقيمة إبسيلون ε ودلتا δ معيّنتَين مرة واحدة، ستكون قد استهلكت ميزانية (ε,δ). إذا نفّذت العملية مرة ثانية، ستكون قد أنفقت ميزانية إجمالية قدرها (2ε, 2δ). وبالمثل، إذا حسبت إحصاءات متعدّدة باستخدام PrivacySpec (وميزانية خصوصية متتالية) بقيمة (ε,δ)، ستكون قد أنفقت ميزانية إجمالية بقيمة (2ε, 2δ). وهذا يعني أنّك تقلّل من ضمانات الخصوصية.

لتجنُّب ذلك، عندما تريد احتساب إحصاءات متعدّدة على البيانات الأساسية نفسها، من المفترض أن تستخدم PrivacySpec واحدة مع إجمالي الميزانية التي تريد استخدامها. بعد ذلك، عليك تحديد قيمة إبسيلون ودلتا التي تريد استخدامها لكل عملية تجميع. في النهاية، ستحصل على ضمان الخصوصية نفسه بشكل عام، ولكن كلما زادت قيمة إبسيلون ودلتا في عملية تجميع معيّنة، زادت دقتها.

للاطّلاع على كيفية عمل ذلك، يمكننا احتساب الإحصاءات الثلاث (العدد والمتوسط والمجموع) التي احتسبناها بشكل منفصل سابقًا في مسار واحد.

يمكنك العثور على الرمز البرمجي لهذا المثال في codelab/multiple.go. لاحظ كيف نقسّم إجمالي ميزانية (ε,δ) بالتساوي بين عمليات التجميع الثلاث:

func ComputeCountMeanSum(s beam.Scope, col beam.PCollection) (visitsPerHour, meanTimeSpent, revenues beam.PCollection) {
    s = s.Scope("ComputeCountMeanSum")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    // Budget is shared by count, mean and sum.
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    // Create a PCollection of output partitions, i.e. restaurant's work hours
    // (from 9 am till 9pm (exclusive)).
    hours := beam.CreateList(s, [12]int{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})

    visitHours := pbeam.ParDo(s, extractVisitHourFn, pCol)
    visitsPerHour = pbeam.Count(s, visitHours, pbeam.CountParams{
        Epsilon:                  epsilon / 3,
        Delta:                    0,
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed: 1,
        // Visitors can visit the restaurant once within an hour
        MaxValue:                 1,
        // Visitors only visit during work hours
        PublicPartitions:         hours,
    })

    hourToTimeSpent := pbeam.ParDo(s, extractVisitHourAndTimeSpentFn, pCol)
    meanTimeSpent = pbeam.MeanPerKey(s, hourToTimeSpent, pbeam.MeanParams{
        Epsilon:                      epsilon / 3,
        Delta:                        0,
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed:     1,
        // Visitors can visit the restaurant once within an hour
        MaxContributionsPerPartition: 1,
        // Minimum time spent per user (in mins)
        MinValue:                     0,
        // Maximum time spent per user (in mins)
        MaxValue:                     60,
        // Visitors only visit during work hours
        PublicPartitions:             hours,
    })

    hourToMoneySpent := pbeam.ParDo(s, extractVisitHourAndMoneySpentFn, pCol)
    revenues = pbeam.SumPerKey(s, hourToMoneySpent, pbeam.SumParams{
        Epsilon:                  epsilon / 3,
        Delta:                    0,
        // Visitors can visit the restaurant once (one hour) a day
        MaxPartitionsContributed: 1,
        // Minimum money spent per user (in euros)
        MinValue:                 0,
        // Maximum money spent per user (in euros)
        MaxValue:                 40,
        // Visitors only visit during work hours
        PublicPartitions:         hours,
    })

    return visitsPerHour, meanTimeSpent, revenues
}

9- (اختياري) تعديل مَعلمات الخصوصية التفاضلية

لقد رأيت عددًا قليلاً من المَعلمات المذكورة في هذا الدرس العملي، مثل epsilon وdelta وmaxPartitionsContributed وما إلى ذلك. ويمكننا تقسيمها تقريبًا إلى فئتَين: مَعلمات الخصوصية ومَعلمات الأداة المساعدة.

مَعلمات الخصوصية

إبسيلون ودلتا هما المَعلمتان اللتان تحدّدان كمية الخصوصية التي نوفّرها باستخدام الخصوصية التفاضلية. وبشكل أكثر دقة، يمثّل كلّ من إبسيلون ودلتا مقياسًا لمقدار المعلومات التي يحصل عليها مهاجم محتمل حول البيانات الأساسية من خلال النظر إلى الناتج المخفي الهوية. كلما زادت قيمة إبسيلون ودلتا، زادت المعلومات التي يحصل عليها المهاجم حول البيانات الأساسية، ما يشكّل خطرًا على الخصوصية.

من ناحية أخرى، كلما انخفضت قيمة إبسيلون ودلتا، زادت الضوضاء التي يجب إضافتها إلى الناتج ليكون مجهول الهوية، وزاد عدد المستخدمين الفريدين الذين يجب أن يكونوا في كل قسم للحفاظ على هذا القسم في الناتج المجهول الهوية. لذلك، يجب تحقيق التوازن بين الفائدة والخصوصية هنا.

في Privacy on Beam، عليك الاهتمام بضمانات الخصوصية التي تريدها في الناتج المجهول الهوية عند تحديد إجمالي ميزانية الخصوصية في PrivacySpec. يجب الانتباه إلى أنّه إذا أردت الحفاظ على ضمانات الخصوصية، عليك اتّباع النصائح الواردة في هذا الدرس التطبيقي حول الترميز حول عدم الإفراط في استخدام ميزانيتك من خلال تخصيص PrivacySpec منفصل لكل عملية تجميع البيانات أو تشغيل مسار العرض عدة مرات.

لمزيد من المعلومات حول الخصوصية التفاضلية ومعنى مَعلمات الخصوصية، يمكنك الاطّلاع على المراجع.

مَعلمات الأداة

هذه هي المَعلمات التي لا تؤثّر في ضمانات الخصوصية (طالما تم اتّباع النصائح حول كيفية استخدام ميزة "الخصوصية على Beam" بشكلٍ صحيح)، ولكنّها تؤثّر في دقة النتائج، وبالتالي في فائدتها. يتم توفيرها في بنى Params لكل عملية تجميع بيانات، مثل CountParams وSumParams وما إلى ذلك. تُستخدَم هذه المَعلمات لتوسيع نطاق التشويش الذي تتم إضافته.

إحدى مَعلمات الأداة المضمّنة في Params والتي تنطبق على جميع عمليات التجميع هي MaxPartitionsContributed. يتوافق القسم مع مفتاح PCollection الذي يتم إخراجه من خلال عملية تجميع Privacy On Beam، أي Count وSumPerKey وما إلى ذلك. وبالتالي، يحدّ MaxPartitionsContributed من عدد قيم المفاتيح المميزة التي يمكن للمستخدم المساهمة بها في الناتج. إذا ساهم مستخدم في أكثر من MaxPartitionsContributed مفتاح في البيانات الأساسية، سيتم إسقاط بعض مساهماته لكي يساهم في MaxPartitionsContributed مفتاحًا بالضبط.

على غرار MaxPartitionsContributed، تحتوي معظم عمليات التجميع على المَعلمة MaxContributionsPerPartition. يتم توفيرها في بنى Params، ويمكن أن يتضمّن كل تجميع قيمًا منفصلة لها. على عكس MaxPartitionsContributed، تضع MaxContributionsPerPartition حدًا لمساهمة المستخدم لكل مفتاح. بمعنى آخر، يمكن للمستخدم تقديم MaxContributionsPerPartition قيم فقط لكل مفتاح.

يتم قياس الضوضاء المضافة إلى الناتج باستخدام MaxPartitionsContributed وMaxContributionsPerPartition، لذا هناك مقايضة هنا: يعني ارتفاع قيمتَي MaxPartitionsContributed وMaxContributionsPerPartition الاحتفاظ بمزيد من البيانات، ولكن سينتهي بك الأمر بنتيجة أكثر تشويشًا.

تتطلّب بعض عمليات التجميع MinValue وMaxValue. تحدّد هذه القيم حدود مساهمات كل مستخدم. إذا قدّم المستخدم قيمة أقل من MinValue، سيتم تثبيت هذه القيمة عند MinValue. وبالمثل، إذا أضاف المستخدم قيمة أكبر من MaxValue، سيتم تقليل هذه القيمة إلى MaxValue. يعني هذا أنّه للاحتفاظ بالمزيد من القيم الأصلية، عليك تحديد حدود أكبر. على غرار MaxPartitionsContributed وMaxContributionsPerPartition، يتم تغيير مقياس التشويش حسب حجم الحدود، لذا تعني الحدود الأكبر الاحتفاظ بمزيد من البيانات، ولكن سينتهي بك الأمر بنتيجة أكثر تشويشًا.

آخر مَعلمة سنتحدّث عنها هي NoiseKind. نوفّر آليتَي تشويش مختلفتَين في Privacy On Beam، وهما GaussianNoise وLaplaceNoise. لكل منهما مزايا وعيوب، ولكن يوفّر توزيع لابلاس فائدة أفضل مع حدود مساهمة منخفضة، ولهذا السبب تستخدم Privacy On Beam هذا التوزيع تلقائيًا. ومع ذلك، إذا كنت تريد استخدام ضوضاء توزيع غاوسي، يمكنك تقديم Params مع متغيّر pbeam.GaussianNoise{}.

10. ملخّص

أحسنت صنعًا. لقد أكملت تجربة Privacy on Beam. تعرّفت على الكثير من المعلومات حول الخصوصية التفاضلية والخصوصية على Beam:

  • تحويل PCollection إلى PrivatePCollection من خلال الاتصال بالرقم MakePrivateFromStruct
  • استخدام Count لاحتساب عدد مرات الظهور مع الحفاظ على الخصوصية التفاضلية
  • استخدام MeanPerKey لاحتساب المتوسطات الخاصة بالفروق.
  • استخدام SumPerKey لاحتساب المجموعات التي تحافظ على الخصوصية التفاضلية
  • حساب إحصاءات متعددة باستخدام PrivacySpec واحدة في مسار واحد
  • (اختياري) تخصيص مَعلمات PrivacySpec والتجميع (CountParams, MeanParams, SumParams)

ولكن، هناك العديد من عمليات التجميع الأخرى (مثل الشرائح المئوية، واحتساب القيم المميزة) التي يمكنك إجراؤها باستخدام Privacy on Beam. يمكنك الاطّلاع على مزيد من المعلومات حولها في مستودع GitHub أو godoc.

إذا كان لديك الوقت، يُرجى إرسال ملاحظاتك حول الدرس العملي من خلال ملء استطلاع.