ナード戦隊データマン

機械学習と自然言語処理についてのブログ

単語グラフから単語Embeddingを生成する

前回の記事( https://datanerd.hateblo.jp/entry/2019/12/13/123857 )で単語の共起関係からグラフを作成しました。今回は、そこで用いた共起関係を用いてEmbeddingを作成します。

github.com

考えた方法

単語共起グラフをword2vecのようなベクトルに変換できないかと考えました。 単語の共起グラフに保存されている情報は以下です:

  1. 親語
  2. 子語
  3. 親語に対して小語が共起した頻度。

親語ベクトルは子語ベクトルを用いて更新することができると考えます。

parent(t+1) = parent(t)×alpha + sum(child×prob for child, prob in children(t))×(1-alpha)

probは頻度のカウントを率に変換したものです。

コード

共起語の生成

$ git clone https://github.com/sugiyamath/wordgraph
$ cd wordgraph
$ python3 data_builder.py ./data/data.txt.gz db.pkl

Embeddingの訓練

$ cd wordgraph
$ python3 create_emb.py

不正なEmbeddingの修正

$ cd wordgraph
$ python3 fix_emb.py

Embeddingのテスト

$ cd wordgraph
$ python3 test_emb.py ダルビッシュ
$ python3 test_emb.py 羽生

[ダルビッシュ出力]

[('先発登板', 0.9852018356323242), ('マニー・ラミレス', 0.9833218455314636), ('ジェフ・バニスター', 0.9828331470489502), ('負け投手', 0.9826960563659668), ('トミー・ジョン', 0.9826253056526184), ('左腕', 0.982460618019104), ('送球', 0.9824425578117371), ('球団側', 0.9821796417236328), ('テキサス・レンジャーズ', 0.982058584690094), ('史上', 0.9815831184387207)]

[羽生出力]

[('羽生結弦', 0.987086296081543), ('世界フィギュアスケート選手権', 0.9820781946182251), ('散歩道', 0.9819827079772949), ('男子シングル', 0.9816972017288208), ('北海道札幌市', 0.9809283018112183), ('ミハイル・コリヤダ', 0.980816125869751), ('ゲイリー・ムーア', 0.9808042049407959), ('NHK杯', 0.9804912209510803), ('トゥクタミシェワ', 0.9797825217247009), ('世界フィギュアスケート国別対抗戦', 0.9795533418655396)]

主要コード

import numpy as np
import pickle
from tqdm import tqdm

class WordGraphEmbBuilder:
    def __init__(self, dbfile="db.pkl", dim=100, alpha=0.3):
        with open(dbfile, "rb") as f:
            self._db = pickle.load(f)
        self._dim = dim
        self._init_emb()
        self._alpha = alpha

    def save(self, outfile="emb.txt"):
        with open(outfile, "w") as f:
            f.write("{} {}\n".format(len(self.emb), self._dim))
            for word, vector in self.emb.items():
                f.write(
                    ' '.join([word] + [str(x) for x in vector.tolist()]) + "\n"
                )
        
    def train(self, epochs=5):
        for _ in tqdm(range(epochs)):
            self._update_emb()

    def _update_emb(self):
        for word1, items in tqdm(self._db.items()):
            v1 = self.emb[word1]
            v2 = np.sum(
                [self.emb[word2]*prob for word2, prob in items.items()],
                axis=0)
            self.emb[word1] = v1*self._alpha + v2*(1-self._alpha)
                
        
    def _init_emb(self):
        self.emb = {}
        for word1, items in self._db.items():
            items = list(items.items())
            total = sum(x[1] for x in items)
            if total == 0:
                self._db.pop(word1, None)
                continue
            if word1 not in self.emb:
                self.emb[word1] = np.random.uniform(-1, 1, self._dim)
            for word2, count in items:
                if word2 not in self.emb:
                    self.emb[word2] = np.random.uniform(-1, 1, self._dim)
                self._db[word1][word2] = float(count)/float(total) + 0.000001


if __name__ == "__main__":
    builder = WordGraphEmbBuilder()
    builder.train()
    builder.save()

考察

完全に思いつきで試してみたのですが、うまくいったようです。

共起グラフを使うというやり方は空間効率が悪い気はします。今回使ったコーパスは小規模なので訓練はOOMが発生せずに完了させられましたが、巨大コーパスで実行する場合は空間効率のよいやり方を考える必要があります。

今回の場合、着目するのは名詞だけだったので、類似度をとると「共起しやすい語」が評価されますが、「同一文脈で使われる語」が評価されるかは確かではありません。

リンク