python

pythonのnumpyだけでリッジ(Ridge)回帰をフルスクラッチ実装!!

今回はRidge回帰を実装していきます!

実はリッジ回帰は重回帰とほとんど同じコードで作成することができるのです!

なので、先に重回帰の実装を見てから読むことをオススメします。

pythonのnumpyだけで重回帰をフルスクラッチ実装!!こんにちは。 みなさん、重回帰してますか? 僕は研究でよくしてます。計算が速い上に安定してそこそこの精度を出すRidge回帰...

リッジ(Ridge)回帰とは?

重回帰のパラメータを求める際に、正則化項を入れることで、重回帰よりもロバストな結果を返すモデルです。

k近傍法などのノンパラメトリック手法や、ニューラルネットと比べて計算が速い上に、

だいたいどんな回帰問題でも解ける問題なら安定してそこそこの精度を出します。(経験則)

 

なので、方針を決める際に、この問題は回帰問題として解く価値があるか否かを決定するために一番最初に使用したり、ニューラルネットなどのベースラインとして設定することが多いです。

 

重回帰では多重共線性 multicollinearity がある際に、訓練データに対する最適解として$w$にとんでもなく絶対値の大きい、正の値と負の値を出力してくることがあります。

これでは、全然一般的なモデルにならないです。

そういった問題も少し改善してくれるのがリッジ回帰だったりします。(基本的に多重共線性は避けるべきです。だた、最初から多重共線性があることを知っている場合の方が少ないので、リッジ回帰が良いよという意味です。)

文字の説明

  • $y$ : 目的変数 ( 従属変数 )
  • $x$: 説明変数 ( 独立変数 )
  • $ϕ_j$ : 基底関数 ( $x_j$, $x_j^2$, $ln{(x_j)}$ など )
  • $ω_j$ : 回帰係数
  • $ε$ : 撹乱項、誤差項
  • $λ_2$:正則化項 ← New!!

どんな考え方をするの?

基本的には重回帰と全く同じです。$y$を求めるための$w$を求めます。

$y =ω_0+ω_1 ϕ_1 (x)+ω_2 ϕ_2 (x)+⋯+ω_{K-1} ϕ_{K-1} (x)+ε$
  $=w^T ϕ+ε$

ただし、

$w= (ω_0,ω_1,⋯,ω_{K-1})^T,   ϕ=(ϕ_0 (x),ϕ_1 (x) , ⋯,ϕ_{K-1} (x))^T$

すなわち、$y$を求めるために必要なものは、

  1. 重みとなる$ω$
  2. 独立関数$ϕ$

この二つです。

$ϕ$については、$x$の何乗までを入力にしてあげるかを関数の中で決めれるようにします。

なので、ここでは解説を飛ばします。

どうやってωを求めるの?

良い感じの$f(x)$について考えるために誤差$E$を調べます。

ここで初めて、重回帰との違いが現れます!

$E = \frac{1}{2}||w^T ϕ – y || + \frac{1}{2}λ_2||w||^2$

のように、$λ_2||w||^2$のようにL2ノルムと呼ばれる、正則化項を加えます。

これによって、$w$がとんでもなく大きな値となることを防ぎます。

 

それ以外は全く一緒です。

やはり、$E$が最小になってくれると嬉しいので、最小二乗法を用いて、$E$が最小となる$ω$を計算します。

最初値を求めたいので、$E$を$ω$で微分した結果が0になればよいです。

微分を計算をして$ω$について整理すると、

$ω=(ϕ^T ϕ + λ_2 I)^{-1} ϕ^T y$

となります。(ただし、$I$は単位行列)

これで、$ω$を求めるコトができました。

$y$を求める

$y=w^T ϕ$

と適応をしてあげれば完成です。

リッジ(Ridge)回帰の実装

ちなみに、fitの部分では一番最初のインデックスには切片のための全て1の列が入っています。

predictの部分ではfitの際取得した切片、self.intercept_があるので、全て1の列は削除しています。

import numpy as np

class MyRidgeRegressor:
    def __init__(self, alpha=1.0):
        """
        alpha : float,  default=1.0
            Regularization strength
        """
        self.alpha = float(alpha) # ←New!!
        self.dim = None
        self.coef_ = None
        self.intercept_ = None
        self.n_feature = None
        self.__name__ = "Ridge"

    def fit(self, X, y, dim=1):
        """
        Calculate weight of ridge regression
        Parameters
        X : array_like (n_sample, n_feature)
            The matrix of explanatory variables
        y : array_like (n_sample, 1)
            Objective variable
        dim : int, default 1
            Dimension of multiple regression
        """
        n_sample, n_feature = X.shape
        # X (n_sample, dim, n_feature)
        X = np.array([[x**i for i in range(dim + 1)] for x in X])
        # delete duplicated ones array
        X = X.reshape(n_sample, -1)[:, n_feature - 1:]
        # define identity matrix
        Eye = np.eye(X.shape) # ←New!!
        # calculate coefficient
        weight = np.dot(np.dot(np.linalg.inv(np.dot(X.T, X) + self.alpha * Eye), X.T), y) # ←New!!
        self.coef_ = weight
        self.intercept_ = weight[0]
        self.dim = dim
        self.n_feature = n_feature

    def predict(self, X):
        """
        Calculate ridge regression
        Parameters
        X : array_like (n_sample, n_feature)
            The matrix of explanatory variables in test dataset
        Returns
            Predicted objective variable
        """
        n_sample, n_feature = X.shape
        # X (n_sample, dim, n_feature)
        X = np.array([[x**i for i in range(self.dim + 1)] for x in X])
        # delete ones array
        X = X.reshape(n_sample, -1)[:, n_feature:]
        return (np.dot(X, self.coef_) + self.intercept_)

    def mse(self, X, y):
        """
        Calculate mean square error 
        Parameters
        X : array_like (n_sample, n_feature)
            The matrix of explanatory variables in test dataset
        y : Ground truth of objective variable
        Returns
            Mean square error
        """
        return np.mean((self.predict(X) - y)**2)

    def title(self):
        """
        Make title for plot
        Return
            string of equation
        """
        Xn = [f"x_{k}" for k in range(1, self.n_feature + 1)]
        title = "$f(x) = $" + f"{float(self.intercept_): .2f}"
        for d in range(1, self.dim + 1):
            for k in range(self.n_feature):
                if self.coef_[(d - 1) * self.n_feature + k] > 0:
                    sign = "+"
                elif self.coef_[(d - 1) * self.n_feature + k] < 0:
                    sign = "-"
                else:
                    continue
                if d == 1:
                    title += f"${sign}{abs(float(self.coef_[(d-1) * self.n_feature + k])):.2f}{Xn[k]}$"
                else:
                    title += f"${sign}{abs(float(self.coef_[(d-1) * self.n_feature + k])):.2f}{Xn[k]}^{d}$"
        return title

 

Newと記載した部分が重回帰との違いです。

見て分かるとおり、ほとんど同じです。

それでは、性能の比較をしていきます。

リッジ(Ridge)回帰を適応

import argparse
import pandas as pd
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from sklearn.model_selection import KFold

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-d", "--dim", type=int, default=1, help="回帰の次元数.デフォルトは1")
    parser.add_argument("-r", "--regressor", type=str, default="m", help="回帰モデルの名前.デフォルトは'm'")
    parser.add_argument("-f", "--fname", type=str, default="data1.csv", help="読み込むファイルの名前.デフォルトは'data1.csv'")
    parser.add_argument("-a", "--alpha", type=float, default=1.0, help="正則化項の係数.デフォルトは1.0")
    args = parser.parse_args()

    # define regression model
    dim = args.dim
    alpha = args.alpha
    regressor = args.regressor
    if regressor == "m":
        model = MyMultipleRegressor()
    elif regressor == "r":
        model = MyRidgeRegressor(alpha=alpha)

    # load data
    fname = args.fname
    data = pd.read_csv('../' + fname)
    # split data
    n_sample, n_cols = data.shape
    feature_cols = list(data.columns[:-1])
    target_col = list(data.columns[-1:])
    kf = KFold(n_splits=2, shuffle=True)
    for train_idx, test_idx in kf.split(data):
        break
    # train data
    train_X = data.loc[train_idx, feature_cols].values.reshape(-1, n_cols - 1)
    train_y = data.loc[train_idx, target_col].values.reshape(-1, 1)
    model.fit(train_X, train_y, dim=dim)
    train_mse = model.mse(train_X, train_y)
    # test data
    test_X = data.loc[test_idx, feature_cols].values.reshape(-1, n_cols - 1)
    test_y = data.loc[test_idx, target_col].values.reshape(-1, 1)
    test_mse = model.mse(test_X, test_y)
    title = model.title()
    if n_cols == 2:
        # plot original data
        plt.figure(figsize=(10, 8))
        plt.xlabel(feature_cols[0], size=16)
        plt.ylabel(target_col[0], size=16)
        plt.title(f"{model.__name__} Regression\n" + title + f"\nTrain MSE : {train_mse:.4f}, Test MSE : {test_mse:.4f}", size=20)
        plt.scatter(train_X, train_y, label="Train Observed")
        plt.scatter(test_X, test_y, label="Test Observed")
        # plot predicted data
        X1 = np.linspace(data[feature_cols].min(), data[feature_cols].max(), 500)
        y1_hat = model.predict(X1)
        plt.plot(X1, y1_hat, c="r", label="Predicted")
        plt.legend(fontsize=16)
        plt.savefig(fname[:-4] + f"{model.__name__}_{dim}D.png", bbox_inches="tight")
        plt.show()
    elif n_cols == 3:
        # plot original data
        fig = plt.figure(figsize=(10, 8))
        ax = Axes3D(fig)
        ax.set_xlabel(feature_cols[0], size=14)
        ax.set_ylabel(feature_cols, size=14)
        ax.set_zlabel(target_col[0], size=14)
        plt.title(f"{model.__name__} Regression\n" + title + f"\nTrain MSE : {train_mse:.4f}, Test MSE : {test_mse:.4f}", size=20)
        ax.plot(data.loc[train_idx, feature_cols[0]].values, data.loc[train_idx, feature_cols].values,
                data.loc[train_idx, target_col].values.flatten(),
                marker="o", linestyle='None', label="Train Observed")
        ax.plot(data.loc[test_idx, feature_cols[0]].values, data.loc[test_idx, feature_cols].values,
                data.loc[test_idx, target_col].values.flatten(),
                marker="o", linestyle='None', label="Test Observed")
        # plot predicted data
        X1 = np.linspace(data[feature_cols[0]].min(), data[feature_cols[0]].max(), 30)
        X2 = np.linspace(data[feature_cols].min(), data[feature_cols].max(), 30)
        X1, X2 = np.meshgrid(X1, X2)
        X3 = np.concatenate([X1.reshape(-1, 1), X2.reshape(-1, 1)], axis=1)
        y3_hat = model.predict(X3).reshape(X1.shape)
        ax.plot_wireframe(X1, X2, y3_hat, color="red", label="Predictied")
        plt.legend(loc='center left', fontsize=16)
        plt.savefig(fname[:-4] + f"{model.__name__}_{dim}D.png", bbox_inches="tight")
        plt.show()


if __name__ == "__main__":
    main()

 

データを見て比較

data2

左がリッジ回帰、右が重回帰です。

これを見ると重回帰の方が良さそうです。

これは、訓練データとテストデータの分布がほとんど同じだからです。

基本的には重回帰の方がモデルの表現力は高いです。

 

こういったデータを扱う場合は、重回帰の方がいいですね。

ずっと、リッジ回帰が良い、と言っていましたが、常にリッジ回帰が良いという訳ではないことが分かりました。

data3

左がリッジ回帰、右が重回帰です。

こちらのデータではリッジ回帰らしい特徴が現れています!!(そうなるように調整しました。笑)

Train MSEは重回帰の方が小さいですが、Tset MSEになるとリッジ回帰の方が小さくなっています。

正則化項を加えることによって、よりロバストなモデルを作成できた証です。

多くの場合は、訓練データとテストデータで多少なりとも分布が異なります。

そういった影響をなるべく少なくするためにもロバストなモデルを作成することは重要です。

正則化項による違いを確認

 

正則化項は左上から順に

$λ_2 = 1e-4, 10.0, 10000.0$

となっています。

 

モデルの柔軟度は$λ_2$が小さければ小さいほど大きくなります。

この場合だと、左上の写真が最も柔軟度が大きいです。

そのため、Train MSEが最小になっていることが分かります。

 

ですが、Test MSEに着目をすると$λ_2=10.0$のときに最小になっていることが分かります。

この結果から、リッジパラメータの設定も重要であることが分かります。

最後に

以上がリッジ回帰に実装とその解説でした。

様々な回帰の中で、個人的に一番好きなモデルです。(分かりやすくて精度が良いから)

数学的な部分が理解できると安心して使えると思います。

ブログが一助になれば幸いです。

次回予告

次回は、ラッソ(Lasso)回帰を実装していきます。

Lasso回帰は、数学の部分が難しいです。

興味のある方は是非見てみてください!

 

こんな感じのブログを書いていきます。

もし興味を持っていただけたら、フォローお願いします。<(_ _)>

pythonのnumpyだけで重回帰をフルスクラッチ実装!!こんにちは。 みなさん、重回帰してますか? 僕は研究でよくしてます。計算が速い上に安定してそこそこの精度を出すRidge回帰...
pythonのnumpyだけでラッソ(Lasso)回帰をフルスクラッチ実装!今回は、 Lasso回帰は多くのパラメータを0にするので説明するのに役立つ と巷では言われるLasso回帰を実装して...
オススメのプログラミングスクールをご紹介

タイピングもままならない完全にプログラミング初心者から

アホいぶきんぐ
アホいぶきんぐ
プログラミングってどこの国の言語なの~?

たった二ヶ月で

いぶきんぐ
いぶきんぐ
え!?人工知能めっちゃ簡単にできるじゃん!

応用も簡単にできる…!!

という状態になるまで、一気に成長させてくれたオススメのプログラミングスクールをご紹介します!

テックアカデミーのPython+AIコースを受講した僕が本音のレビュー・割引あり! というプログラミング完全初心者だった僕が Tech Academy(テックアカデミー)のPython×AIコース を二ヶ月間...

COMMENT

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です