アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

自然言語処理タスクでいろんなRNN系ニューラルネットでの精度を検証してみた【keras・機械学習】

深層学習を使った自然言語処理のタスクで、ネガポジの2値分類をやった。


その際、自然言語処理向けのいろんなRNN系のニューラルネットワーク(NN)を使ったので、各NNの精度を順にまとめてく。

ちなみに、
・不均衡データに対して損失関数でチューニングする方法

・大容量データ対策

もまとめた


f:id:trafalbad:20190326080327p:plain


目次
・使うデータセット
1.LSTMだけ
2.GRUだけ
3.GRU+dropout + recurrent_dropout
4.GRUベースの双方向RNN
5.1次元CNN
6.不均衡データの精度を損失関数(class-balanced-loss)で上げる方法
・最後に:大容量データ対策にfit_generater()
・追記:LSTM層(GRU層)の多層化で精度再検証


使うデータセット

使うデータは「IMDbデータセット」という映画に関するレビュー文章。

すでに文章はidベクトル化されてて、それのネガポジ判定の2値分類をした際の、NNごとの精度を見ていく。

入力形式は自然言語処理で使うembbedingレイヤーを使うため、ゼロパディングして、ミニバッチ化した。やり方は下記記事を参照。

trafalbad.hatenadiary.jp


IMDbデータセットの「訓練データ数、テストデータ数、共通するパラメータ条件.etc」は以下の通り。

・trainデータ:2000こ(ゼロパディング済み)

・testデータ:1000こ(ゼロパディング済み)

・epoch=5

・batch_size=128

・validation_split=0.2

・ネガポジ判定の2値分類


1.LSTMだけ

まず定番、LSTM。

from keras.datasets import imdb
from keras.preprocessing import sequence
import os
import math
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from keras.optimizers import SGD, RMSprop, Adam
from keras.callbacks import History, LearningRateScheduler, Callback
from keras import layers
from keras.models import Model, Sequential
from keras.layers import Dense, Input, Lambda, LSTM, GRU, Embedding


max_words=88585 # 単語のインデックスの数
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(LSTM(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train,
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

# 精度検証
_, acc = model.evaluate(x_test, y_test, verbose=1)
print('\nTest accuracy: {0}'.format(acc))


精度は72%。

simple-RNNよりは確実にいい。


2.GRUだけ

max_words=88585 # 単語のインデックスの数
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(GRU(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train,
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

精度は72.6%。

GRUはLSTMより計算量が少なく、速い。 LSTMとほぼ変わらないけど、計算量少ないからGRU使ってる人が多いのかな。



3.GRU+dropout + recurrent_dropout

今度は過学習防止(dropoutは偶発的な相関関係を破壊する効果がある)のため、dropoutとRNN専用dropoutの「recurrent_dropout」を設定。

max_words=88585 # 単語のインデックスの数
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(GRU(32, dropout=0.1, recurrent_dropout=0.1,))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train,
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

精度は73.6%。

dropoutの最適値はいくつか知らないけど、とりあえず0.1あたりに設定。

データ数少ないから、精度にはほとんど反映されてないけど、過学習は確実に防げる。




4.GRUベースの双方向RNN

双方向RNNはBidirectional層を使う。

max_words=88585 # 単語のインデックスの数
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(layers.Bidirectional(GRU(32)))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train,
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

精度78.2%。

LSTMやGRUより上がってる。ちなみにLSTMベースの双方向RNNも可能。

LSTMやGRUはベクトルを一方向から計算しないため、一方向の文章構造の特徴しか把握できない。

一方、双方向RNNは逆向きにもベクトルを計算するため、両方向の文章構造の特徴を把握できる。

NN系はベクトル構造による特徴把握が精度に大きく関わるので、逆向きでもベクトル構造の特徴が把握できれば、双方向RNNは普通のRNNより精度はいいことになる。

文章ならベクトルにしてしまえば、逆からでもベクトル構造の特徴に意味が含まれていることがほとんどなので、双方向RNNはより自然言語処理向けのRNNだといえる。



5.1次元CNN

max_words=88585
max_len=1629
model = Sequential()
model.add(layers.Embedding(max_words, 128, input_length=max_len))
model.add(layers.SeparableConv1D(32, 7, activation='relu'))
model.add(layers.MaxPooling1D(5))
model.add(layers.SeparableConv1D(32, 7, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(lr=1e-4),
              loss='binary_crossentropy',
              metrics=['acc'])


精度は49%。

1次元CNNについてわかったこと

1次元CNNは自然言語処理にはあまり向いてない

testデータの2番目の次元をtrainデータと同じにしなければ、予測に使えないため、面倒(x_train shape: (2000, 1629)
x_test shape: (1000, 1629))

気温予測とかの時系列データには、LSTMとかと遜色ない精度出てる

最近では普通の畳み込み層(Conv1Dとか)よりも、dw畳み込み層(SeparableConv1Dとか)を使った方が、「計算量も高く、表現力も高い」。  

なのでCNNでは、普通の畳み込み層より、dw畳み込み層使うべし。


自然言語処理だとRNN系のニューラルネットより、精度低いけど計算量少ない

一次元CNNとRNNを連結させ、長いシーケンスデータを処理(CNNでより汎用的な特徴を抽出した後、それをRNNで処理)するやり方もある



過学習防止のための、L2正規化(& L1正規化 & L2/L1正規化)のkerasでのやり方

# L2正規化 & L1正規化 &  L2/L1正規化
keras.regularizers.l2(0.)
keras.regularizers.l1(0.)
keras.regularizers.l1_l2(l1=0.01, l2=0.01)

# 利用例(https://keras.io/ja/regularizers/)
from keras import regularizers
model.add(Dense(64, input_dim=64, kernel_regularizer=regularizers.l2(0.01),
                activity_regularizer=regularizers.l1(0.01)))


6.不均衡データの精度を損失関数(class-balanced-loss)で上げる方法

今回はほぼ均衡データだけど、不均衡データ(クラス毎のデータ数に偏りがあるデータ)に対してのチューニング方法として、"class-balanced-loss"をがあるので、やってみる。

"class-balanced-loss"はデータ数が少ないクラスの損失関数に重み付けして、データ数が少ないクラスの精度を上げる方法。

やり方

# https://datascience.stackexchange.com/questions/13490/how-to-set-class-weights-for-imbalanced-classes-in-keras (参考記事)

# ラベルのarrayを作成(y_trainはラベル:0 label=506、1 label =494)
np.unique(y_train)
# >>> array([0, 1])

from sklearn.utils import class_weight

class_weights = class_weight.compute_class_weight('balanced', np.unique(y_train),  y_train)

class_weights
# >>> array([1.01071356, 0.98951118])


# model.fit()の引数のclass_weightに格納.
history = model.fit(x_train, y_train,
                    epochs=5, batch_size=128, validation_split=0.2, callbacks=callback, class_weight=class_weights)

本家記事のやり方だと、「現実のデータ数≠リアルのデータ数」として扱っているため、上のコードは本来の"class-balanced-loss"とは少し違う。

けど、チューニング方法としては大体同じ。



結果的に精度は

・双方向RNN => 78.2%

・LSTM => 72%

・GRU => 72.6%

・GRU+dropout + recurrent_dropout => 73.6%

・1次元CNN => 49%


=> 双方向RNN > GRU+dropout + recurrent_dropout > GRU ≒ LSTM > 1次元CNN

結論、「双方向RNNが一番精度が高く、自然言語処理に向いてる」。


ちなみに、データ量が多くてGPUによる分散処理をする場合、one-hotベクトルでNNに入力するとか、パディングなしでミニバッチ化せずにNNに入力すると、GPUの分散処理での学習のとき、文章の依存関係が壊れて精度が悪くなるらしい。

なので、embbedingレイヤーに入るとき、word embbedingでid表現かつゼロパディングしてミニバッチ化するのが、自然言語処理の深層学習テクとしては一般的な手法。


最後に、tree-RNNとか、attentionで拡張したRNNとかは翻訳、応答、文章生成あたりのガチ自然言語処理のタスクで使われてる事例があるけど、kerasで実装してる例がなかったので、今回は割愛した。


最後に:大容量データ対策にfit_generater()

大容量データ対策として、学習時に.fit()の代わりに、.fit_generater()を使う。例えば、画像データをよみこませるとき、一定サイズ毎に区切りながらデータを読み込せて、メモリ問題を解決できる。


.fit_generater()には、Generatorオブジェクト(バッチ単位にデータを提供する仕組みを実装したもの)を渡す(参考:Kerasで大容量データをModel.fit_generatorを使って学習する



自然言語処理でも使ってる事例があるので、データ量がでかいときにオススメ



追記:LSTM層(GRU層)の多層化で精度再検証
GRUベースの双方向RNNと、ただのLSTMを2層に(多層化)して、精度を再検証してみた。
なんか専門用語だとLSTM層の多層化のことを「層のスタック」とか言ってるらしい。

一層目の出力は完全なシーケンスデータを返さなきゃならないので、「return_sequences=True」を指定。

# 2層のLSTM(+ dropout + recurrent_dropout)
max_words=88585 # 単語のインデックスの数
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(LSTM(32, return_sequences=True))
model.add(LSTM(32, dropout=0.1, recurrent_dropout=0.5))
model.add(Dense(1, activation='sigmoid'))


# 2層のGRUベースの双方向RNN( + dropout + recurrent_dropout)
model = Sequential()
model.add(Embedding(max_words, 32))
model.add(layers.Bidirectional(GRU(32, return_sequences=True)))
model.add(layers.Bidirectional(GRU(32, dropout=0.1, recurrent_dropout=0.5)))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train, epochs=5, batch_size=128, validation_split=0.2, callbacks=callback)

精度は

・2層のLSTM(前の精度72%)
74%

・2層のGRUベース双方向RNN(前の精度78.2%)
78.6%

わかったこと

・多層化はdropoutを使ったとき、性能がボトルネックになるから行うことが多い

自然言語処理では多層でも隠れ層の値(今回は32)は全部の層で同じ

自然言語処理で2層目以降にrelu関数使ったら勾配消失した。(時系列データでは多層でも2層目以降に、relu関数使ってるケースあり。)

結果的に、多層化したら計算量上がるし、精度も上がる。

今回はデータセットが少量だから結果にほとんど反映されなかったけど、わかったことは「LSTMモデルは、精度が高いモデルは多層化してるし、多層化すればたいてい精度は上がる」ということ。

自然言語処理タスクにLSTM層の多層化はdropoutと併用すると、かなり効果あるようだ。


参考記事Kerasで実装するSeq2Seq_その3_多層LSTMとBidirectional