Google AdSenseでGDPR同意メッセージを作成する

このブログではGoogle AdSenseを導入していますが、少し前から 管理画面にログインすると、GDPR同意メッセージを作成するよう促すポップアップが表示されるようになっていました。表示されるのはこれです。

かなり面倒だな、もう欧州からのアクセスをブロックしたい、くらいに思って放置していたのですがようやく重い腰を上げて対応したのでそのメモを残しておきます。

どうやら上のポップアップから指示に従って進めていけば設定は完了するようです。念のためドキュメントを見ておきたいという場合は、以下のページが参考になります。
GDPR 同意メッセージについて – Google AdSense ヘルプ
GDPR メッセージを作成する – Google AdSense ヘルプ

自分はアドセンスを設定しているサイトがこのブログだけなので、上のポップアップから完了させてしまいました。

手順

最初にGDPR同意メッセージ作成の同意方法を選択します。上の画面の3つの選択肢(3番目は作成しない、なので実質選択肢は2個)から一つ選ぶのですが、僕は一番上の Google 認定のCMPを使用する方法を選択しました。(細かい違いは理解できてないのですが、おそらく一番上が推奨だと思ったので。)
これで確認を押した時点でポップアップは消えます。

すると、アドセンス上部の赤い枠の注意メッセージで以下の文言が表示されます。

2024 年 1 月 16 日より、欧州経済領域(EEA)または英国(UK)のユーザーに広告を配信するすべてのパブリッシャー様は、Google の認定を受けた同意管理プラットフォーム(CMP)をご使用いただくことが必要となります。欧州経済領域と英国で広告を配信する際には、Google 独自の同意管理ソリューションを含む Google 認定の CMP をご利用いただけます。Google の同意管理ソリューションに関心をお持ちの場合は、まず GDPR メッセージを設定してください。

GDPRメッセージを作成、って文がその下にあるのでそこを押します。

すると、次の4ステップでメッセージが作成できるという説明が出ます。

  1. サイトにプライバシー ポリシーの URL を追加する
  2. 含める同意オプションを選択します
  3. GDPR アカウント設定を確認します
  4. GDPR メッセージを公開します

「使ってみる」を押して進めます。

見慣れない画面に行くので、右側の画面からサイトの選択をします。(自分がGoogle Adsenseを設定しているサイトの一覧がみれると思います。僕はこのブログだけです。)

この時、プライバシーポリシーのURLも設定する必要があります。まだない人はこの機会に作りましょう。

言語は日本語にしました。

「同意しない」や「閉じる」のオプションはオフとしました。

この辺りのオプションはどう選択したらどのようにユーザーに表示されるのかプレビューがー随時見れるので確認しながらいじりましょう。

「スタイル」を選ぶと他にも細かな調整ができます。僕はこのブログにロゴを持ってないので、ヘッダーのロゴをオフにしました(デフォルトがオンだったので)。

ここまで設定したら右上の「公開」ボタンを押します。

今後、メッセージを修正したい場合は、
プライバシーとメッセージから GDPRを選ぶことで先ほどの設定画面に戻れるようです。

テスト方法

設定した内容をテストする方法も用意されています。

参考: プライバシーとメッセージについて – Google アド マネージャー ヘルプ

上記のヘルプページ内の、「サイトのメッセージをテストする手順とパラメータ」という折りたたみコンテンツの中にパラメーターがいくつか紹介されています。

それによると、 自分のブログのURLの最後に、?fc=alwaysshow をつけてアクセスすると、地域を考慮せずにメッセージが表示されるようです。

僕は無事に作成したGDPRメッセージが表示されることを確認できました。

InteractiveShell オブジェクトを利用してマジックコマンドを実行したり変数を操作したりする

いつも使っているJupyterに関する話です。Jupyterの中では、InteractiveShellというオブジェクトが使われていて、これを利用することでさまざまな高度な操作を行うことができます。

多くのことができるのですが、最近これを使って変数を操作する機会があったのでその辺を紹介していきます。

そもそも、InteractiveShell オブジェクトとは?という話ですが、これはIPython(Jupyter Labのバックエンドとして動作する対話的なPythonシェル)の中心的なクラスです。このオブジェクトを通じて、現在のセッションの状態を取得したり、マジックコマンドを実行したりすることができます。

このInteractiveShellはget_ipython() という関数を使うことでそのnotebook自身で取得し、アクセスすることができます。

マジックコマンドの動的な実行

このInteractiveShellオブジェクトを使うと例えば、マジックコマンドをプログラムで実行したりできます。

例えば、%pwd っていう現在のでディレクトリを取得するマジックコマンドがありますが、これが次のようにして実行できます。

ip = get_ipython()
ip.run_line_magic('pwd', '')

(pwdをメソッドとして実行するメリットはなかなか薄いので例が適切でなくてすみません。)

マジックコマンドをそのままマジックコマンドととして実行するのと比べると、結果を文字列として取得して以降の処理で使えるとか、実行するコマンドをプログラムで動的に変化させて実行させることができるといった利点があります。

もちろん、 run_cell_magic() って関数もあるのでセルマジックも実行できますよ。

変数の取得や設定

今回紹介するもう一つの使い方は変数の取得や追加です。名前空間の操作といった方がわかりやすいかもしれません。

InteractiveShellオブジェクトを使って、upyter Labのセッション内で定義されている変数を取得したり、新しい変数を設定したりすることができます。

変数の取得は user_ns 属性を使います。現在定義されている変数が全部辞書として入っています。デフォルトで入ってるJupyter/IPython固有の変数も多いので、自分が宣言した覚えのないものもあると思います。

例えば次のようにして使います。

x = 10
y = "Hello"

ip = get_ipython()
print(ip.user_ns['x'])
# 10
print(ip.user_ns['y'])
# "Hello"

変数の取得としてはあまり用途ないかもしれないですね。どちらかというと既に宣言されているかどうかのチェックとかで使えるかもしれません。

逆に、次のようにして新しい変数を定義することもできます。

ip = get_ipython()
ip.push({'z': "新しい変数"})

print(z)
# 新しい変数

この例だけ見てると、 z=”新しい変数” って直接書くのと同じやん、と思えますが、この方法の利点は変数名の方もプログラムで動的に指定できることです。

まぁ、 exec(“z=’新しい変数'”) ってすれば変数名を動的に変えられるので固有のメリットというわけではないのですが、execは本当にリスクの大きな手段なのでそれよりは安全に使えるという利点があると思います。

この変数の名の指定は、関数やマジックコマンドの中で使うと、例えばユーザーが指定した変数名に結果を格納するといった応用ができます。

最近、別の場所で書いているnote記事の中で自分が自作したマジックコマンドを紹介したのですが、そこで使っているクエリの結果を引数で指定した変数に格納するのにこのip.pushを使っています。

参考: Snowflakeに手軽にSQLを打てるJupyterマジックコマンドを作ってみた

ip.push を使わずに、
ip.user_ns[‘z’] = “新しい変数”
みたいに、user_nsオブジェクトに直接突っ込んでも同じことができますので合わせて覚えておいてください。

InteractiveShellオブジェクトでできることのごく一部ではありますが、今回の記事としては以上となります。

二つの区間の重複を判定する効率的な方法

はじめに

ちょっと重い記事が続いてたので今回は小ネタです。

二つの区間データ、要するに数値の区間であれば最小値と最大値のペア、時刻の区間であれば開始時間と終了時間のデータが二つあったときに、それらの区間に重複があるかどうかをサクッと判定するアルゴリズムを紹介します。昔どこかでみた覚えがあるのですが、久々に実装で必要になったときにちょっとだけ悩んだのでそのメモです。

結論

結果だけ知りたい人向けに先に結論だけ書いときます。

区間[s1, e1]と[s2, e2]に重複があるかどうかは次の2つの方法で判定できます。

方法1: $(s1 \leq e2) \land (s2 \leq e1)$ ならばその2区間には重複があります。

方法2: $\max(s1, s2) \leq \max(e1, e2)$ ならばその2区間には重複があります。

両方の方法のメリットデメリットですが、方法1の方が計算速度が速いです。大小比較2個と論理演算ですからmax関数呼び出すより有利ですね。ただ、方法2の方は、応用として、二つの区間に重複があった場合、$\max(e1, e2) – \max(s1, s2)$で重複区間の長さを算出できます。(値が負になったら重複無し。)

(max関数がそんな極端に遅いってこともなく、わざわざ時間測定しない限り体感することもない速度差ではあるのであくまでも参考にどうぞ。)

方法1の導出

これだけで終えてしまうと記事量としてあまりに少ないのと、結論だけ書いてるとすぐ忘れそうなので、真面目に導出を説明しておきます。まずは方法1の方からです。

まず、二つの区間の相対的な位置関係は次の6パターンが存在します。(説明の単純化のため。s1,s2,e1,e2は全て異なる値とします。)

  1. 区間1全体が区間2より小さい。つまり、 $s1 < e1 < s2 < e2$.
  2. 区間1全体が区間2より大きい。つまり、 $s2 < e2 < s1 < e1$.
  3. 区間1が区間2に内包される。つまり、$s2 < s1 < e1 < e2$.
  4. 区間1が区間2を内包する。つまり、$s1 < s2 < e2 < e1$.
  5. 区間1の後半と区間2の前半が重なる。つまり、$s1 < s2 < e1 < e2$.
  6. 区間1の前半と区間2の後半が重なる。つまり、$s2 < s1 < e2 < e1$.

上記の6パターンのうち、1,2 が区間に重複がなく、3,4,5,6は区間に重複があります。

ここで、「そうか!3,4,5,6のどれかを満たすことを確認すればいいんだ!」と判断して4パターンの論理式を書いてorで繋ぐ、としないことがコツです。

区間に重複がないパターンの方が2種類しかなくて判定が単純なんですよね。そして、区間の定義から$s1<e1$と$s2<e2$はわかってるので、あと確認するのはe1とs2, e2とs1の代償関係だけなんですよ。

ということで、 $(e1 < s2) \lor (e2 < s1)$ であれば二つの区間に重複はないと言えます。

これの否定を考えると、ドモルガンの法則から方法1としてあげた式が導かれます。

方法2の導出

続いて方法2の導出です。

これは、先ほど挙げた3,4,5,6の4パターンの不等式を眺めると見つかる法則性なのですが、左の2辺はs1,かs2で、右の2辺はe1, e2なんですよね。

これを素直に数式に落とすと、$\max(s1, s2) \leq \max(e1, e2)$ となります。等号が成立するのはただ1点だけが重複する場合。

そして、区間が重複する場合は、$\max(s1, s2)$は重複区間の開始点であり、$\max(e1, e2)$は重複区間の終了点なので、この2項の差を取ると重複部分の長さも得られます。

まとめ

そんなに難しい計算ではなく、覚えてさえればサクッと実装できる式ではありますが、重複のパターンって4種類あるよなぁと考え始めてしまうと意外に手間取ります。

結果を丸暗記しなくても、区間が重複しないパターンは2個しかなくてそれを否定したら簡単だってことを頭の片隅にでも置いといてもらえると幸いです。

ジニ不純度について

ここ数週間にわたってエントロピー関連の話が続きましたので、ついでみたいになるのですが決定木の評価関数として同じようによく使われるジニ不純度についても紹介しておきます。
ジニ係数(はじパタなど)や、ジニ指数(カステラ本など)といった呼び名もあるのですが、経済学で所得格差の指標に用いられるジニ係数と紛らわしくなるので、この記事ではジニ不純度と呼びます。

ジニ不純度の定義

ジニ不純度は、決定木やランダムフォレストなどの機械学習のアルゴリズムにおいて、ノードの純度を計測するための指標の一つとして用いられます。要するに、そのノードに分類される観測値のクラスの内訳が全部同じだとなると不純度は小さく、同じノードに複数のクラスの観測値が混在してたら不純度は高くなります。エントロピーと似た挙動ですね。

数式としては次のように定義されます。$n$クラスに分類する問題の場合、$i$番目のクラスのサンプル比率を$p_i$とすると、ジニ不純度は次のように定義されます。

$$I = 1 – \sum_{i=1}^n p_i^2.$$

例えば、3クラスの分類問題を考えると、あるノードの観測値が全部同じクラスだったとします。すると、$p_1 = 1, p_2=0, p_3 = 0$みたいになるのですが、この時$I=0$と、不純度が0になります。また、3クラスが均等に混ざっていた場合、$p_1 = p_2 = p_3 = 2/3$となり、時に不純度は$I = 2/3$となります。$n$クラスの分類問題の場合、この$1-1/n$が最大値です。

もう7年近く前になりますが、初めてこの定義式を見た時は、1から確率の平方和を引くことに対してちょっと違和感を感じました。ただ、これは計算を効率的にやるためにこの形にしているだけで、実際は次の形で定義される値と考えておくと納得しやすいです。

$$I = \sum_{i=1}^n p_i (1-p_i).$$

これ展開すると、$\sum_{i=1}^n p_i – p_i^2$となり、$\sum_{i=1}^n p_i =1$ですから、先に出した定義式と一致することがわかります。

ジニ不純度の解釈

それでは、$p_i(1-p_i)$とは何か、という話なのですが解釈が二つあります。

まず一つ目。ある観測値がクラスiであれば1、クラスi出なければ0というベルヌーイ試行を考えるとその分散がこの$p_i(1-p_i)$になります。これを各クラス分足し合わせたのがジニ不純度ですね。

クラスiである確率が0の場合と1の2種類の場合がまじりっけ無しで不純度0ということでとてもわかりやすいです。

もう一つは、そのノードにおける誤分類率です。あるノードにおける観測値を、それぞれ確率$p_i$で、クラス$i$である、とジャッジすると、それが本当はクラス$i$ではない確率が$(1-p_i)$ありますから、誤分類率は$\sum_{i=1}^n p_i (1-p_i)$となります。

あるノードの観測値のクラスが全部一緒なら誤分類は発生しませんし、$n$クラスが均等に含まれていたら誤分類率は最大になりますね。不純度の定義としてわかりやすいです。

エントロピーと比較した場合のメリット

決定木の分割の評価として使う場合、エントロピーとジニ不純度は少しだけ異なる振る舞いをし、一長一短あるのですが、ジニ不純度には計算効率の面で優位性があるとも聞きます。というのも、$p\log{p}$より$p^2$のほうが計算が簡単なんですよね。scikit-learnがginiの方をデフォルトにしているのはこれが理由じゃないかなと思ってます。(個人的な予想ですが。)

ただ、実際に実験してみると計算速度はそこまで大差はないです(np.log2の実装は優秀)。なんなら自分で書いて試したらエントロピーの方が速かったりもします。とはいえ、$p\log{p}$の計算はちゃんとやるなら、$p=0$ではないことをチェックして場合分けを入れたりしないといけないのでそこまできちんと作ったらジニ不純度の方が速くなりそうです。

Pythonでの実装

数式面の説明が続きましたが、最後に実装の話です。実は主要ライブラリではジニ不純度を計算する便利な関数等は提供されていません。まぁ、簡単なので自分で書けば良いのですが。

scikit-learnのソースコードを見ると、ここで実装されて内部で使われていますね。

確率$p$のリストが得られたら、全部二乗して足して1から引けば良いです。
$1/6+1/3+1/2=1$なので、確率のリストは$[1/6, 1/3, 1/2]$としてやってみます。

import numpy as np


p_list = np.array([1/6, 1/3, 1/2])
print(1-(p_list**2).sum())
# 0.6111111111111112

まぁ、確かにこれならわざわざライブラリ用意してもらわなくてもって感じですね。

割合ではなく要素数でデータがある場合は全体を合計で割って割合にすればよいだけですし。

以上で、ジニ不純度の説明を終えます。エントロピーと比べて一長一短あるのでお好みの方を使ってみてください。

Pythonによる各種エントロピーや相互情報量の計算

エントロピーや相互情報量の記事が続いていますが、今回の記事で計算の実装方法を紹介して一旦区切りとします。

エントロピーも相互情報量も数式はそこまで難しくないので、numpyで定義通り計算しても良いのですが、エントロピー関係はSciPyに、相互情報量はscikit-learnに用意されているので今回の記事ではそれを使っていきます。

計算対象のデータは、[“a1”, “a2”, “a1”, “a1”, “a2”] みたいにローデータの一覧で保有している場合もあれば、”a1″が3個で”a2″が2個のようにカウント済みのものがある場合もあると思うのでそれぞれ説明していきます。

エントロピーの計算

まず一番基本的なエントロピーの計算からです。これは、scipy.stats.entropy メソッドを使います。
参考: scipy.stats.entropy — SciPy v1.11.3 Manual

基本的な引数はpkなので、確率の一覧を渡すのが想定されていますが、和が1でないなら1になるように正規化してくれるのでサンプルがある場合は個数を渡しても大丈夫です。また、base引数で対数関数の底を指定でき、デフォルトが$e$なので、情報理論で使う場合は$2$を指定しましょう。

やってみます。

import numpy as np  # データ作りに利用
import pandas as pd  # データ作りに利用
from scipy.stats import entropy


pk = np.array([1/2, 1/3, 1/6])  # 確率の一覧が得られた場合。
print(entropy(pk, base=2))
# 1.459147917027245

count_list = np.array([3, 2, 1])  # データの個数の場合
print(entropy(count_list, base=2))
# 1.4591479170272446

# カウント前のデータの一覧がある場合
data_sr = pd.Series(["a1", "a1", "a1", "a2", "a2", "a3"])
# value_counts()で数えあげたものをentropyに渡す
print(entropy(data_sr.value_counts(), base=2))
# 1.4591479170272446

結合エントロピーの計算

次は結合エントロピーです。エントロピーを単純に2次元に拡張したやつですね。(条件付きエントロピーではないので注意してください、

例えば次のような例を考えましょうか。

b1b2
a141
a223

結合エントロピーの場合はですね、元のカウントデータを2次元から1次元に並び替えて渡します。

matrix_data = np.array([[4, 1], [2, 3]])
print(matrix_data)
"""
[[4 1]
 [2 3]]
"""

# ravel か flattenで1次元化して計算する
print(entropy(matrix_data.ravel(), base=2))
# 1.8464393446710157

# 標本データがある場合
df = pd.DataFrame({
    "A": ["a1", "a1", "a1", "a1", "a1", "a2", "a2", "a2", "a2", "a2"],
    "B": ["b1", "b1", "b1", "b1", "b2", "b1", "b1", "b2", "b2", "b2"],
})

# カウントしたデータを使う
print(df.groupby(["A", "B"]).size())
"""
A   B
a1  b1    4
    b2    1
a2  b1    2
    b2    3
dtype: int64
"""

print(entropy(df.groupby(['A', 'B']).size(), base=2))
# 1.8464393446710157

条件付きエントロピー

次は条件付きエントロピーです。残念なことなのですが、メジャーなライブラリでは条件付きエントロピー専用の関数は提供されていません。

そこで、$H(A|B) = H(A, B) – H(B)$などのエントロピー間の関係式を使って計算することになります。相互情報量も含めて、$H(A|B) = H(A) – I(A; B)$などで計算してもいいのですが、SciPyで完結できるので最初の式のほうが良いでしょう。

先ほどの表データをサンプルとします。$H(B)$については、表データを縦に足し合わせてBだけのカウントデータを作って計算します。

data_B = matrix_data.sum(axis=0)
print(data_B)
# [6 4]

# H(B)の計算
entropy_B = entropy(data_B, base=2)
print(entropy_B)
# 0.9709505944546688

# H(A, B)の計算
joint_entropy = entropy(matrix_data.ravel(), base=2)
print(joint_entropy)
# 1.8464393446710157

# H(A|B) = H(A, B) - H(B)
conditional_entropy_A_given_B = joint_entropy - entropy_B
print(conditional_entropy_A_given_B)
# 0.8754887502163469

# 標本データがある場合
entropy_B = entropy(df["B"].value_counts(), base=2)
joint_entropy = entropy(df.groupby(["A", "B"]).size(), base=2)

conditional_entropy_A_given_B = joint_entropy - entropy_B
print(conditional_entropy_A_given_B)
# 0.8754887502163469

以上で、3種類のエントロピーが計算できました。

相互情報量

最後に相互情報量の計算方法です。

$I(A; B) =H(A)-H(A|B)$など複数の表現方法があるので、ここまでに計算してきた値から算出することもできます。

entropy_A = entropy(df["A"].value_counts(), base=2)
print(entropy(df["A"].value_counts(), base=2) - conditional_entropy_A_given_B)
# 0.12451124978365313

ただ、scikit-learnに専用のメソッドがあるのでこちらの使い方も見ておきましょう。
参考: sklearn.metrics.mutual_info_score — scikit-learn 0.18.2 documentation

引数は、mutual_info_score(labels_truelabels_predcontingency=None)
となっており、標本データを受け取るのが標準的な使い方で、その第一,第二引数をNoneにしてcontingency引数にカウントデータを渡すこともできます。(contingencyがNoneでない場合はこれが優先されて、先の二つの引数が無視されます。)

1点注意しないといけないのは、entropyと違って対数の底が指定できず、自然対数に固定されてしまうことです。底を$2$で考えたい場合は、$\ln{x}/\ln{2} = \log_2{x}$を使って変換が必要です。

from sklearn.metrics import mutual_info_score


# np.log(2)で割ることを忘れない
# カウントした表データがある場合
print(mutual_info_score(None, None, contingency=matrix_data)/np.log(2))
# 0.12451124978365345

# 標本データがある場合
print(mutual_info_score(df["A"], df["B"])/np.log(2))
# 0.12451124978365345

計算の都合上超軽微な誤差がありますが、それ以外は想定通りの値が得られていますね。

以上で、相互情報量も計算できるようになりました。

シャノンの補助定理とその応用

今回も引き続き情報量(エントロピー)関係の記事です。直近、エントロピーに関する記事を複数書いていますが、その中でいくつか証明をしていない性質があります。シャノンの補助定理という面白い定理を使うとそれらが証明できるので見ていきます。

情報量の話なのでこの記事では底を省略して書いた対数関数の底は$2$とします。$\log=log_2$です。一方で、自然対数も使うのでそれは$\ln=\log_e$と書きます。

早速、シャノンの補助定理を見ていきます。

定理

二つの確率事象系$A = \{a_k | k=1,\dots, n \}$と$B = \{b_k | k=1,\dots, n\}$の確率をそれぞれ$p_k$, $q_k$とします。つまり、
$\sum_{k=1}^n p_k = 1$, $\sum_{k=1}^n q_k = 1$です。この時次の関係が成り立ちます。

$$ – \sum_{k=1}^n p_k\log{p_k} \leq – \sum_{k=1}^n p_k\log{q_k}.$$

以上が、シャノンの補助定理の主張です。左辺は$A$のエントロピーで、右辺は対数関数の中身だけ別の確率事象系の確率になっていますね。

証明

定理を証明する前に、自然体数に関する$\ln{x} \leq x – 1$ $(x > 0)$という関係を使うのでこれを先に証明します。

正の実数$x$に対して、$f(x) = x -1 – \ln{x}$ とおきます。すると$f'(x) = 1 – \frac{1}{x}$となります。$f'(x)$の値を見ていくと、 $0 < x < 1$ の時$f'(x)<0$であり、$f'(1)=0$、そして$1 < x$の時$f'(x)>0$となるので、$x=1$で最小値をとり、その値は$f(1) = 1-1-\ln{1} = 0$です。

よって、$f(x) \leq 0$であり、$\ln{x} \leq x – 1$ が示されました。

ここから定理の証明に入ります。定理の左辺から右辺を引いたものを考えて、底の変換をし、先ほどの関係式を適用します。

$$ \begin{align}
– \sum_k p_k \log{p_k} + \sum_k p_k\log{q_k} &= \sum_k p_k \log{\frac{q_k}{p_k}}\\
&=\frac{1}{\ln{2}} \sum_k p_k \ln{\frac{q_k}{p_k}}\\
&\leq \frac{1}{\ln{2}} \sum_k p_k \left(\frac{q_k}{p_k} – 1 \right)\\
&= \frac{1}{\ln{2}} \sum_k (q_k – p_k)\\
&= \frac{1}{\ln{2}} \sum_k q_k – \frac{1}{\ln{2}} \sum_k p_k\\
&=0\end{align}.$$

よって、$ – \sum_{k=1}^n p_k\log{p_k} \leq – \sum_{k=1}^n p_k\log{q_k}$ が示されました。等号が成立するのは$p_k = q_k$の場合です。

この補助定理を使って、エントロピー関連の性質を見ていきます。

応用1. エントロピーの最大値

最初に見ていくのは、「平均情報量(エントロピー)の定義について」で見た、エントロピーの範囲、$0 \le H(a) \le \log{n}$ の最大値の方です。これは、$q_k = \frac{1}{n}$としてシャノンの補助定理を使うと証明できます。

$$H(a) = – \sum_k p_k\log{p_k} \leq – \sum_k p_k\log{\frac{1}{n}} = \log{n}$$

となります。

応用2. 相互情報量が0以上であること

次に証明するのは、「相互情報量について」で最後に書いた、相互情報量が非負の値を取るという話です。

これは、シャノンの補助定理をそのまま2次元の確率分布に拡張して使います。
$k = 1, \dots, n$, $l = 1, \dots, m$とし、$\sum_{k, l} p_{kl} = 1$、$\sum_{k, l} q_{kl} = 1$とすると、

$$- \sum_k \sum_l p_{kl} \log{p_{kl}} \leq – \sum_k \sum_l p_{kl} \log{q_{kl}}$$

ですから、

$$\sum_k \sum_l p_{kl} \log{\frac{p_{kl}}{q_{kl}}} \geq 0$$

となります。ここで、$p_{kl} = P(a_k, b_l)$とし、$q_{kl} = P(a_k) P(b_l)$とすると、

$$\sum_k \sum_l P(a_k, b_l) \log{\frac{P(a_k, b_l)}{P(a_k) P(b_l)}} \geq 0$$

となります。この左辺が相互情報量$I(A; B)$の定義そのものなのでこれが非負であることが示されました。

応用3. 条件付きエントロピーがエントロピーより小さいこと

3つ目はさっきの相互情報量の話から続けて導けるものです。「結合エントロピーと条件付きエントロピー」で、重要な性質として$H(A|B) \le H(A)$が成り立つと証明無しで言いました。

これはシンプルに、$I(A; B) = H(A) – H(A|B)$であり、$I(A; B) \geq 0$なので、移項するだけで、$$H(A) \geq H(A|B)$$ が言えます。

まとめ

この記事ではシャノンの補助定理とその応用をいくつか紹介しました。応用1,2,3で証明したことはそれぞれの記事で証明無しで紹介していたのを個人的にモヤモヤしていたので、それを解消できてよかったです。

エントロピーの最大値についても、相互情報量の非負性についても結構シンプルな主張なのですが、シャノンの補助定理無しで証明するのは結構難しいのでこの定理の偉大さを感じます。

ここしばらくエントロピーの理論面の記事が続いていますが、次あたりでPythonで実際に計算する方法を紹介してこのシリーズを完結させようかなと思っています。

相互情報量について

相互情報量の定義と意味

エントロピー(情報量)関係の記事の3つ目です。前回の記事の最後で、エントロピー、結合エントロピー、条件付きエントロピーの間に成り立つ関係式で4つの量が等しくなるという話をしました。
参考: 結合エントロピーと条件付きエントロピー

その値のことを、相互情報量と呼び、$I(A; B)$と書きます。($A$と$B$は対等で交換も可能なのに$I(A, B)$ではなく$I(A; B)$と書く理由が気になりますね。)

改めて式を書くとこうなります。

$$\begin{align} I(A;B) &= H(A)-H(A|B)\\
&=H(B)-H(B|A)\\
&=H(A)+H(B)-H(A, B)\\
&=H(A, B)-H(A|B)-H(B|A)\end{align}$$

現実的には、一番上の行の等式で定義することが多いようです。

事象$A$の不確実性である、$H(A)$から、事前情報として$B$を知っている場合の$A$の不確実性$H(A|B)$を引いているわけですから、$I(A;B)$は、事前情報として$B$を知ったことによって減った$A$の不確実性と考えられます。$B$について知ることで分かった$A$に関する情報の方がわかりやすいかもですね。

個人的には$H(A) = I(A; B) + H(A|B)$ と移項して、{Aの情報量} = {Bを知った時点で得られるAの情報量} + {Bを知った後にAを観察して初めてわかるAの情報量} と考える方が理解しやすいと思っています。

この相互情報量は情報理論において非常に重要な概念です。これは、2つの確率変数間の情報の共有度合いを測る指標として使用されています。

相互情報量の確率表現

実用上の話として、$H(A)-H(A|B)$のままでは計算しにくいので確率$P$の式として相互情報量を表すことを目指します。一番上の定義をスタートとして計算してみましょう。

$$\begin{align}I(A;B) &= H(A)-H(A|B)\\
&= -\sum_{A}P(a)\log{P(a)} – \left\{ – \sum_{A}\sum_{B} P(a, b) \log{P(a|b)} \right\}\\
&= -\sum_{A}\sum_{B}P(a, b)\log{P(a)} + \sum_{A}\sum_{B} P(a, b) \log{\frac{P(a,b)}{P(b)}} \\
&= \sum_{A}\sum_{B} P(a, b) \log{\frac{P(a, b)}{P(a)P(b)}}.
\end{align}$$

なかなか綺麗な形に導けましたね。この形で見ると$A$, $B$を入れ替えても同じ値になることなどが容易にわかります。

Wikipedia などもそうですが、この確率表現の形の方を相互情報量の定義として、先に挙げた等式類を定理として導く流儀もあるようです。というより僕自身も元々そちらで理解していました。

相互情報量の性質

相互情報量は常に非負の値を取ります。

$$I(A;B)\ge 0.$$

また、$A$と$B$が独立の時、$P(a, b) = P(a)P(b)$ ですから、$\log$の中身が$1$になり、値が$0$になるため、$I(A;B)=0$となります。

これに関してはここ最近の証明してない他の性質とまとめて別記事で証明を紹介します。

逆に、$I(A; B) \le H(A)$ と $I(A; B) \le H(B)$ もそれぞれ成立します。

$A$の例を見ていきますが、等号が成立するのは $H(A|B)=0$となる場合です。これは、$B$の情報を得た時点で$A$のことが完全にわかってAを知っても情報が増えないケースが該当します。

結合エントロピーと条件付きエントロピー

前回の記事に続いて平均情報量(エントロピー)の話です。

参考: 平均情報量(エントロピー)の定義について

今回の記事ではその2次元版とも言える2種類のエントロピーを紹介します。この2つについてはどうも情報量よりエントロピーと呼ぶことが多いようなのでそのように書きます。また、数式中に和の記号がたくさん登場し、添え字書いていくと大変だし、かえって混乱もまねくので、$\sum_{k=1}^n f(a_k)$ を $\sum_{A}f(a)$のようにシンプルに書きます。

そのため、$A = \{a_k\}$のエントロピーは次のように書きます。

$$H(A) = \sum_{A}P(a)\log{P(a)}.$$

参考にした本は前回の記事同様、平田先生の「情報理論のエッセンス」です。

結合エントロピー

まず紹介するのはエントロピーの2次元版ともいえる、結合エントロピーです。これは二つの事象の集合 $A={a_k}$ と $B={b_l}$に対して定義されます。$a_k$と$b_l$が発生する確率を$P(a_k, b_l)$ とすると、結合エントロピーは次のように定義されます。

$$H(A, B) = – \sum_{A}\sum_{B}P(a, b)\log{P(a, b)}.$$

ここで、$\sum_{A}\sum_{B}P(a, b)=1$であることに注意してください。

これはもう、$A$と$B$の組みあわせとして発生しうる事象を全て列挙してそれに対してエントロピーを計算したようなもので、とてもわかりやすい定義だと思います。

$A$と$B$の結果を知った時に得られると期待できる情報量、とも言えます。

条件付きエントロピー

次に紹介するのは条件付きエントロピーです。これは先ほどの結合エントロピーと少し状況が異なり、あらかじめ$B$に関する情報を知った上で後から$A$についての結果を得たらどれだけの情報量を得られるかを表すものです。

こちらは結合エントロピーと違って、定義をバシッと書いてもわかりにくいので例を取り上げて順番に説明します。

本の例では、$A$がサイコロの目で$\{1, 2, 3, 4, 5, 6\}$、$B$が偶数/奇数というものを取り上げていましたのでここでもそれにならいます。$b_1$が偶数で$b_2$が奇数です。

まず$A$のエントロピーは$H(A)=-\sum_{k=1}^{6}\frac{1}{6}\log{\frac{1}{6}}=\log{6}\fallingdotseq 2.58$ です。

ここで、サイコロをの目を見る前になぜか偶数か奇数かだけわかり、$b_1$ = 偶数だったとしましょう。その後に目を確認するとすると、$2, 4, 6$が出ている確率は$1/3$で、$1, 3, 5$が出ている確率は$0$ということになります。

つまりサイコロの目を見ることで得られる情報量は、

$H(A|b_1) = -\sum_{A}P(a|b_1)\log{P(a|b_1)} = -\sum_{a\in\{2, 4,6\}}\frac{1}{3}\log{\frac{1}{3}} = \log{3} \fallingdotseq 1.58$

となります。情報量が1だけ減っていますね。この1は、$\log{2}$相当の値で、事前に偶数か奇数かという情報を得たことで起こりうる確実性が$1/2$になったことを示しています。

同様に計算すると、事前に奇数だっと知らされた後に出た目を確認することによって得られる情報量$H(A|b_2)$も$\log{3}$だとわかります。

そして、いよいよ本題なのですが、ここで算出した$H(A|b_1)$, $H(A|b_2)$に、$B$の結果が$b_1$であるか$b_2$であるか、要するに偶数であるか奇数であるかの確率をかけて期待値を取ると、偶数か奇数かの事前情報がある場合の$A$のエントロピーの期待値$H(A|B)$を次のように求められます。

$$\begin{align}
H(A|B) &= H(A|b_1)P(b_1) + H(A|b_2)P(b_2)\\
&= \frac{1}{2}\log{3} + \frac{1}{2}\log{3} = \log{3} \fallingdotseq 1.58
\end{align}$$

この$H(A|B)$が条件付きエントロピーです。もっと一般の事例について定義し、$P$の式として表記できるよう変形しておくと、次のようになります。

$$\begin{align}
H(A|B) &= \sum_{B} H(A|b)P(b)\\
&= -\sum_{A}\sum_{B} P(a|b)P(b)\log{P(a|b)}\\
&= -\sum_{A}\sum_{B} P(a, b)\log{P(a|b)}.\\
\end{align}$$

結合エントロピーとぱっと見似ていますがよく見ると違いますね。$\log$の中身が結合確率ではなく条件付き確率になっています。

また、重要な性質として、次の大小関係が成り立ちます。

$$H(A|B) \le H(A).$$

各種エントロピー間の関係式

前回の記事も含めて3種類のエントロピーを定義したので、それぞれの間にある関係式を見ていきます。まず紹介するのは $H(A, B) = H(B) + H(A|B)$です。情報理論のエッセンスだと、ベン図みたいなのを描いてイメージで説明されていますが、計算すると次ようになります。

まず定義から次のようになります。

$$H(A, B) = – \sum_{A}\sum_{B}P(a, b)\log{P(a, b)}.$$

ここで、$P(a, b) = P(a | b) P(b)$であることと、$\log$内の積は和に分けれるので、次のようになります。

$$H(A, B) = – \sum_{A}\sum_{B}P(a, b)\log{P(a|b)} – \sum_{A}\sum_{B}P(a, b)\log{P(b)}$$

前半は$H(A|B)$の定義が出てきましたね。後半は、$\sum_{A}P(a, b) = P(b)$であることに注意すると、次のようになります。

$$H(A, B)= H(A| B) – \sum_{B}P(b)\log{P(b)} = H(A|B) + H(B)$$

$a, b$ 入れ替えて、$P(a, b) = P(b | a) P(a)$を使うと、全く同じようにして、$H(A, B) = H(A) + H(B|A)$ も示せます。

以上で、

$$H(A, B) = H(A) + H(B|A) = H(B) + H(A|B)$$

が示せました。これの中辺と右辺に着目して移行すると次も言えます。

$$H(A) – H(A|B) = H(B) – H(B|A).$$

また、左辺と右辺に着目し、$H(A, B)$と$H(A|B)$をそれぞれ移行して両辺に$H(A)$を足すとつ次の式が出てきます。

$$H(A) – H(A|B) = H(A) + H(B) – H(A, B).$$

左辺と中辺に着目して、$H(B|A)$を移行して両辺から$H(A|B)$を引くと、次の式が出てきます。
$$H(A) – H(A|B) = H(A, B) – H(A|B) – H(B|A).$$

以上をまとめると、次の4つの値は違いに等しいと言えます。
$H(A) – H(A|B)$
$H(B) – H(B|A)$
$H(A) + H(B) – H(A, B)$
$H(A, B) – H(A|B) – H(B|A)$

これを相互情報量と呼ぶのですが、長くなってきたので次の記事で詳しく説明します。

まとめ

今回の記事では結合エントロピーと条件付きエントロピーを紹介しました。

結合エントロピーの方は多次元へのシンプルな拡張というイメージでしたが、条件付きエントロピーはすでに何かしらの情報を得てる状況で新たに何かを知ったらどれほどの情報を得られるかを示すものでありなかなか重要な概念です。

また、結合エントロピーの方も重要ではないというわけではありません。実際にプログラム等で値を計算をするときは、条件付きエントロピーを直接算出する関数が主要ライブラリで提供されいていないという事情により、頻繁に算出することになります。

平均情報量(エントロピー)の定義について

はじめに

データサイエンスや機械学習関連の本を読んでいると時々エントロピーという概念に出逢います。例えば、決定木の分類の品質の測定などはそうですね。scikit-learnでは引数でジニ不純度とエントロピー、ログロスの中から選べるので必須でも無いのですが。

個人的な話ですが、このエントロピーは定義がいまいちしっくりこなかったのであまり使ってきませんでした。scikit-learnの決定木もcriterionのデフォルトは”gini”だし、こちらの方が扱いやすく感じていたというのもあります。

ただ、最近平田先生の「情報理論のエッセンス」という本を読みましてこのエントロピーについてだいぶ理解が深まったので軽くまとめておきます。

平均情報量の定義

この記事では離散な事象の場合を取り扱います。最初に平均情報量(エントロピー)の定義を書いておきます。

事象の集合$A=\{a_k\,|\,k=1, 2, \dots, n \}$の各事象$a_k$に対して、その事象が発生する確率を$P(a_k)$とすると、平均情報量$H(A)$は次のように定義されます。

$$H(A) = -\sum_{k=1}^nP(a_k)\log{P(a_k)}.$$

この平均情報量は、式の形が熱力学等のエントロピーと同じなので、情報理論においてもエントロピーと呼ばれています。

この記事では、平均情報量がなぜこのように定義されるのかというのを見ていきます。

熱力学の便利な式を適当に持ってきたわけでもなく、何となく$\log$を使っているわけでもなくきちんと背景があってこのように定義されたのだ、というのが伝われば幸いです。

情報量の数値化について

そもそものモチベーションは情報を数値化することにあります。何かの事象を観測したり、ニュースを聞いたりした時にその結果や内容がどれだけ「予想外」もしくは「驚き」であったかを数値化したものです。

先述の情報理論のエッセンスでは、次の二つのニュースが例として挙げられていました。
(a) 東京に雪が降りました。
(b) 北極に雪が降りました。

(b)の方はありふれた話であるのに対して、(a)の方は珍しいことであって(a)のニュースの方がニュース価値が大きいものであると考えられます。

このニュースとしての価値を定量的に定義しようというのが、そもそもの情報量の目的です。

情報量が満たすべき性質とそれを満たす式

ここから、具体的に事象$x$の情報量$i(x)$について考えていきますが、そのためにこれがどんな性質を持つべきなのか見ていきます。

まず考えておかないといけないのは、受け取り手が得られる情報としての価値にはその人の主観が入り得ることです。東京の天気には一切興味がなく、北極にはすごく関心があるという人にとっては(b)のニュースの方が価値があるかも知れません。これは「主観的立場」による情報の評価ですが、情報理論で扱う情報量はそうではなく、誰でも同じように評価できるようにするために「客観的立場」で情報を評価します。

客観的に評価するために、情報量を定義する際はその内容ではなく、そのニュースが発生する確率を元に価値を評価します。そのため、「情報量は確率の関数である」というのが一つ目の条件です。つまり次のように書けます。

$$i(x) = i(P(x)).$$

そして、起こりにくい事象ほどそれについて知った時の情報価値は大きいということから、$P(a)<P(b)$ならば$i(a) > i(b)$となっていることが求められます。つまり、二つ目の条件は次のようにいえます。

$i(P(x))$は$P(x)$の減少関数である。

この時点では減少関数なんていくらでもあるので情報量がどうとでも定義できてしまいます。

そこで最後に、情報の加法性という性質を要求します。これは独立した二つの情報を知った時に、その情報を同時に知った場合と、順番に知った場合、またその逆順に知った場合で得られる情報の量は同じである、ということです。

例えば、トランプを1枚引いて、以下のように確認した場合に最終的に得られる情報は全部一緒ということです。
– そのカードはスペードの10だった。
– そのカードの数字は10だった。そしてマークはスペードだった。
– そのカードのマークはスペードだった。そして数字は10だった。

スペードだったという情報を$a$、 数字が10だったという情報を$b$とすると、
$i(a\land b) = i(a) + i(b) = i(b) + i(a)$ であって欲しいということです。二つの情報が独立である場合、$P(a, b) = P(a) P(b)$ですから、この性質は次のように書くことができ、これが三つ目の条件です。

$$i(P(x)P(y)) = i(P(x)) + i(P(y)).$$

これを満足する関数を考えると、対数関数しかなくなります。

先に出てる条件で減少関数としないといけませんから、底を$s$として次のようになります。

$$i(x) = i(P(x)) = -\log_s{P(x)}$$

底の$s$としては$2$,$e$,$10$など考えられますが、情報理論においては$s=2$が使われます。ここから底は省略して$2$とします。

コインの裏表やスイッチのON/OFFなど、$1/2$の確率で得られる情報の情報量がちょうど$1$になります。また、6面のサイコロを転がして何か目が出たら、そこから得られる情報量は$\log{6}$となります。

平均情報量の定義

ここまでで、何かニュースを得たり事象を観測したらどの程度の情報が得られるかを定量化することができました。

次に、そのニュースを得る前に、そのニュースを得たらどの程度の情報量が得られるか、の期待値をを考えます。つまり、先述の例で言えば、「東京に雪が降るかどうか」を確認したらどの程度の情報量が得られるのかの期待値を定量化しようという話です。

これは、各事象ごとにその事象が発生する確率と、得られる情報の積をとって足し合わせることで得られます。通常の期待値の定義ですね。つまり平均情報量は次のようになります。

$$\begin{align}H(A) &= \sum_{k=1}^n i(a_k)P(a_k)\\
&=-\sum_{k=1}^n P(a_k)\log {P(a_k)}
\end{align}$$

これでこの記事の最初にあげた定義式が出てきました。

元々$-p\log{p}$ってなんだ?と思ってたのですが、導出を追いかけてみるとこれしか無いって感じる自然な定義ですね。

平均情報量の性質

基本的な性質を一つ紹介しておきます。平均情報量(エントロピー)$H(A)$は次の関係を満たします。

$$0 \le H(a) \le \log{n}.$$

左側の等号は $^{\exists}l \; P(a_l) = 1, k\neq l \Rightarrow P(a_k)=0$ の時に成立します。1種類の結果しか生じ得ない実験をやっても得られる情報量は0であるってことですね。

右側の統合は、$^\forall k P(a_k)=\frac{1}{n}$の時に成立します。これは発生しうる$n$種類の結果が全て等しい確率で起こりうる場合ですね。

平均情報量のもう一つの見方

特にエントロピーと呼ばれる時はそうなのですが、平均情報量は「不確実性の尺度」として理解されることも多くあります。熱力学ではそうですね。

不確実性と得られる情報量では全く逆のものに見えるのですが、これが同一の式で定義されているのは、「得られる情報量」を「不確実性の減少量」と考えられるからです。

ある実験をする前に、この事象にはこれだけ不確実性があるぞ、と評価してるのが熱力学等のエントロピーで、事象を実際に観測してこれだけの情報が得られたと言っているのが情報理論のエントロピーって感じですかね。

まとめ

ここまでで、情報の量を定式化したいというモチベーションから、定式化するとしたらどんな性質を持たないといけないか、そしてどのような式で定義できるかを見てきました。

平均情報量(エントロピー)の定義にについての話は以上になります。次回以降の記事でもう少し発展させて条件付きエントロピーや相互情報量について取り扱っていこうと思います。

PythonでYAMLファイルを読み書きする

自分はあまり使ってこなかったのですが、構造化したデータを保存するのに利用されるYAMLというデータ形式が存在します。いかにもPythonと相性が良さそうなフォーマットです。
参考: YAML – Wikipedia

最近、そこそこ大きいYAMLファイルの総チェックを行う必要があったので、Pythonでスクリプトを書いてチャチャっとやりました。その時のYAMLの読み込み方法のメモと、ついでに逆にデータをYAMLに書き出す方法を残しておきます。

サンプルとしては、次の内容が保存した sample.yaml というファイルを使います。

name: John Doe
age: 30
address:
  city: Tokyo
  country: Japan

必要なライブラリはいくつかありますがPyYAMLが代表的です。インストール時と利用時で名前が違うので注意してください。
参考: PyYAML · PyPI

# インストールコマンド
$ pip install PyYAML

# Pythonでインポートする時
>>> import yaml

それではやっていきましょう。まずはYAMLファイルの読み込みからです。

YAMLを読み込むメソッドは、load(), load_all(), safe_load(), safe_load_all() と4つも用意されています。普通はsafe_load()を使えば良いです。safeがついてない2メソッドにはセキュリティ面のリスクもあるので使用を避けることを推奨します。

import yaml


with open("sample.yaml", "r") as f:
    data = yaml.safe_load(f)

print(data)
# {'name': 'John Doe', 'age': 30, 'address': {'city': 'Tokyo', 'country': 'Japan'}}

簡単ですね。気をつけないといけないのは、safe_load()にファイルパスを渡しても読み込んではくれず、openでファイルを開いて、それを渡すって点です。

YAMLファイルは、”—“というセパレーターを使うことで単一ファイルの中に複数のYAMLオブジェクトを記述できますが、そのようなファイルを読み込むときはsafe_load_all()の方を使うので注意してください。読み込んだ結果はジェネレーターで返ってきます。

ファイルにするが手間だったのでセパレーターで区切られたYAML文字列を直接作ってやって見ました。

yaml_data = yaml.safe_load_all("""name: John Doe
age: 30
---
name: Jane Smith
age: 25
---
name: Bob Johnson
age: 40""".strip())

for y in yaml_data:
    print(y)
"""
{'name': 'John Doe', 'age': 30}
{'name': 'Jane Smith', 'age': 25}
{'name': 'Bob Johnson', 'age': 40}
"""

以上が、YAMLの読み込みです。

次は書き出しです。最初のコード例の、data変数をYAML形式ファイルに保存してみます。

保存には yaml.safe_dump() というメソッドを使います。これにPythonのオブジェクトを渡すとYAMLのテキストに変換してくれます。

print(yaml.safe_dump(data))
"""
address:
  city: Tokyo
  country: Japan
age: 30
name: John Doe
"""

ただし、普通はファイルに保存することになると思いますので、次のように使うことになると思います。

with open("output.yaml", "w") as f:
    yaml.safe_dump(data, f)

これで先ほどのテキストが、”output.yaml”ファイルに保存されます。

YAMLには一般的なブロックスタイルと、JSONに近い見た目のフロースタイルという形式がありますが、defalut_flow_style という引数をTrueにすると、フロースタイルで出力できます。ただ、これは使うことあまり無いかな。

また、インデントの数はindentという引数で指定できます。デフォルトは2ですね。

以上がPythonを用いたYAMLファイルの読み書き方法でした。