Privacy on Beam によるプライベート統計情報の計算

集計した統計情報では、統計情報を構成するデータを所有する個人に関する情報が漏洩することはないとお考えかもしれません。しかし、集計した統計情報から攻撃者がデータセット内の個人に関する機密情報を知る方法は、多数存在します。

個人のプライバシーを保護するために、Privacy on Beam の差分プライベート集計を使用してプライベート統計情報を生成する方法を学びます。Privacy on Beam は、Apache Beam と連携する差分プライバシー フレームワークです。

「プライベート」とは

この Codelab 全体で「プライベート」という単語を使用する場合、データ内の個人に関するいかなる個人情報も漏洩させない方法で出力が生成されることを意味します。これを実現するために、匿名化の強力なプライバシー概念である差分プライバシーを使用します。匿名化は、ユーザーのプライバシーを保護するために、複数のユーザー間でデータを集計するプロセスです。すべての匿名化メソッドで集計が使用されますが、すべての集計メソッドで匿名化が達成されるわけではありません。一方、差分プライバシーは情報漏洩とプライバシーに関してある程度の保証を提供します。

差分プライバシーについて理解を深めるために、簡単な例で考えてみましょう。

この棒グラフは、ある特定の夜の、小さなレストランの混雑状況を示しています。午後 7 時に多くの来店がありますが、午前 1 時にレストランは完全に空になります。

a43dbf3e2c6de596.png

問題なさそうです。

しかし、落とし穴があります。新たな来店があると、この事実が棒グラフですぐに明らかになります。グラフを見ると、新しいゲストが 1 人いて、このゲストは午前 1 時頃に来店していることが明確にわかります。

bda96729e700a9dd.png

プライバシーの観点から、これはよくありません。真に匿名化された統計情報では、個々の寄与が明らかになってはなりません。2 つのグラフを並べると、より明確になります。オレンジ色の棒グラフの方が、ゲストが 1 人多く、午前 1 時頃に来店しています。

d562ddf799288894.png

繰り返しになりますが、これはよくありません。どうすればよいでしょうか。

ランダムノイズを追加して、棒グラフの精度を少し下げます。

下の 2 つの棒グラフをご覧ください。完全に正確というわけではありませんが、それでもまだ有用であり、個々の寄与が明らかになりません。

838a0293cd4fcfe3.gif

差分プライバシーでは、個々の寄与をマスクするために適切な量のランダムノイズを加える

ここまでの分析はやや単純化されたものです。差分プライバシーを適切に実装することは、より複雑であり、まったく予想外な実装上の微妙な点がいくつもあります。暗号と同様に、差分プライバシーの独自の実装を作成することはおすすめできません。独自のソリューションを実装する代わりに、Privacy on Beam を使用できます。独自の差分プライバシーは展開しない方がよいでしょう。

この Codelab では、Privacy on Beam を使用して差分プライベート分析を行う方法について説明します。

まず、Privacy on Beam をダウンロードします。

https://github.com/google/differential-privacy/archive/main.zip

または、GitHub リポジトリのクローンを作成します。

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

Privacy on Beam は、最上位の privacy-on-beam/ ディレクトリにあります。

この Codelab のコードとデータセットは privacy-on-beam/codelab/ ディレクトリにあります。

また、Bazel がパソコンにインストールされている必要があります。お使いのオペレーティング システム向けのインストール手順については、Bazel のウェブサイトをご覧ください。

自分がレストランのオーナーで、来店の多い時刻を公開するなど、レストランに関する統計情報を共有するとします。差分プライバシーと匿名化について把握していれば、個々の来店者に関する情報を漏洩させない方法で行うことができます。

このサンプルのコードは codelab/count.go にあります。

特定の月曜日におけるレストランへの来店を含むモック データセットを読み込むことから始めましょう。このためのコードは、この Codelab にはあまり関係ありませんが、codelab/main.gocodelab/utils.gocodelab/visit.go で確認できます。

来店者 ID

入店時刻

滞在時間(分)

支払い額(ユーロ)

1

9:30:00 AM

26

24

2

11:54:00 AM

53

17

3

1:05:00 PM

81

33

まず、次のコードサンプルで Beam を使用して、レストラン来店時刻の非プライベート棒グラフを作成します。Scope はパイプラインを表します。データに対して新しいオペレーションを行うたびに、Scope に追加されます。CountVisitsPerHour は、Scope と、来店のコレクション(Beam では PCollection として表されます)を受け取ります。コレクションに extractVisitHour 関数を適用することで、各来店の時刻を抽出します。その後、1 時間ごとの発生回数をカウントして返します。

func CountVisitsPerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("CountVisitsPerHour")
    visitHours := beam.ParDo(s, extractVisitHour, col)
    visitsPerHour := stats.Count(s, visitHours)
    return visitsPerHour
}

func extractVisitHour(v Visit) int {
    return v.TimeEntered.Hour()
}

これにより、(bazel run codelab -- --example="count" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/count.csv --output_chart_file=$(pwd)/count.png を実行することで)現在のディレクトリに count.png として正常に棒グラフが生成されます。

a179766795d4e64a.png

次のステップでは、パイプラインと棒グラフをプライベートなものに変換します。手順は次のとおりです。

まず、PCollection<V>MakePrivateFromStruct を呼び出して、PrivatePCollection<V> を取得します。入力 PCollection は、構造体のコレクションである必要があります。MakePrivateFromStruct への入力として PrivacySpecidFieldPath を入力する必要があります。

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

PrivacySpec は、データの匿名化に使用する差分プライバシー パラメータ(イプシロンとデルタ)を保持する構造体です(これについては今のところ気にする必要はありません。詳細については後ほどオプションのセクションがあります)。

idFieldPath は、構造体(この場合は Visit)内のユーザー ID フィールドのパスです。ここで、来店者のユーザー ID は VisitVisitorID フィールドです。

次に、stats.Count() の代わりに pbeam.Count() を呼び出します。pbeam.Count() は、出力の精度に影響する MaxValue などのパラメータを保持する CountParams 構造体を入力として受け取ります。

visitsPerHour := pbeam.Count(s, visitHours, pbeam.CountParams{
    MaxPartitionsContributed: 1, // Visitors can visit the restaurant once (one hour) a day
    MaxValue:                 1, // Visitors can visit the restaurant once within an hour
})

同様に、MaxPartitionsContributed は、ユーザーが寄与する可能性のある来店時間の数を制限します。普通は 1 日に 1 回しかレストランに来店しない(または 1 日に何度来店してもかまわない)と考え、1 に設定します。これらのパラメータについては、オプションのセクションで詳しく説明します。

MaxValue は、カウントする値に 1 人のユーザーが寄与する可能性のある回数を制限します。このケースでは、カウントする値は来店時間であり、ユーザーがレストランに 1 回だけ来店する(または 1 時間に何度来店してもかまわない)と考え、このパラメータを 1 に設定します。

最終的に、コードは次のようになります。

func PrivateCountVisitsPerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateCountVisitsPerHour")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, delta)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    visitHours := pbeam.ParDo(s, extractVisitHour, pCol)
    visitsPerHour := pbeam.Count(s, visitHours, pbeam.CountParams{
        MaxPartitionsContributed: 1, // Visitors can visit the restaurant once (one hour) a day
        MaxValue:                 1, // Visitors can visit the restaurant once within an hour
    })
    return visitsPerHour
}

差分プライベート統計情報の同様の棒グラフ(count_dp.png)が表示されます(前のコマンドでは、非プライベート パイプラインとプライベート パイプラインの両方が実行されます)。

d6a0ace1acd3c760.png

これで、初めての差分プライベート統計情報を計算できました。

コードを実行したときに表示される棒グラフは、これとは異なる場合がありますが、問題ありません。差分プライバシーのノイズにより、コードを実行するたびに異なる棒グラフが表示されますが、元の非プライベート棒グラフと多少似ていることがわかります。

プライバシーを保証するためには、パイプラインを(棒グラフの見栄えを良くする目的などで)複数回再実行しないようにすることが非常に重要です。パイプラインを再実行すべきでない理由については、「複数の統計情報の計算」セクションをご覧ください。

前のセクションで、一部のパーティション(時間など)について、すべての来店(データ)が省略されていたことにお気づきでしょうか。

d7fbc5d86d91e54a.png

これは、出力パーティションの存在がユーザーデータ自体に依存する場合に差分プライバシーを確実に保証するための重要なステップである、パーティション選択 / しきい値設定に起因します。このような場合、出力にパーティションが存在するだけで、データ内に個々のユーザーの存在することが漏洩する可能性があります(これでプライバシーが侵害される理由については、こちらのブログ投稿をご覧ください)。これを防ぐために、Privacy on Beam では、十分な数のユーザーがいるパーティションのみを保持します。

出力パーティションのリストがプライベート ユーザーデータに依存しない場合(公開情報の場合など)、このパーティション選択ステップは必要ありません。これは、実際にレストランのサンプルに当てはまります。レストランの営業時間(9:00~21:00)はわかっています。

このサンプルのコードは codelab/public_partitions.go にあります。

ここでは、単に 9~21 時(両端を含まない)の PCollection を作成し、CountParamsPublicPartitions フィールドに入力します。

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, extractVisitHour, pCol)
    visitsPerHour := pbeam.Count(s, visitHours, pbeam.CountParams{
        MaxPartitionsContributed: 1,     // Visitors can visit the restaurant once (one hour) a day
        MaxValue:                 1,     // Visitors can visit the restaurant once within an hour
        PublicPartitions:         hours, // Visitors only visit during work hours
    })
    return visitsPerHour
}

なお、パブリック パーティションとラプラスノイズ(デフォルト)を使用している場合は、デルタを 0 に設定できます。

パブリック パーティションを使用して(bazel run codelab -- --example="public_partitions" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/public_partitions.csv --output_chart_file=$(pwd)/public_partitions.png)パイプラインを実行すると、次のようになります。

7c950fbe99fec60a.png

このように、パブリック パーティションなしで以前省略されていたパーティション 9、10、16 が、今回は保持されています。

パブリック パーティションを使用すると、より多くのパーティションを保持できるだけでなく、各パーティションに追加されるノイズがパブリック パーティションを使用しない場合と比較して約半分になります。これは、パーティション選択にイプシロンやデルタなどのプライバシー予算を使用しないためです。そのため、元のカウントとプライベート カウントの差が、以前の実行と比べてわずかに小さくなります。

パブリック パーティションを使用する場合、次の 2 点に注意してください。

  1. 元データからパーティションのリストを導出する場合は注意が必要です。ユーザーデータ内のすべてのパーティションのリストを単に読み取るなど、差分プライベートの方法で行わないと、パイプラインで差分プライバシーが保証されなくなります。差分プライベートでこれを行う方法については、後述の高度なセクションをご覧ください。
  2. 一部のパブリック パーティションにデータ(来店数など)がない場合、そうしたパーティションには、差分プライバシーを保持するためにノイズが適用されます。たとえば、(9~21 ではなく)0~24 の時間を使用した場合、すべての時間にノイズがかかり、来店がない場合でもいくつか表示されることがあります。

(高度)データからパーティションを導出する

同じパイプラインで出力パーティションの同じリストを使用して複数の集計を行う場合、SelectPartitions() を使用してパーティションのリストを 1 回導出し、そのパーティションを PublicPartition 入力として各集計に提供できます。これはプライバシーの観点から安全であるだけでなく、パイプライン全体でパーティション選択にプライバシー予算を 1 回だけ使用するため、加えるノイズを少なくできます。

差分プライベートの方法でカウントする方法について把握したところで、平均の計算について見てみましょう。具体的には、来店者の平均滞在時間を計算します。

このサンプルのコードは codelab/mean.go にあります。

通常、滞在時間の非プライベート平均を計算するには、来店数の受信 PCollectionPCollection<K,V> に変換する前処理ステップとともに、stats.MeanPerKey() を使用します(ここで K は来店時間、V は来店者がレストランで過ごした時間です)。

func MeanTimeSpent(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("MeanTimeSpent")
    hourToTimeSpent := beam.ParDo(s, extractVisitHourAndTimeSpentFn, col)
    meanTimeSpent := stats.MeanPerKey(s, hourToTimeSpent)
    return meanTimeSpent
}

func extractVisitHourAndTimeSpentFn(v Visit) (int, int) {
    return v.TimeEntered.Hour(), v.MinutesSpent
}

これにより、(bazel run codelab -- --example="mean" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/mean.csv --output_chart_file=$(pwd)/mean.png を実行することで)現在のディレクトリに mean.png として正常に棒グラフが生成されます。

bc2df28bf94b3721.png

これを差分プライベートにするために、再度 PCollectionPrivatePCollection に変換し、stats.MeanPerKey()pbeam.MeanPerKey() に置き換えます。Count と同様に、精度に影響する MinValueMaxValue などのパラメータを持つ MeanParams があります。MinValueMaxValue は、各キーに対する各ユーザーの寄与の限界を表します。

meanTimeSpent := pbeam.MeanPerKey(s, hourToTimeSpent, pbeam.MeanParams{
    MaxPartitionsContributed:     1,  // Visitors can visit the restaurant once (one hour) a day
    MaxContributionsPerPartition: 1,  // Visitors can visit the restaurant once within an hour
    MinValue:                     0,  // Minimum time spent per user (in mins)
    MaxValue:                     60, // Maximum time spent per user (in mins)
})

この場合、各キーは 1 時間を表し、値は来店者の滞在時間を表します。来店者のレストラン滞在時間が 0 分未満となることは想定していないため、MinValue を 0 に設定します。MaxValue を 60 に設定します。来店者が 60 分を超えて滞在した場合、60 分滞在したものとして扱うことになります。

最終的に、コードは次のようになります。

func PrivateMeanTimeSpent(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateMeanTimeSpent")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    // Create a PCollection of output partitions, i.e. restaurant's work hours (from 9 am till 9pm (exclusive)).
    hours := beam.CreateList(s, [12]int{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})

    hourToTimeSpent := pbeam.ParDo(s, extractVisitHourAndTimeSpentFn, pCol)
    meanTimeSpent := pbeam.MeanPerKey(s, hourToTimeSpent, pbeam.MeanParams{
        MaxPartitionsContributed:     1,     // Visitors can visit the restaurant once (one hour) a day
        MaxContributionsPerPartition: 1,     // Visitors can visit the restaurant once within an hour
        MinValue:                     0,     // Minimum time spent per user (in mins)
        MaxValue:                     60,    // Maximum time spent per user (in mins)
        PublicPartitions:             hours, // Visitors only visit during work hours
    })
    return meanTimeSpent
}

差分プライベート統計情報の同様の棒グラフ(mean_dp.png)が表示されます(前のコマンドでは、非プライベート パイプラインとプライベート パイプラインの両方が実行されます)。

e8ac6a9bf9792287.png

繰り返しになりますが、カウントと同様に、これは差分プライベート オペレーションであるため、得られる結果は実行するたびに異なります。しかし、差分プライベートの滞在時間は、実際の結果からそれほどかけ離れてはいないことがわかります。

確認できるもう 1 つの興味深い統計情報は、1 日のうち 1 時間あたりの収益です。

このサンプルのコードは codelab/sum.go にあります。

ここでも、非プライベート バージョンで開始します。モック データセットに前処理を行い、PCollection<K,V> を作成します。ここで K は来店時間、V は来店者がレストランで支払った金額です。1 時間あたりの非プライベート収益を計算するには、stats.SumPerKey() を呼び出すことで来店者の支払い額を単にすべて合計します。

func RevenuePerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("RevenuePerHour")
    hourToMoneySpent := beam.ParDo(s, extractVisitHourAndMoneySpent, col)
    revenues := stats.SumPerKey(s, hourToMoneySpent)
    return revenues
}

func extractVisitHourAndMoneySpent(v Visit) (int, int) {
    return v.TimeEntered.Hour(), v.MoneySpent
}

これにより、(bazel run codelab -- --example="sum" --input_file=$(pwd)/day_data.csv --output_stats_file=$(pwd)/sum.csv --output_chart_file=$(pwd)/sum.png を実行することで)現在のディレクトリに sum.png として正常に棒グラフが生成されます。

548619173fad0c9a.png

これを差分プライベートにするために、再度 PCollectionPrivatePCollection に変換し、stats.SumPerKey()pbeam.SumPerKey() に置き換えます。CountMeanPerKey と同様に、精度に影響する MinValueMaxValue などのパラメータを持つ SumParams があります。

revenues := pbeam.SumPerKey(s, hourToMoneySpent, pbeam.SumParams{
    MaxPartitionsContributed: 1,  // Visitors can visit the restaurant once (one hour) a day
    MinValue:                 0,  // Minimum money spent per user (in euros)
    MaxValue:                 40, // Maximum money spent per user (in euros)
})

この場合、MinValueMaxValue は、各来店者の支払い額の限界を表します。来店者がレストランで支払う金額が 0 ユーロ未満となることは想定していないため、MinValue を 0 に設定します。MaxValue を 40 に設定します。来店者が 40 ユーロを超えて支払った場合、40 ユーロ支払ったものとして扱うことになります。

最終的に、コードは次のようになります。

func PrivateRevenuePerHour(s beam.Scope, col beam.PCollection) beam.PCollection {
    s = s.Scope("PrivateRevenuePerHour")
    // Create a Privacy Spec and convert col into a PrivatePCollection
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0)
    pCol := pbeam.MakePrivateFromStruct(s, col, spec, "VisitorID")

    // Create a PCollection of output partitions, i.e. restaurant's work hours (from 9 am till 9pm (exclusive)).
    hours := beam.CreateList(s, [12]int{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20})

    hourToMoneySpent := pbeam.ParDo(s, extractVisitHourAndTimeSpentFn, pCol)
    revenues := pbeam.SumPerKey(s, hourToMoneySpent, pbeam.SumParams{
        MaxPartitionsContributed: 1,     // Visitors can visit the restaurant once (one hour) a day
        MinValue:                 0,     // Minimum money spent per user (in euros)
        MaxValue:                 40,    // Maximum money spent per user (in euros)
        PublicPartitions:         hours, // Visitors only visit during work hours
    })
    return revenues
}

差分プライベート統計情報の同様の棒グラフ(sum_dp.png)が表示されます(前のコマンドでは、非プライベート パイプラインとプライベート パイプラインの両方が実行されます)。

e86a11a22a5a2dc.png

繰り返しになりますが、カウントや平均と同様に、これは差分プライベート オペレーションであるため、得られる結果は実行するたびに異なります。しかし、差分プライベートの結果は、1 時間あたりの実際の収益に非常に近いことがわかります。

ほとんどの場合、カウント、平均、合計で行ったように、同じ基となるデータに対して複数の統計情報を計算することに興味があるのではないでしょうか。これは通常、1 つの Beam パイプラインと 1 つバイナリで行う方が簡潔です。Privacy on Beam でも行うことができます。変換と計算を実行する 1 つのパイプラインを記述し、パイプライン全体に 1 つの PrivacySpec を使用できます。

これを 1 つの PrivacySpec で行う方が便利であるだけでなく、プライバシーの観点からも優れています。PrivacySpec に指定するイプシロン パラメータとデルタ パラメータは、プライバシー予算という、基となるデータの中でユーザーのプライバシーがどの程度漏洩するかの尺度を表します。

プライバシー予算について覚えておくべき重要な点は、加法的であるということです。特定のイプシロン(ε)とデルタ(δ)持つパイプラインを 1 回実行すると、(ε,δ) 予算を使うことになります。2 回実行すると、合計で (2ε, 2δ) の予算を使うことになります。同様に、(ε,δ) の PrivacySpec で(および連続してプライバシー予算で)複数の統計情報を計算すると、合計で (2ε, 2δ) の予算を使うことになります。つまり、プライバシーの保証を低下させています。

これを回避するために、同じ基となるデータに対して複数の統計情報を計算する場合は、使用する合計予算で 1 つの PrivacySpec を使用します。その後、各集計に使用するイプシロンとデルタを指定する必要があります。最終的には、全体的なプライバシー保証は同じになります。ただし、特定の集計のイプシロンとデルタが大きければ大きいほど、精度は高くなります。

これを実際に確認するため、1 つのパイプラインで、以前個別に計算した 3 つの統計情報(カウント、平均、合計)を計算します。

このサンプルのコードは codelab/multiple.go にあります。合計 (ε,δ) 予算を 3 つの集計の間で均等に分割している点に注目してください。

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
    spec := pbeam.NewPrivacySpec(epsilon, /* delta */ 0) // Shared by count, mean and sum.
    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, extractVisitHour, pCol)
    visitsPerHour = pbeam.Count(s, visitHours, pbeam.CountParams{
        Epsilon:                  epsilon / 3,
        Delta:                    0,
        MaxPartitionsContributed: 1,     // Visitors can visit the restaurant once (one hour) a day
        MaxValue:                 1,     // Visitors can visit the restaurant once within an hour
        PublicPartitions:         hours, // Visitors only visit during work hours
    })

    hourToTimeSpent := pbeam.ParDo(s, extractVisitHourAndTimeSpentFn, pCol)
    meanTimeSpent = pbeam.MeanPerKey(s, hourToTimeSpent, pbeam.MeanParams{
        Epsilon:                      epsilon / 3,
        Delta:                        0,
        MaxPartitionsContributed:     1,     // Visitors can visit the restaurant once (one hour) a day
        MaxContributionsPerPartition: 1,     // Visitors can visit the restaurant once within an hour
        MinValue:                     0,     // Minimum time spent per user (in mins)
        MaxValue:                     60,    // Maximum time spent per user (in mins)
        PublicPartitions:             hours, // Visitors only visit during work hours
    })

    hourToMoneySpent := pbeam.ParDo(s, extractVisitHourAndTimeSpentFn, pCol)
    revenues = pbeam.SumPerKey(s, hourToMoneySpent, pbeam.SumParams{
        Epsilon:                  epsilon / 3,
        Delta:                    0,
        MaxPartitionsContributed: 1,     // Visitors can visit the restaurant once (one hour) a day
        MinValue:                 0,     // Minimum money spent per user (in euros)
        MaxValue:                 40,    // Maximum money spent per user (in euros)
        PublicPartitions:         hours, // Visitors only visit during work hours
    })

    return visitsPerHour, meanTimeSpent, revenues
}

この Codelab では、イプシロン、デルタ、maxPartitionsContributed など、非常に多くのパラメータについて説明しました。これらは、プライバシー パラメータ、ユーティリティ パラメータという、2 つのカテゴリに大別できます。

プライバシー パラメータ

イプシロンとデルタは、差分プライバシーを使用して Google が提供しているプライバシーを定量化するパラメータです。より正確に言えば、イプシロンとデルタは、潜在的な攻撃者が匿名化された出力を調べることで、基となるデータについてどの程度の情報を得るのか示す指標です。イプシロンとデルタが大きいほど、基となるデータについて攻撃者が得る情報が多くなり、プライバシーのリスクとなります。

一方、イプシロンとデルタが小さいほど、匿名化するために出力に加える必要のあるノイズが多くなり、匿名化した出力にパーティションを保持するために各パーティションに必要なユニーク ユーザーの数が多くなります。そのため、ここでユーティリティとプライバシーはトレードオフの関係にあります。

Beam on Privacy では、PrivacySpec で合計プライバシー予算を指定する場合、匿名化された出力に必要なプライバシー保証について考慮する必要があります。プライバシー保証を維持する場合、この Codelab のアドバイスどおりにする必要があります。つまり、集計ごとに別々の PrivacySpec を用意するか、パイプラインを複数回実行して、予算を使いすぎないようにします。

差分プライバシーとプライバシー パラメータの意味について詳しくは、こちらの文献をご覧ください。

ユーティリティ パラメータ

これらのパラメータは、プライバシー保証には影響しませんが(Privacy on Beam の使用方法に関するアドバイスに沿っている限り)、精度に影響し、結果として出力の実用性にも影響します。各集計の Params 構造体(CountParamsSumParams など)で提供され、加えられるノイズをスケーリングするために使用されます。

Params で提供される、すべての集計に適用可能なユーティリティ パラメータは MaxPartitionsContributed です。パーティションは、Privacy On Beam 集計オペレーションによって出力される PCollection のキーに対応します(CountSumPerKey など)。そのため MaxPartitionsContributed は、出力でユーザーが寄与する可能性のある個別のキー値の数を制限します。基となるデータでユーザーの寄与が MaxPartitionsContributed キーより多い場合、寄与の一部が省略され、寄与は MaxPartitionsContributed キーどおりになります。

MaxPartitionsContributed と同様に、ほとんどの集計には MaxContributionsPerPartition パラメータがあります。これらは Params 構造体で提供され、集計ごとに別々の値を設定できます。MaxPartitionsContributed とは対照的に、MaxContributionsPerPartition は、キーごとにユーザーの寄与を制限します。つまり、キーごとにユーザーの寄与は MaxContributionsPerPartition の値にしかなりません。

出力に追加されるノイズは MaxPartitionsContributedMaxContributionsPerPartition でスケーリングされるため、ここではトレードオフの関係にあります。MaxPartitionsContributedMaxContributionsPerPartition のどちらを大きくしても、保持できるデータが多くなりますが、結果的にノイズが多くなります。

一部の集計には、MinValueMaxValue が必要です。これにより、各ユーザーの寄与の限界を指定します。ユーザーの寄与が MinValue よりも低い値であると、その値は MinValue に固定されます。同様に、ユーザーの寄与が MaxValue より大きい値であると、その値は MaxValue に固定されます。つまり、元の値をより多く保持するには、より大きな限界を指定する必要があります。MaxPartitionsContributedMaxContributionsPerPartition と同様に、ノイズは限界のサイズに合わせてスケーリングされるため、限界を大きくすると保持できるデータが多くなりますが、結果的にノイズが多くなります。

最後に紹介するパラメータは NoiseKind です。Privacy On Beam では、2 種類のノイズ メカニズム「GaussianNoise」と「LaplaceNoise」がサポートされています。どちらにも長所と短所がありますが、ラプラス分布の方が、小さな寄与限界で優れた実用性が得られます。そのため、Privacy On Beam ではこれがデフォルトで使用されます。ただし、ガウス分布のノイズを使用する場合は、Paramspbeam.GaussianNoise{} 変数を指定します。

お疲れさまでした。Privacy on Beam の Codelab は以上です。差分プライバシーと Privacy on Beam について多くのことを学びました。

  • MakePrivateFromStruct を呼び出して PCollectionPrivatePCollection にする。
  • Count を使用して差分プライベート カウントを計算する。
  • MeanPerKey を使用して差分プライベート平均を計算する。
  • SumPerKey を使用して差分プライベート合計を計算する。
  • 1 つのパイプラインで 1 つの PrivacySpec を使用して、複数の統計情報を計算する。
  • (任意)PrivacySpec パラメータと集計パラメータ(CountParams, MeanParams, SumParams)をカスタマイズする。

ただし、Privacy on Beam で行える集計は他にもたくさんあります。詳細については、GitHub リポジトリまたは godoc をご覧ください。

よろしければ、アンケートにご協力いただき、この Codelab についてのフィードバックをお寄せください。