Batch Normalization は本当に効くのか?──MNISTで「BNあり/なし」を徹底比較して分かったこと

投稿日:2025年9月8日月曜日 最終更新日:

BatchNormalization Google Colab Keras MNIST

X f B! P L
Batch Normalization解説記事のアイキャッチ画像(イメージ)

本記事では、Batch Normalization(BN)を使った場合と使わない場合で、学習の安定性・収束速度・最終精度がどう変わるかを MNIST + CNN で実験し、分かりやすくまとめています。
そのまま動かせる Google Colabコード付き なので、「どう効くのかを実際に確かめたい」方に最適です。


Batch Normalizationとは?(超シンプルに)

Batch Normalization(BN)は、各層の出力を標準化して学習を安定化させる手法です。勾配が暴れにくくなるため、 「速く・安定して学習が進む」というメリットがあります。

なぜ効くのか?直感的な理解

  • 勾配が安定する:スケールが整うことで爆発や消失を防ぎやすい
  • 収束が速い:多少高めの学習率でも壊れにくい
  • わずかな正則化効果:ミニバッチ統計の揺らぎが過学習を抑制

TIP(実務)
Conv層では Conv → BatchNorm → ReLU が定番。
Dense層でも Dense → BatchNorm → ReLU がよく使われます。

実験設定(MNIST・CNN)

  • データ: MNIST(28×28グレースケール・10クラス)
  • モデル: 同一アーキテクチャで BNありBNなし を比較
  • 評価: トレーニング/検証の精度とロスの推移、最終テスト精度
  • 実行環境: Google Colab + TensorFlow/Keras(CPUでも数分)

Colabで実行:BNあり/なしを比較

以下のコードをColabに貼り付ければ、そのまま実験できます(ランダム性により結果は毎回少し変わります)。

# %% Colab-ready: TensorFlow/Keras MNIST BN vs No-BN
import os, random, numpy as np, tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, models

# 再現性(目安)
seed = 42
random.seed(seed); np.random.seed(seed); tf.random.set_seed(seed)

# 1) データセット読み込み
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train = (x_train.astype("float32") / 255.0)[..., None]  # (60000,28,28,1)
x_test  = (x_test.astype("float32")  / 255.0)[..., None]

# 検証分割
x_train, x_val = x_train[:-5000], x_train[-5000:]
y_train, y_val = y_train[:-5000], y_train[-5000:]

def conv_block(filters, use_bn):
    block = keras.Sequential()
    block.add(layers.Conv2D(filters, 3, padding="same", use_bias=not use_bn))
    if use_bn:
        block.add(layers.BatchNormalization())
    block.add(layers.ReLU())
    return block

def build_model(use_bn=False):
    inputs = keras.Input(shape=(28,28,1))
    x = conv_block(32, use_bn)(inputs)
    x = layers.MaxPooling2D()(x)
    x = conv_block(64, use_bn)(x)
    x = layers.MaxPooling2D()(x)
    x = layers.Flatten()(x)
    x = layers.Dense(128, use_bias=not use_bn)(x)
    if use_bn:
        x = layers.BatchNormalization()(x)
    x = layers.ReLU()(x)
    outputs = layers.Dense(10, activation="softmax")(x)
    model = keras.Model(inputs, outputs, name=f"mnist_cnn_bn_{use_bn}")
    model.compile(
        optimizer=keras.optimizers.Adam(learning_rate=1e-3),
        loss="sparse_categorical_crossentropy",
        metrics=["accuracy"],
    )
    return model

bn_false = build_model(use_bn=False)
bn_true  = build_model(use_bn=True)

callbacks = [keras.callbacks.EarlyStopping(monitor="val_accuracy", patience=3, restore_best_weights=True)]

hist_no = bn_false.fit(
    x_train, y_train, validation_data=(x_val, y_val),
    epochs=10, batch_size=128, callbacks=callbacks, verbose=1)

hist_bn = bn_true.fit(
    x_train, y_train, validation_data=(x_val, y_val),
    epochs=10, batch_size=128, callbacks=callbacks, verbose=1)

test_no = bn_false.evaluate(x_test, y_test, verbose=0)
test_bn = bn_true.evaluate(x_test, y_test, verbose=0)

print("=== Test Accuracy ===")
print("No-BN :", round(float(test_no[1]), 4))
print("BN    :", round(float(test_bn[1]), 4))

# 観察用:最終エポックの検証精度を出力
print("ValAcc(No-BN):", round(hist_no.history["val_accuracy"][-1], 4))
print("ValAcc(BN)   :", round(hist_bn.history["val_accuracy"][-1], 4))

実行結果

Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11490434/11490434 ━━━━━━━━━━━━━━━━━━━━ 1s 0us/step
Epoch 1/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 12s 12ms/step - accuracy: 0.8475 - loss: 0.4886 - val_accuracy: 0.9804 - val_loss: 0.0707
Epoch 2/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 12s 5ms/step - accuracy: 0.9817 - loss: 0.0601 - val_accuracy: 0.9894 - val_loss: 0.0449
Epoch 3/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 2s 4ms/step - accuracy: 0.9885 - loss: 0.0384 - val_accuracy: 0.9874 - val_loss: 0.0493
Epoch 4/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 3s 5ms/step - accuracy: 0.9909 - loss: 0.0295 - val_accuracy: 0.9902 - val_loss: 0.0408
Epoch 5/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 2s 4ms/step - accuracy: 0.9928 - loss: 0.0228 - val_accuracy: 0.9894 - val_loss: 0.0411
Epoch 6/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 2s 4ms/step - accuracy: 0.9950 - loss: 0.0167 - val_accuracy: 0.9896 - val_loss: 0.0435
Epoch 7/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 3s 5ms/step - accuracy: 0.9963 - loss: 0.0128 - val_accuracy: 0.9892 - val_loss: 0.0436
Epoch 1/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 8s 12ms/step - accuracy: 0.9353 - loss: 0.2316 - val_accuracy: 0.4478 - val_loss: 1.7774
Epoch 2/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 6s 5ms/step - accuracy: 0.9907 - loss: 0.0360 - val_accuracy: 0.9868 - val_loss: 0.0466
Epoch 3/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 2s 5ms/step - accuracy: 0.9962 - loss: 0.0171 - val_accuracy: 0.9908 - val_loss: 0.0316
Epoch 4/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 3s 5ms/step - accuracy: 0.9988 - loss: 0.0080 - val_accuracy: 0.9796 - val_loss: 0.0655
Epoch 5/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 3s 6ms/step - accuracy: 0.9989 - loss: 0.0057 - val_accuracy: 0.9894 - val_loss: 0.0410
Epoch 6/10
430/430 ━━━━━━━━━━━━━━━━━━━━ 5s 5ms/step - accuracy: 0.9985 - loss: 0.0058 - val_accuracy: 0.9886 - val_loss: 0.0472
=== Test Accuracy ===
No-BN : 0.9848
BN    : 0.9915
ValAcc(No-BN): 0.9892
ValAcc(BN)   : 0.9886

観察ポイント(収束・汎化・安定性)

  • 収束速度: 同じエポック数ならBNありモデルの方が検証精度の立ち上がりが早いことが多い。
  • 汎化: BNにより検証ロスの乱高下が減り、過学習に入りにくい挙動が見える場合がある。
  • 安定性: バッチ統計でスケールが整い、学習率を少し上げても破綻しにくい傾向。

※ 実際の数値は実行環境・乱数により変動します。上記は一般的に観察されやすい傾向です。

うまく使うコツ & よくある落とし穴

コツ

  • 層の順序: Conv/Dense → BatchNorm → 活性化(ReLUなど)。
  • 学習率: BN導入時は 1e-3 から開始し、学習が安定する上限を探る。
  • ドロップアウトとの併用: まずBNだけで安定させ、必要に応じて最後段に少量のDropoutを追加。

落とし穴

  • バッチサイズが極端に小さい: 統計が不安定。32〜128程度を目安に。
  • 推論時の挙動: 学習時と推論時でBNは動作が異なるため、model.eval()(PyTorch)や training=False(TF一部API)などの扱いに注意。
  • 転移学習時: 事前学習モデル内のBNを凍結/微調整する戦略で精度が変わる。両方試すと良い。

まとめ

  • BNは学習の安定化収束の加速に寄与しやすく、MNISTでもその傾向を観察しやすい。
  • 実装は簡単:Conv/Dense → BN → 活性化 を基本に設計。
  • 学習率・バッチサイズ・Dropoutなどと合わせて最適点を探すと、より効果が出る。

FAQ

Q. 小規模データでもBNは有効?

A. 有効なことが多いですが、バッチサイズが極小だと不安定になることがあります。GroupNormやLayerNormを試すのも手です。

Q. 活性化の前と後、どっちに置く?

A. 本記事では一般的な実装として活性化の前(Conv/Dense → BN → ReLU)を推奨しています。

Q. BNとDropoutはどちらを先に?

A. まずBNで安定化し、必要に応じて最終段にDropoutを少量追加する構成を試すと、チューニングしやすいです。