この記事では、Batch Normalization(BN)あり/なしで構成が同じCNNをMNISTで学習させ、収束速度・安定性・汎化への影響を比べます。Colabでそのまま動くコード付き。
✅ モデル設計や実装レビューの相談はココナラ
へ(外部リンク)
Batch Normalizationとは
Batch Normalization(BN)は、ミニバッチごとに中間表現を正規化し、学習を安定させる手法です。各層の出力を平均0・分散1へスケーリングしつつ、学習可能なパラメータ gamma
(スケール)と beta
(シフト)で表現力を保ちます。
なぜ効く?—直感的な理解
- 勾配の爆発/消失の緩和: 入力分布のスケールが整うため、勾配が安定しやすい。
- 収束の加速: 学習率を少し高めにしても壊れにくく、同じエポック数でも到達精度が上がりやすい。
- わずかな正則化効果: ミニバッチ統計に由来するノイズが入ることで過学習が抑制される場合がある。
実務TIP
Conv層では Conv → BatchNorm → ReLU
の順がよく使われます。
Dense層でも Dense → BatchNorm → ReLU
が一般的です。
実験設定(MNIST・CNN)
- データ: MNIST(28×28グレースケール・10クラス)
- モデル: 同一アーキテクチャで BNあり・BNなし を比較
- 評価: トレーニング/検証の精度とロスの推移、最終テスト精度
- 実行環境: Google Colab + TensorFlow/Keras(CPUでも数分)
🔧 学習率・BN配置のチューニング相談はココナラ
へ(外部リンク)
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を少量追加する構成を試すと、チューニングしやすいです。
0 件のコメント:
コメントを投稿