見出し画像

scikit-learn機械学習⑦L1・L2正則化

前回は、ロジスティック回帰を実装しました。もっと以前には、線形回帰を実装しました。

両方のモデルに共通するのは、一つまたは複数の独立変数(特徴量)と、それらに依存する従属変数(目的変数、ラベル)との関係をモデリングすることにあります。その際に、特定の特徴が目的変数に与える影響の大きさがモデルの重み(傾き)によって表現されていました。

特に前回行ったようなデータの正規化を行なっている場合は、データの分散はどれも同じようになるので、各特徴量に対する重みがその特徴量の重要度を反映している場合が多いと考えられます。

しかし、一方で訓練データに対して過学習(overfitting)が生じている場合もあります。その場合、ある特定の特徴量に対して過剰反応している(つまり重みが大きくなっている)ことが起こりえます。

よって、訓練データに対してだけ良い予測性能を持つことだけを目指すのではなく、未知のデータに対しても良い性能を発揮できる一般化能力を持つことが重要です。訓練データに含まれる特徴量とその影響を正確にモデル化することは必要ですが、訓練データのノイズや偶発的なパターンを過剰に学習してしまうと、新しいデータに対する予測が不正確になる可能性があります。過学習は、モデルの汎用性を低下させ、実際の運用において性能が落ちる原因となります。

そこで正則化の手法が登場します。正則化は過学習を防ぐための効果的な方法の一つです。この記事では、L1・L2正則化の仕組みについて解説します。

特に、L1正則化が特徴量選択のメカニズムとして機能することやL2正則化が特徴量を残しつつ重みを小さく調節する理由について勾配の仕組みから説明していきます。また、Elastic Net正則化にも触れ、なぜそのような正則化を使うのかを解説します。


正則化の仕組み

正則化は、モデルの複雑さにペナルティを課すことにより、過度に複雑なモデルが訓練データに過剰に適合することを防ぎます。

線形回帰やロジスティック回帰では、そもそも線形のモデルを中心にしているので、非線形のモデルよりも単純であり、モデルが複雑になりにくい側面はあります。

それでも訓練データにノイズが多く含まれていたり、特徴量の数が多かったりすると機械学習は巧みにモデルをフィットさせてしまい、結果として過学習が起きる可能性があります。

そこで具体的には、正則化はモデルの重みに対して制約を加え、重みの値が大きくなりすぎるのを防ぐことで、モデルが訓練データのノイズに敏感になるのを抑制します。

なお、正則化には、主にL1正則化(ラッソ正則化、Lasso正則化)とL2正則化(リッジ正則化、Ridge正則化)があります。

L1正則化

基本的な仕組み

これは重みの絶対値の合計に対してペナルティを課します。損失関数を次のように変更します。

$$
\text{損失関数}_{L1} = \text{元の損失関数} + \lambda \sum\limits_{i=1}^n |w_i|
$$

元の損失関数は、モデルの基本的な損失関数です(例えば、線形回帰の場合は二乗誤差)。

$${\lambda \sum\limits_{i=1}^n |w_i|}$$の項がL1正則化になります。ここで、$${w_i}$$は、各特徴量に対する重みです。

$$
y = w_1 x_1 + w_2 x_2 + \dots + w_n x_n + b
$$

また、$${\lambda}$$は、元の損失関数とのバランスを取るためのハイパーパラメータで正の定数を指定します。この値を大きくするほど、重みに対するペナルティが強くなり、より多くの重みが0に近づきます。

なお、L1正則化を使用すると、いくつかの重みが完全に0になり、モデルから特定の特徴が除外されることがあります。このため、L1正則化は必要な特徴量だけを残すという特徴選択のメカニズムとして機能するので、モデルをシンプルに保つという性質があります。

次に、なぜL1正則化が特徴量選択のメカニズムとして機能するのかを解説します。

重みがゼロになる理由

L1正則化が重みを0にすることを勾配の観点から説明するには、正則化項の勾配がどのように重みの更新に影響を与えるかを理解する必要があります。L1正則化を適用した場合、モデルの全体的な損失関数には元の損失関数(例えば、二乗誤差)に加えて、重みの絶対値の和に比例する正則化項が含まれます。この正則化項が重みの更新プロセスにどのように作用するかを見てみましょう。

重みの更新は、損失関数の勾配に基づいて行われます。重み$${w_i}$$に対するL1正則化の勾配は、重みの符号に依存します。具体的には、

  • $${w_i > 0}$$の場合、$${|w_i|}$$の勾配は$${1}$$です。

  • $${w_i < 0}$$の場合、$${|w_i|}$$の勾配は$${-1}$$です。

  • $${w_i = 0}$$の場合、勾配は定義されませんが、実際の最適化プロセスではこの点での勾配は0とみなします。

元の損失関数を無視してL1正則化だけによる重みの更新を考えてみます。

上述の勾配の結果として、重みの更新において一定の値($${\lambda}$$の影響を受ける)が引かれるか加えられます。つまり、このプロセスによると、重みの大きさによらず常に重みをゼロに近づけます。また、$${w_i = 0}$$では、勾配は0と見做されるのでこれ以上の変更は起きません。つまり、重みは0のままになります。

もちろん、元の損失関数の勾配からも重みは更新されるので、損失を減らすために重要な重みは生き残ります。なぜなら、こちらの勾配による重み更新への影響がL1正則からの勾配を上まるからです。

逆に言うと、あまり重要でない重みはL1正則化によって0の方向へ引き寄せられて消失します。これがL1正則化によって不必要な重みが0になり、特徴選択のメカニズムと機能する理由です。

scikit-learn Lasso

scikit-learnには、L1正則化を含んだ線形回帰モデルとしてLassoがあります。以下は、scikit-learnのサンプルコードから変更して引用したものです。

from sklearn import linear_model

clf = linear_model.Lasso(alpha=0.1)
clf.fit([[0,0], [1, 1], [2, 2]], [0, 1, 2])

print(clf.coef_)

alpha が L1正則化の係数$${\lambda}$$に相当します。

L2正則化

基本的な仕組み

L2正則化では、モデルの損失関数に重みの二乗の合計に比例する項を追加します。これは以下のように表現されます。

$$
\text{損失関数}_{L2} = \text{元の損失関数} + \lambda \sum\limits_{i=1}^n w_i^2
$$

  • $${\text{元の損失関数}}$$、$${w_i}$$、$${\lambda}$$の意味はL1正則化の説明と同様です。

  • $${\lambda}$$はここでも正則化の強度を調整する正の定数ですが、L2正則化では重みの二乗の和にペナルティを課します。これにより、重みの絶対値が大きくなることを防ぎますが、L1正則化とは異なり、重みを完全に0にすることはありません。

重みがゼロにならない理由

重み(w_i)に対するL2正則化項の勾配は、以下のように計算されます。

$$
\frac{\partial}{\partial w_i}(\lambda w_i^2) = 2\lambda w_i
$$

この勾配により、重みの更新時には重み自体に比例する値が引かれることになります。具体的には、重み$${w_i}$$の更新は元の勾配(元の損失関数からの勾配)に、$${2\lambda w_i}$$を加えたものに基づいて行われます。よって、L2正則化によるこのプロセスは、重みを原点に向かって縮小させる効果があります。

なお、L2正則化の勾配は、重みが大きくなるほど、その重みを小さくする力が強くなります。これにより、重みの絶対値が大きくなることを抑制し、過学習を防ぎます。

しかし、この効果は重みをゼロに近づけるものの、L1正則化のように重みを完全にゼロにするわけではありません。その理由は、重みが小さくなるにつれて、ゼロに向かって引く力(勾配)も小さくなるためです。これはL1正則化と大きく異なる点です。L1正則は、重みの正負によって勾配の値が一定に決まり、重みが小さくなっても勾配が小さくなることはりませんでしt。

以上より、L2正則化は、モデルの重みを全体的に小さくすることで、モデルの複雑さを制限し、過学習を防ぐ効果があります。損失値を下げるために重要な重みはそれなりの大きさを保ちますが、あまり重要でない重みは小さくなります。ただし、上記の理由より重みはゼロにはなりにくくなっています。

重みがゼロにならないため、すべての特徴がある程度モデルに影響を与え続けることになります。よって、特徴量を捨てたくない場合にはL2正則化が有効だと言えます。

scikit-learn Ridge

scikit-learnには、L2正則化を含んだ線形回帰モデルとしてRidgeがあります。以下は、scikit-learnのサンプルコードから変更して引用したものです。

from sklearn.linear_model import Ridge
import numpy as np

y = ...
X = ...

clf = Ridge(alpha=1.0)
clf.fit(X, y)

alpha が L2正則化の係数$${\lambda}$$に相当します。

Elastic Net正則化

基本的な仕組み

Elastic Net正則化は、L1正則化とL2正則化の両方を組み合わせたもので、二つの正則化のバランスをとるパラメータを持ちます。これにより、L1正則化とL2正則化の利点を両方得ることが可能になります。

Elastic Netの損失関数は次のように表されます。

$$
\text{損失関数}{\text{Elastic Net}} = \text{元の損失関数} + \lambda_1 \sum\limits_{i=1}^{n} |w_i| + \lambda_2 \sum\limits_{i=1}^{n} w_i^2
$$

  • $${\text{元の損失関数}}$$はモデルの基本的な損失関数です(例:線形回帰では二乗誤差)。

  • $${w_i}$$はモデルの重みです。

  • $${\lambda_1}$$と$${\lambda_2}$$は、それぞれL1正則化とL2正則化の強度を調整する正の定数です。

Elastic Net正則化を使う理由

これは単純で、L2正則化は過学習を防ぐ効果がありますが、すべての特徴に対して小さな重みを割り当てるため、特徴選択は行われません。Elastic NetではL1の特徴選択能力とL2の過学習防止効果をバランス良く組み合わせられるので、不要な特徴量を切り捨て、尚且つ全体の重みの大きさを小さく保つことが出来ます。よって、うまくいけば、より汎用性の高いモデルを構築することができます。

「うまくいけば」と言ったのは、$${\lambda_1}$$と$${\lambda_2}$$のパラメータを調整する必要があるからです。L1正則化を強くし過ぎるとL2正則化の意味がなりますし、逆も起こりえます。うまく調節できれば、データセットに対して柔軟に対応することが可能ですが、ちょっとしたアート(芸術)でもあります。

また、L1正則化の特徴(というか問題点)として、複数の特徴量の間に相関性がある場合、そのうちの1つまたはごく少数の特徴にのみ重みを割り当て、残りを0にする可能性があります。しかし、この選択はデータのランダムな変動によって左右されることがあり、どの特徴量が削除されるかはデータの中のノイズに影響される可能性があります。すると、重要な特徴が誤って排除されることがありえます。

つまり、L1正則化は複数の特徴が強く相関している場合、L1正則化はこれらのうちの一部しか選択しない(またはランダムに選択する)可能性があり、これがモデルの予測性能や解釈性に影響を及ぼすことがあります。

その一方で、Elastic NetはL1とL2の正則化を組み合わせるので、特にL2正則化の影響がうまく調節されていると特徴が切り捨てられにくくなるため、L1正則化による問題を緩和することができます。もちろん、L2正則化によって過学習を防ぐことが出来ます。それでも、本当に不要な特徴はL1正則化によって効果的にモデルから除外できます。

もちろん、「うまくいけば」の話ですが。

scikit-learn ElasticNet

scikit-learnには、Elastic Net正則化を含んだ線形回帰モデルとしてElasticNetがあります。以下は、scikit-learnのサンプルコードをちょっと改良してから引用したものです。

from sklearn.linear_model import ElasticNet

X = ...
y = ...

regr = ElasticNet(alpha=1.0, l1_ratio=0.5)
regr.fit(X, y)

ここで、alpha と l1_ratio が登場しています。

  • alphaは、正則化の全体的な強度を制御します。alphaが大きいほど、正則化は強くなり、モデルの重みはより小さくなります。このalphaは、L1正則化とL2正則化の両方の強度に影響を与えます。

  • l1_ratioは、L1正則化の比率を指定します。具体的には、l1_ratioの値は0から1の間で、l1_ratioにalphaを掛けたものがL1正則化の強度$${\lambda_1}$$に相当します。また、(1 - l1_ratio)にalphaを掛けたものがL2正則化の強度$${\lambda_2}$$に相当します。

したがって、Elastic Netの正則化項は以下のように表されます:

$$
\alpha \left( \text{l1}_{\text{ratio}} \cdot \sum\limits_{i=1}^n |w_i| + (1 - \text{l1}_\text{ratio})\cdot \dfrac{1}{2} \sum\limits_{i=1}^n w_i^2 \right)
$$

  • $${|w_i|}$$は重みの絶対値(L1正則化項)

  • $${w_i^2}$$は重みの二乗(L2正則化項)

なお、L2正則化項に$${\frac{1}{2}}$$がついているのは、勾配の式から2を除外するためです。なぜなら、L2正則化項は、重み$${w_i}$$の二乗の合計に基づいているからです。

$$
\lambda_2 \sum_{i=1}^{n} w_i^2
$$

この項の勾配を計算すると、各重み$${w_i}$$に対する偏微分は以下のようになります。

$$
\frac{\partial}{\partial w_i} (\lambda_2 w_i^2) = 2\lambda_2 w_i
$$

勾配降下法などの最適化アルゴリズムでは、この勾配を使用して重みを更新します。ここで、2が勾配の式に現れるため、更新の際にはこの2が影響を与えます。

scikit-learnのElasticNetにおけるL2正則化項の定義を2で割ることで、勾配の式から直接的に2を除外し、重みの更新式を単純化しています。

$$
\alpha (1 - \text{l1}_\text{ratio})\cdot \frac{1}{2} \sum w_i^2
$$

結果的に、重みの更新は以下のようになります。

$$
\frac{\partial}{\partial w_i} \left(\alpha (1 - \text{l1}_\text{ratio})\cdot \frac{1}{2} \sum w_i^2\right) = \alpha  (1 - \text{l1}_\text{ratio}) \cdot w_i
$$

こうすることで、重みの更新時の計算が直感的に理解しやすくなり、実装上の便宜を図っています。

もう一度、使用例を見てみます。

from sklearn.linear_model import ElasticNet

X = ...
y = ...

regr = ElasticNet(alpha=1.0, l1_ratio=0.5)
regr.fit(X, y)

この例では、alpha=1.0 で全体的な正則化の強度を、l1_ratio=0.5 でL1とL2のバランスをそれぞれ均等に設定しています。l1_ratio を0に設定すると、L2正則化のみが適用され(Ridge正則化に相当)、l1_ratio を1に設定すると、L1正則化のみが適用されます(Lasso正則化に相当)。

この方法により、scikit-learn の ElasticNet では、正則化の強度とL1とL2のバランスを柔軟に調整できます。

ちなみに、alpha=0.0 にするとL1正則化とL2正則化の両方とも無効になるので、その場合は、LinearRegression クラスを使うのと同じです。

ロジスティック回帰と正則化

最後に、ロジスティック回帰と正則化について解説します。

scikit-learn の LogisticRegression クラス自体が L1正則化と L2正則化 をサポートしています。以下は、サンプルコードを改良して引用したものです。

まずは、L1正則化はこのように penalty として指定します。

from sklearn.linear_model import LogisticRegression

X = ...
y = ...

clf = LogisticRegression(penalty='l1', C=1.0, solver='saga')
clf.fit(X, y)

なお、penalty には次の値がサポートされています。

  • None: 正則化なし

  • 'l2': L2正則化(デフォルト)

  • 'l1': L1正則化

  • 'elasticnet': Elastic Net正則化

C は、$${\lambda}$$の逆数になっています。よって、Cが小さいほど正則化が強くなります。

また、solver として L1正則化をサポートしているものを指定する必要があります。詳しくは、scikit-learn のドキュメントを参照してください。

Elastic Net正則化は次のように指定します。

from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(penalty='elasticnet',
                         C=1.0, 
                         l1_ratio=0.5,
                         solver='saga')
  • Cは正則化全体にかかります

  • l1_ratio は L1正則化の割合です。0から1の値を指定します。よって、L2正則化には 1 - l1_ratio がかかります。

なお、'elasticnet' を使用する場合、'l1'正則化を使用するので、'saga'のような対応するソルバーを指定する必要があります。

次回予告

次回は、もう少しロジスティック回帰を使った機械学習の実装を続けます。

お楽しみに!

いいなと思ったら応援しよう!