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

1. מבוא

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

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

מה המשמעות של "פרטי"?

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

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

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

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

a43dbf3e2c6de596.png

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

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

bda96729e700a9dd.png

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

d562ddf799288894.png

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

נהפוך את תרשימי העמודות קצת פחות מדויקים באמצעות הוספת רעש אקראי.

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

838a0293cd4fcfe3.gif

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

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

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

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

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

שימו לב שה-Codelab הזה מיועד לגרסה 1.1.0 של הספרייה.

קודם כול, מורידים את האפליקציה 'פרטיות' ב-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

13: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

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

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

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

אנחנו פשוט ניצור מערך נתונים קבוע של שעות בין 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
}

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

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

יש שני דברים שחשוב לזכור כשמשתמשים במחיצות ציבוריות:

  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, הם מייצגים משהו שנקרא תקציב פרטיות, שהוא מדד של רמת הפרטיות של המשתמשים בנתונים הבסיסיים שאתם מדלפים.

מה שחשוב לזכור לגבי תקציב פרטיות הוא שהוא שטח נוסף: אם אתם מפעילים צינור עיבוד נתונים עם אפסילון בפינה ו-דלתא מילת מפתח מסוימת פעם אחת, אתם מנצלים תקציב מסוים (כמו אימג', {6/}). אם תפעילו אותו בפעם השנייה, התקציב הכולל שלכם ינוצל באופן הבא: (2total, 2####). באופן דומה, אם תחשבו כמה נתונים סטטיסטיים באמצעות PrivacySpec (וברציפות תקציב פרטיות) של (≤,פט), התקציב הכולל שלכם ינוצל בסכום של (2 בפינה, 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 הזה: epsilon, delta, 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 לכל מפתח.

הרעש שנוסף לפלט מותאם לעומס (scaling) על ידי MaxPartitionsContributed ו-MaxContributionsPerPartition, ולכן יש יתרונות וחסרונות: MaxPartitionsContributed ו-MaxContributionsPerPartition גדולים יותר יגרמו לך לשמור יותר נתונים, אבל בסופו של דבר תתקבל תוצאה רועשת יותר.

לצבירת נתונים מסוימים נדרש MinValue ו-MaxValue. ההגדרות האלה מציינות את גבולות התרומות של כל משתמש. אם משתמש יתרום ערך הנמוך מ-MinValue, הערך הזה יוגבל עד MinValue. באופן דומה, אם משתמש יתרום ערך גדול מ-MaxValue, המערכת תצמצם את הערך הזה ל-MaxValue. כלומר, כדי לשמור יותר מהערכים המקוריים, צריך לציין גבולות גדולים יותר. בדומה ל-MaxPartitionsContributed ול-MaxContributionsPerPartition, הרעש מוגדל לפי גודל הגבולות, כך שגבולות גדולים יותר מאפשרים לשמור יותר נתונים, אבל בסופו של דבר מקבלים תוצאה עם רעשי רקע יותר.

הפרמטר האחרון שנדבר עליו הוא NoiseKind. אנחנו תומכים בשני מנגנוני רעש שונים ב-Privacy On Beam: GaussianNoise ו-LaplaceNoise. לשניהם יש יתרונות וחסרונות, אבל ההפצה של Laplace נותנת שירות טוב יותר עם גבולות נמוכים של תרומה, ולכן הכלי Privacy On Beam משתמש בה כברירת מחדל. עם זאת, אם אתם רוצים להשתמש ברעש של התפלגות גאוס, אפשר לספק את Params עם משתנה pbeam.GaussianNoise{}.

10. סיכום

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

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

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

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