TensorFlow 2.0 Alpha : Beginner Tutorials : ML 基本 :- 映画レビューでテキスト分類 (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 03/27/2019
* 本ページは、TensorFlow の本家サイトの TF 2.0 Alpha – Beginner Tutorials – ML basics の以下のページを翻訳した上で
適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
ML 基本 :- 映画レビューでテキスト分類
このノートブックは (映画) レビューのテキストを使用して映画レビューを肯定的か否定的として分類します。これは二値 — あるいは 2 クラス — 分類の例で、重要で広く利用可能な種類の機械学習問題です。
私達は IMDB データセット を使用します、これは Internet Movie Database からの 50,000 映画レビューのテキストを含みます。これらは訓練のための 25,000 レビューとテストのための 25,000 レビューに分割されます。訓練とテストセットは均等です、つまりそれらがポジティブとネガティブ・レビューの同じ数を含むことを意味します。
この notebook は tf.keras を使用します、TensorFlow でモデルを構築して訓練するための高位 API です。tf.keras を使用したより進んだテキスト分類チュートリアルについては、MLCC テキスト分類ガイド を見てください。
from __future__ import absolute_import, division, print_function !pip install -q tensorflow==2.0.0-alpha0 import tensorflow as tf from tensorflow import keras import numpy as np print(tf.__version__)
2.0.0-alpha0
IMDB データセットをダウンロードする
IMDB データセットは TensorFlow でパッケージ化されています。それは既にレビュー (単語のシークエンス) が数字のシークエンスに変換されるように前処理されていて、そこでは各整数は辞書の特定の単語を表しています。
次のコードは IMDB データセットを貴方のマシンにダウンロードします (あるいは既にそれをダウンロードしているのであればキャッシュされたコピーを使用します) :
imdb = keras.datasets.imdb (train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb.npz 17465344/17464789 [==============================] - 0s 0us/step
引数 num_words=10000 は訓練データにおいて最も頻度高く出現する top 10,000 の単語を保持します。データのサイズを管理可能に保つために稀な単語は捨てられます。
データを調査する
データのフォーマットを理解するために少し時間をつかいましょう。データセットは前処理されています : 各サンプルは映画レビューの単語を表わす数字の配列です。各ラベルは 0 か 1 の整数値で、そこでは 0 は否定的なレビューで、1 は肯定的なレビューです。
print("Training entries: {}, labels: {}".format(len(train_data), len(train_labels)))
Training entries: 25000, labels: 25000
レビューのテキストは整数に変換され、そこでは各整数は辞書の特定の単語を表します。最初のレビューがどのように見えるかがここにあります :
print(train_data[0])
[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32]
映画レビューは異なる長さかもしれません。下のコードは最初と 2 番目のレビューの単語の数を示します。ニューラルネットワークへの入力は同じ長さでなければなりませんので、これを後で解決する必要があります。
len(train_data[0]), len(train_data[1])
(218, 189)
整数を単語に変換し戻す
整数をどのようにテキストに変換し戻すかを知ることは有用かもしれません。ここで、整数から文字列へのマッピングを含む辞書オブジェクトに問い合わせるためのヘルパー関数を作成します :
# A dictionary mapping words to an integer index word_index = imdb.get_word_index() # The first indices are reserved word_index = {k:(v+3) for k,v in word_index.items()} word_index[""] = 0 word_index[" "] = 1 word_index[" "] = 2 # unknown word_index[" "] = 3 reverse_word_index = dict([(value, key) for (key, value) in word_index.items()]) def decode_review(text): return ' '.join([reverse_word_index.get(i, '?') for i in text])
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb_word_index.json 1646592/1641221 [==============================] - 0s 0us/step
今では最初のレビューのテキストを表示するために decode_review 関数を使用することができます :
decode_review(train_data[0])
"<START> this film was just brilliant casting location scenery story direction everyone's really suited the part they played and you could just imagine being there robert <UNK> is an amazing actor and now the same being director <UNK> father came from the same scottish island as myself so i loved the fact there was a real connection with this film the witty remarks throughout the film were great it was just brilliant so much that i bought the film as soon as it was released for <UNK> and would recommend it to everyone to watch and the fly fishing was amazing really cried at the end it was so sad and you know what they say if you cry at a film it must have been good and this definitely was also <UNK> to the two little boy's that played the <UNK> of norman and paul they were just brilliant children are often left out of the <> list i think because the stars that play them all grown up are such a big profile for the whole film but these children are amazing and should be praised for what they have done don't you think the whole story was so lovely because it was true and was someone's life after all that was shared with us all"
データを準備する
レビュー — 整数の配列 — はニューラルネットワークに供給される前に tensor に変換されなければなりません。この変換は 2 つの方法で成されます :
- 配列を単語の出現を示す 0 と 1 のベクトルに変換します、one-hot エンコーディングに類似しています。例えば、シークエンス [3, 5] は、(1 である) インデックス 3 と 5 を除いて総てゼロの 10,000 次元ベクトルになるでしょう。それから、これをネットワークの最初の層 — Dense 層 — にします、これは浮動小数点ベクトルデータを処理できます。けれども、このアプローチはメモリ集約的で、num_words * num_reviews サイズ行列を必要とします。
- 代わりに、配列をそれらが総て同じ長さを持つようにパッドすることもできます、それから shape num_examples * max_length の整数 tensor を作成します。この shape をネットワークの最初の層として扱う埋め込み層を使用できます。
このチュートリアルでは、2 番目のアプローチを使用します。
映画レビューは同じ長さでなければならないので、長さを標準化するために pad_sequences 関数を使用します :
train_data = keras.preprocessing.sequence.pad_sequences(train_data, value=word_index[""], padding='post', maxlen=256) test_data = keras.preprocessing.sequence.pad_sequences(test_data, value=word_index[" "], padding='post', maxlen=256)
さてサンプルの長さを見てみましょう :
len(train_data[0]), len(train_data[1])
(256, 256)
そして (今はパッドされた) 最初のレビューを調査します :
print(train_data[0])
[ 1 14 22 16 43 530 973 1622 1385 65 458 4468 66 3941 4 173 36 256 5 25 100 43 838 112 50 670 2 9 35 480 284 5 150 4 172 112 167 2 336 385 39 4 172 4536 1111 17 546 38 13 447 4 192 50 16 6 147 2025 19 14 22 4 1920 4613 469 4 22 71 87 12 16 43 530 38 76 15 13 1247 4 22 17 515 17 12 16 626 18 2 5 62 386 12 8 316 8 106 5 4 2223 5244 16 480 66 3785 33 4 130 12 16 38 619 5 25 124 51 36 135 48 25 1415 33 6 22 12 215 28 77 52 5 14 407 16 82 2 8 4 107 117 5952 15 256 4 2 7 3766 5 723 36 71 43 530 476 26 400 317 46 7 4 2 1029 13 104 88 4 381 15 297 98 32 2071 56 26 141 6 194 7486 18 4 226 22 21 134 476 26 480 5 144 30 5535 18 51 36 28 224 92 25 104 4 226 65 16 38 1334 88 12 16 283 5 16 4472 113 103 32 15 16 5345 19 178 32 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
モデルを構築する
ニューラルネットワークは層をスタックすることにより作成されます — これは 2 つの主要なアーキテクチャ的な決定を必要とします :
- モデルで幾つの層を使用するか?
- 各層のために幾つの隠れユニットを使用するか?
このサンプルでは、入力データは単語インデックスの配列から成ります。予測するラベルは 0 か 1 です。この問題に対するモデルを構築しましょう :
# input shape is the vocabulary count used for the movie reviews (10,000 words) vocab_size = 10000 model = keras.Sequential() model.add(keras.layers.Embedding(vocab_size, 16)) model.add(keras.layers.GlobalAveragePooling1D()) model.add(keras.layers.Dense(16, activation='relu')) model.add(keras.layers.Dense(1, activation='sigmoid')) model.summary()
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding (Embedding) (None, None, 16) 160000 _________________________________________________________________ global_average_pooling1d (Gl (None, 16) 0 _________________________________________________________________ dense (Dense) (None, 16) 272 _________________________________________________________________ dense_1 (Dense) (None, 1) 17 ================================================================= Total params: 160,289 Trainable params: 160,289 Non-trainable params: 0 _________________________________________________________________
分類器を構築するために層はシーケンシャルにスタックされます :
- 最初の層は Embedding 層です。この層は整数エンコードされた語彙を取って各単語インデックスのための埋め込みベクトルを検索します。これらのベクトルはモデルが訓練されるときに学習されます。ベクトルは出力配列に次元を追加します。結果としての次元は (batch, sequence, embedding) です。
- 次に、GlobalAveragePooling1D 層は各サンプルについて sequence 次元に渡り平均することにより固定長出力ベクトルを返します。これは可能な最も単純な方法でモデルが可変長の入力を扱うことを可能にします。
- この固定長出力ベクトルは 16 隠れユニットを持つ完全結合 (Dense) 層を通してパイプされます。
- 最後の層は単一の出力ノードに密に接続されています。sigmoid 活性化関数を使用し、この値は 0 と 1 の間の浮動小数点で、確率、または確信レベルを表します。
隠れユニット
上のモデルは入力と出力の間に、2 つの中間層あるいは「隠れ」層を持ちます。出力 (ユニット、ノード、またはニューロン) の数は層のための具象空間の次元です。換言すれば、内部表現を学習するときにネットワークが許容される自由度の総量です。
モデルがより多くの隠れユニット (より高い次元の表現空間) and/or より多くの層を持てば、ネットワークはより複雑な表現を学習できます。けれども、それはネットワークをより計算的に高価にして望まないパターンを学習することに繋がるかもしれません — このパターンは訓練データ上の性能を改良しますがテストデータ上ではそうではないものです。これは overfitting と呼ばれ、後でそれを調査します。
損失関数と optimizer
モデルは訓練のために損失関数と optimizer を必要とします。これは二値分類問題でモデルは確率を出力します (sigmoid 活性を持つシングルユニット層) ので、binary_crossentropy 損失関数を使用します。
これは損失関数のための唯一の選択ではありません、例えば、mean_squared_error を選択できるでしょう。しかし、一般的に、binary_crossentropy は確率を扱うためにはより良いです — それは確率分布間、あるいは私達のケースでは、正解の分布と予測の間の「距離」を測ります。
後で、回帰問題 (例えば、家の価格を予測する) を調べているときに、mean squared error と呼ばれるもう一つの損失関数をどのように使用するかを見ます。
さて、optimizer と損失関数を使用するためにモデルを configure します :
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
検証セットを作成する
訓練時、モデルの精度をそれが前に見ていないデータ上でチェックすることを望みます。元の訓練データから 10,000 サンプルを分離することによって検証セットを作成します。(テストセットを何故今使わないのでしょう?目標は訓練データのみを使用してモデルを開発して調整することですから、テストデータは精度を評価するためだけに一度だけ使用します。)
x_val = train_data[:10000] partial_x_train = train_data[10000:] y_val = train_labels[:10000] partial_y_train = train_labels[10000:]
モデルを訓練する
モデルを 512 サンプルのミニバッチで 40 エポック訓練します。これは x_train と y_train tensor の総てのサンプルに渡る 40 iteration (反復) です。訓練の間、検証セットからの 10,000 サンプル上でモデルの損失と精度を監視します :
history = model.fit(partial_x_train, partial_y_train, epochs=40, batch_size=512, validation_data=(x_val, y_val), verbose=1)
Train on 15000 samples, validate on 10000 samples Epoch 1/40 15000/15000 [==============================] - 1s 71us/sample - loss: 0.6923 - accuracy: 0.5915 - val_loss: 0.6907 - val_accuracy: 0.7308 Epoch 2/40 15000/15000 [==============================] - 1s 59us/sample - loss: 0.6880 - accuracy: 0.7417 - val_loss: 0.6851 - val_accuracy: 0.7471 Epoch 3/40 15000/15000 [==============================] - 1s 59us/sample - loss: 0.6794 - accuracy: 0.7533 - val_loss: 0.6742 - val_accuracy: 0.7330 Epoch 4/40 15000/15000 [==============================] - 1s 59us/sample - loss: 0.6643 - accuracy: 0.7613 - val_loss: 0.6564 - val_accuracy: 0.7254 Epoch 5/40 15000/15000 [==============================] - 1s 58us/sample - loss: 0.6408 - accuracy: 0.7765 - val_loss: 0.6312 - val_accuracy: 0.7571 Epoch 6/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.6097 - accuracy: 0.7941 - val_loss: 0.6000 - val_accuracy: 0.7910 Epoch 7/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.5727 - accuracy: 0.8119 - val_loss: 0.5641 - val_accuracy: 0.7990 Epoch 8/40 15000/15000 [==============================] - 1s 59us/sample - loss: 0.5328 - accuracy: 0.8303 - val_loss: 0.5279 - val_accuracy: 0.8178 Epoch 9/40 15000/15000 [==============================] - 1s 57us/sample - loss: 0.4926 - accuracy: 0.8463 - val_loss: 0.4922 - val_accuracy: 0.8288 Epoch 10/40 15000/15000 [==============================] - 1s 58us/sample - loss: 0.4542 - accuracy: 0.8590 - val_loss: 0.4599 - val_accuracy: 0.8371 Epoch 11/40 15000/15000 [==============================] - 1s 56us/sample - loss: 0.4195 - accuracy: 0.8669 - val_loss: 0.4312 - val_accuracy: 0.8470 Epoch 12/40 15000/15000 [==============================] - 1s 56us/sample - loss: 0.3887 - accuracy: 0.8759 - val_loss: 0.4075 - val_accuracy: 0.8536 Epoch 13/40 15000/15000 [==============================] - 1s 58us/sample - loss: 0.3626 - accuracy: 0.8838 - val_loss: 0.3859 - val_accuracy: 0.8594 Epoch 14/40 15000/15000 [==============================] - 1s 56us/sample - loss: 0.3389 - accuracy: 0.8897 - val_loss: 0.3693 - val_accuracy: 0.8631 Epoch 15/40 15000/15000 [==============================] - 1s 56us/sample - loss: 0.3192 - accuracy: 0.8933 - val_loss: 0.3551 - val_accuracy: 0.8673 Epoch 16/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.3014 - accuracy: 0.8997 - val_loss: 0.3433 - val_accuracy: 0.8701 Epoch 17/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.2854 - accuracy: 0.9041 - val_loss: 0.3334 - val_accuracy: 0.8705 Epoch 18/40 15000/15000 [==============================] - 1s 56us/sample - loss: 0.2713 - accuracy: 0.9089 - val_loss: 0.3244 - val_accuracy: 0.8761 Epoch 19/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.2587 - accuracy: 0.9110 - val_loss: 0.3169 - val_accuracy: 0.8768 Epoch 20/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.2472 - accuracy: 0.9160 - val_loss: 0.3114 - val_accuracy: 0.8762 Epoch 21/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.2361 - accuracy: 0.9194 - val_loss: 0.3063 - val_accuracy: 0.8769 Epoch 22/40 15000/15000 [==============================] - 1s 57us/sample - loss: 0.2266 - accuracy: 0.9219 - val_loss: 0.3014 - val_accuracy: 0.8812 Epoch 23/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.2169 - accuracy: 0.9257 - val_loss: 0.2986 - val_accuracy: 0.8802 Epoch 24/40 15000/15000 [==============================] - 1s 57us/sample - loss: 0.2083 - accuracy: 0.9283 - val_loss: 0.2943 - val_accuracy: 0.8819 Epoch 25/40 15000/15000 [==============================] - 1s 54us/sample - loss: 0.1994 - accuracy: 0.9327 - val_loss: 0.2913 - val_accuracy: 0.8825 Epoch 26/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.1914 - accuracy: 0.9354 - val_loss: 0.2899 - val_accuracy: 0.8815 Epoch 27/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.1839 - accuracy: 0.9387 - val_loss: 0.2880 - val_accuracy: 0.8832 Epoch 28/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.1767 - accuracy: 0.9428 - val_loss: 0.2863 - val_accuracy: 0.8838 Epoch 29/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.1706 - accuracy: 0.9443 - val_loss: 0.2869 - val_accuracy: 0.8826 Epoch 30/40 15000/15000 [==============================] - 1s 56us/sample - loss: 0.1642 - accuracy: 0.9472 - val_loss: 0.2851 - val_accuracy: 0.8839 Epoch 31/40 15000/15000 [==============================] - 1s 59us/sample - loss: 0.1578 - accuracy: 0.9505 - val_loss: 0.2847 - val_accuracy: 0.8855 Epoch 32/40 15000/15000 [==============================] - 1s 56us/sample - loss: 0.1519 - accuracy: 0.9531 - val_loss: 0.2850 - val_accuracy: 0.8855 Epoch 33/40 15000/15000 [==============================] - 1s 58us/sample - loss: 0.1463 - accuracy: 0.9545 - val_loss: 0.2860 - val_accuracy: 0.8836 Epoch 34/40 15000/15000 [==============================] - 1s 57us/sample - loss: 0.1413 - accuracy: 0.9572 - val_loss: 0.2863 - val_accuracy: 0.8858 Epoch 35/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.1368 - accuracy: 0.9586 - val_loss: 0.2879 - val_accuracy: 0.8863 Epoch 36/40 15000/15000 [==============================] - 1s 57us/sample - loss: 0.1317 - accuracy: 0.9608 - val_loss: 0.2888 - val_accuracy: 0.8865 Epoch 37/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.1270 - accuracy: 0.9629 - val_loss: 0.2901 - val_accuracy: 0.8859 Epoch 38/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.1226 - accuracy: 0.9652 - val_loss: 0.2923 - val_accuracy: 0.8848 Epoch 39/40 15000/15000 [==============================] - 1s 57us/sample - loss: 0.1189 - accuracy: 0.9657 - val_loss: 0.2939 - val_accuracy: 0.8838 Epoch 40/40 15000/15000 [==============================] - 1s 55us/sample - loss: 0.1147 - accuracy: 0.9679 - val_loss: 0.2955 - val_accuracy: 0.8850
モデルを評価する
そしてモデルどのように遂行するか見ましょう。2 つの値が返されます。損失 (エラーを表わす数字です、より低ければより良いです)、そして精度です。
results = model.evaluate(test_data, test_labels) print(results)
25000/25000 [==============================] - 1s 45us/sample - loss: 0.3145 - accuracy: 0.8742 [0.3145076360368729, 0.87424]
このかなり素朴なアプローチは約 87 % の精度を得ます。より進んだアプローチでは、モデルは 95 % に近づくはずです。
時間とともに精度と損失のグラフを作成する
model.fit() は History オブジェクトを返します、これは訓練の間に発生した総てを持つ辞書を含みます :
history_dict = history.history history_dict.keys()
dict_keys(['val_accuracy', 'accuracy', 'loss', 'val_loss'])
4 つのエントリがあります: 訓練と検証の間に各々監視されたメトリックのために一つ (ずつ) です。比較のために訓練と検証精度に加えて、訓練と検証損失をプロットするためにこれらを使用することができます :
import matplotlib.pyplot as plt acc = history_dict['accuracy'] val_acc = history_dict['val_accuracy'] loss = history_dict['loss'] val_loss = history_dict['val_loss'] epochs = range(1, len(acc) + 1) # "bo" is for "blue dot" plt.plot(epochs, loss, 'bo', label='Training loss') # b is for "solid blue line" plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.xlabel('Epochs') plt.ylabel('Loss') plt.legend() plt.show()
<Figure size 640x480 with 1 Axes>
plt.clf() # clear figure plt.plot(epochs, acc, 'bo', label='Training acc') plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy') plt.xlabel('Epochs') plt.ylabel('Accuracy') plt.legend() plt.show()
このプロットでは、点線は訓練損失と精度を表し、そして実線は検証損失と精度です。
訓練損失は各エポックとともに減少して訓練精度は各エポックで増加することに気がつくでしょう。これは勾配降下最適化を使用するときに期待されるものです — それは総ての反復で望まれる量を最小化するはずです。
これは検証損失と精度については当てはまりません — それらは約 20 エポック後に最大になるようです。これは overfitting の例です : モデルは、それが前に決して見ていないデータ上よりも訓練データ上でより上手く遂行します。このポイント後、モデルは過剰に最適化されてテストデータに一般化されない訓練データに固有の表現を学習します。
この特定のケースのためには、単純に 20 程度のエポック後に訓練を停止することで overfitting を回避できるでしょう。後で、これを callback で自動的にどのように行なうかを見るでしょう。
以上