Calcul de statistiques privées avec Privacy on Beam

Vous pensez peut-être que les statistiques agrégées ne divulguent aucune information sur les personnes concernées. Cependant, les pirates informatiques connaissent différents moyens pour obtenir des informations sensibles sur les personnes présentes dans un ensemble de données à partir d'une statistique agrégée.

Pour protéger la vie privée des individus, découvrez comment générer des statistiques privées à l'aide d'agrégations différentiellement privées via Privacy on Beam. Privacy on Beam est un framework de confidentialité différentielle fonctionnant avec Apache Beam.

Qu'entendons-nous par "privé" ?

Dans cet atelier de programmation, lorsque nous utilisons le mot "privé", cela signifie que la sortie est générée de manière à ne pas divulguer d'informations privées sur les personnes concernées par les données. Pour cela, nous appliquons le principe de confidentialité différentielle, qui fait partie des notions essentielles de l'anonymisation. L'anonymisation est un processus consistant à agréger les données de plusieurs utilisateurs afin de protéger leur vie privée. Si toutes les méthodes d'anonymisation utilisent l'agrégation, toutes les méthodes d'agrégation n'aboutissent pas à une anonymisation. La confidentialité différentielle, quant à elle, offre des garanties mesurables concernant les fuites d'informations et la confidentialité.

Pour mieux comprendre en quoi consiste la confidentialité différentielle, prenons un exemple simple.

Ce graphique à barres montre la fréquentation d'un petit restaurant un soir particulier. De nombreux clients arrivent à 19h, et le restaurant est complètement vide à 1h du matin :

a43dbf3e2c6de596.png

Utile, n'est-ce pas ?

Il y a cependant un petit souci. Lorsqu'un nouveau client arrive, cela se voit immédiatement sur le graphique à barres. Regardez le graphique. Il montre clairement qu'un nouveau client est arrivé vers 1h du matin :

bda96729e700a9dd.png

Ce graphique n'est pas idéal en termes de confidentialité. Une statistique réellement anonymisée ne doit pas montrer de contributions individuelles. En plaçant ces deux graphiques côte à côte, les choses sont encore plus évidentes. Le graphique à barres orange montre qu'un client supplémentaire est arrivé vers 1h :

d562ddf799288894.png

Là encore, cette représentation n'est pas idéale. Que pouvons-nous faire ?

Nous allons réduire légèrement la précision des graphiques à barres en ajoutant un bruit aléatoire.

Regardez les deux graphiques à barres ci-dessous. Même s'ils ne sont pas totalement exacts, ils restent utiles et ils ne dévoilent aucune contribution individuelle. Parfait !

838a0293cd4fcfe3.gif

La confidentialité différentielle consiste à ajouter la bonne quantité de bruit aléatoire afin de masquer les contributions individuelles.

Nous avons simplifié à l'extrême notre analyse. La mise en place d'une confidentialité différentielle nécessite un peu plus de travail et présente plusieurs subtilités. Comme pour la cryptographie, il n'est pas forcément judicieux de chercher à développer sa propre solution de confidentialité différentielle. À la place, vous pouvez utiliser Privacy on Beam. Ne déployez pas votre propre solution de confidentialité différentielle !

Dans cet atelier de programmation, vous allez découvrir comment effectuer une analyse différentiellement privée à l'aide de Privacy on Beam.

Vous n'avez pas besoin de télécharger Privacy on Beam pour pouvoir suivre le codelab car tout le code et les graphiques pertinents se trouvent dans ce document. Cependant, si vous souhaitez télécharger pour jouer avec le code, l'exécuter vous-même ou utiliser Privacy on Beam plus tard, n'hésitez pas à le faire en suivant les étapes ci-dessous.

Notez que ce codelab est pour la version 1.1.0 de la bibliothèque.

Tout d'abord, téléchargez Privacy on Beam :

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

Vous pouvez également cloner le dépôt GitHub :

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

Privacy on Beam se trouve dans le répertoire de premier niveau privacy-on-beam/.

Le code de cet atelier de programmation et l'ensemble de données se trouvent dans le répertoire privacy-on-beam/codelab/.

Bazel doit également être installé sur votre ordinateur. Pour accéder aux instructions d'installation propres à votre système d'exploitation, consultez le site Web de Bazel.

Imaginons que vous êtes propriétaire d'un restaurant et que vous souhaitez partager certaines statistiques telles que les horaires des visites. Fort heureusement, vous connaissez les principes de confidentialité différentielle et d'anonymisation, et vous souhaitez donc éviter de divulguer des informations sur des visiteurs spécifiques.

Le code de cet exemple est disponible dans codelab/count.go.

Commençons par charger un ensemble de données fictif contenant les visites de votre restaurant un lundi particulier. Le code concerné n'a pas spécialement d'intérêt dans le cadre de cet atelier de programmation, mais vous pouvez le consulter dans codelab/main.go, codelab/utils.go et codelab/visit.go.

ID du visiteur

Heure d'arrivée

Temps passé sur place (min)

Somme dépensée (euros)

1

9:30:00

26

24

2

11:54:00

53

17

3

13:05:00

81

33

Dans l'exemple de code ci-dessous, vous allez d'abord créer un graphique à barres non privé des heures des visites de votre restaurant à l'aide de Beam. Scope est une représentation du pipeline, et chaque nouvelle opération effectuée sur les données est ajoutée à Scope. CountVisitsPerHour utilise un élément Scope et une collection de visites, représentées par PCollection dans Beam. L'heure de chaque visite est extraite en appliquant la fonction extractVisitHour à la collection. Les occurrences pour chaque heure sont ensuite comptabilisées, puis affichées.

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

Vous obtenez alors un graphique à barres (en exécutant bazel run codelab -- --example="count" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/count.csv --output_chart_file=$(pwd)/count.png) sous la forme d'un fichier count.png stocké dans le répertoire actuel :

a179766795d4e64a.png

L'étape suivante consiste à convertir votre pipeline et votre graphique à barres en un graphique privé. Voici la marche à suivre.

Commencez par appeler MakePrivateFromStruct sur une collection PCollection<V> pour obtenir une collection PrivatePCollection<V>. La collection PCollection d'entrée doit être une collection de structures. Nous devons fournir à MakePrivateFromStruct une valeur PrivacySpec et une valeur idFieldPath.

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

PrivacySpec est une structure contenant les paramètres de confidentialité différentielle (epsilon et delta) que nous souhaitons utiliser pour anonymiser les données. (Vous n'avez pas à vous en soucier pour l'instant. Si vous souhaitez en savoir plus à ce sujet, vous pourrez par la suite consulter la section facultative.)

idFieldPath correspond au chemin d'accès du champ d'identifiant utilisateur au sein de la structure (Visit dans notre cas). Ici, l'identifiant utilisateur des visiteurs est le champ VisitorID de Visit.

Nous appelons ensuite pbeam.Count() au lieu de stats.Count(), et pbeam.Count() utilise comme entrée une structure CountParams contenant les paramètres tels que MaxValue, qui ont une incidence sur la précision du résultat.

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

De même, MaxPartitionsContributed limite le nombre de visites par heure liées à un utilisateur donné. Nous supposons que les visiteurs n'entrent pas dans le restaurant plus d'une fois par jour (en cas de visites multiples pendant une même journée, nous n'en tenons pas compte), et nous avons donc attribué la valeur 1 à ce paramètre. Nous étudierons ces paramètres plus en détail dans une section facultative.

MaxValue limite le nombre de contributions possibles d'un utilisateur aux valeurs que nous comptabilisons. Dans ce cas précis, les valeurs que nous comptabilisons sont des heures de visite, et nous supposons qu'un utilisateur n'entre qu'une seule fois dans le restaurant (s'il entre plusieurs fois par heure, nous n'en tenons pas compte). Nous avons donc attribué la valeur 1 à ce paramètre.

Au final, voici à quoi ressemble le code :

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
}

Un graphique à barres similaire (count_dp.png) s'affiche pour la statistique différentiellement privée (la commande précédente exécute à la fois les pipelines non privé et privé) :

d6a0ace1acd3c760.png

Félicitations ! Vous avez calculé votre première statistique différentiellement privée.

Le graphique à barres obtenu lorsque vous exécutez le code peut être différent de celui présenté ci-dessous. C'est tout à fait normal. En raison du bruit lié à la confidentialité différentielle, vous obtenez un graphique à barres différent chaque fois que vous exécutez le code, mais vous pouvez constater qu'ils sont tous plus ou moins similaires au graphique à barres d'origine non privé dont nous disposions déjà.

Pour que les garanties de confidentialité soient respectées, il est essentiel de ne pas exécuter le pipeline à plusieurs reprises (pour obtenir un graphique à barres de meilleure qualité, par exemple). Pour mieux comprendre pourquoi vous ne devez pas exécuter à nouveau vos pipelines, consultez la section "Calcul de plusieurs statistiques".

Dans la section précédente, vous avez peut-être remarqué que nous avons supprimé toutes les visites (données) de certaines partitions, c'est-à-dire de certaines plages horaires.

d7fbc5d86d91e54a.png

Cela est dû à la sélection des partitions et à la mise en place d'un seuil. Cette étape est importante pour respecter les garanties de confidentialité différentielle lorsque l'existence de partitions de sortie dépend des données utilisateur elles-mêmes. Lorsque c'est le cas, la simple existence d'une partition dans la sortie peut dévoiler l'existence d'un utilisateur spécifique dans les données (consultez cet article de blog pour comprendre pourquoi il s'agit là d'une violation de la confidentialité). Pour éviter cette situation, Privacy on Beam conserve uniquement les partitions contenant un nombre suffisant d'utilisateurs.

Lorsque la liste des partitions de sortie ne dépend pas d'informations privées sur les utilisateurs et que les informations sont donc publiques, cette étape de sélection des partitions n'est pas requise. C'est le cas pour le restaurant de notre exemple : nous connaissons les heures d'ouverture du restaurant (de 9h à 21h).

Le code de cet exemple est disponible dans codelab/public_partitions.go.

Nous allons simplement créer une collection PCollection d'heures comprises entre 9h et 21h (exclues), puis la saisir dans le champ PublicPartitions de 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
}

Notez que vous pouvez attribuer la valeur 0 à delta si vous utilisez des partitions publiques et un bruit de Laplace (par défaut), comme dans le cas ci-dessus.

En exécutant le pipeline avec des partitions publiques (avec 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), voici ce que nous obtenons (public_partitions_dp.png) :

7c950fbe99fec60a.png

Comme vous pouvez le constater, nous conservons désormais les partitions 9, 10 et 16, que nous avons précédemment supprimées sans partitions publiques.

Non seulement l'utilisation de partitions publiques vous permet de conserver plus de partitions, mais cela ajoute aussi environ la moitié du bruit à chaque partition par rapport à l'utilisation de partitions publiques, puisque vous ne dépensez aucun budget de confidentialité (epsilon et delta) pour la sélection de partitions. C'est pourquoi la différence entre les chiffres bruts et les chiffres privés est légèrement moindre que lors de l'exécution précédente.

Lorsque vous utilisez des partitions publiques, tenez compte de deux points importants :

  1. Soyez prudent lorsque la liste des partitions est déduite des données brutes : si vous n'appliquez pas de méthode différentiellement privée (simple lecture de la liste des partitions dans les données utilisateur, par exemple), votre pipeline ne respecte plus les garanties de confidentialité différentielle. Consultez la section "Avancé" ci-dessous pour savoir comment procéder en utilisant une méthode différentiellement privée.
  2. En l'absence de données (visites, par exemple) pour certaines des partitions publiques, le bruit est appliqué à ces partitions afin de préserver la confidentialité différentielle. Par exemple, si nous utilisions des plages horaires allant de 0 à 24 (plutôt que de 9 à 21), toutes les heures comporteraient du bruit et pourraient présenter des visites alors qu'il n'y en a aucune.

(Avancé) Déduction de partitions à partir des données

Si vous exécutez plusieurs agrégations avec la même liste de partitions de sortie non publiques dans le même pipeline, vous pouvez déduire la liste des partitions une fois en utilisant SelectPartitions() et en fournissant les partitions à chaque agrégation comme entrée PublicPartition. C'est non seulement parfaitement sûr du point de vue de la confidentialité, mais cela vous permet également d'ajouter moins de bruit, car vous ne consommez votre budget de confidentialité qu'une seule fois pour l'ensemble du pipeline lors de la sélection des partitions.

Maintenant que nous savons comment effectuer un comptage différentiellement privé, examinons comment calculer les moyennes. Plus précisément, nous allons maintenant calculer la durée moyenne sur place des visiteurs.

Le code de cet exemple est disponible dans codelab/mean.go.

Normalement, pour calculer une moyenne non privée de la durée passée sur place, nous utilisons stats.MeanPerKey() avec une étape de prétraitement convertissant la collection entrante PCollection des visites en collection PCollection<K,V>, où "K" correspond à l'heure d'arrivée et "V" correspond au temps passé dans le restaurant.

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
}

Nous obtenons alors un graphique à barres (en exécutant bazel run codelab -- --example="mean" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/mean.csv --output_chart_file=$(pwd)/mean.png) sous la forme d'un fichier mean.png stocké dans le répertoire actuel :

bc2df28bf94b3721.png

Pour que cette propriété soit différentiellement privée, nous convertissons de nouveau PCollection en PrivatePCollection, en remplaçant stats.MeanPerKey() par pbeam.MeanPerKey(). Comme pour Count, MeanParams contient certains paramètres tels que MinValue et MaxValue, qui influent sur la précision. MinValue et MaxValue représentent les limites de la contribution de chaque utilisateur pour chaque clé.

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

Dans ce cas, chaque clé représente une heure, tandis que les valeurs correspondent au temps passé sur place par les visiteurs. Nous avons appliqué la valeur 0 à MinValue, car en principe, les visiteurs ne passent pas moins de 0 minute dans le restaurant. Nous avons appliqué la valeur 60 à MaxValue, ce qui signifie que si un visiteur passe plus de 60 minutes sur place, nous faisons comme s'il était resté exactement 60 minutes.

Au final, voici à quoi ressemble le code :

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
}

Un graphique à barres similaire (mean_dp.png) s'affiche pour la statistique différentiellement privée (la commande précédente exécute à la fois les pipelines non privé et privé) :

e8ac6a9bf9792287.png

Là encore, comme pour le nombre de visiteurs, nous obtenons des résultats différents à chaque exécution, puisqu'il s'agit d'une opération différentiellement privée. Vous pouvez cependant constater que la durée sur place différentiellement privée n'est pas très éloignée des résultats réels.

Il peut également être intéressant de regarder l'évolution du chiffre d'affaires horaire au cours de la journée.

Le code de cet exemple est disponible dans codelab/sum.go.

Encore une fois, nous allons commencer par la version non privée. Moyennant un prétraitement de notre ensemble de données fictif, nous pouvons créer une collection PCollection<K,V>, où "K" correspond à l'heure d'arrivée et "V" à la somme dépensée par le visiteur dans le restaurant. Pour calculer un chiffre d'affaires horaire non privé, nous pouvons tout simplement additionner les sommes dépensées par tous les visiteurs en appelant 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
}

Nous obtenons alors un graphique à barres (en exécutant bazel run codelab -- --example="sum" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/sum.csv --output_chart_file=$(pwd)/sum.png) sous la forme d'un fichier sum.png stocké dans le répertoire actuel :

548619173fad0c9a.png

Pour que cette propriété soit différentiellement privée, nous convertissons de nouveau PCollection en PrivatePCollection, en remplaçant stats.SumPerKey() par pbeam.SumPerKey(). Comme pour Count et MeanPerKey, SumParams contient certains paramètres tels que MinValue et MaxValue, qui influent sur la précision.

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

Dans ce cas, MinValue et MaxValue représentent les limites que nous appliquons à la somme dépensée par chaque visiteur. Nous avons appliqué la valeur 0 à MinValue, car en principe, les visiteurs ne dépensent pas moins de 0 euro dans le restaurant. Nous avons appliqué la valeur 40 à MaxValue, ce qui signifie que si un visiteur dépense plus de 40 euros, nous faisons comme s'il avait dépensé exactement 40 euros.

Au final, voici à quoi ressemble le code :

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
}

Un graphique à barres similaire (sum_dp.png) s'affiche pour la statistique différentiellement privée (la commande précédente exécute à la fois les pipelines non privé et privé) :

46c375e874f3e7c4.png

Là encore, comme pour le nombre de visiteurs et la moyenne, nous obtenons des résultats différents à chaque exécution, puisqu'il s'agit d'une opération différentiellement privée. Vous pouvez cependant constater que le résultat différentiellement privé est très proche du chiffre d'affaires horaire réel.

La plupart du temps, vous souhaiterez peut-être calculer plusieurs statistiques à partir des mêmes données sous-jacentes, comme vous l'avez fait pour le nombre de visiteurs, la moyenne et la somme. Il est généralement plus efficace et plus facile de le faire dans un seul pipeline Beam et dans un seul binaire. Vous pouvez également le faire avec Privacy on Beam. Vous pouvez écrire un pipeline unique pour exécuter vos transformations et calculs, et utiliser une seule structure PrivacySpec pour l'ensemble du pipeline.

Il est non seulement plus pratique de le faire avec une seule structure PrivacySpec, mais c'est également préférable en termes de confidentialité. Souvenez-vous des paramètres epsilon et delta que nous fournissons à la structure PrivacySpec : ils représentent un budget de confidentialité, qui correspond à la proportion de confidentialité de chaque utilisateur que vous divulguez dans les données sous-jacentes.

N'oubliez pas que ce budget de confidentialité s'additionne : si vous exécutez un pipeline avec des valeurs epsilon ε et delta δ spécifiques une fois, vous dépensez un budget de (ε,δ). Si vous l'exécutez une deuxième fois, le budget total dépensé est de (2ε, 2δ). De même, si vous calculez plusieurs statistiques avec une structure PrivacySpec (et donc avec un budget de confidentialité) de (ε,δ), le budget total dépensé est de (2ε, 2δ). Vous dégradez donc les garanties de confidentialité.

Pour contourner ce problème, lorsque vous souhaitez calculer plusieurs statistiques avec les mêmes données sous-jacentes, vous êtes censé n'utiliser qu'une seule structure PrivacySpec avec le budget total que vous souhaitez utiliser. Vous devez ensuite spécifier les valeurs epsilon et delta à utiliser pour chaque agrégation. Au final, vous vous retrouverez avec la même garantie de confidentialité globale, mais plus les valeurs epsilon et delta d'une agrégation donnée sont élevées, plus le résultat sera précis.

Pour observer ce principe, nous pouvons calculer les trois statistiques (nombre, moyenne et somme) que nous avons calculées séparément dans un même pipeline.

Le code de cet exemple est disponible dans codelab/multiple.go. Notez que nous répartissons le budget total (ε,δ) de manière égale entre les trois agrégations :

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{
        // 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{
        // 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{
        // 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
}

Différents paramètres ont été mentionnés dans cet atelier de programmation : epsilon, delta, maxPartitionsContributed, etc. Ils se répartissent en deux grandes catégories : les paramètres de confidentialité et les paramètres utilitaires.

Paramètres de confidentialité

Les paramètres epsilon et delta sont des paramètres permettent de quantifier le niveau de confidentialité que nous fournissons grâce à la confidentialité différentielle. Plus précisément, epsilon et delta mesurent la quantité d'informations qu'un pirate informatique potentiel pourrait obtenir concernant les données sous-jacentes en consultant la sortie anonymisée. Plus les valeurs epsilon et delta sont élevées, plus le pirate informatique peut obtenir d'informations sur les données sous-jacentes, avec à la clé un risque pour la confidentialité des données.

À l'opposé, plus les valeurs epsilon et delta sont faibles, plus vous devez ajouter de bruit à la sortie pour l'anonymiser, et plus le nombre d'utilisateurs uniques dans chaque partition doit être élevé pour que la partition puisse être conservée dans le résultat anonymisé. Vous devez donc faire un compromis entre utilité et confidentialité.

Dans Privacy on Beam, vous devez réfléchir aux garanties de confidentialité que vous souhaitez appliquer à votre résultat anonymisé quand vous spécifiez le budget total de confidentialité dans la structure PrivacySpec. Si vous souhaitez que vos garanties de confidentialité soient respectées, vous devez suivre les conseils de cet atelier de programmation : veillez à ne pas surconsommer votre budget en définissant une structure PrivacySpec distincte pour chaque agrégation ou en exécutant le pipeline à plusieurs reprises.

Pour en savoir plus sur la confidentialité différentielle et sur la signification des paramètres de confidentialité, nous vous invitons à consulter la documentation.

Paramètres utilitaires

Ces paramètres n'ont aucune incidence sur les garanties de confidentialité (tant que vous suivez nos conseils d'utilisation de Privacy on Beam), mais ils influent sur la précision et donc sur l'utilité du résultat. Ils sont fournis dans les structures Params de chaque agrégation, par exemple CountParams, SumParams, etc. Ces paramètres permettent d'ajuster le bruit ajouté.

MaxPartitionsContributed fait partie des paramètres utilitaires fournis dans Params et applicables à toutes les agrégations. Une partition correspond à une clé de la collection PCollection générée par une opération d'agrégation Privacy on Beam telle que Count, SumPerKey, etc. Par conséquent, MaxPartitionsContributed limite le nombre de valeurs clés distinctes auxquelles un utilisateur peut contribuer dans le résultat. Si un utilisateur contribue à plus de MaxPartitionsContributed clés dans les données sous-jacentes, certaines de ses contributions seront supprimées afin qu'il contribue exactement à MaxPartitionsContributed clés.

Comme pour MaxPartitionsContributed, la plupart des agrégations comportent un paramètre MaxContributionsPerPartition. Elles sont fournies dans les structures Params, et chaque agrégation peut avoir des valeurs distinctes. Au contraire de MaxPartitionsContributed, MaxContributionsPerPartition limite la contribution d'un utilisateur pour chaque clé. En d'autres termes, un utilisateur ne peut contribuer qu'à MaxContributionsPerPartition valeurs pour chaque clé.

Le bruit ajouté à la sortie est ajusté par MaxPartitionsContributed et MaxContributionsPerPartition, moyennant un compromis : plus les valeurs MaxPartitionsContributed et MaxContributionsPerPartition sont élevées, plus vous conservez de données, mais vous vous retrouverez alors avec plus de bruit dans les résultats.

Certaines agrégations nécessitent MinValue et MaxValue. Ces valeurs définissent les limites appliquées aux contributions de chaque utilisateur. Si un utilisateur contribue à une valeur inférieure à MinValue, cette valeur sera plafonnée à MinValue. De même, si un utilisateur contribue à une valeur supérieure à MaxValue, cette valeur sera réduite à MaxValue. Par conséquent, pour conserver davantage de valeurs d'origine, vous devez élargir les limites. Comme pour MaxPartitionsContributed et MaxContributionsPerPartition, le bruit est ajusté en fonction de la taille des limites : plus les limites sont larges, plus vous conservez de données, mais vous vous retrouverez alors avec plus de bruit dans les résultats.

Pour terminer, examinons le paramètre NoiseKind. Nous acceptons deux mécanismes de bruit différents dans Privacy on Beam : GaussianNoise et LaplaceNoise. Tous deux ont leurs avantages et leurs inconvénients, mais la distribution Laplace est plus efficace avec les limites de contribution faibles, c'est pourquoi Privacy on Beam l'utilise par défaut. Toutefois, si vous souhaitez utiliser un bruit à distribution gaussienne, vous pouvez fournir une variable pbeam.GaussianNoise{} à Params.

Bravo ! Vous avez terminé l'atelier de programmation consacré à Privacy on Beam. Vous en savez désormais beaucoup sur la confidentialité différentielle et sur Privacy on Beam :

  • Conversion d'une collection PCollection en collection PrivatePCollection avec MakePrivateFromStruct
  • Utilisation de Count pour calculer des nombres différentiellement privés
  • Utilisation de MeanPerKey pour calculer des moyennes différentiellement privées
  • Utilisation de SumPerKey pour calculer des sommes différentiellement privées
  • Calcul de plusieurs statistiques avec une seule structure PrivacySpec dans un même pipeline
  • (Facultatif) Personnalisation de la structure PrivacySpec et des paramètres d'agrégation (CountParams, MeanParams, SumParams)

Privacy on Beam vous permet cependant d'effectuer bien d'autres agrégations (par exemple, quantiles, comptage de valeurs distinctes) ! Pour en savoir plus à ce sujet, accédez au dépôt GitHub ou à la page godoc.

Si vous avez le temps, n'hésitez pas à nous faire part de vos commentaires sur l'atelier de programmation en répondant à notre enquête.