正則化パラメータλをlogスケールで最適化する理由 - ProphetをOptunaで最適化する際に出た疑問 -
結論
正則化パラメータ(Prophetのprior_scale
系のパラメータなど)はlogスケールでチューニングしよう!
はじめに
ProphetはFacebookが公開しているオープンソースの時系列予測ライブラリであり、シンプルな設計と軽量さ、また実用的な精度を兼ね備えたツールとして広く使われています。
そんな便利なProphetですが、こちらの記事で示されているように、ハイパーパラメータチューニングを行うことでデフォルトよりも大きく性能改善することが多く、Prophetを活用する場合はハイパーパラメータチューニングが必須と言えます。
Prophetの公式ドキュメントには、ハイパーパラメータチューニング可能なパラメータとして以下のパラメータが記載されています。
-
changepoint_prior_scale
:トレンドの変化点の影響度を調整する正則化パラメータ -
seasonality_prior_scale
:季節性の影響度を調整する正則化パラメータ -
holidays_prior_scale
:休日効果の影響度を調整する正則化パラメータ -
seasonality_mode
:季節性を加法(additive
)的に取り扱うか、乗法(multiplicative
)的に取り扱うかを調整するパラメータ
4つのうち上3つのパラメータはどれもprior_scale
であり、例えば、changepoint_prior_scale
の説明には、
Parameters like this (regularization penalties; this is effectively a lasso penalty) are often tuned on a log scale.
(訳)この種のパラメータ(正則化項。実質的にはlassoペナルティ)は、対数スケールで調整されることがよくあります。
と書かれています。
これを考慮してOptunaの最適化関数を書くとすると
import optuna
def objective(trial):
# ...(略)...
changepoint_prior_scale = trial.suggest_float(
"changepoint_prior_scale", 0.001, 0.5, log=True
)
# ...(略)...
となるわけですが、デフォルトではlog=False
であり、何も考えないと線形スケールでの最適化をすることになります。
正則化のパラメータだからlogスケールで最適化すると言えばそれまでですが、なんでそうしなきゃいけないのかがパッと説明できなかったので、色々と実験しながら整理し、自分なりに納得した内容を共有いたします。
事前分布と正則化の関係性
結論、prior_scale
は正則化に関係するパラメータなのですが、これを理解するには、パラメータに事前分布を導入したベイズ推定の式と正則化項を導入した目的関数との数学的な対応を確認する必要があります。
以下、線形回帰モデルを前提として、細かい数式の展開を省いて説明します。
モデルの前提
以下のように変数を定義します。
- 説明変数:
\mathbf{X} \in \R^{n \times d} - 目的変数:
\mathbf{y} \in \R^n - モデルパラメータ:
\boldsymbol{\theta} \in \R^{d} - 観測ノイズ:
\boldsymbol{\epsilon} \sim \mathcal{N}(\mathbf{0}, \mathbf{I}) -
:サンプル数(データの件数)n -
:特徴量の次元数(説明変数の数)d
なお、正規分布の確率密度関数は以下のように表されます(単純のため1次元で記載)。
上の変数を使い、線形回帰モデルは、以下のように定義します。
また、パラメータ
このprior_scale
となります。
MAP推定
事前分布を仮定した際は、データが得られた後のパラメータの事後確率を最大化するMAP(Maximum A Posteriori)推定を用いてパラメータを推定します。
ベイズの定理を使うと
対数をとっても最適解は変わらないため、対数を取ると以下のように変形できます。
また、各項を計算すると、第1項目が、
第2項目が
となり、MAP推定の最適化の式は以下のように書き換えられます。
定数倍は最適解に影響を与えないため落としています。
おや、この式に似た式をどこかで見たことありますね。
Ridge回帰の目的関数
Ridge回帰の目的関数は以下で定義されます。
ここで
先ほど導出したMAP推定の式において、最適化の方向を最大化から最小化に変えて
とおくと、Ridge回帰は、平均0・分散
ここで、prior_scale
が
という関係が成り立ちます。
したがって、prior_scale
を小さく設定するほど、
逆に、prior_scale
を大きくすると、正則化は弱まり、より自由なパラメータが許されるようになります。
正則化パラメータのスケールとモデルパラメータの変化の関係性
prior_scale
が正則化に関係するパラメータであることがわかったところで、なぜ正則化のパラメータはlogスケールで調整する必要があるのかを確認します。
この項以降は簡単のためprior_scale
ではなく正則化パラメータ
正則化の役割(簡易的な説明)
よくある絵を使ってRidge回帰の例に正則化の役割について説明します。
Ridge回帰の正則化のイメージ(キカガクより引用)
この図は、損失関数の等高線(楕円)と、正則化項による制約領域(円)の関係を示しています。
- 楕円:誤差関数(例えば
)の等高線。中心に近いほど誤差が小さい\|\mathbf{y} - \mathbf{X} \boldsymbol{\theta}\|^2 - 円:正則化項(
)により作られるモデルの複雑さに対する制限\|\boldsymbol{\theta}\|^2
Ridge回帰では、これらの2つが交わる点が最適解となります。つまり、「誤差を最小にしたい」という力学と「パラメータの大きさを抑えたい」という力学のバランスをとった解がRidge回帰では求められるというわけです。
このバランスを調整するのが正則化のパラメータ
モデルパラメータは正則化パラメータによってどう変化するのか(線形回帰の場合)
次に、正則化を加味した目的関数を最適化した場合に、
ここでは説明変数
このとき、Ridge回帰の目的関数は次のようになります。
この目的関数を
この式から
非常に簡単なモデルにおいても正則化のパラメータはモデルのパラメータに対して非線形に影響を及ぼすため、高次元のモデルでも同様に非線形に影響を及ぼすだろうことが推察されます。
この結果から、正則化のパラメータを変化させたときにモデルのパラメータが非線形に変化するならば、正則化のパラメータを線形に調整するとよくないのではないかという仮説が立てられます。
正則化パラメータをlogスケール、線形スケールで探索させた時の探索結果の比較
では実際に正則化パラメータをlogスケール、線形スケールで探索した際に探索結果がどう変わるのかみてみましょう。
条件設計
今回は簡単のため、2つのモデルパラメータ
2つのモデルパラメータに対し、真のパラメータを所与とした上でデータをノイズを加えて100サンプル作成し、そのデータに対してRidge回帰を行います。
また、正則化パラメータ
import numpy as np
# data creation
np_rng = np.random.RandomState(42)
n_samples = 100
w_true = np.array([10.4, 6.4])
X = np_rng.randn(n_samples, 2)
noise = np_rng.normal(0, 1, size=n_samples)
y = X @ w_true + noise
# lambda space
lambdas_log = np.logspace(-3, 3, 100)
lambdas_lin = np.linspace(0.001, 1000, 100)
パラメータ最適化と可視化
まずは、最小二乗法でパラメータを求めてみます。
from sklearn.linear_model import LinearRegression
# Simple Linear Regression check
model_ols = LinearRegression(fit_intercept=False)
model_ols.fit(X, y)
w_ols = model_ols.coef_
w_ols
array([10.57610297, 6.23139683])
データにノイズを与えているので最適解とは少しずれていることがわかります。
次に、上で定義した
import numpy as np
from sklearn.linear_model import Ridge
# weight calculation loop
def solver_loop(lambdas, X, y):
w_list = []
for l in lambdas:
ridge = Ridge(alpha=l, fit_intercept=False)
ridge.fit(X, y)
w_list.append(ridge.coef_)
return np.array(w_list)
w_ridge_log = solver_loop(lambdas_log, X, y)
w_ridge_lin = solver_loop(lambdas_lin, X, y)
最後に、
import numpy as np
import matplotlib.pyplot as plt
# visualize parameters while changing lambda (logspace vs linspace)
fig, ax = plt.subplots(1, 2, figsize=(16, 6), gridspec_kw={'width_ratios': [1, 1], 'wspace': 0.25})
# logspace
sc1 = ax[0].scatter(w_ridge_log[:, 0], w_ridge_log[:, 1],
c=np.log10(lambdas_log))
ax[0].plot(w_ridge_log[:, 0], w_ridge_log[:, 1], color='gray', linewidth=1, alpha=0.5)
ax[0].plot(w_true[0], w_true[1], 'ro', label='True parameters', markersize=8)
ax[0].plot(w_ols[0], w_ols[1], 'kx', label='OLS solution', markersize=8)
ax[0].set_title("Log-spaced λ (1e-3 to 1e3)")
ax[0].set_xlabel(r"$w_1$")
ax[0].set_ylabel(r"$w_2$")
ax[0].grid(True)
ax[0].legend()
# linspace
sc2 = ax[1].scatter(w_ridge_lin[:, 0], w_ridge_lin[:, 1],
c=np.log10(lambdas_lin))
ax[1].plot(w_ridge_lin[:, 0], w_ridge_lin[:, 1], color='gray', linewidth=1, alpha=0.5)
ax[1].plot(w_true[0], w_true[1], 'ro', label='True parameters', markersize=8)
ax[1].plot(w_ols[0], w_ols[1], 'kx', label='OLS solution', markersize=8)
ax[1].set_title("Linearly-spaced λ (0.001 to 1000)")
ax[1].set_xlabel(r"$w_1$")
ax[0].set_ylabel(r"$w_2$")
ax[1].grid(True)
ax[1].legend()
# color bar
fig.subplots_adjust(right=0.80)
cbar_ax = fig.add_axes([0.83, 0.15, 0.02, 0.7])
cbar = fig.colorbar(sc2, cax=cbar_ax)
cbar.set_label(r'$\log_{10}(\lambda)$')
plt.suptitle("Comparison of Ridge Coefficient Trajectories (Log vs Linear λ)", y=1.02)
plt.show()
左側がlogスケールで正則化パラメータ
logスケールでは、
一般的に
一方で、線形スケールの方は、最適なパラメータ付近での探索点が少なく、
誤差関数の最適解から遠い部分に対して細かく調べることが最適なモデルパラメータの探索に対して効果的とは言えず、パラメータ探索の方策としては適切ではないと考えられます。
この結果から、正則化パラメータの最適化においてモデルの挙動変化を満遍なく確認するためには、logスケールでの探索が適切であることがわかりました。
Optunaなどの自動最適化における注意点
Optunaのようなベイズ最適化ライブラリを用いると、探索空間の形に合わせて効率的にパラメータをサンプリングしてくれるため、ランダムサーチやグリッドサーチよりも賢く探索してくれることが期待されます。
ですが、Optunaのベースサンプリングはスケールに合わせて一様分布でサンプリングするため、logスケールの指定を明示しなければベイズ最適化で適切な箇所を見つけるまでの初期のサンプリングでは線形スケールでの探索となり最適な領域を見逃す可能性があります。
そのため、正則化のような非線形影響をもつパラメータに対してはlog=True
を明示することが重要だと言えそうです。
まとめ
Prophetにおける prior_scale
系パラメータが正則化に関連するパラメータであり、logスケールでチューニングすべきであることを解析的、実験的に理由づけしてみました。
生成AIがコーディングもしてくれる便利な世の中になりましたが、生成AIが出すコードが適切に記載されているかを確認するために、こういった基礎をしっかり身につけていきたいなと思いました。
ご覧いただきありがとうございました!
参考文献
Prophet Hyperparameter Tuning
Optuna
ベイズ推定
Discussion