機械学習や統計モデリングを学んでいると、よく 正則化 という言葉が出てきます。
「過学習を防ぐらしい」
「Ridge回帰では係数を小さくするらしい」
「でも、係数を小さくするとなぜ嬉しいのか?」
このあたりは、言葉だけで説明されると少し分かりにくいところです。
この記事では、ダミーデータを作成し、通常の線形回帰とRidge回帰を比較します。
Google Colabで実行できるPythonコードを使いながら、正則化が何をしているのかを図で見ていきます。
この記事のゴール
この記事のゴールは、次の3つです。
- 正則化 が何をしているのかを直感的に理解する
- Ridge回帰 が係数を小さくする意味を理解する
- 正則化が弱すぎる場合・強すぎる場合に何が起きるかを見る
まず結論
Ridge回帰の正則化は、ざっくり言うと、
データに合わせるだけでなく、係数が大きくなりすぎないようにする仕組み
です。
通常の回帰では、モデルは「訓練データにできるだけ合う」ことを目指します。
一方、Ridge回帰では、
訓練データに合うこと
係数を大きくしすぎないこと
の両方を同時に考えます。
その結果、訓練データへの当てはまりは少し悪くなることがあります。
しかし、未知のデータに対しては安定した予測になりやすくなります。
用語の整理
最初に、この記事で使う用語を軽く整理します。
回帰
回帰とは、入力 $x$ から連続値 $y$ を予測する問題です。
たとえば、
- 気温からアイスの売上を予測する
- 実験条件から測定値を予測する
- 時間から信号の値を予測する
といった問題が回帰です。
特徴量
モデルに入力する値を 特徴量 と呼びます。
たとえば、入力が1つだけなら、
$$
x
$$
です。
ただし、この記事では少し複雑な曲線を表すために、
$$
x, x^2, x^3, \cdots, x^{12}
$$
のような多項式特徴量を使います。
係数
線形回帰モデルは、特徴量に重みをつけて足し合わせます。
たとえば、次のような形です。
$$
\hat{y}=
w_0
+
w_1x
+
w_2x^2
+
w_3x^3
+
\cdots
+
w_dx^d
$$
ここで、$w_1, w_2, \cdots, w_d$ が 係数 です。
係数は、各特徴量をどれくらい強く使うかを表します。
損失関数
モデルの予測値 $\hat{y}_i$ と、実際の値 $y_i$ のズレを評価する関数を 損失関数 と呼びます。
通常の最小二乗法では、次の量を小さくします。
$$
\sum_i (y_i - \hat{y}_i)^2
$$
これは、予測のズレを二乗して足し合わせたものです。
過学習
過学習とは、訓練データにはよく合っているのに、未知のデータにはうまく対応できない状態です。
訓練データにはノイズが含まれます。
モデルがそのノイズまで一生懸命覚えてしまうと、見かけ上は訓練データにぴったり合います。
しかし、それは本質的な関係を学んだのではなく、たまたまの揺らぎに合わせすぎているだけかもしれません。
Ridge回帰の数式
通常の線形回帰では、次の損失を小さくします。
$$
\mathrm{Loss}_{\mathrm{OLS}}=
\sum_i (y_i - \hat{y}_i)^2
$$
Ridge回帰では、ここに係数の大きさに対するペナルティを加えます。
$$
\mathrm{Loss}_{\mathrm{Ridge}}=
\sum_i (y_i - \hat{y}_i)^2
+
\lambda \sum_j w_j^2
$$
右側の第1項は、データへの当てはまりです。
$$
\sum_i (y_i - \hat{y}_i)^2
$$
第2項は、係数が大きくなりすぎることへの罰則です。
$$
\lambda \sum_j w_j^2
$$
$\lambda$ は正則化の強さを表すパラメータです。
scikit-learnの Ridge では、この値を alpha と呼びます。
Ridge回帰の直感
Ridge回帰は、モデルに対して次のように言っているようなものです。
データにはなるべく合ってほしい。
でも、係数をむやみに大きくしてまで合わせにいくのはやめてほしい。
係数が大きいモデルは、入力 $x$ の少しの変化に敏感になりやすいです。
特に、多項式回帰のように $x, x^2, x^3, \cdots$ を使う場合、係数が大きくなると曲線が不自然に揺れやすくなります。
そこでRidge回帰では、係数を小さめに保つことで、モデルを少し「おとなしく」します。
Google Colabで実行するコード
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.metrics import mean_squared_error
# ============================================
# 0. Settings
# ============================================
SEED = 28
rng = np.random.default_rng(SEED)
os.makedirs("fig", exist_ok=True)
# ============================================
# 1. Generate dummy data
# ============================================
def true_function(x):
"""
This is the hidden true curve used only for simulation.
実際の解析では真の関数は分からない、という設定です。
"""
return np.sin(2 * np.pi * x) + 0.3 * np.cos(6 * np.pi * x)
n_train = 20
n_test = 200
noise_sigma = 0.30
x_train = np.sort(rng.uniform(0, 1, n_train))
y_train = true_function(x_train) + rng.normal(0, noise_sigma, n_train)
x_test = np.sort(rng.uniform(0, 1, n_test))
y_test = true_function(x_test) + rng.normal(0, noise_sigma, n_test)
x_grid = np.linspace(0, 1, 500)
y_true_grid = true_function(x_grid)
# scikit-learnでは、入力Xは2次元配列にする
X_train = x_train.reshape(-1, 1)
X_test = x_test.reshape(-1, 1)
X_grid = x_grid.reshape(-1, 1)
# ============================================
# 2. Build models
# ============================================
degree = 12
models = {
"No regularization": make_pipeline(
PolynomialFeatures(degree=degree, include_bias=False),
StandardScaler(),
LinearRegression()
),
"Ridge alpha=0.001": make_pipeline(
PolynomialFeatures(degree=degree, include_bias=False),
StandardScaler(),
Ridge(alpha=0.001)
),
"Ridge alpha=0.1": make_pipeline(
PolynomialFeatures(degree=degree, include_bias=False),
StandardScaler(),
Ridge(alpha=0.1)
),
"Ridge alpha=10": make_pipeline(
PolynomialFeatures(degree=degree, include_bias=False),
StandardScaler(),
Ridge(alpha=10.0)
),
"Ridge alpha=100": make_pipeline(
PolynomialFeatures(degree=degree, include_bias=False),
StandardScaler(),
Ridge(alpha=100.0)
)
}
# ============================================
# 3. Fit models and summarize
# ============================================
rows = []
for name, model in models.items():
model.fit(X_train, y_train)
y_pred_train = model.predict(X_train)
y_pred_test = model.predict(X_test)
regressor = model.steps[-1][1]
coef_l2_norm = np.sqrt(np.sum(regressor.coef_ ** 2))
rows.append({
"model": name,
"train_MSE": mean_squared_error(y_train, y_pred_train),
"test_MSE": mean_squared_error(y_test, y_pred_test),
"coefficient_L2_norm": coef_l2_norm
})
summary = pd.DataFrame(rows)
display(summary)
# ============================================
# 4. Figure 1: Dummy data
# ============================================
plt.figure(figsize=(8, 5))
plt.plot(x_grid, y_true_grid, label="true function")
plt.scatter(x_train, y_train, s=45, label="training data")
plt.title("Dummy data")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.tight_layout()
plt.savefig("fig/fig1_dummy_data.png", dpi=160)
plt.show()
# ============================================
# 5. Figure 2: Prediction curves
# ============================================
plt.figure(figsize=(8, 5))
plt.plot(x_grid, y_true_grid, label="true function")
plt.scatter(x_train, y_train, s=45, label="training data")
for name, model in models.items():
y_pred_grid = model.predict(X_grid)
plt.plot(x_grid, y_pred_grid, label=name)
# 見やすくするために表示範囲を制限
plt.ylim(-2.5, 2.5)
plt.title("Prediction curves")
plt.xlabel("x")
plt.ylabel("y")
plt.legend(fontsize=8)
plt.tight_layout()
plt.savefig("fig/fig2_prediction_curves.png", dpi=160)
plt.show()
# ============================================
# 6. Figure 3: Train/Test MSE
# ============================================
summary_plot = summary.set_index("model")[["train_MSE", "test_MSE"]]
plt.figure(figsize=(9, 5))
summary_plot.plot(kind="bar", ax=plt.gca())
plt.yscale("log")
plt.title("Train/Test MSE")
plt.ylabel("MSE, log scale")
plt.xticks(rotation=30, ha="right")
plt.tight_layout()
plt.savefig("fig/fig3_train_test_mse.png", dpi=160)
plt.show()
# ============================================
# 7. Figure 4: Coefficients for selected models
# ============================================
coef_df = pd.DataFrame({"degree": np.arange(1, degree + 1)})
for name, model in models.items():
coef_df[name] = model.steps[-1][1].coef_
display(coef_df)
plt.figure(figsize=(9, 5))
for name in models.keys():
plt.plot(
coef_df["degree"],
np.abs(coef_df[name]),
marker="o",
label=name
)
plt.yscale("log")
plt.title("Absolute coefficient values")
plt.xlabel("polynomial degree")
plt.ylabel("|coefficient|, log scale")
plt.legend(fontsize=8)
plt.tight_layout()
plt.savefig("fig/fig4_coefficients.png", dpi=160)
plt.show()
# ============================================
# 8. Figure 5 and 6: Alpha sweep
# ============================================
alphas = np.logspace(-6, 3, 80)
alpha_rows = []
for alpha in alphas:
model = make_pipeline(
PolynomialFeatures(degree=degree, include_bias=False),
StandardScaler(),
Ridge(alpha=alpha)
)
model.fit(X_train, y_train)
train_mse = mean_squared_error(y_train, model.predict(X_train))
test_mse = mean_squared_error(y_test, model.predict(X_test))
coef_l2_norm = np.sqrt(np.sum(model.steps[-1][1].coef_ ** 2))
alpha_rows.append({
"alpha": alpha,
"train_MSE": train_mse,
"test_MSE": test_mse,
"coefficient_L2_norm": coef_l2_norm
})
alpha_df = pd.DataFrame(alpha_rows)
plt.figure(figsize=(8, 5))
plt.plot(
alpha_df["alpha"],
alpha_df["coefficient_L2_norm"],
marker="o",
markersize=3
)
plt.xscale("log")
plt.yscale("log")
plt.title("Alpha vs coefficient L2 norm")
plt.xlabel("alpha")
plt.ylabel("coefficient L2 norm, log scale")
plt.tight_layout()
plt.savefig("fig/fig5_alpha_vs_coef_norm.png", dpi=160)
plt.show()
plt.figure(figsize=(8, 5))
plt.plot(alpha_df["alpha"], alpha_df["train_MSE"], label="train MSE")
plt.plot(alpha_df["alpha"], alpha_df["test_MSE"], label="test MSE")
plt.xscale("log")
plt.yscale("log")
plt.title("Alpha vs train/test MSE")
plt.xlabel("alpha")
plt.ylabel("MSE, log scale")
plt.legend()
plt.tight_layout()
plt.savefig("fig/fig6_alpha_vs_mse.png", dpi=160)
plt.show()
best_idx = alpha_df["test_MSE"].idxmin()
print("Best alpha in this simulation:")
print(alpha_df.loc[best_idx])
図1:今回使うダミーデータ
今回は、真の関数として次のような曲線を用意しました。
$$
y=
\sin(2\pi x)
+
0.3\cos(6\pi x)
+
\epsilon
$$
ここで、$\epsilon$ はノイズです。
実際のデータ解析では、もちろん真の関数は分かりません。
今回は教育用のシミュレーションなので、「本当はこういう関係がある」という曲線を裏で作っておき、そこからノイズ入りの観測データを作っています。
この設定により、
- 真の関係
- ノイズを含んだ観測データ
- モデルの予測曲線
を比較できます。
図2:正則化なしとRidge回帰の予測曲線
図2では、次のモデルを比較しています。
- 正則化なし
- Ridge回帰、
alpha=0.001 - Ridge回帰、
alpha=0.1 - Ridge回帰、
alpha=10 - Ridge回帰、
alpha=100
正則化なしのモデルは、訓練データにかなり合わせようとします。
その結果、曲線が不自然に揺れやすくなります。
一方、Ridge回帰では、係数が大きくなりすぎないようにペナルティをかけます。
そのため、曲線が少しなめらかになります。
ただし、alpha を大きくしすぎると、今度は曲線が単純になりすぎます。
この状態は アンダーフィット と呼ばれます。
つまり、正則化にはバランスがあります。
図3:訓練データとテストデータでのMSE
今回の実行では、おおよそ次のような結果になりました。
| model | train_MSE | test_MSE | coefficient_L2_norm |
|---|---|---|---|
| No regularization | 0.046 | 0.457 | $1.99 \times 10^7$ |
| Ridge alpha=0.001 | 0.063 | 0.168 | 5.88 |
| Ridge alpha=0.1 | 0.101 | 0.165 | 2.43 |
| Ridge alpha=10 | 0.267 | 0.217 | 0.443 |
| Ridge alpha=100 | 0.507 | 0.430 | 0.127 |
正則化なしのモデルは、訓練データに対する誤差 train_MSE が小さいです。
しかし、テストデータに対する誤差 test_MSE は大きくなっています。
これは、
訓練データにはよく合っているが、未知のデータには弱い
という状態です。
一方、Ridge回帰では、訓練誤差は少し大きくなります。
しかし、テスト誤差は小さくなっています。
ここが正則化の重要なポイントです。
正則化は、訓練データへの当てはまりを少し犠牲にして、未知データへの安定性を高めることがある。
図4:係数は本当に小さくなっているのか?
図4では、各モデルの係数の絶対値を比較しています。
縦軸は対数スケールです。
正則化なしのモデルでは、係数が非常に大きくなっています。
今回の例では、係数の大きさをまとめた coefficient_L2_norm が $10^7$ オーダーになりました。
一方、Ridge回帰では係数が大きく抑えられています。
これが、
Ridge回帰は係数を小さくする
ということの実際の意味です。
なぜ係数が大きいと不安定になりやすいのか?
多項式回帰では、
$$
x, x^2, x^3, \cdots, x^{12}
$$
のような特徴量を使います。
このとき、モデルは次のような形になります。
$$
\hat{y}=
w_0
+
w_1x
+
w_2x^2
+
\cdots
+
w_{12}x^{12}
$$
係数 $w_j$ が非常に大きくなると、各項が互いに大きく打ち消し合うようなモデルになることがあります。
たとえば、ある項では大きな正の値、別の項では大きな負の値を取り、全体としてはなんとか訓練データに合う、という状態です。
このようなモデルは、訓練データの範囲では一見うまく見えることがあります。
しかし、少しデータが変わると予測が大きく揺れやすくなります。
Ridge回帰は、係数の二乗和にペナルティをかけることで、このような不安定な解を避けようとします。
図5:alphaを大きくすると係数はどうなるか?
図5では、alpha を少しずつ変えながら、係数の大きさを調べています。
横軸が alpha、縦軸が係数ベクトルのL2ノルムです。
係数ベクトルのL2ノルムは、次のように計算されます。
$$
|w|_2=
\sqrt{
w_1^2
+
w_2^2
+
\cdots
+
w_d^2
}
$$
図を見ると、alpha が大きくなるほど、係数のL2ノルムが小さくなっています。
つまり、alpha は、
係数をどれくらい強く小さくするか
を決めるパラメータだと考えられます。
図6:alphaを大きくすればするほど良いのか?
では、alpha は大きければ大きいほど良いのでしょうか。
答えは いいえ です。
図6では、alpha を変えたときの訓練MSEとテストMSEを示しています。
alpha が小さいと、正則化が弱くなります。
そのため、モデルは訓練データに合わせやすくなります。
alpha が大きくなると、係数が強く抑えられます。
しかし、強く抑えすぎると、モデルが単純になりすぎます。
その結果、訓練データにもテストデータにも合わなくなります。
今回のシミュレーションでは、テストMSEが最小になった alpha はおおよそ次の値でした。
$$
\alpha \approx 0.028
$$
ただし、この値はデータの作り方やノイズの大きさによって変わります。
実際の解析では、検証データや交差検証を使って alpha を選ぶことが多いです。
Ridge回帰を一言でいうと
Ridge回帰は、
予測誤差を小さくしたいが、係数が大きくなりすぎるモデルは避けたい
という考え方の回帰モデルです。
数式で書くと、
$$
\mathrm{Loss}_{\mathrm{Ridge}}=
\sum_i (y_i - \hat{y}_i)^2
+
\lambda \sum_j w_j^2
$$
です。
第1項は、データへの当てはまりを表します。
$$
\sum_i (y_i - \hat{y}_i)^2
$$
第2項は、係数の大きさへのペナルティです。
$$
\lambda \sum_j w_j^2
$$
Ridge回帰では、この2つを同時に小さくしようとします。
標準化が重要な理由
今回のコードでは、StandardScaler() を使っています。
make_pipeline(
PolynomialFeatures(degree=degree, include_bias=False),
StandardScaler(),
Ridge(alpha=0.1)
)
Ridge回帰では、係数の大きさにペナルティをかけます。
そのため、特徴量のスケールがバラバラだと、ペナルティのかかり方もバラバラになってしまいます。
たとえば、ある特徴量は値の範囲が $0$ から $1$、別の特徴量は $0$ から $10000$ だとします。
この場合、同じ意味の変化であっても、係数の大きさがスケールに依存して変わってしまいます。
そのため、Ridge回帰やLasso回帰のように係数にペナルティをかける手法では、標準化が重要です。
正則化は魔法ではない
正則化は便利ですが、万能ではありません。
正則化でできるのは、主に次のようなことです。
- 係数が大きくなりすぎることを抑える
- 過学習を抑えることがある
- ノイズに対して少し安定したモデルにする
一方で、正則化だけでは解決できない問題もあります。
- データが少なすぎる
- 重要な特徴量が入っていない
- データに系統的な偏りがある
- 訓練データとテストデータの分布が大きく違う
- データリークがある
正則化は、モデルを安定化させるための重要な道具です。
しかし、データの質や検証設計の代わりにはなりません。
まとめ
この記事では、Ridge回帰を使って、正則化が何をしているのかを見ました。
ポイントは次の通りです。
- 通常の回帰は、予測誤差を小さくしようとする
- Ridge回帰は、予測誤差に加えて、係数の大きさにもペナルティをかける
- 係数が大きくなりすぎると、訓練データに合わせすぎた不安定なモデルになりやすい
- Ridge回帰では、係数を小さく保つことで過学習を抑えることがある
-
alphaが小さすぎると正則化が弱く、大きすぎるとアンダーフィットしやすい - 実際には、検証データや交差検証で
alphaを選ぶことが重要
正則化は、単に「係数を小さくするテクニック」ではありません。
むしろ、
データへの当てはまりと、モデルの複雑さのバランスを取る方法
と考えると理解しやすいと思います。
おまけ:試してみると理解が深まる変更
余裕があれば、次の値を変えて実行してみてください。
1. 多項式の次数を変える
degree = 3
degree = 6
degree = 12
degree = 20
次数が高いほど、モデルは複雑な曲線を表現できます。
その一方で、過学習もしやすくなります。
2. ノイズを変える
noise_sigma = 0.05
noise_sigma = 0.30
noise_sigma = 0.60
ノイズが大きいほど、訓練データに合わせすぎる危険性が高くなります。
3. 訓練データ数を変える
n_train = 10
n_train = 20
n_train = 100
データ数が少ないと、複雑なモデルは不安定になりやすいです。
データ数が増えると、正則化なしでも安定する場合があります。
4. alphaを変える
Ridge(alpha=0.0001)
Ridge(alpha=0.01)
Ridge(alpha=1)
Ridge(alpha=100)
alpha を変えると、係数の大きさと予測曲線がどう変わるかを確認できます。
5. StandardScalerを外してみる
make_pipeline(
PolynomialFeatures(degree=degree, include_bias=False),
Ridge(alpha=0.1)
)
標準化を外すと、特徴量のスケールの影響を受けやすくなります。
Ridge回帰では、標準化の有無が結果に影響することがあります。
次に学ぶとよいこと
Ridge回帰を理解したら、次は以下のテーマにつながります。
- Lasso回帰:係数を0にしやすい正則化
- Elastic Net:RidgeとLassoを組み合わせた正則化
- 交差検証:
alphaをどう選ぶか - 過学習と汎化性能
- バイアス・バリアンスのトレードオフ
正則化は、機械学習だけでなく、統計モデリングや実験データ解析でも重要な考え方です。
まずは今回のような小さなダミーデータで、係数・誤差・曲線の変化を見ておくと、かなり理解しやすくなります。





