word2vec(Continuous Bag-of-wordsとSkip-gram)とfasttextによる単語のベクトル表現

書いてる理由

  • NLPやるぞー

参考

pytorchによる発展ディープラーニング

ソース

github.com

概要

1個前で学習用のデータからボキャブラリーを作成し、そのボキャブラリーの単語にindex番号を振ることで単語を数値化して、それを集めて文章を数値化することを書いた。
ただこのやり方は2つの課題がある。 1つ目は、単語同士の関係性を表現できていないこと。
例えば"男"と"男性"は似ている単語である、とか、"アメリカ"と"日本"は国というラベルで意味が近い、などがindex番号には表現されていない。
単語の意味の表現ができると、類似しているとか同じグループであるとかの解析の醍醐味が利用できる様になるため、これが欲しくなる。
2つ目は、データ量が膨大になること。
解析時、one-hot表現[データのある点だけが1、その他は全て0]で扱うことが多く、少量のボキャブラリーなら良いが、例えば10万のボキャブラリーがあると「私は歩く」という文章が(3, 100000)というデータ量になってしまう。(私, は, 歩くをそれぞれ10万次元で表現)
デカすぎる。。計算時間がやばい上に、ほとんど中身が0のため勿体無い。

これらの問題点を解決するため、単語を数百次元くらいのベクトルで扱うのが今回の趣旨。

詳細

単語同士の関係性の表現について

問題点1つ目の単語同士の関係性についてもう少し説明。
例えば、男性、女性、男、ハエ、ペンギンを以下のベクトルで表現したとする。

"男性" = [0.7, 0.9, 0.0]
"女性" = [0.2, 0.9, 0.0]
"男" = [0.9, 0.6, 0.0]
"ハエ" = [0.5, 0.2, 0.9]
"ペンギン" = [0.3, 0.3, 0.7]

第一成分に着目すると、"男性"と"男"のが高く、"女性"は低い、ことから性別に関係する?
第二成分は、"男性"、"女性"、"男"は高めで、"ハエ"、"ペンギン"は低い、ことから人間に関係する?
第三成分は、"ハエ"と"ペンギン"が高くて、飛べることに関係する?
とか、実際に各ベクトル成分に正確なものはなく、ラベルを与えるとするとそれを解釈する人間次第だが、単語ごとの距離みたいなものを表現できる。
これがしたい。

やり方

大量の文章を集めて、ベクトル表現を作成する方法が2つある。
1. 前後の単語から、注目している単語を予測するモデルを作り、その中間層の変換により単語をベクトル表現する。[CBOW]
2. 注目している単語から、前後の単語を予測するモデルを作り、その中間層の変換により単語をベクトル表現する。[skip-gram]

どういうことかと言うと、これは共通して「ある単語は、その単語が利用される前後の単語で表現ができる」と言う仮定に基づいた考え方になっている。
まだ良くわからんと思うけど、具体的には、以下の様な文章があったとする。

今日の天気は曇りで温度は22度で過ごしやすい。
明後日のイベントは雨で温度が低いみたいだよ。

上の文章の"曇り"とか"雨"は天候に関する単語で、天候に関する単語の前後には似た様な単語が来やすい。
天候だけでなく、国に関することとか、政治に関することとかなんでもいいけど、意味が似た単語は前後で使われる単語も同じ様な感じになる傾向がある。
そのため、前後の単語を使って注目したい単語を予測する様なモデルを作り、モデルの変換パラメータをその単語のベクトルとして使うと、似た意味の単語が近いベクトルが得られるでしょうという考え方。

例えば、今適当にニュースから持って来た文章「世界規模であらゆるスポーツの選手が十分に練習できない環境にあるため、最高の競技レベルを維持するのが困難になっている」を使って"選手"という単語を予測したいとすると、
分かち書きして、"世界", "規模", "で", "あらゆる", "スポーツ", "の", "選手", "が", "十分", "に", "練習", "でき", ない", "環境", "に", "ある", "ため", "、", "最高", "の", "競技", "レベル", "を", "維持する", "の", "が", "困難", "に", "なって", "いる"と分割。
注目単語の前後5つとかの数字を固定で決めて、ボキャブラリー(単語の集合)を作っておき、出現した単語の番号を1を立てる様な形でinputを作成する。outputは注目している単語だけが1が立ってるベクトル。以下のイメージ。

f:id:raishi12:20200322031042p:plain
CBOWの入力と出力イメージ

こん感じで、簡単に全結合層を二つくっつけただけのモデルで大量に学習させると、
前後5つの単語で注目している単語を予測できる様な変換ができるパラメータを得ることができる。
この時の全結合層の1つ目が、ボキャブラリー次元を300次元に圧縮している動作であると捉えることができる。
そのため、ベクトル化したい単語の前後5つを1立てたinputを入れて得られた300次元をその単語のベクトルとして利用する。わけよ。

これがCBOWでやっていること。

skip-gramはこの逆で、inputが注目単語だけ1が立っているデータで、outputが前後5つの単語が1が立っているデータで学習する。
これにより、ボキャブラリーの次元数が、中間層の次元数に圧縮でき、それは前後5単語を予測するのに最適なパラメータでの変換がされたものであるため、これを単語のベクトルとして扱う。

CBOWとskip-gramはどちらも単語を前後の単語を使ってベクトル表現する方法で、前後の単語から注目単語を当てるタスクを学習しているのか、注目単語から前後の単語を予測するタスクを学習しているかだけが異なる。
どっちが良いかというと、skip-gramの方が良いらしい。1単語から前後の単語を当てる方がタスク的に難しいため、これを学習しているモデルが凄そう?という感覚的な話と、前後がなかったらベクトル表現が片方paddingだらけになるのできつそうという俺の勝手な想像。明確な理由は不明。
ここまでがword2vecで単語をベクトル表現するための2つのやり方の話。

word2vecとは別に、fasttextというやり方がある。
fasttextは未知語に対応することを考慮されて提案されたもので、学習時に単語をサブワードに分割して学習されるところがword2vecと異なる。
例えば、"サファリパーク"という単語があったら、これを"サファリ"、"サファリパ"、"サファリパー"と一文字づつ増やしながらこれも学習する。
これが何故未知語の対応になるかというと、未知語であってもその中にサブサードが学習されていれば、それなりのベクトルが得られるであろうといイメージ。
例えば"サファリマウス"という新しいアニメのキャラクターが出てきて、これがボキャブラリーに存在しない未知語だったとしても、サファリなどのサブワードでサファリパークの前後の単語を当てる学習がされているため、サファリパークと近しいのでは的なベクトルが未知の単語だけど得られるという感じ。

ここまでが概要の説明。以下でこれらのコードを記述する。

コードの説明

学習済みのskip-gramのword2vecのモデルの準備

世の中便利で、word2vecのskip-gramの学習済みモデルが公開されているのでそれを使って確認。
リンクの20170201.tar.bz2を解凍すると、entity_vectorというディレクトリの配下にentity_vector.model.binというファイルがあり、それを使って以下のpythonコードで実行すると、japanese_word2vec_vectors.vecというモデルファイルを取得できる。

def save_word2vec_model():
    # http://www.cl.ecei.tohoku.ac.jp/~m-suzuki/jawiki_vector/data/20170201.tar.bz2を解凍して得たbinファイル。
    from gensim.models import KeyedVectors
    model = KeyedVectors.load_word2vec_format('entity_vector.model.bin', binary=True)
    model.wv.save_word2vec_format('../data/japanese_word2vec_vectors.vec')

skip-gramのword2vecを用いたデータの確認

TEXT.build_vocab(train_ds, vectors=japanese_word2vec_vectors, min_freq=1)はモデルファイルをtorchtext.vocab.Vectorsに渡して前回作成したdatasetでボキャブラリーを作成する。
TEXT.vocab.vectorsは、text_train.tsvには51個の単語があったため、51の単語が200次元で表現されている。(この学習済みモデルの中間層が200次元だった。)
TEXT.vocab.stoiボキャブラリーの単語のIDが確認できる。

defaultdict(<bound method Vocab._default_unk_index of <torchtext.vocab.Vocab object at 0x1250ba358>>, 
{'<unk>': 0, '<pad>': 1, 'と': 2, '': 3, 'eos': 4, '。': 5, 'な': 6, 'の': 7, '文章': 8, '、': 9, 'が': 10, 'し': 11, 'を': 12, 'いる': 13, 'か': 14, 'て': 15, 'ます': 16, '分類': 17, '本章': 18, '評価': 19, '0': 20, 
'い': 21, 'から': 22, 'する': 23, 'その': 24, 'た': 25, 'で': 26, 'です': 27, 'に': 28, 'に対して': 29, 'は': 30, 'まし': 31, 'クラス': 32, 'ネガティブ': 33, 'ポジティブ': 34, 'モデル': 35, 'レビュー': 36, 
'値': 37, '取り組み': 38, '商品': 39, '女性': 40, '女王': 41, '好き': 42, '姫': 43, '構築': 44, '機械学習': 45, '王': 46, '王子': 47, '男性': 48, '短い': 49, '自然言語処理': 50})
from torchtext.vocab import Vectors

    # word2vecでの単語のベクトル化
    japanese_word2vec_vectors = Vectors(name='data/entity_vector/'
                                             'japanese_word2vec_vectors.vec')

    print('1単語を表現する次元数:{}'.format(japanese_word2vec_vectors.dim))  # 200次元
    print('単語数:{}'.format(len(japanese_word2vec_vectors.itos)))        # 1015474個

    TEXT.build_vocab(train_ds, vectors=japanese_word2vec_vectors, min_freq=1)
    print(TEXT.vocab.vectors.shape)  # torch.Size([51, 200])
    print(TEXT.vocab.stoi)

単語のベクトルを使って単語同士の加減算を確認

こっからが単語のベクトル表現の真骨頂で、冒頭で説明した通り単語をベクトル表現する理由の一つは単語同士の関係性を持たせることだった。
今得ている単語のベクトルはこの関係性を持っている。

例えば、ボキャブラリーの中の姫{43}から女性{40}を引いて男性{48}を足したベクトルを作る。
意味合い的には、姫の男版なので王とか王子に近くなりそう。
これをコサイン類似度を使って距離を測ってみる。
コサイン類似度はtorch.nn.functional.cosine_similarity(x, y, dim=n) n: n次元目のベクトルで計算 でサクッとできるので確認。

    import torch.nn.functional as F
    tensor_calc = TEXT.vocab.vectors[43] - TEXT.vocab.vectors[40] + TEXT.vocab.vectors[48]  # 姫 - 女性 + 男性
    print('女王と計算したベクトルとのコサイン類似度:{}'.format(F.cosine_similarity(tensor_calc, TEXT.vocab.vectors[41], dim=0)))
    print('王と計算したベクトルとのコサイン類似度:{}'.format(F.cosine_similarity(tensor_calc, TEXT.vocab.vectors[46], dim=0)))
    print('王子と計算したベクトルとのコサイン類似度:{}'.format(F.cosine_similarity(tensor_calc, TEXT.vocab.vectors[47], dim=0)))
    print('機械学習と計算したベクトルとのコサイン類似度:{}'.format(F.cosine_similarity(tensor_calc, TEXT.vocab.vectors[45], dim=0)))

> 女王と計算したベクトルとのコサイン類似度:0.3839624524116516
> 王と計算したベクトルとのコサイン類似度:0.3668966591358185
> 王子と計算したベクトルとのコサイン類似度:0.5488658547401428
> 機械学習と計算したベクトルとのコサイン類似度:-0.14043690264225006

王子が一番類似度が近く、王や女王も近い感じになっている。一方、関係ない機械学習との距離は遠くなっていることがわかる。

fasttextでやってみる

fasttextもほぼ一緒。Vectorsにfasttext用の学習済みモデルを読み込む。
ボキャブラリーを作成する。
中身確認して加減算して確認。

    # fasttextでの単語のベクトル化
    japanese_fasttext_vectors = Vectors(name='data/fasttext_model.vec')

    print('1単語を表現する次元数:{}'.format(japanese_fasttext_vectors.dim))  # 300
    print('単語数:{}'.format(len(japanese_fasttext_vectors.itos)))

    TEXT.build_vocab(train_ds, vectors=japanese_fasttext_vectors, min_freq=1)
    print(TEXT.vocab.vectors.shape)  # torch.Size([51, 300])
    print(TEXT.vocab.stoi)

    import torch.nn.functional as F
    tensor_calc_ft = TEXT.vocab.vectors[43] - TEXT.vocab.vectors[40] + TEXT.vocab.vectors[48]  # 姫 - 女性 + 男性
    print('女王と計算したベクトルとのコサイン類似度:{}'.format(F.cosine_similarity(tensor_calc_ft, TEXT.vocab.vectors[41], dim=0)))
    print('王と計算したベクトルとのコサイン類似度:{}'.format(F.cosine_similarity(tensor_calc_ft, TEXT.vocab.vectors[46], dim=0)))
    print('王子と計算したベクトルとのコサイン類似度:{}'.format(F.cosine_similarity(tensor_calc_ft, TEXT.vocab.vectors[47], dim=0)))
    print('機械学習と計算したベクトルとのコサイン類似度:{}'.format(F.cosine_similarity(tensor_calc_ft, TEXT.vocab.vectors[45], dim=0)))

> 女王と計算したベクトルとのコサイン類似度:0.36503130197525024
> 王と計算したベクトルとのコサイン類似度:0.34610462188720703
> 王子と計算したベクトルとのコサイン類似度:0.5530638098716736
> 機械学習と計算したベクトルとのコサイン類似度:0.09522878378629684

似たような感じ。
ここまでで、word2vecとfasttextの説明終了〜。次は、文章をベクトルにできるようになったから学習のための準備。