MLエンジニアへの道 #24 - GloVe

Last Edited: 10/2/2024

このブログ記事では、NLPにおける分散表現の生成方法であるGloVeについて紹介します。

ML

潜在意味解析

Word2Vecでは、各行に中央の単語のトークン、コンテキスト単語のトークン、およびそのラベルを含むデータセットを作成しますが、 これらの結果を組み合わせて、ウィンドウベースの共起行列を作成することもできます。この行列では、行が中央の単語、列がコンテキスト単語、値が共起カウントです。 次に、この共起行列に対して特異値分解 (SVD) を適用し、単語の分散表現(または低ランク近似)を抽出します。 この技術は 潜在意味解析 (LSA) と呼ばれ、単語の類似性を捉えることができます。

X=UΣVT X = U \Sigma V^T

ここで、XXは共起行列、UUは左特異ベクトル(単語ベクトル)、Σ\Sigma は特異値、そしてVVは右特異ベクトル(コンテキスト単語ベクトル)です。 UUVVはどちらも直交しており、Σ\SigmaXXのランクと同じランクを持つ対角行列です。(SVD の詳細については、ritvikmath のSingular Value Decomposition : Data Science Basicsをご覧ください。) UUを得るために、次のようにXTX^Tを掛けることができます。

XXT=UΣVTVΣUT=UΣ2U1 XX^T = U \Sigma V^TV \Sigma U^T = U \Sigma^2 U^{-1}

VVは直交しており、Σ\Sigmaは対角行列であるため、VVTVV^Tは単位行列になりΣΣ\Sigma\SigmaΣ2\Sigma^2になります。 したがって、UUΣ\SigmaXXTXX^Tの固有値分解における固有ベクトルおよび固有値の平方根に相当します。 (固有値分解の詳細については、ritvikmath の Eigendecomposition : Data Science Basicsをご覧ください。) LSA はニューラルネットワークのトレーニングを必要とせず、グローバルな統計情報を利用して、単語の文法的・意味的な類似性を効率的に捉える分散表現を作成します。 しかし、この方法は高頻度の単語に過度に重点を置くことがあり、Word2Vec が成功して捉えるその他の特性を捉えることができないこともあります。

GloVe

GloVe は、ウィンドウベースの共起行列の行列分解を行うことで、Word2Vec の微妙な意味を捉える能力と、LSA のグローバル統計の効率的な使用を組み合わせることを目的としています。 行列分解は、元の行列を行と列の小さい行列の積として近似することで、行と列の潜在表現を低次元で捉えます。

Xm,n=Um,pVn,pT X_{m,n} = U_{m,p}V_{n,p}^T

行列分解は通常、勾配降下法のような学習アルゴリズムを使用して、UUVVの最適な潜在表現を学習します。 この技術は推薦システムでもよく取り上げられます。(推薦システムについては、後のシリーズで詳しく取り上げる予定です) GloVe では、ウィンドウベースの共起行列に対して行列分解を行い、分散表現U+VU+Vを得ます。 (この場合、UUVVはどちらも同じ形状を持ち、単語を潜在空間で表現しているため、それらを足し合わせることで良好な分散表現が得られることが実験的に示されています。)

J(θ)=12i,j=1Wf(Xi,j)(uiTvjlog(Xi,j))2f(X)={(Xi,jXmax)αif Xi,j<Xmax1otherwise J(\theta) = \frac{1}{2} \sum_{i,j=1}^{W} f(X_{i,j}) (u_i^Tv_j - log(X_{i,j}))^2 \\ f(X) = \begin{cases} (\frac{X_{i, j}}{X_{max}})^\alpha &\text{if } X_{i,j} < X_{max} \\ 1 &\text{otherwise} \end{cases}

上記は、行列分解に使用する目的関数です。少し複雑に見えますが、実際はシンプルです。 単語ベクトルとコンテキスト単語ベクトルの積(uTvju^Tv_j) を共起カウントの対数 log(Xi,j)log(X_{i,j})に近似しようとしています。 この際、各単語ペアWWに対して二乗誤差を用いますが、低頻度および高頻度の単語を扱うために重みf(Xi,j)f(X_{i,j})を適用します。

コードの実装

GloVe を実装するには、ウィンドウベースの共起行列を作成する必要があります。これを効率的に行うために、以下の方法を使用できます。 (テキストの前処理やトークナイゼーションなど、以前の手順は前の記事で使用したものと同様です。)

def create_cooccurrence_matrix(tokenized_corpus, window_size=5):
    co_occurrence = defaultdict(float)
    for i, word in enumerate(tokenized_corpus):
        start = max(0, i - window_size)
        end = min(len(tokenized_corpus), i + window_size + 1)
        for j in range(start, end):
            if i != j:
                context_word = tokenized_corpus[j]
                co_occurrence[(word, context_word)] += 1
 
    return co_occurrence

共起行列から、以下のようにトレーニングデータを作成できます。

def create_training_data(co_occurrence):
    words = []
    contexts = []
    counts = []
    
    for (word, context_word), count in co_occurrence.items():
        words.append(word)
        contexts.append(context_word)
        counts.append(count)
    
    return np.array(words), np.array(contexts), np.array(counts, dtype=np.float32)

(TensorFlow や PyTorch のデータセット作成については、前の記事で説明しているため、ここでは省略します。)その後、上記の目的関数を使用して GloVe モデルを構築し、トレーニングを行います。以下は GloVe の TensorFlow 実装です。

class GloVe(tf.keras.Model):
  def __init__(self, vocab_size, embedding_dim):
    super(GloVe, self).__init__()
    self.word_embedding = layers.Embedding(vocab_size,
                                      embedding_dim,
                                      embeddings_initializer="glorot_normal",
                                      embeddings_regularizer="l2",)
    self.context_embedding = layers.Embedding(vocab_size,
                                       embedding_dim,
                                       embeddings_initializer="glorot_normal",
                                       embeddings_regularizer="l2",)
 
  def call(self, pair):
    word, context = pair
    word_emb = self.word_embedding(word)
    context_emb = self.context_embedding(context)
    dots = tf.reduce_sum(word_emb * context_emb, axis=-1)
    return dots
 
def custom_loss(y_pred, y_true):
      y_true = tf.clip_by_value(y_true, clip_value_min=1e-5, clip_value_max=100)
      f = y_true / 100
      log_y_true = tf.math.log(y_true)
      return 0.5 * f * tf.math.square(y_pred - log_y_true)
 
embedding_dim = 1024
vocab_size = len(tokens)
glove = GloVe(vocab_size, embedding_dim)
glove.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
                 loss=custom_loss)
glove.fit(dataset, epochs=15)

グローバル統計を使用しているため、トレーニング時間が大幅に短縮されることに気づくかもしれません。 一方で、GloVe によって生成された分散表現は、Word2Vec から生成されたものと同じくらい表現力があることが分かっています。 チャレンジとして、分散表現とコンテキスト分散表現を加算する機能を追加し、上記のモデルを PyTorch で実装してみてください。

結論

この記事では、Word2Vec の代替手法としての潜在意味解析 (LSA) を取り上げ、そのメリットとデメリットについて説明しました。 これにより、GloVe の動機が理解できました。また、FastText のような他の代替手法もあるため、興味があればチェックしてみてください。 これで、単語埋め込みができたので、次は言語モデルの構築に進む準備が整いました。

リソース