正則化:重みが大きすぎることにも、罰を与える
前のレッスンの14次式は、訓練データの15点をほぼすべて通りましたが、点のない場所で大きく暴れました。このとき、式の係数を調べると、大きさが約7億4000万という、とても大きな数になっていました。大きな係数どうしが打ち消し合って点を通り、少しでも点から離れると、打ち消し合いが崩れて暴れる のです。
そこで、係数(ニューラルネットワークなら重み)が大きくなりすぎないように、損失に「係数の大きさの罰」を足します。これを 正則化(せいそくか)と呼びます。
L2正則化:係数の2乗の和を、罰として足す
損失=誤差の2乗の平均+α×(w12+w22+⋯+wd2)
w1,w2,… は係数、α(アルファ)は罰の強さを決める数です。学習(損失を小さくすること)では、誤差を小さくしたい一方で、係数を大きくすると罰が増えるので、必要以上に大きな係数を使わなくなります。係数の2乗を使うので L2正則化 と呼び、線形回帰にL2正則化を加えたものを Ridge回帰(リッジ回帰)と呼びます。
実験:14次式にL2正則化を加える
前のレッスンと同じ15点に、14次式を当てはめます(テストは、当てはめに使っていない別の200点)。
- 14次式+L2(α=0.01)
- 14次式+L2(α=10)
15個の点(○)に、L2正則化を加えた14次式を当てはめた曲線。α=0.01では、点の間をなめらかに通る波の形になり、暴れなくなった。α=10では、罰が強すぎて曲がれず、山のない、ゆるやかに右下がりの曲線になり、x=1.6の点(1.31)から大きく離れている
- α=0.01 では、係数が約7億4000万から3.22まで小さくなり、曲線が暴れなくなりました。テスト誤差0.097は、前のレッスンの3次式(ちょうどよい式)に近い値です。次数が高い式でも、正則化を加えれば過学習を抑えられます
- α を大きくするほど、係数は小さくなりますが、強すぎると曲線が曲がれなくなり、訓練誤差もテスト誤差も大きくなります(過少適合)
α はちょうどよい強さを選ぶ必要がある ということです。どう選ぶかは、レッスン7で学びます。
L1正則化と、ニューラルネットワークの正則化
L1正則化:係数の絶対値の和を、罰として足す
L2正則化の「2乗」の代わりに、「絶対値」(符号を外した大きさ、例えば ∣−3∣=3)を使うこともできます。
損失=誤差の2乗の平均+α×(∣w1∣+∣w2∣+⋯+∣wd∣)
これを L1正則化 と呼び、線形回帰にL1正則化を加えたものを Lasso回帰(ラッソ回帰)と呼びます。
L1正則化には、いくつかの係数を、ちょうど0にする という特徴があります。同じ14次式に、L1正則化(α=0.001)を加えると、次のようになりました。
L1正則化では、14個の係数のうち10個がちょうど0になり、x・x2・x5(と、もう1つ、ごく小さい値)の項だけが残りました。テスト誤差は、L2正則化とほぼ同じです。多くの係数が0になっている状態を スパース(まばら)と呼びます。
L1とL2の違いを、直感で理解する
- L2 は、大きな係数ほど強く罰します(2乗なので、係数が2倍になると罰は4倍)。そのため、全部の係数を、少しずつ小さくします。0にはなりにくい
- L1 は、係数の大きさに比例して罰します。係数を少し減らすことで得られる罰の減り方が、小さな係数でも大きな係数でも同じなので、「あまり役に立っていない係数」は、ちょうど0まで減らされます
L1正則化は、たくさんの特徴量の中から、本当に必要なものだけを選びたいとき(どの特徴量が効いているかを知りたいとき)に便利です。
ニューラルネットワークの正則化(weight decay)
ニューラルネットワークでも、損失に重みの2乗の和を足すL2正則化がよく使われます。勾配降下法で学習すると、罰の分だけ、毎回すべての重みが0に向かって少しずつ縮むので、weight decay(重みの減衰)とも呼ばれます。
第6章の MLPClassifier では、alpha という設定がL2正則化の強さです。ノイズ(ばらつき)の多い2クラスの分類のデータ(訓練60点・テスト500点)で、大きなネットワーク(隠れ層100個を2層)を学習させると、次のようになりました。
正則化を加えると、訓練データの正解率は下がりますが(丸暗記しにくくなるため)、テストデータの正解率は上がりました。ここでも、強すぎると下がります。
Pythonで正則化を使う、よくある誤解と振り返り
scikit-learnでは、L2正則化の線形回帰は Ridge、L1正則化は Lasso です。alpha で罰の強さを決めます。
import numpy as np
from sklearn.linear_model import Ridge, Lasso
x = np.array([0.02, 0.10, 0.20, 0.25, 1.62, 3.26, 3.64, 3.82,
4.38, 4.38, 4.88, 4.90, 5.14, 5.48, 5.61])
y = np.array([-0.20, -0.06, 0.11, 0.37, 1.31, -0.16, -0.07, -0.83,
-0.84, -0.67, -0.96, -1.21, -1.18, -0.86, -0.56])
# 特徴量: x, x², x³, ..., x¹⁴ を横に並べた表(15行×14列)
X = np.column_stack([x ** k for k in range(1, 15)])
# 標準化: 列ごとに、平均を引いて標準偏差で割る(大きさをそろえる)
X = (X - X.mean(axis=0)) / X.std(axis=0)
ridge = Ridge(alpha=0.01).fit(X, y)
print(np.abs(ridge.coef_).max()) # → 約3.22(係数の最大の大きさ)
lasso = Lasso(alpha=0.001, max_iter=200000).fit(X, y)
print(np.sum(lasso.coef_ != 0)) # → 4(0でない係数の数)
np.column_stack は、配列を列として横に並べて表にします
- 標準化: x14 は x=5 で約61億にもなり、x とは桁がまったく違います。このまま正則化すると、係数の罰が列ごとに不公平になるので、列ごとに平均を引いて標準偏差で割り、どの列も「平均0、ばらつき1」にそろえてから当てはめます。第3章の前処理で学んだ「値の大きさをそろえる」の一つの方法です
max_iter は、Lassoの計算(少しずつ係数を動かす)を最大何回まで繰り返すかです
よくある誤解
- 「正則化をすると、訓練データの正解率が下がるので、悪い変化」: 正則化は、訓練データへの当てはまりを少しあきらめる代わりに、新しいデータでの当てはまりをよくする方法です。見るべきはテストデータでの結果です
- 「alphaは大きいほど安全」: 大きすぎると、係数を小さくしすぎて過少適合になります(α=10 の曲線)
- 「L1とL2は、どちらも係数を0にする」: ちょうど0にしやすいのはL1です。L2は全体を少しずつ小さくします
振り返り
- 正則化は、損失に「係数(重み)の大きさの罰」を足して、係数が大きくなりすぎるのを防ぐ。過学習を抑えられる
- L2(Ridge、weight decay)は係数の2乗の和、L1(Lasso)は絶対値の和。L1は多くの係数をちょうど0にする(スパース)
- 罰の強さ α は、強すぎても弱すぎてもよくない