論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
グリッドサーチ
Grid Search
ML基礎

🔖 キーワード索引

グリッドサーチGrid SearchHPOクロスバリデーションRandomSearchOptunaBayesian OptGridSearchCV

本ページは グリッドサーチ(Grid Search)を 12 のセクションで多角的に解説します。 上のチップは検索・関連語の手がかりです。 以下のリンクで各セクションに直接ジャンプできます:

💡 30秒結論📍 文脈🎨 直感📐 数式🔬 数式を言葉で読み解く🧮 実値計算🐍 Python 実装⚠️ 落とし穴🌐 関連手法🔗 関連用語📚 グループ教材

💡 30秒で分かる結論

🍰 まずはやさしく

設定の組み合わせをすべて試す方法です。

一番良い設定を見つけるために使います。

スマホのアプリ設定を全部試すようなものです。

この章ではグリッドサーチの結論を読みます。

グリッドサーチ(Grid Search):ハイパーパラメータの組合せを網羅的に試す手法

💡 30秒で分かる結論

📍 あなたが今見ているもの

🍰 まずはやさしく

人間が決める設定値をさがす手法です。

モデルの性能を最大限に引き出します。

部活の練習メニューの組み合わせを選ぶ例です。

この章では使う場面や注意点を読みます。

機械学習モデルには 「学習で決まる重み」(例:回帰係数)「人間が指定する設定値」(ハイパーパラメータ) があります。 Ridge 回帰の α、 ランダムフォレストの n_estimators、 SVM の Cgamma など。 これらを「総当たり」で試して最良を選ぶのがグリッドサーチ。 論文の 「best parameters were selected via grid search with 5-fold CV」 という記述が出てきたら、 これのことです。

📍 文脈 — どこで使う概念か

グリッドサーチは ハイパーパラメータの候補を格子状に並べ、 全組合せを試して最良を選ぶ手法。 シンプルで実装容易だが、 候補数が指数的に増えるため 3–4 ハイパラまでが現実的。 それ以上は Random Search や Bayesian Optimization に切り替える。

🎨 直感で掴む

🍰 まずはやさしく

地図のマス目を全部歩くようなイメージです。

どの設定が正解かを確認するために使います。

買い物で色とサイズを全部試すようなものです。

この章では具体的な動き方を読みます。

グリッドサーチの動作イメージ

Ridge回帰の正則化強度 α を選びたいとします。 候補:[0.01, 0.1, 1, 10, 100]

  1. α=0.01 で学習 → 5-fold CV で平均 RMSE を測定
  2. α=0.1 で学習 → 平均 RMSE を測定
  3. 同様に α=1, 10, 100 まで全て測定
  4. 5 つの RMSE の中で 最小のもの に対応する α を採用

2 パラメータなら格子状(grid)に組合せが並ぶので「グリッドサーチ」。

α \ C0.010.1110
linear0.520.480.410.45
rbf0.550.500.430.47

上記の例なら (C=1, kernel=linear) で CV-RMSE = 0.41 が最小なのでこれを採用。

🎨 直感で掴む — 具体例で理解する

Grid Search は「地図上の格子点を全部歩いてみる」イメージ。 確実だが時間がかかる。 一方、 ハイパラの一部だけが性能に影響することが多い(Bergstra & Bengio 2012)ため、 Random Search の方が同じ計算量で良い解に到達することが知られる。 とはいえ Grid は「結果が再現的・説明しやすい」ため、 候補が少ない時の標準。

📐 定義 / 数式

🍰 まずはやさしく

設定の組み合わせを数式で表したものです。

正解を計算で導き出すために使います。

テストの点数を最大にする組み合わせ選びです。

この章では計算方法や定義を読みます。

【グリッドサーチの定式化】
$$\theta^* = \arg\min_{\theta \in \Theta_{\text{grid}}} \frac{1}{K} \sum_{k=1}^{K} L(\mathcal{D}_k^{\text{val}}, \hat{f}_{\theta}(\mathcal{D}_k^{\text{train}}))$$
$\Theta_{\text{grid}}$ は探索する格子、 K は CV の分割数
【総組合せ数】
$$|\Theta_{\text{grid}}| = \prod_{j=1}^{p} |\Theta_j|$$
パラメータ数 p の積で組合せが指数的に増える(curse of dimensionality)

📐 数式・定義

グリッドサーチを数式 / 形式定義で表す:

$$\hat{\boldsymbol{\lambda}} = \arg\min_{\boldsymbol{\lambda} \in \Lambda} \; \text{CV-Loss}\!\big(\text{model}_{\boldsymbol{\lambda}}\big)$$

ハイパーパラメータ集合 $\Lambda$ の中で、 交差検証損失を最小にする組合せ $\hat{\boldsymbol{\lambda}}$ を選ぶ。 Grid は $\Lambda$ を直積(格子)で構成する。

📐 探索空間の数式を言葉で読み解く

ハイパラ探索の総評価回数は次式で表せる。

$$N_{\text{eval}} = k \times \prod_{i=1}^{d} |H_i|$$

数式を言葉で読み解く

例:5 ハイパラ × 各 5 候補 × 5-fold CV = $5 \times 5^5 = 15{,}625$ 回学習。 1 回 1 分なら 10 日以上。 これが「grid search の指数的爆発」の正体。

Random Search では $N_{\text{eval}}$ を 固定(例 100 回)し、 各ハイパラを独立に確率分布から抽出。 重要なハイパラの粒度を細かく取れる利点がある。

🔬 記号・式を言葉で読み解く

$\theta$
ハイパーパラメータベクトル。 例:$(\alpha, C, \gamma)$
$\Theta_{\text{grid}}$
ユーザが指定した候補値の格子。 例:$\{0.01, 0.1, 1, 10\}^3$
$K$
交差検証の分割数。 通常 5 か 10。
$L$
評価指標。 回帰なら RMSE/MAE、 分類なら accuracy/F1。

🔬 数式を言葉で読み解く

上の数式に出てきた記号を 1 つずつ解説します。 数式が出てくる試験問題(統計検定・G 検定・基本情報)では、 各記号の意味を答えられるかが分岐点:

記号意味
$\boldsymbol{\lambda}$ハイパーパラメータベクトル
$\Lambda$候補集合(格子)
CV-Loss交差検証損失
$\hat{\boldsymbol{\lambda}}$選ばれた最適ハイパラ

🔬 発展トピック

「グリッドサーチ」を入門レベルで習得した次に進むべき発展テーマ:

① 理論的拡張

基本概念を 確率論・情報理論・最適化理論の観点で再定式化すると、 隣接する手法との理論的な関係が見えてきます。 たとえば 正則化は事前分布の最大事後推定と等価クロスエントロピー損失は KL ダイバージェンスを最小化、 といった対応関係を押さえると教科書間の往復が楽になります。

② 実装的拡張

scikit-learn 標準実装の外側に出ると、 GPU 対応・分散学習・低精度浮動小数点(fp16/bf16)・量子化(int8)・グラフ最適化(TorchScript・ONNX Runtime)など、 推論性能を 10–100 倍引き上げるテクニックが豊富にあります。 本番運用では モデル精度と推論コストのトレードオフを意識した実装が鍵。

③ 評価・解釈の拡張

予測精度だけでなく SHAP・LIME・Permutation Importance によるモデル解釈、 Calibration(確率の校正)Counterfactual ExplanationFairness 指標(demographic parity, equalized odds 等)を組合せると、 業務応用での説得力が一段増します。

④ 業界応用

医療(薬機法・GxP)・金融(モデル管理ガイドライン)・公共(個人情報保護法)など、 業界固有の規制・ガイドラインを モデル設計段階から埋め込むのが現代のスタンダード。 「グリッドサーチ」を業務適用するときは、 ドメインの専門家・法務との早期コラボレーションが成否を分けます。

🧮 実データで計算してみる

SSDSE-B-2026(2023 年断面・47 都道府県)で Ridge 回帰の最適な α を選ぶ。 X=年齢 3 区分人口(A1301/A1302/A1303)を StandardScaler で標準化し、 y=出生数(A4101)。 下の実装ブロックと同一設定での実測値:

α5-fold CV の RMSE 平均(人)
0.01991.6 ← 最小
0.11128.5
1.01373.7
102053.9
1007642.4

α=0.01 が最良 → モデル最終化はこの α で全訓練データを使って再学習し、 ホールドアウトしておいたテストで最終評価。 なお最良値がグリッドのに来ているため、 本来はさらに小さい α まで範囲を広げて確認するのが定石(この課題は特徴量 3 個 × 47 行で過学習が弱く、 正則化を弱めるほど OLS の性能に漸近する)。

🧮 SSDSE-B 実値計算 — 都道府県データで手を動かす

SSDSE-B-2026 の 「総人口 → 出生数」回帰で、 ランダムフォレストの 3 ハイパラ(n_estimators / max_depth / min_samples_leaf)を Grid Search し、 最良 R² を出すパラメータを発見する。 2023 年断面(47 行)・random_state=42 での実測は best params: max_depth=7, min_samples_leaf=1, n_estimators=50、 CV R²=0.8041

使用データ:SSDSE-B-2026.csv(独立行政法人 統計センター提供、 47 都道府県 × 100 超の社会経済指標)。 出典

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) F3101(新規求職申込件数(一般)) 北海道 5,092,000 1,681,000 24,430 156,458 東京都 14,086,000 3,205,000 86,348 270,954 沖縄県 1,468,000 350,000 12,549 43,877 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import GridSearchCV

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023]   # 2023年断面(47都道府県)

X = df[['A1101', 'A1303', 'F3101']].fillna(0).values
y = df['A4101'].values

# 教材では計算時間の都合で候補を絞っています。実務ではもっと広い範囲を探索します。
# (本来は n_estimators=[50,100,200] × max_depth=[3,5,7,None] × min_samples_leaf=[1,2,4] など)
param_grid = {
    'n_estimators':     [30, 60],
    'max_depth':        [3, 5, None],
    'min_samples_leaf': [1, 4],
}
gs = GridSearchCV(RandomForestRegressor(random_state=42),
                  param_grid, cv=5, scoring='r2', n_jobs=-1)
gs.fit(X, y)
print(f'Best params: {gs.best_params_}')
print(f'Best CV R² : {gs.best_score_:.4f}')

▲ 上記コードはそのまま実行可能。 CP932 エンコーディング・skiprows=1(英語ヘッダ行をスキップ)・列名の英数字コード(A1101 = 総人口 など)に注意。

🧮 SSDSE-B-2026 で Grid Search 実行

このコードでやること:SSDSE-B-2026 の 2023 年断面(47 行)で、 Ridge 回帰の正則化強度 alpha を 5 候補で grid search する。 人口系の特徴量は百万オーダーなので、 StandardScaler を Pipeline に組み込んで標準化してから探索する(標準化しないと alpha がほぼ効かず、 どの候補でも R² が同値になってしまう)。 最良値を 5-fold CV で選び、 出力結果を確認する。

📥 入力データ:SSDSE-B-2026、 X = 15歳未満人口・15-64歳人口・65歳以上人口、 y = 出生数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import pandas as pd
from sklearn.linear_model import Ridge
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023]   # 2023年断面(47都道府県)
X = df[['A1301', 'A1302', 'A1303']]
y = df['A4101']

pipe = make_pipeline(StandardScaler(), Ridge())   # 標準化してから Ridge
param_grid = {'ridge__alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}
gs = GridSearchCV(pipe, param_grid, cv=5, scoring='r2')
gs.fit(X, y)

print("最良 alpha:", gs.best_params_)
print(f"最良 CV R²: {gs.best_score_:.4f}")
print("全 alpha のスコア:")
for a, s in zip(param_grid['ridge__alpha'], gs.cv_results_['mean_test_score']):
    print(f"  alpha={a:7.2f} → R²={s:.4f}")

📤 実行例

最良 alpha: {'ridge__alpha': 0.01} 最良 CV R²: 0.9917 全 alpha のスコア: alpha= 0.01 → R²=0.9917 alpha= 0.10 → R²=0.9857 alpha= 1.00 → R²=0.9642 alpha= 10.00 → R²=0.9467 alpha= 100.00 → R²=0.4135

💬 結果の読み方:標準化したことで alpha がはっきり効く。 alpha が大きいほど正則化が強まり係数が縮小され(alpha=0.01 で約 15,244 あった第 1 係数は alpha=100 で約 3,340 まで縮む)、 この課題では R² が単調に低下(alpha=100 で 0.41 まで悪化)。 最良は最小候補 0.01 でグリッドの境界にあるため、 定石どおり範囲を広げて確認する — 実測では alpha=0.001 でも約 0.992 で頭打ちし、 OLS(正則化なし、 CV R²≈0.9923)に漸近する。 特徴量 3 個 × 47 行では過学習が弱く、 正則化の恩恵が出にくい設定だと分かる。 候補値の 対数スケールでの探索が肝心(線形に並べると重要域を見逃す)。

🧮 数式に値を入れて手で計算する: グリッド組合せと CV 計算量

3 ハイパーパラメータ × 5 CV の組合せ数と計算量を計算する。

Step 1: パラメータ範囲

パラ候補数
C5
γ4
kernel3

Step 2: 組合せ

グリッド = 5×4×3 = 60 組 CV=5 → 60·5 = 300 フィット 1 フィット 10 秒 → 300·10 = 3,000 秒 ≈ 50 分

🐍 Python で再現

1
2
3
4
5
6
7
8
import numpy as np
params = np.array([5, 4, 3])
grid = np.prod(params)
cv = 5
fits = grid * cv
print(f"グリッド組: {grid}")
print(f"総フィット: {fits}")
print(f"所要時間 (10s/fit): {fits*10/60:.0f} 分")

📤 実行結果

グリッド組: 60 総フィット: 300 所要時間 (10s/fit): 50 分

💬 手計算 (Step 2) 60 組 / 50 分と Python 出力が完全一致。

なぜ alpha は「対数で」刻むのか — 実測で確かめる

正則化の強さ alpha のような「桁で効く」パラメータは、等間隔ではなく対数間隔で刻みます。 理由は実際に走らせると一目で分かります。SSDSE-B-2026(2023 年・47 都道府県)で、 総人口・15 歳未満人口・65 歳以上人口・婚姻件数から出生数を当てる Ridge 回帰を組み、 5-fold 交差検証の R² を測った結果が次の表です。

alpha(対数で 8 点)CV R²読み取れること
0.001+0.99540.001〜0.1 はほぼ差が無い。正則化が弱すぎて「実質かけていない」状態。
0.01+0.9950
0.1+0.9909
1+0.9848ここから少しずつ効き始める。
10+0.9800
100+0.8313急に崩れ始める。変化はこの桁で起きている
1000+0.2305係数が潰れ、ほぼ平均を答えるだけのモデルに。
10000+0.0129

同じ 8 点を 1 から 1000 まで等間隔(1, 143.7, 286.4, …, 1000)に取ると、こうなります。

alpha1.0143.7286.4429.1571.9714.6857.31000.0
CV R²+0.985+0.752+0.560+0.440+0.361+0.305+0.263+0.231

💬 ここが要点:等間隔では 8 点のうち 7 点が「もう崩れきった領域」に落ちてしまい、 いちばん知りたい 0.001〜10 の範囲を 1 点(alpha=1.0)でしか見ていません。 対数間隔なら同じ 8 回の計算で 0.001 から 10000 まで 8 桁を均等に覆えます。 np.logspace(-3, 4, 8) と書くだけなので、桁で効くパラメータは必ず logspaceと覚えてください。 学習率・C・gamma・alpha はすべてこの仲間です。逆に max_depthn_estimators のように 「1 ずつ増える」性質のものは等間隔(あるいは整数の候補列)で構いません。

探索点数は「掛け算」で増える — 実測時間

グリッドサーチの学習回数は (候補の積)×(fold 数) です。パラメータを 1 つ足すと点数は足し算ではなく掛け算で増えます。 上と同じ 47 行のデータで RandomForest を 5-fold で回した実測が次の表です。

探索するパラメータ点数学習回数実測時間
n_estimators(2 候補)2100.1 秒
+ max_depth(3 候補)6300.3 秒
+ min_samples_leaf(2 候補)12600.7 秒

47 行なら 12 点でも 1 秒未満ですが、データが 1 万行になれば同じ 12 点で数分〜数十分になります。 パラメータを 5 個、各 5 候補にすると 5⁵ = 3,125 点 × 5 fold = 15,625 回の学習です。 ここが「グリッドサーチは丁寧だが高くつく」と言われる理由で、 ランダムサーチや逐次的な絞り込み(Successive Halving)が使われる動機になります。 まずは効きそうな 2 つに絞って粗く探し、当たりの周辺だけ細かく刻み直すのが、実務では一番早く済みます。

🐍 Python 実装

SSDSE-B-2026(47 都道府県・2023 年データ)を題材にした最小コード(雛形:X_train / y_train は準備済みとする。 Ridge 系は実際には StandardScaler を Pipeline に組み込んで標準化してから探索する — 実測例は上の 🧮 を参照):

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A1301(15歳未満人口) A1303(65歳以上人口) A4101(出生数) A9101(婚姻件数) 北海道 5,092,000 514,000 1,681,000 24,430 17,281 東京都 14,086,000 1,513,000 3,205,000 86,348 71,774 沖縄県 1,468,000 236,000 350,000 12,549 6,316 …(全 47 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import pandas as pd

# ── この抜粋だけで動くように、47 都道府県・最新年度を読み込む ──
_d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
_d = _d[_d['SSDSE-B-2026'] == 2023]
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.linear_model import Ridge

# ── この抜粋だけで動くように、学習用データを用意する ──
_X = _d[['A1101', 'A1301', 'A1303', 'A9101']].astype(float).values
_y = _d['A4101'].astype(float).values
X_train, X_test, y_train, y_test = train_test_split(_X, _y, test_size=0.3, random_state=0)

param_grid = {'alpha': [0.01, 0.1, 1, 10, 100]}
gs = GridSearchCV(Ridge(), param_grid, cv=5, scoring='neg_root_mean_squared_error')
gs.fit(X_train, y_train)

print('Best:', gs.best_params_, '| CV RMSE:', -gs.best_score_)
best_model = gs.best_estimator_

🐍 Python 実装バリエーション

「グリッドサーチ」を扱う代表的なライブラリ別実装。 同じ目的でも書き方が違うため、 自分のプロジェクトの依存関係に合わせて選択する:

① pandas + numpy(最小依存)

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A4101(出生数) A5101(転入者数(日本人移動者)) F3101(新規求職申込件数(一般)) 北海道 5,092,000 24,430 47,388 156,458 東京都 14,086,000 86,348 406,749 270,954 沖縄県 1,468,000 12,549 26,410 43,877 …(全 47 行)
1
2
3
4
5
6
7
8
import pandas as pd
import numpy as np

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df.rename(columns={df.columns[2]: 'pref'})

print('行数:', len(df), '列数:', df.shape[1])
print(df[['pref', 'A1101', 'A4101', 'A5101', 'F3101']].head())

② scikit-learn(学習・評価)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score, mean_squared_error
from sklearn.model_selection import train_test_split
import numpy as np

X = df[['A1101', 'A1303']].fillna(0).values
y = df['A4101'].values
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
m = LinearRegression().fit(X_tr, y_tr)
pred = m.predict(X_te)
print(f'R²   = {r2_score(y_te, pred):.3f}')
print(f'RMSE = {np.sqrt(mean_squared_error(y_te, pred)):.2f}')

③ scipy.stats(統計検定・分布)

1
2
3
4
5
6
7
8
9
from scipy import stats

# 例: 2 変数の Pearson 相関 + p 値
r, p = stats.pearsonr(df['A1101'], df['A4101'])
print(f'相関係数 r = {r:.3f}, p 値 = {p:.2e}')

# 例: 1 標本 t 検定(平均が一定値と異なるか)
t, p = stats.ttest_1samp(df['A4101'], popmean=df['A4101'].mean())
print(f't = {t:.3f}, p = {p:.3f}')

④ 可視化(matplotlib + seaborn)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import matplotlib.pyplot as plt
import seaborn as sns

fig, ax = plt.subplots(figsize=(8,5))
sns.scatterplot(data=df, x='A1101', y='A4101', ax=ax)
ax.set_xlabel('総人口')
ax.set_ylabel('出生数')
ax.set_title(f'{len(df)} 都道府県の関係')
plt.tight_layout()
plt.savefig('out.png', dpi=120)
plt.close()

🐍 Random Search vs Grid Search 比較

このコードでやること:同じ計算予算(20 回評価)で Grid Search と Random Search を比較する。 Random Search が小さい予算で広範囲を探索できることを確認。 2023 年断面(47 行)・seed 固定(random_state=0 / 42)の実測。

📥 入力データ:SSDSE-B-2026、 X = 人口関連 3 変数、 y = 出生数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from scipy.stats import randint

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023]   # 2023年断面(47都道府県)
X = df[['A1101', 'A1301', 'A4200']]
y = df['A4101']

# 教材では計算時間の都合で候補を絞っています。実務ではもっと広い範囲を探索します。
# 木の本数の上限を 1000 → 100 に下げています(比較の趣旨は変わりません)
# Grid: 5x4 = 20 評価
grid = {'n_estimators': [20, 40, 60, 80, 100], 'max_depth': [3, 5, 10, 20]}
gs = GridSearchCV(RandomForestRegressor(random_state=0), grid, cv=3,
                  scoring='r2', n_jobs=-1)
gs.fit(X, y)

# Random: 20 サンプル
dist = {'n_estimators': randint(20, 101), 'max_depth': randint(3, 21)}
rs = RandomizedSearchCV(RandomForestRegressor(random_state=0), dist,
                        n_iter=20, cv=3, scoring='r2', random_state=42, n_jobs=-1)
rs.fit(X, y)

print(f"Grid 最良: {gs.best_params_} R²={gs.best_score_:.4f}")
print(f"Random 最良: {rs.best_params_} R²={rs.best_score_:.4f}")

📤 実行例

Grid 最良: {'max_depth': 5, 'n_estimators': 20} R²=0.9207 Random 最良: {'max_depth': 17, 'n_estimators': 81} R²=0.9188

💬 結果の読み方:Random Search は 連続値の細かい候補(n_estimators=81 のような格子に無い値)を試せる。 今回は Grid のほうがわずかに上(0.9207 vs 0.9188)だが、この差は fold 分割のゆらぎの範囲。 Grid は離散値で固定されるため、 最適値が候補に含まれていない可能性がある。 今回の差は僅少で fold 分割にも依存するが、 高次元(パラメータ 4+)では Random Search の優位が明確になる。

🐍 Optuna(ベイズ最適化)で SSDSE-B-2026

このコードでやること:Optuna の TPE(Tree-structured Parzen Estimator)で Ridge の alpha を 連続範囲から最適化する。 過去の評価結果を使って次の試行を賢く選ぶため、 同じ予算でより良い解が見つかる。 Ridge は StandardScaler+Pipeline で標準化し、 TPESampler(seed=42) で結果を再現可能にする(seed を固定しないと結果は試行ごとに変動する)。 2023 年断面(47 行)の実測。

📥 入力データ:SSDSE-B-2026、 X = 人口 3 変数、 y = 出生数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pandas as pd
import optuna
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023]   # 2023年断面(47都道府県)
X = df[['A1101', 'A1301', 'A4200']]
y = df['A4101']

def objective(trial):
    alpha = trial.suggest_float('alpha', 1e-3, 1e3, log=True)
    model = make_pipeline(StandardScaler(), Ridge(alpha=alpha))
    return cross_val_score(model, X, y, cv=5, scoring='r2').mean()

sampler = optuna.samplers.TPESampler(seed=42)   # seed 固定で再現可能に
study = optuna.create_study(direction='maximize', sampler=sampler)
study.optimize(objective, n_trials=30, show_progress_bar=False)

print(f"最良 alpha: {study.best_params['alpha']:.4f}")
print(f"最良 R²: {study.best_value:.4f}")

📤 実行例

最良 alpha: 0.0010 最良 R²: 0.9901

💬 結果の読み方:Optuna は対数一様分布 [1e-3, 1e3] を 30 試行で探索し、 alpha≈0.001(探索範囲の下端)に収束、 CV R²=0.9901。 grid の離散候補に縛られず連続値を試せるのが TPE の利点だが、 この課題では正則化を弱めるほど良い(特徴量 3 個 × 47 行で過学習が弱く OLS に漸近する。 🧮 実値計算の結論と整合)ため、 最良が範囲の端に張り付いた。 端に来た場合は下限をさらに下げて確認するのが定石。 TPESampler(seed=42) を固定しているので上記の値は再現される(seed を外すと試行系列が変わり best alpha は毎回ぶれる)。

🐍 Successive Halving で予算を節約

このコードでやること:scikit-learn の HalvingGridSearchCV で、 少量のリソースで多数の候補を評価 → 上位だけ残してリソースを増やす、 を繰り返す。 grid search より大幅に高速。

📥 入力データ:SSDSE-B-2026、 X = 人口 3 変数、 y = 出生数(A4101)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pandas as pd
from sklearn.experimental import enable_halving_search_cv  # noqa
from sklearn.model_selection import HalvingGridSearchCV
from sklearn.ensemble import GradientBoostingRegressor

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023]   # 2023年断面(47都道府県)
X = df[['A1101', 'A1301', 'A4200']]
y = df['A4101']

# 教材では計算時間の都合で候補を絞っています。実務ではもっと広い範囲を探索します。
# n_estimators の上限を 500 → 150 に下げています(候補数 64 は変えていません)
param_grid = {
    'n_estimators': [20, 40, 80, 150],
    'max_depth': [2, 3, 5, 7],
    'learning_rate': [0.01, 0.05, 0.1, 0.3]
}
hs = HalvingGridSearchCV(
    GradientBoostingRegressor(random_state=0),
    param_grid, cv=3, scoring='r2',
    factor=3, resource='n_samples', min_resources=15, random_state=42, n_jobs=-1
)
hs.fit(X, y)
print(f"最良: {hs.best_params_}")
print(f"最良 CV R²: {hs.best_score_:.4f}")
print(f"各段の n_samples: {hs.n_resources_}")

📤 実行例

最良: {'learning_rate': 0.3, 'max_depth': 5, 'n_estimators': 150} 最良 CV R²: 0.9429 各段の n_samples: [15, 45]

💬 結果の読み方:候補 64 個を全評価せず、 まず 15 サンプルで全 64 候補をスクリーニング → 上位 22 候補だけ 45 サンプルで再評価(n_candidates_=[64, 22])。 これで学習回数を大幅に節約できる。 ただし今回は 47 行しかなく 第 1 段の 15 サンプルが荒すぎるため、 通常の GridSearchCV の最良とは別の解(R²=0.9429)に落ち着いた。 Successive Halving は計算量削減と引き換えに、 低リソース段のノイズで真の最適候補を早期に切り落とすリスクがある——データが小さいほど min_resources を大きめに取るのが安全。 大規模データでこそ本領を発揮する。

📊 主要モデルのハイパラ典型探索範囲

モデル主要ハイパラ推奨範囲スケール
Ridge / Lassoalpha[1e-3, 1e3]log
SVMC, gamma[1e-3, 1e3]log
Random Forestn_estimators, max_depth, min_samples_leaf[50, 1000] / [3, 20] / [1, 20]linear
XGBoost / LightGBMlearning_rate, max_depth, n_estimators, subsample[0.01, 0.3] / [3, 10] / [100, 2000] / [0.5, 1.0]log / linear
Neural Netlr, batch_size, dropout, weight_decay[1e-5, 1e-1] / [16, 512] / [0, 0.5] / [1e-6, 1e-2]log
k-NNn_neighbors[1, 50]linear

注:「推奨範囲」はあくまで出発点。 業務データやモデルサイズに応じて狭めるか広げる。

🐍 cv_results_ を可視化して傾向を掴む

このコードでやることGridSearchCV.cv_results_ を DataFrame に変換し、 各ハイパラ値ごとのスコアを表として確認する。 ハイパラ感度(どのパラメータが効くか)を読み取れる。

📥 入力データ:SSDSE-B-2026、 X = 人口関連、 y = 出生数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pandas as pd
from sklearn.linear_model import ElasticNet
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023]   # 2023年断面(47都道府県)
X = df[['A1101', 'A1301', 'A4200']]
y = df['A4101']

pipe = make_pipeline(StandardScaler(), ElasticNet(max_iter=5000))
grid = {
    'elasticnet__alpha': [0.01, 0.1, 1.0],
    'elasticnet__l1_ratio': [0.1, 0.5, 0.9]
}
gs = GridSearchCV(pipe, grid, cv=5, scoring='r2')
gs.fit(X, y)

res = pd.DataFrame(gs.cv_results_)[[
    'param_elasticnet__alpha', 'param_elasticnet__l1_ratio',
    'mean_test_score', 'std_test_score']]
pivot = res.pivot(index='param_elasticnet__alpha',
                  columns='param_elasticnet__l1_ratio',
                  values='mean_test_score')
print(pivot.round(4))

📤 実行例

param_elasticnet__l1_ratio 0.1 0.5 0.9 param_elasticnet__alpha 0.01 0.9720 0.9795 0.9883 0.10 0.9403 0.9456 0.9704 1.00 0.8353 0.9072 0.9396

💬 結果の読み方:標準化したことで 両軸ともスコアが明確に動く。 alpha を上げるほど正則化が強まり性能が落ち(0.01 行 → 1.0 行で最大 0.988→0.835 まで低下)、 l1_ratio を上げる(L1 寄り=スパース化)ほど改善する(特に alpha=1.0 で 0.835→0.940 と大きく効く)。 最良セルは alpha=0.01・l1_ratio=0.9 の 0.9883。 → 次の探索では alpha を小さい側([0.001, 0.1] など)に絞り、 l1_ratio は高め(0.9 近傍)を重点的に刻む戦略が立てられる。 cv_results_ の ヒートマップ可視化は、 こうした軸ごとの感度を読み取る出発点として最強。

⚠️ よくある落とし穴

⚠️ テストデータでハイパラを決める
テストが擬似的に訓練に使われ、 真の汎化性能を過大評価。 → 訓練データ内で CV を回す。
⚠️ 組合せ数の爆発
4パラメータ×10候補で 10,000 通り。 計算量が現実的でない。 → RandomizedSearch または Optuna。
⚠️ 離散候補しか試せない
α=0.5 がベストでも、 候補が [0.1, 1, 10] だと見つからない。 → 連続最適化(ベイズ最適化)の方が効率的。
⚠️ CV の foldごとに前処理を fit していない
標準化を全訓練データで先に fit するとリークが起きる。 → Pipeline 内で組む。
⚠️ scoring 指定ミス
GridSearchCV はデフォルトでスコアを最大化する。 損失系の指標は neg_ プレフィックスを使う。

⚠️ よくある落とし穴(5 件)

「グリッドサーチ」を実務・試験で扱うときに頻発する典型的なミスです。 各項目を 1 度読んでおけば 9 割の事故が防げます:

❌ 計算量爆発
3 ハイパラ × 各 5 値 = 125 通り、 さらに CV=5 で 625 学習。 候補は計画的に絞る。
❌ 評価データへの過適合
Grid を CV 無しで Test スコアで決めると、 Test に過適合。 必ず CV を回す。
❌ 無関係ハイパラを混ぜる
性能に効かないハイパラを格子に入れると無駄。 事前に感度分析を。
❌ 離散値だけで離散化が粗い
学習率 [0.001, 0.01, 0.1] のような対数スケールを使うか、 Random Search で連続探索。
❌ ベストの再現性
Grid 結果は random_state を固定しないと毎回ブレる。 シード固定 + n_jobs=1 で完全再現。

⚠️ Grid Search の落とし穴 7 件

  1. 探索空間の爆発:パラメータ 5 個 × 各 10 候補 = 100,000 評価。 1 回 10 秒でも 280 時間。 → BO + 早期打ち切り。
  2. 候補値の線形配置:alpha=[1,2,3,4,5] のような線形ではなく、 [0.01, 0.1, 1, 10, 100] の 対数が原則。
  3. CV の楽観バイアス:同じ CV で選んだハイパラの評価は楽観的。 → Nested CV か独立 test set。
  4. ローカル最適:grid の境界に最適値があると、 範囲を広げないと気づかない。 → 結果を可視化して境界チェック。
  5. シード固定:random_state を忘れると毎回異なる最適解。 → 必ず seed を固定。
  6. 業務 KPI とのズレ:CV スコアが上がっても業務 KPI が改善しないことがある。 → 評価指標を業務に揃える。
  7. 計算予算の暴走:Slurm/Cloud で意図せず大規模 grid を投げると課金事故。 → n_jobs=-1 と並列度を確認。

🗓 ハイパラ探索の歴史

出来事
1960s最適実験計画(Box & Wilson)
1998SMBO(Sequential Model-Based Optimization)の理論
2011Hyperopt(TPE)公開(Bergstra ら)
2012Bergstra & Bengio「Random Search が Grid より効率的」を実証
2013scikit-learn 0.14 で GridSearchCV 標準化
2017Hyperband 提案(Li ら)
2019Optuna 公開(Preferred Networks)
2020scikit-learn 0.24 で HalvingGridSearchCV 追加

❓ よくある質問

Q1. Grid と Random はいつ使い分ける?

A. パラメータ数 ≤ 3 なら Grid、 4 以上なら Random または BO。 Bergstra & Bengio の論文を読むと納得しやすい。

Q2. Optuna は本番運用にも使える?

A. Yes。 SQLite/PostgreSQL バックエンドで study を永続化でき、 分散実行も対応。 LightGBM/XGBoost との統合 API もある。

Q3. 並列化したい場合は?

A. GridSearchCV は n_jobs=-1 で全 CPU 使用可。 Optuna は n_jobs 引数 + RDB 永続化で分散実行可能。

Q4. 探索範囲はどう決める?

A. 公式ドキュメント・論文・Kaggle 解法を参考に。 alpha, learning_rate は対数スケール、 max_depth は線形でよい。

Q5. 結果を信頼する条件は?

A. (1) CV スコアの fold 間ばらつきが小さい、 (2) ベスト候補が grid の境界にない、 (3) 独立 test set で検証済み、 の 3 点が揃ったとき。

🗺 概念マップ

グリッドサーチを中心に、ハイパーパラメータ探索の代替手法(ランダムサーチ・ベイズ最適化・Hyperband)、評価フレーム(交差検証・Nested CV)、scikit-learn の実装(GridSearchCV)、応用先(SVM のカーネル C/γ・Random Forest の n_estimators/max_depth・XGBoost の learning_rate/depth)を放射状に配置した。次元の呪い(高次元では指数的に組合せ爆発)と、ベイズ最適化への移行ポイントを矢印で示している。

グリッドサーチ 3 段階アプローチ ランダムサーチ 早期打ち切り 並列実験管理 探索ログの分析 交差検証 CV

グリッドサーチを中心とする概念マップは、 「ハイパーパラメータ最適化」「交差検証」「探索ログの分析」「Test set を絶対に見ない」の 4 つの隣接トピックで構成される。 scikit-learn の GridSearchCV は cv (交差検証分割数) と param_grid を同時に与えると、 (パラメータ組合せ数) × (cv 分割数) 回のモデル学習を自動実行し、 最良パラメータと CV スコアを返す。 探索空間が大きいときは RandomizedSearchCV (Bergstra-Bengio 2012)、 評価コストが高いときは BayesSearchCV (skopt) や Optuna の TPE で代替する流れが定番。

🔗 隣接手法への橋渡し

「グリッドサーチ」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。

上流のハイパーパラメータ範囲設定と評価指標選択で探索空間を定義し、 並列のランダムサーチ・ベイズ最適化・Hyperband と探索効率を比較し、 下流のクロスバリデーションで最適パラメータを評価する。 グリッドサーチは全探索の素朴版で、 次元の呪いに早く到達するため小規模問題か他手法のベースラインとして使う。

🌳 手法選択フロー

「グリッドサーチ」を実際の課題に当てはめるとき、 状況別に何を選ぶかを 3 段階で判定する。

  1. ハイパーパラメータ数は? 1〜3 個 → グリッドサーチで網羅、 4 個以上 → ランダムサーチ・ベイズ最適化が効率的
  2. 各試行のコストは? 軽 (数秒) → グリッドで OK、 重 (数時間) → ベイズ最適化・Hyperband で早期打ち切り併用
  3. 並列計算可能か? 可能 → グリッドの並列化で時間短縮、 不可 → 逐次的に賢く探索するベイズ最適化・遺伝的アルゴリズム

グリッドサーチは「全探索による安心感」と引き換えに次元の呪いを抱える。 4 次元以上は急速に非現実的になるため、 ランダムサーチを第一選択、 ベイズ最適化 (Optuna・Hyperopt) を最適化として用いる。