深層学習モデルは数百万〜数十億のパラメータを持ち、訓練データに対してはほぼ完璧な精度を達成できます。しかし、訓練データにあまりにもフィットしすぎると、新しいデータに対する予測性能(汎化性能)が低下します。これが過学習(overfitting)です。
たとえるなら、試験の過去問だけを丸暗記した学生のようなものです。過去問の答えは完璧に再現できますが、新しい問題には対応できません。本当に必要なのは、問題の背後にある「概念」を理解することです。
過学習は「モデルの容量(パラメータ数)がデータ量に対して過剰」なときに起こりやすくなります。しかし、モデルを小さくすれば表現力が下がるため、理想的にはモデルの容量を保ちながら汎化性能を高めたいのです。
過学習対策を理解すると、以下のことが可能になります。
- 訓練・テスト精度のギャップを縮小: 実用的なモデルの性能を最大化
- 少量データでの学習: データが限られた場面でも安定した性能を実現
- ハイパーパラメータの調整: どの正則化手法をどの程度適用すべきかの判断
- 学習曲線の解釈: 過学習の兆候を早期に検出
本記事の内容
- 過学習の原理とバイアス-バリアンス分解
- L1/L2正則化(Weight Decay)の理論
- Dropoutの理論 — アンサンブル学習との関係
- Early Stopping — 暗黙の正則化
- Data Augmentation — データを増やす
- Pythonによる実装と効果の可視化
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
- ニューラルネットワークの基礎 — MLPの構造
- 誤差逆伝播法の理論と実装 — 勾配計算
過学習の原理
バイアス-バリアンス分解
モデルの予測誤差は、バイアス(bias)とバリアンス(variance)の2つの要因に分解できます。
$$ \begin{equation} E[(y – \hat{f}(\bm{x}))^2] = \text{Bias}[\hat{f}(\bm{x})]^2 + \text{Var}[\hat{f}(\bm{x})] + \sigma^2 \end{equation} $$
バイアス: モデルの予測の平均と真の値との差。モデルが単純すぎると大きい(過少学習)。
バリアンス: 訓練データの変動に対するモデルの予測の変動。モデルが複雑すぎると大きい(過学習)。
$\sigma^2$: データの本質的なノイズ(削減不可能)。
過学習はバリアンスが大きい状態です。正則化の目的は、バイアスをわずかに増やす代わりにバリアンスを大幅に減らし、全体の誤差を最小化することです。
過学習の判定
過学習の兆候は以下の学習曲線パターンで判定できます。
- 訓練損失は下がり続けるが、テスト損失が上がり始める: 典型的な過学習
- 訓練精度は100%に近いが、テスト精度が低い: 過学習が深刻
- 訓練損失とテスト損失に大きなギャップ: 汎化ギャップが大きい
バイアスとバリアンスのトレードオフを理解した上で、具体的な過学習対策を見ていきましょう。
L1/L2正則化(Weight Decay)
L2正則化の理論
L2正則化は損失関数に重みの二乗和のペナルティを追加します。
$$ \begin{equation} J_\text{reg}(\bm{\Theta}) = J(\bm{\Theta}) + \frac{\lambda}{2}\sum_{l}\|\bm{W}^{(l)}\|_F^2 \end{equation} $$
ここで $\|\bm{W}\|_F^2 = \sum_{i,j} w_{ij}^2$ はフロベニウスノルムの二乗、$\lambda > 0$ は正則化の強さを制御するハイパーパラメータです。
勾配降下法での更新式は
$$ \bm{W} \leftarrow \bm{W} – \eta \left(\frac{\partial J}{\partial \bm{W}} + \lambda \bm{W}\right) = (1 – \eta\lambda)\bm{W} – \eta\frac{\partial J}{\partial \bm{W}} $$
$(1 – \eta\lambda)$ の係数が重みを毎ステップ縮小するため、Weight Decay(重み減衰)とも呼ばれます。大きな重みにペナルティを課すことで、モデルが訓練データの個々のノイズに過剰にフィットすることを防ぎます。
L2正則化の幾何学的解釈
L2正則化は、パラメータ空間で原点を中心とする球($\|\bm{W}\|^2 \leq c$)の内側に重みを制約する効果があります。損失関数の等高線と球の交点が正則化された最適解になります。
直感的には、L2正則化は重みを「均等に小さく」する傾向があります。一部の重みだけが大きくなることを防ぎ、全体的に滑らかなモデルを促します。
L1正則化
L1正則化は重みの絶対値の和をペナルティにします。
$$ J_\text{reg}(\bm{\Theta}) = J(\bm{\Theta}) + \lambda\sum_{l}\|\bm{W}^{(l)}\|_1 $$
L1正則化は重みをスパースにする(多くの重みが正確に0になる)傾向があり、特徴選択の効果があります。ただし、深層学習ではL2正則化の方が一般的に使われます。
| L1正則化 | L2正則化 | |
|---|---|---|
| ペナルティ | $\lambda\sum|w|$ | $\frac{\lambda}{2}\sum w^2$ |
| 効果 | スパース化(特徴選択) | 重みの均一な縮小 |
| 幾何学的 | 菱形(ダイヤモンド)制約 | 球制約 |
| 深層学習での使用 | まれ | Weight Decayとして標準的 |
Dropout
Dropoutの仕組み
Dropout(Srivastava et al., 2014)は、学習時に各ユニットを確率 $p$ でランダムに「ドロップ」(無効化)する手法です。
訓練時の各ユニットの出力は
$$ \begin{equation} \tilde{h}_i = m_i \cdot h_i, \quad m_i \sim \text{Bernoulli}(1 – p) \end{equation} $$
$m_i$ はマスク変数で、確率 $p$ で0、確率 $1-p$ で1を取ります。$p$ はドロップ率(通常0.2〜0.5)です。
推論時はDropoutを適用しませんが、出力のスケールを合わせるために重みに $(1-p)$ を掛けます。
$$ h_i^\text{test} = (1-p) \cdot h_i $$
実装上は逆Dropout(inverted dropout)が一般的で、学習時に $1/(1-p)$ でスケーリングし、推論時は何もしません。
$$ \tilde{h}_i = \frac{m_i}{1-p} \cdot h_i \quad (\text{学習時}), \quad h_i^\text{test} = h_i \quad (\text{推論時}) $$
Dropoutがなぜ効くのか
Dropoutの正則化効果は、以下の3つの観点から理解できます。
アンサンブル効果: 各ミニバッチで異なるユニットがドロップされるため、事実上、指数的に多くの異なるサブネットワークを同時に学習していることになります。推論時はこれらのサブネットワークの「平均的な予測」を使っているとみなせます。
共適応の防止: Dropoutなしでは、特定のユニットが他のユニットの存在に強く依存する「共適応」(co-adaptation)が起きます。Dropoutは各ユニットが他のユニットに頼らず独立に有用な特徴を学習することを促します。
ノイズ注入: Dropoutは隠れ層にノイズを注入する正則化と見ることもできます。これにより、ネットワークが特定の活性化パターンに過度に依存することを防ぎます。
Dropoutの適用箇所
| 箇所 | 推奨ドロップ率 | 備考 |
|---|---|---|
| 全結合層 | 0.3〜0.5 | 最も効果的 |
| 畳み込み層 | 0.1〜0.2 | 空間的な情報を保つため小さめ |
| 入力層 | 0.1〜0.2 | データのデノイジング効果 |
| 出力層 | 適用しない | 出力は全て使う |
| BatchNormと併用 | 不要な場合が多い | BatchNormが正則化効果を持つため |
正則化手法の理論を押さえたところで、次に学習の「いつ止めるか」に関わるEarly Stoppingを見ていきましょう。
Early Stopping
暗黙の正則化
Early Stoppingは、バリデーション損失が上がり始めたら学習を止めるという、最もシンプルかつ効果的な正則化手法です。
訓練を続けると、モデルは徐々に訓練データのノイズまで学習し始めます。バリデーション損失の推移を監視し、一定回数(patience)改善しなかったら学習を停止します。
$$ \text{Stop if } \mathcal{L}_\text{val}^{(t)} > \min_{s \leq t} \mathcal{L}_\text{val}^{(s)} \text{ for } \text{patience} \text{ epochs} $$
L2正則化との関係
Early Stoppingには、L2正則化と似た効果があることが理論的に示されています。学習初期のパラメータは初期値(ゼロ付近)に近く、学習が進むにつれて大きくなります。Early Stoppingで学習を途中で止めることは、パラメータの大きさを制限することに相当します。
具体的には、二次損失関数の場合、学習率 $\eta$、エポック数 $T$ のSGDは、正則化パラメータ $\lambda \approx 1/(\eta T)$ のL2正則化と等価であることが示されています。
実装のポイント
import numpy as np
# Early Stopping の概念的な実装
class EarlyStopping:
"""バリデーション損失が改善しなくなったら学習を停止"""
def __init__(self, patience=10, min_delta=1e-4):
self.patience = patience
self.min_delta = min_delta
self.best_loss = np.inf
self.counter = 0
self.best_weights = None
def check(self, val_loss, model_weights):
if val_loss < self.best_loss - self.min_delta:
self.best_loss = val_loss
self.counter = 0
self.best_weights = [w.copy() for w in model_weights]
return False # 継続
else:
self.counter += 1
if self.counter >= self.patience:
return True # 停止
return False # 継続
# 使用例
early_stopper = EarlyStopping(patience=10)
print(f"patience={early_stopper.patience}: "
f"バリデーション損失が{early_stopper.patience}エポック改善しなければ停止")
print(f"min_delta={early_stopper.min_delta}: "
f"改善と見なす最小の損失減少量")
このコードはEarly Stoppingの実装パターンを示しています。patience はバリデーション損失が改善しなくなってから何エポック待つかを指定し、min_delta は改善と見なす最小の変化量です。PyTorchでは torch.optim.lr_scheduler.ReduceLROnPlateau と組み合わせて、学習率を下げてもなお改善しない場合に停止する戦略がよく使われます。
Data Augmentation
データを「水増し」する
Data Augmentation(データ拡張)は、既存のデータに変換を加えて新しいデータを生成する手法です。モデルの観点では、より多様なデータで学習することになり、過学習を防ぐ効果があります。
画像認識での代表的なデータ拡張手法は以下の通りです。
| 手法 | 説明 | パラメータ例 |
|---|---|---|
| 水平反転 | 左右を反転 | 確率0.5 |
| ランダムクロップ | 画像の一部を切り出す | パディング4px |
| 回転 | ランダムな角度で回転 | $\pm 15°$ |
| 色変換 | 明度・コントラスト・彩度を変化 | $\pm 0.2$ |
| Cutout | 画像の一部をマスク | 16×16ピクセル |
| Mixup | 2枚の画像とラベルを線形補間 | $\alpha = 0.2$ |
Mixupの理論
Mixup(Zhang et al., 2017)は、2つの訓練サンプルを線形補間して新しいサンプルを生成する手法です。
$$ \begin{align} \tilde{\bm{x}} &= \lambda \bm{x}_i + (1 – \lambda) \bm{x}_j \\ \tilde{y} &= \lambda y_i + (1 – \lambda) y_j \end{align} $$
$\lambda \sim \text{Beta}(\alpha, \alpha)$ はベータ分布から生成されます。$\alpha$ が小さいほど $\lambda$ は0か1に近い値(あまり混ぜない)を取り、大きいほど $\lambda \approx 0.5$(強く混ぜる)になります。
Mixupはラベル空間でも補間を行うため、モデルの出力がソフトになり、過学習を防ぐ効果があります。
CutMix
CutMix(Yun et al., 2019)は、Mixupのアイデアを空間的に適用したものです。画像の一部の領域を別の画像の同じ領域で置き換え、ラベルは面積比に応じて混合します。Cutoutのような情報欠落が起きず、Mixupよりも自然な画像が生成されるため、画像分類タスクで高い性能が報告されています。
Pythonでの実装と比較実験
各手法の効果を比較する
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
np.random.seed(42)
def sigmoid(z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
class RegularizedMLP:
"""正則化付きMLP"""
def __init__(self, dims, lr=0.1, l2_lambda=0, dropout_rate=0):
self.Ws = []
self.bs = []
self.lr = lr
self.l2_lambda = l2_lambda
self.dropout_rate = dropout_rate
self.training = True
for i in range(len(dims) - 1):
self.Ws.append(np.random.randn(dims[i+1], dims[i])
* np.sqrt(2.0 / dims[i]))
self.bs.append(np.zeros((dims[i+1], 1)))
def forward(self, X):
self.hs = [X]
self.zs = []
self.masks = []
h = X
for i in range(len(self.Ws)):
z = self.Ws[i] @ h + self.bs[i]
self.zs.append(z)
if i < len(self.Ws) - 1:
h = np.maximum(0, z)
# Dropout(学習時のみ)
if self.training and self.dropout_rate > 0:
mask = (np.random.rand(*h.shape) > self.dropout_rate).astype(float)
h = h * mask / (1 - self.dropout_rate)
self.masks.append(mask)
else:
self.masks.append(None)
else:
h = sigmoid(z)
self.hs.append(h)
return h
def backward(self, t):
m = t.shape[1]
delta = self.hs[-1] - t
for i in range(len(self.Ws) - 1, -1, -1):
# L2正則化の勾配を追加
dW = (1/m) * delta @ self.hs[i].T + self.l2_lambda * self.Ws[i]
db = (1/m) * np.sum(delta, axis=1, keepdims=True)
if i > 0:
delta = (self.Ws[i].T @ delta) * (self.zs[i-1] > 0).astype(float)
# Dropoutマスクの適用
if self.masks[i-1] is not None:
delta = delta * self.masks[i-1] / (1 - self.dropout_rate)
self.Ws[i] -= self.lr * dW
self.bs[i] -= self.lr * db
def compute_loss(self, y, t):
eps = 1e-8
loss = -np.mean(t * np.log(y + eps) + (1-t) * np.log(1-y + eps))
# L2正則化項
if self.l2_lambda > 0:
l2_term = sum(np.sum(W**2) for W in self.Ws)
loss += 0.5 * self.l2_lambda * l2_term
return loss
# --- データ準備(わざと少量にして過学習を起こしやすくする) ---
X_data, y_data = make_moons(n_samples=200, noise=0.2, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
X_data, y_data, test_size=0.3, random_state=42)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
X_tr = X_train.T
t_tr = y_train.reshape(1, -1)
X_te = X_test.T
t_te = y_test.reshape(1, -1)
# --- 各正則化手法の比較 ---
configs = [
("No Regularization", {"l2_lambda": 0, "dropout_rate": 0}),
("L2 Regularization\n($\\lambda=0.01$)", {"l2_lambda": 0.01, "dropout_rate": 0}),
("Dropout (p=0.3)", {"l2_lambda": 0, "dropout_rate": 0.3}),
("L2 + Dropout", {"l2_lambda": 0.005, "dropout_rate": 0.2}),
]
dims = [2, 128, 128, 128, 1]
epochs = 2000
colors = ["tab:red", "tab:blue", "tab:green", "tab:orange"]
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
for ax_idx, (name, config) in enumerate(configs):
ax = axes[ax_idx // 2, ax_idx % 2]
np.random.seed(42)
model = RegularizedMLP(dims, lr=0.3, **config)
train_losses = []
test_losses = []
for epoch in range(epochs):
model.training = True
y_pred = model.forward(X_tr)
train_loss = model.compute_loss(y_pred, t_tr)
model.backward(t_tr)
train_losses.append(train_loss)
model.training = False
y_test_pred = model.forward(X_te)
test_loss_val = -np.mean(
t_te * np.log(y_test_pred + 1e-8)
+ (1 - t_te) * np.log(1 - y_test_pred + 1e-8))
test_losses.append(test_loss_val)
ax.plot(train_losses, linewidth=2, color="tab:blue",
alpha=0.8, label="Train")
ax.plot(test_losses, linewidth=2, color="tab:orange",
alpha=0.8, label="Test")
ax.set_xlabel("Epoch", fontsize=11)
ax.set_ylabel("Loss", fontsize=11)
ax.set_title(name, fontsize=12)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_ylim(0, 1.5)
# 最終精度を表示
model.training = False
y_final = (model.forward(X_te) > 0.5).astype(int)
acc = np.mean(y_final == t_te)
gap = train_losses[-1] - test_losses[-1]
ax.text(0.98, 0.95, f"Test acc: {acc:.1%}\nGap: {abs(gap):.3f}",
transform=ax.transAxes, fontsize=10, ha="right", va="top",
bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow"))
plt.tight_layout()
plt.savefig("regularization_comparison.png", dpi=150, bbox_inches="tight")
plt.show()
この実験結果から、各正則化手法の効果が比較できます。
-
正則化なし(左上): 訓練損失(青)は順調に下がりますが、テスト損失(オレンジ)は途中から上昇しています。訓練損失とテスト損失のギャップが大きく、典型的な過学習パターンです
-
L2正則化(右上): 重みの大きさにペナルティを課すことで、訓練損失とテスト損失のギャップが縮小しています。訓練損失は正則化なしより高いですが、テスト損失はより低く安定しています
-
Dropout(左下): 訓練時にランダムにユニットを無効化するため、訓練損失にノイズが見られます。テスト損失は安定しており、過学習が効果的に抑制されています
-
L2 + Dropout(右下): 両手法を組み合わせることで、最もバランスの取れた学習曲線が得られています。訓練・テスト損失のギャップが最も小さく、汎化性能が高いことが示唆されます
正則化手法の選び方
実践的なガイドライン
| 状況 | 推奨される対策 |
|---|---|
| まず試すべき | Data Augmentation + L2正則化(Weight Decay) |
| 過学習が解消しない | Dropout(0.2〜0.5)を追加 |
| 学習が不安定 | Early Stopping + 学習率スケジューリング |
| データが極端に少ない | 転移学習 + Data Augmentation |
| モデルが大きすぎる | モデルサイズの削減を検討 |
組み合わせの注意点
- BatchNorm + Dropout: 併用すると性能が悪化することがある。BatchNormがDropoutのノイズを増幅するため。一般にBatchNormがある場合はDropoutは不要
- Weight Decay + Adam: Adamでの Weight Decay は
torch.optim.AdamWを使う。標準のAdamでは Weight Decay が適切に機能しない(AdamWが推奨) - Data Augmentation は常に有効: 他の正則化手法と干渉しないため、基本的に常に適用すべき
まとめ
本記事では、ニューラルネットワークの過学習を防ぐ主要な4手法を解説しました。
- L2正則化(Weight Decay): 損失関数に重みの二乗和ペナルティを追加。重みを原点方向に縮小し、モデルの複雑さを制限する
- Dropout: 学習時にランダムにユニットを無効化。アンサンブル効果と共適応の防止により汎化性能を向上
- Early Stopping: バリデーション損失の悪化を検知して学習を停止。L2正則化と同等の暗黙的な正則化効果
- Data Augmentation: データ変換(反転、回転、Mixupなど)でデータを水増し。データの多様性を増やして過学習を防ぐ
- 実践的には、Data Augmentation + Weight Decay を基本とし、必要に応じてDropout、Early Stoppingを追加
次のステップとして、以下の記事も参考にしてください。
- 転移学習の理論と実践 — 少量データでの対策
- BatchNorm vs LayerNorm — BatchNormの正則化効果
- PyTorch入門 — PyTorchでの正則化の実装