PyTorch バッチ正規化と Dropout

バッチ正規化(Batch Normalization)と Dropout は、深層ニューラルネットワークのトレーニングにおける最も中心的な正則化および安定化技術の2つです。前者はトレーニング中の内部共変量シフトの問題を解決し、より深いネットワークを学習可能にします。後者はニューロンをランダムに破棄することで過学習を防ぎ、モデルの汎化能力を高めます。両者は通常組み合わせて使用され、現代のニューラルネットワークの標準コンポーネントです。


1. バッチ正規化(Batch Normalization)

1.1 基本原理

深層ネットワークの学習時、前の層のパラメータのわずかな変化は層が深くなるにつれて増幅され続け、後続の層の入力分布が絶えず変化します。この現象は内部共変量シフト(Internal Covariate Shift)と呼ばれます。バッチ正規化は各層の出力を標準化することで、活性化値を安定した分布に強制的に戻し、この問題を解決します。

正規化の式:

\[\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}\]

\[y_i = \gamma \hat{x}_i + \beta\]

ただし:

  • \(\mu_B\)、\(\sigma_B^2\) は現在のバッチの平均と分散です
  • \(\epsilon\) はゼロ除算を防ぐ小さな定数です(デフォルト1e-5)
  • \(\gamma\)、\(\beta\) は学習可能なスケール・シフトパラメータで、ネットワークが最終的な分布の形を自分で決定できるようにします

バッチ正規化の利点:

  • より大きな学習率を使用でき、学習速度が向上する
  • 初期化への感度が低減し、学習が安定する
  • 一定の正則化効果があり、Dropout への依存を軽減する
  • 勾配消失・爆発を緩和し、より深いネットワークを学習可能にする

1.2 BatchNorm1d / 2d / 3d

PyTorch は入力の次元に応じて3つのバージョンを提供しています。使用方法は完全に同じで、扱うデータの形状だけが異なります。

BatchNorm1d: 全結合層・系列データ用

例

import torch
import torch.nn as nn

# 入力形状:(N, C) または (N, C, L)
# N=batch_size, C=特徴数/チャンネル数, L=系列長
bn1d = nn.BatchNorm1d(
    num_features=128,    # 特徴数/チャンネル数
    eps=1e-5,            # ゼロ除算を防ぐ小さな定数(デフォルト 1e-5)
    momentum=0.1,        # 移動平均のモメンタム(デフォルト 0.1)
    affine=True,         # gamma と beta を学習するかどうか(デフォルト True)
    track_running_stats=True,  # 実行時の平均・分散を追跡するかどうか(デフォルト True)
)

# 全結合層の後で使用
x = torch.randn(32, 128)     # (batch=32, features=128)
out = bn1d(x)
print(out.shape)              # torch.Size([32, 128])

# 系列データ(1D 畳み込みの後など)
x_seq = torch.randn(32, 128, 50)   # (batch, channels, seq_len)
out_seq = bn1d(x_seq)
print(out_seq.shape)               # torch.Size([32, 128, 50])

BatchNorm2d: CNN の画像特徴マップ用

例

# 入力形状:(N, C, H, W)
# 各チャンネル C ごとに独立して正規化
bn2d = nn.BatchNorm2d(num_features=64)  # 64 チャンネル

x = torch.randn(32, 64, 28, 28)    # (batch, channels, height, width)
out = bn2d(x)
print(out.shape)                    # torch.Size([32, 64, 28, 28])

BatchNorm3d: 3D 畳み込み用(動画・医用画像)

例

# 入力形状:(N, C, D, H, W)
bn3d = nn.BatchNorm3d(num_features=32)

x = torch.randn(4, 32, 16, 32, 32)    # (batch, channels, depth, h, w)
out = bn3d(x)
print(out.shape)                        # torch.Size([4, 32, 16, 32, 32])

学習可能なパラメータを確認

例

bn = nn.BatchNorm2d(64)

print(f"weight (gamma) shape: {bn.weight.shape}")   # torch.Size([64])
print(f"bias   (beta)  shape: {bn.bias.shape}")     # torch.Size([64])
print(f"running_mean   shape: {bn.running_mean.shape}")  # torch.Size([64])
print(f"running_var    shape: {bn.running_var.shape}")   # torch.Size([64])

# running_mean / running_var は学習可能なパラメータではありません
# これらは推論時に使用する移動平均統計量です
print(f"学習可能なパラメータ数: {sum(p.numel() for p in bn.parameters())}")  # 128(64×2)

momentum パラメータの意味

例

# PyTorch の momentum は通常の理解とは逆です:
# running_mean = (1 - momentum) × running_mean + momentum × batch_mean
つまり momentum が大きいほど、現在のバッチの統計量への重みが大きくなります

# デフォルト 0.1:通常は適切
# バッチサイズが小さい場合は小さく調整推奨:momentum=0.01、統計量の不安定さを回避
bn = nn.BatchNorm2d(64, momentum=0.01)

# momentum=None:累積移動平均(CMA)を使用、非常に小さいバッチに適する
bn = nn.BatchNorm2d(64, momentum=None)

1.3 LayerNorm 層正規化

に対して単一サンプルのすべての特徴を正規化します。バッチに依存せず、バッチサイズが小さいまたは可変のシナリオに適します。Transformer の標準的な正規化方法。

BatchNorm:在 batch 维度计算均值/方差(跨样本,同通道)
LayerNorm:在特征维度计算均值/方差(同样本,跨特征)

例

import torch
import torch.nn as nn

# ── 全結合・Transformer 用 ────────────────────────
# normalized_shape:最後のいくつかの次元を正規化
ln = nn.LayerNorm(normalized_shape=512)

x = torch.randn(32, 10, 512)   # (batch, seq_len, embed_dim)
out = ln(x)
print(out.shape)                # torch.Size([32, 10, 512])

# ── 複数の次元を正規化 ───────────────────────────────
ln_2d = nn.LayerNorm([64, 28, 28])   # C, H, W の3次元を正規化
x = torch.randn(8, 64, 28, 28)
out = ln_2d(x)

# ── Transformer での典型的な使い方 ──────────────────────
class TransformerBlock(nn.Module):
    def __init__(self, embed_dim, num_heads, ff_dim):
        super().__init__()
        self.attn  = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
        self.ff    = nn.Sequential(
            nn.Linear(embed_dim, ff_dim),
            nn.GELU(),
            nn.Linear(ff_dim, embed_dim),
        )
        self.norm1 = nn.LayerNorm(embed_dim)   # ← 各サブレイヤーの後に正規化
        self.norm2 = nn.LayerNorm(embed_dim)

    def forward(self, x):
        attn_out, _ = self.attn(x, x, x)
        x = self.norm1(x + attn_out)            # 残差 + 正規化
        x = self.norm2(x + self.ff(x))
        return x

1.4 GroupNorm グループ正規化

チャンネルをいくつかのグループに分け、组各グループ内で正規化します。バッチサイズに依存せず、小バッチのシナリオ(物体検出、医用画像分割など)で BatchNorm より優れています。

例

# num_groups:チャンネルをいくつのグループに分けるか(num_channels を割り切れる必要があります)
# 特殊なケース:num_groups=1 は LayerNorm と等価
# num_groups=num_channels は InstanceNorm と等価
gn = nn.GroupNorm(
    num_groups=32,       # グループ数(よく使う:8, 16, 32)
    num_channels=256,    # チャンネル数
    eps=1e-5,
    affine=True,
)

x = torch.randn(4, 256, 56, 56)    # 小バッチ(4枚の画像)、256チャンネル
out = gn(x)
print(out.shape)                    # torch.Size([4, 256, 56, 56])

# GroupNorm は batch=1 のとき(例:推論で1枚の画像)効果がまったく影響を受けない
x_single = torch.randn(1, 256, 56, 56)
out_single = gn(x_single)           # 完全に正常に動作する

1.5 InstanceNorm インスタンス正規化

に対して各サンプルの各チャンネル独立に正規化し、バッチ次元に関与しない。よく使われるのは画像スタイル変換——各画像自身のスタイル情報を保持し、バッチ内の他画像の統計量が干渉しないようにする。

例

# InstanceNorm2d:各 (N, C) の H×W に対して正規化を行う
in2d = nn.InstanceNorm2d(
    num_features=64,
    affine=False,    # デフォルトは False(gamma/beta を学習しない)
)

x = torch.randn(8, 64, 128, 128)
out = in2d(x)
print(out.shape)    # torch.Size([8, 64, 128, 128])

1.6 各正規化手法の比較

方法 正規化次元 batch 依存 適用シーン
BatchNorm バッチ間(同チャンネル) 強く依存 CNN 画像分類(大バッチ)
LayerNorm 特徴間(同サンプル) 依存なし Transformer、NLP、RNN
GroupNorm グループ内チャンネル(同サンプル) 依存なし 小バッチ物体検出、セグメンテーション
InstanceNorm 空間次元(同サンプル同チャンネル) 依存なし 画像スタイル変換、生成モデル

選択の提案早見表

# batch_size >= 16、CNN画像タスク → BatchNorm2d
# Transformer / BERT / GPT            → LayerNorm
# batch_size < 8、物体検出/セグメンテーション → GroupNorm(num_groups=32)
# 画像スタイル変換 / CycleGAN → InstanceNorm2d

2. Dropout

2.1 基本原理

Dropout は訓練時にランダムに一部のニューロンの出力を 0 にする(確率はp)、ネットワークが単一のニューロンに依存できないようにし、よりロバストで分散された特徴表現を学習させ、過学習を効果的に防ぐ。

訓練時:

\[y_i = \begin{cases} 0 & \text{確率 } p \\ \dfrac{x_i}{1-p} & \text{確率 } 1-p \end{cases}\]

保持されたニューロンは \(\frac{1}{1-p}\) 倍に拡大され、期待値を変えないようにする(すなわち Inverted Dropout)。

推論時: Dropout 自動的にオフ、すべてのニューロンが通常どおり計算に関与し、複数のサブネットワークのアンサンブル平均に相当する。

训练时示意(p=0.5):
输入:  [1.0,  2.0,  3.0,  4.0,  5.0]
掩码:  [ 1,    0,    1,    0,    1 ]   ← 随机生成
输出:  [2.0,  0.0,  6.0,  0.0, 10.0]  ← 保留的值乘以 1/(1-0.5)=2

推理时:
输出:  [1.0,  2.0,  3.0,  4.0,  5.0]  ← 原样输出

2.2 Dropout / Dropout2d / Dropout3d

Dropout:全結合層に使用

例

import torch
import torch.nn as nn

dropout = nn.Dropout(p=0.5)   # p:ゼロにする確率

x = torch.ones(2, 10)
print("訓練モード:")
dropout.train()
print(dropout(x))    # 約 50% の値が 0、保持された値は 2.0

print("\n評価モード:")
dropout.eval()
print(dropout(x))    # すべて 1.0、Dropout オフ

出力例:

训练模式:
tensor([[2., 0., 2., 0., 2., 0., 0., 2., 2., 0.],
        [0., 2., 0., 2., 0., 2., 2., 0., 0., 2.]])

评估模式:
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])

Dropout2d:CNN 特徴マップに使用(チャンネル全体を破棄)

Dropout2dチャンネル単位でランダムに破棄する、つまりチャンネル全体のすべての空間位置がまとめてゼロにされる。これはポイント単位の Dropout よりも畳み込み特徴に適している。隣接ピクセルの活性化値は高い相関があるため、ポイント単位の破棄は効果が弱い。

例

dropout2d = nn.Dropout2d(p=0.3)

# 入力:(N, C, H, W)
x = torch.ones(4, 16, 8, 8)    # 4枚の画像、16チャンネル
out = dropout2d(x)
print(out.shape)                # torch.Size([4, 16, 8, 8])

# 検証:チャンネル全体の破棄
# 0枚目の画像のチャンネル破棄状況(非ゼロ = 保持チャンネル、全ゼロ = 破棄されたチャンネル)
kept_channels = (out[0].sum(dim=(1, 2)) != 0).sum().item()
print(f"0枚目の画像は {kept_channels}/16 チャンネルを保持")

Dropout3d:3D 畳み込みに使用

例

# 入力:(N, C, D, H, W)、全体の (D, H, W) 特徴ボリュームが破棄される
dropout3d = nn.Dropout3d(p=0.2)
x = torch.ones(2, 8, 16, 16, 16)
out = dropout3d(x)
print(out.shape)   # torch.Size([2, 8, 16, 16, 16])

p 値の選択推奨

例

# 全結合層(隠れ層が広い):比較的高い Dropout
nn.Dropout(p=0.5)     # 古典的な値、隠れ層の標準

# 全結合層(隠れ層が狭い)または小規模ネットワーク:比較的低い Dropout
nn.Dropout(p=0.2)     # 未学習を避ける

# 畳み込み層 Dropout2d:一般に低め
nn.Dropout2d(p=0.1)   # 畳み込み層自体にすでにある程度の正則化効果がある

# 分類ヘッド前の Dropout(例:EfficientNet / ViT)
nn.Dropout(p=0.3)     # 通常 0.2〜0.5 の間

# Transformer のアテンション / FFN Dropout
nn.Dropout(p=0.1)     # BERT のデフォルト値、Transformer は通常小さめ

2.3 AlphaDropout

専用にSELU 活性化関数設計された Dropout 変種であり、ニューロンを破棄した後、自動的に平均と分散を調整し、自己正規化(self-normalizing)属性を維持する。

例

# SELU と組み合わせて使用(自己正規化ネットワーク SELU + AlphaDropout)
class SelfNormalizingNet(nn.Module):
    def __init__(self, in_dim, hidden_dim, num_classes):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(in_dim, hidden_dim),
            nn.SELU(),
            nn.AlphaDropout(p=0.1),   # ← SELU と組み合わせて使用
            nn.Linear(hidden_dim, hidden_dim),
            nn.SELU(),
            nn.AlphaDropout(p=0.1),
            nn.Linear(hidden_dim, num_classes),
        )

    def forward(self, x):
        return self.net(x)

注意:AlphaDropout は SELU 活性化関数を使用する場合にのみ意味があり、他の活性化関数と組み合わせる場合は通常の Dropout を直接使用する。


3. 学習モードと評価モード

BatchNorm と Dropout の動作は訓練時と評価時でまったく異なる、正しく切り替える必要がある。これは初心者に最もよくある間違いの一つである。

動作の違いの比較

コンポーネント model.train() model.eval()
BatchNorm 現在のバッチの平均/分散で正規化する;更新する running_mean/var running_mean/var で正規化する;更新しない
Dropout ランダムにゼロにする(確率 p で) オフ、すべてのニューロンが通常どおり出力

正しい使い方

例

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(128, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(256, 10),
)

# ── 訓練フェーズ ──────────────────────────────────────
model.train()    # ← 訓練モードに切り替え(デフォルト状態)

for inputs, labels in train_loader:
    optimizer.zero_grad()
    outputs = model(inputs)   # BN はバッチ統計を使用、Dropout はランダムに破棄
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

# ── 評価/検証フェーズ ─────────────────────────────────
model.eval()     # ← 評価モードに切り替え(非常に重要!)

with torch.no_grad():              # 同時に勾配計算をオフ
    for inputs, labels in val_loader:
        outputs = model(inputs)    # BN は実行統計を使用、Dropout はオフ
        # ...

# ── 画像1枚の推論 ──────────────────────────────────
model.eval()
with torch.inference_mode():       # no_grad より高速、推論専用
    output = model(single_input.unsqueeze(0))
    pred   = output.argmax(1).item()

モード切り替えが正しいか確認する

例

# このコードで BN と Dropout が正しいモードで動作しているか検証する
x = torch.randn(8, 128)

model.train()
out_train_1 = model(x)
out_train_2 = model(x)
print("訓練モードで2回の出力は同じ?", torch.allclose(out_train_1, out_train_2))
# False ← 毎回 Dropout マスクが異なり、出力が異なる(正常な現象)

model.eval()
out_eval_1 = model(x)
out_eval_2 = model(x)
print("評価モードで2回の出力は同じ?", torch.allclose(out_eval_1, out_eval_2))
# True ← Dropout オフ、BN は固定統計量を使用、出力は決定的(正常な現象)

4. ネットワーク内での使用位置

BatchNorm の配置場所

BatchNorm の位置には2つの流派があり、現在は両方とも実践されている:

流派1: 活性化関数の前(原著論文)

# Conv → BN → ReLU
nn.Sequential(
    nn.Conv2d(32, 64, 3, padding=1),
    nn.BatchNorm2d(64),      # BN は活性化の前
    nn.ReLU(inplace=True),
)

流派2: 活性化関数の後(一部の研究ではより効果的とされる)

# Conv → ReLU → BN
nn.Sequential(
    nn.Conv2d(32, 64, 3, padding=1),
    nn.ReLU(inplace=True),
    nn.BatchNorm2d(64),      # BN は活性化の後
)

実際の推奨:参考にしている論文の設計に従う;特別な要件がなければ、Conv → BN → ReLU がより主流

Dropout の配置場所

全結合ネットワーク: 活性化関数の後

nn.Sequential(
    nn.Linear(512, 256),
    nn.ReLU(),
    nn.Dropout(0.5),         # ReLU の後
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(128, 10),      # 出力層の前には通常 Dropout を追加しない
)

CNN: BN の後(BN があれば Dropout は省略可)

# 注意:BN 自体に正則化効果があり、CNN で既に BN がある場合、畳み込み層に Dropout を加える必要はない
# Dropout は通常、全結合層または分類ヘッドの前だけに追加する
nn.Sequential(
    nn.Conv2d(64, 128, 3, padding=1),
    nn.BatchNorm2d(128),
    nn.ReLU(inplace=True),
    # 通常ここには Dropout を追加しない(BN が正則化効果を提供済み)
)

Transformer: FFN とアテンション層の内部

class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),
            nn.Dropout(dropout),  # FFN 内部に Dropout を追加
            nn.Linear(d_ff, d_model),
            nn.Dropout(dropout),  # 出力前にさらに一度追加
        )
    def forward(self, x):
        return self.net(x)

BN と Dropout は同時に使うか?

全結合ネットワーク: 同時に使用可能

nn.Sequential(
    nn.Linear(512, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Dropout(0.3),         # BN + Dropout の共存は、一般的かつ効果的
    nn.Linear(256, 10),
)

畳み込みネットワーク:通常はどちらか一方のみ、またはヘッド部分のみに Dropout を使用。

原因:BN の正規化操作と Dropout は「分散シフト」問題を引き起こす可能性があります。つまり、訓練時には Dropout が活性化値の分散を変え、推論時には Dropout がオフになるため、BN が推論時に見る統計量が訓練時と一致しなくなります。

推奨される方法:

方法 A:畳み込み層では BN のみを使用し、全結合/分類ヘッドでは Dropout のみを使用

例

class ConvNet(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, 3, padding=1),
            nn.BatchNorm2d(64),       # 畳み込み部分:BN のみ使用
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 128, 3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.AdaptiveAvgPool2d(1),
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Dropout(0.5),          # 全結合部分:Dropout のみ使用
            nn.Linear(256, num_classes),
        )

    def forward(self, x):
        return self.classifier(self.features(x))

5. 完全なネットワーク例

例1: 全結合分類ネットワーク(BN + Dropout)

例

import torch
import torch.nn as nn

class MLPClassifier(nn.Module):
    """
BatchNorm1d と Dropout を備えた全結合分類器
表形式データの分類タスクに適しています
    """

    def __init__(self, in_dim, hidden_dims, num_classes, dropout=0.5):
        super().__init__()
        layers = []
        prev_dim = in_dim

        for hidden_dim in hidden_dims:
            layers += [
                nn.Linear(prev_dim, hidden_dim),
                nn.BatchNorm1d(hidden_dim),
                nn.ReLU(inplace=True),
                nn.Dropout(dropout),
            ]
            prev_dim = hidden_dim

        layers.append(nn.Linear(prev_dim, num_classes))
        self.net = nn.Sequential(*layers)

    def forward(self, x):
        return self.net(x)


model = MLPClassifier(
    in_dim=784,
    hidden_dims=[512, 256, 128],
    num_classes=10,
    dropout=0.4
)

# 出力形状を検証
x = torch.randn(32, 784)
model.eval()
print(model(x).shape)   # torch.Size([32, 10])

例2:CNN 画像分類(BN 主体)

例

class ConvBNBlock(nn.Module):
    """標準的な畳み込みブロック:Conv → BN → ReLU"""
    def __init__(self, in_ch, out_ch, stride=1):
        super().__init__()
        self.block = nn.Sequential(
            nn.Conv2d(in_ch, out_ch, 3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(out_ch),    # bias=False:BN の beta がバイアスの役割を果たす
            nn.ReLU(inplace=True),
        )

    def forward(self, x):
        return self.block(x)


class SmallCNN(nn.Module):
    """CIFAR-10 用の小型 CNN"""
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            ConvBNBlock(3, 32),               # 32×32 → 32×32
            ConvBNBlock(32, 64, stride=2),    # 32×32 → 16×16
            ConvBNBlock(64, 128, stride=2),   # 16×16 → 8×8
            ConvBNBlock(128, 256, stride=2),  # 8×8 → 4×4
            nn.AdaptiveAvgPool2d(1),          # 4×4 → 1×1
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Dropout(0.5),                  # 全結合層でのみ Dropout を使用
            nn.Linear(128, num_classes),
        )

    def forward(self, x):
        return self.classifier(self.features(x))


model = SmallCNN(num_classes=10)
x = torch.randn(16, 3, 32, 32)
model.eval()
print(model(x).shape)   # torch.Size([16, 10])

例3: 完全な学習ループ

例

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR

DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
EPOCHS = 50

model     = SmallCNN(num_classes=10).to(DEVICE)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS)

def run_epoch(model, loader, optimizer=None, train=True):
    if train:
        model.train()    # ← BN はバッチ統計を使用、Dropout は活性化
    else:
        model.eval()     # ← BN はランニング統計を使用、Dropout はオフ

    total_loss, correct = 0.0, 0

    ctx = torch.enable_grad() if train else torch.no_grad()
    with ctx:
        for imgs, labels in loader:
            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)

            if train:
                optimizer.zero_grad()

            outputs = model(imgs)
            loss    = criterion(outputs, labels)

            if train:
                loss.backward()
                optimizer.step()

            total_loss += loss.item() * imgs.size(0)
            correct    += (outputs.argmax(1) == labels).sum().item()

    n = len(loader.dataset)
    return total_loss / n, correct / n


best_acc = 0.0
for epoch in range(1, EPOCHS + 1):
    train_loss, train_acc = run_epoch(model, train_loader, optimizer, train=True)
    val_loss,   val_acc   = run_epoch(model, val_loader,   train=False)
    scheduler.step()

    print(f"Epoch {epoch:2d}/{EPOCHS} | "
          f"Train {train_loss:.4f}/{train_acc:.4f} | "
          f"Val {val_loss:.4f}/{val_acc:.4f}")

    if val_acc > best_acc:
        best_acc = val_acc
        torch.save(model.state_dict(), 'best.pth')

6. ハイパーパラメータ調整ガイド

BatchNorm パラメータ調整

例

# ── momentum(最も調整が必要なパラメータ)─────────────────
# デフォルト値 0.1 はほとんどの場合有効

# batch_size が非常に小さい(< 8)→ 統計量が不安定 → momentum を減らす
nn.BatchNorm2d(64, momentum=0.01)

# 訓練ステップ数が多い、データ分布が安定 → momentum を適宜増やせる
nn.BatchNorm2d(64, momentum=0.2)

# ── affine=False(gamma/beta を学習しない)──────────────
# めったに使わず、通常はデフォルトの True のまま
# 明確に BN に特徴のスケールとシフトを変更させたくない場合を除く
nn.BatchNorm2d(64, affine=False)

# ── track_running_stats=False ──────────────────────
# 毎回の推論でも現在のバッチ統計を使用(訓練と推論の動作が一致)
# オンライン学習、ストリーミングデータなどの特殊なシーンに適する
nn.BatchNorm2d(64, track_running_stats=False)

Dropout 調整戦略

例

# ── 過学習の程度に応じて p を調整 ───────────────────────────
# 訓練 acc が検証 acc よりはるかに高い(深刻な過学習) → p を大きくする
# 訓練 acc と検証 acc が近いが両方低い(未学習) → p を小さくするか Dropout を削除

# ── 推奨 p 値の参考 ───────────────────────────────
p_values = {
    '大規模全結合隠れ層(> 1024)': 0.5,
    '中規模全結合隠れ層(256~1024)': 0.3,
    '小規模全結合隠れ層(< 256)': 0.2,
    'CNN 畳み込み層(Dropout2d)': 0.1,
    '分類ヘッドの前': 0.3,
    'Transformer FFN': 0.1,
    'Transformer アテンション': 0.1,
}

# ── 動的 Dropout(訓練プロセスに応じて調整)──────────────────
# 一部の実践では、訓練初期は p を小さくし、後期に大きくする(モデルが複雑になるにつれて正則化を強化)
class DynamicDropoutNet(nn.Module):
    def __init__(self, initial_p=0.1):
        super().__init__()
        self.dropout = nn.Dropout(initial_p)
        self.fc = nn.Linear(256, 10)

    def set_dropout(self, p):
        self.dropout.p = p

    def forward(self, x):
        return self.fc(self.dropout(x))

model = DynamicDropoutNet(initial_p=0.1)
# 訓練中期になったら、Dropout を増やす
model.set_dropout(0.4)

7. よくある間違いと注意点

間違い1: eval() モードへの切り替え忘れ

誤った書き方

# 誤り:評価時に model.eval() を呼び出していない
model.train()   # デフォルト状態
for imgs, labels in val_loader:
    with torch.no_grad():
        outputs = model(imgs)   # Dropout はまだランダムに破棄!BN はまだ更新中!
        # 結果が不安定で、精度が実際より低くなり、running_mean/var を汚染する

正しい書き方

# 正しい:評価前に明示的にモードを切り替える
model.eval()
with torch.no_grad():
    outputs = model(imgs)    # 決定論的な出力

間違い2: Conv 層に bias を付けて BN を接続

誤った書き方

# 冗長:Conv の bias は BN の beta によって打ち消され、パラメータを浪費する
nn.Conv2d(64, 128, 3, padding=1, bias=True),   # デフォルトは True
nn.BatchNorm2d(128),

正しい書き方

# 正しい:Conv を BN に接続する場合、bias をオフにする
nn.Conv2d(64, 128, 3, padding=1, bias=False),  # ← bias=False
nn.BatchNorm2d(128),

間違い3: batch size=1 で BatchNorm を使用

誤った書き方

# 誤り:batch=1 の場合、BN は分散を計算できず、訓練時に NaN や inf が発生する
# 単一サンプルの推論は問題ない(running stats を使用)が、単一サンプルの訓練はクラッシュする
model.train()
x = torch.randn(1, 64, 32, 32)
out = bn2d(x)    # ← 訓練モードでは batch=1 はエラーや NaN を引き起こす

解決策

# 解決策1:GroupNorm を代わりに使用
nn.GroupNorm(num_groups=32, num_channels=64)

# 解決策2:LayerNorm を使用
nn.LayerNorm([64, 32, 32])

# 解決策3:推論時に eval() に切り替える、BN には影響しない
model.eval()
out = bn2d(x)    # 推論モードでは batch=1 は完全に正常

間違い4: Dropout による BN の分散シフト

問題

# 問題:Dropout は訓練時に活性化値の分散を変える
# 推論時は Dropout がオフになり、BN は訓練時の running_var(Dropout の影響を含む)を使用
# 統計量が一致せず、推論結果にずれが生じる
nn.Sequential(
    nn.Conv2d(64, 128, 3),
    nn.Dropout2d(0.5),        # ← 畳み込み層で Dropout を使用
    nn.BatchNorm2d(128),      # ← BN が見る分散は Dropout によって変更されている
)

解決策

# 方法1:BN を先に、Dropout を後
nn.Sequential(
    nn.Conv2d(64, 128, 3),
    nn.BatchNorm2d(128),      # 最初に BN(Dropout の影響を受けない)
    nn.ReLU(),
    nn.Dropout2d(0.1),        # その後に Dropout
)

# 方法2:畳み込み層では BN のみ、全結合層では Dropout のみ(最も推奨)

間違い5: 凍結した BN 層を eval モードに変更し忘れる

誤った書き方

# 転移学習で、バックボーンを凍結する場合、事前学習済み BN の running stats を更新すべきではない
# そうしないと、新しいデータ分布が事前学習の統計量を汚染する

# 誤り:パラメータを凍結しても、BN の running_mean/var はまだ更新される
model = models.resnet50(weights='IMAGENET1K_V2')
for param in model.parameters():
    param.requires_grad = False   # 学習可能なパラメータのみ凍結、BN 統計量は依然として更新される

正しい書き方

# 正しい方法:バックボーンの BN 層も eval モードに切り替える
def freeze_bn(model):
    for module in model.modules():
        if isinstance(module, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d)):
            module.eval()              # running_mean/var を固定
            for param in module.parameters():
                param.requires_grad = False

model = models.resnet50(weights='IMAGENET1K_V2')
freeze_bn(model)          # すべての BN を凍結
model.fc = nn.Linear(model.fc.in_features, 10)  # 新しい FC 層のみ訓練可能

クイック診断チェックリスト

現象 考えられる原因 確認項目
検証 loss が不安定 忘れmodel.eval() 評価前に呼び出すことを確認eval()
訓練/検証 acc の差が極端に大きい Dropout p大きすぎる 減らすことを試すpまたは Dropout を削除
訓練中に NaN/inf batch=1 で BatchNorm を使用 GroupNorm または LayerNorm に変更
転移学習の効果が低い BN running stats が汚染されている バックボーン BN 層で呼び出しeval()
モデル推論結果が不安定 推論時に切り替えていないeval() 推論前に必ずmodel.eval()
Conv パラメータ数が多い Conv+BN で bias をオフにしていない 設定bias=False
その他の拡張