Calcolo di statistiche private con privacy su Beam

1. Introduzione

Potresti pensare che le statistiche aggregate non rivelino informazioni sulle persone di cui sono composti i dati. Tuttavia, esistono molti modi in cui un malintenzionato può apprendere informazioni sensibili sugli individui in un set di dati da una statistica aggregata.

Per proteggere la privacy delle persone, imparerai a produrre statistiche private utilizzando aggregazioni con privacy differenziale da Privacy on Beam. Privacy on Beam è un framework di privacy differenziale che funziona con Apache Beam.

Che cosa intendiamo con "privato"?

Quando utilizziamo la parola "privato" in questo Codelab, intendiamo che l'output viene prodotto in modo da non divulgare informazioni private sugli individui nei dati. Possiamo farlo utilizzando la privacy differenziale, un concetto di anonimizzazione che garantisce una privacy efficace. L'anonimizzazione è il processo di aggregazione dei dati di più utenti per proteggere la loro privacy. Tutti i metodi di anonimizzazione utilizzano l'aggregazione, ma non tutti i metodi di aggregazione consentono di ottenere l'anonimizzazione. La privacy differenziale, invece, fornisce garanzie misurabili in merito alla divulgazione di informazioni e alla privacy.

2. Panoramica della privacy differenziale

Per comprendere meglio la privacy differenziale, esaminiamo un semplice esempio.

Questo grafico a barre mostra l'affluenza in un piccolo ristorante in una determinata serata. Molti ospiti arrivano alle 19:00 e il ristorante è completamente vuoto all'1:00:

a43dbf3e2c6de596.png

Sembra utile.

C'è un trucco. Quando arriva un nuovo ospite, questo fatto viene immediatamente rivelato dal grafico a barre. Nel grafico è evidente che è arrivato un nuovo ospite, intorno all'1 di notte:

bda96729e700a9dd.png

Dal punto di vista della privacy, non è il massimo. Le statistiche anonimizzate non devono rivelare i contributi individuali. Se metti i due grafici uno accanto all'altro, la differenza è ancora più evidente: il grafico a barre arancione ha un ospite in più arrivato verso l'1:00:

d562ddf799288894.png

Anche in questo caso, non è il massimo. Che cosa facciamo?

Renderemo i grafici a barre un po' meno precisi aggiungendo rumore casuale.

Osserva i due grafici a barre riportati di seguito. Anche se non sono completamente accurate, sono comunque utili e non rivelano i contributi individuali. Bene!

838a0293cd4fcfe3.gif

La privacy differenziale aggiunge la giusta quantità di rumore casuale per mascherare i contributi individuali.

La nostra analisi è stata un po' troppo semplificata. L'implementazione corretta della privacy differenziale è più complessa e presenta una serie di sottigliezze di implementazione piuttosto inaspettate. Analogamente alla crittografia, creare una propria implementazione della privacy differenziale potrebbe non essere una buona idea. Puoi utilizzare Privacy on Beam anziché implementare una soluzione personalizzata. Non implementare la privacy differenziale autonomamente.

In questo codelab, ti mostreremo come eseguire l'analisi con privacy differenziale utilizzando Privacy on Beam.

3. Download di Privacy su Beam

Non è necessario scaricare Privacy on Beam per seguire il codelab, perché tutto il codice pertinente e i grafici sono disponibili in questo documento. Tuttavia, se vuoi scaricarlo per giocare con il codice, eseguirlo autonomamente o utilizzare Privacy on Beam in un secondo momento, puoi farlo seguendo i passaggi riportati di seguito.

Tieni presente che questo codelab è per la versione 1.1.0 della libreria.

Innanzitutto, scarica Privacy on Beam:

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

In alternativa, puoi clonare il repository GitHub:

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

La privacy su Beam si trova nella directory di primo livello privacy-on-beam/.

Il codice per questo codelab e il set di dati si trova nella directory privacy-on-beam/codelab/.

Sul computer deve essere installato anche Bazel. Trova le istruzioni di installazione per il tuo sistema operativo sul sito web di Bazel.

4. Calcolo delle visite all'ora

Immagina di essere il proprietario di un ristorante e di voler condividere alcune statistiche sul tuo ristorante, ad esempio gli orari di visita più popolari. Per fortuna, conosci la privacy differenziale e l'anonimizzazione, quindi vuoi farlo in modo da non divulgare informazioni su nessun singolo visitatore.

Il codice per questo esempio si trova in codelab/count.go.

Iniziamo caricando un set di dati simulato contenente le visite al tuo ristorante in un determinato lunedì. Il codice non è interessante ai fini di questo codelab, ma puoi consultarlo in codelab/main.go, codelab/utils.go e codelab/visit.go.

ID visitatore

Ora di inserimento

Tempo di utilizzo (minuti)

Denaro speso (euro)

1

09:30:00

26

24

2

11:54:00

53

17

3

13:05:00

81

33

Per prima cosa, produci un grafico a barre non privato degli orari di visita al tuo ristorante utilizzando Beam nell'esempio di codice riportato di seguito. Scope è una rappresentazione della pipeline e ogni nuova operazione che eseguiamo sui dati viene aggiunta a Scope. CountVisitsPerHour prende un Scope e una raccolta di visite, che è rappresentata come un PCollection in Beam. Estrae l'ora di ogni visita applicando la funzione extractVisitHour alla raccolta. Poi conta le occorrenze di ogni ora e le restituisce.

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()
}

In questo modo viene generato un grafico a barre (eseguendo bazel run codelab -- --example="count" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/count.csv --output_chart_file=$(pwd)/count.png) nella directory corrente come count.png:

a179766795d4e64a.png

Il passaggio successivo consiste nel convertire la pipeline e il grafico a barre in una pipeline e un grafico a barre privati. Ecco come procediamo.

Innanzitutto, chiama il numero MakePrivateFromStruct su un PCollection<V> per ottenere un PrivatePCollection<V>. L'input PCollection deve essere una raccolta di struct. Dobbiamo inserire un PrivacySpec e un idFieldPath come input per MakePrivateFromStruct.

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

PrivacySpec è una struct che contiene i parametri di privacy differenziale (epsilon e delta) che vogliamo utilizzare per anonimizzare i dati. Per ora non devi preoccuparti di questi elementi. Se vuoi saperne di più, abbiamo una sezione facoltativa più avanti.

idFieldPath è il percorso del campo dell'identificatore utente all'interno dello struct (Visit nel nostro caso). In questo caso, l'identificatore utente dei visitatori è il campo VisitorID di Visit.

Poi chiamiamo pbeam.Count() anziché stats.Count(). pbeam.Count() accetta come input una struct CountParams che contiene parametri come MaxValue che influiscono sull'accuratezza dell'output.

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,
})

Allo stesso modo, MaxPartitionsContributed limita il numero di ore di visita diverse che un utente può contribuire. Ci aspettiamo che visitino il ristorante al massimo una volta al giorno (o non ci interessa se lo visitano più volte nel corso della giornata), quindi impostiamo anche questo valore su 1. Parleremo di questi parametri in modo più dettagliato in una sezione facoltativa.

MaxValue limita il numero di volte in cui un singolo utente può contribuire ai valori che stiamo conteggiando. In questo caso specifico, i valori che conteggiamo sono le ore di visita e prevediamo che un utente visiti il ristorante una sola volta (o non ci interessa se lo visita più volte all'ora), quindi impostiamo questo parametro su 1.

Alla fine, il codice avrà questo aspetto:

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
}

Vediamo un grafico a barre simile (count_dp.png) per la statistica con privacy differenziale (il comando precedente esegue sia le pipeline non private che quelle private):

d6a0ace1acd3c760.png

Complimenti! Hai calcolato la tua prima statistica con privacy differenziale.

Il grafico a barre che ottieni quando esegui il codice potrebbe essere diverso da questo. Non importa. A causa del rumore nella privacy differenziale, ogni volta che esegui il codice ottieni un grafico a barre diverso, ma puoi notare che sono più o meno simili al grafico a barre originale non privato.

Tieni presente che è molto importante per le garanzie di privacy non eseguire più volte la pipeline (ad esempio, per ottenere un grafico a barre più bello). Il motivo per cui non devi eseguire nuovamente le pipeline è spiegato nella sezione "Calcolo di più statistiche".

5. Utilizzo delle partizioni pubbliche

Nella sezione precedente, potresti aver notato che abbiamo eliminato tutte le visite (dati) per alcune partizioni, ovvero ore.

d7fbc5d86d91e54a.png

Ciò è dovuto alla selezione/soglia delle partizioni, un passaggio importante per garantire la privacy differenziale quando l'esistenza delle partizioni di output dipende dai dati utente stessi. In questo caso, la semplice esistenza di una partizione nell'output può rivelare l'esistenza di un singolo utente nei dati (consulta questo post del blog per una spiegazione del motivo per cui ciò viola la privacy). Per evitare questo problema, Privacy on Beam conserva solo le partizioni con un numero sufficiente di utenti.

Quando l'elenco delle partizioni di output non dipende dai dati privati degli utenti, ovvero si tratta di informazioni pubbliche, non è necessario questo passaggio di selezione delle partizioni. Questo è effettivamente il caso del nostro esempio di ristorante: conosciamo l'orario di lavoro del ristorante (dalle 9:00 alle 21:00).

Il codice per questo esempio si trova in codelab/public_partitions.go.

Creeremo semplicemente un PCollection di ore tra le 9 e le 21 (escluse) e lo inseriremo nel campo PublicPartitions di 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
}

Tieni presente che è possibile impostare delta su 0 se utilizzi partizioni pubbliche e rumore di Laplace (impostazione predefinita), come nel caso precedente.

Quando eseguiamo la pipeline con partizioni pubbliche (con 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), otteniamo (public_partitions_dp.png):

7c950fbe99fec60a.png

Come puoi vedere, ora manteniamo le partizioni 9, 10 e 16 che avevamo eliminato in precedenza senza partizioni pubbliche.

L'utilizzo di partizioni pubbliche non solo ti consente di conservare più partizioni, ma aggiunge anche circa la metà del rumore a ciascuna partizione rispetto al caso in cui non vengono utilizzate partizioni pubbliche, in quanto non viene speso alcun budget per la privacy, ovvero epsilon e delta, per la selezione delle partizioni. Per questo motivo, la differenza tra i conteggi grezzi e privati è leggermente inferiore rispetto all'esecuzione precedente.

Quando utilizzi le partizioni pubbliche, devi tenere presente due aspetti importanti:

  1. Fai attenzione quando derivi l'elenco delle partizioni dai dati non elaborati: se non lo fai in modo differenzialmente privato, ad esempio leggendo semplicemente l'elenco di tutte le partizioni nei dati utente, la tua pipeline non fornisce più garanzie di privacy differenziale. Per scoprire come farlo in modo differenziale, consulta la sezione avanzata di seguito.
  2. Se non sono presenti dati (ad es. visite) per alcune delle partizioni pubbliche, a queste verrà applicato del rumore per preservare la privacy differenziale. Ad esempio, se utilizzassimo le ore tra 0 e 24 (anziché tra 9 e 21), tutte le ore sarebbero disturbate e potrebbero mostrare alcune visite quando non ce ne sono.

(Avanzato) Derivare le partizioni dai dati

Se esegui più aggregazioni con lo stesso elenco di partizioni di output non pubbliche nella stessa pipeline, puoi derivare l'elenco delle partizioni una sola volta utilizzando SelectPartitions() e fornendo le partizioni a ogni aggregazione come input PublicPartition. Oltre a essere sicuro dal punto di vista della privacy, questo approccio consente anche di aggiungere meno rumore, in quanto il budget per la privacy viene utilizzato per la selezione delle partizioni una sola volta per l'intera pipeline.

6. Calcolo della durata media del soggiorno

Ora che sappiamo come conteggiare gli elementi in modo differenziale, vediamo come calcolare le medie. Più nello specifico, ora calcoleremo la durata media del soggiorno dei visitatori.

Il codice per questo esempio si trova in codelab/mean.go.

Normalmente, per calcolare una media non privata delle durate dei soggiorni, utilizziamo stats.MeanPerKey() con un passaggio di pre-elaborazione che converte l'PCollection delle visite in un PCollection<K,V>, dove K è l'ora della visita e V è il tempo trascorso dal visitatore nel ristorante.

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
}

In questo modo viene generato un grafico a barre (eseguendo bazel run codelab -- --example="mean" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/mean.csv --output_chart_file=$(pwd)/mean.png) nella directory corrente come mean.png:

bc2df28bf94b3721.png

Per renderlo differenzialmente privato, convertiamo di nuovo PCollection in PrivatePCollection e sostituiamo stats.MeanPerKey() con pbeam.MeanPerKey(). Analogamente a Count, abbiamo MeanParams che contengono alcuni parametri come MinValue e MaxValue che influiscono sull'accuratezza. MinValue e MaxValue rappresentano i limiti che abbiamo per il contributo di ciascun utente a ogni chiave.

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,
})

In questo caso, ogni chiave rappresenta un'ora e i valori sono il tempo trascorso dai visitatori. Abbiamo impostato MinValue su 0 perché non ci aspettiamo che i visitatori trascorrano meno di 0 minuti nel ristorante. Abbiamo impostato MaxValue su 60, il che significa che se un visitatore trascorre più di 60 minuti, consideriamo che abbia trascorso 60 minuti.

Alla fine, il codice avrà questo aspetto:

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
}

Vediamo un grafico a barre simile (mean_dp.png) per la statistica con privacy differenziale (il comando precedente esegue sia le pipeline non private che quelle private):

e8ac6a9bf9792287.png

Anche in questo caso, come per il conteggio, poiché si tratta di un'operazione di privacy differenziale, otterremo risultati diversi ogni volta che la eseguiamo. Tuttavia, puoi notare che le durate del soggiorno con privacy differenziale non si discostano molto dal risultato effettivo.

7. Calcolo delle entrate all'ora

Un'altra statistica interessante che potremmo esaminare è il fatturato orario nel corso della giornata.

Il codice per questo esempio si trova in codelab/sum.go.

Anche in questo caso, inizieremo con la versione non privata. Con un po' di pre-elaborazione sul nostro set di dati simulato, possiamo creare un PCollection<K,V> in cui K è l'ora della visita e V è il denaro speso dal visitatore nel ristorante: per calcolare un fatturato non privato per ora, possiamo semplicemente sommare tutto il denaro speso dai visitatori chiamando 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
}

In questo modo viene generato un grafico a barre (eseguendo bazel run codelab -- --example="sum" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/sum.csv --output_chart_file=$(pwd)/sum.png) nella directory corrente come sum.png:

548619173fad0c9a.png

Per renderlo differenzialmente privato, convertiamo di nuovo PCollection in PrivatePCollection e sostituiamo stats.SumPerKey() con pbeam.SumPerKey(). Analogamente a Count e MeanPerKey, abbiamo SumParams che contengono alcuni parametri come MinValue e MaxValue che influiscono sull'accuratezza.

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,
})

In questo caso, MinValue e MaxValue rappresentano i limiti che abbiamo per la spesa di ciascun visitatore. Abbiamo impostato MinValue su 0 perché non ci aspettiamo che i visitatori spendano meno di 0 euro nel ristorante. Abbiamo impostato MaxValue su 40, il che significa che se un visitatore spende più di 40 euro, ci comportiamo come se avesse speso 40 euro.

Alla fine, il codice avrà questo aspetto:

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
}

Vediamo un grafico a barre simile (sum_dp.png) per la statistica con privacy differenziale (il comando precedente esegue sia le pipeline non private che quelle private):

46c375e874f3e7c4.png

Come per il conteggio e la media, poiché si tratta di un'operazione di privacy differenziale, otterremo risultati diversi ogni volta che la eseguiamo. Tuttavia, puoi notare che il risultato con privacy differenziale è molto vicino alle entrate effettive per ora.

8. Calcolo di più statistiche

La maggior parte delle volte, potresti essere interessato a calcolare più statistiche sugli stessi dati sottostanti, in modo simile a quanto hai fatto con conteggio, media e somma. In genere è più semplice e pulito eseguire questa operazione in una singola pipeline Beam e in un singolo binario. Puoi farlo anche con Privacy su Beam. Puoi scrivere una singola pipeline per eseguire le trasformazioni e i calcoli e utilizzare un unico PrivacySpec per l'intera pipeline.

Non solo è più comodo farlo con un unico PrivacySpec, ma è anche meglio in termini di privacy. Se ricordi i parametri epsilon e delta che forniamo a PrivacySpec, rappresentano un budget di privacy, ovvero una misura della quantità di privacy degli utenti nei dati sottostanti che stai divulgando.

Un aspetto importante da ricordare del budget di privacy è che è additivo: se esegui una pipeline con un determinato epsilon ε e delta δ una sola volta, spendi un budget (ε,δ). Se lo esegui una seconda volta, avrai speso un budget totale di (2ε, 2δ). Analogamente, se calcoli più statistiche con un PrivacySpec (e di conseguenza un budget di privacy) di (ε,δ), avrai speso un budget totale di (2ε, 2δ). Ciò significa che stai riducendo le garanzie di privacy.

Per aggirare questo problema, quando vuoi calcolare più statistiche sugli stessi dati sottostanti, devi utilizzare un unico PrivacySpec con il budget totale che vuoi utilizzare. Dovrai quindi specificare l'epsilon e il delta che vuoi utilizzare per ogni aggregazione. Alla fine, otterrai la stessa garanzia di privacy complessiva, ma maggiore è l'epsilon e il delta di una determinata aggregazione, maggiore sarà la sua precisione.

Per vedere questa funzionalità in azione, possiamo calcolare le tre statistiche (conteggio, media e somma) che abbiamo calcolato separatamente in precedenza in una singola pipeline.

Il codice per questo esempio si trova in codelab/multiple.go. Nota come dividiamo il budget totale (ε,δ) equamente tra le tre aggregazioni:

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. (Facoltativo) Modificare i parametri di privacy differenziale

In questo codelab sono stati menzionati diversi parametri: epsilon, delta, maxPartitionsContributed e così via. Possiamo suddividerli approssimativamente in due categorie: parametri di privacy e parametri di utilità.

Parametri di privacy

Epsilon e delta sono i parametri che quantificano la privacy che forniamo utilizzando la privacy differenziale. Più precisamente, epsilon e delta sono una misura della quantità di informazioni che un potenziale malintenzionato ottiene sui dati sottostanti esaminando l'output anonimizzato. Maggiore è il valore di epsilon e delta, più informazioni l'aggressore ottiene sui dati sottostanti, il che rappresenta un rischio per la privacy.

D'altra parte, più bassi sono epsilon e delta, più rumore devi aggiungere all'output per renderlo anonimo e maggiore è il numero di utenti unici che devi avere in ogni partizione per mantenerla nell'output anonimizzato. Pertanto, esiste un compromesso tra utilità e privacy.

In Privacy su Beam, devi preoccuparti delle garanzie di privacy che vuoi nell'output anonimizzato quando specifichi il budget di privacy totale in PrivacySpec. L'avvertenza è che, se vuoi che le garanzie di privacy siano valide, devi seguire i consigli di questo codelab per non utilizzare eccessivamente il budget creando un PrivacySpec separato per ogni aggregazione o eseguendo la pipeline più volte.

Per ulteriori informazioni sulla privacy differenziale e sul significato dei parametri della privacy, puoi consultare la documentazione.

Parametri di utilità

Questi parametri non influiscono sulle garanzie della privacy (a condizione che i consigli su come utilizzare Privacy su Beam vengano seguiti correttamente), ma influiscono sull'accuratezza e, di conseguenza, sull'utilità dell'output. Vengono forniti nelle strutture Params di ogni aggregazione, ad esempio CountParams, SumParams e così via. Questi parametri vengono utilizzati per scalare il rumore aggiunto.

Un parametro di utilità fornito in Params e applicabile a tutte le aggregazioni è MaxPartitionsContributed. Una partizione corrisponde a una chiave dell'output PCollection generato da un'operazione di aggregazione Privacy On Beam, ad esempio Count, SumPerKey e così via. Pertanto, MaxPartitionsContributed limita il numero di valori chiave distinti a cui un utente può contribuire nell'output. Se un utente contribuisce a più di MaxPartitionsContributed chiavi nei dati sottostanti, alcuni dei suoi contributi verranno eliminati in modo che contribuisca esattamente a MaxPartitionsContributed chiavi.

Come per MaxPartitionsContributed, la maggior parte delle aggregazioni ha un parametro MaxContributionsPerPartition. Sono forniti nelle strutture Params e ogni aggregazione potrebbe avere valori separati. A differenza di MaxPartitionsContributed, MaxContributionsPerPartition limita il contributo di un utente per ogni chiave. In altre parole, un utente può contribuire solo con MaxContributionsPerPartition valori per ogni chiave.

Il rumore aggiunto all'output viene scalato in base a MaxPartitionsContributed e MaxContributionsPerPartition, quindi esiste un compromesso: valori di MaxPartitionsContributed e MaxContributionsPerPartition più grandi significano che conservi più dati, ma il risultato sarà più rumoroso.

Alcune aggregazioni richiedono MinValue e MaxValue. Questi specificano i limiti per i contributi di ciascun utente. Se un utente contribuisce con un valore inferiore a MinValue, questo valore verrà aumentato fino a MinValue. Allo stesso modo, se un utente inserisce un valore maggiore di MaxValue, questo valore verrà ridotto a MaxValue. Ciò significa che, per conservare più valori originali, devi specificare limiti più ampi. Analogamente a MaxPartitionsContributed e MaxContributionsPerPartition, il rumore viene scalato in base alle dimensioni dei limiti, quindi limiti più grandi significano che conservi più dati, ma il risultato sarà più rumoroso.

L'ultimo parametro di cui parleremo è NoiseKind. Supportiamo due diversi meccanismi di rumore in Privacy On Beam: GaussianNoise e LaplaceNoise. Entrambe hanno vantaggi e svantaggi, ma la distribuzione di Laplace offre un'utilità migliore con limiti di contributo bassi, motivo per cui Privacy On Beam la utilizza per impostazione predefinita. Tuttavia, se vuoi utilizzare un rumore con distribuzione gaussiana, puoi fornire Params con una variabile pbeam.GaussianNoise{}.

10. Riepilogo

Ottimo lavoro. Hai completato il codelab Privacy on Beam. Hai imparato molto sulla privacy differenziale e sulla privacy su Beam:

  • Trasformando il tuo PCollection in un PrivatePCollection chiamando il numero MakePrivateFromStruct.
  • Utilizzo di Count per calcolare i conteggi con privacy differenziale.
  • Utilizzo di MeanPerKey per calcolare le medie con privacy differenziale.
  • Utilizzo di SumPerKey per calcolare somme con privacy differenziale.
  • Calcolo di più statistiche con un unico PrivacySpec in una singola pipeline.
  • (Facoltativo) Personalizzazione dei parametri PrivacySpec e di aggregazione (CountParams, MeanParams, SumParams).

Tuttavia, con Privacy on Beam puoi eseguire molte altre aggregazioni (ad es.quantili, conteggio di valori distinti). Puoi scoprire di più su questi pacchetti nel repository GitHub o nella godoc.

Se hai tempo, inviaci un feedback sul codelab compilando un sondaggio.