「ハイパーパラメータ」を取り巻く中核キーワード群です。
ハイパーパラメータは、学習が始まる前に人が決める設定値(正則化の強さ α、木の深さ、近傍数 k、学習率など)で、学習で決まるパラメータ(係数・重み)と対になる。本ページでは SSDSE-B-2026 の 47 都道府県で、α・深さ・k を変えると結果がどう変わるか、選び方の良し悪しをどう測るかを確かめる。
🍰 まずはやさしく
モデルの設定値のことです。
学習の進め方を決めるために使います。
スマホのアプリ設定のようなものです。
結論と選び方について読みましょう。
lr、 決定木の深さ max_depth、 正則化強度 alpha、 ニューラルネットの層数。🍰 まずはやさしく
プログラムで指定する数値のことです。
AIの性能を調整するために使います。
部活の練習メニューを決めるのと似ています。
どこでこの設定が出てくるか読みましょう。
scikit-learn で RandomForestClassifier(n_estimators=100, max_depth=5) と書いた瞬間、 あなたはハイパーパラメータを決めています。 n_estimators も max_depth も学習で自動には決まりません。 「なぜ 100 にしたの?」と聞かれて答えられないなら、 まずこの概念を押さえる必要があります。
🍰 まずはやさしく
料理の火加減のようなものです。
いい結果を出すために調整します。
勉強のやり方を変える感覚に近いです。
直感的なイメージを掴みましょう。
料理のレシピで言えば、 火加減・調理時間 がハイパーパラメータ、 でき上がりの味 がパラメータ。
機械学習では:
lr=0.01、 隠れ層数 n_layers=3、 ドロップアウト率 p=0.5w、 線形回帰の係数 βハイパーパラメータ調整を実務で使いこなすには、 (1) 正則化強度と汎化誤差の関係、 (2) 標本の大きさと「最適値」の安定性、 (3) クラスタ数のような離散ハイパーパラメータでの目的関数の形 の 3 つを併せて読むと理解が深まります。 ここでは SSDSE-B-2026 の実データで 3 枚の図を描き、 ハイパーパラメータ最適化の「土地勘」を一枚絵で身につけます。
読み取るポイント:
読み取るポイント:
読み取るポイント:
この 3 枚を順に読むと、 「目的関数の形 → 探索戦略 → サンプルサイズの確認」 というハイパーパラメータ最適化の正攻法が一筆書きで身につく。 図 A → C → B の順に逆向きに辿るのが実務で有効: まず最適化対象 (図 C/A の形) を確かめ、 次に検証データの大きさ (図 B) を点検する。 関連用語 Optuna / クロスバリデーション / 正則化 へ進むと、 各図の理論的背景を実装と結びつけられる。
🍰 まずはやさしく
数式で表した設定値のことです。
一番いい数値を見つけるために使います。
買い物の予算を決めるように選びます。
詳しい定義と計算方法を読みましょう。
つまり 二段階最適化 です。 内側ループで $\boldsymbol{\theta}$ を訓練、 外側ループで $\boldsymbol{\lambda}$ を探索。
汎化誤差を $E(\theta, h)$ とすると、 通常の最適化は重み $\theta$ について $\min_\theta E$ を解くが、 ハイパーパラメータ $h$ は 学習の外側のループ で $\min_h \mathbb{E}_{\text{val}}\!\left[E(\hat{\theta}(h), h)\right]$ を解く 2 階層問題(bi-level)になる。
言い換えると、内側のループは「ハイパーパラメータ $h$ を固定して、訓練データで重み $\hat\theta(h)$ を求める」学習そのもの、外側のループは「$h$ を変えながら、検証データ(または交差検証)で内側の結果を採点し、最も良い $h$ を選ぶ」探索である。外側の採点には訓練に使っていないデータを使うことが肝心で、訓練データで採点すると、決定木なら深さ無制限、Ridge なら alpha=0 のように、いつも「最も複雑な設定」が選ばれてしまう。
ハイパーパラメータ と近接する手法群の位置づけを並べる。 隣接領域を押さえると、 自分のタスクで何を使うべきかが立体的に見えてくる。
| 手法 | 位置づけ | 代表ツール |
|---|---|---|
| Grid Search | 全格子点 | 次元の呪い |
| Random Search | ランダム k 点 | Bergstra & Bengio (2012) で、同じ試行回数なら grid より効く次元を細かく試せると示された |
| Bayesian Optimization | GP / TPE で次の点を推定 | Optuna 標準 |
| Hyperband / ASHA | 悪い試行を早期打ち切り | リソース効率最大 |
| Evolution Strategy | 遺伝的探索 | NAS にも応用 |
| Manual Tuning | 人間の経験則 | 実は強い場合あり |
「うまくいくケース」より「失敗ケース」のほうが学びが多い。 実プロジェクトで頻発する 5 つを示す。
Random Forest で max_depth を {3, 5, 7, 10, None} の中から選ぶ場合:
| max_depth | 訓練精度 | CV 精度(5-fold) |
|---|---|---|
| 3 | 0.82 | 0.81 |
| 5 | 0.88 | 0.86 |
| 7 | 0.92 | 0.85 |
| 10 | 0.97 | 0.82 |
| None | 1.00 | 0.78 |
選択 = max_depth=5。 訓練精度は 5 のほうが None より低いが、 CV 精度が最高 → 汎化性能が良い。
合成データで EI (Expected Improvement) スコアを計算する。
| 候補 | 予測 μ | 不確実度 σ | EI |
|---|---|---|---|
| p1 | 0.82 | 0.05 | 0.0315 |
| p2 | 0.78 | 0.15 | 0.0504 |
| p3 | 0.85 | 0.02 | 0.0500 |
現在最高 f* = 0.80
1 2 3 4 5 6 7 8 9 | import numpy as np from scipy.stats import norm mu = np.array([0.82, 0.78, 0.85]) sigma = np.array([0.05, 0.15, 0.02]) f_best = 0.80 z = (mu - f_best) / sigma EI = (mu - f_best) * norm.cdf(z) + sigma * norm.pdf(z) print(f"EI: {EI.round(3)}") print(f"次候補 index: {EI.argmax()}") |
💬 Step 2 の手計算 0.0315・0.0504・0.0500 を 3 桁に丸めると 0.032・0.050・0.050 で、Python 出力と一致する。p2 と p3 の差は 0.0004 しかなく、丸めた表示では同点に見えるが argmax は 1(p2)を返す。σ が 0.15 から少し下がるだけで p3 が逆転するので、EI の順位は不確実度の見積もり次第で入れ替わる。
ハイパーパラメータとパラメータの関係は、説明変数 1 つの Ridge 回帰なら手で追える。平均を引いた \(x, y\) について、Ridge の係数は次の式で決まる。
$$\hat\beta(\alpha) = \frac{\sum_i (x_i-\bar x)(y_i-\bar y)}{\sum_i (x_i-\bar x)^2 + \alpha}$$\(\alpha\) は人が決める値(ハイパーパラメータ)、\(\hat\beta\) はデータと \(\alpha\) から計算で決まる値(パラメータ)である。\(\alpha=0\) なら通常の最小二乗法の傾きになり、\(\alpha\) を大きくするほど分母が大きくなって \(\hat\beta\) は 0 に近づく。SSDSE-B-2026(2023 年度)の 5 都県で、15 歳未満人口の割合(%)\(x\) から合計特殊出生率 \(y\) を予測する。
Step 1: 平均からの差を取る(\(\bar x = 12.02\)、\(\bar y = 1.288\))
| 都県 | x(%) | y | x−x̄ | y−ȳ | (x−x̄)(y−ȳ) | (x−x̄)² |
|---|---|---|---|---|---|---|
| 東京都 | 10.7 | 0.99 | −1.32 | −0.298 | 0.39336 | 1.7424 |
| 秋田県 | 9.1 | 1.10 | −2.92 | −0.188 | 0.54896 | 8.5264 |
| 愛知県 | 12.4 | 1.29 | 0.38 | 0.002 | 0.00076 | 0.1444 |
| 島根県 | 11.8 | 1.46 | −0.22 | 0.172 | −0.03784 | 0.0484 |
| 沖縄県 | 16.1 | 1.60 | 4.08 | 0.312 | 1.27296 | 16.6464 |
| 合計 | 2.1782 | 27.108 |
Step 2: α を変えて β を計算する
Step 3: 読み取り 15 歳未満の割合が 1 ポイント高い県ほど出生率が 0.080 高い、という傾きが、\(\alpha=100\) では 0.017 まで縮む。同じデータでも、人が決めた \(\alpha\) しだいで「学習された」傾きが約 5 分の 1 になる。分母の \(\sum(x-\bar x)^2 = 27.1\) に対して \(\alpha\) がどれくらいの大きさかで効き方が決まるので、\(x\) の単位(% か割合か)を変えると同じ \(\alpha\) でも効き方が変わる。探索の前に標準化してそろえるのはこのためである。
🎯 このコードでやること:Step 1〜2 の手計算を numpy で再現し、sklearn の Ridge(α=0 のときは最小二乗の傾き)と一致するかを確かめる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import numpy as np, pandas as pd from sklearn.linear_model import Ridge df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].set_index('Prefecture') five = ['東京都', '秋田県', '愛知県', '島根県', '沖縄県'] x = (df.loc[five, 'A1301'] / df.loc[five, 'A1101'] * 100).round(1).values # 15 歳未満割合(%) y = df.loc[five, 'A4103'].values # 合計特殊出生率 print('15歳未満割合 x:', x, ' 合計特殊出生率 y:', y) xc, yc = x - x.mean(), y - y.mean() # Step 1: 平均を引く print(f'x̄ = {x.mean():.2f}, ȳ = {y.mean():.3f}') print('Σ(x−x̄)(y−ȳ) =', round((xc * yc).sum(), 4), ' Σ(x−x̄)² =', round((xc ** 2).sum(), 4)) for a in [0, 1, 10, 100]: # Step 2: β(α) = Σxy / (Σx² + α) b = (xc * yc).sum() / ((xc ** 2).sum() + a) b_sk = Ridge(alpha=a).fit(x.reshape(-1, 1), y).coef_[0] if a > 0 else np.polyfit(x, y, 1)[0] print(f'α = {a:>3}: 手計算 β = {b:.4f} sklearn β = {b_sk:.4f}') |
💬 平均 12.02・1.288、Σ(x−x̄)(y−ȳ) = 2.1782、Σ(x−x̄)² = 27.108 は Step 1 の表と一致し、α = 0, 1, 10, 100 の β = 0.0804・0.0775・0.0587・0.0171 も手計算・sklearn の両方で同じ値になる。sklearn の Ridge は切片には罰を掛けないので、平均を引いてから計算した手計算と一致する。
最小再現コード。 SSDSE-B のような実データを前提に、 4〜8 行で動く例です:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import pandas as pd from sklearn.model_selection import train_test_split # X_train / y_train を用意する(総人口が中央値以上かの 2 値分類) _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) _d = _d[_d['SSDSE-B-2026'] == 2023].reset_index(drop=True) _X = _d[['A1301', 'A1303', 'A4101', 'A9101']].astype(float) _y = (_d['A1101'] >= _d['A1101'].median()).astype(int) X_train, X_test, y_train, y_test = train_test_split( _X, _y, test_size=0.3, random_state=0, stratify=_y) from sklearn.model_selection import GridSearchCV from sklearn.ensemble import RandomForestClassifier param_grid = {'max_depth': [3, 5, 7, 10, None]} gs = GridSearchCV(RandomForestClassifier(random_state=0), param_grid, cv=5) gs.fit(X_train, y_train) print('best:', gs.best_params_, gs.best_score_) |
💬 訓練 32 県(test_size=0.3 で 15 県を取り置き)の 5 分割 CV で、max_depth=3 が正解率 0.971 で「最良」と出る。ただし cv_results_ を見ると、3・5・7・10・None の 5 通りがすべて 0.971 で同点で、GridSearchCV は同点のとき候補リストの先頭(ここでは 3)を返す。15 歳未満人口・65 歳以上人口・出生数・婚姻件数はどれも県の規模そのものなので、「総人口が中央値以上か」はどの深さでもほぼ当たり、深さを選ぶ根拠はこのデータには無い。best_params_ を見たら、cv_results_ で 2 位以下との差も確かめる。
🎯 このコードでやること:SSDSE-B-2026 の 47 都道府県データで「総人口 → 着工新設住宅戸数(H1800)」を予測するランダムフォレストを作り、 max_depth を grid search する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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]) d = df[df['SSDSE-B-2026'] == 2023] X = d[['A1101']].values y = d['H1800'].values param = {'max_depth': [2, 3, 5, 10, None]} gs = GridSearchCV(RandomForestRegressor(n_estimators=100, random_state=0), param, cv=5, scoring='r2') gs.fit(X, y) print('最良 max_depth:', gs.best_params_['max_depth']) print(f'最良 CV R² = {gs.best_score_:.3f}') |
💬 結果の読み方:max_depth=5 が選ばれ、 5-fold CV の R² が 0.88。 max_depth=2 は表現力不足(CV R² 0.70)、 max_depth=5 以上はほぼ横ばい(None でも 0.877)。 ちょうど中間で最適化。
🎯 このコードでやること:同じ問題を Optuna で Bayesian Optimization する。 `n_estimators`, `max_depth`, `min_samples_leaf` を 3 次元同時探索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import optuna, pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import cross_val_score df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026'] == 2023] X, y = d[['A1101']].values, d['H1800'].values def objective(trial): p = { 'n_estimators': trial.suggest_int('n_estimators', 50, 500), 'max_depth': trial.suggest_int('max_depth', 2, 15), 'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 8), } m = RandomForestRegressor(random_state=0, **p) return cross_val_score(m, X, y, cv=5, scoring='r2').mean() study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=0)) # seed で再現可能に study.optimize(objective, n_trials=30, show_progress_bar=False) print('Best R²:', round(study.best_value, 3)) print('Best params:', study.best_params) |
💬 結果の読み方:Optuna の TPE は 30 trial で R² 0.878。 Grid search の 0.878 と同等の水準に到達した。 Bayesian は「次に試すべき点」を過去 trial から推定するため、 少ない試行で同水準へ効率よく収束する。
🎯 このコードでやること:Train/Val/Test を 60/20/20 で分け、 ハイパラ選択は Val、 最終評価は Test で行う 3 分割プロトコル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import r2_score df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026'] == 2023] X, y = d[['A1101']].values, d['H1800'].values X_tv, X_te, y_tv, y_te = train_test_split(X, y, test_size=0.2, random_state=0) X_tr, X_va, y_tr, y_va = train_test_split(X_tv, y_tv, test_size=0.24, random_state=0) best_r2, best_d = -1, None for depth in [2, 3, 5, 10, None]: m = RandomForestRegressor(max_depth=depth, n_estimators=200, random_state=0).fit(X_tr, y_tr) r2_val = r2_score(y_va, m.predict(X_va)) if r2_val > best_r2: best_r2, best_d = r2_val, depth final = RandomForestRegressor(max_depth=best_d, n_estimators=200, random_state=0).fit(X_tv, y_tv) print(f'Val 最良 depth: {best_d}, Val R²: {best_r2:.3f}') print(f'Test R²(最終評価): {r2_score(y_te, final.predict(X_te)):.3f}') |
💬 結果の読み方:Val で max_depth=10 が選ばれ、 別に取っておいた Test で 0.950。 今回は Test が Val をやや上回ったが、 これは N=47 と小さく分割の当たり外れ(分散)が大きいため。 いずれにせよ Test は探索に一切使わない独立評価であり、 これが honest な汎化性能。
🎯 このコードでやること:学習率 lr を 1e-4 〜 1e-1 まで対数刻みで grid 探索する場合のコード(決定木系では使わないが SGD/Neural Net 系を想定)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import numpy as np from sklearn.linear_model import SGDRegressor from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026'] == 2023] X, y = d[['A1101']].values, d['H1800'].values for lr in [1e-4, 1e-3, 1e-2, 1e-1]: p = make_pipeline(StandardScaler(), SGDRegressor(eta0=lr, learning_rate='constant', max_iter=2000, random_state=0)) p.fit(X, y) score = p.score(X, y) print(f'lr={lr:.4f} R² (train)={score:.3f}') |
💬 結果の読み方:標準化後は lr=1e-4〜1e-2 で R² 0.975 と安定。 lr=0.1 まで上げると 0.936 とやや低下(更新幅が粗くなる)。 このデータでは壊れないが、 高次元 NN ではさらに発散しやすい。
上の手計算は 1 変数 5 都県だったが、47 都道府県・4 変数でも同じことが起きる。標準化した 4 つの率から合計特殊出生率を Ridge で予測し、α ごとに学習後の係数と交差検証の R² を並べる。
🎯 このコードでやること:標準化 → Ridge の Pipeline を α = 0.01〜1000 で学習し、各 α での係数(標準化後の説明変数 1 標準偏差あたりの出生率の変化)と 5 分割 CV の R² を表示する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import pandas as pd from sklearn.linear_model import Ridge from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.model_selection import cross_val_score, KFold df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True) pop = df['A1101'] X = pd.DataFrame({'高齢化率': df['A1303'] / pop, '15歳未満割合': df['A1301'] / pop, '婚姻率': df['A9101'] / pop * 1000, '保育定員/15歳未満': df['J2505'] / df['A1301']}) y = df['A4103'] # 合計特殊出生率 cv = KFold(5, shuffle=True, random_state=0) print('alpha ' + ' '.join(f'{c:>10s}' for c in X.columns) + ' CV R²') for a in [0.01, 1, 10, 100, 1000]: m = make_pipeline(StandardScaler(), Ridge(alpha=a)).fit(X, y) coef = m[-1].coef_ # 学習で決まるパラメータ(標準化後の係数) r2 = cross_val_score(make_pipeline(StandardScaler(), Ridge(alpha=a)), X, y, cv=cv).mean() print(f'{a:<8}' + ' '.join(f'{c:10.4f}' for c in coef) + f' {r2:.3f}') |
💬 α = 0.01 と 1 では係数も CV R²(0.811・0.807)もほとんど変わらないが、α = 10 で高齢化率の係数が 0.117 → 0.052 に半減し、婚姻率の係数は +0.032 から −0.008 へ符号まで変わる。α = 100 で CV R² は 0.215、α = 1000 ではすべての係数が 0.005 未満に潰れて −0.019(平均値で予測するより悪い)になる。「婚姻率が高い県ほど出生率が高い」かどうかという解釈そのものが α しだいで逆転するので、係数を読むなら、どの α で学習したかと、その α が CV で妥当な範囲かを一緒に示す。
同じ 4 変数・47 県で、決定木の深さを 1〜12 に変えながら、訓練データでの R² と 5 分割 CV の R² を並べる(sklearn の validation_curve)。
🎯 このコードでやること:決定木の max_depth を 1, 2, 3, 4, 5, 6, 8, 12 と変え、各深さで 5 分割それぞれの訓練 R² と検証 R² の平均を出す。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import numpy as np, pandas as pd from sklearn.tree import DecisionTreeRegressor from sklearn.model_selection import validation_curve, KFold df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True) pop = df['A1101'] X = pd.DataFrame({'高齢化率': df['A1303'] / pop, '15歳未満割合': df['A1301'] / pop, '婚姻率': df['A9101'] / pop * 1000, '保育定員/15歳未満': df['J2505'] / df['A1301']}) y = df['A4103'] # 合計特殊出生率 depths = [1, 2, 3, 4, 5, 6, 8, 12] tr, va = validation_curve(DecisionTreeRegressor(random_state=0), X, y, param_name='max_depth', param_range=depths, cv=KFold(5, shuffle=True, random_state=0), scoring='r2') print('深さ 訓練 R² 検証 R²(5-fold 平均) 差') for d, a, b in zip(depths, tr.mean(axis=1), va.mean(axis=1)): print(f'{d:4d} {a:.3f} {b:.3f} {a - b:.3f}') best = depths[int(np.argmax(va.mean(axis=1)))] print('検証 R² が最大の深さ:', best) |
💬 訓練 R² は深さ 1 の 0.486 から単調に上がり、深さ 8 で 1.000(47 県を丸暗記)になる。訓練スコアで選べば必ず最も深い木が選ばれる。検証 R² は深さ 3 の 0.480 が最大だが、深さ 2 の 0.459、深さ 6 の 0.477 との差は 0.02 程度で、深さ 4〜5 の 0.37〜0.38 に落ちた後また上がるなど、47 県では曲線がでこぼこする。この程度の差なら、同程度の中で最も浅い木(深さ 2〜3)を選ぶのが無難である。なお同じデータで Ridge は CV R² 0.81(①)なので、このデータでは決定木の深さを詰めるより、線形モデルを選ぶことのほうがはるかに効く。
🧮 の手計算で見たとおり、Ridge の α は \(\sum(x-\bar x)^2\) との大小で効き方が決まる。4 つの率は標準偏差が 0.01〜0.45 と 40 倍以上違うので、標準化しないまま α を探すとどうなるかを比べる。
🎯 このコードでやること:同じ 4 変数で、標準化なしの Ridge と、標準化 → Ridge の Pipeline のそれぞれについて α を 10^-6〜10^3 の 19 点で探し、最良の α、最良から CV R² が 0.01 以内に収まる α の範囲、既定値 α=1 のときの CV R² を表示する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import numpy as np, pandas as pd from sklearn.linear_model import Ridge from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.model_selection import GridSearchCV, KFold df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True) pop = df['A1101'] X = pd.DataFrame({'高齢化率': df['A1303'] / pop, '15歳未満割合': df['A1301'] / pop, '婚姻率': df['A9101'] / pop * 1000, '保育定員/15歳未満': df['J2505'] / df['A1301']}) y = df['A4103'] print('各列の標準偏差:', X.std().round(4).to_dict()) grid = np.logspace(-6, 3, 19) cv = KFold(5, shuffle=True, random_state=0) for name, m, key in [('標準化なし', Ridge(), 'alpha'), ('標準化あり', make_pipeline(StandardScaler(), Ridge()), 'ridge__alpha')]: g = GridSearchCV(m, {key: grid}, cv=cv).fit(X, y) r = pd.Series(g.cv_results_['mean_test_score'], index=grid) ok = r[r >= r.max() - 0.01].index # 最良から 0.01 以内の α の範囲 print(f'{name}: 最良 α = {g.best_params_[key]:.3g} CV R² = {g.best_score_:.3f} ' f'(最良から 0.01 以内の α: {ok.min():.3g}〜{ok.max():.3g}) α=1 のときの CV R² = {r[1.0]:.3f}') |
💬 どちらも最良の CV R² は 0.811 で同じだが、良い α の範囲がまるで違う。標準化なしでは 10^-4 以下でないと良くならず、既定値の α=1 では CV R² が −0.115 と平均値で予測するより悪い。15 歳未満割合の標準偏差が 0.0107 しかなく、そのばらつきの二乗和に比べて α=1 が大きすぎて係数がほぼ 0 に潰れるためである。しかも最良の α=10^-6 は探索範囲の下端なので、本当の最良はさらに小さいかもしれない。標準化すると 10^-6〜1 の広い範囲で同程度になり、既定値 α=1 でも 0.807 が出る。ハイパーパラメータの「良い値」は前処理と組で決まるので、前処理を変えたら探索し直す。
手を動かさないと身につかない。 5 問とも SSDSE-B-2026 を必ず触る前提で設計。
本ページの独自テーマは パラメータ(学習で決まる)とハイパーパラメータ(人が決める)の区別 です(探索手法の詳細は ハイパーパラメータチューニング のページへ)。 下の 2 つのデモを並べて触ると、 「同じ"数値"でも決まり方が正反対」であることが体感できます。 使用データはどちらも 乱数 seed 固定で生成した合成データ です(実測データではありません)。
| k | 訓練精度 | 検証精度(★=最高) |
|---|
ハイパーパラメータは炊飯器の「炊き込みモード・火加減ボタン」=炊く前に人が押すツマミ。 パラメータは炊き上がったご飯の「水分量・柔らかさ」=炊く過程で自動的に決まる中身です。 コードで言えば、 KNeighborsClassifier(n_neighbors=7) の 7 は引数としてコンストラクタに渡す(=学習前に固定)。 一方 LinearRegression().fit(X, y) の後に model.coef_ を見ると係数が入っている(=fit が書き込んだ)。 scikit-learn の命名規則で末尾にアンダースコアが付く属性(coef_, intercept_, feature_importances_)は「学習で決まったパラメータ」、 コンストラクタ引数はハイパーパラメータ、 と覚えると迷いません。
n_neighbors=5 や max_depth=None は「無難な初期値」であって最適値ではない。 上の表で k=5 と k=7 の検証精度が違ったように、 デフォルトのまま報告するのは「山型カーブの適当な 1 点」を選んだのと同じ。 最低限、 数点は比較する。ハイパーパラメータは「人が決める」ものですが、 決め方自体はアルゴリズム化できます。 上の表のように全候補を試すのが グリッドサーチ、 候補をランダムに打つのがランダムサーチ(高次元では格子より効率的、 Bergstra & Bengio 2012)、 過去の試行から「次に有望な点」を推定するのがベイズ最適化(Optuna の TPE など)。 ただし自動化しても「探索空間・試行回数・評価指標を決める」のは依然として人間であり、 ハイパーパラメータの階層が一段上に移るだけという点は覚えておく価値があります。 探索手法の詳細は ハイパーパラメータチューニング、 個別モデルの例は kNN・線形回帰、 選び損ねた場合に起きる現象は 過学習 を参照してください。
ハイパーパラメータ を実務で扱うとき、 多くの分析者が同じところでつまずきます。 代表的な失敗パターンを先回りで押さえておくと、 後工程のトラブルを大幅に減らせます。
{0.01, 0.1, 1} のような粗いグリッドだと、 最適値 0.03 を逃します。 対数刻みかランダムサーチを。GridSearchCV の best_score_ は「候補の中でいちばん良かった CV スコア」であり、選んだ後の性能ではない。既定値のままのモデルと、GridSearchCV で調整したモデルを、外側の交差検証(入れ子 CV)で公平に比べる。
🎯 このコードでやること:Ridge(α を 13 通り)・k 近傍(k を 7 通り)・決定木(深さ 6 × 葉の最小 4 通り)について、既定値のままの CV R² と、内側 5 分割で調整してから外側 5 分割で測った R² を、外側の分け方 5 通りで平均する。比較のため、47 県全体に GridSearchCV を当てたときの best_score_ も表示する。
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 28 29 30 31 32 33 | import numpy as np, pandas as pd from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import Ridge from sklearn.neighbors import KNeighborsRegressor from sklearn.tree import DecisionTreeRegressor from sklearn.model_selection import GridSearchCV, KFold, cross_val_score df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True) pop = df['A1101'] X = pd.DataFrame({'高齢化率': df['A1303'] / pop, '15歳未満割合': df['A1301'] / pop, '婚姻率': df['A9101'] / pop * 1000, '保育定員/15歳未満': df['J2505'] / df['A1301']}) y = df['A4103'] # 合計特殊出生率 models = { 'Ridge': (make_pipeline(StandardScaler(), Ridge()), {'ridge__alpha': np.logspace(-3, 3, 13)}), 'k近傍': (make_pipeline(StandardScaler(), KNeighborsRegressor()), {'kneighborsregressor__n_neighbors': [1, 2, 3, 5, 7, 10, 15]}), '決定木': (DecisionTreeRegressor(random_state=0), {'max_depth': [1, 2, 3, 4, 6, None], 'min_samples_leaf': [1, 3, 5, 8]}), } print('モデル 既定値のまま 調整あり(入れ子CV) 調整の効果 GridSearchCV の best_score_') for name, (m, grid) in models.items(): rs = [] for seed in range(5): # 外側の分け方を 5 通り変えて平均 outer = KFold(5, shuffle=True, random_state=seed) inner = KFold(5, shuffle=True, random_state=100 + seed) d = cross_val_score(m, X, y, cv=outer).mean() g = GridSearchCV(m, grid, cv=inner) t = cross_val_score(g, X, y, cv=outer).mean() rs.append((d, t)) d, t = np.mean(rs, axis=0) bs = GridSearchCV(m, grid, cv=KFold(5, shuffle=True, random_state=0)).fit(X, y).best_score_ print(f'{name:6s} {d:8.3f} {t:8.3f} {t - d:+.3f} {bs:.3f}') |
💬 入れ子 CV で測ると、調整の効果は Ridge +0.006、決定木 +0.008 とわずかで、k 近傍は −0.016 と既定値の k=5 より悪くなる。47 県では、内側の 4/5(約 30 県)で選んだ設定が外側の県では最適とは限らないからである。一方 best_score_ は Ridge 0.811・k 近傍 0.582・決定木 0.520 と、入れ子 CV の値(0.781・0.488・0.302)より 0.03〜0.22 高い。とくに候補が 24 通りある決定木で差が大きく、候補を増やすほど「たまたま良かった組」を拾って甘くなる。報告するのは入れ子 CV の値であり、小さなデータでは「調整してもほとんど変わらない」こと自体が大事な結果になる。
ここは既存の解説を壊さずに追記した 深掘りセクション です。 数値はすべて SSDSE-B-2026.csv(2023 年・47 都道府県、 encoding='cp932', skiprows=[1])の実測です(合成データは使っていません)。 題材は本ページで一貫して用いている「総人口 A1101 → 着工新設住宅戸数 H1800」の回帰で、 標準化(StandardScaler)+ 5-fold CV の R² を指標とします。
モデルの中には性質の違う 2 種類の数値が同居しています。 パラメータは 回帰の係数やニューラルネットの重みのように、 データを見て学習アルゴリズムが自動で書き込む「中身」。 一方 ハイパーパラメータは学習を始める前に人が固定する「ツマミ」で、 学習では 1 ミリも動きません。 ツマミはモデルの振る舞い(表現力・滑らかさ・正則化の強さ)を制御します。
| 種類 | 誰が決めるか | 代表例 | 制御する振る舞い |
|---|---|---|---|
| パラメータ(重み) | 学習アルゴリズム(fit) | 回帰係数 β、 ニューラルネットの重み w | データへの当てはめそのもの |
| ハイパーパラメータ(設定) | 人(または探索器) | 学習率、 木の深さ max_depth、 正則化強度 α、 kNN の k | 表現力・滑らかさ・過学習の抑制度 |
scikit-learn の命名で言えば、 コンストラクタ引数(Ridge(alpha=1.0) の alpha)がハイパーパラメータ、 末尾アンダースコア属性(coef_, intercept_)が学習で決まったパラメータ。 parameter(学習で決まる重み)そのものについては本用語集に単独ページが未整備のため、 ここでは対比としてテキストで押さえておいてください。
ハイパーパラメータ選びは「試して一番良かった値を採用」という単純作業に見えて、 実は評価の設計で結果が大きく歪みます。 頻出の 7 つを、 実測値とともに押さえます。
max_depth×min_samples_leaf のような組み合わせでしか出ない最適を取り逃します。 多変量同時探索(グリッド/ランダム/Optuna)が基本です。random_state で結果が動きます。 seed を 1 つだけ引いて最良値を報告すると再現不能。 複数 seed の平均で語ります。k=5(CV R²=0.853)よりこのデータでは k=1(0.886)がわずかに良く、 盲信すると R² を 0.03 取りこぼします。 ただし差は 0.03 で、 5-fold の切り方(seed)を変えると順位が入れ替わる程度の大きさです。 「既定が常に最適ではない」と同時に「小さな差で順位を語らない」も併せて覚えてください。🎯 このコードでやること: 下の 2 つの表をそのまま作ります。 説明変数は総人口(A1101)、 目的変数は着工新設住宅戸数(H1800)、 2023 年の 47 都道府県、 標準化してから 5-fold CV(shuffle=True, random_state=0)で R² を測ります。 分割の乱数が変わると小数第 2 位は動くので、 seed を固定して初めて表の数字が再現できます。
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 | # 実測 ① Ridge の正則化強度 α を対数で振って 5-fold CV R² を測る import pandas as pd from sklearn.linear_model import Ridge from sklearn.neighbors import KNeighborsRegressor from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_score, KFold 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"]].astype(float).values # 説明変数: 総人口 y = df["H1800"].astype(float).values # 目的変数: 着工新設住宅戸数 cv = KFold(n_splits=5, shuffle=True, random_state=0) print("α CV R²") for a in [0.01, 0.1, 1, 10, 100, 1000, 10000]: m = make_pipeline(StandardScaler(), Ridge(alpha=a)) print(f"{a:<8} {cross_val_score(m, X, y, cv=cv, scoring='r2').mean():.3f}") print() print("k CV R²") for k in [1, 3, 5, 7, 10, 15, 20]: m = make_pipeline(StandardScaler(), KNeighborsRegressor(n_neighbors=k)) print(f"{k:<3} {cross_val_score(m, X, y, cv=cv, scoring='r2').mean():.3f}") |
📤 実行結果:
実測 ① 正則化強度 α のスイープ(Ridge・標準化・5-fold CV・R²)
| α(正則化強度) | 0.01 | 0.1 | 1 | 10 | 100 | 1000 | 10000 |
|---|---|---|---|---|---|---|---|
| CV R² | 0.944 | 0.944 | 0.949 | 0.938 | 0.473 | 0.047 | −0.022 |
谷ではなく山(R² は大きいほど良い)で、 頂上は α=1。 ここで対数スケールの重要性が効きます: たとえば「大きめの範囲を広く見よう」と {2000, 4000, 6000, 8000, 10000} のような等間隔の線形グリッドを張ると、 全点が崩壊域(R²<0)に落ち、 最適の α≈1 を丸ごと飛び越します。 np.logspace(-2, 4, 7) のように対数で刻むのが定石です。 なお今回はたまたま既定 alpha=1.0 が好成績ですが、 これは題材依存であり「デフォルトだから安全」とは限りません。
実測 ② 近傍数 k のスイープ(kNN 回帰・標準化・5-fold CV・R²)
| k(近傍数) | 1 | 3 | 5(既定) | 7 | 10 | 15 | 20 |
|---|---|---|---|---|---|---|---|
| CV R² | 0.886 | 0.859 | 0.853 | 0.788 | 0.731 | 0.533 | 0.418 |
k は「学習で決まらない、 人が回すツマミ」の典型。 今回のように人口と住宅着工がほぼ単調・密に並ぶデータでは、 k を大きくするほど周辺を均しすぎて R² が単調に低下し、 k=1 が最良でした(k=20 では 0.418 まで劣化)。 ただし k=1 と k=5 の差は 0.033 しかなく、 seed を変えると k=3 が勝つこともあります。 一般には k=1 が過学習になりやすい(本ページ上部の kNN 分類ウィジェットは山型を示す)ため、 「最適 k は題材とサンプルサイズで変わる」ことを実測で確かめる姿勢が肝心です。 N=47 と小さいので、 交差検証の分割(K)や seed を変えて安定性も点検してください。
探索手法の総説は ハイパーパラメータチューニング、 個別手法は グリッドサーチ / Optuna / Nested CV。 評価基盤は 交差検証 / 検証データ / 検証による選択 / train/test 分割。 代表的ツマミは 正則化 / 学習率、 選び損ねると 過学習、 理論背景は バイアス・バリアンス。 前処理は 標準化、 調整対象モデルは ランダムフォレスト / kNN / 線形回帰 / XGBoost。 parameter(学習で決まる重み) と AutoML は本用語集に単独ページが未整備のため、 本節のテキスト説明を参照してください。
関連概念を視覚的に整理した概念マップ。
上図の中心「ハイパーパラメータ」から、 探索戦略 (グリッド / ランダム / ベイズ / Successive Halving)、 自動化 (AutoML)、 落とし穴 (テストリーク等) の 5 軸が放射する。 これらは互いに代替・補完関係にあり、 計算予算とパラメータ数に応じて選び分ける (例: 5 次元以下ならグリッド、 10 次元以上ならランダム / ベイズ)。
ハイパーパラメータ調整は、 学習アルゴリズム本体 (パラメータ θ 推定) を外側から包む「メタ最適化」層で、 評価指標と組み合わせて初めて機能する。
ハイパーパラメータと学習パラメータの違いを混同するとリークが発生する: 学習データから推定するのが学習パラメータ (θ)、 学習データ外で決めるのがハイパーパラメータ (λ)。 探索時は必ず validation セットを分離する。
ハイパーパラメータ探索戦略を選ぶとき、 パラメータ次元数と計算予算で判定する。
初心者は「グリッドで全部試そう」と考えがちだが、 5 パラメータ × 5 値 = 3125 試行 × k-fold で計算量が爆発する。 ランダムサーチの方がベイズ最適化より単純で、 同じ予算で広範囲を探索できるため実務上の標準。
探索の前に決めておくことが 2 つある。1 つは前処理で、標準化するかどうかで良い α の範囲が 4 桁ずれる(実データで確かめる③:標準化なしでは α≤10^-4、標準化ありでは 10^-6〜1)。もう 1 つはモデルの種類で、同じ 4 変数・47 県でも Ridge の CV R² 0.81 に対し決定木は深さをどう選んでも 0.48 程度にとどまる(実データで確かめる①②)。ハイパーパラメータの探索は、この 2 つを決めた後の「仕上げ」と考え、データが小さいときは既定値との差を入れ子 CV で確かめてから採用する。