文脈を考慮したWord Embedding, ELMoをTensorFlow Hubを使って試してみる

こんにちは．

本記事では文脈を考慮した単語埋め込み表現（Word Embedding）である　**ELMo** (Embeddings from Language Models)を，TensorFlow Hubを用いて利用する方法について解説します．

実装はGitHubの**[こちら](https://github.com/tks10/ELMo)**

Word Embeddingとは一体...，という方はこちらがわかりやすそうです．

> [Word2Vecとは？](https://deeplearning4j.org/ja/word2vec)

# ELMo
エルモと聞くとまずあの赤いキャラクターを彷彿とさせますが，こっちのエルモはWord2Vecで有名な**Word Embeddingの手法の１つ**です．

さて，ここで以下の３文を見てみましょう．

- You should turn **right** at the next corner. <br>
（あなたは次の角を**右**に曲がるべきです）
- Your opinion is more or less **right**.<br>
（君の意見はだいたい**正しい**よ）
- I have a **right** to ask a question !<br>
（僕には質問をする**権利**がある！）

もはや見るだけで何を言いたいか伝わりそうですが，自然言語にはいわゆる**多義性**というものが存在します．つまり，まったく同じ単語でも文脈によっては異なる意味を持つということですね．

ELMoは，双方向LSTM（Bidirectional LSTM）を用いて大量のコーパスを学習することで，同じ単語でも文脈によって異なる埋め込み表現を獲得することができる手法の一つです．今年（2018年）にarXivに投稿され，加えてTensorFlow Hubに学習済みモデルが公開されたことから話題になりました．

ではTensorFlow Hubとはなんなのか，簡単に説明していきます．


# TensorFlow Hub
[TensorFlow Hub](https://www.tensorflow.org/hub/)とは学習済みの機械学習モデルを利用できるライブラリで，転移学習をするときなどにとても便利です．今回のELMo等の自然言語モデルは学習において常人のリソースではとても扱えない膨大なコーパスを利用することがあるので，エンドユーザにとっては嬉しい機能ですね．

使い方は非常に簡単で，学習済みモデルが置かれているURLを指定するだけですぐに利用可能です．ただし，初回実行時はモデルをサーバからダウンロードしてくるため数分待つことになるので，気長に待ちましょう．（2回目からはローカルに保存されたモデルが自動的に利用されます！）

パッケージは`pip`コマンドでインストール可能です．

```bash
pip install tensorflow-hub
```

例えば，Feed-Forward型NNを用いたWord EmbeddingモデルのNNLMを利用したい場合は，

```python
nnlm = hub.Module("https://tfhub.dev/google/nnlm-en-dim128/1")
```

たったこれだけでモデルが使用可能になります．それぞれのモデルに関する情報は，TensorFlow公式に載っていて，NNLMならば以下のようにModule URLやOverviewが公開されています．ありがたい．

<img width="759" alt="NNLM_on_TensorFlowHub.png" src="https://qiita-image-store.s3.amazonaws.com/0/181933/9215f0d1-2ddd-5f41-23b2-bb3c37a7aab4.png">

> [Module google/nnlm-en-dim128/1](https://www.tensorflow.org/hub/modules/google/nnlm-en-dim128/1)

TensorFlow Hubの使い方がわかったところで，本題のELMoを使った実装をしていきましょう．


# 実装

文を入力した際に，任意のアルゴリズムで対応する文ベクトルを得る機能を実装します．今回は実験でNNLMも利用するので，ELMoとNNLMによるEmbedding機能を作っていきましょう．またこれらはモジュールとして再利用可能なように，embeddings.py上に実装していきます．

はじめに，ELMoとNNLMのModule URLを定義しておきます．

```python:embeddings.py
ELMO = "https://tfhub.dev/google/elmo/2"
NNLM = "https://tfhub.dev/google/nnlm-en-dim128/1"
```

次に，モデル名をキーとして引数に与えられた文に対応するベクトルを返却する関数`embed`を作成します．

```python:embeddings.py
import tensorflow_hub as hub
import tensorflow as tf

def embed(model_name, sentences):
    if model_name == "elmo":
        elmo = hub.Module(ELMO, trainable=True)
        executable = elmo(
            sentences,
            signature="default",
            as_dict=True)["elmo"]

    elif model_name == "nnlm":
        nnlm = hub.Module(NNLM)
        executable = nnlm(sentences)

    else:
        raise NotImplementedError

    return execute(executable)


def execute(tensor):
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        sess.run(tf.tables_initializer())

        return sess.run(tensor)

```

この時，`hub.Module`に文を投げただけでは結果が帰ってこないことに注意しましょう．帰ってくるのはあくまでTensorオブジェクトですので，Sessionに投げて実行する必要があります．上記コードでは，`execute`関数として実装しています．

また，`sess.run`する前に，

- `tf.global_variables_initializer()`
- `tf.tables_initializer()`

の２つで初期化することを忘れないようにしましょう．Hub上のサンプルコードに書いていないため，ハマりがちです．忘れると

```bash
Not found: Container localhost does not exist. (Could not find resource: localhost/module/bilm/char_embed)
```

みたいなのを含んだ大量のエラーが出てつらいです．

最後に，ELMoとNNLMの文ベクトルを同時に取得できるように，先ほど作成した関数を利用した`get_embeddings_elmo_nnlm`関数を作っておきます．これでこの関数に文のリストを与えてあげるだけで，対応する文ベクトルが取得できますね．

```python:embeddings.py
def word_to_sentence(embeddings):
    return embeddings.sum(axis=1)


def get_embeddings_elmo_nnlm(sentences):
    return word_to_sentence(embed("elmo", sentences)), embed("nnlm", sentences)
```

NNLMはデフォルトで文ベクトルを出力しますが，ELMoはあくまで単語ベクトルですので，関数`word_to_sentence`で単語ベクトルを足し合わせています．

そんな単純に足し合わせて良いのかという話もありますが，最近こそSentence Embeddingの研究が盛んになっているものの，単純に単語ベクトルの線形結合をとるだけである程度タスクに対して有効だったりします．ですから，今回はNNLMとの比較ということで`sum`（荷重1の線形結合と同じ）をとります．計算するのはcos類似度ですから，単語長で全体を正規化することは今回は考えません．

ここまででできた`embeddings.py`をインポートした，`comparison_test.py`を作成し，以下のコードを記述します．

```python:embeddings.py
import embeddings
import numpy as np
import argparse


def cos_sim(a, b):
    return np.inner(a, b) / (np.linalg.norm(a) * (np.linalg.norm(b)))


def get_parser():
    parser = argparse.ArgumentParser(
        prog='ELMO vs NNLM',
        usage='python comparison_test.py [sentence1] [sentence2]',
        description='This module demonstrates comparison of ELMo and NNLM',
        add_help=True
    )

    parser.add_argument("sentences", nargs=2, help="The sentences to compare cosine similarity")

    return parser


if __name__ == "__main__":
    parser = get_parser().parse_args()
    sentence1, sentence2 = parser.sentences

    # Get embeddings corresponding to each sentences
    results_elmo, results_nnlm = embeddings.get_embeddings_elmo_nnlm([sentence1, sentence2])

    print("[Cosine Similarity]")
    print("\"{}\" vs \"{}\"".format(sentence1, sentence2))
    print("ELMo:", cos_sim(results_elmo[0], results_elmo[1]))
    print("NNLM:", cos_sim(results_nnlm[0], results_nnlm[1]))

```


# 実験

簡単な実験として，同じ単語を使った文同士のコサイン類似度が1.0にならないことを確認してみましょう．

- people read the book
- the book people read

使っている単語は全く同じですが，意味合い的には変わってきます．以下のように実行することで，結果が確認できます．

```bash:bash
python comparison_test.py "people read the book" "the book people read"

[Cosine Similarity]
"people read the book" vs "the book people read"
ELMo: 0.83875865
NNLM: 1.0
```

文脈を考慮しないNNLMではコサイン類似度が1.0，すなわち２文のベクトルが全く同じ方向を向いていることがわかります．対してELMoでは0.84程度と，語順によって異なるベクトルを得られていますね．

ただ，この文は冒頭で例をあげたような明確な多義性を持つわけではないので，正しく文脈を考慮した埋め込み表現になったのかを検証できたわけではありませんが，手法の差異を確認することはできたでしょう．（何か面白い例があれば教えてください）


# まとめ
今回は，文脈を考慮した単語埋め込み表現，ELMoをTensorFlow Hubを用いて利用してみました．実験で試験的に用いた文は４語と短いものでしたが，時系列モデルを採用しているためより長い文に対してより有用な効果が得られると思います．ではまた．