TensorFlow使ってみた~IMDB~L2正則化&ドロップアウト

映画レビューのテキスト分類問題では、過学習が起きていました。

maedax.hatenablog.com

この記事のチュートリアルでは過学習を抑制する手法を実装します。

www.tensorflow.org

このノートブックでは、重みの正則化ドロップアウトという、よく使われる2つの正則化テクニックをご紹介します。
これらを使って、IMDBの映画レビューを分類するノートブックの改善を図ります。

この記事ではGoogle Colabを使用していません。
Colabだとクラッシュしてしまうので、ローカルで環境を用意しました。

諸々インポート

from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf
from tensorflow import keras

import numpy as np
import matplotlib.pyplot as plt

print(tf.__version__)

IMDBデータセットのダウンロード

以前のノートブックで使用したエンベディングの代わりに、ここでは文をマルチホットエンコードします。
このモデルは、訓練用データセットをすぐに過学習します。
このモデルを使って、過学習がいつ起きるかということと、どうやって過学習と戦うかをデモします。

リストをマルチホットエンコードすると言うのは、0と1のベクトルにするということです。
具体的にいうと、例えば[3, 5]というシーケンスを、インデックス3と5の値が1で、それ以外がすべて0の、10,000次元のベクトルに変換するということを意味します。

NUM_WORDS = 10000

(train_data, train_labels), (test_data, test_labels) = keras.datasets.imdb.load_data(num_words=NUM_WORDS)

def multi_hot_sequences(sequences, dimension):
    # 形状が (len(sequences), dimension)ですべて0の行列を作る
    results = np.zeros((len(sequences), dimension))
    for i, word_indices in enumerate(sequences):
        results[i, word_indices] = 1.0  # 特定のインデックスに対してresults[i] を1に設定する
    return results


train_data = multi_hot_sequences(train_data, dimension=NUM_WORDS)
test_data = multi_hot_sequences(test_data, dimension=NUM_WORDS)

結果として得られるマルチホットベクトルの1つを見てみましょう。
単語のインデックスは頻度順にソートされています。
このため、インデックスが0に近いほど1が多く出現するはずです。分布を見てみましょう。

plt.plot(train_data[0])

出力

マルチホットベクトルの分布
マルチホットベクトルの分布

過学習のデモ

オーバーフィッティングを防ぐ最も簡単な方法は、小さなモデルから始めることです。
学習可能なパラメータの数が少ないモデル(層の数と層ごとのユニットの数によって決定される)。
ディープラーニングでは、モデルの学習可能なパラメータの数は、しばしばモデルの「キャパシティ」と呼ばれます。

直感的には、より多くのパラメータを持つモデルは、より多くの「記憶容量」を持ち、したがって、訓練サンプルとそのターゲットの間の完全な辞書のようなマッピングを簡単に学習することができますが、一般化する力のないマッピングは、以前に見たことのないデータで予測を行う場合には役に立たないでしょう。
このことを常に念頭に置いてください:深層学習モデルは訓練データにフィットするのが得意な傾向がありますが、本当の課題は一般化であって、フィットすることではありません。

一方、ネットワークの記憶リソースが限られている場合、マッピングを簡単に学習することはできません。
その損失を最小限に抑えるためには、より予測力の高い圧縮表現を学習する必要があります。
同時に、モデルを小さくしすぎると、学習データにフィットするのが難しくなります。
「容量が多すぎる」と「容量が足りない」のバランスがあります。

残念ながら、モデルの正しいサイズやアーキテクチャ(レイヤーの数や各レイヤーの正しいサイズ)を決定する魔法の公式はありません。一連の異なるアーキテクチャを使用して実験する必要があります。

適切なモデル・サイズを見つけるには、比較的少数のレイヤとパラメータから始めて、検証損失のリターンが減少するのがわかるまでレイヤのサイズを大きくするか、新しいレイヤを追加するのがベストです。
ベースラインとして layers.Dense のみを使用したシンプルなモデルから始め、より大きなバージョンを作成して比較します。
※www.DeepL.com/Translator(無料版)で翻訳しました。

比較基準を作る

baseline_model = keras.Sequential([
    # `.summary` を見るために`input_shape`が必要 
    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

baseline_model.compile(optimizer='adam',
                       loss='binary_crossentropy',
                       metrics=['accuracy', 'binary_crossentropy'])

baseline_model.summary()

出力

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 16)                160016    
_________________________________________________________________
dense_1 (Dense)              (None, 16)                272       
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 17        
=================================================================
Total params: 160,305
Trainable params: 160,305
Non-trainable params: 0
_________________________________________________________________
baseline_history = baseline_model.fit(train_data,
                                      train_labels,
                                      epochs=20,
                                      batch_size=512,
                                      validation_data=(test_data, test_labels),
                                      verbose=2)

出力

Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 5s - loss: 0.4640 - accuracy: 0.8100 - binary_crossentropy: 0.4640 - val_loss: 0.3234 - val_accuracy: 0.8834 - val_binary_crossentropy: 0.3234
Epoch 2/20
25000/25000 - 5s - loss: 0.2385 - accuracy: 0.9175 - binary_crossentropy: 0.2385 - val_loss: 0.2840 - val_accuracy: 0.8882 - val_binary_crossentropy: 0.2840
Epoch 3/20
25000/25000 - 4s - loss: 0.1757 - accuracy: 0.9401 - binary_crossentropy: 0.1757 - val_loss: 0.2949 - val_accuracy: 0.8831 - val_binary_crossentropy: 0.2949
Epoch 4/20
25000/25000 - 3s - loss: 0.1409 - accuracy: 0.9538 - binary_crossentropy: 0.1409 - val_loss: 0.3154 - val_accuracy: 0.8780 - val_binary_crossentropy: 0.3154
Epoch 5/20
25000/25000 - 3s - loss: 0.1156 - accuracy: 0.9639 - binary_crossentropy: 0.1156 - val_loss: 0.3466 - val_accuracy: 0.8743 - val_binary_crossentropy: 0.3466
Epoch 6/20
25000/25000 - 4s - loss: 0.0929 - accuracy: 0.9731 - binary_crossentropy: 0.0929 - val_loss: 0.3798 - val_accuracy: 0.8696 - val_binary_crossentropy: 0.3798
Epoch 7/20
25000/25000 - 4s - loss: 0.0759 - accuracy: 0.9796 - binary_crossentropy: 0.0759 - val_loss: 0.4200 - val_accuracy: 0.8658 - val_binary_crossentropy: 0.4200
Epoch 8/20
25000/25000 - 4s - loss: 0.0583 - accuracy: 0.9852 - binary_crossentropy: 0.0583 - val_loss: 0.4577 - val_accuracy: 0.8632 - val_binary_crossentropy: 0.4577
Epoch 9/20
25000/25000 - 3s - loss: 0.0432 - accuracy: 0.9918 - binary_crossentropy: 0.0432 - val_loss: 0.4984 - val_accuracy: 0.8619 - val_binary_crossentropy: 0.4984
Epoch 10/20
25000/25000 - 3s - loss: 0.0307 - accuracy: 0.9949 - binary_crossentropy: 0.0307 - val_loss: 0.5467 - val_accuracy: 0.8585 - val_binary_crossentropy: 0.5467
Epoch 11/20
25000/25000 - 3s - loss: 0.0214 - accuracy: 0.9974 - binary_crossentropy: 0.0214 - val_loss: 0.5864 - val_accuracy: 0.8577 - val_binary_crossentropy: 0.5864
Epoch 12/20
25000/25000 - 3s - loss: 0.0152 - accuracy: 0.9988 - binary_crossentropy: 0.0152 - val_loss: 0.6238 - val_accuracy: 0.8576 - val_binary_crossentropy: 0.6238
Epoch 13/20
25000/25000 - 4s - loss: 0.0110 - accuracy: 0.9992 - binary_crossentropy: 0.0110 - val_loss: 0.6535 - val_accuracy: 0.8570 - val_binary_crossentropy: 0.6535
Epoch 14/20
25000/25000 - 3s - loss: 0.0082 - accuracy: 0.9997 - binary_crossentropy: 0.0082 - val_loss: 0.6824 - val_accuracy: 0.8559 - val_binary_crossentropy: 0.6824
Epoch 15/20
25000/25000 - 3s - loss: 0.0064 - accuracy: 0.9998 - binary_crossentropy: 0.0064 - val_loss: 0.7166 - val_accuracy: 0.8560 - val_binary_crossentropy: 0.7166
Epoch 16/20
25000/25000 - 3s - loss: 0.0049 - accuracy: 0.9999 - binary_crossentropy: 0.0049 - val_loss: 0.7451 - val_accuracy: 0.8553 - val_binary_crossentropy: 0.7451
Epoch 17/20
25000/25000 - 3s - loss: 0.0038 - accuracy: 1.0000 - binary_crossentropy: 0.0038 - val_loss: 0.7688 - val_accuracy: 0.8551 - val_binary_crossentropy: 0.7688
Epoch 18/20
25000/25000 - 3s - loss: 0.0031 - accuracy: 1.0000 - binary_crossentropy: 0.0031 - val_loss: 0.7921 - val_accuracy: 0.8550 - val_binary_crossentropy: 0.7921
Epoch 19/20
25000/25000 - 3s - loss: 0.0026 - accuracy: 1.0000 - binary_crossentropy: 0.0026 - val_loss: 0.8113 - val_accuracy: 0.8545 - val_binary_crossentropy: 0.8113
Epoch 20/20
25000/25000 - 3s - loss: 0.0022 - accuracy: 1.0000 - binary_crossentropy: 0.0022 - val_loss: 0.8320 - val_accuracy: 0.8546 - val_binary_crossentropy: 0.8320

より小さいモデルの構築

今作成したばかりの比較基準となるモデルに比べて隠れユニット数が少ないモデルを作りましょう。

smaller_model = keras.Sequential([
    keras.layers.Dense(4, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(4, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

smaller_model.compile(optimizer='adam',
                      loss='binary_crossentropy',
                      metrics=['accuracy', 'binary_crossentropy'])

smaller_model.summary()

出力

Model: "sequential_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_9 (Dense)              (None, 4)                 40004     
_________________________________________________________________
dense_10 (Dense)             (None, 4)                 20        
_________________________________________________________________
dense_11 (Dense)             (None, 1)                 5         
=================================================================
Total params: 40,029
Trainable params: 40,029
Non-trainable params: 0
_________________________________________________________________

同じデータを使って訓練を行います。

smaller_history = smaller_model.fit(train_data,
                                    train_labels,
                                    epochs=20,
                                    batch_size=512,
                                    validation_data=(test_data, test_labels),
                                    verbose=2)

出力

Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 5s - loss: 0.6147 - accuracy: 0.7212 - binary_crossentropy: 0.6147 - val_loss: 0.5274 - val_accuracy: 0.8317 - val_binary_crossentropy: 0.5274
Epoch 2/20
25000/25000 - 3s - loss: 0.4284 - accuracy: 0.8729 - binary_crossentropy: 0.4284 - val_loss: 0.3866 - val_accuracy: 0.8688 - val_binary_crossentropy: 0.3866
Epoch 3/20
25000/25000 - 3s - loss: 0.3101 - accuracy: 0.8996 - binary_crossentropy: 0.3101 - val_loss: 0.3217 - val_accuracy: 0.8806 - val_binary_crossentropy: 0.3217
Epoch 4/20
25000/25000 - 3s - loss: 0.2485 - accuracy: 0.9165 - binary_crossentropy: 0.2485 - val_loss: 0.2967 - val_accuracy: 0.8848 - val_binary_crossentropy: 0.2967
Epoch 5/20
25000/25000 - 3s - loss: 0.2114 - accuracy: 0.9279 - binary_crossentropy: 0.2114 - val_loss: 0.2843 - val_accuracy: 0.8878 - val_binary_crossentropy: 0.2843
Epoch 6/20
25000/25000 - 3s - loss: 0.1851 - accuracy: 0.9366 - binary_crossentropy: 0.1851 - val_loss: 0.2826 - val_accuracy: 0.8868 - val_binary_crossentropy: 0.2826
Epoch 7/20
25000/25000 - 3s - loss: 0.1646 - accuracy: 0.9450 - binary_crossentropy: 0.1646 - val_loss: 0.2857 - val_accuracy: 0.8863 - val_binary_crossentropy: 0.2857
Epoch 8/20
25000/25000 - 3s - loss: 0.1483 - accuracy: 0.9512 - binary_crossentropy: 0.1483 - val_loss: 0.2956 - val_accuracy: 0.8827 - val_binary_crossentropy: 0.2956
Epoch 9/20
25000/25000 - 3s - loss: 0.1348 - accuracy: 0.9564 - binary_crossentropy: 0.1348 - val_loss: 0.3016 - val_accuracy: 0.8822 - val_binary_crossentropy: 0.3016
Epoch 10/20
25000/25000 - 3s - loss: 0.1227 - accuracy: 0.9608 - binary_crossentropy: 0.1227 - val_loss: 0.3140 - val_accuracy: 0.8802 - val_binary_crossentropy: 0.3140
Epoch 11/20
25000/25000 - 3s - loss: 0.1120 - accuracy: 0.9644 - binary_crossentropy: 0.1120 - val_loss: 0.3268 - val_accuracy: 0.8777 - val_binary_crossentropy: 0.3268
Epoch 12/20
25000/25000 - 3s - loss: 0.1024 - accuracy: 0.9689 - binary_crossentropy: 0.1024 - val_loss: 0.3407 - val_accuracy: 0.8760 - val_binary_crossentropy: 0.3407
Epoch 13/20
25000/25000 - 3s - loss: 0.0938 - accuracy: 0.9721 - binary_crossentropy: 0.0938 - val_loss: 0.3576 - val_accuracy: 0.8735 - val_binary_crossentropy: 0.3576
Epoch 14/20
25000/25000 - 3s - loss: 0.0858 - accuracy: 0.9750 - binary_crossentropy: 0.0858 - val_loss: 0.3744 - val_accuracy: 0.8709 - val_binary_crossentropy: 0.3744
Epoch 15/20
25000/25000 - 3s - loss: 0.0784 - accuracy: 0.9776 - binary_crossentropy: 0.0784 - val_loss: 0.3936 - val_accuracy: 0.8692 - val_binary_crossentropy: 0.3936
Epoch 16/20
25000/25000 - 3s - loss: 0.0719 - accuracy: 0.9810 - binary_crossentropy: 0.0719 - val_loss: 0.4107 - val_accuracy: 0.8679 - val_binary_crossentropy: 0.4107
Epoch 17/20
25000/25000 - 3s - loss: 0.0652 - accuracy: 0.9839 - binary_crossentropy: 0.0652 - val_loss: 0.4301 - val_accuracy: 0.8666 - val_binary_crossentropy: 0.4301
Epoch 18/20
25000/25000 - 3s - loss: 0.0595 - accuracy: 0.9857 - binary_crossentropy: 0.0595 - val_loss: 0.4518 - val_accuracy: 0.8650 - val_binary_crossentropy: 0.4518
Epoch 19/20
25000/25000 - 3s - loss: 0.0541 - accuracy: 0.9881 - binary_crossentropy: 0.0541 - val_loss: 0.4726 - val_accuracy: 0.8635 - val_binary_crossentropy: 0.4726
Epoch 20/20
25000/25000 - 3s - loss: 0.0498 - accuracy: 0.9897 - binary_crossentropy: 0.0498 - val_loss: 0.4932 - val_accuracy: 0.8623 - val_binary_crossentropy: 0.4932

より大きなモデルの構築

練習として、より大きなモデルを作成し、どれほど急速に過学習が起きるかを見ることもできます。
次はこのベンチマークに、この問題が必要とするよりはるかに容量の大きなネットワークを追加しましょう。

bigger_model = keras.models.Sequential([
    keras.layers.Dense(512, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(512, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

bigger_model.compile(optimizer='adam',
                     loss='binary_crossentropy',
                     metrics=['accuracy','binary_crossentropy'])

bigger_model.summary()

bigger_history = bigger_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)

出力

Model: "sequential_10"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_30 (Dense)             (None, 512)               5120512   
_________________________________________________________________
dense_31 (Dense)             (None, 512)               262656    
_________________________________________________________________
dense_32 (Dense)             (None, 1)                 513       
=================================================================
Total params: 5,383,681
Trainable params: 5,383,681
Non-trainable params: 0
_________________________________________________________________
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 23s - loss: 0.3399 - accuracy: 0.8558 - binary_crossentropy: 0.3399 - val_loss: 0.2920 - val_accuracy: 0.8816 - val_binary_crossentropy: 0.2920
Epoch 2/20
25000/25000 - 24s - loss: 0.1404 - accuracy: 0.9493 - binary_crossentropy: 0.1404 - val_loss: 0.3293 - val_accuracy: 0.8756 - val_binary_crossentropy: 0.3293
Epoch 3/20
25000/25000 - 24s - loss: 0.0463 - accuracy: 0.9864 - binary_crossentropy: 0.0463 - val_loss: 0.4360 - val_accuracy: 0.8695 - val_binary_crossentropy: 0.4360
Epoch 4/20
25000/25000 - 24s - loss: 0.0075 - accuracy: 0.9988 - binary_crossentropy: 0.0075 - val_loss: 0.6165 - val_accuracy: 0.8703 - val_binary_crossentropy: 0.6165
Epoch 5/20
25000/25000 - 25s - loss: 7.3355e-04 - accuracy: 1.0000 - binary_crossentropy: 7.3355e-04 - val_loss: 0.7182 - val_accuracy: 0.8700 - val_binary_crossentropy: 0.7182
Epoch 6/20
25000/25000 - 25s - loss: 2.1531e-04 - accuracy: 1.0000 - binary_crossentropy: 2.1531e-04 - val_loss: 0.7602 - val_accuracy: 0.8720 - val_binary_crossentropy: 0.7602
Epoch 7/20
25000/25000 - 25s - loss: 1.3107e-04 - accuracy: 1.0000 - binary_crossentropy: 1.3107e-04 - val_loss: 0.7923 - val_accuracy: 0.8717 - val_binary_crossentropy: 0.7923
Epoch 8/20
25000/25000 - 24s - loss: 9.3464e-05 - accuracy: 1.0000 - binary_crossentropy: 9.3464e-05 - val_loss: 0.8164 - val_accuracy: 0.8721 - val_binary_crossentropy: 0.8164
Epoch 9/20
25000/25000 - 24s - loss: 7.0720e-05 - accuracy: 1.0000 - binary_crossentropy: 7.0720e-05 - val_loss: 0.8392 - val_accuracy: 0.8719 - val_binary_crossentropy: 0.8392
Epoch 10/20
25000/25000 - 24s - loss: 5.5254e-05 - accuracy: 1.0000 - binary_crossentropy: 5.5254e-05 - val_loss: 0.8583 - val_accuracy: 0.8715 - val_binary_crossentropy: 0.8583
Epoch 11/20
25000/25000 - 24s - loss: 4.4350e-05 - accuracy: 1.0000 - binary_crossentropy: 4.4350e-05 - val_loss: 0.8763 - val_accuracy: 0.8715 - val_binary_crossentropy: 0.8763
Epoch 12/20
25000/25000 - 24s - loss: 3.6208e-05 - accuracy: 1.0000 - binary_crossentropy: 3.6208e-05 - val_loss: 0.8923 - val_accuracy: 0.8716 - val_binary_crossentropy: 0.8923
Epoch 13/20
25000/25000 - 24s - loss: 2.9936e-05 - accuracy: 1.0000 - binary_crossentropy: 2.9936e-05 - val_loss: 0.9085 - val_accuracy: 0.8713 - val_binary_crossentropy: 0.9085
Epoch 14/20
25000/25000 - 24s - loss: 2.5072e-05 - accuracy: 1.0000 - binary_crossentropy: 2.5072e-05 - val_loss: 0.9230 - val_accuracy: 0.8716 - val_binary_crossentropy: 0.9230
Epoch 15/20
25000/25000 - 24s - loss: 2.1205e-05 - accuracy: 1.0000 - binary_crossentropy: 2.1205e-05 - val_loss: 0.9372 - val_accuracy: 0.8717 - val_binary_crossentropy: 0.9372
Epoch 16/20
25000/25000 - 24s - loss: 1.8097e-05 - accuracy: 1.0000 - binary_crossentropy: 1.8097e-05 - val_loss: 0.9511 - val_accuracy: 0.8714 - val_binary_crossentropy: 0.9511
Epoch 17/20
25000/25000 - 24s - loss: 1.5544e-05 - accuracy: 1.0000 - binary_crossentropy: 1.5544e-05 - val_loss: 0.9639 - val_accuracy: 0.8716 - val_binary_crossentropy: 0.9639
Epoch 18/20
25000/25000 - 24s - loss: 1.3450e-05 - accuracy: 1.0000 - binary_crossentropy: 1.3450e-05 - val_loss: 0.9770 - val_accuracy: 0.8713 - val_binary_crossentropy: 0.9770
Epoch 19/20
25000/25000 - 24s - loss: 1.1714e-05 - accuracy: 1.0000 - binary_crossentropy: 1.1714e-05 - val_loss: 0.9885 - val_accuracy: 0.8715 - val_binary_crossentropy: 0.9885
Epoch 20/20
25000/25000 - 24s - loss: 1.0258e-05 - accuracy: 1.0000 - binary_crossentropy: 1.0258e-05 - val_loss: 1.0009 - val_accuracy: 0.8713 - val_binary_crossentropy: 1.0009

訓練時と検証時の損失をグラフにする

実線は訓練用データセットの損失、破線は検証用データセットでの損失です
(検証用データでの損失が小さい方が良いモデルです)。
これをみると、小さいネットワークのほうが比較基準のモデルよりも過学習が始まるのが遅いことがわかります
(4エポックではなく6エポック後)。
また、過学習が始まっても性能の低下がよりゆっくりしています。

def plot_history(histories, key='binary_crossentropy'):
  plt.figure(figsize=(16,10))
    
  for name, history in histories:
    val = plt.plot(history.epoch, history.history['val_'+key],
                   '--', label=name.title()+' Val')
    plt.plot(history.epoch, history.history[key], color=val[0].get_color(),
             label=name.title()+' Train')

  plt.xlabel('Epochs')
  plt.ylabel(key.replace('_',' ').title())
  plt.legend()

  plt.xlim([0,max(history.epoch)])


plot_history([('baseline', baseline_history),
              ('smaller', smaller_history),
              ('bigger', bigger_history)])

出力

損失の推移
損失の推移

より大きなネットワークでは、すぐに、1エポックで過学習が始まり、その度合も強いことに注目してください。
ネットワークの容量が大きいほど訓練用データをモデル化するスピードが早くなり(結果として訓練時の損失値が小さくなり)ますが、より過学習しやすく(結果として訓練時の損失値と検証時の損失値が大きく乖離しやすく)なります。

過学習防止の戦略

重みの正規化を追加

オッカムの剃刀の原理をご存知かもしれません:
何かについて2つの説明が与えられた場合、最も正しい可能性が高い説明は「最も単純な」ものであり、仮定の量が最も少ないものです。
これはニューラルネットワークによって学習されるモデルにも当てはまります:
いくつかの学習データとネットワークアーキテクチャが与えられると、データを説明できる重み値の複数のセット(複数のモデル)が存在し、単純なモデルは複雑なモデルよりもオーバーフィットする可能性が低くなります。

この文脈での「単純なモデル」とは、パラメータ値の分布がエントロピーが少ないモデル(または上のセクションで見たように、完全に少ないパラメータを持つモデル)のことです。
したがって、オーバーフィッティングを緩和する一般的な方法は、ネットワークの複雑さに制約を加えることで、重みの値の分布をより「規則的」にすることです。
これは「重みの正則化」と呼ばれ、ネットワークの損失関数に大きな重みを持つことに関連したコストを加えることで行われます。このコストには2つの種類があります。

  • L1正則化では、追加されるコストは重み係数の絶対値に比例します(すなわち、重みの「L1ノルム」と呼ばれるものに比例します)。
  • L2 正則化,ここで追加されるコストは,重み係数の値の2乗に比例する(すなわち,重みの2乗 "L2ノルム "と呼ばれるものに比例する).L2正則化は、ニューラルネットワークの文脈では、重みの減衰とも呼ばれます。名前の違いで混乱しないようにしてください:重み減衰は数学的にはL2正則化と全く同じです。


L1正則化は、重みを正確にゼロに向かって押し上げ、疎なモデルを促進します。
L2正則化は、小さな重みではペナルティがゼロになるので、疎なモデルにすることなく重みパラメータにペナルティを与えます。
tf.kerasでは、キーワード引数として重み正則化インスタンスをレイヤーに渡すことで重み正則化を追加します。
ここではL2の重み正則化を追加してみましょう。
※www.DeepL.com/Translator(無料版)で翻訳しました。

l2_model = keras.models.Sequential([
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

l2_model.compile(optimizer='adam',
                 loss='binary_crossentropy',
                 metrics=['accuracy', 'binary_crossentropy'])

l2_model_history = l2_model.fit(train_data, train_labels,
                                epochs=20,
                                batch_size=512,
                                validation_data=(test_data, test_labels),
                                verbose=2)

plot_history([('baseline', baseline_history),
              ('l2', l2_model_history)])

出力

Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 5s - loss: 0.4987 - accuracy: 0.8208 - binary_crossentropy: 0.4577 - val_loss: 0.3676 - val_accuracy: 0.8802 - val_binary_crossentropy: 0.3244
Epoch 2/20
25000/25000 - 3s - loss: 0.2957 - accuracy: 0.9120 - binary_crossentropy: 0.2491 - val_loss: 0.3363 - val_accuracy: 0.8866 - val_binary_crossentropy: 0.2872
Epoch 3/20
25000/25000 - 3s - loss: 0.2487 - accuracy: 0.9317 - binary_crossentropy: 0.1975 - val_loss: 0.3375 - val_accuracy: 0.8860 - val_binary_crossentropy: 0.2849
Epoch 4/20
25000/25000 - 3s - loss: 0.2257 - accuracy: 0.9418 - binary_crossentropy: 0.1716 - val_loss: 0.3489 - val_accuracy: 0.8818 - val_binary_crossentropy: 0.2936
Epoch 5/20
25000/25000 - 4s - loss: 0.2114 - accuracy: 0.9488 - binary_crossentropy: 0.1548 - val_loss: 0.3639 - val_accuracy: 0.8782 - val_binary_crossentropy: 0.3065
Epoch 6/20
25000/25000 - 3s - loss: 0.2004 - accuracy: 0.9518 - binary_crossentropy: 0.1420 - val_loss: 0.3797 - val_accuracy: 0.8752 - val_binary_crossentropy: 0.3207
Epoch 7/20
25000/25000 - 3s - loss: 0.1931 - accuracy: 0.9549 - binary_crossentropy: 0.1332 - val_loss: 0.3975 - val_accuracy: 0.8722 - val_binary_crossentropy: 0.3369
Epoch 8/20
25000/25000 - 3s - loss: 0.1858 - accuracy: 0.9579 - binary_crossentropy: 0.1246 - val_loss: 0.4124 - val_accuracy: 0.8696 - val_binary_crossentropy: 0.3507
Epoch 9/20
25000/25000 - 3s - loss: 0.1811 - accuracy: 0.9596 - binary_crossentropy: 0.1188 - val_loss: 0.4319 - val_accuracy: 0.8652 - val_binary_crossentropy: 0.3690
Epoch 10/20
25000/25000 - 3s - loss: 0.1768 - accuracy: 0.9622 - binary_crossentropy: 0.1131 - val_loss: 0.4407 - val_accuracy: 0.8662 - val_binary_crossentropy: 0.3768
Epoch 11/20
25000/25000 - 3s - loss: 0.1727 - accuracy: 0.9632 - binary_crossentropy: 0.1085 - val_loss: 0.4680 - val_accuracy: 0.8612 - val_binary_crossentropy: 0.4031
Epoch 12/20
25000/25000 - 3s - loss: 0.1694 - accuracy: 0.9648 - binary_crossentropy: 0.1038 - val_loss: 0.4760 - val_accuracy: 0.8616 - val_binary_crossentropy: 0.4102
Epoch 13/20
25000/25000 - 3s - loss: 0.1636 - accuracy: 0.9669 - binary_crossentropy: 0.0973 - val_loss: 0.4963 - val_accuracy: 0.8585 - val_binary_crossentropy: 0.4297
Epoch 14/20
25000/25000 - 4s - loss: 0.1657 - accuracy: 0.9664 - binary_crossentropy: 0.0985 - val_loss: 0.5019 - val_accuracy: 0.8607 - val_binary_crossentropy: 0.4335
Epoch 15/20
25000/25000 - 3s - loss: 0.1609 - accuracy: 0.9681 - binary_crossentropy: 0.0916 - val_loss: 0.5214 - val_accuracy: 0.8571 - val_binary_crossentropy: 0.4518
Epoch 16/20
25000/25000 - 3s - loss: 0.1535 - accuracy: 0.9731 - binary_crossentropy: 0.0836 - val_loss: 0.5408 - val_accuracy: 0.8537 - val_binary_crossentropy: 0.4706
Epoch 17/20
25000/25000 - 3s - loss: 0.1464 - accuracy: 0.9759 - binary_crossentropy: 0.0760 - val_loss: 0.5308 - val_accuracy: 0.8563 - val_binary_crossentropy: 0.4604
Epoch 18/20
25000/25000 - 3s - loss: 0.1463 - accuracy: 0.9763 - binary_crossentropy: 0.0751 - val_loss: 0.5585 - val_accuracy: 0.8519 - val_binary_crossentropy: 0.4867
Epoch 19/20
25000/25000 - 4s - loss: 0.1384 - accuracy: 0.9804 - binary_crossentropy: 0.0663 - val_loss: 0.5737 - val_accuracy: 0.8513 - val_binary_crossentropy: 0.5015
Epoch 20/20
25000/25000 - 3s - loss: 0.1354 - accuracy: 0.9826 - binary_crossentropy: 0.0628 - val_loss: 0.5671 - val_accuracy: 0.8559 - val_binary_crossentropy: 0.4941

基準モデルとL2正則化モデルの比較
基準モデルとL2正則化モデルの比較

L2正則化ありのモデルは比較基準のモデルに比べて過学習しにくくなっています。
つまり、オレンジ破線のL2正則化ありモデルは、エポック数が増えていっても、縦軸の損失が大きくなりにくい。ということが見て取れます。

ドロップアウトを追加する

ドロップアウトは、ニューラルネットワーク正則化テクニックとして最もよく使われる手法の一つです。
この手法は、トロント大学のヒントンと彼の学生が開発したものです。
ドロップアウトは層に適用するもので、訓練時に層から出力された特徴量に対してランダムに「ドロップアウト(つまりゼロ化)」を行うものです。
例えば、ある層が訓練時にある入力サンプルに対して、普通は[0.2, 0.5, 1.3, 0.8, 1.1] というベクトルを出力するとします。
ドロップアウトを適用すると、このベクトルは例えば[0, 0.5, 1.3, 0, 1.1]のようにランダムに散らばったいくつかのゼロを含むようになります。

ドロップアウト率」はゼロ化される特徴の割合で、通常は0.2から0.5の間に設定します。
テスト時は、どのユニットもドロップアウトされず、代わりに出力値がドロップアウト率と同じ比率でスケールダウンされます。
これは、訓練時に比べてたくさんのユニットがアクティブであることに対してバランスをとるためです。

tf.kerasでは、Dropout層を使ってドロップアウトをネットワークに導入できます。
ドロップアウト層は、その直前の層の出力に対してドロップアウトを適用します。

それでは、IMDBネットワークに2つのドロップアウト層を追加しましょう。

dpt_model = keras.models.Sequential([
    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(1, activation='sigmoid')
])

dpt_model.compile(optimizer='adam',
                  loss='binary_crossentropy',
                  metrics=['accuracy','binary_crossentropy'])

dpt_model_history = dpt_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)

plot_history([('baseline', baseline_history),
              ('dropout', dpt_model_history)])

出力

Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 6s - loss: 0.6167 - accuracy: 0.6617 - binary_crossentropy: 0.6167 - val_loss: 0.4929 - val_accuracy: 0.8520 - val_binary_crossentropy: 0.4929
Epoch 2/20
25000/25000 - 4s - loss: 0.4515 - accuracy: 0.8164 - binary_crossentropy: 0.4515 - val_loss: 0.3508 - val_accuracy: 0.8819 - val_binary_crossentropy: 0.3508
Epoch 3/20
25000/25000 - 4s - loss: 0.3519 - accuracy: 0.8689 - binary_crossentropy: 0.3519 - val_loss: 0.2900 - val_accuracy: 0.8899 - val_binary_crossentropy: 0.2900
Epoch 4/20
25000/25000 - 4s - loss: 0.2878 - accuracy: 0.9005 - binary_crossentropy: 0.2878 - val_loss: 0.2775 - val_accuracy: 0.8881 - val_binary_crossentropy: 0.2775
Epoch 5/20
25000/25000 - 4s - loss: 0.2470 - accuracy: 0.9138 - binary_crossentropy: 0.2470 - val_loss: 0.2814 - val_accuracy: 0.8863 - val_binary_crossentropy: 0.2814
Epoch 6/20
25000/25000 - 4s - loss: 0.2147 - accuracy: 0.9257 - binary_crossentropy: 0.2147 - val_loss: 0.2904 - val_accuracy: 0.8859 - val_binary_crossentropy: 0.2904
Epoch 7/20
25000/25000 - 4s - loss: 0.1864 - accuracy: 0.9372 - binary_crossentropy: 0.1864 - val_loss: 0.3154 - val_accuracy: 0.8831 - val_binary_crossentropy: 0.3154
Epoch 8/20
25000/25000 - 4s - loss: 0.1644 - accuracy: 0.9426 - binary_crossentropy: 0.1644 - val_loss: 0.3114 - val_accuracy: 0.8826 - val_binary_crossentropy: 0.3114
Epoch 9/20
25000/25000 - 3s - loss: 0.1521 - accuracy: 0.9464 - binary_crossentropy: 0.1521 - val_loss: 0.3334 - val_accuracy: 0.8824 - val_binary_crossentropy: 0.3334
Epoch 10/20
25000/25000 - 4s - loss: 0.1365 - accuracy: 0.9523 - binary_crossentropy: 0.1365 - val_loss: 0.3649 - val_accuracy: 0.8807 - val_binary_crossentropy: 0.3649
Epoch 11/20
25000/25000 - 4s - loss: 0.1240 - accuracy: 0.9562 - binary_crossentropy: 0.1240 - val_loss: 0.3704 - val_accuracy: 0.8789 - val_binary_crossentropy: 0.3704
Epoch 12/20
25000/25000 - 5s - loss: 0.1132 - accuracy: 0.9598 - binary_crossentropy: 0.1132 - val_loss: 0.4014 - val_accuracy: 0.8794 - val_binary_crossentropy: 0.4014
Epoch 13/20
25000/25000 - 4s - loss: 0.1057 - accuracy: 0.9599 - binary_crossentropy: 0.1057 - val_loss: 0.4179 - val_accuracy: 0.8791 - val_binary_crossentropy: 0.4179
Epoch 14/20
25000/25000 - 4s - loss: 0.0983 - accuracy: 0.9621 - binary_crossentropy: 0.0983 - val_loss: 0.4239 - val_accuracy: 0.8774 - val_binary_crossentropy: 0.4239
Epoch 15/20
25000/25000 - 3s - loss: 0.0908 - accuracy: 0.9646 - binary_crossentropy: 0.0908 - val_loss: 0.4614 - val_accuracy: 0.8782 - val_binary_crossentropy: 0.4614
Epoch 16/20
25000/25000 - 3s - loss: 0.0847 - accuracy: 0.9662 - binary_crossentropy: 0.0847 - val_loss: 0.4900 - val_accuracy: 0.8770 - val_binary_crossentropy: 0.4900
Epoch 17/20
25000/25000 - 3s - loss: 0.0813 - accuracy: 0.9670 - binary_crossentropy: 0.0813 - val_loss: 0.4878 - val_accuracy: 0.8768 - val_binary_crossentropy: 0.4878
Epoch 18/20
25000/25000 - 4s - loss: 0.0789 - accuracy: 0.9684 - binary_crossentropy: 0.0789 - val_loss: 0.5279 - val_accuracy: 0.8758 - val_binary_crossentropy: 0.5279
Epoch 19/20
25000/25000 - 4s - loss: 0.0737 - accuracy: 0.9702 - binary_crossentropy: 0.0737 - val_loss: 0.5535 - val_accuracy: 0.8756 - val_binary_crossentropy: 0.5535
Epoch 20/20
25000/25000 - 3s - loss: 0.0754 - accuracy: 0.9675 - binary_crossentropy: 0.0754 - val_loss: 0.5403 - val_accuracy: 0.8754 - val_binary_crossentropy: 0.5403

基準モデルとドロップアウトモデルの比較
基準モデルとドロップアウトモデルの比較

ドロップアウトを追加することで、比較対象モデルより明らかに改善が見られます。

組み合わせてみよう

L2正則化ドロップアウトを組み合わせてみます

# 組み合わせ
hybrid_model = keras.models.Sequential([
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu'),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(1, activation='sigmoid')
])

hybrid_model.compile(optimizer='adam',
                 loss='binary_crossentropy',
                 metrics=['accuracy', 'binary_crossentropy'])

hybrid_model_history = hybrid_model.fit(train_data, train_labels,
                                epochs=20,
                                batch_size=512,
                                validation_data=(test_data, test_labels),
                                verbose=2)
# 基準モデルと組み合わせモデルを比較
plot_history([('baseline', baseline_history),
              ('hybrid', hybrid_model_history)])

出力

学習結果は省略・・・

ハイブリッド
組み合わせてみよう

かなり過学習を抑えられています。

まとめ

要約すると、ニューラルネットワークのオーバーフィットを防ぐ最も一般的な方法は以下の通りです。

  • より多くの学習データを取得する。
  • ネットワークの容量を減らす。
  • 重みの正則化を加える。
  • ドロップアウトを追加する。

このガイドでは取り上げていない2つの重要なアプローチは以下の通りです。

  • データの補強
  • 一括正規化

それぞれの方法はそれだけでも役立ちますが、組み合わせることでさらに効果的になることが多いことを覚えておいてください。

最後に今回のチュートリアル実装をまとめて記載します。
一部コメントアウトしています。
全部をまともに実行すると結構時間かかります。

from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf
from tensorflow import keras

import numpy as np
import matplotlib.pyplot as plt

print(tf.__version__)

# IMDBデータセットのダウンロード
NUM_WORDS = 10000

(train_data, train_labels), (test_data, test_labels) = keras.datasets.imdb.load_data(num_words=NUM_WORDS)

def multi_hot_sequences(sequences, dimension):
    # 形状が (len(sequences), dimension)ですべて0の行列を作る
    results = np.zeros((len(sequences), dimension))
    for i, word_indices in enumerate(sequences):
        results[i, word_indices] = 1.0  # 特定のインデックスに対してresults[i] を1に設定する
    return results


train_data = multi_hot_sequences(train_data, dimension=NUM_WORDS)
test_data = multi_hot_sequences(test_data, dimension=NUM_WORDS)

# 比較基準を作る
baseline_model = keras.Sequential([
    # `.summary` を見るために`input_shape`が必要 
    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

baseline_model.compile(optimizer='adam',
                       loss='binary_crossentropy',
                       metrics=['accuracy', 'binary_crossentropy'])

baseline_model.summary()

# モデルにフィットさせる
baseline_history = baseline_model.fit(train_data,
                                      train_labels,
                                      epochs=20,
                                      batch_size=512,
                                      validation_data=(test_data, test_labels),
                                      verbose=2)

# より小さいモデルの構築
# smaller_model = keras.Sequential([
#     keras.layers.Dense(4, activation='relu', input_shape=(NUM_WORDS,)),
#     keras.layers.Dense(4, activation='relu'),
#     keras.layers.Dense(1, activation='sigmoid')
# ])

# smaller_model.compile(optimizer='adam',
#                       loss='binary_crossentropy',
#                       metrics=['accuracy', 'binary_crossentropy'])

# smaller_model.summary()

# smaller_history = smaller_model.fit(train_data,
#                                     train_labels,
#                                     epochs=20,
#                                     batch_size=512,
#                                     validation_data=(test_data, test_labels),
#                                     verbose=2)

# より大きいモデルの構築
# bigger_model = keras.models.Sequential([
#     keras.layers.Dense(512, activation='relu', input_shape=(NUM_WORDS,)),
#     keras.layers.Dense(512, activation='relu'),
#     keras.layers.Dense(1, activation='sigmoid')
# ])

# bigger_model.compile(optimizer='adam',
#                      loss='binary_crossentropy',
#                      metrics=['accuracy','binary_crossentropy'])

# bigger_model.summary()

# bigger_history = bigger_model.fit(train_data, train_labels,
#                                   epochs=20,
#                                   batch_size=512,
#                                   validation_data=(test_data, test_labels),
#                                   verbose=2)


def plot_history(histories, key='binary_crossentropy'):
  plt.figure(figsize=(16,10))
    
  for name, history in histories:
    val = plt.plot(history.epoch, history.history['val_'+key],
                   '--', label=name.title()+' Val')
    plt.plot(history.epoch, history.history[key], color=val[0].get_color(),
             label=name.title()+' Train')

  plt.xlabel('Epochs')
  plt.ylabel(key.replace('_',' ').title())
  plt.legend()

  plt.xlim([0,max(history.epoch)])


# plot_history([('baseline', baseline_history),
#               ('smaller', smaller_history),
#               ('bigger', bigger_history)])

# L2正則化モデルを追加
# l2_model = keras.models.Sequential([
#     keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
#                        activation='relu', input_shape=(NUM_WORDS,)),
#     keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
#                        activation='relu'),
#     keras.layers.Dense(1, activation='sigmoid')
# ])

# l2_model.compile(optimizer='adam',
#                  loss='binary_crossentropy',
#                  metrics=['accuracy', 'binary_crossentropy'])

# l2_model_history = l2_model.fit(train_data, train_labels,
#                                 epochs=20,
#                                 batch_size=512,
#                                 validation_data=(test_data, test_labels),
#                                 verbose=2)

# 基準モデルとL2正則化追加モデルを比較
# plot_history([('baseline', baseline_history),
#               ('l2', l2_model_history)])

# ドロップアウトモデルを追加
# dpt_model = keras.models.Sequential([
#     keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
#     keras.layers.Dropout(0.5),
#     keras.layers.Dense(16, activation='relu'),
#     keras.layers.Dropout(0.5),
#     keras.layers.Dense(1, activation='sigmoid')
# ])

# dpt_model.compile(optimizer='adam',
#                   loss='binary_crossentropy',
#                   metrics=['accuracy','binary_crossentropy'])

# dpt_model_history = dpt_model.fit(train_data, train_labels,
#                                   epochs=20,
#                                   batch_size=512,
#                                   validation_data=(test_data, test_labels),
#                                   verbose=2)

# 基準モデルとドロップアウトモデルを比較
# plot_history([('baseline', baseline_history),
#               ('dropout', dpt_model_history)])

# 組み合わせ
hybrid_model = keras.models.Sequential([
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu'),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(1, activation='sigmoid')
])

hybrid_model.compile(optimizer='adam',
                 loss='binary_crossentropy',
                 metrics=['accuracy', 'binary_crossentropy'])

hybrid_model_history = hybrid_model.fit(train_data, train_labels,
                                epochs=20,
                                batch_size=512,
                                validation_data=(test_data, test_labels),
                                verbose=2)
# 基準モデルと組み合わせモデルを比較
plot_history([('baseline', baseline_history),
              ('hybrid', hybrid_model_history)])

TensorFlow使ってみた~GoogleColab/IMDB~テキスト分類

ここでは、映画のレビューをそのテキストを使って肯定的か否定的かに分類します。
これは、二値分類あるいは2クラス分類という問題の例であり、機械学習において重要でいろいろな応用が可能なものです。

ここでは、インターネット動画データベースから抽出した50,000件の映画レビューを含む、IMDBデータセットを使います。
レビューは訓練用とテスト用に25,000件ずつに分割されています。
訓練用とテスト用のデータは均衡しとりると、それぞれが同数の肯定的及び否定的なレビューを含んでいます。

www.tensorflow.org

諸々インポート

from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf
from tensorflow import keras

import numpy as np

print(tf.__version__)

出力

2.2.0

データセット取得

IMDBデータセットは、TensorFlowにパッケージ化されています。
それは前処理済みのものであり、(単語の連なりである)レビューが、整数の配列に変換されています。
そこでは整数が辞書中の特定の単語を表します。 次のコードは、IMDBデータセットをあなたのパソコンにダウンロードします。(すでにダウンロードしていれば、キャッシュされたコピーを使用します)

imdb = keras.datasets.imdb

(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)

num_words=10000という引数は、訓練データ中に出てくる単語のうち、最も頻繁に出現する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)

整数を単語に戻してみる

整数をテキストに戻す方法を知っていると便利です。
整数を文字列にマッピングする辞書オブジェクトを検索するためのヘルパー関数を定義します。

# 単語を整数にマッピングする辞書
word_index = imdb.get_word_index()

# インデックスの最初の方は予約済み
word_index = {k:(v+3) for k,v in word_index.items()} 
word_index["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2  # unknown
word_index["<UNUSED>"] = 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])

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 <UNK> 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"

※DeepLで翻訳

この映画は素晴らしいキャスティング、ロケ地、風景、ストーリー演出、全員が本当に自分の演じた役に合っていて、そこにいることを想像することができました。ロバート<UNK>は素晴らしい俳優で、今では監督でもある<UNK>の父親は私と同じスコットランドの島の出身なので、私はこの映画と本当のつながりがあるという事実が大好きでした。最後に泣いたのはとても悲しかったし、映画を見て泣いてしまったら、それは良い映画に違いないと言われていることを知っています。この映画では、彼らを演じるスターたちは、映画全体のために大きなプロフィールを持っていますが、これらの子供たちは素晴らしく、彼らがしたことを賞賛されるべきではないでしょうか。

データの準備

レビュー(整数の配列)は、ニューラルネットワークに投入する前に、テンソルに変換する必要があります。
これには2つの方法があります。

・配列をワンホット(one-hot)インデックスと同じように、単語の出現を表す0と1のベクトルに変換します。
例えば、[3、5]という配列は、インデックス3と5を除いてすべてゼロの10,000次元のベクトルになります。
そして、これをネットワークの最初の層、今回、浮動小数点のベクトルデータを扱うことができるDense(全結合)層とします。
ただし、これは単語数×レビュー数の行列が必要なメモリ集約的な方法です。

・もう一つの方法では、サンプル数 * 長さの最大値配列をパディングによって同じ長さに揃え、の形の整数テンソルにします。
そして、この形式を扱うことができるEmbedding(埋め込み)層をネットワークの最初の層にします。

このチュートリアルでは、後者を採用することにします。
映画レビューは同じ長さ以外ならないので、長さを標準化するpad_sequences関数を使うことにします。

train_data = keras.preprocessing.sequence.pad_sequences(train_data,
                                                        value=word_index["<PAD>"],
                                                        padding='post',
                                                        maxlen=256)

test_data = keras.preprocessing.sequence.pad_sequences(test_data,
                                                       value=word_index["<PAD>"],
                                                       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です。
この問題のためのモデルを構築しましょう。

# 入力の形式は映画レビューで使われている語彙数(10,000語)
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
_________________________________________________________________

これらの層は、分類器を構成するため一列に積み重ねられます。

  1. 最初の層は(埋め込み)層です。この層は、整数にエンコードされた語彙を読み取り、それぞれの単語インデックスに対応する埋め込みベクトルを検索します。埋め込みベクトルは、モデルの訓練の中で学習されます。ベクトル化のために、出力行列には次元が1つ追加されます。その結果、次元は、(batch, sequence, embedding)となります。
  2. 次は、GlobalAveragePooling1D(1次元のグローバル平均プーリング)層です。この層は、それぞれのサンプルについて、シーケンスの次元方向に平均値をもとめ、固定長のベクトルを返します。この結果、モデルは最も単純な形で、可変長の入力を扱うことができるようになります。
  3. この固定長の出力ベクトルは、16個の隠れユニットを持つ全結合(Dense)層に受け渡されます。
  4. 最後の層は、1個の出力ノードに全結合されます。sigmoidシグモイド()活性化関数を使うことで、値は確率あるいは確信度を表す0と1の間の浮動小数点数となります。

隠れ層

上記のモデルには、入力と出力の間に、2つの中間層あるいは「隠れ」層があります。
出力(ユニット、ノード、またはニューロン)は、その層の内部表現の次元数です。
このネットワークが学習によって内部表現を獲得する際の自由度ということです。

モデルにより多くの隠れユニットがある場合(内部表現空間の次元数がより大きい場合)、または、より多くの層がある場合、あるいはその両方の場合、ネットワークはより複雑な内部表現を学習することができるできます。
しかしながら、その結果として、ネットワークの計算量が多くなるほか、学習してほしくないパターンを学習するようになります。
学習してほしくないパターンとは、訓練データでの性能は向上するものの、テスト用データの性能が向上しないパターンです。
この問題を過学習(オーバーフィッティング)といいます。この問題は後ほど検証することになります。

損失関数とオプティマイザ

モデルを訓練するには、損失関数とオプティマイザが必要です。
今回の問題は二値分類問題であり、モデルの出力は確率(1ユニットの層とシグモイド活性化関数)であるため、損失関数としてbinary_crossentropy( 2値のクロスエントロピー)関数を使用することにします。

損失関数の候補はこれだけではありません。
例えば、'mean_squared_error(平均二乗誤差)を使うこともできます。 しかし、一般的には、binary_crossentropyは確率を扱う方に適しています。 binary_crossentropy`は、確率分布の間の「今回の場合には、真の分布と予測値の分布の間の距離ということになります。

回帰問題を検証する際には(mean_squared_error例えば家屋の値段を推定するとか)、もう一つの損失関数である(平均二乗誤差)の使い方を目にすることになります。

model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])

検証用データを作る

訓練を行う際、モデルが見ていないデータでの正解率を検証したいと思います。
もとの訓練用データから、10,000個のサンプルを取り分けて検証用データ(検証セット)を作ります。
(なぜ、ここでテスト用データを使わないのでしょう?今回の目的は、訓練用データだけを使って、モデルの開発とチューニングを行うことです。その後、テスト用データを1回だけ使い、正解率を検証するのです。)

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に含まれるすべてのサンプルを40回繰り返すことになります。
訓練中、検証用データの10,000サンプルを用いて、モデルの損失と正解率をモニタリングします。

history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=40,
                    batch_size=512,
                    validation_data=(x_val, y_val),
                    verbose=1)

出力

Epoch 1/40
30/30 [==============================] - 1s 23ms/step - loss: 0.6924 - accuracy: 0.4971 - val_loss: 0.6904 - val_accuracy: 0.5384
Epoch 2/40
30/30 [==============================] - 1s 18ms/step - loss: 0.6872 - accuracy: 0.6104 - val_loss: 0.6830 - val_accuracy: 0.6733
Epoch 3/40
30/30 [==============================] - 1s 18ms/step - loss: 0.6761 - accuracy: 0.6671 - val_loss: 0.6689 - val_accuracy: 0.6994
Epoch 4/40
30/30 [==============================] - 1s 18ms/step - loss: 0.6561 - accuracy: 0.7235 - val_loss: 0.6448 - val_accuracy: 0.7409
Epoch 5/40
30/30 [==============================] - 1s 18ms/step - loss: 0.6244 - accuracy: 0.7733 - val_loss: 0.6106 - val_accuracy: 0.7586
Epoch 6/40
30/30 [==============================] - 1s 18ms/step - loss: 0.5827 - accuracy: 0.8035 - val_loss: 0.5691 - val_accuracy: 0.8040
Epoch 7/40
30/30 [==============================] - 1s 18ms/step - loss: 0.5348 - accuracy: 0.8351 - val_loss: 0.5245 - val_accuracy: 0.8213
Epoch 8/40
30/30 [==============================] - 1s 18ms/step - loss: 0.4862 - accuracy: 0.8503 - val_loss: 0.4818 - val_accuracy: 0.8360
Epoch 9/40
30/30 [==============================] - 1s 18ms/step - loss: 0.4403 - accuracy: 0.8625 - val_loss: 0.4440 - val_accuracy: 0.8454
Epoch 10/40
30/30 [==============================] - 1s 18ms/step - loss: 0.4000 - accuracy: 0.8745 - val_loss: 0.4122 - val_accuracy: 0.8537
・・・・・・・・・・・
・・・・・・・・・・・
・・・・・・・・・・・
Epoch 37/40
30/30 [==============================] - 1s 18ms/step - loss: 0.1036 - accuracy: 0.9724 - val_loss: 0.3028 - val_accuracy: 0.8823
Epoch 38/40
30/30 [==============================] - 1s 18ms/step - loss: 0.1001 - accuracy: 0.9734 - val_loss: 0.3044 - val_accuracy: 0.8840
Epoch 39/40
30/30 [==============================] - 1s 18ms/step - loss: 0.0960 - accuracy: 0.9755 - val_loss: 0.3077 - val_accuracy: 0.8835
Epoch 40/40
30/30 [==============================] - 1s 19ms/step - loss: 0.0929 - accuracy: 0.9770 - val_loss: 0.3134 - val_accuracy: 0.8808

モデルの評価

さて、モデルの性能を見てみましょう。
2つの値が返されます。
損失(エラーを示す数値であり、小さい方が良い)と正解率です。

results = model.evaluate(test_data,  test_labels, verbose=2)

print(results)

出力

782/782 - 1s - loss: 0.3248 - accuracy: 0.8732
[0.3248487412929535, 0.8732399940490723]

この、かなり素朴なアプローチでも87%前後の正解率を達成しました。
もっと高度なアプローチを使えば、モデルの正解率は95%に近づけることもできるでしょう。

正解率と損失の時系列グラフを描く

model.fit()は、訓練中に発生したすべてのことを記録した辞書を含むオブジェクトを返します。

history_dict = history.history
history_dict.keys()

出力

dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy'])

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()

出力

損失
損失

plt.clf()   # 図のクリア

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エポックを過ぎたあたりから、横ばいになっているようです。
これが、過学習の一例です。
モデルの性能が、訓練用データでは高い常に、見たことの無いデータではそれほど高くないというものです。
このポイントをすぎると、モデルが最適化しすぎて、訓練用データでは特徴的であるが、テスト用データには一般化できない内部表現を学習しています。

まとめ

テキストをインプットとしたチュートリアルの実装を行いました。
過学習していますね、が結論となっています。
次の記事で、過学習に対する対応を実装します。
最後に、、、
データ確認等、余計なものを省いたソースコード全量を記載します。

from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf
from tensorflow import keras

import numpy as np

print(tf.__version__)

imdb = keras.datasets.imdb

(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)

# 単語を整数にマッピングする辞書
word_index = imdb.get_word_index()

# インデックスの最初の方は予約済み
word_index = {k:(v+3) for k,v in word_index.items()} 
word_index["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2  # unknown
word_index["<UNUSED>"] = 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])

# データの準備
train_data = keras.preprocessing.sequence.pad_sequences(train_data,
                                                        value=word_index["<PAD>"],
                                                        padding='post',
                                                        maxlen=256)

test_data = keras.preprocessing.sequence.pad_sequences(test_data,
                                                       value=word_index["<PAD>"],
                                                       padding='post',
                                                       maxlen=256)

# 入力の形式は映画レビューで使われている語彙数(10,000語)
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.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])

# 検証用データを作る
x_val = train_data[:10000]
partial_x_train = train_data[10000:]

y_val = train_labels[:10000]
partial_y_train = train_labels[10000:]

# モデルの訓練
history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=40,
                    batch_size=512,
                    validation_data=(x_val, y_val),
                    verbose=1)

# モデルの評価
results = model.evaluate(test_data,  test_labels, verbose=2)

print(results)

# 正解率と損失の時系列グラフを描く
history_dict = history.history
history_dict.keys()

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()

# 認証精度のグラフ
plt.clf()   # 図のクリア

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()

TensorFlow使ってみた~GoogleColab/Auto MPG~回帰問題の実装

Auto MPGデータセットを使用した回帰問題

www.tensorflow.org

チュートリアルより引用

回帰問題では、価格や確率といった連続的な値の出力を予測することが目的となります。
これは、分類問題の目的が、(例えば、写真にリンゴが写っているかオレンジが写っているかといった)離散的なラベルを予測することであるのとは対照的です。

MNIST手書き数字の認識ではインプットを0~9のクラスに分類したのに対して、予測した値を求めます。
Auto MPGモデルを使用した例では、車種の情報から、燃費性能を予測します。
AutoはAutoMobile、つまり自動車です。
MPGはMiles Per Gallon(1ガロンあたり何マイル走れるの?)です。
1マイルは約1.6km、1ガロンはイギリスでは約4.5リットル、アメリカでは約3.8リットルだそうです。
なんで国によって量が違うんですかね

実装

チュートリアルに従って実装を行っていきます。

インポートとか

# ペアプロットのためseabornを使用します
!pip install -q seaborn

from __future__ import absolute_import, division, print_function, unicode_literals

import pathlib

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers

print(tf.__version__)
2.2.0-rc4

データセット読み込み

Downloading data from https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data
32768/30286 [================================] - 0s 4us/step

dataset.tail()
dataset.tail()

データクレンジング

# データクレンジング
# このデータには、いくつか欠損値があります。
dataset.isna().sum()
# この最初のチュートリアルでは簡単化のためこれらの行を削除します。
dataset = dataset.dropna()

# "Origin"の列は数値ではなくカテゴリーです。このため、ワンホットエンコーディングを行います。
origin = dataset.pop('Origin')

dataset['USA'] = (origin == 1)*1.0
dataset['Europe'] = (origin == 2)*1.0
dataset['Japan'] = (origin == 3)*1.0
dataset.tail()

dataset.tail()
dataset.tail()

データを訓練用セットとテスト用セットに分割

# データを訓練用セットとテスト用セットに分割
# データセットを訓練用セットとテスト用セットに分割しましょう。
# テスト用データセットは、作成したモデルの最終評価に使用します。
train_dataset = dataset.sample(frac=0.8,random_state=0)
test_dataset = dataset.drop(train_dataset.index)

データの観察

# データの観察
sns.pairplot(train_dataset[["MPG", "Cylinders", "Displacement", "Weight"]], diag_kind="kde")

# 全体の統計値
train_stats = train_dataset.describe()
train_stats.pop("MPG")
train_stats = train_stats.transpose()
train_stats

訓練用セットのいくつかの列の組み合わせの同時分布
訓練用セットのいくつかの列の組み合わせの同時分布

MPG:燃費(Mile/Gallon)
Cylinders:気筒数
Displacement:排気量
Weight:車体重量

全体の統計値
全体の統計値

ラベルと特徴量の分離

# ラベルと特徴量の分離
# ラベル、すなわち目的変数を特徴量から切り離しましょう。このラベルは、モデルに予測させたい数量です。
train_labels = train_dataset.pop('MPG')
test_labels = test_dataset.pop('MPG')

データの正規化

それぞれの特徴量の範囲がどれほど違っているかに注目してください。

スケールや値の範囲が異なる特徴量を正規化するのはよい習慣です。
特徴量の正規化なしでもモデルは収束するかもしれませんが、モデルの訓練はより難しいなり、結果として得られたモデルも入力で使われる単位に依存することになります。

def norm(x):
  return (x - train_stats['mean']) / train_stats['std']
normed_train_data = norm(train_dataset)
normed_test_data = norm(test_dataset)

モデルの構築

それではモデルを構築しましょう。 ここでは、2つの全結合の隠れ層と、1つの連続値を返す出力層からなる、Sequentialモデルを使います。

def build_model():
  model = keras.Sequential([
    layers.Dense(64, activation='relu', input_shape=[len(train_dataset.keys())]),
    layers.Dense(64, activation='relu'),
    layers.Dense(1)
  ])

  optimizer = tf.keras.optimizers.RMSprop(0.001)

  model.compile(loss='mse',
                optimizer=optimizer,
                metrics=['mae', 'mse'])
  return model

model = build_model()

モデルの検証

summaryメソッドを使って、モデルの簡単な説明を表示します。

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 64)                640       
_________________________________________________________________
dense_1 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 65        
=================================================================
Total params: 4,865
Trainable params: 4,865
Non-trainable params: 0
_________________________________________________________________

モデルを試す

モデルを試してみましょう。
訓練用データのうち10個のサンプルからなるバッチを取り出し、それを使ってメソッドを呼び出します。

example_batch = normed_train_data[:10]
example_result = model.predict(example_batch)
example_result
_________________________________________________________________
array([[ 0.04067548],
       [ 0.11909638],
       [ 0.04571575],
       [ 0.3901804 ],
       [-0.12568757],
       [ 0.23469958],
       [-0.10220642],
       [ 0.4401984 ],
       [ 0.12558347],
       [-0.3357187 ]], dtype=float32)

うまく動作しているようです。
予定どおりの型と形状の出力が得られています。

モデルの訓練

モデルを1000エポック訓練し、訓練と検証の正解率をhistoryオブジェクトに記録します。

# エポックが終わるごとにドットを一つ出力することで進捗を表示
class PrintDot(keras.callbacks.Callback):
  def on_epoch_end(self, epoch, logs):
    if epoch % 100 == 0: print('')
    print('.', end='')

EPOCHS = 1000

history = model.fit(
  normed_train_data, train_labels,
  epochs=EPOCHS, validation_split = 0.2, verbose=0,
  callbacks=[PrintDot()])
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................

モデルの訓練の様子を可視化

historyオブジェクトに保存された数値を使ってモデルの訓練の様子を可視化します。

hist = pd.DataFrame(history.history)
hist['epoch'] = history.epoch
hist.tail()

最後の5エポック
最後の5エポック

def plot_history(history):
  hist = pd.DataFrame(history.history)
  hist['epoch'] = history.epoch
  
  plt.figure()
  plt.xlabel('Epoch')
  plt.ylabel('Mean Abs Error [MPG]')
  plt.plot(hist['epoch'], hist['mae'],
           label='Train Error')
  plt.plot(hist['epoch'], hist['val_mae'],
           label = 'Val Error')
  plt.ylim([0,5])
  plt.legend()
  
  plt.figure()
  plt.xlabel('Epoch')
  plt.ylabel('Mean Square Error [$MPG^2$]')
  plt.plot(hist['epoch'], hist['mse'],
           label='Train Error')
  plt.plot(hist['epoch'], hist['val_mse'],
           label = 'Val Error')
  plt.ylim([0,20])
  plt.legend()
  plt.show()


plot_history(history)

平均絶対誤差(Mean Absolute Error)
平均絶対誤差(Mean Absolute Error)
平均二乗誤差(Mean Square Error)
平均二乗誤差(Mean Square Error)

このグラフを見ると、検証エラーは100エポックを過ぎたあたりで改善が見られなくなり、むしろ悪化しているようです。
検証スコアの改善が見られなくなったら自動的に訓練を停止するように、model.fitメソッド呼び出しを変更します。
ここでは、エポック毎に訓練状態をチェックするEarlyStoppingコールバックを使用します。
設定したエポック数の間に改善が見られない場合、訓練を自動的に停止します。

EarlyStoppingによる訓練自動停止

model = build_model()

# patience は改善が見られるかを監視するエポック数を表すパラメーター
early_stop = keras.callbacks.EarlyStopping(monitor='val_loss', patience=10)

history = model.fit(normed_train_data, train_labels, epochs=EPOCHS,
                    validation_split = 0.2, verbose=0, callbacks=[early_stop, PrintDot()])

plot_history(history)
...................................................................

平均絶対誤差2(Mean Absolute Error)
平均絶対誤差2(Mean Absolute Error)
平均二乗誤差2(Mean Square Error)
平均二乗誤差2(Mean Square Error)

検証用データセットでのグラフを見ると、平均誤差は+/- 2 MPG(マイル/ガロン)前後です。
これはよい精度でしょうか? その判断はおまかせします。

テストデータでの評価

モデルの訓練に使用していないテスト用データセットを使って、モデルがどれくらい汎化できているか見てみましょう。
これによって、モデルが実際の現場でどれくらい正確に予測できるかがわかります。

loss, mae, mse = model.evaluate(normed_test_data, test_labels, verbose=2)

print("Testing set Mean Abs Error: {:5.2f} MPG".format(mae))
3/3 - 0s - loss: 5.8512 - mae: 1.8385 - mse: 5.8512
Testing set Mean Abs Error:  1.84 MPG

モデルを使った予測

最後に、テストデータを使ってMPG値を予測します。

test_predictions = model.predict(normed_test_data).flatten()

plt.scatter(test_labels, test_predictions)
plt.xlabel('True Values [MPG]')
plt.ylabel('Predictions [MPG]')
plt.axis('equal')
plt.axis('square')
plt.xlim([0,plt.xlim()[1]])
plt.ylim([0,plt.ylim()[1]])
_ = plt.plot([-100, 100], [-100, 100])

予測値の散布図
予測値の散布図

そこそこよい予測ができているように見えます。誤差の分布を見てみましょう。

error = test_predictions - test_labels
plt.hist(error, bins = 25)
plt.xlabel("Prediction Error [MPG]")
_ = plt.ylabel("Count")

誤差の分布
誤差の分布

とても正規分布には見えませんが、サンプル数が非常に小さいからだと考えられます。

結論

このノートブックでは、回帰問題を扱うためのテクニックをいくつか紹介しました。
・平均二乗誤差(MSE:Mean Squared Error)は回帰問題に使われる一般的な損失関数です(分類問題には異なる損失関数が使われます)。
・同様に、回帰問題に使われる評価指標も分類問題とは異なります。回帰問題の一般的な評価指標は平均絶対誤差(MAE:Mean Absolute Error)です。
・入力数値特徴量の範囲が異なっている場合、特徴量ごとにおなじ範囲に正規化するべきです。
・訓練用データが多くない場合、過学習を避けるために少ない隠れ層をもつ小さいネットワークを使うというのがよい方策の1つです。
・Early Stoppingは過学習を防止するための便利な手法の一つです。

まとめ

というわけで回帰問題の実装をチュートリアルに従い行いました。
分類問題との大きな違いは、損失関数と最適化手法ですかね。
細かい点はもっとありますが・・・
いろいろなパターンを実装していきノウハウを積んでいきましょう。

TensorFlow使ってみた~GoogleColab/MNIST~

TensorFlowとは

TensorFlowはGoogleが開発しオープンソースで公開している、機械学習に用いるためのフレームワークです。
「TensorFlow 使い方」とかでググるとバージョン1.0の時の記事が多くヒットしました。
しかし、現在2.0バージョンが公開されており、1.0の時の記事をそのまま実行してもうまくいきません。

しょうがない。 チュートリアル見て自分でやってみよう、となった次第。
そして、その備忘録。

www.tensorflow.org

ゼロから作るDeep LearningPythonで学ぶディープラーニングの理論と実装

オライリーが出しているDeepLearningの本です。 DeepLearningの基礎勉強として、この書籍を一通り追ってみたあと、TensorFlowに手を出してみようとした次第です。

チュートリアル

下記チュートリアルを参考に、オライリーでやったMNISTの画像認証を実装してみます。 www.tensorflow.org

チュートリアルに書いてあった通りGoogle Colabを使用しました。
Googleってやっぱりすごいな・・・

from __future__ import absolute_import, division, print_function, unicode_literals

# TensorFlow と tf.keras のインポート
import tensorflow as tf
from tensorflow import keras

# ヘルパーライブラリのインポート
import numpy as np
import matplotlib.pyplot as plt

必要なライブラリ系のインポートを行います。

# MNISTの画像をインポート
mnist = keras.datasets.mnist

# 訓練データとテストデータを取得する
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 正規化
x_train = x_train / 255.0
x_test = x_test / 255.0

MNIST画像を取得します。
keras内に定義されている?ようで、1行で簡単に取得できます。

# モデルの定義
model = keras.Sequential([
    # インプット画像のレイヤ
    keras.layers.Flatten(input_shape=(28, 28)),
    # 隠れ層1層目のレイヤ(ノード数100、活性化関数にReLU関数を使用)
    keras.layers.Dense(100, activation='relu'),
    # 出力層のレイヤ(10クラス分類、出力関数にsoftmax関数を使用)
    keras.layers.Dense(10, activation='softmax')
])

モデルの定義をします。 つまり、ニューラルネットワークの定義と言ったらよいのでしょうか・・・
隠れ層の数や、隠れ層のノード数、活性化関数などを定義します。

# モデルのコンパイル
              # 最適化にADAMを使用
model.compile(optimizer='adam', 
              # 損失関数に交差エントロピー誤差を使用
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

上記で定義したモデルのコンパイルします。
ここで、最適化手法や、損失関数を指定します。

# モデルの訓練
model.fit(x_train, y_train, epochs=15)

fit関数で、モデルの学習を行います。 エポック数は15にしています。

# 正解率の評価
test_loss, test_acc = model.evaluate(x_test,  y_test, verbose=2)

print('\nTest accuracy:', test_acc)

テストデータに対して推論を行います。
正解率を出力しています。

実行結果

実行すると

Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 0s 0us/step
Epoch 1/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.2714 - accuracy: 0.9227
Epoch 2/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.1226 - accuracy: 0.9632
Epoch 3/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0849 - accuracy: 0.9745
Epoch 4/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0656 - accuracy: 0.9796
Epoch 5/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0518 - accuracy: 0.9838
Epoch 6/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0423 - accuracy: 0.9866
Epoch 7/15
1875/1875 [==============================] - 4s 2ms/step - loss: 0.0336 - accuracy: 0.9894
Epoch 8/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0281 - accuracy: 0.9912
Epoch 9/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0239 - accuracy: 0.9929
Epoch 10/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0204 - accuracy: 0.9940
Epoch 11/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0168 - accuracy: 0.9951
Epoch 12/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0139 - accuracy: 0.9958
Epoch 13/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0127 - accuracy: 0.9963
Epoch 14/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0108 - accuracy: 0.9970
Epoch 15/15
1875/1875 [==============================] - 3s 2ms/step - loss: 0.0095 - accuracy: 0.9970

ここまでが、訓練フェーズの出力

313/313 - 0s - loss: 0.0900 - accuracy: 0.9792

Test accuracy: 0.979200005531311

これが、テストデータでの正答率の評価の出力です。

訓練データでの認証精度:99.7%
テストデータでの認証精度:97.92%

若干過学習になっている模様。
モデルの学習で繰り返し回数増やしすぎ・・・?

ともあれ、TensorFlowを使ってMNIST画像の認識を実装することができました。
ドキュメントもいろいろ用意されているので、どんどん掘り下げていきましょう。