からっぽのしょこ

読んだら書く!書いたら読む!読書読読書読書♪同じ事は二度調べ(たく)ない

2.4.1:相互情報量【ゼロつく2のノート(実装)】

はじめに

 『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 本の内容を1つずつ確認しながらゆっくりと組んでいきます。

 この記事は、2.4.1項「相互情報量」の内容です。相互情報量を説明して、Pythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

2.4.1 相互情報量

 単語がテキストに出現する頻度そのままではなく、情報量という概念を導入します。情報量とは確率を基にした値です。詳しくは1.1.8-10:カルバック・ライブラー・ダイバージェンスとイェンゼンの不等式【『トピックモデル』の勉強ノート】 - からっぽのしょこを参照してください。

 2つの単語$x,\ y$の出現確率をそれぞれ$P(x),\ P(y)$とします。$x$の出現確率は

$$ P(x) = \frac{C(x)}{N} $$

で計算します。ここで$C(x)$は単語$x$が(テキストではなく)コーパス(共起行列)にカウントされた回数、$N$はコーパスの総単語数とします。
 また単語$x,\ y$が共起(ウィンドウサイズ内で続けて出現)した回数を$C(x, y)$とします。これは前節の共起行列の各要素に対応します。そして2つの単語の共起確率を

$$ P(x, y) = \frac{C(x, y)}{N} $$

とします。

 これを用いて、単語$x,\ y$の相互情報量(PMI)は次のように定義されます。

$$ \mathrm{PMI}(x, y) = \log_2 \frac{ P(x, y) }{ P(x) P(y) } \tag{2.2} $$

 またこの式は、次のように変形することで

$$ \begin{align} \mathrm{PMI}(x, y) &= \log_2 \left( P(x, y) \frac{1}{P(x)} \frac{1}{P(y)} \right) \\ &= \log_2 \left( \frac{C(x, y)}{N} \frac{N}{C(x)} \frac{N}{C(y)} \right) \\ &= \log_2 \frac{ C(x, y) N }{ C(x) C(y) } \tag{2.3} \end{align} $$

出現回数と総単語数から直接で計算できることが分かります。

 ただし出現回数が0のときに不都合が生じるため、次の正の相互情報量(PPMI)を用います。

$$ \mathrm{PPMI}(x, y) = \max(0, \mathrm{PMI}(x, y)) \tag{2.6} $$

 ここで$\max(x, y)$は引数の最大値を返す関数です。$\max(0, x)$のときReLU関数と同じ働きをします。

・処理の確認

 まずは、前節で実装した関数を用いて共起行列を作成します。

# テキストを設定
text = 'You say goodbye and I say hello.'

# 単語と単語IDに関する変数を取得
corpus, word_to_id, id_to_word = preprocess(text)

# 単語の種類数を取得
vocab_size = len(word_to_id)

# 共起行列を作成
C = create_co_matrix(corpus, vocab_size, window_size=1)
print(C)
[[0 1 0 0 0 0 0]
 [1 0 1 0 1 1 0]
 [0 1 0 1 0 0 0]
 [0 0 1 0 1 0 0]
 [0 1 0 1 0 0 0]
 [0 1 0 0 0 0 1]
 [0 0 0 0 0 1 0]]

 全ての単語の組み合わせの共起回数をまとめた行列を作成できました。
 共起行列の$i, j$要素C[i, j]が、2つの単語$i,\ j$の共起回数$C(i, j)$に対応します。

 コーパスCの全ての和が総単語、各行の和が各単語の出現回数になります。

# 総単語数
N = np.sum(C)
print(N)

# 各単語の出現回数
S = np.sum(C, axis=0)
print(S)
14
[1 4 2 2 2 2 1]

 単語$i$の出現回数$C(i)$は、S[i]の値です。

 定義式に従い正の相互情報量を計算します。

# PPMI行列の受け皿を作成
M = np.zeros_like(C, dtype=np.float32)

# 単語を指定
i, j = 2, 1

# PMIを計算:式(2.3)
pmi = np.log2(C[i, j] * N / (S[j] * S[i]) + 1e-8)

# PPMIを計算:式(2.6)
M[i, j] = max(0, pmi)
print(M)
[[0.        0.        0.        0.        0.        0.        0.       ]
 [0.        0.        0.        0.        0.        0.        0.       ]
 [0.        0.8073549 0.        0.        0.        0.        0.       ]
 [0.        0.        0.        0.        0.        0.        0.       ]
 [0.        0.        0.        0.        0.        0.        0.       ]
 [0.        0.        0.        0.        0.        0.        0.       ]
 [0.        0.        0.        0.        0.        0.        0.       ]]

 全ての単語の組み合わせでこの処理を繰り返し行います。

 ただし$\log_2 0 = - \infty$となり計算できなくなるのを回避するために、微小な値を加えておきます。

# 0のとき
print(np.log2(0))

# 微小な値を指定
eps = 1e-8

# 微小な値を加えて計算
print(np.log2(0 + eps))
-inf
-26.575424759098897

 ちなみにepsは、$\epsilon$(イプシロン)のことです。

・実装

 処理の確認ができたので、関数として実装します。

# 相互情報量行列の作成関数の実装
def ppmi(C, verbose=False, eps=1e-8):
    
    # PPMI行列の受け皿を作成
    M = np.zeros_like(C, dtype=np.float32)
    
    # PPMIに用いる値を計算
    N = np.sum(C) # 総単語数
    S = np.sum(C, axis=0) # 各単語の出現回数
    
    # 進行状況確認用の値を計算
    total = C.shape[0] * C.shape[1]
    cnt = 0 # 処理回数を初期化
    
    # 1語ずつ正の相互情報量を計算
    for i in range(C.shape[0]): # 各行
        for j in range(C.shape[1]): # 各列
            
            # PPMIを計算
            pmi = np.log2(C[i, j] * N / (S[j] * S[i]) + eps) # 式(2.3)
            M[i, j] = max(0, pmi) # 式(2.6)
            
            # 進行状況を表示
            if verbose: # `verbose=True`のとき
                cnt += 1 # カウントアップ
                if cnt % (total // 100 + 1) == 0: # いい感じに表示回数を間引く
                    print('%.1f%% done' % (100 * cnt / total))
    
    return M

 %//はどちらも割り算に関する算術演算子で、%は余りを//は整数部分を返します。つまりtotalが100未満であれば1回ごとに、100以上200未満であれば2回ごとに、200以上300未満であれば3回ごとに進行具合を表示します。

 実装した関数を試してみましょう。

# 正の相互情報量を計算
W = ppmi(C, verbose=True)
2.0% done
4.1% done
6.1% done
(省略)
98.0% done
100.0% done


 共起行列とPPMI行列を見比べてみます。

# 共起行列
print(C)

# 正の相互情報量行列
print(np.round(W, 2))
[[0 1 0 0 0 0 0]
 [1 0 1 0 1 1 0]
 [0 1 0 1 0 0 0]
 [0 0 1 0 1 0 0]
 [0 1 0 1 0 0 0]
 [0 1 0 0 0 0 1]
 [0 0 0 0 0 1 0]]
[[0.   1.81 0.   0.   0.   0.   0.  ]
 [1.81 0.   0.81 0.   0.81 0.81 0.  ]
 [0.   0.81 0.   1.81 0.   0.   0.  ]
 [0.   0.   1.81 0.   1.81 0.   0.  ]
 [0.   0.81 0.   1.81 0.   0.   0.  ]
 [0.   0.81 0.   0.   0.   0.   2.81]
 [0.   0.   0.   0.   0.   2.81 0.  ]]

 共起行列をPPMI行列に変換できました。

 この単語ベクトルの変換によるコサイン類似度の変化を確認しましょう。

# クエリを指定
query = 'you'

# 共起行列を用いた類似度の上位単語を表示
most_similar(query, word_to_id, id_to_word, C, top=5)

# PPMIを用いた類似度の上位単語を表示
most_similar(query, word_to_id, id_to_word, W, top=5)
[query] you
 goodbye: 0.7071067691154799
 i: 0.7071067691154799
 hello: 0.7071067691154799
 say: 0.0
 and: 0.0

[query] you
 goodbye: 0.40786147117614746
 i: 0.40786147117614746
 hello: 0.2763834297657013
 say: 0.0
 and: 0.0

 「you」は「i(I)」との類似度が高いことが予想されます。しかし(少ないテキストの)共起回数を用いて求めたコサイン類似度では、「goodbye」「i」「hello」が同一の類似度となりました。相互情報量を用いてコサイン類似度を求めることで、「i」と「hello」で類似度に差が生じました。こちらの方が直感的な類似度に近いことがうかがえます。

 しかしどちらの単語ベクトルも多くの要素が0です。0という情報量が無い情報を用いるのは効率的ではありません。次項では、疎なベクトルから重要な情報を抽出することを考えます。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.

おわりに

 KL情報量に関することは勉強したけど、他の情報量についても整理して勉強したい気持ちを抑えて先へ。

【次節の内容】