1. Wprowadzenie
Możesz uważać, że statystyki zbiorcze nie ujawniają żadnych informacji o osobach, których dane są wykorzystywane do ich tworzenia. Istnieje jednak wiele sposobów, w jakie osoba atakująca może uzyskać informacje poufne o osobach w zbiorze danych na podstawie statystyki zbiorczej.
Aby chronić prywatność osób, dowiesz się, jak tworzyć prywatne statystyki za pomocą agregacji z prywatnością różnicową w Privacy on Beam. Privacy on Beam to platforma prywatności różnicowej, która współpracuje z Apache Beam.
Co rozumiemy przez „prywatne”?
W tym ćwiczeniu używamy słowa „prywatne” w odniesieniu do danych wyjściowych, które są generowane w taki sposób, aby nie ujawniać żadnych informacji prywatnych o osobach, których dane są wykorzystywane. Możemy to zrobić za pomocą prywatności różnicowej, czyli zaawansowanej metody anonimizacji. Anonimizacja to proces agregowania danych wielu użytkowników w celu ochrony ich prywatności. Wszystkie metody anonimizacji wykorzystują agregację, ale nie wszystkie metody agregacji zapewniają anonimizację. Prywatność różnicowa zapewnia natomiast mierzalne gwarancje dotyczące wycieku informacji i prywatności.
2. Omówienie prywatności różnicowej
Aby lepiej zrozumieć prywatność różnicową, przyjrzyjmy się prostemu przykładowi.
Ten wykres słupkowy pokazuje, jak bardzo oblegana jest mała restauracja w jeden konkretny wieczór. Wielu gości przychodzi o 19:00, a o 1:00 w nocy restauracja jest już pusta:

To wygląda przydatnie.
Jest jednak pewien haczyk. Gdy przyjdzie nowy gość, od razu zobaczysz to na wykresie słupkowym. Spójrz na wykres: widać na nim nowego gościa, który przybył około godziny 1:00:

Z punktu widzenia prywatności nie jest to najlepsze rozwiązanie. Prawdziwie zanonimizowane statystyki nie powinny ujawniać wkładu poszczególnych osób. Porównanie tych dwóch wykresów jest jeszcze bardziej wymowne: na pomarańczowym wykresie słupkowym widać dodatkowego gościa, który przybył około 1:00:

To też nie jest najlepsze rozwiązanie. Co robimy?
Dodamy losowy szum, aby wykresy słupkowe były nieco mniej dokładne.
Spójrz na 2 wykresy słupkowe poniżej. Nie są one całkowicie dokładne, ale nadal przydatne i nie ujawniają indywidualnych wkładów. Super!

Prywatność różnicowa dodaje odpowiednią ilość losowego szumu, aby zamaskować indywidualny wkład.
Nasza analiza była nieco zbyt uproszczona. Prawidłowe wdrożenie prywatności różnicowej jest bardziej skomplikowane i ma wiele dość nieoczekiwanych subtelności. Podobnie jak w przypadku kryptografii, tworzenie własnej implementacji prywatności różnicowej może nie być dobrym pomysłem. Zamiast wdrażać własne rozwiązanie, możesz użyć Privacy on Beam. Nie twórz własnych rozwiązań z zakresu prywatności różnicowej.
W tym ćwiczeniu pokażemy, jak przeprowadzić analizę z zachowaniem prywatności różnicowej za pomocą Privacy on Beam.
3. Pobieranie Privacy on Beam
Nie musisz pobierać Privacy on Beam, aby móc przejść to ćwiczenie w Codelabs, ponieważ cały odpowiedni kod i wykresy znajdziesz w tym dokumencie. Jeśli jednak chcesz pobrać kod, aby się nim pobawić, uruchomić go samodzielnie lub później użyć Privacy on Beam, wykonaj te czynności.
Pamiętaj, że to ćwiczenie dotyczy biblioteki w wersji 1.1.0.
Najpierw pobierz Privacy on Beam:
https://github.com/google/differential-privacy/archive/refs/tags/v1.1.0.tar.gz
Możesz też sklonować repozytorium GitHub:
git clone --branch v1.1.0 https://github.com/google/differential-privacy.git
Prywatność w Beam znajduje się w katalogu privacy-on-beam/ najwyższego poziomu.
Kod tego ćwiczenia i zbiór danych znajdują się w katalogu privacy-on-beam/codelab/.
Na komputerze musi być też zainstalowany Bazel. Instrukcje instalacji dla swojego systemu operacyjnego znajdziesz na stronie Bazel.
4. Obliczanie wizyt na godzinę
Wyobraź sobie, że jesteś właścicielem restauracji i chcesz udostępnić statystyki dotyczące Twojej restauracji, np. popularne godziny odwiedzin. Na szczęście znasz prywatność różnicową i anonimizację, więc chcesz to zrobić w taki sposób, aby nie ujawniać informacji o żadnym z użytkowników.
Kod tego przykładu znajdziesz w codelab/count.go.
Zacznijmy od wczytania przykładowego zbioru danych zawierającego wizyty w Twojej restauracji w określony poniedziałek. Kod tego elementu nie jest istotny w tym ćwiczeniu, ale możesz go sprawdzić w plikach codelab/main.go, codelab/utils.go i codelab/visit.go.
Identyfikator odwiedzającego | Czas wpisania | Spędzony czas (min) | Wydane środki (w euro) |
1 | 9:30:00 AM | 26 | 24 |
2 | 11:54:00 AM | 53 | 17 |
3 | 13:05:00 | 81 | 33 |
Najpierw w przykładowym kodzie poniżej utworzysz za pomocą Beam wykres słupkowy przedstawiający godziny wizyt w Twojej restauracji. Scope reprezentuje potok, a każda nowa operacja wykonywana na danych jest dodawana do Scope. CountVisitsPerHour przyjmuje Scope i kolekcję wizyt, która jest reprezentowana jako PCollection w Beam. Wyodrębnia godzinę każdej wizyty, stosując funkcję extractVisitHour do kolekcji. Następnie zlicza wystąpienia każdej godziny i zwraca wynik.
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()
}
Spowoduje to utworzenie w bieżącym katalogu ładnego wykresu słupkowego (po uruchomieniu polecenia bazel run codelab -- --example="count" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/count.csv --output_chart_file=$(pwd)/count.png) w postaci pliku count.png:

Następnym krokiem jest przekształcenie potoku i wykresu słupkowego w elementy prywatne. Robimy to w ten sposób.
Najpierw zadzwoń pod numer MakePrivateFromStruct na PCollection<V>, aby uzyskać PrivatePCollection<V>. Dane wejściowe PCollection muszą być zbiorem struktur. Musimy wprowadzić PrivacySpec i idFieldPath jako dane wejściowe do MakePrivateFromStruct.
spec := pbeam.NewPrivacySpec(epsilon, delta) pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")
PrivacySpec to struktura, która zawiera parametry prywatności różnicowej (epsilon i delta), których chcemy użyć do anonimizacji danych. (Nie musisz się nimi teraz przejmować. Jeśli chcesz dowiedzieć się o nich więcej, w dalszej części znajdziesz sekcję opcjonalną).
idFieldPath to ścieżka pola identyfikatora użytkownika w strukturze (w naszym przypadku Visit). W tym przypadku identyfikatorem użytkownika odwiedzających jest pole VisitorID w Visit.
Następnie wywołujemy funkcję pbeam.Count() zamiast stats.Count(). Funkcja pbeam.Count() przyjmuje jako dane wejściowe strukturę CountParams, która zawiera parametry, takie jak MaxValue, które wpływają na dokładność danych wyjściowych.
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,
})
Podobnie MaxPartitionsContributed ogranicza liczbę różnych godzin wizyt, które użytkownik może dodać. Oczekujemy, że użytkownik odwiedzi restaurację co najwyżej raz dziennie (lub nie obchodzi nas, czy odwiedzi ją kilka razy w ciągu dnia), więc ustawiamy tę wartość na 1. Więcej informacji o tych parametrach znajdziesz w sekcji opcjonalnej.
MaxValue ogranicza liczbę razy, kiedy jeden użytkownik może mieć wpływ na zliczane wartości. W tym konkretnym przypadku liczymy godziny wizyt i oczekujemy, że użytkownik odwiedzi restaurację tylko raz (lub nie obchodzi nas, czy odwiedzi ją wiele razy w ciągu godziny), więc ustawiamy ten parametr na 1.
Ostatecznie kod będzie wyglądać tak:
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
}
Podobny wykres słupkowy (count_dp.png) widzimy w przypadku statystyki zgodnej z prywatnością różnicową (poprzednie polecenie uruchamia zarówno potok niezgodny z prywatnością, jak i zgodny z nią):

Gratulacje! Obliczyłeś(-aś) pierwszą statystykę z ochroną prywatności różnicowej.
Wykres słupkowy, który uzyskasz po uruchomieniu kodu, może się różnić od tego. To świetnie. Ze względu na szum w prywatności różnicowej za każdym razem, gdy uruchomisz kod, otrzymasz inny wykres słupkowy, ale możesz zauważyć, że są one mniej więcej podobne do pierwotnego wykresu słupkowego bez ochrony prywatności.
Pamiętaj, że w celu zachowania gwarancji prywatności nie należy wielokrotnie uruchamiać potoku (np. aby uzyskać bardziej atrakcyjny wykres słupkowy). Powód, dla którego nie należy ponownie uruchamiać potoków, został wyjaśniony w sekcji „Obliczanie wielu statystyk”.
5. Korzystanie z partycji publicznych
W poprzedniej sekcji można było zauważyć, że w przypadku niektórych partycji, czyli godzin, usunęliśmy wszystkie wizyty (dane).

Wynika to z wyboru partycji lub ustalania progów, co jest ważnym krokiem w zapewnianiu gwarancji prywatności różnicowej, gdy istnienie partycji wyjściowych zależy od samych danych użytkownika. W takim przypadku samo istnienie partycji w danych wyjściowych może ujawnić obecność konkretnego użytkownika w danych (wyjaśnienie, dlaczego narusza to prywatność, znajdziesz w tym poście na blogu). Aby temu zapobiec, Privacy on Beam zachowuje tylko partycje, w których jest wystarczająca liczba użytkowników.
Jeśli lista partycji wyjściowych nie zależy od prywatnych danych użytkownika, czyli są to informacje publiczne, nie potrzebujemy tego kroku wyboru partycji. Tak jest w przypadku naszej restauracji: znamy jej godziny pracy (9:00–21:00).
Kod tego przykładu znajdziesz w codelab/public_partitions.go.
Utworzymy po prostu obiekt typu PCollection godzin między 9 a 21 (bez tych wartości) i wprowadzimy ją do pola PublicPartitions w 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
}
Pamiętaj, że w przypadku korzystania z partycji publicznych i szumu Laplace’a (domyślnego), jak w powyższym przykładzie, można ustawić wartość delta na 0.
Gdy uruchomimy potok z partycjami publicznymi (z 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), otrzymamy (public_partitions_dp.png):

Jak widać, zachowujemy teraz partycje 9, 10 i 16, które wcześniej usunęliśmy bez partycji publicznych.
Korzystanie z partycji publicznych nie tylko pozwala zachować więcej partycji, ale też dodaje do każdej z nich około połowy szumu w porównaniu z sytuacją, w której nie są one używane. Dzieje się tak, ponieważ na wybór partycji nie wydajesz żadnego budżetu na ochronę prywatności, czyli epsilona i delty. Dlatego różnica między liczbą nieprzetworzoną a prywatną jest nieco mniejsza niż w przypadku poprzedniego uruchomienia.
Korzystając z partycji publicznych, należy pamiętać o 2 ważnych kwestiach:
- Zachowaj ostrożność podczas tworzenia listy partycji na podstawie surowych danych: jeśli nie zrobisz tego w sposób zapewniający prywatność różnicową, np. po prostu odczytując listę wszystkich partycji w danych użytkownika, Twój potok nie będzie już gwarantować prywatności różnicowej. W sekcji zaawansowanej poniżej znajdziesz informacje o tym, jak to zrobić w sposób zapewniający prywatność różnicową.
- Jeśli w przypadku niektórych publicznych partycji nie ma danych (np. wizyt), do tych partycji zostanie dodany szum, aby zachować prywatność różnicową. Jeśli na przykład użyjemy godzin od 0 do 24 (zamiast od 9 do 21), wszystkie godziny zostaną zaszumione i mogą wykazywać wizyty, gdy ich nie ma.
(Zaawansowane) Tworzenie partycji na podstawie danych
Jeśli w tym samym potoku uruchamiasz wiele agregacji z tą samą listą niepublicznych partycji wyjściowych, możesz raz wyprowadzić listę partycji za pomocą funkcji SelectPartitions() i przekazać partycje do każdej agregacji jako dane wejściowe PublicPartition. Jest to nie tylko bezpieczne z punktu widzenia prywatności, ale też pozwala ograniczyć szum, ponieważ budżet prywatności jest wykorzystywany do wyboru partycji tylko raz w przypadku całego potoku.
6. Obliczanie średniej długości pobytu
Wiemy już, jak zliczać elementy w sposób zapewniający prywatność różnicową, więc przyjrzyjmy się teraz obliczaniu średnich. Od teraz będziemy obliczać średnią długość pobytu użytkowników.
Kod tego przykładu znajdziesz w codelab/mean.go.
Zwykle, aby obliczyć nieprywatną średnią czasu trwania pobytu, używamy stats.MeanPerKey() z krokiem wstępnego przetwarzania, który przekształca przychodzące PCollection wizyt w PCollection<K,V>, gdzie K to godzina wizyty, a V to czas, jaki gość spędził w restauracji.
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
}
Spowoduje to utworzenie w bieżącym katalogu ładnego wykresu słupkowego (po uruchomieniu polecenia bazel run codelab -- --example="mean" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/mean.csv --output_chart_file=$(pwd)/mean.png) w postaci pliku mean.png:

Aby zapewnić prywatność różnicową, ponownie przekształcamy PCollection w PrivatePCollection i zastępujemy stats.MeanPerKey() wartością pbeam.MeanPerKey(). Podobnie jak w przypadku Count, mamy MeanParams, które zawierają niektóre parametry, np. MinValue i MaxValue, wpływające na dokładność. MinValue i MaxValue to granice, w których mieści się wkład każdego użytkownika w poszczególne klucze.
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,
})
W tym przypadku każdy klucz reprezentuje godzinę, a wartości to czas spędzony przez użytkowników. Ustawiliśmy wartość MinValue na 0, ponieważ nie spodziewamy się, że goście spędzą w restauracji mniej niż 0 minut. Ustawiamy wartość MaxValue na 60, co oznacza, że jeśli użytkownik spędzi w witrynie więcej niż 60 minut, będziemy traktować ten czas jako 60 minut.
Ostatecznie kod będzie wyglądać tak:
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
}
Podobny wykres słupkowy (mean_dp.png) widzimy w przypadku statystyki zgodnej z prywatnością różnicową (poprzednie polecenie uruchamia zarówno potok niezgodny z prywatnością, jak i zgodny z nią):

Podobnie jak w przypadku funkcji count, ponieważ jest to operacja zapewniająca prywatność różnicową, za każdym razem, gdy ją uruchomimy, uzyskamy inne wyniki. Jak widać, długości pobytu chronione przez prywatność różnicową nie odbiegają zbytnio od rzeczywistych wyników.
7. Obliczanie przychodów na godzinę
Kolejną ciekawą statystyką, na którą możemy spojrzeć, są przychody na godzinę w ciągu dnia.
Kod tego przykładu znajdziesz w codelab/sum.go.
Zaczniemy od wersji nieprywatnej. Po wstępnym przetworzeniu naszego przykładowego zbioru danych możemy utworzyć PCollection<K,V>, gdzie K to godzina wizyty, a V to kwota wydana przez klienta w restauracji. Aby obliczyć nieprywatne przychody na godzinę, możemy po prostu zsumować wszystkie pieniądze wydane przez klientów, wywołując 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
}
Spowoduje to utworzenie w bieżącym katalogu ładnego wykresu słupkowego (po uruchomieniu polecenia bazel run codelab -- --example="sum" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/sum.csv --output_chart_file=$(pwd)/sum.png) w postaci pliku sum.png:

Aby zapewnić prywatność różnicową, ponownie przekształcamy PCollection w PrivatePCollection i zastępujemy stats.SumPerKey() wartością pbeam.SumPerKey(). Podobnie jak w przypadku Count i MeanPerKey, mamy SumParams, które zawierają niektóre parametry, np. MinValue i MaxValue, wpływające na dokładność.
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,
})
W tym przypadku MinValue i MaxValue to przedziały, w których mieszczą się wydatki każdego odwiedzającego. Ustawiliśmy wartość MinValue na 0, ponieważ nie spodziewamy się, że goście wydadzą w restauracji mniej niż 0 euro. Ustawiamy wartość MaxValue na 40, co oznacza, że jeśli użytkownik wyda więcej niż 40 euro, traktujemy go tak, jakby wydał 40 euro.
Ostatecznie kod będzie wyglądać tak:
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
}
Podobny wykres słupkowy (sum_dp.png) widzimy w przypadku statystyki zgodnej z prywatnością różnicową (poprzednie polecenie uruchamia zarówno potok niezgodny z prywatnością, jak i zgodny z nią):

Podobnie jak w przypadku funkcji count i mean, ponieważ jest to operacja zapewniająca prywatność różnicową, za każdym razem, gdy ją zastosujemy, uzyskamy inne wyniki. Jak widać, wynik uzyskany dzięki prywatności różnicowej jest bardzo zbliżony do rzeczywistych przychodów na godzinę.
8. Obliczanie wielu statystyk
Zwykle możesz być zainteresowany obliczaniem wielu statystyk na podstawie tych samych danych, podobnie jak w przypadku liczby, średniej i sumy. Zwykle jest to prostsze i bardziej przejrzyste, gdy wykonuje się to w jednym potoku Beam i w jednym pliku binarnym. Możesz to zrobić również za pomocą Privacy on Beam. Możesz napisać jeden potok, aby uruchamiać przekształcenia i obliczenia, i używać jednego PrivacySpec dla całego potoku.
Jest to nie tylko wygodniejsze, ale też lepsze z punktu widzenia ochrony prywatności.PrivacySpec Jeśli pamiętasz parametry epsilon i delta, które przekazujemy do PrivacySpec, reprezentują one coś, co nazywamy budżetem prywatności. Jest to miara tego, ile prywatności użytkowników w danych bazowych ujawniasz.
Ważną rzeczą, o której należy pamiętać w przypadku budżetu prywatności, jest to, że jest on addytywny: jeśli uruchomisz potok z określonymi wartościami epsilon ε i delta δ jeden raz, wykorzystasz budżet (ε,δ). Jeśli zastosujesz go po raz drugi, wydasz budżet całkowity (2ε, 2δ). Podobnie jeśli obliczysz wiele statystyk z PrivacySpec (a w konsekwencji budżetem prywatności) o wartości (ε,δ), wydasz łącznie budżet o wartości (2ε, 2δ). Oznacza to obniżenie gwarancji prywatności.
Aby tego uniknąć, gdy chcesz obliczyć wiele statystyk na podstawie tych samych danych, użyj jednego PrivacySpec z łącznym budżetem, którego chcesz użyć. Następnie musisz określić wartości epsilon i delta, które chcesz wykorzystać w przypadku każdej agregacji. Ostatecznie uzyskasz taką samą ogólną gwarancję prywatności, ale im wyższe wartości ypsilon i delta w przypadku konkretnej agregacji, tym większa będzie jej dokładność.
Aby zobaczyć, jak to działa, możemy obliczyć 3 statystyki (liczbę, średnią i sumę), które wcześniej obliczyliśmy osobno, w jednym potoku.
Kod tego przykładu znajdziesz w codelab/multiple.go. Zwróć uwagę, że dzielimy całkowity budżet (ε,δ) po równo na 3 agregacje:
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. (Opcjonalnie) Dostosowywanie parametrów prywatności różnicowej
W tym ćwiczeniu pojawiło się kilka parametrów: epsilon, delta, maxPartitionsContributed itp. Możemy je zgrubnie podzielić na 2 kategorie: parametry prywatności i parametry użyteczności.
Parametry prywatności
Epsilon i delta to parametry, które określają liczbowo poziom prywatności zapewniany przez prywatność różnicową. Dokładniej mówiąc, epsilon i delta to miara tego, ile informacji o danych źródłowych może uzyskać potencjalny atakujący, analizując anonimizowane dane wyjściowe. Im większe są wartości epsilon i delta, tym więcej informacji o danych bazowych uzyskuje osoba atakująca, co stanowi zagrożenie dla prywatności.
Z drugiej strony im mniejsze są wartości epsilon i delta, tym więcej szumu musisz dodać do danych wyjściowych, aby zachować anonimowość, i tym więcej unikalnych użytkowników musisz mieć w każdej partycji, aby zachować ją w anonimizowanych danych wyjściowych. Pojawia się tu więc kompromis między użytecznością a prywatnością.
W przypadku prywatności w Beam musisz zadbać o gwarancje prywatności, które chcesz uzyskać w anonimizowanych danych wyjściowych, gdy określasz łączny budżet na potrzeby prywatności w PrivacySpec. Warunkiem jest to, że jeśli chcesz zachować gwarancje prywatności, musisz postępować zgodnie z wskazówkami podanymi w tym ćwiczeniu dotyczącymi nieprzekraczania budżetu przez używanie osobnego PrivacySpec dla każdej agregacji lub wielokrotne uruchamianie potoku.
Więcej informacji o prywatności różnicowej i znaczeniu parametrów prywatności znajdziesz w literaturze.
Parametry narzędzia
Są to parametry, które nie mają wpływu na gwarancje prywatności (o ile przestrzegane są wskazówki dotyczące korzystania z Privacy on Beam), ale wpływają na dokładność, a co za tym idzie – na przydatność danych wyjściowych. Są one podawane w strukturach Params każdej agregacji, np. CountParams, SumParams itp. Te parametry służą do skalowania dodawanego szumu.
Parametrem narzędziowym dostępnym w Params i mającym zastosowanie do wszystkich agregacji jest MaxPartitionsContributed. Partycja odpowiada kluczowi PCollection wygenerowanemu przez operację agregacji Privacy On Beam, np. Count, SumPerKey itp. Wartość MaxPartitionsContributed określa, ile różnych wartości klucza użytkownik może przekazać w danych wyjściowych. Jeśli użytkownik ma wpływ na więcej niż MaxPartitionsContributed kluczy w danych źródłowych, niektóre z jego działań zostaną pominięte, tak aby miał wpływ na dokładnie MaxPartitionsContributed kluczy.
Podobnie jak w przypadku funkcji MaxPartitionsContributed, większość agregacji ma parametr MaxContributionsPerPartition. Są one podawane w strukturach Params, a każda agregacja może mieć dla nich osobne wartości. W przeciwieństwie do MaxPartitionsContributed, MaxContributionsPerPartition ogranicza dane pochodzące od użytkownika w przypadku każdego klucza. Innymi słowy, każdy użytkownik może podać tylko MaxContributionsPerPartition wartości dla każdego klucza.
Szum dodawany do danych wyjściowych jest skalowany przez MaxPartitionsContributed i MaxContributionsPerPartition, więc istnieje tu kompromis: większe wartości MaxPartitionsContributed i MaxContributionsPerPartition oznaczają, że zachowasz więcej danych, ale uzyskasz bardziej zaszumiony wynik.
Niektóre agregacje wymagają MinValue i MaxValue. Określają one zakres danych pochodzących od każdego użytkownika. Jeśli użytkownik poda wartość niższą niż MinValue, zostanie ona ograniczona do MinValue. Podobnie, jeśli użytkownik poda wartość większą niż MaxValue, zostanie ona ograniczona do MaxValue. Oznacza to, że aby zachować więcej pierwotnych wartości, musisz określić większe zakresy. Podobnie jak w przypadku MaxPartitionsContributed i MaxContributionsPerPartition, szum jest skalowany według rozmiaru zakresów, więc większe zakresy oznaczają, że zachowasz więcej danych, ale uzyskasz bardziej zaszumiony wynik.
Ostatnim parametrem, o którym porozmawiamy, jest NoiseKind. W Privacy On Beam obsługujemy 2 różne mechanizmy szumu: GaussianNoise i LaplaceNoise. Oba te rozkłady mają swoje zalety i wady, ale rozkład Laplace’a zapewnia lepszą użyteczność przy niskich ograniczeniach wkładu, dlatego Privacy On Beam używa go domyślnie. Jeśli jednak chcesz użyć szumu o rozkładzie normalnym, możesz podać Params ze zmienną pbeam.GaussianNoise{}.
10. Podsumowanie
Brawo! Ukończono ćwiczenia z zakresu prywatności w Beam. Dowiedziałeś się wiele o prywatności różnicowej i prywatności w Beam:
- Zamienianie
PCollectionwPrivatePCollectionprzez zadzwonienie pod numerMakePrivateFromStruct. - Używanie
Countdo obliczania liczności z zachowaniem prywatności różnicowej. - Używanie
MeanPerKeydo obliczania średnich z zachowaniem prywatności różnicowej. - Używanie
SumPerKeydo obliczania sum z zachowaniem prywatności różnicowej. - Obliczanie wielu statystyk za pomocą pojedynczej funkcji
PrivacySpecw jednym potoku. - (Opcjonalnie) Dostosowywanie parametrów
PrivacySpeci agregacji (CountParams, MeanParams, SumParams).
W Beam możesz jednak wykonywać wiele innych agregacji (np.kwantyle, zliczanie unikalnych wartości). Więcej informacji o nich znajdziesz w repozytorium GitHub lub na stronie godoc.
Jeśli masz czas, podziel się z nami opinią o tym laboratorium, wypełniając ankietę.