Python for NLPの連載は今回で17回目です。前回は、自然言語処理のための深層学習についての議論を開始しました。
前回は主に単語の埋め込みに焦点を当て、単語の埋め込みを利用してテキストを対応する密なベクトルに変換し、それを任意の深層学習モデルの入力として使用する方法を見ました。我々は、単語埋め込みを用いた基本的な分類タスクを実行しました。我々は、映画に関する16の架空のレビューを含むカスタムデータセットを使用した。さらに、分類アルゴリズムは同じデータで学習とテストを行った。最後に、私たちのアルゴリズムをテストするために、密結合ニューラルネットワークのみを使用しました。
この記事では、前回の記事で勉強した概念を基に、実際のデータセットを使って分類をより詳細に見ていきます。今回は、3種類のディープニューラルネットワークを使用します。密結合ニューラルネットワーク(Basic Neural Network)、畳み込みニューラルネットワーク(CNN)、リカレントニューラルネットワークの変種である長短期記憶ネットワーク(LSTM)です。さらに、全く未知のデータに対してディープラーニングモデルを評価する方法を紹介します。
注:この記事では、Keras Embedding LayerとGloVe word embeddingsを使用して、テキストを数値に変換しています。これらの概念をすでに理解していることが重要です。そうでなければ、私の前回の記事を読んでから、この記事に戻ってきて続きを読むことができます。
データセット
このKaggleのリンクからダウンロードできるデータセットです。
データセットをダウンロードし、圧縮ファイルを展開すると、CSVファイルが表示されます。このファイルには50,000件のレコードと、reviewとsentimentの2つのカラムが含まれています。reviewカラムはレビューのテキストを含み、sentimentカラムはレビューのセンチメントを含みます。sentiment列は2つの値、すなわち「ポジティブ」と「ネガティブ」を持つことができ、これは我々の問題をバイナリ分類問題にします。
必須ライブラリのインポート
以下のスクリプトは、必要なライブラリをインポートします。
import pandas as pd
import numpy as np
import re
import nltk
from nltk.corpus import stopwords
from numpy import array
from keras.preprocessing.text import one_hot
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers.core import Activation, Dropout, Dense
from keras.layers import Flatten
from keras.layers import GlobalMaxPooling1D
from keras.layers.embeddings import Embedding
from sklearn.model_selection import train_test_split
from keras.preprocessing.text import Tokenizer
データセットのインポートと分析
それでは、データセットをインポートして解析してみましょう。以下のスクリプトを実行してください。
movie_reviews = pd.read_csv("E:\Datasets\IMDB Dataset.csv")
movie_reviews.isnull().values.any()
movie_reviews.shape
上のスクリプトでは、pandasライブラリの read_csv()
メソッドを用いて、データセットが格納されたCSVファイルを読み込んでいます。次の行では、データセットにNULL値が含まれているかどうかをチェックします。最後に、データセットの形状を表示します。
では、head()
メソッドを使ってデータセットの最初の5行を表示してみましょう。
movie_reviews.head()
出力結果には、以下のようなデータフレームが表示されます。
では、これから処理するテキストについて知るために、レビューのどれかを見てみましょう。次のスクリプトを見てください。
movie_reviews["review"][3]
次のようなレビューが表示されるはずです。
"Basically there's a family where a little boy (Jake) thinks there's a zombie in his closet & his parents are fighting all the time.<br/<br/This movie is slower than a soap opera... and suddenly, Jake decides to become Rambo and kill the zombie.<br/<br/OK, first of all when you're going to make a film you must Decide if its a thriller or a drama! As a drama the movie is watchable. Parents are divorcing & arguing like in real life. And then we have Jake with his closet which totally ruins all the film! I expected to see a BOOGEYMAN similar movie, and instead i watched a drama with some meaningless thriller spots.<br/<br/3 out of 10 just for the well playing parents & descent dialogs. As for the shots with Jake: just ignore them."
このテキストには、句読点、括弧、そしていくつかのHTMLタグが含まれていることがわかります。このテキストは次のセクションで前処理をします。
最後に、データセット内のポジティブとネガティブの感情の分布を見てみましょう。
import seaborn as sns
sns.countplot(x='sentiment', data=movie_reviews)
出力
出力から、データセットが同数のポジティブとネガティブなレビューを含んでいることが明らかです。
データ前処理
我々のデータセットには句読点やHTMLタグが含まれていることがわかった。このセクションでは、テキスト文字列をパラメーターとして受け取り、文字列から特殊文字やHTMLタグを除去する前処理を行う関数を定義します。最後に、その文字列は呼び出した関数に返されます。次のスクリプトを見てください。
def preprocess_text(sen):
# Removing html tags
sentence = remove_tags(sen)
# Remove punctuations and numbers
sentence = re.sub('[^a-zA-Z]', ' ', sentence)
# Single character removal
sentence = re.sub(r"\s+[a-zA-Z]\s+", ' ', sentence)
# Removing multiple spaces
sentence = re.sub(r'\s+', ' ', sentence)
return sentence
TAG_RE = re.compile(r'<[^>]+>')
def remove_tags(text):
return TAG_RE.sub('', text)
preprocess_text()メソッドでは、最初にHTMLタグを除去します。HTMLタグを削除するために、
remove_tags()関数が定義されています。remove_tags
関数は、開閉する <>
の間を空白に置き換えるだけです。
次に preprocess_text
関数では、大文字と小文字の英字以外はすべて削除されるため、意味のない一文字になってしまいます。例えば、”Mark’s “という単語からアポストロフィーを取り除くと、アポストロフィーは空白に置き換わります。したがって、「s」という一文字が残ってしまう。
次に、この一文字をすべて削除し、空白に置き換えると、テキストに複数の空白が生じます。最後に、テキストから複数のスペースを削除します。
次に、レビューの前処理を行い、以下のような新しいリストに格納します。
X = []
sentences = list(movie_reviews['review'])
for sen in sentences:
X.append(preprocess_text(sen))
それでは、4番目のレビューを見てみましょう。
X[3]
出力はこのようになります。
'Basically there a family where little boy Jake thinks there a zombie in his closet his parents are fighting all the time This movie is slower than soap opera and suddenly Jake decides to become Rambo and kill the zombie OK first of all when you re going to make film you must Decide if its thriller or drama As drama the movie is watchable Parents are divorcing arguing like in real life And then we have Jake with his closet which totally ruins all the film expected to see BOOGEYMAN similar movie and instead watched drama with some meaningless thriller spots out of just for the well playing parents descent dialogs As for the shots with Jake just ignore them '
出力から、HTMLタグ、句読点、数字が削除されていることがわかります。残るはアルファベットだけです。
次に、ラベルを数字に変換する必要があります。出力には「ポジティブ」と「ネガティブ」という2つのラベルしかないので、単純に整数に変換すればよい。positive “を “1 “に、”negative “を “0 “に置き換えることで、以下のように簡単に整数に変換することができる。
y = movie_reviews['sentiment']
y = np.array(list(map(lambda x: 1 if x=="positive" else 0, y)))
最後に,データセットを訓練セットとテストセットに分ける必要がある.トレーニングセットはディープラーニングモデルの学習に使用され、テストセットはモデルの性能を評価するために使用されます。
以下のように、 sklearn.model.selection
モジュールの train_test_split
メソッドを使用することができます。
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)
上記のスクリプトでは、データを80%のトレーニングセットと20%のテストセットに分割しています。
次に、埋め込み層のスクリプトを書きます。エンベッディング層は、テキストデータを数値データに変換し、Kerasのディープラーニングモデルの最初の層として使用されます。
エンベッディングレイヤーの準備
最初のステップとして、 keras.preprocessing.text
モジュールの Tokenizer
クラスを使って、単語からインデックスへの辞書を作成することにする。word-to-index 辞書では、コーパスに含まれる各単語をキーとし、それに対応する一意のインデックスをキーの値として使用する。以下のスクリプトを実行する。
tokenizer = Tokenizer(num_words=5000)
tokenizer.fit_on_texts(X_train)
X_train = tokenizer.texts_to_sequences(X_train)
X_test = tokenizer.texts_to_sequences(X_test)
変数エクスプローラで変数 X_train
を見ると、40,000個のリストがあり、それぞれのリストには整数が格納されていることがわかる。各リストは実際にはトレーニングセットの各文章に対応する。また、各リストのサイズが異なることにも気づかれるでしょう。これは文の長さが異なるためです。
ここでは、各リストの最大サイズを100に設定しました。別のサイズを試すこともできます。サイズが100より大きいリストは、100に切り詰められます。長さが100未満のリストについては、最大長になるまでリストの末尾に0を追加します。この処理をパディングと呼びます。
以下のスクリプトでは、訓練セットとテストセットに対して、語彙の大きさを求め、パディングを実行しています。
# Adding 1 because of reserved 0 index
vocab_size = len(tokenizer.word_index) + 1
maxlen = 100
X_train = pad_sequences(X_train, padding='post', maxlen=maxlen)
X_test = pad_sequences(X_test, padding='post', maxlen=maxlen)
ここで、X_train
または X_test
を表示すると、すべてのリストの長さが同じ、つまり100であることがわかる。また、変数 vocabulary_size
の値が 92547 になっているが、これはこのコーパスが 92547 個のユニークな単語を持っていることを意味する。
GloVeの埋め込みを利用して特徴行列を作成する。以下のスクリプトでは、GloVeの単語埋め込みをロードし、単語をキー、それに対応する埋め込みリストを値とする辞書を作成します。
from numpy import array
from numpy import asarray
from numpy import zeros
embeddings_dictionary = dict()
glove_file = open('E:/Datasets/Word Embeddings/glove.6B.100d.txt', encoding="utf8")
for line in glove_file:
records = line.split()
word = records[0]
vector_dimensions = asarray(records[1:], dtype='float32')
embeddings_dictionary [word] = vector_dimensions
glove_file.close()
最後に、コーパス中の単語のインデックスを行番号とする埋め込み行列を作成します。この行列は100列で、各列にはコーパスに含まれる単語に対するGloVeの単語埋め込みが格納されます。
embedding_matrix = zeros((vocab_size, 100))
for word, index in tokenizer.word_index.items():
embedding_vector = embeddings_dictionary.get(word)
if embedding_vector is not None:
embedding_matrix[index] = embedding_vector
上記のスクリプトを実行すると、embedding_matrix
は92547行になることがわかります(コーパスの各単語に対して1行ずつ)。これで深層学習モデルを作成する準備が整った。
シンプルなニューラルネットワークによるテキスト分類
最初に開発するディープラーニングモデルは、シンプルなディープニューラルネットワークです。以下のスクリプトを見てください。
model = Sequential()
embedding_layer = Embedding(vocab_size, 100, weights=[embedding_matrix], input_length=maxlen , trainable=False)
model.add(embedding_layer)
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
上のスクリプトでは、Sequential()
モデルを作成します。次に、埋め込み層を作成します。埋め込み層は入力が100、出力ベクトルが100です。語彙のサイズは92547語です。今回はGloVeの埋め込みを利用するため、trainable
をFalse
に設定し、weights
属性に独自の埋め込み行列を渡します。
これで、埋め込み層がモデルに追加されました。次に、埋め込み層を密な層に直接接続するため、埋め込み層を平坦化します。最後に、シグモイド活性化関数を持つ密な層を追加します。
このモデルをコンパイルするために、adam
オプティマイザーを使い、binary_crossentropy
を損失関数、accuracy
を評価基準として、モデルの要約を出力します。
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
print(model.summary())
出力はこのようになります。
Layer (type) Output Shape Param #=================================================================
embedding_1 (Embedding) (None, 100, 100) 9254700
_________________________________________________________________
flatten_1 (Flatten) (None, 10000) 0
_________________________________________________________________
dense_1 (Dense) (None, 1) 10001
=================================================================
Total params: 9,264,701
Trainable params: 10,001
Non-trainable params: 9,254,700
コーパスには92547個の単語があり、各単語は100次元のベクトルで表現されるので、埋め込み層で学習可能なパラメータは「92547×100」個となる。平坦化層では、単純に行と列を掛け合わせるだけである。最後に、密な層では、パラメータの数は10000(平坦化層から)とバイアスパラメータに1、合計10001となります。
それでは、モデルを学習してみましょう。
history = model.fit(X_train, y_train, batch_size=128, epochs=6, verbose=1, validation_split=0.2)
上のスクリプトでは、fit
メソッドを使ってニューラルネットワークを学習しています。訓練セットだけで訓練していることに注意してください。Validation_split` を 0.2 とすると、学習データの 20% を使ってアルゴリズムの学習精度を求めることになります。
学習の結果、学習精度は約85.52%であることがわかります。
モデルの性能を評価するには、テストセットをモデルの evaluate
メソッドに渡せばよいのです。
score = model.evaluate(X_test, y_test, verbose=1)
テストの精度と損失を確認するには、以下のスクリプトを実行します。
print("Test Score:", score[0])
print("Test Accuracy:", score[1])
上記のスクリプトを実行すると、テストの精度が74.68%であることがわかります。トレーニングの精度は85.52%でした。これは、このモデルがトレーニングセットに対してオーバーフィットしていることを意味します。オーバーフィッティングとは、モデルがテストセットよりもトレーニングセットでより良い性能を発揮することです。理想的には、トレーニングセットとテストセットの性能差は最小であるべきです。
それでは、トレーニングセットとテストセットの損失と精度の差をプロットしてみましょう。以下のスクリプトを実行してください。
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train','test'], loc='upper left')
plt.show()
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train','test'], loc='upper left')
plt.show()
出力
トレーニングセットとテストセットの損失と精度の違いがよくわかります。
畳み込みニューラルネットワークによるテキスト分類
畳み込みニューラルネットワークは、主に画像のような2次元データの分類に使われるネットワークの一種です。畳み込みニューラルネットワークは、最初の層で画像内の特定の特徴を見つけようとします。次の層では、最初に検出された特徴を結合して、より大きな特徴を形成する。このようにして、画像全体が検出される。
畳み込みニューラルネットワークは、テキストデータにも有効であることが分かっている。テキストデータは1次元ですが、1次元の畳み込みニューラルネットワークを使って、データから特徴を抽出することができます。畳み込みニューラルネットワークについて詳しく知りたい方は、こちらの記事をご覧ください。
それでは、1畳み込み層と1プーリング層からなるシンプルな畳み込みニューラルネットワークを作成してみましょう。埋め込み層を作成するまでのコードはそのままで、埋め込み層を作成した後に以下のコードを実行します。
model = Sequential()
embedding_layer = Embedding(vocab_size, 100, weights=[embedding_matrix], input_length=maxlen , trainable=False)
model.add(embedding_layer)
model.add(Conv1D(128, 5, activation='relu'))
model.add(GlobalMaxPooling1D())
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
上のスクリプトでは、逐次モデルを作成し、次にエンベッディング・レイヤーを作成しています。このステップは、先ほどと同じです。次に、128の特徴(カーネル)を持つ1次元の畳み込み層を作成します。カーネルサイズは5で、活性化関数はシグモイドを使用する。次に、特徴サイズを小さくするために、グローバルマックスプーリング層を追加する。最後に、シグモイド活性化を持つ密な層を追加します。コンパイルのプロセスは前のセクションと同じです。
それでは、我々のモデルの概要を見てみましょう。
print(model.summary())
_________________________________________________________________
Layer (type) Output Shape Param #=================================================================
embedding_2 (Embedding) (None, 100, 100) 9254700
_________________________________________________________________
conv1d_1 (Conv1D) (None, 96, 128) 64128
_________________________________________________________________
global_max_pooling1d_1 (Glob (None, 128) 0
_________________________________________________________________
dense_2 (Dense) (None, 1) 129
=================================================================
Total params: 9,318,957
Trainable params: 64,257
Non-trainable params: 9,254,700
上記のケースでは、埋め込み層を平坦化する必要がないことがわかります。また、プーリング層を使って、特徴量のサイズが小さくなっていることもわかります。
それでは、モデルを学習させ、学習セットで評価しましょう。モデルを学習し、テストするプロセスは同じです。そのためには、それぞれ fit
と evaluate
メソッドを使います。
history = model.fit(X_train, y_train, batch_size=128, epochs=6, verbose=1, validation_split=0.2)
score = model.evaluate(X_test, y_test, verbose=1)
次のスクリプトは結果を表示します。
print("Test Score:", score[0])
print("Test Accuracy:", score[1])
学習精度とテスト精度を比較すると、CNNの学習精度は92%程度となり、単純なニューラルネットワークの学習精度よりも高いことがわかります。テスト精度はCNNで約82%で、これも単純なニューラルネットワークのテスト精度(約74%)より高いです。
しかし、学習精度とテスト精度の間に大きな差があるため、CNNモデルはまだオーバーフィットしています。トレーニングセットとテストセットの損失と精度の差をプロットしてみましょう。
import matplotlib.pyplot as plt
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train','test'], loc = 'upper left')
plt.show()
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train','test'], loc = 'upper left')
plt.show()
出力
トレーニングセットとテストセットの損失と精度の違いがよくわかります。
それでは、3つ目のディープラーニングモデルであるリカレントニューラルネットワークをトレーニングし、オーバーフィッティングを解消できるかどうか見てみましょう。
リカレントニューラルネットワーク(LSTM)によるテキスト分類
リカレントニューラルネットワークは、シーケンスデータでうまく動作することが証明されているニューラルネットワークの一種です。テキストは実際には単語の列なので、リカレントニューラルネットワークはテキスト関連の問題を解決するために自動的に選択されます。ここでは、RNNの変種であるLSTM(Long Short Term Memory network)を使って、センチメント分類の問題を解くことにする。
もう一度、単語埋め込みの部分まで実行し、その後、以下のコードを実行してください。
model = Sequential()
embedding_layer = Embedding(vocab_size, 100, weights=[embedding_matrix], input_length=maxlen , trainable=False)
model.add(embedding_layer)
model.add(LSTM(128))
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
上のスクリプトでは、まず逐次モデルを初期化し、次に埋め込み層を作成しています。次に、128ニューロンからなるLSTM層を作成します(ニューロン数は自由に変更できます)。残りのコードはCNNの場合と同じである。
このモデルの概要をプロットしてみよう。
print(model.summary())
モデルの概要は以下のようになる。
_________________________________________________________________
Layer (type) Output Shape Param #=================================================================
embedding_3 (Embedding) (None, 100, 100) 9254700
_________________________________________________________________
lstm_1 (LSTM) (None, 128) 117248
_________________________________________________________________
dense_3 (Dense) (None, 1) 129
=================================================================
Total params: 9,372,077
Trainable params: 117,377
Non-trainable params: 9,254,700
次のステップは、トレーニングセットでモデルを学習させ、テストセットでその性能を評価することである。
history = model.fit(X_train, y_train, batch_size=128, epochs=6, verbose=1, validation_split=0.2)
score = model.evaluate(X_test, y_test, verbose=1)
上のスクリプトは、テストセットでモデルを学習させます。バッチサイズは128、エポック数は6です。学習が終了した時点で、学習精度が約85.40%であることがわかります。
モデルの学習が完了したら、以下のスクリプトでテストセットでのモデル結果を確認することができます。
print("Test Score:", score[0])
print("Test Accuracy:", score[1])
出力では、テスト精度が約85.04%であることがわかります。このテスト精度は、CNNと密結合ニューラルネットワークの両方よりも優れています。また、学習精度とテスト精度の差が非常に小さいことから、このモデルがオーバーフィットしていないことがわかります。
トレーニングセットとテストセットの損失と精度の差をプロットしてみましょう。
import matplotlib.pyplot as plt
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train','test'], loc='upper left')
plt.show()
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train','test'], loc='upper left')
plt.show()
出力
この出力から、トレーニングセットとテストセットの精度値の差は、単純なニューラルネットワークやCNNと比較して非常に小さいことがわかります。同様に、損失値の差も無視できるほど小さく、我々のモデルがオーバーフィットしていないことを示しています。このように、今回の問題では、RNNが最適なアルゴリズムであると結論付けることができます。
この記事では、層数、ニューロン、ハイパーパラメータなどをランダムに選択しました。この記事で取り上げた3つのニューラルネットワークの層数、ニューロン数、活性化関数をすべて変えてみて、どのニューラルネットワークが一番うまくいくかを確認することをお勧めします。
シングルインスタンスでの予測
この記事の最後のセクションは、単一のインスタンスまたは単一のセンチメントに対して予測を行う方法を説明します。コーパスから任意のレビューを取得し、そのセンチメントを予測することを試みます。
まず、コーパスから任意のレビューをランダムに選択します。
instance = X[57]
print(instance)
出力
I laughed all the way through this rotten movie It so unbelievable woman leaves her husband after many years of marriage has breakdown in front of real estate office What happens The office manager comes outside and offers her job Hilarious Next thing you know the two women are going at it Yep they re lesbians Nothing rings true in this Lifetime for Women with nothing better to do movie Clunky dialogue like don want to spend the rest of my life feeling like had chance to be happy and didn take it doesn help There a wealthy distant mother who disapproves of her daughter new relationship sassy black maid unbelievable that in the year film gets made in which there a sassy black maid Hattie McDaniel must be turning in her grave The woman has husband who freaks out and wants custody of the snotty teenage kids Sheesh No cliche is left unturned
これは明らかに否定的なレビューであることがわかる。このレビューのセンチメントを予測するために、このレビューを数値形式に変換する必要があります。これは単語の埋め込みで作成した tokenizer
を使って行うことができます。text_to_sequences` メソッドは文章を数値に変換する。
次に、コーパスの場合と同じように、入力配列をパッドで埋める必要がある。最後に、モデルの predict
メソッドを使用して、処理した入力配列を渡します。次のコードを見てください。
instance = tokenizer.texts_to_sequences(instance)
flat_list = []
for sublist in instance:
for item in sublist:
flat_list.append(item)
flat_list = [flat_list]
instance = pad_sequences(flat_list, padding='post', maxlen=maxlen)
model.predict(instance)
出力はこのようになる。
array([[0.3304276]], dtype=float32)
しかし、シグモイド関数は0と1の間の浮動値を予測します。 値が0.5より小さい場合、センチメントはネガティブと見なされ、値が0.5より大きい場合、センチメントはポジティブと見なされます。このインスタンスのセンチメント値は0.33であり、センチメントはネガティブと予測され、実際その通りでした。
結論
テキスト分類は、最も一般的な自然言語処理タスクの1つです。この記事では、Keras深層学習ライブラリを使用してテキスト分類の一種であるセンチメント分析を実行する方法を見ました。3種類のニューラルネットワークを使用して、さまざまな映画に関する一般人の感情を分類しました。その結果、RNNの変種であるLSTMが、CNNと単純なニューラルネットワークの両方を上回ることが分かりました。