静かなる名辞

pythonとプログラミングのこと


sklearnで正則化回帰(Ridge, Lasso, ElasticNet)するときはCV付きのモデルがいいよ

はじめに

 正則化回帰は割と定番のモデルなのですが、sklearnのAPIリファレンスをよく見ると、CVが末尾についたモデルがあることがわかります。

  • Lasso→LassoCV
  • Ridge→RidgeCV
  • ElasticNet→ElasticNetCV

API Reference — scikit-learn 0.21.2 documentation

 なんのこっちゃと思っていたのですが、このCVはCross Validation、要は交差検証です。正則化回帰では正則化パラメータを定める必要があるのですが、一概にどの値が良いとは言えないので、パラメータチューニングを行う必要があります。CV付きのモデルは内部的にチューニングを行ってくれるようです。

 どうせチューニングするので、使えるものは使った方が良さそうにも思います。でも、GridSearchCVという見慣れたモデルもあるので、そちらとどちらが良いのか気になりますね。

 試してみましょう。

実験

 Ridgeで、3次多項式の回帰。係数は適当

import time
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import Ridge, RidgeCV
from sklearn.model_selection import KFold, GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.metrics import r2_score

def main():
    np.random.seed(0)
    x = np.arange(-5, 5, 0.3)
    y = 7 + 5*x + 3*x**2 + 1*x**3 + np.random.normal(scale=3, size=x.shape)
    X = x.reshape(-1, 1)

    x_test = np.arange(-7, 7, 0.1)
    y_test = 7 + 5*x_test + 3*x_test**2 + 1*x_test**3
    X_test = x_test.reshape(-1, 1)
    
    ridge_origin = Pipeline(
        [("pf", PolynomialFeatures(degree=3, 
                                   include_bias=False)),
         ("r", Ridge())])

    ridge_cv = Pipeline(
        [("pf", PolynomialFeatures(degree=3,
                                   include_bias=False)),
         ("r", RidgeCV(cv=KFold(n_splits=6, shuffle=True, random_state=0)))])

    ridge_grid = GridSearchCV(
        Pipeline(
            [("pf", PolynomialFeatures(degree=3,
                                       include_bias=False)),
             ("r", Ridge())]),
        param_grid={"r__alpha":[0.1, 1.0, 10.0]},
        cv=KFold(n_splits=6, shuffle=True, random_state=0))
    
    for name, reg in [("origin", ridge_origin), 
                      ("CV", ridge_cv), 
                      ("GridSearchCV", ridge_grid)]:
        t1 = time.time()
        reg.fit(X, y)
        t2 = time.time()
        fit_time = t2 - t1
        y_pred = reg.predict(X_test)
        score = r2_score(y_test, y_pred)
        print("{0:13} fit_time:{1:.6f} r^2:{2:.6f}".format(
            name, fit_time, score))

    print(ridge_cv.named_steps.r.alpha_)
    print(ridge_grid.best_estimator_.named_steps.r.get_params()["alpha"])

if __name__ == "__main__":
    main()

 結果。

origin        fit_time:0.002750 r^2:0.999419
CV            fit_time:0.031689 r^2:0.999850
GridSearchCV  fit_time:0.053099 r^2:0.999850
10.0
10.0

 RidgeCVでやろうとGridSearchCVでやろうと同じ結果に落ち着いているようですが、RdigeCVの方が微妙に速いです。

 考察すると、やはりGridSearchCVだと汎用的なぶん余計なオーバーヘッドが多く、RdigeCVの方は高速に実行できるようです。あと、n_jobsを指定したら理不尽なほど遅くなったので、そういう方向で高速化も無理だと思います。

まとめ

 こういうものがある、ということを知っておくと、パラメータチューニングが捗るかと思います。あと、LogisticRegressionCVもあったりするので、分類でも使えます。

 なんでもかんでもGridSearchCV、というのではなく、使えるモデルを使い分けるという姿勢が大切だと思いました。