Ahogrammer

Deep Dive Into NLP, ML and Cloud

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

はじめに

自然言語処理で文書分類は最も基本的なタスクの一つです。 文書分類は、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

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

参考資料

デバッガを使ってKerasのモデルをデバッグする

3行まとめ

  • Keras で作成したモデルをデバッグしたい。
  • Keras には標準でデバッガが用意されていない。
  • Keras の Session オブジェクトを tfdbg でラップしてデバッグする。

オープンソースニューラルネットワークライブラリである Keras は計算グラフに基づいています。 一般的な Keras のプログラムは以下の段階から構成されています。

  1. Sequentialモデルか Functional API を用いて、モデルを構築する
  2. 計算グラフを実行し、学習や予測を行う

ここで問題となるのが、2番目の計算グラフの実行中にエラーやバグが発生した場合、デバッグが難しいことです。 なぜなら、内部構造や内部状態が外から見えないため、どのような状況で計算に失敗したのかわからないからです。

Kerasには今のところdebuggerは存在しませんが、TensorFlowには tfdbg と呼ばれるdebuggerが用意されています。 tfdbgを使うことで、学習中や予測中の内部構造や内部状態を明らかにすることができます。

本記事では tfdbg をKerasで使ってデバッグを行う方法を紹介します。 tfdbgを使うため、KerasのバックエンドにはTensorFlowを想定しています。 では、短いサンプルコードを通してデバッガの動作を見てみます。 以下のコードは、簡単な評判分析のモデルを Sequentialモデルで定義し、学習します。

import keras.backend as K
from keras.preprocessing import sequence
from keras.models import Sequential
from keras.layers import Dense, Embedding, LSTM
from keras.datasets import imdb
from tensorflow.python import debug as tf_debug

sess = K.get_session()
sess = tf_debug.LocalCLIDebugWrapperSession(sess)
K.set_session(sess)

max_features = 20000
maxlen = 80
batch_size = 32

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)

model = Sequential()
model.add(Embedding(max_features, 128))
model.add(LSTM(128, dropout=0.2, recurrent_dropout=0.2))
model.add(Dense(1, activation='sigmoid'))

model.compile(loss='binary_crossentropy',
              optimizer='adam', metrics=['accuracy'])
model.fit(x_train, y_train, batch_size=batch_size,
          epochs=15, validation_data=(x_test, y_test))
score, acc = model.evaluate(x_test, y_test,
                            batch_size=batch_size)

この例でわかるように、Keras の Session オブジェクトをデバッグ用のクラス(LocalCLIDebugWrapperSession)でラップしています。 これにより、計算グラフを実行した際に、 tfdbg インターフェースが起動します。この インターフェース内でマウスクリックやコマンドを使うと、その後のコードの実行や、グラフのノードや属性の調査をできます。詳しい使い方については、tfdbgのドキュメントを見てください。

f:id:Hironsan:20170915085618p:plain

まとめ

  • Kerasで作成したモデルをデバッグしたい。
  • Kerasには標準でデバッガが用意されていない。
  • Kerasのsessionオブジェクトをtfdbgでラップしてデバッグする。

参考資料

TensorFlow のデバッガ tfdbg に関する公式ドキュメント
Debugging TensorFlow Programs

Google Developers blog による tfdbg の解説
Debug TensorFlow Models with tfdbg

Keras backends の公式ドキュメント
Keras backends

Kerasでカスタムレイヤーを作成する方法

Kerasでは様々なレイヤーが事前定義されており、それらをレゴブロックのように組み合わせてモデルを作成していきます。 たとえば、EmbeddingやConvolution, LSTMといったレイヤーが事前定義されています。 通常は、これらの事前定義された便利なレイヤーを使ってモデルを作成します。

しかし、事前定義されたレイヤーだけでは必要な機能を実装できない時があります。 たとえば、最新の論文を実装しようとしたときに、その中で使われている機能がKerasに存在しないということはよくあります。

必要なレイヤーがKerasに存在しない場合、自分でカスタムレイヤーを実装する必要があります。 Kerasは自分でカスタムレイヤーを実装するための機能を提供してくれています。 基本的に重みを持たないレイヤーに関してはLambdaで定義し、学習可能な重みを持たせたい場合はカスタムレイヤーの実装を検討することになります。

本記事では、Kerasでのカスタムレイヤーの作成方法を紹介します。

カスタムレイヤーの作り方

カスタムレイヤーを作成するためには、keras.engine.topologyモジュールのLayerクラスを継承したクラスを作る必要があります。 具体的には、Layerで定義されているメソッドをオーバーライドして、必要なレイヤーを作成します。

その際に重要なメソッドは以下の4つです:

  • __init__
  • build
  • call
  • compute_output_shape

以下では各メソッドの役割について説明していきます。

__init__()

初期化メソッドでは、レイヤーを初期化します。 具体的には、必要な機能を実装する際に必要な情報を渡しておきます。 たとえば、重みの初期化方法とか内部のユニット数とかそんな情報を渡します。

build(self, input_shape)

buildでは学習可能な重みを定義します。 具体的には、self.add_weightsメソッドにshapeや初期化方法、正規化方法を渡して重みを定義します。 ここで使用する初期化方法や正規化方法は__init__()メソッドに渡しておいたものを使用します。

また、buildの最後には必ず、self.build = Trueと設定しなければなりません。 self.buildに直接設定してもいいのですが、super([Layer], self).build()を呼び出してもできます。

call(self, x, mask=none)

callメソッドにはForwardの計算で行いたいことを実装します。 callメソッドの中では、__init__で設定した情報、buildで定義した重み、そしてレイヤーへの入力を使って、自分の行いたい計算をします。 カスタムレイヤーのメソッドの中でも最も重要なメソッドの一つです。

compute_output_shape(self, input_shape)

このメソッドは名前の通り出力のshapeを計算します。 作成したレイヤーの内部で入力のshapeを変更する場合には,ここでshape変換のロジックを指定します。 変更しない場合は定義する必要はありません。入力のshapeがそのまま返されます。

カスタムレイヤの保存と読み込み

Kerasではモデルを保存する際に、レイヤーの情報をjsonに変換します。 その際、レイヤーのget_configメソッドが重要な役割を果たします。 get_configメソッド内に、どの情報をjson化するのか記述していきます。 このjsonには、カスタムレイヤーを復元するのに必要な情報を含める必要があります。

カスタムレイヤーの読み込みについては公式のFAQに方法が示されています。
Handling custom layers (or other custom objects) in saved models

簡単に述べておくと、load_model関数の引数であるcustom_objectsに定義したカスタムレイヤーを渡しましょうという話です。 以下のような感じで渡すと、保存したモデルを正常に読み込むことができます。

from keras.models import load_model
# AttentionLayer というカスタムレイヤーを定義したと仮定
model = load_model('my_model.h5', custom_objects={'AttentionLayer': AttentionLayer})

各メソッドの呼ばれる順番

ここでは各メソッドが呼ばれる順番について書いておきます。 呼ばれる順番について知らなくてもレイヤーを定義することはできますが、知っているとデバッグするときに役立つかもしれません。

まず、レイヤーをインスタンス化する際に __init__ が呼ばれます。これは問題ないでしょう。 レイヤーをインスタンス化したら、レイヤーに入力を与えて呼び出します。 一応書いておくと、これは以下のようなイメージです:

x = Input(batch_shape=(None, maxlen), dtype="int32")
embedding = Embedding(vocab_size, dim)(x)

上記のEmbeddingレイヤーのように、レイヤーを呼び出した際に実行されるのが、__call__メソッドです。 __call__ の中では以下の順序でメソッドが呼ばれます:

  • build
  • call
  • compute_mask
  • compute_output_shape

ちなみに、compute_maskではマスキングの計算を行います。

実際のコード

カスタムレイヤーの公式Exampleとして、以下のファイルが挙げられます。
keras/antirectifier.py at master · fchollet/keras · GitHub

上記のExampleで基本的なところを理解したら、Kerasに事前定義されているレイヤーを見るのが一番勉強になるかなと感じています。

ちなみに、私もカスタムレイヤーを定義してみました。Keras用のCRF層です。
keras-crf-layer

参考資料

カスタムレイヤーの作り方に関する公式ドキュメント
Writing your own Keras layers

カスタムレイヤーの公式Example
keras/antirectifier.py at master · fchollet/keras · GitHub

カスタムレイヤーの作成方法のチュートリアル
For beginners; Writing a custom Keras layer

カスタムレイヤーでシリアライズを行う話
kerasでカスタムレイヤーのシリアライズを行う

カスタムレイヤーの読み込み方法
Handling custom layers (or other custom objects) in saved models