はじめに
機械学習でパラメータ・チューニングをしたい場合、グリッドサーチを行うのが定石とされています。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に変換できる辞書として扱えることも利用しています。
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を使うというのもいい手だと思います。