scikit-learn

【scikit-learn】グリッドサーチとランダムサーチでハイパーパラメータをチューニングする方法(GridSearchCV, RandomizedSearchCV)

【scikit-learn】グリッドサーチとランダムサーチでハイパーパラメータをチューニングする方法(GridSearchCV, RandomizedSearchCV)

Pythonの機械学習ライブラリであるscikit-learnでは、ハイパーパラメータをチューニングする方法としてグリッドサーチ(GridSearchCV)ランダムサーチ(RandomizedSearchCV)が用意されています。それぞれを使ったパラメータチューニングの方法について解説します。

ハイパーパラメータのチューニング

機械学習の分野では学習データから各種モデルの構築を行うのですが、適用するモデルの種類により、人手により調整しなければならないパラメータが存在します。そういったパラメータの事をハイパーパラメータと言います。

このハイパーパラメータの値によりモデルの精度が変化するため、適切なハイパーパラメータを見つけるためにハイパーパラメータを都度手作業で変えて学習、評価を繰り返す必要があり、非常に時間と手間がかかります。このような作業をハイパーパラメータチューニングと言います。

手作業でチューニングを行うのは大変なので、このハイパーパラメータを自動で計算機に見つけてもらうことを考えます。自動でのパラメータチューニングの主な方法としては、グリッドサーチランダムサーチの2つがあります。

Pythonの機械学習ライブラリであるscikit-learnでは、グリッドサーチはGridSearchCV、ランダムサーチはRandomizedSearchCVといった形でそれぞれの既に用意されています。

本記事では、グリッドサーチ、ランダムサーチそれぞれの方法の概要と違いをまず説明し、scikit-learnのGridSearchCV、RandomizedSearchCVの使用方法例について紹介していきたいと思います。

グリッドサーチ

グリッドサーチは、調整したいハイパーパラメータの値の候補を明示的に複数指定してハイパーパラメータのセットを作成します。例えば以下のような感じです。

  • パラメータ1:[1, 2, 3, 4, 5]
  • パラメータ2:[True, False]
  • パラメータ3:[0, 0.5, 1.0]

このように、事前に用意しておいたパラメータに対してグリッドサーチでは全ての組み合わせに対して学習と評価を繰り返し、最もよいパラメータを選びます。

上記の例だと、パラメータ1が5個、パラメータ2が2個、パラメータ3が3個の値を持っているので、5×2×3=30通りの組み合わせで評価されます。

この方法は、パラメータの値にある程度見当がついている場合に向いています。また、計算量が多くなるため大きなデータセットに対して適用することはあまりなく後述するランダムサーチの方が使われることが多いかと思います。

ランダムサーチ

ランダムサーチは、調整したいハイパーパラメータが取りうる値の範囲を指定します。範囲指定のパラメータでは、確率で決定されたパラメータを用いるので、グリッドサーチでは離散的な値ですが、ランダムサーチでは連続的な値をとることができるのが特徴です。

範囲指定をする場合には、値を選択するための確率関数を指定することになります。後ほどscikit-learnを用いたランダムサーチの例を紹介しますが、確率関数としてはscipy.statsのモジュールがよく使用されます。

Note

SciPyとは、Pythonのライブラリの一つで、科学技術計算に適したライブラリです。統計などの高度な数学的計算を簡単に実行することが可能になります。

グリッドサーチとランダムサーチの特徴の違い

グリッドサーチとランダムサーチの特徴の違いを簡単に整理しておきます。

手法特徴
グリッドサーチ・True or False等の連続ではない値の組み合わせの探索に向いている。
・調整する値に対してある程度見当がついている場合に向いている。
・モデルの訓練回数が増えるので計算コストが高い。
ランダムサーチ・連続な値に対しての探索にも対応できる。
・調整する値に見当がつかない場合でも範囲をとって探索ができる。
・ランダムに組み合わせを決めるのでグリッドサーチよりも計算コストは低い。
・確率的な探索のため適切なパラメータが見つけられない可能性がある。

scikit-learnでのグリッドサーチ(GridSearchCV)とランダムサーチ(RandomizedSearchCV)の使い方

グリッドサーチ(GridSearchCV)の使い方

実装例

scikit-learnのload_digits(手書き文字)データセットをSVMで分類する例を使って、グリッドサーチを実行するGridSearchCVの使い方を紹介します。以下の例は処理時間が結構かかるので注意してください。

import numpy as np
from sklearn.datasets import load_digits
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import train_test_split


def main():
    """メイン関数"""
    seed = 0
    np.random.seed(seed)

    # ===== データの生成
    data = load_digits()
    # 学習データとテストデータに分割
    train_data, test_data, train_label, test_label = train_test_split(
        data.data, data.target, random_state=seed
    )

    # ===== モデルのハイパーパラメータ候補値を設定する
    model_param_set = {
        SVC(): {
            "kernel": ["linear", "poly", "rbf", "sigmoid"],
            "C": [10**i for i in range(-5, 5)],
            "decision_function_shape": ["ovo", "ovr"],
            "random_state": [seed],
        }
    }

    # ===== グリッドサーチ
    max_score = 0
    best_param = None

    for model, param in model_param_set.items():
        # グリッドサーチを生成
        clf = GridSearchCV(model, param)
        # 訓練の実行
        clf.fit(train_data, train_label)
        # データの予測と正解率の計算
        pred = clf.predict(test_data)
        score = accuracy_score(test_label, pred)
        # 最高スコアの更新
        if max_score < score:
            max_score = score
            best_param = clf.best_params_

        # # パラメータの組み合わせを確認
        # print(clf.cv_results_["params"])

    print(f"ハイパーパラメータ: {best_param}")
    print(f"最高スコア: {max_score}")


if __name__ == "__main__":
    main()
【実行結果】
ハイパーパラメータ: {'C': 10, 'decision_function_shape': 'ovo', 'kernel': 'rbf', 'random_state': 0}
最高スコア: 0.9911111111111112

実装内容の解説

上記で紹介した実装例の各部分ごとに内容を解説していきます。

必要モジュールのインポート

from sklearn.model_selection import GridSearchCV

グリッドサーチを使用するには、sklearn.model_selectionからGridSearchCVをインポートします。他にもnumpy等インポートしていますがそちらの説明は省略します。

データセットの準備

    # ===== データの生成
    data = load_digits()
    # 学習データとテストデータに分割
    train_data, test_data, train_label, test_label = train_test_split(
        data.data, data.target, random_state=seed
    )

データは、load_digits()により読み込みます。学習データとテスト用のデータを分割するために、train_test_splitを使用しています。

ハイパーパラメータセットの設定

    # ===== モデルのハイパーパラメータ候補値を設定する
    model_param_set = {
        SVC(): {
            "kernel": ["linear", "poly", "rbf", "sigmoid"],
            "C": [10**i for i in range(-5, 5)],
            "decision_function_shape": ["ovo", "ovr"],
            "random_state": [seed],
        }
    }

モデルとパラメーターの組み合わせを指定するための辞書を準備しています。モデルのクラスインスタンスをキーにして、パラメータのキーとリストを持つ辞書を値として指定しています。

今回は、SVMのモデルであるSVC()を指定しています。パラメータとしては、kernel, C等の値をリストで指定している形です。

グリッドサーチの実行

    # ===== グリッドサーチ
    max_score = 0
    best_param = None

    for model, param in model_param_set.items():
        # グリッドサーチを生成
        clf = GridSearchCV(model, param)
        # 訓練の実行
        clf.fit(train_data, train_label)
        # データの予測と正解率の計算
        pred = clf.predict(test_data)
        score = accuracy_score(test_label, pred)
        # 最高スコアの更新
        if max_score < score:
            max_score = score
            best_param = clf.best_params_

        # # パラメータの組み合わせを確認
        # print(clf.cv_results_['params'])

    print(f"ハイパーパラメータ: {best_param}")
    print(f"最高スコア: {max_score}")

上記の部分でグリッドサーチを実行しています。先ほど指定したパラメータセットから順にモデル・パラメータを取り出します。

グリッドサーチを実行するには、GridSearchCVに対象とするモデルとパラメータ辞書をGridSearchCV(model, param)のように指定します。後は、scikit-learnの訓練のメソッドであるfitを実行すると、paramに指定されているパラメータセットを利用してベストなパラメータの探索を行いつつ、学習を行います。

predictメソッドは、GridSearchCVが見つけたベストパラメータを使って予測をします。また、その予測の正解率(accuracy)をaccuracy_scoreを利用して計算し、scoreに格納しています。

max_scoreを更新する部分がありますが、ここは後述する複数モデルの比較にそのまま使えることを意識して作成しています。(上記例ではモデルはSVC()の一つなのでfor文は1回しかループしません)

【補足:パラメータの組み合わせの確認】

今回実際にどのようなパラメータセットで評価がされているのかを確認してみましょう。実行時の情報などはcv_results_の中に入っています。以下の部分をコメントアウトしていますが、コメントアウトを外して実行してみてください。

        # # パラメータの組み合わせを確認
        # print(clf.cv_results_["params"])

実行すると以下のような辞書のリストが取得できます。

[
{'C': 1e-05, 'decision_function_shape': 'ovo', 'kernel': 'linear', 'random_state': 0},
{'C': 1e-05, 'decision_function_shape': 'ovo', 'kernel': 'poly', 'random_state': 0},
{'C': 1e-05, 'decision_function_shape': 'ovo', 'kernel': 'rbf', 'random_state': 0},
{'C': 1e-05, 'decision_function_shape': 'ovo', 'kernel': 'sigmoid', 'random_state': 0},
{'C': 1e-05, 'decision_function_shape': 'ovr', 'kernel': 'linear', 'random_state': 0},
{'C': 1e-05, 'decision_function_shape': 'ovr', 'kernel': 'poly', 'random_state': 0},
{'C': 1e-05, 'decision_function_shape': 'ovr', 'kernel': 'rbf', 'random_state': 0}, 
{'C': 1e-05, 'decision_function_shape': 'ovr', 'kernel': 'sigmoid', 'random_state': 0}, 
{'C': 0.0001, 'decision_function_shape': 'ovo', 'kernel': 'linear', 'random_state': 0}, 
{'C': 0.0001, 'decision_function_shape': 'ovo', 'kernel': 'poly', 'random_state': 0}, 
・・・途中略・・・
{'C': 1000, 'decision_function_shape': 'ovr', 'kernel': 'rbf', 'random_state': 0}, 
{'C': 1000, 'decision_function_shape': 'ovr', 'kernel': 'sigmoid', 'random_state': 0}, 
{'C': 10000, 'decision_function_shape': 'ovo', 'kernel': 'linear', 'random_state': 0}, 
{'C': 10000, 'decision_function_shape': 'ovo', 'kernel': 'poly', 'random_state': 0}, 
{'C': 10000, 'decision_function_shape': 'ovo', 'kernel': 'rbf', 'random_state': 0}, 
{'C': 10000, 'decision_function_shape': 'ovo', 'kernel': 'sigmoid', 'random_state': 0}, 
{'C': 10000, 'decision_function_shape': 'ovr', 'kernel': 'linear', 'random_state': 0}, 
{'C': 10000, 'decision_function_shape': 'ovr', 'kernel': 'poly', 'random_state': 0}, 
{'C': 10000, 'decision_function_shape': 'ovr', 'kernel': 'rbf', 'random_state': 0}, 
{'C': 10000, 'decision_function_shape': 'ovr', 'kernel': 'sigmoid', 'random_state': 0}
]

今回は、’kernel’が4個、’C’が10個、’decision_function_shape’が2個、random_stateが1個のパラメータ数なので、4×10×2×1=80通りのパラメータの組み合わせが使われます。このようにグリッドサーチでは全ての指定したパラメータの組み合わせが使用されることになるので、実行時間も長くなります。

cv_results_は辞書になっていて、今回はparamキーの値を取り出しました。他にも色々と結果情報がありますので、公式ドキュメントを確認しつつ結果の中身を見てみると面白いかと思います。

Note

sklearn.model_selection.GridSearchCVの公式ドキュメントはこちらを参照してください。

ランダムサーチ(RandomizedSearchCV)の使い方

実装例

次は、同様にload_digits(手書き文字)データセットをSVMで分類する例を使って、ランダムサーチを実行するRandomizedSearchCVの使い方を紹介します。

import numpy as np
import scipy.stats
from sklearn.datasets import load_digits
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
from sklearn.model_selection import RandomizedSearchCV
from sklearn.model_selection import train_test_split


def main():
    """メイン関数"""
    seed = 0
    np.random.seed(seed)

    # ===== データの生成
    data = load_digits()
    # 学習データとテストデータに分割
    train_data, test_data, train_label, test_label = train_test_split(
        data.data, data.target, random_state=seed
    )

    # ===== モデルのハイパーパラメータ候補値を設定する
    model_param_set = {
        SVC(): {
            "kernel": ["linear", "poly", "rbf", "sigmoid"],
            "C": scipy.stats.uniform(0.00001, 10000),
            "decision_function_shape": ["ovo", "ovr"],
            "random_state": [seed],
        }
    }

    # ===== ランダムサーチ
    max_score = 0
    best_param = None

    for model, param in model_param_set.items():
        # ランダムサーチを生成
        clf = RandomizedSearchCV(model, param)
        # 訓練の実行
        clf.fit(train_data, train_label)
        # データの予測と正解率の計算
        pred = clf.predict(test_data)
        score = accuracy_score(test_label, pred)
        # 最高スコアの更新
        if max_score < score:
            max_score = score
            best_param = clf.best_params_

        # # パラメータの組み合わせを確認
        # print(clf.cv_results_["params"])

    print(f"ハイパーパラメータ: {best_param}")
    print(f"最高スコア: {max_score}")


if __name__ == "__main__":
    main()
【実行結果】
ハイパーパラメータ: {'C': 4236.548003389047, 'decision_function_shape': 'ovr', 'kernel': 'rbf', 'random_state': 0}
最高スコア: 0.9911111111111112

実装内容の解説

上記で紹介した実装例の各部分ごとに内容を解説していきます。グリッドサーチと重複する部分の説明については省略します。

上記で紹介した実装例の各部分ごとに内容を紹介していきます。

必要モジュールのインポート

from sklearn.model_selection import RandomizedSearchCV

ランダムサーチを使用するには、sklearn.model_selectionからRandomizedSearchCVをインポートします。

ハイパーパラメータセットの設定

    # ===== モデルのハイパーパラメータ候補値を設定する
    model_param_set = {
        SVC(): {
            "kernel": ["linear", "poly", "rbf", "sigmoid"],
            "C": scipy.stats.uniform(0.00001, 10000),
            "decision_function_shape": ["ovo", "ovr"],
            "random_state": [seed],
        }
    }

ハイパーパラメータを設定している部分で、グリッドサーチと少し異なっているのはCの指定の部分です。

ここでは、scipy.statsのuniformを使って0.00001~10000の範囲での一様分布を指定しています。このように確率分布を指定することで、RandomizedSearchCVは、分布に従うパラメータをサンプルしてくれます。もちろん他の確率分布を指定することも可能です。

ランダムサーチの実行

    # ===== ランダムサーチ
    max_score = 0
    best_param = None

    for model, param in model_param_set.items():
        # ランダムサーチを生成
        clf = RandomizedSearchCV(model, param)
        # 訓練の実行
        clf.fit(train_data, train_label)
        # データの予測と正解率の計算
        pred = clf.predict(test_data)
        score = accuracy_score(test_label, pred)
        # 最高スコアの更新
        if max_score < score:
            max_score = score
            best_param = clf.best_params_

        # # パラメータの組み合わせを確認
        # print(clf.cv_results_["params"])

    print(f"ハイパーパラメータ: {best_param}")
    print(f"最高スコア: {max_score}")

RandomizedSearchCVを使っているという点以外は、ランダムサーチの例と同じです。

【補足:パラメータの組み合わせの確認】

実行してみるとグリッドサーチに比べてすごく早いことに気づくと思います。ランダムサーチで確認したように比較したパラメータを表示してみましょう。以下のコメントアウトされている部分を外して実行してみてください。

        # # パラメータの組み合わせを確認
        # print(clf.cv_results_["params"])

以下のようなパラメータの組み合わせが表示されるかと思います。

[
{'C': 5488.135049273247, 'decision_function_shape': 'ovr', 'kernel': 'linear', 'random_state': 0}, 
{'C': 6027.633770716438, 'decision_function_shape': 'ovr', 'kernel': 'sigmoid', 'random_state': 0}, 
{'C': 4236.548003389047, 'decision_function_shape': 'ovr', 'kernel': 'rbf', 'random_state': 0}, 
{'C': 4375.8721226269245, 'decision_function_shape': 'ovo', 'kernel': 'linear', 'random_state': 0}, 
{'C': 9636.627615010293, 'decision_function_shape': 'ovo', 'kernel': 'poly', 'random_state': 0}, 
{'C': 7917.250390826645, 'decision_function_shape': 'ovr', 'kernel': 'rbf', 'random_state': 0}, 
{'C': 5680.445620939323, 'decision_function_shape': 'ovr', 'kernel': 'poly', 'random_state': 0}, 
{'C': 710.3605919788694, 'decision_function_shape': 'ovr', 'kernel': 'linear', 'random_state': 0}, 
{'C': 202.1839844032572, 'decision_function_shape': 'ovr', 'kernel': 'poly', 'random_state': 0}, 
{'C': 7781.567519498504, 'decision_function_shape': 'ovr', 'kernel': 'linear', 'random_state': 0}
]

組み合わせの種類としては10通り出てくるのではないかと思います。この10というのは、RandomizedSearchCVのn_iter引数のデフォルトで指定されている数字です。公式ドキュメントの引数を確認してみてください。

もし、試す組み合わせの数を増減させる場合には、以下のようn_iterを明示的に指定してRandomizedSearchCVをインスタンス化してください。

        # ランダムサーチを生成
        clf = RandomizedSearchCV(model, param, n_iter=20)

Note

sklearn.model_selection.RandomizedSearchCVの公式ドキュメントはこちらを参照してください。

複数モデルを比較する方法

上記の例では、グリッドサーチ、ランダムサーチのイメージをつかんでもらうために一つのモデルに対してパラメータを変えて実行してきましたが、複数のモデルとパラメータを試して最もよいモデルとパラメータを探索することもできます。ここでは、その実装例を紹介します。

今回は、処理時間を考慮してランダムサーチの例ですが、グリッドサーチでも同じように実装できます。

実装例

以下の実装例は、ランダムサーチを使って、非線形SVM、決定木、ランダムフォレストを使用する例です。

import numpy as np
import scipy.stats
from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import RandomizedSearchCV
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier


def main():
    """メイン関数"""
    seed = 0
    np.random.seed(seed)

    # ===== データの生成
    data = load_digits()
    # 学習データとテストデータに分割
    train_data, test_data, train_label, test_label = train_test_split(
        data.data, data.target, random_state=seed
    )

    # ===== モデルのハイパーパラメータ候補情報
    model_param_set = {
        # 非線形SVM
        SVC(): {
            "C": scipy.stats.uniform(0, 100000),
            "kernel": ["rbf", "poly"],
            "decision_function_shape": ["ovo", "ovr"],
            "random_state": [seed],
        },
        # 決定木
        DecisionTreeClassifier(): {
            "max_depth": [i for i in range(1, 21)],
            "random_state": [seed],
        },
        # ランダムフォレスト
        RandomForestClassifier(): {
            "n_estimators": [i for i in range(1, 51)],
            "max_depth": [i for i in range(1, 21)],
            "random_state": [seed],
        },
    }

    # ===== ランダムサーチで最適モデルを調べる
    max_score = 0
    best_method = None
    best_param = None

    for model, param in model_param_set.items():
        # print(model, param)
        method = model.__class__.__name__
        # ランダムサーチを生成
        clf = RandomizedSearchCV(model, param)
        # 訓練の実行
        clf.fit(train_data, train_label)
        # データの予測と正解率の計算
        pred = clf.predict(test_data)
        score = accuracy_score(test_label, pred)
        # 最高スコアの更新
        if max_score < score:
            max_score = score
            best_method = method
            best_param = clf.best_params_

        # # パラメータの組み合わせを確認
        # print(clf.cv_results_["params"])

    print("==========")
    print(f"学習モデル: {best_method}")
    print(f"パラメーター: {best_param}")
    print(f"最高スコア: {max_score}")
    print("==========")


if __name__ == "__main__":
    main()
【実行結果】
==========
学習モデル: SVC
パラメーター: {'C': 54881.35039273248, 'decision_function_shape': 'ovr', 'kernel': 'rbf', 'random_state': 0}
最高スコア: 0.9911111111111112
==========

実装内容の解説

上記で紹介した実装例の各部分ごとに内容を解説していきます。これまでの説明と重複する部分については省略します。

ハイパーパラメータセットの設定

    # ===== モデルのハイパーパラメータ候補情報
    model_param_set = {
        # 非線形SVM
        SVC(): {
            "C": scipy.stats.uniform(0, 100000),
            "kernel": ["rbf", "poly"],
            "decision_function_shape": ["ovo", "ovr"],
            "random_state": [seed],
        },
        # 決定木
        DecisionTreeClassifier(): {
            "max_depth": [i for i in range(1, 21)],
            "random_state": [seed],
        },
        # ランダムフォレスト
        RandomForestClassifier(): {
            "n_estimators": [i for i in range(1, 51)],
            "max_depth": [i for i in range(1, 21)],
            "random_state": [seed],
        },
    }

パラメータの辞書を用意するときに、複数のモデルインスタンスをキーに設定します。今回の例では、SVC()、DecisionTreeClassifier()、RandomForestClassifier()をキーとして設定し、それぞれのパラメータ辞書を値としてセットしています。

今回は3つのモデルですが、ロジスティック回帰やK近傍法等他のモデルを同じように追加していけば比較の対象を増やしていくことができます。

ランダムサーチの実行

    # ===== ランダムサーチで最適モデルを調べる
    max_score = 0
    best_method = None
    best_param = None

    for model, param in model_param_set.items():
        # print(model, param)
        method = model.__class__.__name__
        # ランダムサーチを生成
        clf = RandomizedSearchCV(model, param)
        # 訓練の実行
        clf.fit(train_data, train_label)
        # データの予測と正解率の計算
        pred = clf.predict(test_data)
        score = accuracy_score(test_label, pred)
        # 最高スコアの更新
        if max_score < score:
            max_score = score
            best_method = method
            best_param = clf.best_params_

        # # パラメータの組み合わせを確認
        # print(clf.cv_results_["params"])

    print("==========")
    print(f"学習モデル: {best_method}")
    print(f"パラメーター: {best_param}")
    print(f"最高スコア: {max_score}")
    print("==========")

上記の部分については、後でベストだったモデル名を取得できるようにmethodに実行モデル名を格納している以外はこれまでのソースと同じです。

今回の結果としては非線形SVMが最もよいモデルであるということが分かりました。

このように、グリッドサーチやランダムサーチをうまく使うと複数モデルに対して各パラメータの探索をしつつモデル評価をすることができ、非常に便利です。是非グリッドサーチ、ランダムサーチの使い方を覚えてみてください。

まとめ

機械学習のハイパーパラメータチューニングを自動で行う方法としてグリッドサーチランダムサーチという方法について概要を説明しました。

また、Pythonの機械学習ライブラリであるscikit-learnで自動でハイパーパラメータチューニングができるGridSearchCVRandomizedSearchCVの使い方を紹介しました。

グリッドサーチは、ハイパーパラメータにある程度見当がついているときやパラメータ数が少ない場合には適していますが、計算コストが高くなる特徴があります。

一方、ランダムサーチは値の範囲を指定して確率的に値をとるため、値に見当がついていない場合など範囲をとって確認ができ、計算コストもグリッドサーチよりは少なくなります。ただし、確率的に探索するため、運任せな部分があり適切なパラメータを見つけられない可能性もあります。

適用するモデルのハイパーパラメータの種類、データセットのサイズ、計算機の性能などを考慮して、どちらを使うかを検討してもらうとよいかと思います。