ハイパーパラメータ探索の基本的な手法#

本項では、機械学習におけるハイパーパラメータ探索の基本的な手法であるgrid searchとrandom searchについて解説します。機械学習におけるハイパーパラメータの定義から始め、また機械学習ライブラリscikit-learnにおけるハイパーパラメータ関連の実装についても簡単に触れます。

import warnings

warnings.filterwarnings("ignore")
import numpy as np


np.random.seed(42)

機械学習におけるハイパーパラメータ#

機械学習の文脈において、ハイパーパラメータ (hyperparameter) とは特定のアルゴリズムで特定のモデルの重み(パラメータ)の学習を開始する前に定めておく必要のあるパラメータのことを指します。個々の学習プロセスにおけるハイパーパラメータは学習に利用するモデル、アルゴリズム、あるいは解析全体のパイプラインに応じて異なりますが、典型的には以下のようなパラメータがハイパーパラメータとして扱われます。

モデルの構造に関するパラメータ#

モデルの構造に関するパラメータは、利用するアルゴリズムを通じて学習されない場合はハイパーパラメータとして扱われます。例えば、単変量の多項式回帰モデル

\[ f_\text{poly} ( x ; ( w_k )_{p=0}^d ) = w_0 + \sum_{p=1}^d w_p x^p \]

を通常の最小二乗法 (OLS) で学習する場合、モデルの最大次数 \(d\) はハイパーパラメータとなります。また、三層パーセプトロン (three-layer perceptron, TLP) を用いた単変量の回帰モデル

\[ f_\text{TLP} ( x ; W^{(0)}, W^{(1)}) = w_0^{(1)} + \sum_{k=1}^K w_k^{(1)} \sigma \left( w_{0, k}^{(0)} + w_{1, k}^{(0)} x \right) \]

(ただし \(W^{(0)} = (w_{i, k}^{(0)})_{i, k}\) および \(W^{(1)} = (w_k^{(1)})_k\) とします)を確率的勾配降下法 (SGD) で学習する場合、中間層のユニット数 \(K\) や中間層の活性化関数 \(\sigma\) はハイパーパラメータとなります。

さらに、利用するモデルの種類を予め決めていない場合には、これら多項式回帰や三層パーセプトロン回帰といったモデルの種類そのものをハイパーパラメータとして考えることもできます。即ち、上記の例では関数 \(f_m\) のラベル \(m \in \{ \text{poly}, \text{TLP} \}\) 自体をハイパーパラメータと見なします。この場合、多項式回帰の最大次数 \(d\)\(m = \text{poly}\) の場合のみ、三層パーセプトロンの中間層のユニット数 \(K\) や活性化関数 \(\sigma\)\(m = \text{TLP}\) の場合のみそれぞれ存在する(あるいは、意味を持つ)ハイパーパラメータとなります。

ハイパーパラメータ間の階層性は一般的な性質で、モデルの種類の選択のみに関わる話ではありません。例えばサポートベクター回帰 (SVR) などのカーネル法では候補となるカーネル関数ごとに固有のパラメータ(例えば多項式カーネルの最大次数など)が存在しますし、一般の多層パーセプトロンでは中間層の層数というハイパーパラメータに応じて各中間層のユニット数というハイパーパラメータの個数が異なります。

正則化パラメータ#

線形回帰やニューラルネットワークなどのモデルで重みの正則化を行う場合、正則化項の乗数は(データから適応的に決定されるのでなければ)ハイパーパラメータとして扱われます。例えば重み \(w\) に対する \(L_2\)-正則化

\[ L ( y, f(x ; w ) ) + \lambda_2 \| w \|_2^2 \]

における乗数 \(\lambda_2\) や、 \(L_1\)-正則化

\[ L ( y, f(x ; w ) ) + \lambda_1 \| w \|_1 \]

における乗数 \(\lambda_1\) がそれにあたります。ただし \(L\) は適当な損失関数とします。

学習アルゴリズムのパラメータ#

機械学習で用いられる学習アルゴリズム(多くの場合は反復法による最適化アルゴリズム)にパラメータがある場合、それらも一般にハイパーパラメータとして扱われます。例えばモメンタムつきのミニバッチ勾配効果法

\[\begin{split} \begin{array}{rl} \Delta w^{(n + 1)} & = \eta \left( \frac{1}{B} \sum_{i \in \mathcal{B}^{(n)}} \nabla C_i ( w^{(n)} ) \right) + \gamma \Delta w^{(n)} \\ w^{(n + 1)} & = w^{(n)} - \Delta w^{(n + 1)} \end{array} \end{split}\]

において、学習率 \(\eta > 0\) やモメンタムの係数 \(\gamma > 0\) 、またミニバッチサイズ \(B \equiv | \mathcal{B}^{(n)} |\) は一般にハイパーパラメータとして扱われます。なお、形式的には重みの初期値 \(w^{(0)}\) や個々のミニバッチ \(\mathcal{B}^{(n)}\) の具体的な値(実用上はそれぞれ対応するランダムシード)をハイパーパラメータと思うことも可能ですが、ランダム性の喪失による過学習の懸念などの理由もあり、通常は積極的に探索されることがありません。

データの前処理に関するパラメータ#

入力データに対して施す各種の前処理についても、場合によっては、予め指定するパラメータがある場合はそれを(解析全体のパイプラインにおける)ハイパーパラメータとして捉えた方がよいかもしれません。例えば前処理の中で主成分分析 (PCA) を用いて

\[ g ( x ; V ) = x V \]

(ただし \(V \in \mathbf{R}^{n \times p}\) とします)のように \(n\) 次元のデータ \(x \in \mathbf{R}^{1 \times n}\)\(p\) 次元に圧縮している場合、この \(p\) を直接指定しているのであればハイパーパラメータとして捉えるのが適当です。

いま、入力 \(x \in \mathbf{R}^{1 \times n}\) を前処理 \(g ( \cdot ; v )\)\(z = g ( x ; v ) \in \mathbf{R}^{1 \times p}\) に変換し、この \(z\) に対して関数 \(f ( \cdot ; w )\) を用いた回帰モデル \(y = f ( z ; w ) + \epsilon\) を当てはめるようなパイプラインを考えます( \(\epsilon\) は適当な雑音)。このとき \(y = f ( g ( x ; v ) ; w ) + \epsilon\) と書けるので、このパイプライン全体は関数 \(h ( \cdot ; v, w ) = f ( g ( \cdot ; v ) ; w )\) を用いた回帰モデルと思うことができます。即ち、前処理 \(g\) はこの回帰モデルの一部を取り出したものと思うことができ、 \(g\) のパラメータ \(v\) を学習することは \(h\) のパラメータ \((v, w)\) の一部 \(v\) を入力データから(残りの \(w\) とは別の方法で)学習しているものと思うことができるのです。

Scikit-learnでの実装例#

機械学習ライブラリのscikit-learnにおいて、これらハイパーパラメータは当該のモデルや前処理のクラスを初期化するときに与える引数として実装されています。例えば \(L_2\)-正則化つきの線形回帰(a.k.a. Ridge回帰)のクラス Ridge では

from sklearn.linear_model import Ridge
_model = Ridge(
    alpha=1.0,  # float or array-like: 正則化パラメータ(ハイパーパラメータ)。
    fit_intercept=True,  # bool: 回帰モデルの切片項の有無。モデルの構造に関するパラメータ(ハイパーパラメータ)。
    normalize=False,  # bool: 入力のL2-正規化の有無。前処理に関するパラメータ(ハイパーパラメータ)。
    copy_X=True,  # bool: 入力を複製してクラス内部で保持するか否か。
    max_iter=None,  # int: 反復法による学習での最大反復回数。学習アルゴリズムのパラメータ(ハイパーパラメータ)。
    tol=0.001,  # float: 反復法による学習での停止条件。学習アルゴリズムのパラメータ(ハイパーパラメータ)。
    solver="auto",  # str: 学習アルゴリズムを指定するラベル。学習アルゴリズムのパラメータ(ハイパーパラメータ)。
    random_state=None,  # int, RandomState, or None: 学習時のランダムシード。
)
_model
Ridge(alpha=1.0, copy_X=True, fit_intercept=True, max_iter=None,
      normalize=False, random_state=None, solver='auto', tol=0.001)

のように、このモデルの構造と学習に関する各種のハイパーパラメータを与えてクラスを初期化します(なお、scikit-learnの場合はすべてのハイパーパラメータにデフォルト値が設定されています)。ただし、引数 normalizefit_intercept=True の場合のみ、 max_itertolsolver で特定のアルゴリズムを指定した場合のみそれぞれ意味を持ちます。

前処理についても同様で、例えば主成分分析のクラス PCA では

from sklearn.decomposition import PCA
_prep = PCA(
    n_components=None,  # int, float, None, or str: 圧縮後の成分数またはその導出方法(ハイパーパラメータ)。
    copy=True,  # bool: 入力を複製してクラス内で保持するか否か。
    whiten=False,  # bool: 事前の白色化の有無(ハイパーパラメータ)。
    svd_solver="auto",  # str: 特異値分解 (SVD) のソルバの指定(ハイパーパラメータ)。
    tol=0.0,  # float: 反復法によるSVDでの停止条件(ハイパーパラメータ)。
    iterated_power="auto",  # int or str: 冪乗法によるSVDでの反復回数(ハイパーパラメータ)。
    random_state=None,  # int, RandomState, or None: 学習時のランダムシード。
)
_prep
PCA(copy=True, iterated_power='auto', n_components=None, random_state=None,
    svd_solver='auto', tol=0.0, whiten=False)

のように、やはりこの前処理のパラメータを学習するのに必要なハイパーパラメータを与えてクラスを初期化します。ただし、引数 toliterated_powersolver で特定のソルバを指定した場合のみ意味を持ちます。

Scikit-learnでは上記の関数 \(h\) のように、入力をPCAで圧縮して(関数 \(g\) )からRidge回帰のモデル(関数 \(f\) )を学習するというパイプラインを単一のオブジェクトにまとめることもできます。これには Pipeline クラスを利用して

from sklearn.pipeline import Pipeline
pca_ridge = Pipeline(
    steps=[
        ("pca", PCA(n_components=3)),  # 'pca' という名前でPCAによる変換を登録する
        ("ridge", Ridge(alpha=0.01)),  # 'ridge' という名前でRidge回帰を登録する
    ],  # Transformerを含むタプルを0個以上と、最後にEstimatorを含むタプルを1個だけ並べたリスト
)
pca_ridge
Pipeline(memory=None,
         steps=[('pca',
                 PCA(copy=True, iterated_power='auto', n_components=3,
                     random_state=None, svd_solver='auto', tol=0.0,
                     whiten=False)),
                ('ridge',
                 Ridge(alpha=0.01, copy_X=True, fit_intercept=True,
                       max_iter=None, normalize=False, random_state=None,
                       solver='auto', tol=0.001))],
         verbose=False)

のように記述します。パイプライン中の各ステップのオブジェクトに渡された引数は、パイプラインに登録した対応する名前によるmanglingが行われて

pca_ridge.get_params()
{'memory': None,
 'steps': [('pca',
   PCA(copy=True, iterated_power='auto', n_components=3, random_state=None,
       svd_solver='auto', tol=0.0, whiten=False)),
  ('ridge', Ridge(alpha=0.01, copy_X=True, fit_intercept=True, max_iter=None,
         normalize=False, random_state=None, solver='auto', tol=0.001))],
 'verbose': False,
 'pca': PCA(copy=True, iterated_power='auto', n_components=3, random_state=None,
     svd_solver='auto', tol=0.0, whiten=False),
 'ridge': Ridge(alpha=0.01, copy_X=True, fit_intercept=True, max_iter=None,
       normalize=False, random_state=None, solver='auto', tol=0.001),
 'pca__copy': True,
 'pca__iterated_power': 'auto',
 'pca__n_components': 3,
 'pca__random_state': None,
 'pca__svd_solver': 'auto',
 'pca__tol': 0.0,
 'pca__whiten': False,
 'ridge__alpha': 0.01,
 'ridge__copy_X': True,
 'ridge__fit_intercept': True,
 'ridge__max_iter': None,
 'ridge__normalize': False,
 'ridge__random_state': None,
 'ridge__solver': 'auto',
 'ridge__tol': 0.001}

のように、例えば 'pca''tol' なら 'pca__tol' として、 'ridge''tol' なら 'ridge__tol' として、パイプライン中で一意に識別されます。パイプラインは上述の関数 \(h\) のように全体で1つの識別器ないし回帰器として機能し、

from sklearn.datasets import load_diabetes
x, y = load_diabetes(return_X_y=True)  # 糖尿病データセットを利用
x_train, y_train = x[:300], y[:300]  # 訓練データ
x_test, y_test = x[300:], y[300:]  # 試験データ
pca_ridge.fit(x_train, y_train)
print(f"Test R2 score: {pca_ridge.score(x_test, y_test)}.")
Test R2 score: 0.38736989961757406.

のように、通常の識別器や回帰器であるかのように利用できます。

ハイパーパラメータの探索#

これらのハイパーパラメータは学習の目的に応じて適切に選択される必要があります。機械学習の文脈では主にモデルの未知データに対する予測性能が重視されるため、訓練データで交差検証 (cross validation, CV) を行って適当な損失を評価し、複数の候補の中で平均の損失が最も小さいハイパーパラメータを選択することが一般的です。

交差検証の対象とするハイパーパラメータ(の組)の探索には多様な戦略が存在します。前述の通り、本稿ではその最も基本的なものとして主にgrid searchおよびrandom searchという2つのアルゴリズムを解説します。

小括#

本稿では、機械学習におけるハイパーパラメータの定義とその基本的な探索手法、およびそれらのscikit-learnにおける実装について簡単に解説しました。

それで、結局ハイパーパラメータの探索はどうすればよいのでしょうか? 参考までに、 [Bergstra and Bengio, 2012] で挙げられている各手法の利点には次のようなものがあります:

  • Manual search:

    • 解析者がハイパーパラメータと汎化性能の関係についての洞察を得られる

    • 実行する上で技術的なオーバーヘッドや障壁が存在しない

  • Grid search:

    • 実装が容易で並列化も自明

    • 同じ時間をかけるならmanual searchより良い候補が見つかりやすい

    • ハイパーパラメータの数が少ないときは信頼できる

  • Random search:

    • どの時点で探索を中止しても完全な結果が得られる

    • 追加の計算資源が得られた場合、既存の計画を変更することなく新しい試行を追加できる

    • 個々の候補に対する試行を非同期に実行できる

    • 個々の候補に対する試行で失敗しても、探索全体を危険に晒すことなく放棄ないし再実行できる

この内容を素直に受け取るのであれば、必要に応じてまず数回〜十数回程度のmanual searchで全体の雰囲気を掴んだ上で、探索するハイパーパラメータの数が(利用できる計算資源・時間と比較して相対的に)少なくて密なgridが組める場合はgrid searchを、そうでなければrandom searchを行って最適なハイパーパラメータを選ぶのが良さそうに思えます。

とはいえgrid searchも、random searchも今までの探索結果を利用せずに次の候補を決めているし、一度評価すると決めた点は最後まできっちり評価するという点では大差ありません。最初のmanual searchで意識せずともそうしていたように、何となく良さそうなハイパーパラメータを選んで学習するとか、学習や交差検証の途中でも結果が余りにどうしようもないときはそのハイパーパラメータでの学習を打ち切って次の候補に移るとか、そういうもっと柔軟で知的な探索はできないのでしょうか?(実際これは一部の人々が十分な時間も、計算資源もあるのにmanual searchを偏愛する理由になっています)

実は、ハイパーパラメータ探索の問題はハイパーパラメータ(の組)を与えると対応するスコアを返す(確率的な)関数の最適化問題と捉えることができ、その意味でハイパーパラメータ最適化 (hyperparameter optimization, HPO) の問題と呼ばれることもあります。近年ではこの観点からベイズ最適化やバンディット最適化の知見を取り入れた探索手法が研究され、そうした効率のよい探索を実行するものとして広く利用されています。

参考文献#