はじめに
RandomizedSearchCVなるものがあるということを知ったので、使ってみます。うまく使うとグリッドサーチよりよい結果を生むかもしれないということです。
sklearn.model_selection.RandomizedSearchCV — scikit-learn 0.21.3 documentation
グリッドサーチでは最初に探索するパラメータの空間を決め打ちにしますが、Randomized Searchではパラメータを確率分布に基づいて決定します。
比較実験
とりあえず、先に使い慣れたグリッドサーチでやってみます。digitsデータをSVMで分類するという、100回くらい見た気がするネタです。
コード
import time import pandas as pd from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV from sklearn.datasets import load_digits def main(): digits = load_digits() svm = SVC() params = {"C":[0.1, 1, 10], "gamma":[0.001, 0.01, 0.1]} clf = GridSearchCV(svm, params, cv=5, iid=True, return_train_score=False) t1 = time.time() clf.fit(digits.data, digits.target) t2 = time.time() print("{:.2f} 秒かかった".format(t2 - t1)) result_df = pd.DataFrame(clf.cv_results_) result_df.sort_values( by="rank_test_score", inplace=True) print(result_df[["rank_test_score", "params", "mean_test_score"]]) if __name__ == "__main__": main()
結果
16.67 秒かかった rank_test_score params mean_test_score 6 1 {'C': 10, 'gamma': 0.001} 0.972732 3 2 {'C': 1, 'gamma': 0.001} 0.971619 0 3 {'C': 0.1, 'gamma': 0.001} 0.943239 7 4 {'C': 10, 'gamma': 0.01} 0.705620 4 5 {'C': 1, 'gamma': 0.01} 0.695047 1 6 {'C': 0.1, 'gamma': 0.01} 0.111297 5 7 {'C': 1, 'gamma': 0.1} 0.104062 8 7 {'C': 10, 'gamma': 0.1} 0.104062 2 9 {'C': 0.1, 'gamma': 0.1} 0.102393
まあ、これはこんなもんでしょう。しっかり最適パラメータを探せている気がします。3*3=9通りのグリッドサーチで、計算には16.67秒かかったとのことです。
RandomizedSearchCVにしてみる
RandomizedSearchCVの特色は、scipyで作れる確率分布のオブジェクトを渡せることです。パラメータのリストを渡すことも可能ですが、それだと特色を活かした使い方にはなりません。
scipyで確率分布のオブジェクトを作る方法については、以前の記事で説明したのでこちらを見てください。
scipyで確率分布のサンプルと確率密度関数を生成する - 静かなる名辞
ここで期待されている「確率分布のオブジェクト」はrvs(分布に従うサンプルを得るメソッド)さえ使えれば何でも良いです。連続分布も離散分布も渡せます。なんなら自作でも行けると思います。
といってみても、パラメータ設定を確率分布に落とし込むまでは少し頭の体操が要ると思います。
次のように考えます。たとえば、SVMでパラメータチューニングするなら、とりあえずこんな感じでざっくり見る人が多いのではないかと思います。
[0.001, 0.01, 0.1, 1, 10, 100]
ここで、0.001が出る頻度に対して0.002はそこそこ出てきてほしいけど、100の頻度に対して100.001は出てきてほしくなくて200とかになってほしい訳です。要するに、上の方になるほど出てくる確率が下がる分布がほしい訳で、こういうのは指数分布が向いている気がします。ということを踏まえて、scipy.stats.exponを使うことにします。
scipy.stats.expon — SciPy v1.3.0 Reference Guide
scipyのこの辺のドキュメントの説明はお世辞にもわかりやすいとは言えないんですが、指数分布のパラメータは基本的に一つだけであり、ドキュメントの記述によると
A common parameterization for expon is in terms of the rate parameter lambda, such that pdf = lambda * exp(-lambda * x). This parameterization corresponds to using scale = 1 / lambda.
ということらしいので、これに従ってを設定します。指数分布の期待値は
なので、要するに平均にしたいあたりをscaleに指定すればいいことになります。
ちょっと確認してみます。
>>> import matplotlib.pyplot as plt >>> result = stats.expon.rvs(scale=0.1, size=1000) >>> plt.figure() <matplotlib.figure.Figure object at 0x7fe9e5e4b6a0> >>> plt.hist(result) (array([639., 222., 76., 44., 11., 3., 3., 1., 0., 1.]), array([7.33160559e-07, 9.56411007e-02, 1.91281468e-01, 2.86921836e-01, 3.82562203e-01, 4.78202571e-01, 5.73842938e-01, 6.69483306e-01, 7.65123673e-01, 8.60764041e-01, 9.56404408e-01]), <a list of 10 Patch objects>) >>> plt.savefig("result1.png") >>> result = stats.expon.rvs(scale=1, size=1000) >>> plt.figure() <matplotlib.figure.Figure object at 0x7fe9e5e1bba8> >>> plt.hist(result) (array([490., 246., 133., 66., 30., 23., 2., 6., 3., 1.]), array([2.21631197e-04, 6.36283260e-01, 1.27234489e+00, 1.90840652e+00, 2.54446814e+00, 3.18052977e+00, 3.81659140e+00, 4.45265303e+00, 5.08871466e+00, 5.72477629e+00, 6.36083791e+00]), <a list of 10 Patch objects>) >>> plt.savefig("result2.png")
上手く行っているようですが、指数分布の場合は分散がで要するに期待値の2乗なので、場合によってはいわゆる中央値より少し大きめにしたり、逆に小さめにしたいと思うこともあるかもしれません。
というあたりを踏まえて、いよいよRandomizedSearchCVでやってみます。基本的な使い方はコードを見れば分かる通りで、paramsの値にscipyの確率分布を渡すこと、n_iterで試す回数を指定できること以外大きな違いはありません。n_iterですが、今回は30回やってみます。
コード
import time import pandas as pd from scipy import stats from sklearn.svm import SVC from sklearn.model_selection import RandomizedSearchCV from sklearn.datasets import load_digits def main(): digits = load_digits() svm = SVC() params = {"C":stats.expon(scale=1), "gamma":stats.expon(scale=0.01)} clf = RandomizedSearchCV(svm, params, cv=5, iid=True, return_train_score=False, n_iter=30) t1 = time.time() clf.fit(digits.data, digits.target) t2 = time.time() print("{:.2f}秒かかった".format(t2 - t1)) result_df = pd.DataFrame(clf.cv_results_) result_df.sort_values( by="rank_test_score", inplace=True) print(result_df[["rank_test_score", "param_C", "param_gamma", "mean_test_score"]]) if __name__ == "__main__": main()
大きな相違点は、
params = {"C":stats.expon(scale=1), "gamma":stats.expon(scale=0.01)}
のところで、見てわかるようにscipyの確率分布オブジェクトを渡しています。
結果
63.04秒かかった rank_test_score param_C param_gamma mean_test_score 23 1 2.62231 0.000465122 0.972732 15 2 1.85208 0.00195101 0.967168 14 3 0.716893 0.00232262 0.963829 26 4 0.633723 0.000583885 0.960490 20 5 0.534343 0.00280065 0.952699 24 6 1.33912 0.00344179 0.949360 2 7 4.06141 0.00355782 0.947691 21 8 0.460892 0.00307683 0.945465 19 9 1.78068 0.00481671 0.912632 10 10 3.26205 0.00679844 0.817474 29 11 0.288305 0.00525321 0.738453 1 12 1.01437 0.00874084 0.731219 3 13 0.385384 0.00590435 0.725097 5 14 0.61161 0.00702304 0.701169 12 15 0.317451 0.00597252 0.626600 6 16 0.122253 0.00444626 0.521425 4 17 1.1791 0.0187758 0.373400 28 18 0.960299 0.0174957 0.277685 7 19 1.91593 0.0219393 0.272120 18 20 1.17152 0.0264625 0.209794 13 21 1.48332 0.0297287 0.180301 22 22 0.496221 0.0110038 0.178631 11 23 0.747978 0.0178759 0.130217 17 24 0.549571 0.0147944 0.123539 0 25 0.0357021 0.00627669 0.116305 25 26 0.0551073 0.00739485 0.114079 27 27 0.0852409 0.00982068 0.111297 9 27 0.148459 0.0101576 0.111297 16 29 0.818395 0.0521121 0.102393 8 29 0.00447628 0.0442941 0.102393
最良スコアそのものは実はGridSearchCVと(たまたま)同じですし、時間がケチれるということもないのですが、見ての通りある程度アタリのついている状態でうまい分布を指定してあげれば「最良パラメータの周囲を細かく探索する」といったカスタマイズが可能になります。
とりあえずこれでどの辺りがいいのかは大体わかったので、今度はその辺りを平均にしてやってみます。2行、次のように書き換えただけです。
改変箇所
params = {"C":stats.expon(scale=3), "gamma":stats.expon(scale=0.001)}
結果
22.06秒かかった rank_test_score param_C param_gamma mean_test_score 28 1 4.17858 0.000512401 0.974402 3 2 3.64468 0.000429315 0.973845 14 3 6.71633 0.000842716 0.973289 22 3 2.69443 0.00116543 0.973289 6 3 5.40707 0.000698026 0.973289 13 3 2.80969 0.00118559 0.973289 1 7 3.00941 0.00135159 0.972732 11 7 2.10982 0.000993251 0.972732 12 7 3.73687 0.00112881 0.972732 2 10 1.18243 0.00144449 0.971063 24 11 1.86039 0.00167388 0.970506 16 11 1.25451 0.00150173 0.970506 10 13 2.45206 0.000338623 0.968837 0 14 0.827324 0.00171363 0.967724 25 15 0.651184 0.00175566 0.965498 29 16 1.24774 0.000424962 0.964385 20 17 0.625333 0.000705793 0.963829 15 18 7.14503 0.000158783 0.962716 27 19 0.461709 0.00106818 0.962159 5 20 2.82219 0.00280609 0.961046 4 21 1.47712 0.000229419 0.959377 7 22 0.536423 0.0005259 0.958264 8 23 2.39408 9.90463e-05 0.953812 18 24 3.53976 6.63105e-05 0.953255 21 24 3.46599 6.85291e-05 0.953255 23 26 3.43756 5.60631e-05 0.952142 26 27 12.188 2.79396e-05 0.951586 9 28 0.331784 0.00038362 0.951029 17 29 0.73821 4.9347e-05 0.930440 19 30 0.000110158 0.000966082 0.141347
今度はGridSearchCVのときより良いスコアになるパラメータがいくつか出ました。まあ、digitsの大して多くもないデータ数で0.001の差を云々しても「誤差じゃね?」って感じですが・・・
二段グリッドサーチでやっても同じことはできますが、グリッドを手打ちで入力したりといった手間がかかります。その点、RandomizedSearchCVは分布のパラメータを打ち込めば勝手にやってくれるので、大変助かるものがあります。
結論
使えます。特に、グリッドサーチのグリッドを手打ちで入れるのが面倒くさいという人に向いています。ただし、どういう分布が良いのかは知識として持っていないといけません。まあ、わからなければ一様分布でアタリをつけて正規分布にして・・・とかでもなんとかなるでしょう。
ただし、それなりに工夫する要素があるので、玄人向きです。kaggleで使われたりするのも納得がいきます。また、確率である以上ある程度の回数は回さないと安定しないので、そのへんにも注意した方が良いと思います(それでも数が増えてくるとグリッドサーチできめ細かくやるより全然マシですが・・・)。