静かなる名辞

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


GridSearchCV『の』パラメータ・チューニング 高速化中心に

はじめに

 機械学習でパラメータ・チューニングをしたい場合、グリッドサーチを行うのが定石とされています。sklearnではグリッドサーチはGridSearchCVで行うことができます。

sklearn.model_selection.GridSearchCV — scikit-learn 0.21.2 documentation

 それで何の問題もないかというと、さにあらず。

 グリッドサーチは計算コストの高い処理ですから*1、素直に書くとデータとアルゴリズム次第ではとんでもない処理時間がかかります。

 もちろん「寝ている間、出かけている間に回すから良い」と割り切るという方法もありますが、可能なら速くしたいですよね。

 そうすると、パラメータ・チューニングのために使うGridSearchCV『の』パラメータを弄り回すという本末転倒気味な目に遭います。そういうとき、どうしたら良いのかを、この記事で書きます。

 先に結論を言ってしまうと、本質的に計算コストの高い処理なので、劇的に速くすることは不可能です。それでも、ちょっとの工夫で2倍程度なら速くすることができます。その2倍で救われる人も結構いると思うので*2、単純なことですがまとめておきます。

 目次

スポンサーリンク


下準備とベースライン

 とりあえず、何も考えずに「GridSearchCVをデフォルトパラメタで使ってみた場合」の時間を測ります。

 そのためには適当なタスクを回してやる必要がありますが、今回はPCA+SVMでdigitsの分類でもやってみることにします。

 コードはこんな感じです。

import timeit
import pandas as pd
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
from sklearn.svm import SVC
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV

digits = load_digits()
svm = SVC()
pca = PCA(svd_solver="randomized")
pl = Pipeline([("pca", pca), ("svm", svm)])

params = {"pca__n_components":[10, 15, 30, 45],
          "svm__C":[1, 5, 10, 20], 
          "svm__gamma":[0.0001, 0.0005, 0.001, 0.01]}

def print_df(df):
    print(df[["param_pca__n_components",
              "param_svm__C", "param_svm__gamma", 
              "mean_score_time", 
              "mean_test_score"]])

def main1():
    clf = GridSearchCV(pl, params, n_jobs=-1)
    clf.fit(digits.data, digits.target)
    df = pd.DataFrame(clf.cv_results_)
    print_df(df)
    
if __name__ == "__main__":
    print(timeit.timeit(main1, number=1))

 色々なテクニックを使っているコードなので多少解説すると、とりあえずPipelineを使っています。また、GridSearchCV.cv_results_はそのままpandas.DataFrameに変換できる辞書として扱えることも利用しています。

www.haya-programming.com

 digits, svm, pca, pl, paramsの変数はmain関数の外でグローバル変数として作っていますが、これはあとでmain2とかmain3を作って使い回すための処置です。

 あと、速くするために必要と思われる常識的なこと(PCAのsvd_solver="randomized"とか、GridSearchCVのn_jobs=-1とか)はすでに実施しています。

 そんなことより本当にやりたいことは、この処理にどれだけ時間がかかるかを知ることです。そのために、timeitを使って時間を計測しています。

timeit --- 小さなコード断片の実行時間計測 — Python 3.7.4 ドキュメント

 さて、私の環境(しょぼいノートパソコン)ではこのプログラムの実行には42.2秒かかりました。

 これをベースラインにします。ここからどれだけ高速化できるかが今回のテーマです。

cvを指定する(効果:大)

 さて、GridSearchCVにはcvというパラメータがあります。default=3であり、この設定だと3分割交差検証になります。交差検証について理解していれば、特に不自然なところはないと思います。

 これを2にしてみます。交差検証できる最低の数字です。こうすると、

  • 交差検証のループ回数が3回→2回になり、それだけで1.5倍速くなる
  • チューニング対象のモデルの計算量が学習データサイズnに対してO(n)以上なら、それ(nが小さくなること)によるご利益もある。なお予測データサイズmに対する予測時間は普通O(m)なので、影響はない

 この相乗効果で、高速化が期待できます。

 この方法のデメリットは学習データを減らしたことで性能が低めになることですが、チューニングのときはパラメータの良し悪し(スコアの大小関係)がわかれば良いので、あまり問題になりません。とにかくやってみます。

def main2():
    clf = GridSearchCV(pl, params, cv=2, n_jobs=-1)
    clf.fit(digits.data, digits.target)
    df = pd.DataFrame(clf.cv_results_)
    print_df(df)

if __name__ == "__main__":
    # print(timeit.timeit(main1, number=1))
    print(timeit.timeit(main2, number=1))

 上のコードと重複する部分は削ってあります。見比べると、ほとんど変わっていないことが、おわかりいただけるかと思います。

 この処置で、処理時間は28.0秒に改善しました。ちょっといじっただけで、2/3くらいに改善してしまった訳です。そして「mean_test_score」はやはり全体的に低くなりましたが、傾向は同じでした。よってパラメータチューニングには使えます。

return_train_score=Falseする(効果:それなり)

 さて、GridSearchCVはデフォルトの設定ではreturn_train_score='warn'になっています。「'warn'って何さ?」というと、こんな警告を出します。

FutureWarning: You are accessing a training score ('std_train_score'), which will not be available by default any more in 0.21. If you need training scores, please set return_train_score=True

 return_train_scoreは要するに学習データに対するスコアを計算するかどうかを指定できる引数です。この警告は割とくだらないことが書いてあるのですが、将来的にはこれがdefault=Falseにされるという警告です。

 基本的に、パラメータチューニングで見たいのはテストデータに対するスコアであるはずです。なのに、現在のデフォルト設定では学習データに対する評価指標も計算されてしまいます。

 これは無駄なので、return_train_score=Falseすると学習データに対する評価指標の計算分の計算コストをケチれます。予測時間なんてたかが知れていますが、それでも一応やってみましょう。

def main3():
    clf = GridSearchCV(pl, params, cv=2,
                       return_train_score=False,
                       n_jobs=-1)
    clf.fit(digits.data, digits.target)
    df = pd.DataFrame(clf.cv_results_)
    print_df(df)
    
if __name__ == "__main__":
    # print(timeit.timeit(main1, number=1))
    # print(timeit.timeit(main2, number=1))
    print(timeit.timeit(main3, number=1))

 この措置によって、処理時間は22.1秒まで短縮されました。ベースラインと比較すると1/2強の時間で済んでいる訳です。

まとめ

 この記事では

  • cv=2にする
  • return_train_score=Falseにする

 という方法で、パラメータチューニングの機能を損なわないまま2倍弱の速度の改善を実現しました。

工夫なし cv=2 cv=2&return_train_score=False
42.2秒 28.0秒 22.1秒

 このテクニックはきっと皆さんの役に立つと思います。

それでも時間がかかりすぎるときは

 そもそもグリッドサーチしようとしているパラメータ候補が多すぎる可能性があります。

 たとえば、3つのパラメータでそれぞれ10個の候補を調べるとなると、10*10*10=1000回の交差検証が行われます。いつまで経っても終わらない訳です。

 今回の記事では4*4*4=64回としましたが、これでもけっこう多い方だと思います。それでも解こうとしている問題が単純なので、デフォルトパラメータでも1分以内には処理できていますが、ちょっと重いモデルにちょっと多量のデータを突っ込んだりすると、もうダメです。何十分とか何時間もかかってしまいます。

 そういう場合、まずは粗いステップ(少ないパラメータ候補数)でざっくりパラメータチューニングしてしまい、どの辺りのパラメータが良いのかわかったら、その周辺に絞ってもう一回パラメータチューニングを行います。こういうのを二段グリッドサーチと言ったりします。

 あるいはベイズ最適化とか、他のアルゴリズムに走るのも一つの手かもしれません。

 粗いグリッドである程度チューニングしてから、RandomizedSearchCVを使うというのもいい手だと思います。

www.haya-programming.com

*1:なにせすべての組み合わせを計算する

*2:たとえば「今から回して、朝までにデータを出さないと教授への報告が間に合わないんだ!」みたいな状況を想定しています