BatchNorm、Conv前vs後どちらが有利?位置による精度差を実験【Keras】

投稿日:2026年7月19日日曜日 最終更新日:

BatchNormalization CIFAR-10 CNN Conv2D Google Colab Keras 過学習

X f B! L
BatchNorm、Conv前vs後どちらが有利?位置による精度差を実験【Keras】 アイキャッチ画像

KerasでCNNを組むとき、BatchNormalizationをどこに入れるかで迷ったことはないでしょうか。

よく見るのは「Conv→BatchNormalization→ReLU」という並び順です。原論文や多くのチュートリアルもこの形を採用しています。一方で「Conv→ReLU→BatchNormalization」という順番を使っているコードも見かけます。理論上はConv→BN→ReLUが定石とされていますが、実際にどれくらい差が出るのかは動かしてみないとわかりません。今回はCIFAR-10でこの2パターンを同条件で比較します。

📘 この記事でわかること
  • BatchNormalizationを「活性化関数の前」に置く理論的根拠
  • Conv→BN→ReLU と Conv→ReLU→BN の実装方法(Keras Functional API)
  • CIFAR-10で実際にどちらが精度・過学習耐性で優れるか
  • 実務でどちらを選ぶべきかの結論

BatchNormalizationの位置とは

BatchNormalizationは「内部共変量シフト(Internal Covariate Shift)」——各層への入力分布が学習中に変化し続け、学習を不安定にする現象——を抑えるために提案された手法です。この理屈に従えば、活性化関数に入力されるに分布を安定させる「Conv→BN→ReLU」が理にかなっています。

並び順考え方
Conv→BN→ReLU(従来型) ReLUに入る前の分布を正規化することで、活性化関数の出力を安定させる
Conv→ReLU→BN ReLU後の非線形な出力を正規化する。実装がシンプルという理由で使われることもある
⚠️ 注意:どちらも「動く」ので気づきにくい
どちらの並び順でもエラーは出ず、学習も一見正常に進みます。差が出るとしても精度で数%以内のことが多く、既存コードのBN位置が理論通りかどうかを意識していない実装者も少なくありません。

実験コード

使用環境はGoogle Colab、データセットはCIFAR-10です。BatchNormalizationの位置以外は完全に同一条件にして、位置の影響だけを取り出します。

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

!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 3 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 1s (6,911 kB/s)
Selecting previously unselected package fonts-ipafont-gothic.
(Reading database ... 118243 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 38.3 MB/s eta 0:00:00
  Preparing metadata (setup.py) ... done
  Building wheel for japanize_matplotlib (setup.py) ... done
環境準備完了

② import・データ準備・モデル定義

import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import matplotlib.pyplot as plt
import japanize_matplotlib
import time

SEED = 42
np.random.seed(SEED)
tf.random.set_seed(SEED)

# データ読み込み・正規化
(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
y_train = y_train.flatten()
y_test  = y_test.flatten()

EPOCHS = 30
BATCH_SIZE = 64

def build_model(bn_position="before"):
    """
    bn_position:
      "before" -> Conv→BN→ReLU(従来型)
      "after"  -> Conv→ReLU→BN
    """
    inputs = keras.Input(shape=(32, 32, 3))
    x = inputs
    for filters in [32, 64, 128]:
        x = layers.Conv2D(filters, 3, padding="same")(x)
        if bn_position == "before":
            x = layers.BatchNormalization()(x)
            x = layers.Activation("relu")(x)
        else:
            x = layers.Activation("relu")(x)
            x = layers.BatchNormalization()(x)
        x = layers.MaxPooling2D()(x)

    x = layers.GlobalAveragePooling2D()(x)
    x = layers.Dense(128, activation="relu")(x)
    x = layers.Dropout(0.3)(x)
    outputs = layers.Dense(10, activation="softmax")(x)

    model = keras.Model(inputs, outputs)
    model.compile(
        optimizer="adam",
        loss="sparse_categorical_crossentropy",
        metrics=["accuracy"],
    )
    return model
実行結果をクリックして内容を開く
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170498071/170498071 ━━━━━━━━━━━━━━━━━━━━ 21s 0us/step

③ 2パターンの学習実行

configs = [
    ("before", "Conv→BN→ReLU"),
    ("after",  "Conv→ReLU→BN"),
]

histories, times, scores = {}, {}, {}

for key, label in configs:
    print(f"\n=== {label} ===")
    tf.random.set_seed(SEED)
    model = build_model(key)
    start = time.time()
    history = model.fit(
        x_train, y_train,
        validation_data=(x_test, y_test),
        epochs=EPOCHS,
        batch_size=BATCH_SIZE,
        verbose=1,
    )
    elapsed = time.time() - start
    score = model.evaluate(x_test, y_test, verbose=0)
    histories[label] = history
    times[label] = elapsed
    scores[label] = score
    print(f"学習時間:{elapsed:.1f}秒 test_accuracy:{score[1]:.4f}")
実行結果をクリックして内容を開く(実行後に差し替え)
=== Conv→BN→ReLU ===
Epoch 1/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 16s 12ms/step - accuracy: 0.4673 - loss: 1.4660 - val_accuracy: 0.4848 - val_loss: 1.4080
Epoch 2/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.5882 - loss: 1.1485 - val_accuracy: 0.4705 - val_loss: 1.5923
Epoch 3/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.6361 - loss: 1.0191 - val_accuracy: 0.5673 - val_loss: 1.2393
Epoch 4/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.6704 - loss: 0.9295 - val_accuracy: 0.4921 - val_loss: 1.5208
Epoch 5/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.6954 - loss: 0.8608 - val_accuracy: 0.5410 - val_loss: 1.3564
Epoch 6/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.7188 - loss: 0.8020 - val_accuracy: 0.5439 - val_loss: 1.3481
Epoch 7/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.7357 - loss: 0.7532 - val_accuracy: 0.6029 - val_loss: 1.1503
Epoch 8/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.7541 - loss: 0.7091 - val_accuracy: 0.6159 - val_loss: 1.1556
Epoch 9/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.7659 - loss: 0.6695 - val_accuracy: 0.5603 - val_loss: 1.3602
Epoch 10/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.7807 - loss: 0.6342 - val_accuracy: 0.6059 - val_loss: 1.1472
Epoch 11/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.7915 - loss: 0.5997 - val_accuracy: 0.5930 - val_loss: 1.2974
Epoch 12/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.8041 - loss: 0.5631 - val_accuracy: 0.6562 - val_loss: 1.0121
Epoch 13/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8139 - loss: 0.5372 - val_accuracy: 0.6374 - val_loss: 1.0951
Epoch 14/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8243 - loss: 0.5060 - val_accuracy: 0.6377 - val_loss: 1.0912
Epoch 15/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.8330 - loss: 0.4812 - val_accuracy: 0.6381 - val_loss: 1.1516
Epoch 16/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8425 - loss: 0.4549 - val_accuracy: 0.6181 - val_loss: 1.3041
Epoch 17/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8517 - loss: 0.4292 - val_accuracy: 0.6865 - val_loss: 0.9805
Epoch 18/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.8568 - loss: 0.4109 - val_accuracy: 0.6478 - val_loss: 1.1632
Epoch 19/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8627 - loss: 0.3897 - val_accuracy: 0.6354 - val_loss: 1.3315
Epoch 20/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.8715 - loss: 0.3630 - val_accuracy: 0.6545 - val_loss: 1.2378
Epoch 21/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.8775 - loss: 0.3499 - val_accuracy: 0.6279 - val_loss: 1.3489
Epoch 22/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8839 - loss: 0.3304 - val_accuracy: 0.6870 - val_loss: 1.0859
Epoch 23/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.8895 - loss: 0.3130 - val_accuracy: 0.5899 - val_loss: 1.6674
Epoch 24/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8939 - loss: 0.3010 - val_accuracy: 0.6022 - val_loss: 1.7114
Epoch 25/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8972 - loss: 0.2898 - val_accuracy: 0.5931 - val_loss: 1.6473
Epoch 26/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.9032 - loss: 0.2728 - val_accuracy: 0.6250 - val_loss: 1.5257
Epoch 27/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9070 - loss: 0.2595 - val_accuracy: 0.6785 - val_loss: 1.3018
Epoch 28/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9107 - loss: 0.2469 - val_accuracy: 0.5977 - val_loss: 1.8090
Epoch 29/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.9144 - loss: 0.2411 - val_accuracy: 0.6122 - val_loss: 1.8161
Epoch 30/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9176 - loss: 0.2295 - val_accuracy: 0.6365 - val_loss: 1.5774
学習時間:145.6秒 test_accuracy:0.6365

=== Conv→ReLU→BN ===
Epoch 1/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 14s 11ms/step - accuracy: 0.4924 - loss: 1.3991 - val_accuracy: 0.5156 - val_loss: 1.4062
Epoch 2/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.6373 - loss: 1.0162 - val_accuracy: 0.5166 - val_loss: 1.5203
Epoch 3/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.7014 - loss: 0.8489 - val_accuracy: 0.6756 - val_loss: 0.9284
Epoch 4/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.7413 - loss: 0.7420 - val_accuracy: 0.6774 - val_loss: 0.9623
Epoch 5/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.7716 - loss: 0.6613 - val_accuracy: 0.6887 - val_loss: 0.9359
Epoch 6/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.7934 - loss: 0.5944 - val_accuracy: 0.6676 - val_loss: 1.0223
Epoch 7/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.8117 - loss: 0.5397 - val_accuracy: 0.6766 - val_loss: 1.0708
Epoch 8/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.8274 - loss: 0.4950 - val_accuracy: 0.6783 - val_loss: 1.1244
Epoch 9/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8423 - loss: 0.4504 - val_accuracy: 0.6823 - val_loss: 1.1053
Epoch 10/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.8555 - loss: 0.4091 - val_accuracy: 0.7077 - val_loss: 0.9943
Epoch 11/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8681 - loss: 0.3734 - val_accuracy: 0.7097 - val_loss: 0.9926
Epoch 12/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8764 - loss: 0.3465 - val_accuracy: 0.7203 - val_loss: 1.0085
Epoch 13/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.8869 - loss: 0.3203 - val_accuracy: 0.7010 - val_loss: 1.1772
Epoch 14/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.8935 - loss: 0.2949 - val_accuracy: 0.6655 - val_loss: 1.3933
Epoch 15/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9016 - loss: 0.2723 - val_accuracy: 0.6290 - val_loss: 1.7875
Epoch 16/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.9082 - loss: 0.2542 - val_accuracy: 0.7238 - val_loss: 1.1941
Epoch 17/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9110 - loss: 0.2437 - val_accuracy: 0.7068 - val_loss: 1.2764
Epoch 18/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9164 - loss: 0.2291 - val_accuracy: 0.6742 - val_loss: 1.5924
Epoch 19/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.9224 - loss: 0.2123 - val_accuracy: 0.7239 - val_loss: 1.2381
Epoch 20/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.9279 - loss: 0.1984 - val_accuracy: 0.7167 - val_loss: 1.4438
Epoch 21/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.9295 - loss: 0.1957 - val_accuracy: 0.7266 - val_loss: 1.3196
Epoch 22/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.9353 - loss: 0.1780 - val_accuracy: 0.7040 - val_loss: 1.5654
Epoch 23/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9381 - loss: 0.1703 - val_accuracy: 0.7243 - val_loss: 1.3757
Epoch 24/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.9387 - loss: 0.1704 - val_accuracy: 0.6951 - val_loss: 1.6370
Epoch 25/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 6ms/step - accuracy: 0.9469 - loss: 0.1479 - val_accuracy: 0.7211 - val_loss: 1.5619
Epoch 26/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9467 - loss: 0.1486 - val_accuracy: 0.7069 - val_loss: 1.5639
Epoch 27/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.9487 - loss: 0.1426 - val_accuracy: 0.7009 - val_loss: 1.6734
Epoch 28/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9528 - loss: 0.1329 - val_accuracy: 0.6962 - val_loss: 1.7242
Epoch 29/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 4s 5ms/step - accuracy: 0.9533 - loss: 0.1289 - val_accuracy: 0.7109 - val_loss: 1.6523
Epoch 30/30
782/782 ━━━━━━━━━━━━━━━━━━━━ 5s 6ms/step - accuracy: 0.9526 - loss: 0.1324 - val_accuracy: 0.6864 - val_loss: 1.9557
学習時間:144.4秒 test_accuracy:0.6864

④ グラフ+サマリー出力

import pandas as pd

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("bn_position_val.png", dpi=150)
plt.show()

rows = []
for label in ["Conv→BN→ReLU", "Conv→ReLU→BN"]:
    val_acc = histories[label].history["val_accuracy"][-1]
    test_acc = scores[label][1]
    train_acc = histories[label].history["accuracy"][-1]
    gap = train_acc - val_acc
    rows.append({
        "パターン": label,
        "val_accuracy": f"{val_acc:.4f}",
        "test_accuracy": f"{test_acc:.4f}",
        "train-val gap": f"{gap:.4f}",
        "学習時間(s)": f"{times[label]:.1f}",
    })
df = pd.DataFrame(rows)
print(df.to_string(index=False))

最終結果サマリー

        パターン val_accuracy test_accuracy train-val gap 学習時間(s)
Conv→BN→ReLU       0.6365        0.6365        0.2811   145.6
Conv→ReLU→BN       0.6864        0.6864        0.2662   144.4

実験結果

精度・損失グラフ

精度グラフ

精度グラフ

損失グラフ

損失グラフ
⚠️ 注記:今回の実験コードではmodel.fitvalidation_dataにテストセットをそのまま使用しているため、val_accuracyとtest_accuracyの値は一致しています。学習中の進捗確認用データとしてテストセットを使う簡易的な構成である点にご留意ください。
パターン val_accuracy test_accuracy train-val gap 学習時間
Conv→BN→ReLU(従来型) 0.6365 0.6365 0.2811 145.6秒
Conv→ReLU→BN 0.6864 0.6864 0.2662 144.4秒

考察

① 精度差はどの程度だったか——理論の定石が逆転

test_accuracyでConv→ReLU→BNが0.6864、Conv→BN→ReLUが0.6365となり、その差は約5.0ptでした。乱数シードのブレでは説明しにくい、明確な差です。「Conv→BN→ReLUが定石」という一般的な理解とは逆の結果になりました。

② 学習序盤の収束スピード——ReLU→BNが明確に早い

エポックごとのval_accuracyを見ると、差は学習の早い段階から出ています。

エポックConv→BN→ReLUConv→ReLU→BN
30.56730.6756
50.54100.6887
100.60590.7077

Conv→ReLU→BNはエポック3の時点で早くも67.6%に到達しているのに対し、Conv→BN→ReLUは同じタイミングで56.7%にとどまっています。エポック10でもその差は10pt前後開いたままで、学習全体を通じてConv→ReLU→BNが優位に推移しました。

③ 過学習耐性(train-val gap)——僅差だがReLU→BNがやや優位

最終エポックのtrain-val gap(train_accuracy − val_accuracy)は、Conv→BN→ReLUが0.2811、Conv→ReLU→BNが0.2662でした。Conv→ReLU→BNの方がtrain_accuracy自体は高い(0.9526 vs 0.9176)にもかかわらず、gapはむしろ小さく収まっています。どちらのパターンも30エポックではかなり過学習している点は共通しており、位置の違いだけで過学習を大きく抑えられるわけではなさそうです。

なお両パターンとも、学習後半でval_lossがtrain_lossから大きく乖離し始めています(Conv→BN→ReLUは最終エポックでtrain_loss 0.2295に対しval_loss 1.5774、Conv→ReLU→BNはtrain_loss 0.1324に対しval_loss 1.9557)。この規模の乖離は、BN位置の調整だけでなく、Dropout強化やEarlyStopping・Data Augmentationの併用も合わせて検討すべきサインです。

④ 理論と実験結果は一致しなかった——なぜか

内部共変量シフトの理論に従えば「ReLUに入る前に正規化するConv→BN→ReLU」の方が有利なはずですが、今回はその逆の結果になりました。考えられる要因はいくつかあります。

  • ReLU後の分布は非負に偏っている:Conv→ReLU→BNでは、常に0以上の値を正規化することになります。この非負分布に対するBatchNormalizationの挙動が、今回のネットワーク規模・Optimizer(Adam)との組み合わせでたまたま良く噛み合った可能性があります。
  • 単一シードの結果である:今回はSEED=42で1回のみの実行です。複数シードで平均を取ると差が縮まる、あるいは逆転する可能性は否定できません。
  • 学習率スケジュールなしのAdam:学習後半のval_loss急上昇(過学習)が両パターンで見られることから、今回の設定自体が「過学習しやすい条件」になっている可能性があります。BN位置の効果が、この過学習ノイズに埋もれている可能性も考えられます。

「Conv→BN→ReLUが常に正解」と鵜呑みにせず、手元のデータ・モデル規模で実際に試す価値があることを示す結果と言えそうです。

実務での推奨

状況推奨理由
新規にモデルを設計する場合両方試す価値あり。特にConv→ReLU→BNも選択肢に今回の結果ではConv→ReLU→BNが精度・収束速度の両面で優位だった
既存コードがConv→BN→ReLUの場合無理に書き換える必要はない差は条件依存。理論的裏付けのある定石を崩してまで変える根拠は弱い
過学習が問題になっている場合BN位置の変更だけに頼らない両パターンとも30エポックで大きく過学習しており、Dropout強化・EarlyStopping・Data Augmentationの併用が本質的な対策になる
まとめ
  • test_accuracyはConv→ReLU→BNが0.6864、Conv→BN→ReLUが0.6365で、約5.0ptの差がついた。一般的な定石(Conv→BN→ReLU)とは逆の結果
  • 収束スピードもConv→ReLU→BNが明確に早く、エポック3時点で67.6%(Conv→BN→ReLUは56.7%)に到達
  • train-val gapはConv→ReLU→BNの方がわずかに小さく(0.2662 vs 0.2811)、過学習耐性でもやや優位。ただし両パターンとも30エポックでは過学習が進んでおり、位置の違いだけで解決する規模ではない
  • 「BNはConv直後・活性化関数の前」という定石は今回のCIFAR-10・Adam・30エポックという条件では支持されなかった。手元の環境で実際に試す価値があることを示す結果
  • 今回は単一シードでの検証のため、複数シードでの再現性確認や、学習率スケジュール・Data Augmentation併用時の傾向は今後の検証課題

English Summary

We compared two BatchNormalization placements on CIFAR-10 over 30 epochs: Conv→BN→ReLU (the conventional order) and Conv→ReLU→BN. Contrary to the common wisdom that BN should precede the activation function, Conv→ReLU→BN outperformed Conv→BN→ReLU by roughly 5.0 points in test accuracy (68.6% vs 63.7%) and converged noticeably faster, reaching 67.6% validation accuracy by epoch 3 versus 56.7% for the conventional order. The train-val gap was also slightly smaller for Conv→ReLU→BN. Both configurations showed substantial overfitting by epoch 30, suggesting that BN placement alone is not a substitute for additional regularization such as Dropout, EarlyStopping, or data augmentation. As this result is based on a single random seed, further verification across multiple seeds is recommended before generalizing.

関連記事