All-but-the-top: 単語分散表現の上位主成分がノイズな件

Skip-gramやGloVeで学習した単語分散表現に簡単な後処理を施すことで後段タスクの性能を向上させる手法が提案されている。

この論文。論文タイトルが提案手法を一言で表現していて洒落ている。

All-but-the-Top: Simple and Effective Postprocessing for Word Representations

arxiv.org

提案手法を一言でいうと,単語分散表現行列を主成分分析して上位主成分を取り除くという超シンプルな方法。 なぜ性能が上がるのかというのは直感的にはよくわからないが,どうも数理的な背景があるらしい。

先行研究において,理想的には任意の単位ベクトルと単語分散表現の内積から計算されるある量(確率モデルの分配関数)が漸近的に定数であるという主張があるが, 実際に学習されたword2vecやGloVeのoff the shelf なモデルはそうなっておらず,本研究でそうなるような後処理をしてみると性能が上がったという話。

もう少し背景を話すと,GloVeやSkip-gramの確率モデルは単語ベクトルとその文脈語の内積をエネルギー関数としてモデル化される。 共起する単語は内積を大きく,すなわち確率を大きくしていくというモデル。 確率モデルの分母の分配関数Z(c)は学習時に明示的に正規化しない(する必要がない)。

\begin{align} Z(c) = \sum_{w \in V} \exp(c \cdot v(w)) \end{align}

ただ,正規化されているほうが望ましいということのようだ。これを自己正規化というらしい。 要するにこの研究ではそのような自己正規化を誘導するような後処理手法を導出したというお話。 自己正規化具合いを定量評価する指標として,本研究では以下のIsotropyという量を定義している。

\begin{align} I(\{v(w)\}) = \frac{\min_{\|c\|=1} Z(c)}{\max_{\|c\|=1} Z(c)} \end{align}

ここで\(c\)は任意の単位ベクトルである。\(Z(c)\)が\(c\)によらず一定であれば良いのだからIsotropyが1に近いほど良いということになる。 つまり,すでに存在している単語ベクトル空間に対してIsotropyが1になるような後処理を施してやるという戦略である。

のちの実験ではランダム単位ベクトルに対して\(Z(c)\)の分布の形状を後処理前後で比較する(論文中のTable 2, Figure 3)。

ともかく実装してみた。

実装

  • wordembeddings.py

GloVeのモデルをロードするクラス

import numpy as np
from tqdm import tqdm

class WordEmbeddings():

    def __init__(self, path='glove.6B.100d.txt'):
        self.word2vec = {}
        with open(path) as f:
            for line in tqdm(f):
                values = line.split()
                word = values[0]
                vec = np.asarray(values[1:], dtype='float64')
                self.word2vec[word] = vec

    def get_embeddings_matrix(self):
        embeddings_matrix = np.stack(list(self.word2vec.values()))
        return embeddings_matrix
  • 単位超球上の一様分布

こちらから拝借しました。ありがとうございます。

def sample_spherical(npoints, ndim):
    vec = np.random.randn(ndim, npoints)
    vec /= np.linalg.norm(vec, axis=0)
    return vec.T # (npoint, dimen)
  • All-but-the-top の本体
def abtt():

    dimen = 50 # dimensions for Word Embeddings
    D = 2 # number of the removed directions

    c = sample_spherical(npoints=100, ndim=dimen) # random unit vectors

    wordemb = wordembeddings.WordEmbeddings(path='glove.6B.{}d.txt'.format(dimen))
    V = wordemb.get_embeddings_matrix()

    Z_before = (np.exp(c @ V.T)).mean(axis=1)
    print('Isotropy(before) : ', np.abs(Z_before.min()/Z_before.max()))

    mu = np.mean(V, axis=0)
    V_tilde = V - mu # centering

    pca = PCA(n_components=D)
    pca.fit(V_tilde)

    coef = pca.components_ @ V_tilde.T
    diff = coef.T @ pca.components_
    V_prime = V_tilde - diff
    Z_af = (np.exp(c @ V_prime.T)).mean(axis=1)
    print('Isotropy(after) : ', np.abs(Z_af.min()/Z_af.max()))

    lower = np.concatenate([Z_before, Z_af]).min() - 0.2
    upper = np.concatenate([Z_before, Z_af]).max() + 0.2
    bins = np.linspace(lower, upper, 50)
    plt.hist(Z_before, bins, alpha = 0.5, label='before')
    plt.hist(Z_af, bins, alpha = 0.5, label='after')
    plt.legend()
    plt.xlabel('Z(c)')
    plt.ylabel('frequency')
    # plt.savefig('abtt.png', bbox_inches='tight')
    # plt.show()

実験

ここでは前処理前後でのIsotropyの変化をみる。論文中のTable 2, Figure 3に対応する。使った分散表現のモデルはここから入手できるGloVeの最も小さいモデル(6B.zipの50次元)。

論文で使用されたGloVeは大きいモデル(ファイルサイズ数GB)だが, 傾向を観るだけなら小さいモデルでも十分であったためそうした。

結果

  • Isotropyの値(論文中のTable2に相当)
    • Isotropy(before) : 0.36
    • Isotropy(after) : 0.92

前処理前後で1に漸近していることがわかる。

  • Z(c)の分布(論文中のFigure 3に相当)

前処理前後で\(Z(c)\)が\(c\)によらず定数になっていることがわかる。概ね傾向としては論文と同様。 これはIsotropy→1を意味する。

私からは以上です。