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