Ahogrammer

Deep Dive Into NLP, ML and Cloud

Keras の RNN/LSTM/GRU で内部状態を取得する

自然言語処理で RNN を使っていると、RNN の内部状態を取得したくなることがあります。 TensorFlow では tf.nn.dynamic_rnn 等の関数を使うと、出力と状態を返してくれます。 しかし、Keras でのやり方については意外と日本語の情報がありませんでした。

本記事では Keras で RNN の内部状態を取得する方法についてまとめてみました。

RNN/LSTM/GRU の内部状態を取得

Keras にはリカレント層として、SimpleRNNLSTMGRU の3種類が用意されています。これらの層から内部状態を取得するためには、インスタンス化時の引数として return_state=True を渡す必要があります。 return_state を True にすることで、出力に加えて最終状態を取得できるようになります。

では、実際に各リカレント層の内部状態を取得してみましょう。 まずは各層への入力を作ります。 ここでは簡単に、単語の分散表現を入力すると仮定します。 以下は入力を作るコードです。ハイパーパラメータについては適当に設定しています。

>>> from keras.layers import Input, Embedding, SimpleRNN, LSTM, GRU
>>> input = Input(shape=(100,))
>>> embedding = Embedding(input_dim=1000, output_dim=300)(input)

では、まずは SimpleRNN の内部状態を取得してみます。 return_state に True を設定することで、出力を一つ、内部状態を一つ返してきます。 ここで、出力は必ずリストの一番目に入っていることに注意してください。これは以降も同様です。

>>> SimpleRNN(units=100, return_state=True)(embedding)
[<tf.Tensor 'simple_rnn_4/TensorArrayReadV3:0' shape=(?, 100) dtype=float32>,
 <tf.Tensor 'simple_rnn_4/while/Exit_2:0' shape=(?, 100) dtype=float32>]

次に、LSTMの内部状態を取得してみます。 return_state に True を設定することで、出力一つ、内部状態2つを返してくることがわかります。

>>> LSTM(units=100, return_state=True)(embedding)
[<tf.Tensor 'lstm_6/TensorArrayReadV3:0' shape=(?, 100) dtype=float32>,
 <tf.Tensor 'lstm_6/while/Exit_2:0' shape=(?, 100) dtype=float32>,
 <tf.Tensor 'lstm_6/while/Exit_3:0' shape=(?, 100) dtype=float32>]

最後に、GRUの内部状態を取得してみます。 同じように return_state に True を設定することで、出力一つ、内部状態一つを返してきます。

>>> GRU(units=100, return_state=True)(embedding)
[<tf.Tensor 'gru_2/TensorArrayReadV3:0' shape=(?, 100) dtype=float32>,
 <tf.Tensor 'gru_2/while/Exit_2:0' shape=(?, 100) dtype=float32>]

課題と解決策

単純に RNN 単体で使う分には問題ないのですが、return_state=True を Bidirectional と合わせて使うと問題が起きます。 これは return_state を True にするとリストが返されるのですが、Bidirectional はリストを受け取ることを想定していないためです。 以下のようなエラーが発生します。

>>> Bidirectional(LSTM(units=100, return_state=True))(embedding)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../keras/engine/topology.py", line 602, in __call__
    output = self.call(inputs, **kwargs)
  File ".../keras/layers/wrappers.py", line 299, in call
    output = K.concatenate([y, y_rev])
  File ".../keras/backend/tensorflow_backend.py", line 1700, in concatenate
    rank = ndim(tensors[0])
  File ".../keras/backend/tensorflow_backend.py", line 544, in ndim
    dims = x.get_shape()._dims
AttributeError: 'list' object has no attribute 'get_shape'

対策として、手動で LSTM を2つ作成し、その結果を結合してあげる手法が考えられます。 その際、片方の LSTM は引数に go_backwards=True を渡します。 これにより、LSTM への入力が逆順に行われるようになります。

from keras.layers import LSTM, Bidirectional
from keras.layers.merge import Concatenate

fwd_state = LSTM(units=100, return_state=True)(embedding)[-1]
bwd_state = LSTM(units=100, return_state=True, go_backwards=True)(embedding)[-1]
state = Concatenate(axis=-1)([fwd_state, bwd_state])

参考資料

※ ちなみに、日本語のドキュメントには return_state 引数についての記載はありませんでした(2017/09/21時点)。

Keras の Conv1D と Convolution1D、MaxPool1D と MaxPooling1D の違い

本日は Keras の小ネタ。

Kerasで書かれたコードを読んでいるとふと気がつくことがある。 それは、Conv1D と Convolution1D、MaxPool1D と MaxPooling1D という同じような名前のクラスが出てくるのだ。 一体これらの違いは何なのだろうか?

公式ドキュメントには Conv1D と、MaxPooling1D だけが書かれている。 しかし、実際には Convolution1D と MaxPool1D を同じように使うことができる。 これは一体どういうことなのだろうか?

プログラマ的に気になるので調べてみた。

結論

結論としては Conv1D と Convolution1D、MaxPool1D と MaxPooling1D に違いはない。 Kerasのソースコードを読むと、どちらもエイリアスとして使えるようになっていることがわかる。

また、実際にそれぞれを is演算子 で比較してみると同一であることがわかる:

>>> from keras.layers import Conv1D, Convolution1D, MaxPool1D, MaxPooling1D
>>> Conv1D is Convolution1D
True
>>> MaxPool1D is MaxPooling1D
True

Keras 1.0 のドキュメントを読むと、昔は Convolution1D であったことがわかる。 ということは、何らかの理由で Conv1D という名前に変えたが、後方互換性のためにエイリアスとして Convolution1D を使えるようにしているのだと思われる。

おわりに

Keras の Conv1D と Convolution1D、MaxPool1D と MaxPooling1D に違いはないことがわかった。 したがって、どちらも安心して使ってよい。 ただし、今後のことを考えると、公式ドキュメントに書かれている Conv1D と MaxPooling1D を使っておいたほうが良いだろう。

文字レベルの畳込みニューラルネットワークによる文書分類

はじめに

自然言語処理で文書分類は最も基本的なタスクの一つです。 文書分類は、SNSに対する評判分析、ニュースのジャンル分類、メールのスパムフィルタや文書の著者推定といった問題の解決に使われています。 このように基本的なタスクである文書分類は広く使われています。

最近では、文書分類に対するニューラルベースのアプローチとして、単語レベルのRNNが広く使われています。 RNNを使うことで、広い文脈と単語の出現順序を自然な形で考慮することが可能になります。 この性質により、RNNベースの手法は文書分類で良い性能を示してきました。

しかし、単語レベルのRNNには2つの問題が挙げられます。 一つ目は、RNNは計算を並列化し難いため、計算の高速化が難しいということです。 二つ目は、単語レベルの入力がSNS等のユーザ生成コンテンツ(UGC: User-Generated Content)に対して弱いということです。

これらの問題を解決するために、文字レベルのCNNを使って文書分類する手法が提案されてきました。 CNNを使うことで、GPUで計算を高速化できるという利点があります。 また、文字レベルの特徴を使うことで、UGCに対してロバストになることを期待できます。

今回はそんな文字ベースのCNNで文書分類を行う論文をKerasで実装してみました。 コードは以下に置いてあります:

手法

入力の特徴

モデルの入力として、各文字をone-hotエンコードしたベクトルを与えます。 文字列の長さは固定していて、論文では1014文字としています。 なぜ1014文字なのかというと、これくらいの長さを取れば重要な情報は入っているだろうということです。 (それにしても1014の意味はわからんけど)

one-hotエンコードするために用いる語彙としては以下の70文字を使用しています。 具体的に言うと、a〜zまでのアルファベット26文字、0〜9まで数字10文字、その他の文字が34文字です。 この語彙に含まれていない文字はすべて0ベクトルに変換します。

abcdefghijklmnopqrstuvwxyz0123456789
-,;.!?:’’’/\|_@#$%ˆ&*˜‘+-=<>()[]{}

モデルのアーキテクチャ

モデルとしては、全9層のネットワークになっています。 具体的には、最初に6層の畳み込み層、その後に3層の全結合層が接続されます。 アーキテクチャを図にすると以下のようになっています:

f:id:Hironsan:20170919092728p:plain

入力は語彙数と等しくなります。今回の場合70です。ここはどういう文字を定義するかに依存します。 また、入力の長さは1014です。 図には書かれていませんが、正則化のために3層の全結合層の間に2層のドロップアウトが入っています。

畳み込み層とプーリングのハイパーパラメータについては以下の表の通りです:

Layer Large Feature Small Feature Kernel Pool
1 1024 256 7 3
2 1024 256 7 3
3 1024 256 3 N/A
4 1024 256 3 N/A
5 1024 256 3 N/A
6 1024 256 3 3

最後の全結合層のユニット数はタスク依存になります。たとえば、10クラス分類ならユニット数は10にします。 全結合層のハイパーパラメータは以下のとおりです:

Layer Output Units Large Output Units Small
7 2048 1024
8 2048 1024
9 タスク依存 タスク依存

実装

上記のネットワークを Keras を使って実装してみました。 一番メインとなるモデルの構築部分のコードは以下のように書けます:

from keras.layers import Input, Conv1D, Dense, MaxPool1D, Flatten, Dropout
from keras.models import Model


def build_model(kernel_sizes, dense_units, vocab_size,
                nb_filter, nb_class, keep_prob, maxlen=1014):
    inputs = Input(batch_shape=(None, maxlen, vocab_size))

    conv1 = Conv1D(nb_filter, kernel_sizes[0], activation='relu')(inputs)
    pool1 = MaxPool1D(pool_size=3)(conv1)

    conv2 = Conv1D(nb_filter, kernel_sizes[1], activation='relu')(pool1)
    pool2 = MaxPool1D(pool_size=3)(conv2)

    conv3 = Conv1D(nb_filter, kernel_sizes[2], activation='relu')(pool2)
    conv4 = Conv1D(nb_filter, kernel_sizes[3], activation='relu')(conv3)
    conv5 = Conv1D(nb_filter, kernel_sizes[4], activation='relu')(conv4)
    conv6 = Conv1D(nb_filter, kernel_sizes[5], activation='relu')(conv5)
    pool3 = MaxPool1D(pool_size=3)(conv6)
    pool3 = Flatten()(pool3)

    fc1 = Dense(dense_units[0], activation='relu')(pool3)
    fc1 = Dropout(keep_prob)(fc1)
    fc2 = Dense(dense_units[1], activation='relu')(fc1)
    fc2 = Dropout(keep_prob)(fc2)
    pred = Dense(nb_class, activation='softmax')(fc2)

    model = Model(inputs=[inputs], outputs=[pred])

    return model

単純に実装しているだけなので、特に言うことはないのですが、Keras に用意されている Conv1DMaxPool1D を使って実装することで、簡単に書くことができます。

学習用のコードを含めた完全な実装は以下にあります:

実行結果

論文と同じ Yelp のデータセットを使って実験してみました。 実験環境としては、AWS EC2のg2.2xlargeインスタンスを使用しています。 時間的には、1エポックに約2500秒(≒40分)かかっています。

Epoch 1/15
7000/7000 [==============================] - 2513s - loss: 0.2202 - acc: 0.9061 - val_loss: 0.1565 - val_acc: 0.9396
Epoch 2/15
7000/7000 [==============================] - 2490s - loss: 0.1527 - acc: 0.9413 - val_loss: 0.1532 - val_acc: 0.9411
Epoch 3/15
 304/7000 [>.............................] - ETA: 2260s - loss: 0.1334 - acc: 0.9514

ちなみに、手元のCPUで計算しようとしたら、1エポックに10万秒(約27時間)くらいかかりそうだったのでやめました。 GPUで計算することをおすすめします。

おわりに

今回は、 Character-level Convolutional Networks for Text Classification を基に、文字レベルの畳込みニューラルネットワークによる文書分類をやってみました。Kerasを使って実装することで、かなりシンプルな形でモデルを構築することができました。本記事が皆様のお役に立てば幸いです。

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

@Hironsan

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

参考資料