GAP前 vs GAP後 vs 両方|Dropoutの挿入位置で精度はどう変わる?【Keras×CIFAR-10実験】

投稿日:2026年8月2日日曜日 最終更新日:

CIFAR-10 CNN Dropout GlobalAveragePooling Google Colab Keras 過学習 画像分類

X f B! L
GAP前 vs GAP後 vs 両方|Dropoutの挿入位置で精度はどう変わる?【Keras×CIFAR-10実験】 アイキャッチ画像

「Dropoutはとりあえず入れておけばいい」と思っていませんか?GlobalAveragePooling2D(GAP)の前に置くか、後に置くかで、実は効き方が大きく変わる可能性があります。

Dropout自体の割合(0.0/0.2/0.5)による効果は既存記事で検証済みですが、今回は割合を0.2で固定し、挿入位置(GAP前/GAP後/両方)だけを変えて精度と過学習の抑制度を比較します。実は、GAPの「平均化」という性質がDropoutのノイズをある程度打ち消してしまう可能性があり、位置によって同じ0.2でも効き目が変わることが予想されます。

📘 この記事でわかること

  • Dropout(0.2)をGAP前・GAP後・両方に置いた場合のtest_accuracyと過学習度の違い
  • GAPの平均化がDropoutの正則化効果を弱める可能性がある理由
  • Dropoutを両方に入れる「二重正則化」が精度を下げるリスク

なぜGAPの前後でDropoutの効き方が変わりうるのか

Dropoutは学習時にランダムに要素を0にし、残った要素を1/(1-p)倍にスケーリングする(inverted dropout)ことで、期待値を変えずにノイズを加える仕組みです。

$$ y_{i,j} = \frac{x_{i,j} \cdot m_{i,j}}{1-p}, \quad m_{i,j} \sim \text{Bernoulli}(1-p) $$

今回のモデルはMaxPooling2Dを2回通すため、GAP直前の特徴マップは8×8=64要素あります。GAPはこの64要素を単純平均するため、DropoutをGAP前に置くと、次のような形になります。

$$ \text{GAP}(\text{Dropout}(x)) = \frac{1}{64}\sum_{i,j} \frac{x_{i,j} \cdot m_{i,j}}{1-p} $$

64要素の平均を取るため、大数の法則により個々の要素がランダムに0になるノイズは平均化の過程である程度打ち消され、GAP後の値の期待値は変わらないもののブレ(分散)は小さくなります。一方、GAP後(Dense層の直前)にDropoutを置いた場合は、GAPで集約された後の低次元ベクトル(今回は128要素)に直接ノイズが乗るため、平均化による打ち消し効果を受けません。この構造の違いが、同じ0.2という割合でも正則化としての「効き目」に差を生む可能性があります。

パターンDropout(0.2)の位置ノイズが乗る対象GAPによる平均化の影響
A:GAP前Conv層の出力(8×8特徴マップ)64要素の各ピクセルあり(ノイズが平均化で弱まる可能性)
B:GAP後Dense(128)の出力128要素の各ユニットなし
C:両方Conv出力とDense出力の両方両方GAP前のみあり

パターンCは実質的に二重正則化になります。既存記事「Weight Decay(L2正則化)の強さで過学習はどう変わる?」でも、Dropout+Weight Decayの組み合わせが過剰な正則化として精度を下げるケースを確認しており、Dropout同士の組み合わせでも同様の現象が起きるかを検証します。


実験コード

環境準備(最初に一度だけ実行)

# ── 環境準備(最初に一度だけ実行)──────────────────────
!apt-get -y install fonts-ipafont-gothic
!rm -rf /root/.cache/matplotlib
!pip install -q japanize_matplotlib
print("環境準備完了")
実行結果をクリックして内容を開く
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  fonts-ipafont-mincho
The following NEW packages will be installed:
  fonts-ipafont-gothic fonts-ipafont-mincho
0 upgraded, 2 newly installed, 0 to remove and 53 not upgraded.
Need to get 8,237 kB of archives.
After this operation, 28.7 MB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu jammy/universe amd64 fonts-ipafont-gothic all 00303-21ubuntu1 [3,513 kB]
Get:2 http://archive.ubuntu.com/ubuntu jammy/universe amd64 fonts-ipafont-mincho all 00303-21ubuntu1 [4,724 kB]
Fetched 8,237 kB in 3s (2,714 kB/s)
Selecting previously unselected package fonts-ipafont-gothic.
(Reading database ... 122403 files and directories currently installed.)
Preparing to unpack .../fonts-ipafont-gothic_00303-21ubuntu1_all.deb ...
Unpacking fonts-ipafont-gothic (00303-21ubuntu1) ...
Selecting previously unselected package fonts-ipafont-mincho.
Preparing to unpack .../fonts-ipafont-mincho_00303-21ubuntu1_all.deb ...
Unpacking fonts-ipafont-mincho (00303-21ubuntu1) ...
Setting up fonts-ipafont-mincho (00303-21ubuntu1) ...
update-alternatives: using /usr/share/fonts/opentype/ipafont-mincho/ipam.ttf to provide /usr/share/fonts/truetype/fonts-japanese-mincho.ttf (fonts-japanese-mincho.ttf) in auto mode
Setting up fonts-ipafont-gothic (00303-21ubuntu1) ...
update-alternatives: using /usr/share/fonts/opentype/ipafont-gothic/ipag.ttf to provide /usr/share/fonts/truetype/fonts-japanese-gothic.ttf (fonts-japanese-gothic.ttf) in auto mode
Processing triggers for fontconfig (2.13.1-4.2ubuntu5) ...
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.1/4.1 MB 112.5 MB/s eta 0:00:00
  Preparing metadata (setup.py) ... done
  Building wheel for japanize_matplotlib (setup.py) ... done
環境準備完了

import・データ準備・モデル構築関数

import tensorflow as tf
from tensorflow import keras
import matplotlib.pyplot as plt
import japanize_matplotlib

(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()
x_train = x_train.astype('float32') / 255.0
x_test  = x_test.astype('float32')  / 255.0

DROPOUT_RATE = 0.2

def build_model(position, name):
    inputs = keras.layers.Input(shape=(32, 32, 3))
    x = keras.layers.Conv2D(64, (3, 3), activation='relu', padding='same')(inputs)
    x = keras.layers.MaxPooling2D((2, 2))(x)
    x = keras.layers.Conv2D(128, (3, 3), activation='relu', padding='same')(x)
    x = keras.layers.MaxPooling2D((2, 2))(x)

    # GAP前:Conv出力(8x8x128の特徴マップ)に直接Dropout
    if position in ('before', 'both'):
        x = keras.layers.Dropout(DROPOUT_RATE)(x)

    x = keras.layers.GlobalAveragePooling2D()(x)
    x = keras.layers.Dense(128, activation='relu')(x)

    # GAP後:Dense(128)の出力にDropout
    if position in ('after', 'both'):
        x = keras.layers.Dropout(DROPOUT_RATE)(x)

    outputs = keras.layers.Dense(10, activation='softmax')(x)
    model = keras.Model(inputs, outputs, name=name)
    return model

def compile_and_fit(position, name):
    keras.backend.clear_session()
    model = build_model(position, name)
    model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.001),
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    history = model.fit(x_train, y_train, epochs=30, batch_size=64,
                        validation_split=0.2, verbose=1)
    return model, history
実行結果をクリックして内容を開く
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170498071/170498071 ━━━━━━━━━━━━━━━━━━━━ 1659s 10us/step

3パターンの学習実行(GAP前 / GAP後 / 両方)

# (position, モデル名=ASCII, 表示ラベル=日本語) の組で管理する
# Kerasのモデル名(name_scope)は日本語を含められないため分離している
patterns = [
    ('before', 'dropout_before', 'GAP前'),
    ('after',  'dropout_after',  'GAP後'),
    ('both',   'dropout_both',   '両方'),
]
histories, scores = {}, {}

for position, model_name, label in patterns:
    print(f"\n=== {label}(position={position}) ===")
    model, h = compile_and_fit(position, model_name)
    s = model.evaluate(x_test, y_test, verbose=0)

    histories[label] = h
    scores[label] = s

    train_acc = h.history['accuracy'][-1]
    val_acc   = h.history['val_accuracy'][-1]
    print(f"train_acc: {train_acc:.4f}  val_acc: {val_acc:.4f}  "
          f"test_acc: {s[1]:.4f}  train-val gap: {train_acc - val_acc:.4f}")
実行結果をクリックして内容を開く
=== GAP前(position=before) ===
Epoch 1/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 11s 9ms/step - accuracy: 0.2745 - loss: 1.9052 - val_accuracy: 0.3447 - val_loss: 1.7314
Epoch 2/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.3661 - loss: 1.6785 - val_accuracy: 0.4146 - val_loss: 1.5894
Epoch 3/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.4296 - loss: 1.5554 - val_accuracy: 0.4431 - val_loss: 1.5017
Epoch 4/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.4712 - loss: 1.4557 - val_accuracy: 0.4696 - val_loss: 1.4375
Epoch 5/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.4967 - loss: 1.3901 - val_accuracy: 0.5157 - val_loss: 1.3403
Epoch 6/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5152 - loss: 1.3358 - val_accuracy: 0.5363 - val_loss: 1.2945
Epoch 7/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5284 - loss: 1.2970 - val_accuracy: 0.5308 - val_loss: 1.3012
Epoch 8/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.5443 - loss: 1.2591 - val_accuracy: 0.5608 - val_loss: 1.2138
Epoch 9/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5573 - loss: 1.2214 - val_accuracy: 0.5398 - val_loss: 1.2829
Epoch 10/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5691 - loss: 1.1972 - val_accuracy: 0.5821 - val_loss: 1.1681
Epoch 11/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5796 - loss: 1.1698 - val_accuracy: 0.5917 - val_loss: 1.1405
Epoch 12/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5861 - loss: 1.1507 - val_accuracy: 0.5960 - val_loss: 1.1241
Epoch 13/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5929 - loss: 1.1256 - val_accuracy: 0.5946 - val_loss: 1.1257
Epoch 14/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5996 - loss: 1.1083 - val_accuracy: 0.5988 - val_loss: 1.1393
Epoch 15/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6100 - loss: 1.0867 - val_accuracy: 0.6080 - val_loss: 1.0919
Epoch 16/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6190 - loss: 1.0666 - val_accuracy: 0.6294 - val_loss: 1.0453
Epoch 17/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6265 - loss: 1.0497 - val_accuracy: 0.6138 - val_loss: 1.0918
Epoch 18/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6308 - loss: 1.0327 - val_accuracy: 0.6199 - val_loss: 1.0585
Epoch 19/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6352 - loss: 1.0172 - val_accuracy: 0.6316 - val_loss: 1.0400
Epoch 20/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6395 - loss: 1.0007 - val_accuracy: 0.6407 - val_loss: 0.9974
Epoch 21/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6463 - loss: 0.9871 - val_accuracy: 0.6492 - val_loss: 0.9843
Epoch 22/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6548 - loss: 0.9673 - val_accuracy: 0.6445 - val_loss: 0.9912
Epoch 23/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.6574 - loss: 0.9571 - val_accuracy: 0.6624 - val_loss: 0.9517
Epoch 24/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6626 - loss: 0.9488 - val_accuracy: 0.6698 - val_loss: 0.9394
Epoch 25/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6626 - loss: 0.9374 - val_accuracy: 0.6573 - val_loss: 0.9635
Epoch 26/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6696 - loss: 0.9241 - val_accuracy: 0.6686 - val_loss: 0.9188
Epoch 27/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6745 - loss: 0.9115 - val_accuracy: 0.6673 - val_loss: 0.9320
Epoch 28/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6805 - loss: 0.8982 - val_accuracy: 0.6614 - val_loss: 0.9501
Epoch 29/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6818 - loss: 0.8924 - val_accuracy: 0.6664 - val_loss: 0.9274
Epoch 30/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6834 - loss: 0.8830 - val_accuracy: 0.6750 - val_loss: 0.9052
train_acc: 0.6834  val_acc: 0.6750  test_acc: 0.6747  train-val gap: 0.0084

=== GAP後(position=after) ===
Epoch 1/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 8s 8ms/step - accuracy: 0.2709 - loss: 1.9168 - val_accuracy: 0.3639 - val_loss: 1.7175
Epoch 2/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.3799 - loss: 1.6628 - val_accuracy: 0.4241 - val_loss: 1.5507
Epoch 3/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.4360 - loss: 1.5292 - val_accuracy: 0.4640 - val_loss: 1.4651
Epoch 4/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.4697 - loss: 1.4493 - val_accuracy: 0.4952 - val_loss: 1.3871
Epoch 5/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.4940 - loss: 1.3805 - val_accuracy: 0.4998 - val_loss: 1.3432
Epoch 6/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5138 - loss: 1.3297 - val_accuracy: 0.5312 - val_loss: 1.2882
Epoch 7/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5281 - loss: 1.2923 - val_accuracy: 0.5289 - val_loss: 1.2875
Epoch 8/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5417 - loss: 1.2560 - val_accuracy: 0.5566 - val_loss: 1.2157
Epoch 9/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5532 - loss: 1.2264 - val_accuracy: 0.5634 - val_loss: 1.1947
Epoch 10/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5602 - loss: 1.2007 - val_accuracy: 0.5774 - val_loss: 1.1708
Epoch 11/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5725 - loss: 1.1730 - val_accuracy: 0.5817 - val_loss: 1.1554
Epoch 12/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.5840 - loss: 1.1496 - val_accuracy: 0.5951 - val_loss: 1.1102
Epoch 13/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5918 - loss: 1.1265 - val_accuracy: 0.5994 - val_loss: 1.1152
Epoch 14/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5978 - loss: 1.1077 - val_accuracy: 0.6012 - val_loss: 1.1062
Epoch 15/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6054 - loss: 1.0872 - val_accuracy: 0.5943 - val_loss: 1.1099
Epoch 16/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6110 - loss: 1.0706 - val_accuracy: 0.6116 - val_loss: 1.0622
Epoch 17/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6217 - loss: 1.0493 - val_accuracy: 0.6083 - val_loss: 1.0771
Epoch 18/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6259 - loss: 1.0369 - val_accuracy: 0.6176 - val_loss: 1.0641
Epoch 19/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6356 - loss: 1.0113 - val_accuracy: 0.6219 - val_loss: 1.0586
Epoch 20/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6390 - loss: 1.0020 - val_accuracy: 0.6347 - val_loss: 1.0136
Epoch 21/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6485 - loss: 0.9838 - val_accuracy: 0.6332 - val_loss: 1.0149
Epoch 22/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.6506 - loss: 0.9691 - val_accuracy: 0.6510 - val_loss: 0.9651
Epoch 23/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6540 - loss: 0.9606 - val_accuracy: 0.6609 - val_loss: 0.9561
Epoch 24/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6629 - loss: 0.9393 - val_accuracy: 0.6605 - val_loss: 0.9582
Epoch 25/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6659 - loss: 0.9296 - val_accuracy: 0.6692 - val_loss: 0.9364
Epoch 26/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6696 - loss: 0.9227 - val_accuracy: 0.6672 - val_loss: 0.9346
Epoch 27/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6770 - loss: 0.9034 - val_accuracy: 0.6496 - val_loss: 0.9779
Epoch 28/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6824 - loss: 0.8895 - val_accuracy: 0.6647 - val_loss: 0.9299
Epoch 29/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6846 - loss: 0.8837 - val_accuracy: 0.6552 - val_loss: 0.9604
Epoch 30/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6893 - loss: 0.8734 - val_accuracy: 0.6712 - val_loss: 0.9264
train_acc: 0.6893  val_acc: 0.6712  test_acc: 0.6646  train-val gap: 0.0181

=== 両方(position=both) ===
Epoch 1/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 9s 9ms/step - accuracy: 0.2636 - loss: 1.9359 - val_accuracy: 0.3533 - val_loss: 1.7221
Epoch 2/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.3631 - loss: 1.6933 - val_accuracy: 0.3824 - val_loss: 1.6197
Epoch 3/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.4220 - loss: 1.5635 - val_accuracy: 0.4491 - val_loss: 1.5166
Epoch 4/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.4572 - loss: 1.4783 - val_accuracy: 0.4746 - val_loss: 1.4531
Epoch 5/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.4828 - loss: 1.4172 - val_accuracy: 0.4869 - val_loss: 1.3988
Epoch 6/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5013 - loss: 1.3637 - val_accuracy: 0.5069 - val_loss: 1.3447
Epoch 7/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5162 - loss: 1.3263 - val_accuracy: 0.5208 - val_loss: 1.3204
Epoch 8/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5278 - loss: 1.2992 - val_accuracy: 0.5519 - val_loss: 1.2417
Epoch 9/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5404 - loss: 1.2693 - val_accuracy: 0.5491 - val_loss: 1.2243
Epoch 10/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5503 - loss: 1.2395 - val_accuracy: 0.5649 - val_loss: 1.1878
Epoch 11/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5623 - loss: 1.2126 - val_accuracy: 0.5766 - val_loss: 1.1706
Epoch 12/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5692 - loss: 1.1928 - val_accuracy: 0.5734 - val_loss: 1.1496
Epoch 13/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5748 - loss: 1.1692 - val_accuracy: 0.5952 - val_loss: 1.1186
Epoch 14/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5854 - loss: 1.1516 - val_accuracy: 0.5965 - val_loss: 1.1202
Epoch 15/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.5921 - loss: 1.1314 - val_accuracy: 0.6063 - val_loss: 1.0812
Epoch 16/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6001 - loss: 1.1094 - val_accuracy: 0.6173 - val_loss: 1.0644
Epoch 17/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6054 - loss: 1.0944 - val_accuracy: 0.6139 - val_loss: 1.0663
Epoch 18/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.6141 - loss: 1.0765 - val_accuracy: 0.6186 - val_loss: 1.0533
Epoch 19/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6158 - loss: 1.0671 - val_accuracy: 0.6389 - val_loss: 1.0051
Epoch 20/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6269 - loss: 1.0434 - val_accuracy: 0.6300 - val_loss: 1.0365
Epoch 21/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6282 - loss: 1.0321 - val_accuracy: 0.6456 - val_loss: 0.9827
Epoch 22/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6321 - loss: 1.0183 - val_accuracy: 0.6540 - val_loss: 0.9629
Epoch 23/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6394 - loss: 1.0076 - val_accuracy: 0.6527 - val_loss: 0.9689
Epoch 24/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6400 - loss: 0.9986 - val_accuracy: 0.6436 - val_loss: 0.9866
Epoch 25/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6499 - loss: 0.9754 - val_accuracy: 0.6634 - val_loss: 0.9375
Epoch 26/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.6517 - loss: 0.9727 - val_accuracy: 0.6585 - val_loss: 0.9522
Epoch 27/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6576 - loss: 0.9601 - val_accuracy: 0.6634 - val_loss: 0.9326
Epoch 28/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6636 - loss: 0.9427 - val_accuracy: 0.6564 - val_loss: 0.9561
Epoch 29/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6649 - loss: 0.9352 - val_accuracy: 0.6672 - val_loss: 0.9238
Epoch 30/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6670 - loss: 0.9254 - val_accuracy: 0.6761 - val_loss: 0.8981
train_acc: 0.6670  val_acc: 0.6761  test_acc: 0.6791  train-val gap: -0.0091

グラフ+サマリー

# ── val_accuracy / val_loss 比較グラフ ───────────────
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for label, h in histories.items():
    axes[0].plot(h.history['val_accuracy'], label=label)
    axes[1].plot(h.history['val_loss'],     label=label)
axes[0].set_title('val_accuracy の比較(全30エポック)')
axes[1].set_title('val_loss の比較(全30エポック)')
for ax in axes:
    ax.set_xlabel('Epoch'); ax.legend(); ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('dropout_position_comparison.png', dpi=150)
plt.show()

print("\n===== 最終結果サマリー =====")
print(f"{'Pattern':>8} | {'Train Acc':>9} | {'Val Acc':>8} | {'Test Acc':>9} | {'Train-Val Gap':>13}")
print("-" * 60)
for label in ['GAP前', 'GAP後', '両方']:
    train_acc = histories[label].history['accuracy'][-1]
    val_acc   = histories[label].history['val_accuracy'][-1]
    test_acc  = scores[label][1]
    gap       = train_acc - val_acc
    print(f"{label:>8} | {train_acc:>9.4f} | {val_acc:>8.4f} | {test_acc:>9.4f} | {gap:>13.4f}")
print("-" * 60)
実行結果をクリックして内容を開く
===== 最終結果サマリー =====
 Pattern | Train Acc |  Val Acc |  Test Acc | Train-Val Gap
------------------------------------------------------------
    GAP前 |    0.6834 |   0.6750 |    0.6747 |        0.0084
    GAP後 |    0.6893 |   0.6712 |    0.6646 |        0.0181
      両方 |    0.6670 |   0.6761 |    0.6791 |       -0.0091
------------------------------------------------------------

実験結果

精度・損失グラフ

精度グラフ
損失グラフ

結果サマリー

パターンtrain_accuracyval_accuracytest_accuracytrain-val gap(過学習度の目安)
A:GAP前68.34%67.50%67.47%0.84pt
B:GAP後68.93%67.12%66.46%1.81pt
C:両方66.70%67.61%67.91%-0.91pt

test_accuracyは両方(67.91%)>GAP前(67.47%)>GAP後(66.46%)という順でした。train-val gapもGAP前(0.84pt)の方がGAP後(1.81pt)より小さく、両方に至ってはマイナス(-0.91pt)という逆転が起きています。これは事前の仮説(GAPの平均化でGAP前の正則化が弱まる/両方は二重正則化で精度が下がる)と逆の結果であり、考察で仮説を修正します。

ハマりポイント

  • 標準のDropoutは「要素単位」でランダムに0にする。畳み込み層の出力(4次元テンソル)に対してkeras.layers.Dropoutを使うと、チャンネル全体ではなくピクセル単位でランダムに0になる。チャンネルごとまるごと落としたい場合はSpatialDropout2Dを使うのが定石だが、今回は「GAPとの位置関係」を純粋に比較する目的で、あえて標準のDropoutで統一している。
  • GAP前のDropoutは、GAPの平均化によってノイズが弱まる可能性がある。8×8=64要素の平均を取る過程で、ランダムに0になった要素の影響が均されるため、同じ0.2という割合でもGAP後(128要素→そのまま次の層へ)ほど強い正則化にならない可能性がある。
  • 「両方」は二重正則化になりうる。Dropout+Weight Decayの組み合わせで過学習抑制が効きすぎて精度が下がった既存記事の知見から類推すると、Dropout×2でも同様の過剰正則化が起きる可能性がある。
  • functional APIでモデルを分岐させる際、DropoutをGAP前後どちらに挿入したか変数名だけでは分かりにくくなりがち。model.summary()で層の並び順を都度確認するのが安全。

考察

① GAP前 vs GAP後:同じ割合でも効き方は違うか

結果は、記事冒頭で立てた「GAPの平均化がノイズを弱める」という仮説とはでした。GAP前(test_accuracy 67.47%、gap 0.84pt)の方が、GAP後(66.46%、gap 1.81pt)よりtest_accuracyが高く、過学習も抑えられていました。

考えられる説明は、「ノイズの平均化による弱まり」よりも「過学習の発生源に近い位置で効かせる効果」の方が支配的だったというものです。今回のモデルは、Conv層の出力(8×8×128=8192要素)というパラメータ数の多い特徴マップを経て、GAPで128次元まで一気に圧縮しています。過学習しやすいのは表現力の高いConv層側であり、GAP前のDropoutはまさにその位置に直接効きます。一方GAP後のDropoutは、すでに情報が圧縮された128次元のDense出力に効かせるため、Conv層側で起きた過学習を後から抑えることしかできず、効果が限定的だった可能性があります。

② 「両方」は二重正則化として精度を下げたか

結果はこちらも仮説と逆で、「両方」がtest_accuracy 67.91%と3パターン中もっとも高く、Weight Decayとの組み合わせで見られたような過剰正則化による精度低下は起きませんでした。GAP前・GAP後それぞれの正則化が異なる位置(Conv層側/Dense層側)に効いているため、単純な「同じ効果の重ね掛け」にはならず、むしろ相互補完的に働いたと考えられます。

ただし、train-val gapが-0.91ptとマイナスになっている点には注意が必要です。これは「両方が最も過学習していない」と単純には読めません。Kerasのfit()で表示されるtrain_accuracyはDropoutが有効な学習モードで計算され、val_accuracy/test_accuracyはDropoutを無効にした推論モードで計算されます。Dropoutを2箇所に入れると学習モード時のノイズが強くなり、train_accuracyが実力より低く表示されやすくなるため、gapがマイナスになったのはこの測定上の特性による部分が大きいと考えられます。

③ 過学習の抑制度(train-val gap)の違い

train-val gapはGAP後(1.81pt)>GAP前(0.84pt)>両方(-0.91pt)の順で、単純な数値だけ見ると「両方」が最も過学習していないように見えます。しかし②で触れた通り、この指標はDropoutが学習モードでtrain_accuracyを押し下げる影響を含むため、Dropout箇所が増えるほどgapが小さく(あるいはマイナスに)見えやすいという性質があります。過学習の実質的な抑制度を見るなら、train-val gapよりtest_accuracyそのものを主指標にする方が安全という教訓が今回得られました。その上でtest_accuracyを見ても、両方>GAP前>GAP後の順で、GAP前・両方の優位は変わりません。

④ 想定と逆の結果が出た理由と確認ポイント

今回はまさに「GAP前のDropoutがGAP後より強く効き、両方が単独パターンより良い結果」という、事前仮説とは逆のパターンが実際に起きました。原因は、①②で述べた通り「GAPの平均化によるノイズの弱まり」よりも「過学習の発生源(Conv層)に近い位置で効かせる効果」の方が支配的だったこと、および「両方」のtrain-val gapのマイナスは正則化の強さそのものではなくDropoutが学習モードのtrain_accuracyを押し下げる測定上の特性によるものと考えられます。同様の逆転が手元の実験で起きた場合は、以下を確認してください。

  • train_accuracyとval_accuracyの算出条件(Dropoutの有無)を混同していないか。gapがマイナスの場合は「過学習していない」ではなく、この測定上の特性を疑う
  • test_accuracy(推論モードでの評価)を主指標にして、train-val gapは補助的に見る
  • Conv層の出力サイズ(今回は8×8×128)とDense層の出力サイズ(128)のどちらがより過学習しやすい構造かをモデルサイズの観点で確認する
ケース実務での推奨
過学習をしっかり抑えたいGAP後単体(Dense直前)よりGAP前(Conv出力直後)に置く方が、今回の実験では過学習抑制・精度の両面で有利だった。まずはGAP前を試す価値がある。
精度を最優先したい今回は「両方」がtest_accuracy最良(67.91%)だった。GAP前後どちらかで悩むより、両方に入れて様子を見る選択肢も検討に値する。
train-val gapで過学習度を判断する場合Dropout箇所が増えるとtrain_accuracyが学習モードの影響で押し下げられ、gapが実態以上に小さく(マイナスに)見えることがある。test_accuracyも必ず併記して判断する。
Conv層の特徴マップに正則化をかけたい標準DropoutよりSpatialDropout2Dの方がチャンネル単位の正則化として理論的に適している。別実験での比較が望ましい。

まとめ

CIFAR-10・Conv2D×2層のモデルでDropout(0.2)の位置を比較した結果、GAP前(Conv出力直後)に置く方がGAP後(Dense出力直後)よりtest_accuracyが高く、過学習も抑えられました(67.47% vs 66.46%)。「GAPの平均化でノイズが弱まる」という事前仮説とは逆に、過学習の発生源に近い位置で正則化をかける効果の方が大きかったと考えられます。また「両方」に入れても二重正則化による精度低下は起きず、むしろ3パターン中もっとも高いtest_accuracy(67.91%)になりました。ただしtrain-val gapがマイナスになった点は、Dropoutが学習モードのtrain_accuracyを押し下げる測定上の特性であり、「過学習していない」という意味には直結しない点に注意が必要です。


関連記事もあわせてどうぞ: