0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

機械学習では、学習データにぴったり合うモデルほど良いとは限りません。

学習データ上の誤差がとても小さいのに、新しいデータでは予測を外してしまうことがあります。これが過学習(overfitting)です。

本記事では、滑らかな規則にランダムなノイズを加えたダミーデータを作り、次数の異なる多項式回帰を比較します。

  • 次数1:単純すぎて曲線を表せない
  • 次数5:この実験では、全体の形を比較的うまく捉える
  • 次数12:学習データに合わせようとして不自然に曲がる

Google Colabでコードを上から順に実行すると、次の3枚の図を作成できます。

  1. 真の規則と学習データ
  2. 次数1・5・12の予測曲線
  3. 多項式の次数と学習・評価誤差の関係

本記事は、過学習の仕組みを直感的に理解するための最小実験です。実際の機械学習では、データ分割、交差検証、正則化などを組み合わせてモデルを評価します。

TL;DR

  • 適合とは、モデルをデータに合わせること
  • 未学習では、モデルが単純すぎて規則を捉えられない
  • 過学習では、モデルが学習データ中の偶然のばらつきまで拾ってしまう
  • モデルを複雑にすると、一般に学習誤差は下げやすくなる
  • しかし、新しいデータに対する誤差は途中から大きくなることがある
  • 大切なのは、学習データへの当てはまりではなく、未知データへの汎化性能である

この記事で扱う用語

用語 本記事での意味
回帰 数値を予測する問題
モデル 入力 $x$ から出力 $\hat{y}$ を計算する規則
学習 モデルの係数をデータに合わせて決めること
学習データ モデルの係数を決めるために使うデータ
評価データ 学習に使わず、未知データへの予測性能を調べるために使うデータ
ノイズ 観測値に含まれる、予測しきれないランダムなばらつき
汎化 学習に使っていない新しいデータでも、よく予測できること
未学習 モデルが単純すぎて、データの規則を十分に表せない状態
過学習 学習データの細かなばらつきまで拾い、新しいデータへの予測が悪くなる状態

回帰とは

回帰は、入力から連続的な数値を予測する問題です。

たとえば、次のような問題が回帰に当たります。

  • 温度から反応速度を予測する
  • 時間から物体の位置を予測する
  • 濃度から測定値を予測する
  • 複数の特徴量から売上を予測する

本記事では、1つの入力 $x$ から数値 $y$ を予測します。

多項式回帰とは

もっとも単純な直線モデルは、次の形です。

$$
\hat{y}=w_0+w_1x
$$

ここで、$w_0$ は切片、$w_1$ は傾きです。

多項式回帰では、$x^2$、$x^3$ などの項を加えます。

$$
\hat{y}=
w_0+w_1x+w_2x^2+\cdots+w_dx^d
$$

$d$ を多項式の次数と呼びます。

  • $d=1$:直線
  • $d=2$:放物線
  • $d$ が大きい:より複雑に曲がれる

次数が高いほど、モデルが表現できる曲線の種類は増えます。ただし、曲がれること正しく予測できることは同じではありません。

平均二乗誤差 MSE とは

本記事では、予測の誤差を平均二乗誤差(Mean Squared Error: MSE)で評価します。

$$
\mathrm{MSE}=
\frac{1}{n}
\sum_{i=1}^{n}
\left(y_i-\hat{y}_i\right)^2
$$

ここで、

  • $y_i$:実際の値
  • $\hat{y}_i$:モデルの予測値
  • $n$:データ数

です。

予測と実測の差を二乗して平均するため、MSEは小さいほど良い値です。

ただし、学習データ上のMSEだけを小さくすればよいわけではありません。新しいデータに対するMSEも確認する必要があります。

今回の実験

ダミーデータを作るため、次の滑らかな関数を「真の規則」とします。

$$
f(x)=\sin(\pi x)+0.3x
$$

観測値にはランダムなノイズを加えます。

$$
y=f(x)+\varepsilon
$$

$$
\varepsilon\sim\mathcal{N}(0,\sigma^2)
$$

今回は $\sigma=0.18$ とします。

モデルに与えるのはノイズを含む観測値だけです。真の関数 $f(x)$ は、モデルには知らせません。

データ数は次のようにします。

  • 学習データ:20点
  • 評価データ:2000点
  • 入力範囲:$-1\le x\le 1$

学習データと評価データは同じ入力範囲から作ります。そのため、今回見ているのは外挿の失敗ではなく、同じ範囲内での過学習です。

Google Colabで実行する

1. ライブラリを読み込み、ダミーデータを作る

次のセルを実行します。

import os

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error


# ------------------------------
# 実験条件
# ------------------------------
SEED = 1
N_TRAIN = 20
N_TEST = 2000
NOISE_STD = 0.18
FIG_DIR = "figures"

rng = np.random.default_rng(SEED)
os.makedirs(FIG_DIR, exist_ok=True)


# ------------------------------
# ダミーデータを作る真の関数
# ------------------------------
def true_function(x):
    return np.sin(np.pi * x) + 0.3 * x


# xの範囲全体を覆うように両端を含め、
# 残りの学習点をランダムに作る
x_inner = rng.uniform(-0.95, 0.95, N_TRAIN - 2)
x_train = np.sort(
    np.concatenate(([-1.0], x_inner, [1.0]))
)
X_train = x_train.reshape(-1, 1)

# 真の関数に正規分布ノイズを加える
y_train = true_function(x_train) + rng.normal(
    loc=0.0,
    scale=NOISE_STD,
    size=N_TRAIN,
)

# 学習には使わない独立な評価データ
x_test = rng.uniform(-1.0, 1.0, N_TEST)
X_test = x_test.reshape(-1, 1)

y_test = true_function(x_test) + rng.normal(
    loc=0.0,
    scale=NOISE_STD,
    size=N_TEST,
)

# 滑らかな曲線を描くための細かい格子
x_plot = np.linspace(-1.0, 1.0, 1000)
X_plot = x_plot.reshape(-1, 1)
y_true_plot = true_function(x_plot)

2. 真の関数と学習データを描く

fig, ax = plt.subplots(figsize=(8, 5))

ax.plot(
    x_plot,
    y_true_plot,
    linewidth=2,
    label="true function",
)

ax.scatter(
    x_train,
    y_train,
    s=55,
    zorder=3,
    label="training data",
)

ax.set(
    title="Dummy data: smooth rule plus random noise",
    xlabel="x",
    ylabel="y",
)

ax.grid(alpha=0.3)
ax.legend()

fig.tight_layout()
fig.savefig(
    f"{FIG_DIR}/fig1_dummy_data.png",
    dpi=160,
    bbox_inches="tight",
)

plt.show()
plt.close(fig)

fig1_dummy_data.png

図1. 滑らかな真の関数と、ノイズを含む20点の学習データ。

青い曲線が真の関数、点がモデルに与える学習データです。

ダミーデータなので真の関数を表示できますが、現実のデータ解析では真の規則は通常わかりません。モデルが見られるのは、ノイズを含んだ観測点だけです。

3. 次数1・5・12の多項式回帰を作る

次に、多項式回帰モデルを作ります。

def make_polynomial_model(degree):
    return make_pipeline(
        PolynomialFeatures(
            degree=degree,
            include_bias=False,
        ),
        StandardScaler(),
        LinearRegression(),
    )

このパイプラインでは、次の3つを順番に行っています。

  1. PolynomialFeatures
    $x$ から $x^2$、$x^3$、$\ldots$ を作る

  2. StandardScaler
    各特徴量のスケールをそろえ、数値計算を安定させる

  3. LinearRegression
    学習データの二乗誤差が小さくなるように係数を決める

include_bias=False としているのは、LinearRegression が切片 $w_0$ を別に推定するためです。

次数1、5、12を学習させ、MSEを計算します。

selected_degrees = [1, 5, 12]

models = {}
metrics = {}

for degree in selected_degrees:
    model = make_polynomial_model(degree)
    model.fit(X_train, y_train)

    models[degree] = model

    metrics[degree] = {
        "train_MSE": mean_squared_error(
            y_train,
            model.predict(X_train),
        ),
        "test_MSE": mean_squared_error(
            y_test,
            model.predict(X_test),
        ),
    }


selected_result = pd.DataFrame(
    [
        {
            "degree": degree,
            "train_MSE": metrics[degree]["train_MSE"],
            "test_MSE": metrics[degree]["test_MSE"],
        }
        for degree in selected_degrees
    ]
)

print(
    selected_result
    .round(4)
    .to_string(index=False)
)

実行結果は次のようになります。

 degree  train_MSE  test_MSE
      1     0.3011    0.2415
      5     0.0362    0.0420
     12     0.0203    0.2696

4. 3つのモデルを並べて比較する

fig, axes = plt.subplots(
    1,
    3,
    figsize=(15, 4.5),
    sharex=True,
    sharey=True,
)

panel_titles = {
    1: "Degree 1: underfitting",
    5: "Degree 5: balanced",
    12: "Degree 12: overfitting",
}


for ax, degree in zip(axes, selected_degrees):
    model = models[degree]

    train_mse = metrics[degree]["train_MSE"]
    test_mse = metrics[degree]["test_MSE"]

    ax.plot(
        x_plot,
        y_true_plot,
        linewidth=2,
        label="true function",
    )

    ax.plot(
        x_plot,
        model.predict(X_plot),
        linewidth=2,
        label="model prediction",
    )

    ax.scatter(
        x_train,
        y_train,
        s=38,
        zorder=3,
        label="training data",
    )

    ax.set_title(
        f"{panel_titles[degree]}\n"
        f"train MSE = {train_mse:.3f}, "
        f"test MSE = {test_mse:.3f}"
    )

    ax.set_xlabel("x")
    ax.set_ylim(-1.7, 1.7)
    ax.grid(alpha=0.3)


axes[0].set_ylabel("y")
axes[0].legend(fontsize=9)

fig.tight_layout()
fig.savefig(
    f"{FIG_DIR}/fig2_model_comparison.png",
    dpi=160,
    bbox_inches="tight",
)

plt.show()
plt.close(fig)

fig2_model_comparison.png

図2. 次数1・5・12の多項式回帰。青線は真の関数、橙線はモデルの予測、点は学習データ。

結果を読む

次数1:未学習

次数1のモデルは直線です。

真の関数は滑らかに曲がっていますが、直線はその形を表現できません。そのため、

  • 学習MSE:0.301
  • 評価MSE:0.241

と、どちらの誤差も大きくなりました。

このように、モデルが単純すぎてデータの主要な規則を捉えられない状態を未学習(underfitting)と呼びます。

次数5:この実験では比較的よいバランス

次数5では、予測曲線が真の関数の大まかな形に沿っています。

  • 学習MSE:0.036
  • 評価MSE:0.042

学習データにある細かなズレをすべて追いかけるのではなく、全体の滑らかな傾向を捉えています。

なお、次数5が常に最良という意味ではありません。このダミーデータでは次数3〜8付近の評価MSEが低く、次数5を代表例として表示しています。

次数12:学習誤差は小さいが、評価誤差は大きい

次数12では、予測曲線が学習点の間で何度も上下しています。

  • 学習MSE:0.020
  • 評価MSE:0.270

次数5よりも学習MSEは小さくなっています。しかし、評価MSEは約6.4倍に増えています。

つまり、学習データにはよく合っていますが、新しいデータの予測には失敗しています。これが過学習です。

学習誤差が最小のモデルが、未知データへの予測に最適とは限りません。

次数を1から12まで変えてみる

モデルの複雑さと誤差の関係を、もう少し細かく見ます。

degrees = np.arange(1, 13)

train_mse_list = []
test_mse_list = []


for degree in degrees:
    model = make_polynomial_model(int(degree))
    model.fit(X_train, y_train)

    train_mse_list.append(
        mean_squared_error(
            y_train,
            model.predict(X_train),
        )
    )

    test_mse_list.append(
        mean_squared_error(
            y_test,
            model.predict(X_test),
        )
    )


error_table = pd.DataFrame(
    {
        "degree": degrees,
        "train_MSE": train_mse_list,
        "test_MSE": test_mse_list,
    }
)


fig, ax = plt.subplots(figsize=(8, 5))

ax.plot(
    error_table["degree"],
    error_table["train_MSE"],
    marker="o",
    label="training MSE",
)

ax.plot(
    error_table["degree"],
    error_table["test_MSE"],
    marker="o",
    label="test MSE",
)

# 値の範囲が広いため、縦軸を対数表示にする
ax.set_yscale("log")

ax.set(
    title="Model complexity and prediction error",
    xlabel="polynomial degree",
    ylabel="mean squared error (log scale)",
    xticks=degrees,
)

ax.grid(alpha=0.3)
ax.legend()

fig.tight_layout()
fig.savefig(
    f"{FIG_DIR}/fig3_degree_vs_error.png",
    dpi=160,
    bbox_inches="tight",
)

plt.show()
plt.close(fig)


print(
    error_table
    .round(4)
    .to_string(index=False)
)

fig3_degree_vs_error.png

図3. 多項式の次数と学習MSE・評価MSEの関係。縦軸は対数表示。

図3では、次の違いが見えます。

  • 学習MSEは、次数を上げるほど小さくなる
  • 評価MSEは、次数3〜8付近で小さい
  • 次数をさらに上げると、評価MSEが大きくなる

評価MSEは次数に対して完全に滑らかには変化せず、多少の凸凹があります。有限個のデータとランダムノイズを使っているためです。

過学習の判断では、「次数を1つ上げたら必ず評価誤差が増える」と考えるのではなく、学習誤差と評価誤差の差や、全体の傾向を見ます。

なぜ学習MSEは下がり続けるのか

次数$d+1$の多項式は、次数$d$のすべての項に、新しい項を1つ加えたものです。

たとえば、

$$
w_0+w_1x+w_2x^2
$$

という2次式に、$w_3x^3$を加えると3次式になります。

新しく加えた係数 $w_3$ を0にすれば、3次モデルは2次モデルと同じ予測を再現できます。そのため、正則化を使わない通常の最小二乗法では、数値誤差を除けば、

\mathrm{MSE}_{\mathrm{train}}(d+1)\le\mathrm{MSE}_{\mathrm{train}}(d)

となります。

複雑なモデルは、単純なモデルができることを含んだうえで、さらに細かく曲がれるからです。

しかし、学習MSEが下がることは、未知データへの予測が改善したことを意味しません。

過学習はなぜ起きるのか

今回の観測データは、

$$
\text{観測値}=
\text{真の規則}
+
\text{ランダムノイズ}
$$

で作られています。

モデルは、観測されたズレが、

  • 真の規則によるものなのか
  • そのデータだけに偶然入ったノイズなのか

を直接知ることができません。

次数の低いモデルは曲がり方が限られているため、細かなノイズまで追いかけられません。

一方、次数の高いモデルは自由に曲がれるため、学習データの偶然の上下まで説明しようとします。

しかし、評価データには別のランダムノイズが入っています。学習データで覚えた細かな曲がり方は、評価データでは再現されません。

その結果、

  • 学習データ:よく合う
  • 新しいデータ:外れる

という状態になります。

これが過学習の基本的な仕組みです。

評価MSEが0にならないのはなぜか

今回のノイズの標準偏差は $\sigma=0.18$ です。

ノイズ分散は、

$$
\sigma^2=0.18^2=0.0324
$$

です。

真の関数を完全に知っていたとしても、新しく加わるランダムノイズそのものを事前に当てることはできません。そのため、ノイズを含む評価値に対するMSEには、理想的なモデルでもおおむねノイズ分散に相当する下限があります。

次数5の評価MSEは約0.042であり、ノイズ分散0.0324に比較的近い値です。

これは、誤差を完全に0へ近づけることが常に正しい目標ではないことも示しています。

「曲線が複雑なら過学習」ではない

ここは誤解しやすい点です。

複雑な形をした予測曲線だから、必ず過学習であるとは限りません。

本当に複雑な規則が存在し、十分なデータがあるなら、複雑なモデルが必要な場合もあります。

反対に、一見滑らかなモデルでも、データの分け方や特徴量の作り方によっては過学習していることがあります。

過学習かどうかは見た目だけでなく、

学習に使っていないデータでも予測性能を保てるか

で判断します。

また、高次数モデルが常に悪いわけでもありません。過学習の起こりやすさは、次の要因に依存します。

  • データ数
  • ノイズの大きさ
  • 真の規則の複雑さ
  • モデルの複雑さ
  • 正則化の有無
  • 評価方法

実務では学習・検証・テストを分ける

本記事では説明を簡潔にするため、学習データと評価データの2つを使いました。

実務では、次の3つに分けることが一般的です。

データ 役割
学習データ モデルの係数を学習する
検証データ 次数やハイパーパラメータを選ぶ
テストデータ 最終的なモデルの性能を1回だけ確認する

評価データを見ながら何度も次数を選ぶと、その評価データにも間接的に適合してしまいます。

そのため、本格的な解析では、

  • 検証データを用意する
  • 交差検証を使う
  • 最終テストデータは最後まで触らない

といった工夫が必要です。

本記事の test_MSE は、過学習を可視化するための独立評価値として使っています。モデル選択まで厳密に行うなら、検証データを別に設けます。

過学習を抑える代表的な方法

1. モデルを単純にする

多項式の次数を下げる、木の深さを制限する、使用する特徴量を減らす、といった方法です。

2. 交差検証を使う

データの分け方を変えながら複数回評価し、特定の分割だけに依存しない性能を確認します。

3. 正則化を使う

係数が極端に大きくなることにペナルティを加えます。

多項式回帰では、Ridge回帰などを組み合わせると、予測曲線の過度な振動を抑えられる場合があります。

4. データを増やす

観測点が増えると、偶然のノイズと再現性のある傾向を区別しやすくなります。

5. 問題に関する知識を使う

現象として不自然な曲がり方や、あり得ない特徴量の関係を避けるため、分野知識をモデル設計に反映します。

追加実験

コード冒頭の値を変えると、過学習への理解が深まります。

実験1:乱数を変える

SEED = 10

高次数モデルの曲がり方が大きく変わることがあります。

少しデータが変わっただけで予測が大きく変わる性質は、複雑なモデルの不安定さを示しています。

実験2:学習データを増やす

N_TRAIN = 50

データ数を増やすと、高次数モデルでも真の傾向を捉えやすくなる場合があります。

実験3:ノイズを変える

NOISE_STD = 0.05

または、

NOISE_STD = 0.30

ノイズが大きいほど、モデルが偶然のばらつきを拾う危険が高まります。

実験4:さらに高い次数を試す

selected_degrees = [1, 5, 15]

高次数では、データ点の間や端付近で予測が大きく振動することがあります。

ただし、高次数の多項式は数値的に不安定になる場合もあるため、結果の解釈には注意が必要です。

まとめ

  • 多項式の次数を上げると、モデルはより複雑に曲がれる
  • 単純すぎるモデルは、主要な規則を捉えられず未学習になる
  • 複雑すぎるモデルは、学習データのノイズまで拾って過学習する
  • 学習誤差は、モデルを複雑にするほど下げやすい
  • しかし、未知データに対する誤差は途中から増えることがある
  • 過学習は、予測曲線の見た目だけでなく、独立データへの汎化性能で判断する
  • 実務では、検証データ、交差検証、正則化などを使ってモデルの複雑さを調整する

機械学習の目的は、手元のデータを完全に暗記することではありません。

まだ見ていないデータにも通用する規則を学ぶこと

が重要です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?