Ahogrammer

Deep Dive Into NLP, ML and Cloud

固有表現認識器に言語モデルを組み込んで、性能を向上させる

最近の自然言語処理では言語モデルを使って転移学習をしたり、性能向上に役立てたりするようになってきました。言語モデルの1つであるELMoでは、言語モデルから得られる分散表現を他のタスクの入力に使うことで、質問応答や固有表現認識、評価分析といった様々なタスクの性能向上に役立つことを示しました。ELMoについては以下の記事で詳しく紹介されています。

kamujun.hatenablog.com

本記事では、以前書いた記事で構築したディープラーニングベースの固有表現認識器の性能をELMoを使って向上させる方法を紹介します。ELMoの学習から始めるのは大変なので、今回はAllenNLPで提供されている学習済みのELMoを使用します。ちなみにAllenNLPとは、自然言語処理をするのに便利な機能を提供しているライブラリです。

記事の構成は以下の3部から成っています。

  • 実装するモデルの説明
  • モデルの実装
  • モデルの学習

全体のコードは以下のGitHubリポジトリに置いてあります。スターしていただけるとうれしいです。m(_ _)m

github.com

では、実装するモデルについて説明していきます。

今回実装するモデル

今回は、ELMoを以前構築したLampleらが提案したモデルに組み合わせたモデルを実装します。このモデルの入力は3つあります。それは、単語とその単語を構成する文字、そしてELMoから出力される単語の分散表現です。ELMoの出力を加えることで、文脈を考慮した分散表現を固有表現の認識に使うことができます。

Lampleらのモデルは主に文字用BiLSTM、単語用BiLSTM、およびCRFを用いて構築されています。まず単語を構成する文字をBiLSTMに入力して、文字から単語表現を獲得します。それを単語分散表現と連結して、単語用BiLSTMに入力します。最後にCRFを入れることで、出力ラベル間の依存性を考慮しています。以下のようなイメージです。

black_painted.png

画像はA Bidirectional LSTM and Conditional Random Fields Approach to Medical Named Entity Recognitionより

今回構築するモデルでは、上記の図のWord EmbeddingにELMoで得られた単語分散表現を連結して固有表現タグの予測を行います。そのために、AllenNLPで提供されているELMoをKerasのモデルに組み込んで使います。変更点はそれだけです。図にすると以下のようなイメージです。

f:id:Hironsan:20180925095818p:plain

画像はImproving NLP tasks by transferring knowledge from large data より参照

ELMoについてのイメージを付けるために、ELMoを使ってテキストを単語分散表現の系列に変換してみましょう。まずは必要なライブラリをインストールします。

pip install allennlp

次に、Pythonインタプリタを立ち上げ、ELMoをロードします。初回はモデルの定義や重みのファイルをダウンロードするので時間がかかります。

from allennlp.modules.elmo import Elmo, batch_to_ids

options_file = "https://s3-us-west-2.amazonaws.com/allennlp/models/elmo/2x4096_512_2048cnn_2xhighway/elmo_2x4096_512_2048cnn_2xhighway_options.json"
weight_file = "https://s3-us-west-2.amazonaws.com/allennlp/models/elmo/2x4096_512_2048cnn_2xhighway/elmo_2x4096_512_2048cnn_2xhighway_weights.hdf5"
elmo = Elmo(options_file, weight_file, 2, dropout=0)

ELMoをロードできたら、単語系列を入力します。単語の系列はbatch_to_idsでIDに変換します。変換したIDをELMoに与えることで、与えた単語系列が単語分散表現の系列として返されます。

sentences = [['First', 'sentence', '.'], ['Another', '.']]
character_ids = batch_to_ids(sentences)
embeddings = elmo(character_ids)

得られたembeddingsは辞書型の変数です。その要素としてはelmo_representationsmaskからなっています。elmo_representationsはELMoの各層で得られた分散表現を表しています。ここで得られる分散表現の次元数は1024です。maskは対応するマスクを表しています。分散表現を表示すると以下のようになります。

>>> embeddings["elmo_representations"][0]
tensor([[[ 0.1474, -0.1475,  0.1376,  ...,  0.0270, -0.4051, -0.0498],
         [ 0.2394,  0.0769,  0.4126,  ..., -0.1671, -0.1707,  0.3884],
         [-0.7602, -0.4944, -0.5355,  ..., -0.0803,  0.0361,  0.1128]],
        [[ 0.2603, -0.4437,  0.2726,  ..., -0.0830, -0.1522, -0.1361],
         [-0.7772, -0.4294, -0.2651,  ..., -0.0803,  0.0361,  0.1128],
         [ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000]]],
       grad_fn=<DropoutBackward>)
>>> embeddings["elmo_representations"][0].shape
torch.Size([2, 3, 1024])

ここでshapeが[2, 3, 1024]になっていることに気が付きます。入力文の系列長はそれぞれ[3, 2]だったので自動的にパディングされているということです。maskが提供されているのは、計算でパディングを考慮しないために使ってくださいねということでしょう。

ここで得られた分散表現をモデルの入力の一つとして使います。

モデルの実装

ではモデルを実装していきましょう。モデルの実装にはKerasを使います。実装は以前に実装したモデルとほとんど同じなので、違うところだけ主に示していきます。一度にすべてを実装するとわかりにくいので、以下の部分に分けて少しずつ実装していくことにします。

  • 入力層
  • Embedding層
  • 出力層

入力層

まずはモデルの入力を用意します。今回のモデルは入力として単語系列とその文字系列、ELMo用の系列をとります。以下のようにして、3つの入力を定義します。

word_ids = Input(shape=(None,), dtype='int32')
char_ids = Input(shape=(None, None), dtype='int32')
elmo_embeddings = Input(shape=(None, 1024), dtype='float32')

word_idschar_idsは単語と文字のIDを入力しますが、elmo_embeddingsにはAllenNLPの ELMo で得られた分散表現を直接入力します。そのため、elmo_embeddingsの最後の次元にはAllenNLPの ELMo で得られる分散表現の次元数(1024)を指定します。

Embedding層

今回のモデルは以下の3つの分散表現を結合します。

  • 文字ベースの単語分散表現
  • 通常の単語分散表現
  • ELMoの単語分散表現

最初に文字用の BiLSTM を定義して、単語を構成する文字をベースに単語ベクトルを構築します。ここではまず、入力した文字系列を分散表現に変換します。次に、分散表現を BiLSTM に入力して、その状態を取得します。

char_embeddings = Embedding(input_dim=char_vocab_size,
                            output_dim=char_emb_size,
                            mask_zero=True
                            )(char_ids)
char_embeddings = TimeDistributed(Bidirectional(LSTM(char_lstm_units)))(char_embeddings)

次に単語の分散表現を定義します。ここはGloVeなどで初期化してもOKです。

word_embeddings = Embedding(input_dim=word_vocab_size,
                            output_dim=word_emb_size,
                            mask_zero=True)(word_ids)

これまでに定義した3つの分散表現(char_embeddings, word_embeddings, elmo_embeddings)を連結して新たな単語分散表現とします。作成した単語分散表現は、BiLSTM に入力します。

x = Concatenate(axis=-1)([word_embeddings, char_embeddings, elmo_embeddings])
x = Bidirectional(LSTM(units=word_lstm_units, return_sequences=True))(x)

出力層

最後にモデルの出力層の部分を定義していきます。今回は出力層としてCRFを使います。CRF層についてはkeras-contribから取ってきてください。

from layers import CRF

x = Dense(word_lstm_units, activation='tanh')(x)
x = Dense(num_tags)(x)
crf = CRF()
pred = crf(x)

あとはModelクラスに入出力を与えてモデルを作成するだけです。

model = Model(inputs=[word_ids, char_ids, elmo_embeddings], outputs=[pred])
model.compile(loss=crf.loss_function, optimizer='adam')

以上でモデルの実装は終わりです。次はモデルを学習させてみましょう。

モデルの学習

ここまでで、モデルを学習させる準備が整いました。実際にCONLL 2003のデータを使って固有表現認識の学習をさせてみましょう。学習はモデルのfitメソッドに学習データを与えるだけです。途中経過ですが以下のような結果を得られました。

...
 - f1: 91.85
 - f1: 92.10
 - f1: 91.54
...

ELMoを加えない場合の性能がf1で90.94であったのに対し、ELMoを加えることで92.10まで性能が上昇しています。論文で示されている性能が92.22 ± 0.10なのでだいたい示された性能通りの数値ということになります。

おわりに

固有表現認識は自然言語処理の様々なタスクで使われる重要な技術です。本記事ではディープラーニングを使った固有表現認識器の入力にELMoを加えて性能向上させてみました。この記事が皆様のお役に立てば幸いです。

私のTwitterアカウントでも機械学習自然言語処理に関する情報をつぶやいています。

この分野にご興味のある方のフォローをお待ちしています。

参考文献