חישוב נתונים סטטיסטיים פרטיים באמצעות פרטיות ב-Beam

1. מבוא

יכול להיות שתחשבו שנתונים סטטיסטיים מצטברים לא חושפים מידע על האנשים שהנתונים שלהם מרכיבים את הנתונים הסטטיסטיים. עם זאת, יש הרבה דרכים שבהן תוקף יכול ללמוד מידע רגיש על אנשים במערך נתונים מסטטיסטיקה מצטברת.

כדי להגן על פרטיות המשתמשים, תלמדו איך ליצור נתונים סטטיסטיים פרטיים באמצעות צבירות פרטיות דיפרנציאליות מ-Privacy on Beam. הפרטיות ב-Beam היא מסגרת של פרטיות דיפרנציאלית שפועלת עם Apache Beam.

מה הכוונה ב'פרטי'?

במהלך השימוש במילה 'פרטי' ב-Codelab הזה, אנחנו מתכוונים לכך שהפלט מופק באופן שלא חושף מידע פרטי על האנשים בנתונים. אנחנו יכולים לעשות את זה באמצעות פרטיות דיפרנציאלית, מושג חזק של אנונימיזציה ששומר על הפרטיות. אנונימיזציה היא תהליך של צבירת נתונים של כמה משתמשים כדי להגן על פרטיות המשתמשים. כל שיטות האנונימיזציה משתמשות בצבירה, אבל לא כל שיטות הצבירה משיגות אנונימיזציה. לעומת זאת, פרטיות דיפרנציאלית מספקת ערבויות מדידות לגבי דליפת מידע ופרטיות.

2. סקירה כללית על פרטיות דיפרנציאלית

כדי להבין טוב יותר מהי פרטיות דיפרנציאלית, נבחן דוגמה פשוטה.

בתרשים העמודות הזה מוצגת רמת הפעילות במסעדה קטנה בערב מסוים. הרבה אורחים מגיעים בשעה 19:00, והמסעדה ריקה לגמרי בשעה 01:00:

a43dbf3e2c6de596.png

זה נראה שימושי!

אבל יש מלכוד. כשאורח חדש מגיע, העובדה הזו מוצגת מיד בתרשים העמודות. מסתכלים על התרשים: ברור שיש אורח חדש, והוא הגיע בערך בשעה 1:00 בלילה:

bda96729e700a9dd.png

מנקודת מבט של פרטיות, זה לא אידיאלי. סטטיסטיקה שעברה אנונימיזציה אמיתית לא אמורה לחשוף תוספות תוכן של משתמשים ספציפיים. כשמציבים את שני התרשימים זה לצד זה, רואים את זה אפילו יותר בבירור: בתרשים העמודות הכתום יש אורח אחד נוסף שהגיע בערך בשעה 1:00:

d562ddf799288894.png

שוב, זה לא טוב. מה עושים?

נוסיף רעש אקראי כדי להפוך את תרשימי העמודות לפחות מדויקים.

כדאי לעיין בתרשימי העמודות שבהמשך. הם לא מדויקים לגמרי, אבל הם עדיין שימושיים, והם לא חושפים תרומות של אנשים פרטיים. איזה יופי!

838a0293cd4fcfe3.gif

הפרטיות הדיפרנציאלית מוסיפה את הכמות הנכונה של רעש אקראי כדי להסתיר תרומות של משתמשים ספציפיים.

הניתוח שלנו היה פשוט מדי. הטמעה נכונה של פרטיות דיפרנציאלית היא מורכבת יותר, ויש בה מספר דקויות הטמעה לא צפויות. בדומה לקריפטוגרפיה, לא מומלץ ליצור הטמעה משלכם של פרטיות דיפרנציאלית. אתם יכולים להשתמש ב-Privacy on Beam במקום להטמיע פתרון משלכם. אל תנסו ליצור פרטיות דיפרנציאלית בעצמכם!

ב-codelab הזה נראה איך לבצע ניתוח פרטיות דיפרנציאלית באמצעות Privacy on Beam.

3. הורדת פרטיות ב-Beam

אין צורך להוריד את Privacy on Beam כדי לעקוב אחרי ה-codelab, כי כל הקוד הרלוונטי והגרפים מופיעים במסמך הזה. עם זאת, אם אתם רוצים להוריד את הקוד כדי להפעיל אותו בעצמכם או להשתמש ב-Privacy on Beam בהמשך, אתם יכולים לעשות זאת לפי השלבים הבאים.

הערה: ה-codelab הזה מיועד לגרסה 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/.

הקוד של ה-Codelab הזה ומערך הנתונים נמצאים בספרייה privacy-on-beam/codelab/.

בנוסף, צריך להתקין את Bazel במחשב. הוראות ההתקנה לפי מערכת ההפעלה זמינות באתר Bazel.

4. חישוב מספר הביקורים בשעה

נניח שאתם בעלי מסעדה ורוצים לשתף נתונים סטטיסטיים על המסעדה שלכם, כמו שעות הביקור הפופולריות. למזלך, יש לך ידע בנושא פרטיות דיפרנציאלית ואנונימיזציה, ולכן אתה רוצה לעשות את זה בלי לחשוף מידע על אף מבקר ספציפי.

הקוד של הדוגמה הזו נמצא ב-codelab/count.go.

נתחיל בטעינה של מערך נתונים לדוגמה שמכיל ביקורים במסעדה שלכם ביום שני מסוים. הקוד של הפונקציה הזו לא רלוונטי למטרות ה-Codelab הזה, אבל אפשר לעיין בקוד שלה בקבצים 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 PM

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

הסיבה לכך היא בחירת המחיצות או הגדרת ערכי הסף שלהן, שלב חשוב להבטחת פרטיות דיפרנציאלית כשקיום מחיצות הפלט תלוי בנתוני המשתמשים עצמם. במקרים כאלה, עצם קיומה של מחיצה בפלט יכול לחשוף את קיומו של משתמש בודד בנתונים (בפוסט הזה בבלוג מוסבר למה זה מהווה הפרה של הפרטיות). כדי למנוע את זה, התכונה Privacy on Beam שומרת רק מחיצות שיש בהן מספר מספיק של משתמשים.

כשרשימת מחיצות הפלט לא תלויה בנתוני משתמש פרטיים, כלומר מדובר במידע ציבורי, אנחנו לא צריכים את שלב בחירת המחיצות הזה. זה המצב בדוגמה של המסעדה: אנחנו יודעים את שעות הפעילות של המסעדה (9:00 עד 21:00).

הקוד של הדוגמה הזו נמצא ב-codelab/public_partitions.go.

פשוט ניצור PCollection של שעות בין 9 ל-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 אם משתמשים במחיצות ציבוריות וב-Laplace Noise (ברירת מחדל), כמו במקרה שלמעלה.

כשמריצים את צינור הנתונים עם מחיצות ציבוריות (עם 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

שוב, בדומה לפעולות count ו-mean, מכיוון שמדובר בפעולה של פרטיות דיפרנציאלית, נקבל תוצאות שונות בכל פעם שנפעיל אותה. אבל אפשר לראות שהתוצאה הפרטית הדיפרנציאלית קרובה מאוד להכנסות בפועל לשעה.

8. חישוב של כמה נתונים סטטיסטיים

ברוב המקרים, תרצו לחשב כמה נתונים סטטיסטיים על אותם נתונים בסיסיים, בדומה למה שעשיתם עם count, ‏ mean ו-sum. בדרך כלל קל יותר לעשות את זה בצינור Beam יחיד ובקובץ בינארי יחיד. אפשר לעשות את זה גם באמצעות Privacy on 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. (אופציונלי) שינוי הפרמטרים של הפרטיות הדיפרנציאלית

ב-codelab הזה הוזכרו כמה פרמטרים: אפסילון, דלתא, maxPartitionsContributed וכו'. אפשר לחלק אותם לשתי קטגוריות: פרמטרים של פרטיות ופרמטרים של שימושיות.

פרמטרים של פרטיות

אפסילון ודלתא הם הפרמטרים שמגדירים את רמת הפרטיות שאנחנו מספקים באמצעות פרטיות דיפרנציאלית. במילים אחרות, אפסילון ודלתא הם מדד לכמות המידע שתוקף פוטנציאלי יכול לקבל על הנתונים הבסיסיים על ידי בחינת הפלט האנונימי. ככל שערכי האפסילון והדלתא גבוהים יותר, התוקף מקבל יותר מידע על הנתונים הבסיסיים, וזה סיכון לפרטיות.

מצד שני, ככל שערכי האפסילון והדלתא נמוכים יותר, כך צריך להוסיף יותר רעשי רקע לפלט כדי להפוך אותו לאנונימי, וכך צריך שיהיו יותר משתמשים ייחודיים בכל מחיצה כדי שהמחיצה הזו תישאר בפלט האנונימי. לכן, יש כאן פשרה בין התועלת לבין הפרטיות.

ב-Privacy on Beam, כשמציינים את תקציב הפרטיות הכולל ב-PrivacySpec, צריך לדאוג להבטחות הפרטיות שרוצים לקבל בפלט האנונימי. חשוב לזכור שאם רוצים להבטיח את שמירת הפרטיות, צריך לפעול לפי ההמלצות שבסדנת ה-codelab הזו כדי לא לנצל את התקציב יתר על המידה. לשם כך, צריך להקצות 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. סיכום

מעולה! סיימתם את ה-Codelab בנושא פרטיות ב-Beam. קיבלתם הרבה מידע על פרטיות דיפרנציאלית ועל פרטיות ב-Beam:

  • הפיכת PCollection לPrivatePCollection על ידי התקשרות אל MakePrivateFromStruct.
  • שימוש ב-Count כדי לחשב ספירות עם פרטיות דיפרנציאלית.
  • שימוש ב-MeanPerKey כדי לחשב ממוצעים עם פרטיות דיפרנציאלית.
  • שימוש ב-SumPerKey כדי לחשב סכומים עם פרטיות דיפרנציאלית.
  • חישוב של כמה נתונים סטטיסטיים באמצעות PrivacySpec אחד בצינור אחד.
  • (אופציונלי) התאמה אישית של הפרמטרים PrivacySpec ושל הצבירה (CountParams, MeanParams, SumParams).

אבל יש עוד הרבה צבירות (למשל, קוונטילים, ספירת ערכים נפרדים) שאפשר לבצע באמצעות Privacy on Beam. מידע נוסף על התוספים האלה זמין במאגר GitHub או ב-godoc.

אם יש לך זמן, נשמח אם תמלא סקר כדי לשלוח לנו משוב על ה-codelab.