محاسبه آمار خصوصی با حریم خصوصی در پرتو

۱. مقدمه

ممکن است فکر کنید که آمار تجمعی هیچ اطلاعاتی در مورد افرادی که داده‌های آنها از آمار تشکیل شده است، فاش نمی‌کند. با این حال، روش‌های زیادی وجود دارد که یک مهاجم می‌تواند از یک آمار تجمعی، اطلاعات حساسی در مورد افراد موجود در یک مجموعه داده به دست آورد.

برای محافظت از حریم خصوصی افراد، یاد خواهید گرفت که چگونه با استفاده از تجمیع‌های خصوصی تفاضلی از Privacy on Beam، آمار خصوصی تولید کنید. Privacy on Beam یک چارچوب حریم خصوصی تفاضلی است که با Apache Beam کار می‌کند.

منظور ما از «خصوصی» چیست؟

وقتی در سراسر این Codelab از کلمه «خصوصی» استفاده می‌کنیم، منظورمان این است که خروجی به گونه‌ای تولید می‌شود که هیچ اطلاعات خصوصی در مورد افراد موجود در داده‌ها فاش نشود. ما می‌توانیم این کار را با استفاده از حریم خصوصی تفاضلی، یک مفهوم قوی از ناشناس‌سازی در حوزه حریم خصوصی، انجام دهیم. ناشناس‌سازی فرآیند جمع‌آوری داده‌ها از چندین کاربر برای محافظت از حریم خصوصی کاربر است. همه روش‌های ناشناس‌سازی از تجمیع استفاده می‌کنند، اما همه روش‌های تجمیع به ناشناس‌سازی دست نمی‌یابند. از سوی دیگر، حریم خصوصی تفاضلی، تضمین‌های قابل اندازه‌گیری در مورد نشت اطلاعات و حریم خصوصی ارائه می‌دهد.

۲. مرور کلی حریم خصوصی تفاضلی

برای درک بهتر حریم خصوصی تفاضلی، اجازه دهید به یک مثال ساده نگاه کنیم.

این نمودار میله‌ای شلوغی یک رستوران کوچک را در یک شب خاص نشان می‌دهد. تعداد زیادی از مهمانان ساعت ۷ عصر می‌آیند و رستوران ساعت ۱ بامداد کاملاً خالی است:

a43dbf3e2c6de596.png

این به نظر مفید میاد!

یک نکته وجود دارد. وقتی یک مهمان جدید وارد می‌شود ، این واقعیت بلافاصله توسط نمودار میله‌ای آشکار می‌شود. به نمودار نگاه کنید: مشخص است که یک مهمان جدید وجود دارد و این مهمان تقریباً ساعت ۱ بامداد رسیده است:

bda96729e700a9dd.png

این از منظر حریم خصوصی خوب نیست. یک آمار واقعاً ناشناس نباید مشارکت‌های فردی را فاش کند. قرار دادن این دو نمودار در کنار هم، این موضوع را آشکارتر می‌کند: نمودار میله‌ای نارنجی یک مهمان اضافی دارد که حدود ساعت ۱ بامداد رسیده است:

d562ddf799288894.png

باز هم، این عالی نیست. چه کار کنیم؟

ما با اضافه کردن نویز تصادفی، دقت نمودارهای میله‌ای را کمی کاهش خواهیم داد!

به دو نمودار میله‌ای زیر نگاه کنید. اگرچه کاملاً دقیق نیستند، اما همچنان مفید هستند و سهم هر فرد را نشان نمی‌دهند. عالی است!

838a0293cd4fcfe3.gif

حریم خصوصی تفاضلی به معنای افزودن مقدار مناسبی از نویز تصادفی برای پنهان کردن مشارکت‌های فردی است .

تحلیل ما تا حدودی بیش از حد ساده‌سازی شده بود. پیاده‌سازی صحیح حریم خصوصی تفاضلی پیچیده‌تر است و ظرافت‌های پیاده‌سازی کاملاً غیرمنتظره‌ای دارد. مشابه رمزنگاری، ایجاد پیاده‌سازی شخصی از حریم خصوصی تفاضلی ممکن است ایده خوبی نباشد. می‌توانید به جای پیاده‌سازی راهکار خودتان، از حریم خصوصی روی بیم (Privacy on Beam) استفاده کنید. حریم خصوصی تفاضلی خودتان را ایجاد نکنید!

در این آزمایشگاه کد، نحوه انجام تحلیل‌های خصوصی تفاضلی را با استفاده از Privacy on Beam نشان خواهیم داد.

۳. دانلود حریم خصوصی در Beam

برای دنبال کردن مراحل کدنویسی نیازی به دانلود Privacy on Beam ندارید، زیرا تمام کدها و نمودارهای مربوطه در این سند موجود است. با این حال، اگر می‌خواهید برای بازی با کد، خودتان آن را دانلود کنید یا بعداً از Privacy on Beam استفاده کنید، می‌توانید با دنبال کردن مراحل زیر این کار را انجام دهید.

توجه داشته باشید که این codelab برای نسخه ۱.۱.۰ کتابخانه است.

ابتدا، حریم خصوصی در Beam را دانلود کنید:

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

یا می‌توانید مخزن گیت‌هاب را کلون کنید:

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

حریم خصوصی در Beam در بالاترین سطح دایرکتوری privacy-on-beam/ قرار دارد.

کد مربوط به این codelab و مجموعه داده در دایرکتوری privacy-on-beam/codelab/ قرار دارد.

همچنین باید Bazel را روی رایانه خود نصب کنید. دستورالعمل‌های نصب برای سیستم عامل خود را در وب‌سایت Bazel بیابید.

۴. محاسبه بازدیدها در هر ساعت

تصور کنید که صاحب رستوران هستید و می‌خواهید برخی آمارها در مورد رستوران خود، مانند افشای زمان‌های بازدید محبوب، را به اشتراک بگذارید. خوشبختانه، شما در مورد حریم خصوصی تفاضلی و ناشناس‌سازی اطلاعات دارید، بنابراین می‌خواهید این کار را به گونه‌ای انجام دهید که اطلاعات هیچ یک از بازدیدکنندگان فاش نشود.

کد این مثال در codelab/count.go قرار دارد.

بیایید با بارگذاری یک مجموعه داده آزمایشی شامل بازدیدهای شما از رستوران در یک دوشنبه خاص شروع کنیم. کد مربوط به این کار برای اهداف این آزمایشگاه کد جالب نیست، اما می‌توانید کد مربوط به آن را در codelab/main.go ، codelab/utils.go و codelab/visit.go بررسی کنید.

شناسه بازدیدکننده

زمان وارد شده

زمان صرف شده (دقیقه)

مبلغ خرج شده (یورو)

۱

۹:۳۰:۰۰ صبح

۲۶

۲۴

۲

۱۱:۵۴:۰۰ صبح

۵۳

۱۷

۳

۱۳:۰۵:۰۰

۸۱

۳۳

ابتدا با استفاده از 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 باید مجموعه‌ای از structها باشد. ما باید یک PrivacySpec و یک idFieldPath به عنوان ورودی به MakePrivateFromStruct وارد کنیم.

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

PrivacySpec یک ساختار است که پارامترهای حریم خصوصی تفاضلی (اپسیلون و دلتا) را که می‌خواهیم برای ناشناس‌سازی داده‌ها استفاده کنیم، در خود نگه می‌دارد. (فعلا لازم نیست نگران آنها باشید، بعداً یک بخش اختیاری داریم که اگر مایل به کسب اطلاعات بیشتر در مورد آنها هستید، می‌توانید از آن استفاده کنید.)

idFieldPath مسیر فیلد شناسه کاربر درون struct (در مورد ما Visit ) است. در اینجا، شناسه کاربر بازدیدکنندگان، فیلد VisitorID از Visit است.

سپس، به جای stats.Count() ، تابع pbeam.Count() () را فراخوانی می‌کنیم. pbeam.Count() یک struct 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 تعداد ساعات بازدید مختلف را که یک کاربر می‌تواند مشارکت کند، محدود می‌کند. ما انتظار داریم که آنها حداکثر یک بار در روز به رستوران مراجعه کنند (یا اهمیتی نمی‌دهیم که آیا آنها چندین بار در طول روز به آن مراجعه می‌کنند)، بنابراین آن را نیز روی ۱ تنظیم می‌کنیم. در مورد این پارامترها با جزئیات بیشتر در یک بخش اختیاری صحبت خواهیم کرد.

MaxValue تعداد دفعاتی را که یک کاربر می‌تواند در مقادیری که ما شمارش می‌کنیم، محدود می‌کند. در این مورد خاص، مقادیری که ما شمارش می‌کنیم، ساعات بازدید هستند و ما انتظار داریم که یک کاربر فقط یک بار از رستوران بازدید کند (یا اهمیتی نمی‌دهیم که آیا چندین بار در ساعت از آن بازدید می‌کند)، بنابراین این پارامتر را روی ۱ تنظیم می‌کنیم.

در نهایت، کد شما به این شکل خواهد بود:

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 ) را برای آماره differentially private می‌بینیم (دستور قبلی هم خطوط لوله غیرخصوصی و هم خطوط لوله خصوصی را اجرا می‌کند):

d6a0ace1acd3c760.png

تبریک! شما اولین آماره خصوصی تفاضلی خود را محاسبه کردید!

نمودار میله‌ای که هنگام اجرای کد دریافت می‌کنید ممکن است با این نمودار متفاوت باشد. اشکالی ندارد. به دلیل نویز موجود در حریم خصوصی تفاضلی، هر بار که کد را اجرا می‌کنید، نمودار میله‌ای متفاوتی دریافت خواهید کرد، اما می‌توانید ببینید که کم و بیش شبیه نمودار میله‌ای غیرخصوصی اصلی هستند که داشتیم.

لطفاً توجه داشته باشید که برای تضمین حریم خصوصی، بسیار مهم است که خط لوله چندین بار اجرا نشود (مثلاً برای داشتن نمودار میله‌ای زیباتر). دلیل اینکه نباید خطوط لوله خود را دوباره اجرا کنید، در بخش «محاسبه آمار چندگانه» توضیح داده شده است.

۵. استفاده از پارتیشن‌های عمومی

در بخش قبلی، شاید متوجه شده باشید که ما تمام بازدیدها (داده‌ها) را برای برخی از پارتیشن‌ها، یعنی ساعت‌ها، حذف کردیم.

d7fbc5d86d91e54a.png

این به دلیل انتخاب/آستانه‌گذاری پارتیشن است، گامی مهم برای تضمین حریم خصوصی تفاضلی زمانی که وجود پارتیشن‌های خروجی به خود داده‌های کاربر بستگی دارد. در این صورت، صرف وجود یک پارتیشن در خروجی می‌تواند وجود یک کاربر خاص در داده‌ها را فاش کند (برای توضیح اینکه چرا این امر حریم خصوصی را نقض می‌کند، به این پست وبلاگ مراجعه کنید). برای جلوگیری از این امر، حریم خصوصی در Beam فقط پارتیشن‌هایی را نگه می‌دارد که تعداد کافی کاربر در آنها وجود دارد.

وقتی لیست پارتیشن‌های خروجی به داده‌های خصوصی کاربر وابسته نباشند، یعنی اطلاعات عمومی باشند، نیازی به این مرحله انتخاب پارتیشن نداریم. در واقع این مورد برای مثال رستوران ما صدق می‌کند: ما ساعات کاری رستوران (۹:۰۰ تا ۲۱:۰۰) را می‌دانیم.

کد مربوط به این مثال در codelab/public_partitions.go قرار دارد.

ما به سادگی یک PCollection از ساعات بین ۹ تا ۲۱ (به صورت انحصاری) ایجاد می‌کنیم و آن را در فیلد 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
}

توجه داشته باشید که اگر از پارتیشن‌های عمومی و نویز لاپلاس (پیش‌فرض) استفاده می‌کنید، می‌توانید دلتا را روی ۰ تنظیم کنید، همانطور که در بالا ذکر شد.

وقتی pipeline را با پارتیشن‌های عمومی اجرا می‌کنیم (با 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

همانطور که می‌بینید، اکنون پارتیشن‌های ۹، ۱۰ و ۱۶ را که قبلاً حذف کرده بودیم، بدون پارتیشن‌های عمومی نگه می‌داریم.

استفاده از پارتیشن‌های عمومی نه تنها به شما امکان می‌دهد پارتیشن‌های بیشتری نگه دارید، بلکه به دلیل عدم صرف بودجه حریم خصوصی، یعنی اپسیلون و دلتا، در انتخاب پارتیشن، تقریباً نصف نویز به هر پارتیشن اضافه می‌کند. به همین دلیل است که تفاوت بین شمارش‌های خام و خصوصی در مقایسه با اجرای قبلی کمی کمتر است.

هنگام استفاده از پارتیشن‌های عمومی، دو نکته مهم وجود دارد که باید در نظر داشته باشید:

  1. هنگام استخراج لیست پارتیشن‌ها از داده‌های خام مراقب باشید: اگر این کار را به روش خصوصی تفاضلی انجام ندهید، مثلاً صرفاً لیست تمام پارتیشن‌ها را در داده‌های کاربر بخوانید، خط لوله شما دیگر تضمین حریم خصوصی تفاضلی را ارائه نمی‌دهد. برای نحوه انجام این کار به روش خصوصی تفاضلی، به بخش پیشرفته زیر مراجعه کنید.
  2. اگر برای برخی از پارتیشن‌های عمومی داده‌ای (مثلاً بازدیدها) وجود نداشته باشد، برای حفظ حریم خصوصی تفاضلی، نویز به آن پارتیشن‌ها اعمال می‌شود. برای مثال، اگر از ساعات بین ۰ تا ۲۴ (به جای ۹ و ۲۱) استفاده کنیم، تمام ساعات دارای نویز می‌شوند و ممکن است برخی از بازدیدها را در زمانی که هیچ بازدیدی وجود ندارد، نشان دهند.

(پیشرفته) استخراج پارتیشن‌ها از داده‌ها

اگر چندین تجمیع را با لیست یکسانی از پارتیشن‌های خروجی غیرعمومی در یک خط لوله اجرا می‌کنید، می‌توانید لیست پارتیشن‌ها را یک بار با استفاده SelectPartitions() استخراج کنید و پارتیشن‌ها را به عنوان ورودی PublicPartition به هر تجمیع ارائه دهید. این کار نه تنها از دیدگاه حریم خصوصی ایمن است، بلکه به شما امکان می‌دهد به دلیل استفاده از بودجه حریم خصوصی در انتخاب پارتیشن فقط یک بار برای کل خط لوله، نویز کمتری ایجاد کنید.

۶. محاسبه میانگین مدت اقامت

حالا که می‌دانیم چگونه می‌توان اشیاء را به روش خصوصی تفاضلی شمارش کرد، بیایید به محاسبه میانگین‌ها بپردازیم. به طور خاص‌تر، اکنون میانگین مدت اقامت بازدیدکنندگان را محاسبه خواهیم کرد.

کد این مثال در 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 را روی ۰ تنظیم می‌کنیم زیرا انتظار نداریم بازدیدکنندگان کمتر از ۰ دقیقه در رستوران صرف کنند. MaxValue روی ۶۰ تنظیم می‌کنیم، به این معنی که اگر یک بازدیدکننده بیش از ۶۰ دقیقه صرف کند، طوری عمل می‌کنیم که انگار آن کاربر ۶۰ دقیقه صرف کرده است.

در نهایت، کد شما به این شکل خواهد بود:

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 ) را برای آماره differentially private می‌بینیم (دستور قبلی هم خطوط لوله غیرخصوصی و هم خطوط لوله خصوصی را اجرا می‌کند):

e8ac6a9bf9792287.png

باز هم، مشابه شمارش، از آنجایی که این یک عملیات differentially private است، هر بار که آن را اجرا می‌کنیم نتایج متفاوتی خواهیم گرفت. اما می‌توانید ببینید که مدت زمان اقامت differentially private با نتیجه واقعی فاصله زیادی ندارد.

۷. درآمد محاسباتی در هر ساعت

یکی دیگر از آمارهای جالب که می‌توانیم به آن نگاه کنیم، درآمد به ازای هر ساعت در طول روز است.

کد این مثال در 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 را روی ۰ تنظیم می‌کنیم زیرا انتظار نداریم بازدیدکنندگان کمتر از ۰ یورو در رستوران خرج کنند. MaxValue روی ۴۰ تنظیم می‌کنیم، به این معنی که اگر یک بازدیدکننده بیش از ۴۰ یورو خرج کند، طوری عمل می‌کنیم که انگار آن کاربر ۴۰ یورو خرج کرده است.

در نهایت، کد به این شکل خواهد بود:

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 ) برای آماره differentially private می‌بینیم (دستور قبلی هم خطوط لوله غیرخصوصی و هم خطوط لوله خصوصی را اجرا می‌کند):

46c375e874f3e7c4.png

باز هم، مشابه count و mean، از آنجایی که این یک عملیات differentially private است، هر بار که آن را اجرا می‌کنیم نتایج متفاوتی خواهیم گرفت. اما می‌توانید ببینید که نتیجه differentially private بسیار نزدیک به درآمد واقعی در هر ساعت است.

۸. محاسبه آمار چندگانه

اغلب اوقات، ممکن است شما علاقه‌مند به محاسبه چندین آماره روی داده‌های زیربنایی یکسان باشید، مشابه کاری که با شمارش، میانگین و مجموع انجام داده‌اید. معمولاً انجام این کار در یک خط لوله 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
}

۹. (اختیاری) تغییر پارامترهای حریم خصوصی تفاضلی

شما پارامترهای زیادی را که در این آزمایشگاه کد ذکر شده‌اند، دیده‌اید: 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 structها ارائه می‌شود و هر تجمیع می‌تواند مقادیر جداگانه‌ای برای آنها داشته باشد. برخلاف MaxPartitionsContributed ، MaxContributionsPerPartition سهم یک کاربر را برای هر کلید محدود می‌کند. به عبارت دیگر، یک کاربر می‌تواند فقط مقادیر MaxContributionsPerPartition را برای هر کلید ارائه دهد.

نویز اضافه شده به خروجی توسط MaxPartitionsContributed و MaxContributionsPerPartition مقیاس‌بندی می‌شود، بنابراین در اینجا یک بده‌بستان وجود دارد: MaxPartitionsContributed و MaxContributionsPerPartition بزرگتر هر دو به این معنی هستند که شما داده‌های بیشتری را نگه می‌دارید، اما در نهایت نتیجه نویز بیشتری خواهید داشت.

برخی از تجمیع‌ها به MinValue و MaxValue نیاز دارند. این دو، محدوده‌ی مشارکت هر کاربر را مشخص می‌کنند. اگر کاربری مقداری کمتر از MinValue ارائه دهد، آن مقدار تا MinValue محدود می‌شود. به طور مشابه، اگر کاربری مقداری بزرگتر از MaxValue ارائه دهد، آن مقدار تا MaxValue محدود می‌شود. این بدان معناست که برای حفظ مقادیر اصلی بیشتر، باید محدوده‌های بزرگتری را مشخص کنید. مشابه MaxPartitionsContributed و MaxContributionsPerPartition ، نویز با اندازه‌ی محدوده‌ها مقیاس‌بندی می‌شود، بنابراین محدوده‌های بزرگتر به این معنی است که داده‌های بیشتری را نگه می‌دارید، اما در نهایت نتیجه‌ی نویزدارتری خواهید داشت.

آخرین پارامتری که در مورد آن صحبت خواهیم کرد NoiseKind است. ما در Privacy On Beam از دو مکانیزم نویز مختلف پشتیبانی می‌کنیم: GaussianNoise و LaplaceNoise . هر دو مزایا و معایب خود را دارند، اما توزیع لاپلاس با مرزهای مشارکت پایین، کاربرد بهتری دارد، به همین دلیل است که Privacy On Beam به طور پیش‌فرض از آن استفاده می‌کند. با این حال، اگر می‌خواهید از نویز توزیع گاوسی استفاده کنید، می‌توانید متغیر pbeam.GaussianNoise{} را به Params اختصاص دهید.

۱۰. خلاصه

عالی بود! شما آزمایشگاه کدنویسی حریم خصوصی در Beam را تمام کردید. چیزهای زیادی در مورد حریم خصوصی تفاضلی و حریم خصوصی در Beam یاد گرفتید:

  • تبدیل PCollection به PrivatePCollection با فراخوانی MakePrivateFromStruct .
  • استفاده از Count برای محاسبه شمارش‌های خصوصی تفاضلی.
  • استفاده از MeanPerKey برای محاسبه میانگین‌های خصوصی تفاضلی
  • استفاده از SumPerKey برای محاسبه مجموع‌های خصوصی تفاضلی
  • محاسبه چندین آمار با یک PrivacySpec واحد در یک خط لوله واحد.
  • (اختیاری) سفارشی‌سازی PrivacySpec و پارامترهای تجمیع ( CountParams, MeanParams, SumParams ).

اما، تجمیع‌های بسیار بیشتری (مثلاً چندک‌ها، شمارش مقادیر متمایز) وجود دارد که می‌توانید با Privacy on Beam انجام دهید! می‌توانید در مخزن GitHub یا godoc درباره آنها اطلاعات بیشتری کسب کنید.

اگر وقت دارید، لطفاً با پر کردن یک نظرسنجی ، نظرات خود را در مورد codelab به ما ارائه دهید.