バイアス-バリアンストレードオフとは?過学習を数式で理解する

機械学習でモデルを訓練するとき、訓練データへの当てはまりが完璧なのに、新しいデータでは性能がガタ落ちする——いわゆる過学習(overfitting)に悩まされた経験はないでしょうか。逆に、モデルが単純すぎてデータの傾向すら捉えられない未学習(underfitting)もまた問題です。

では、ちょうどよいモデルの複雑さはどう見つければよいのでしょうか。この問いに対する理論的な答えがバイアス-バリアンストレードオフ(bias-variance tradeoff)です。

バイアス-バリアンストレードオフを理解すると、以下のような場面で適切な判断ができるようになります。

  • モデル選択: 多項式回帰の次数やニューラルネットワークの層数をどう選ぶか
  • 正則化: L1/L2正則化の強さをなぜ調整する必要があるのか
  • アンサンブル学習: ランダムフォレストやブースティングがなぜ強力なのか(バリアンス低減 vs バイアス低減)
  • データ収集: データを増やすことでどちらの誤差が改善されるのか

本記事の内容

  • バイアスとバリアンスの直感的な理解
  • 二乗誤差のバイアス-バリアンス分解の導出
  • モデルの複雑さと汎化誤差の関係
  • Pythonによるバイアス-バリアンストレードオフの可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

バイアスとバリアンスとは

的当ての比喩で理解する

バイアスとバリアンスの概念は、的当て(ダーツ)で直感的に理解できます。

的の中心が「真の値」だとしましょう。何度もダーツを投げる(=異なる訓練データでモデルを訓練する)と、毎回少しずつ異なる場所に刺さります。

  • バイアスが高い: ダーツが常に的の中心からずれた方向に集まる。投げる回数を増やしても中心に当たらない。モデルが単純すぎて、データの本質的なパターンを捉えきれていない状態です
  • バリアンスが高い: ダーツが的の中心付近にも当たることがあるが、投げるたびにバラバラの場所に刺さる。モデルが複雑すぎて、訓練データのノイズにまで適合してしまっている状態です
  • 理想的: ダーツが毎回、的の中心付近に集中して刺さる。バイアスもバリアンスも低い状態です

ここで重要なのは、一般にバイアスを下げようとするとバリアンスが上がり、バリアンスを下げようとするとバイアスが上がるというトレードオフの関係がある点です。モデルを複雑にすればバイアスは下がりますが、訓練データへの感度が高まりバリアンスが上がります。

このトレードオフを数式で理解するために、まず汎化誤差の分解を導出しましょう。

問題設定

教師あり学習の枠組み

データ生成過程として、入力 $\bm{x}$ と出力 $y$ の関係を次のように仮定します。

$$ y = f(\bm{x}) + \varepsilon $$

ここで $f(\bm{x})$ は真の関数(未知)、$\varepsilon$ は平均0、分散 $\sigma^2$ の観測ノイズです。$\varepsilon$ は $\bm{x}$ とは独立とします。

訓練データ $\mathcal{D} = \{(\bm{x}_1, y_1), \ldots, (\bm{x}_n, y_n)\}$ から、真の関数 $f$ を近似するモデル $\hat{f}_\mathcal{D}(\bm{x})$ を学習します。添字 $\mathcal{D}$ はモデルが訓練データに依存することを明示しています。

汎化誤差の定義

新しいテストデータ $(\bm{x}_0, y_0)$ に対する予測誤差を二乗損失で測ります。

$$ \text{Err}(\bm{x}_0) = E\left[(y_0 – \hat{f}_\mathcal{D}(\bm{x}_0))^2\right] $$

この期待値は、テストデータのノイズ $\varepsilon$ と訓練データ $\mathcal{D}$ の両方に関する期待値です。この汎化誤差がどのような要因に分解されるかを、次のセクションで導出します。

バイアス-バリアンス分解の導出

分解のゴール

汎化誤差 $E[(y_0 – \hat{f}_\mathcal{D}(\bm{x}_0))^2]$ を、バイアスバリアンスノイズの3つの項に分解します。

導出

まず、テスト点 $\bm{x}_0$ における $\hat{f}_\mathcal{D}(\bm{x}_0)$ の訓練データ $\mathcal{D}$ に関する期待値を

$$ \bar{f}(\bm{x}_0) = E_\mathcal{D}[\hat{f}_\mathcal{D}(\bm{x}_0)] $$

と書きます。これはモデルの「平均的な予測」です。

汎化誤差を展開していきます。$y_0 = f(\bm{x}_0) + \varepsilon$ を代入すると

$$ E[(y_0 – \hat{f}_\mathcal{D}(\bm{x}_0))^2] = E[(f(\bm{x}_0) + \varepsilon – \hat{f}_\mathcal{D}(\bm{x}_0))^2] $$

右辺に $\bar{f}(\bm{x}_0)$ を加えて引きます(巧妙な恒等変形です)。

$$ = E[(f(\bm{x}_0) – \bar{f}(\bm{x}_0) + \bar{f}(\bm{x}_0) – \hat{f}_\mathcal{D}(\bm{x}_0) + \varepsilon)^2] $$

3つの項を $a = f(\bm{x}_0) – \bar{f}(\bm{x}_0)$、$b = \bar{f}(\bm{x}_0) – \hat{f}_\mathcal{D}(\bm{x}_0)$、$c = \varepsilon$ とおくと、$(a + b + c)^2$ の展開になります。

$$ = E[a^2] + E[b^2] + E[c^2] + 2E[ab] + 2E[ac] + 2E[bc] $$

ここで交差項がすべて0になることを確認します。

$a = f(\bm{x}_0) – \bar{f}(\bm{x}_0)$ は定数($\mathcal{D}$ にも $\varepsilon$ にも依存しない)なので、$E[ac] = aE[\varepsilon] = 0$ です。

$E[ab] = a \cdot E[\bar{f}(\bm{x}_0) – \hat{f}_\mathcal{D}(\bm{x}_0)] = a \cdot (\bar{f}(\bm{x}_0) – E_\mathcal{D}[\hat{f}_\mathcal{D}(\bm{x}_0)]) = a \cdot 0 = 0$ です。$\bar{f}$ の定義から直ちに従います。

$E[bc] = E[(\bar{f}(\bm{x}_0) – \hat{f}_\mathcal{D}(\bm{x}_0))\varepsilon]$ ですが、$\varepsilon$ はテストデータのノイズであり訓練データ $\mathcal{D}$ と独立なので、$E[bc] = E_\mathcal{D}[\bar{f}(\bm{x}_0) – \hat{f}_\mathcal{D}(\bm{x}_0)] \cdot E[\varepsilon] = 0 \cdot 0 = 0$ です。

したがって、汎化誤差は次の3項に分解されます。

$$ \begin{equation} E[(y_0 – \hat{f}_\mathcal{D}(\bm{x}_0))^2] = \underbrace{(f(\bm{x}_0) – \bar{f}(\bm{x}_0))^2}_{\text{Bias}^2} + \underbrace{E_\mathcal{D}[(\hat{f}_\mathcal{D}(\bm{x}_0) – \bar{f}(\bm{x}_0))^2]}_{\text{Variance}} + \underbrace{\sigma^2}_{\text{Noise}} \end{equation} $$

各項の意味

この分解式の各項を詳しく見ていきましょう。

バイアスの二乗 $(f(\bm{x}_0) – \bar{f}(\bm{x}_0))^2$

モデルの「平均的な予測」$\bar{f}(\bm{x}_0)$ が真の値 $f(\bm{x}_0)$ からどれだけ系統的にずれているかを測ります。バイアスはモデルの仮定の誤り(例: 非線形な真の関数を線形モデルで近似する)に起因します。モデルを複雑にすると、より柔軟に真の関数を近似できるのでバイアスは減少します。

バリアンス $E_\mathcal{D}[(\hat{f}_\mathcal{D}(\bm{x}_0) – \bar{f}(\bm{x}_0))^2]$

訓練データ $\mathcal{D}$ が変わったとき、モデルの予測がどれだけ変動するかを測ります。モデルが複雑すぎると訓練データのノイズにまで適合してしまい、異なる訓練データで全く異なる予測をするため、バリアンスが大きくなります。

既約誤差(ノイズ)$\sigma^2$

データに含まれる本質的なノイズであり、どんなモデルを使っても減らすことができません。これが汎化誤差の下限を決めます。

トレードオフの本質

バイアスとバリアンスのトレードオフが生じる理由は明確です。

  • モデルの複雑さを上げると、バイアスは下がるがバリアンスが上がる
  • モデルの複雑さを下げると、バリアンスは下がるがバイアスが上がる

汎化誤差を最小にするには、バイアスの二乗とバリアンスのが最小になるような複雑さを選ぶ必要があります。この最適点は、訓練データの量やノイズのレベルに依存します。

次に、具体的な例として多項式回帰を使って、このトレードオフを実際に確認してみましょう。

多項式回帰でのバイアス-バリアンス

多項式回帰モデル

バイアス-バリアンストレードオフを具体的に理解するために、1次元の多項式回帰を考えます。

真の関数を $f(x) = \sin(2\pi x)$ とし、ノイズを加えたデータ $y_i = f(x_i) + \varepsilon_i$($\varepsilon_i \sim N(0, \sigma^2)$)から、$d$ 次多項式

$$ \hat{f}_d(x) = w_0 + w_1 x + w_2 x^2 + \cdots + w_d x^d $$

を最小二乗法で当てはめます。

次数 $d$ がモデルの複雑さを制御するパラメータです。

  • $d = 1$(直線): $\sin$ 関数の非線形性を捉えきれない → バイアス大、バリアンス小
  • $d = 3$(3次曲線): ある程度非線形パターンを捉える → バイアス中、バリアンス中
  • $d = 15$(15次曲線): 訓練データのノイズにまで適合する → バイアス小、バリアンス大

理論的なバイアスとバリアンスの変化

多項式回帰の場合、パラメータ数は $d + 1$ です。データ数 $n$ に対して $d + 1$ が大きくなるほど、自由度が増えてバリアンスが増加します。

線形回帰の一般論として、$\hat{f}(\bm{x}_0)$ のバリアンスは近似的に

$$ \text{Var}(\hat{f}(\bm{x}_0)) \approx \frac{(d + 1)\sigma^2}{n} $$

のようにパラメータ数に比例して増加します。一方、バイアスは $d$ を増やすにつれて真の関数をよりよく近似できるため減少します(ただし $f$ が $d$ 次多項式で正確に表現できるなら、$d$ 以上でバイアスは0になります)。

この理論的な振る舞いをPythonで確認してみましょう。

Pythonによるバイアス-バリアンス分解の可視化

以下のコードでは、多項式回帰において訓練データセットを多数生成し、バイアスの二乗とバリアンスをモンテカルロ法で推定します。

import numpy as np
import matplotlib.pyplot as plt
from numpy.polynomial import polynomial as P

np.random.seed(42)

# 真の関数
def f_true(x):
    return np.sin(2 * np.pi * x)

# パラメータ
n_train = 25       # 訓練データ数
sigma = 0.3        # ノイズの標準偏差
n_datasets = 200   # 訓練データセットの数
x_test = np.linspace(0, 1, 100)  # テスト点

degrees = list(range(1, 16))  # 多項式の次数1~15

# 各次数についてバイアス^2とバリアンスを計算
bias_sq_list = []
variance_list = []
mse_list = []

for d in degrees:
    predictions = np.zeros((n_datasets, len(x_test)))

    for i in range(n_datasets):
        # 訓練データの生成
        x_train = np.random.uniform(0, 1, n_train)
        y_train = f_true(x_train) + np.random.normal(0, sigma, n_train)

        # 多項式フィット
        coeffs = np.polyfit(x_train, y_train, d)
        predictions[i] = np.polyval(coeffs, x_test)

    # 平均予測
    f_bar = predictions.mean(axis=0)

    # バイアスの二乗(各テスト点での平均)
    bias_sq = np.mean((f_true(x_test) - f_bar) ** 2)

    # バリアンス(各テスト点での平均)
    variance = np.mean(np.var(predictions, axis=0))

    # MSE = Bias^2 + Variance + sigma^2(ノイズ)
    mse = bias_sq + variance + sigma ** 2

    bias_sq_list.append(bias_sq)
    variance_list.append(variance)
    mse_list.append(mse)

# 可視化
fig, ax = plt.subplots(figsize=(10, 6))

ax.plot(degrees, bias_sq_list, "b-o", linewidth=2, markersize=6, label=r"Bias$^2$")
ax.plot(degrees, variance_list, "r-s", linewidth=2, markersize=6, label="Variance")
ax.plot(degrees, [sigma**2] * len(degrees), "g--", linewidth=1.5, label=r"Noise $\sigma^2$")
ax.plot(degrees, mse_list, "k-^", linewidth=2, markersize=6, label="Total Error (MSE)")

ax.set_xlabel("Polynomial Degree", fontsize=13)
ax.set_ylabel("Error", fontsize=13)
ax.set_title("Bias-Variance Tradeoff in Polynomial Regression", fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.set_xticks(degrees)
ax.set_ylim(bottom=0)

plt.tight_layout()
plt.savefig("bias_variance_tradeoff.png", dpi=150, bbox_inches="tight")
plt.show()

このグラフから、バイアス-バリアンストレードオフの典型的な振る舞いが読み取れます。

  1. バイアスの二乗(青線): 多項式の次数を上げるにつれて急速に減少し、次数3〜4あたりでほぼ0に近づきます。$\sin$ 関数はテイラー展開で3次以上の多項式でよく近似できるため、これは理論と一致します

  2. バリアンス(赤線): 次数の増加とともに単調に増加します。特に次数が10を超えると急激に増加し、パラメータ数がデータ数に近づくことで予測が不安定になることを示しています

  3. 総誤差(黒線): バイアスの二乗とバリアンスとノイズの和であり、次数3〜5付近に最小値をとるU字型のカーブを描きます。この最小点が最適なモデルの複雑さであり、過学習と未学習のバランス点です

  4. 既約誤差(緑破線): $\sigma^2 = 0.09$ は定数で、どの次数でも変わりません。これがどんなモデルでも達成できない誤差の下限です

次に、過学習と未学習の具体的な様子を可視化してみましょう。

過学習と未学習の可視化

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

def f_true(x):
    return np.sin(2 * np.pi * x)

n_train = 15
sigma = 0.3
x_train = np.random.uniform(0, 1, n_train)
y_train = f_true(x_train) + np.random.normal(0, sigma, n_train)
x_plot = np.linspace(0, 1, 200)

fig, axes = plt.subplots(1, 3, figsize=(16, 5))
titles = ["Underfitting (degree=1)", "Good fit (degree=4)", "Overfitting (degree=14)"]
degrees_show = [1, 4, 14]

for ax, d, title in zip(axes, degrees_show, titles):
    coeffs = np.polyfit(x_train, y_train, d)
    y_pred = np.polyval(coeffs, x_plot)

    ax.scatter(x_train, y_train, color="red", s=50, zorder=5, label="Training data")
    ax.plot(x_plot, f_true(x_plot), "g--", linewidth=2, label=r"True $f(x) = \sin(2\pi x)$")
    ax.plot(x_plot, y_pred, "b-", linewidth=2, label=f"Polynomial (d={d})")

    # 訓練誤差の計算
    y_train_pred = np.polyval(coeffs, x_train)
    train_mse = np.mean((y_train - y_train_pred) ** 2)

    ax.set_title(title, fontsize=13)
    ax.set_xlabel("x", fontsize=12)
    ax.set_ylabel("y", fontsize=12)
    ax.set_ylim(-2, 2)
    ax.legend(fontsize=9, loc="upper right")
    ax.grid(True, alpha=0.3)
    ax.text(0.02, 0.02, f"Train MSE = {train_mse:.4f}",
            transform=ax.transAxes, fontsize=10,
            bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow"))

plt.tight_layout()
plt.savefig("underfit_goodfit_overfit.png", dpi=150, bbox_inches="tight")
plt.show()

この3つのグラフは、モデルの複雑さによる過学習と未学習の典型的な様子を示しています。

  1. 左図(未学習, d=1): 直線ではサイン波の非線形パターンを捉えきれず、訓練データに対してもフィットが悪いです。訓練誤差が大きく、バイアスが高い状態を表しています。データを増やしてもこの系統的なずれは改善されません

  2. 中央図(適切, d=4): 4次多項式はサイン波の形状をよく近似しており、訓練データの近傍で真の関数に沿った滑らかな曲線を描いています。バイアスとバリアンスのバランスが取れた理想的な状態です

  3. 右図(過学習, d=14): 14次多項式は全ての訓練データ点をほぼ正確に通過し、訓練誤差はほぼ0です。しかし、データ点の間で激しく振動しており、真の関数から大きく外れています。訓練誤差は小さいがテスト誤差は大きい、典型的な過学習の状態です

この可視化から、訓練誤差が小さいことは必ずしもモデルが良いことを意味しないという重要な教訓が得られます。モデルの評価には必ず検証データやテストデータを用いる必要があるのです。

バイアス-バリアンスと訓練データ数の関係

モデルの複雑さだけでなく、訓練データの量もバイアス-バリアンスのバランスに影響を与えます。

直感的には、データが増えると

  • バイアス: 変化しない(モデルの仮定に依存するため)
  • バリアンス: 減少する(データが多いほど推定が安定する)
  • 汎化誤差: 減少する(バリアンスの低下分だけ)

つまり、同じモデルを使う限り、データを増やすとバリアンスは減りますが、バイアスは減りません。モデルの表現力が不足しているならば、データを増やしてもある水準以上に汎化誤差は改善されないのです。

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

def f_true(x):
    return np.sin(2 * np.pi * x)

sigma = 0.3
n_datasets = 200
x_test = np.linspace(0, 1, 100)
n_train_values = [10, 20, 50, 100, 200, 500]
degrees_plot = [1, 4, 10]

fig, axes = plt.subplots(1, 3, figsize=(16, 5))

for ax, d in zip(axes, degrees_plot):
    bias_sq_vs_n = []
    variance_vs_n = []
    mse_vs_n = []

    for n_train in n_train_values:
        predictions = np.zeros((n_datasets, len(x_test)))

        for i in range(n_datasets):
            x_train = np.random.uniform(0, 1, n_train)
            y_train = f_true(x_train) + np.random.normal(0, sigma, n_train)

            # 次数がデータ数以上の場合はクリップ
            d_actual = min(d, n_train - 1)
            coeffs = np.polyfit(x_train, y_train, d_actual)
            predictions[i] = np.polyval(coeffs, x_test)

        f_bar = predictions.mean(axis=0)
        bias_sq = np.mean((f_true(x_test) - f_bar) ** 2)
        variance = np.mean(np.var(predictions, axis=0))

        bias_sq_vs_n.append(bias_sq)
        variance_vs_n.append(variance)
        mse_vs_n.append(bias_sq + variance + sigma ** 2)

    ax.plot(n_train_values, bias_sq_vs_n, "b-o", linewidth=2, label=r"Bias$^2$")
    ax.plot(n_train_values, variance_vs_n, "r-s", linewidth=2, label="Variance")
    ax.plot(n_train_values, mse_vs_n, "k-^", linewidth=2, label="Total Error")
    ax.axhline(sigma ** 2, color="g", linestyle="--", linewidth=1.5, alpha=0.5)

    ax.set_xlabel("Number of Training Samples", fontsize=12)
    ax.set_ylabel("Error", fontsize=12)
    ax.set_title(f"Degree = {d}", fontsize=13)
    ax.legend(fontsize=9)
    ax.grid(True, alpha=0.3)
    ax.set_xscale("log")

plt.tight_layout()
plt.savefig("bias_variance_vs_n.png", dpi=150, bbox_inches="tight")
plt.show()

このグラフから、データ数とバイアス-バリアンスの関係が明確に読み取れます。

  1. 左図(d=1, 直線): バイアスの二乗はデータ数に関わらず一定の高い値を保っています。直線ではサイン波を近似できないため、いくらデータを増やしてもバイアスは減りません。バリアンスはデータ数が増えるにつれて急速に減少しますが、総誤差はバイアスに支配されて一定水準以下にはなりません

  2. 中央図(d=4, 適切な次数): バイアスの二乗は小さく一定です。バリアンスはデータ数とともに $O(1/n)$ で減少し、総誤差はノイズレベル $\sigma^2$ に漸近していきます。これが理想的な挙動です

  3. 右図(d=10, 過剰な次数): データ数が少ないときはバリアンスが非常に大きく、過学習の影響が顕著です。しかし、データ数が増えるとバリアンスは減少し、最終的にはバイアスの小さい利点が活きて良い汎化性能に収束します。ただし、同等の性能を得るのにd=4よりも多くのデータが必要です

この結果は実務上重要な指針を与えます。テスト誤差が高いとき、それがバイアスの問題なのかバリアンスの問題なのかを見極める必要があります。訓練誤差は低いのにテスト誤差が高い場合はバリアンスの問題(過学習)であり、データの追加や正則化が有効です。訓練誤差もテスト誤差も高い場合はバイアスの問題(未学習)であり、より複雑なモデルに切り替える必要があります。

バイアス-バリアンスとモデル正則化

正則化はバリアンスを減らす

過学習(バリアンスが高い状態)を防ぐ有効な手段の一つが正則化(regularization)です。正則化はモデルのパラメータに制約を加えることで、実効的な複雑さを抑制します。

Ridge回帰(L2正則化)を例にとると、損失関数は

$$ L(\bm{w}) = \sum_{i=1}^n (y_i – \hat{f}(\bm{x}_i))^2 + \lambda \|\bm{w}\|^2 $$

正則化パラメータ $\lambda$ を大きくすると、パラメータ $\bm{w}$ の大きさが制限されるため、モデルの自由度が実質的に減少します。

  • $\lambda = 0$: 正則化なし(最小二乗法と同じ)→ バリアンス大、バイアス小
  • $\lambda \to \infty$: 全てのパラメータが0に近づく → バリアンス小、バイアス大

つまり、$\lambda$ もバイアスとバリアンスのトレードオフを制御するパラメータであり、最適な $\lambda$ は交差検証で選択します。

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

def f_true(x):
    return np.sin(2 * np.pi * x)

n_train = 25
sigma = 0.3
n_datasets = 200
x_test = np.linspace(0, 1, 100)
degree = 12  # 高次多項式

lambdas = np.logspace(-8, 2, 30)

bias_sq_list = []
variance_list = []
mse_list = []

for lam in lambdas:
    predictions = np.zeros((n_datasets, len(x_test)))

    for i in range(n_datasets):
        x_train = np.random.uniform(0, 1, n_train)
        y_train = f_true(x_train) + np.random.normal(0, sigma, n_train)

        # 特徴行列の構築
        X_train = np.vander(x_train, degree + 1, increasing=True)
        X_test = np.vander(x_test, degree + 1, increasing=True)

        # Ridge回帰
        I = np.eye(degree + 1)
        I[0, 0] = 0  # 切片は正則化しない
        w = np.linalg.solve(X_train.T @ X_train + lam * I, X_train.T @ y_train)
        predictions[i] = X_test @ w

    f_bar = predictions.mean(axis=0)
    bias_sq = np.mean((f_true(x_test) - f_bar) ** 2)
    variance = np.mean(np.var(predictions, axis=0))

    bias_sq_list.append(bias_sq)
    variance_list.append(variance)
    mse_list.append(bias_sq + variance + sigma ** 2)

fig, ax = plt.subplots(figsize=(10, 6))

ax.plot(lambdas, bias_sq_list, "b-", linewidth=2, label=r"Bias$^2$")
ax.plot(lambdas, variance_list, "r-", linewidth=2, label="Variance")
ax.plot(lambdas, [sigma**2]*len(lambdas), "g--", linewidth=1.5, label=r"Noise $\sigma^2$")
ax.plot(lambdas, mse_list, "k-", linewidth=2, label="Total Error (MSE)")

optimal_idx = np.argmin(mse_list)
ax.axvline(lambdas[optimal_idx], color="purple", linestyle=":", linewidth=2,
           label=rf"Optimal $\lambda$ = {lambdas[optimal_idx]:.2e}")

ax.set_xscale("log")
ax.set_xlabel(r"Regularization parameter $\lambda$", fontsize=13)
ax.set_ylabel("Error", fontsize=13)
ax.set_title("Bias-Variance Tradeoff with Ridge Regularization (degree=12)", fontsize=14)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_ylim(bottom=0)

plt.tight_layout()
plt.savefig("bias_variance_ridge.png", dpi=150, bbox_inches="tight")
plt.show()

このグラフから、正則化パラメータ $\lambda$ とバイアス-バリアンスの関係が読み取れます。

  1. $\lambda$ が小さい(左端): 正則化がほとんど効いておらず、12次多項式がデータに過剰にフィットするため、バリアンスが大きくなっています。バイアスは小さいですが、総誤差はバリアンスに支配されて高くなります

  2. $\lambda$ が最適値付近: バイアスの上昇がまだ小さく、バリアンスが十分に抑制されている「スイートスポット」があります。ここで総誤差が最小になります。紫の点線がこの最適点を示しています

  3. $\lambda$ が大きい(右端): 正則化が強すぎて全てのパラメータが0に近づき、実質的に定数関数に近くなります。バリアンスは小さいですがバイアスが大きくなり、未学習の状態に陥ります

このように、正則化は「モデルの次数を変える」のとは別の方法でバイアス-バリアンストレードオフを制御しています。次数を固定したまま $\lambda$ を調整することで、実効的な複雑さを連続的に制御できるのが正則化の利点です。

アンサンブル学習との関係

バイアス-バリアンス分解は、アンサンブル学習の成功を理論的に説明する強力な枠組みでもあります。

Bagging(バギング)はバリアンスを減らす

Bagging(Bootstrap AGGregatING)は、複数のモデルの予測を平均することでバリアンスを低減する手法です。

$M$ 個の独立なモデル $\hat{f}_1, \ldots, \hat{f}_M$ の平均 $\hat{f}_{\text{bag}} = \frac{1}{M}\sum_{m=1}^M \hat{f}_m$ を考えます。各モデルのバイアスが同じ($E[\hat{f}_m] = \bar{f}$ for all $m$)ならば

$$ \text{Bias}(\hat{f}_{\text{bag}}) = \text{Bias}(\hat{f}_m) \quad \text{(バイアスは変わらない)} $$

バリアンスについては、各モデルが独立ならば

$$ \text{Var}(\hat{f}_{\text{bag}}) = \frac{1}{M}\text{Var}(\hat{f}_m) \quad \text{(バリアンスは} \frac{1}{M} \text{に減少)} $$

実際にはブートストラップサンプルは完全には独立でないため、各モデル間の相関を $\rho$ とすると

$$ \text{Var}(\hat{f}_{\text{bag}}) = \rho \cdot \text{Var}(\hat{f}_m) + \frac{1 – \rho}{M}\text{Var}(\hat{f}_m) $$

$M \to \infty$ のとき $\rho \cdot \text{Var}(\hat{f}_m)$ に収束するため、相関 $\rho$ が小さいほどバリアンス低減の効果が大きくなります。ランダムフォレストが各ノードで特徴量をランダムに選ぶのは、この $\rho$ を小さくするためです。

Boosting(ブースティング)はバイアスを減らす

一方、Boosting(勾配ブースティングなど)は、前のモデルの残差に新しいモデルをフィットすることでバイアスを低減する手法です。

弱学習器(単純なモデル)は個々にはバイアスが大きいですが、残差に順次フィットしていくことで、アンサンブル全体としてのバイアスが段階的に減少します。ただし、イテレーション数を増やしすぎるとバリアンスが増加し、過学習に陥ります。

このように、BaggingとBoostingはバイアス-バリアンストレードオフの異なる側面にアプローチするアンサンブル手法であり、問題の性質に応じて使い分けることが重要です。

実務的な指針

バイアス-バリアンストレードオフの理論は、実務において以下のような指針を与えてくれます。

診断方法

症状 原因 対処法
訓練誤差: 高、テスト誤差: 高 バイアスが高い(未学習) モデルを複雑にする、特徴量を追加する
訓練誤差: 低、テスト誤差: 高 バリアンスが高い(過学習) 正則化を強める、データを増やす、モデルを単純化する
訓練誤差: 低、テスト誤差: 低 適切なバランス そのままでOK

学習曲線(Learning curve)の活用

訓練データ数を横軸、誤差を縦軸にプロットした学習曲線は、バイアスとバリアンスの問題を診断する強力なツールです。

  • バイアスが高い場合: 訓練誤差とテスト誤差が高い水準で収束し、データを増やしても改善しない
  • バリアンスが高い場合: 訓練誤差は低いがテスト誤差との間にギャップがあり、データを増やすとギャップが縮まる
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.pipeline import make_pipeline

np.random.seed(42)

# データ生成
n_samples = 300
X = np.random.uniform(0, 1, n_samples).reshape(-1, 1)
y = np.sin(2 * np.pi * X.ravel()) + np.random.normal(0, 0.3, n_samples)

fig, axes = plt.subplots(1, 3, figsize=(16, 5))
models = [
    ("High Bias (degree=1)", make_pipeline(PolynomialFeatures(1), LinearRegression())),
    ("Balanced (degree=4)", make_pipeline(PolynomialFeatures(4), LinearRegression())),
    ("High Variance (degree=15)", make_pipeline(PolynomialFeatures(15), LinearRegression())),
]

for ax, (title, model) in zip(axes, models):
    train_sizes, train_scores, test_scores = learning_curve(
        model, X, y, train_sizes=np.linspace(0.1, 1.0, 10),
        cv=5, scoring="neg_mean_squared_error", n_jobs=-1
    )

    train_mse = -train_scores.mean(axis=1)
    test_mse = -test_scores.mean(axis=1)
    train_std = train_scores.std(axis=1)
    test_std = test_scores.std(axis=1)

    ax.plot(train_sizes, train_mse, "b-o", linewidth=2, markersize=5, label="Training error")
    ax.plot(train_sizes, test_mse, "r-s", linewidth=2, markersize=5, label="Validation error")
    ax.fill_between(train_sizes, train_mse - train_std, train_mse + train_std,
                    color="blue", alpha=0.1)
    ax.fill_between(train_sizes, test_mse - test_std, test_mse + test_std,
                    color="red", alpha=0.1)

    ax.set_xlabel("Training Set Size", fontsize=12)
    ax.set_ylabel("MSE", fontsize=12)
    ax.set_title(title, fontsize=13)
    ax.legend(fontsize=9)
    ax.grid(True, alpha=0.3)
    ax.set_ylim(0, 1.5)

plt.tight_layout()
plt.savefig("learning_curves.png", dpi=150, bbox_inches="tight")
plt.show()

学習曲線から、各モデルのバイアス-バリアンスの状態を診断できます。

  1. 左図(高バイアス, d=1): 訓練誤差と検証誤差が両方とも高い水準で収束し、データ数を増やしても改善が見られません。2つの曲線の間にほとんどギャップがないのは、モデルが単純すぎて訓練データにすらフィットできていないためです。この場合、データを増やすよりも、モデルの複雑さを上げる必要があります

  2. 中央図(バランス, d=4): 訓練誤差は低い水準にあり、検証誤差もデータ数とともに訓練誤差に近づいていきます。ギャップは小さく、これがバイアスとバリアンスのバランスが取れた理想的な学習曲線です

  3. 右図(高バリアンス, d=15): データ数が少ないときに訓練誤差と検証誤差の間に大きなギャップがあります。訓練誤差は非常に低い(過学習)のに検証誤差が高いという特徴的なパターンです。ただしデータ数が増えるにつれてギャップが縮まっていくため、この場合はデータの追加が有効な対策になり得ます

まとめ

本記事では、バイアス-バリアンストレードオフの理論と実践的な活用法について解説しました。

  • 汎化誤差はバイアスの二乗 + バリアンス + 既約誤差(ノイズ)に分解される
  • バイアスはモデルの仮定の誤りに起因し、モデルを複雑にすると減少する
  • バリアンスは訓練データへの感度を表し、モデルを複雑にすると増加する
  • 最適なモデルの複雑さは、バイアスとバリアンスの和が最小になる点にある
  • 正則化はモデルの実効的な複雑さを制御し、バリアンスを低減する
  • Baggingはバリアンスを、Boostingはバイアスを主に低減するアンサンブル手法である
  • 学習曲線はバイアスとバリアンスの問題を実務的に診断するための強力なツールである

バイアス-バリアンストレードオフの理解は、機械学習における全てのモデル選択・ハイパーパラメータチューニングの理論的基盤です。次のステップとして、以下の記事も参考にしてください。