論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
ハイパーパラメータ
Hyperparameter
ML基礎

🔖 キーワード索引

「ハイパーパラメータ」を取り巻く中核キーワード群です。

ハイパーパラメータ学習率グリッドサーチランダムサーチOptunaBayesian Optimizationcross-validation

ハイパーパラメータは、学習が始まる前に人が決める設定値(正則化の強さ α、木の深さ、近傍数 k、学習率など)で、学習で決まるパラメータ(係数・重み)と対になる。本ページでは SSDSE-B-2026 の 47 都道府県で、α・深さ・k を変えると結果がどう変わるか、選び方の良し悪しをどう測るかを確かめる。

定義:学習の外側で決める値bi-level 最適化の読み解きRidge の α が係数 β を縮める手計算GridSearchCV と同点の扱い人が決める k と学習が決める傾きbest_score_ と入れ子 CV探索戦略の選び方

💡 30秒で分かる結論 — ハイパーパラメータ

🍰 まずはやさしく

モデルの設定値のことです。

学習の進め方を決めるために使います。

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

結論と選び方について読みましょう。

📍 文脈 — どこで出会うか

🍰 まずはやさしく

プログラムで指定する数値のことです。

AIの性能を調整するために使います。

部活の練習メニューを決めるのと似ています。

どこでこの設定が出てくるか読みましょう。

scikit-learn で RandomForestClassifier(n_estimators=100, max_depth=5) と書いた瞬間、 あなたはハイパーパラメータを決めています。 n_estimators も max_depth も学習で自動には決まりません。 「なぜ 100 にしたの?」と聞かれて答えられないなら、 まずこの概念を押さえる必要があります。

🎨 直感で掴む

🍰 まずはやさしく

料理の火加減のようなものです。

いい結果を出すために調整します。

勉強のやり方を変える感覚に近いです。

直感的なイメージを掴みましょう。

料理のレシピで言えば、 火加減・調理時間 がハイパーパラメータ、 でき上がりの味 がパラメータ。

機械学習では:

🎨 概念図で押さえる — ハイパーパラメータ探索の3つの視点

ハイパーパラメータ調整を実務で使いこなすには、 (1) 正則化強度と汎化誤差の関係、 (2) 標本の大きさと「最適値」の安定性、 (3) クラスタ数のような離散ハイパーパラメータでの目的関数の形 の 3 つを併せて読むと理解が深まります。 ここでは SSDSE-B-2026 の実データで 3 枚の図を描き、 ハイパーパラメータ最適化の「土地勘」を一枚絵で身につけます。

2023 年度 47 都道府県の死亡率を 16 列で予測する Ridge と Lasso の、 α に対する訓練 MSE と検証 MSE
図A: 2023 年度 47 都道府県の死亡率(人口千人当たり)を、 高齢化率・出生率・消費支出・人口当たり病院数など標準化した 16 列で予測する Ridge と Lasso。 横軸が正則化強度 α(ハイパーパラメータ)、 青が訓練 MSE、 橙が 5 分割 × 20 回の繰り返し CV の検証 MSE。 訓練 MSE は α が大きいほど単調に増える(0.125 → Ridge 2.91 / Lasso 4.30)。 検証 MSE は α が小さい端で 0.41 前後、 Lasso では α = 0.1 で 0.256 まで下がる谷があり、 それより大きいと急に悪化する(未学習)。 Ridge の谷(α ≈ 0.16、 0.399)は浅く、 α が 10 以下ならほぼ横ばい。 Grid Search や Bayesian Optimization で探すのはこの検証誤差の底であり、 谷が深いか浅いかはモデルとデータで変わる。

読み取るポイント:

  • 訓練誤差は λ に対して単調 → λ だけで判断してはいけない
  • 検証誤差は谷を持つ(深さはモデル次第)→ 谷の位置がハイパーパラメータ最適値
  • Ridge と Lasso で谷の位置・形は異なる → モデル選択と一体で探索する
N=30・100・300 行の無作為抽出で 5 分割 CV が選んだ Ridge の α の分布
図B: ハイパーパラメータ探索で見落とされがちなのが、 標本が小さいと CV で選ばれる「最適値」そのものがぶれるという点である。 図 A と同じ 16 列・死亡率の問題で、 47 県 × 12 年度(564 行)から N = 30 / 100 / 300 行を 200 回ずつ無作為に抜き、 5 分割 CV で Ridge の α を選び直した分布(横軸は log10 α)。 選ばれた log10 α の標準偏差は N = 30 で 0.50、 N = 300 で 0.26。 10〜90% の範囲は N = 30 で 0.25〜1.25(α で 10 倍の幅)、 N = 300 で 0.50〜1.00(約 3 倍)に縮む。 N = 30 では 1 回だけ探索範囲の端(α = 10⁻³)が選ばれた。 同じ県の別年度が混ざる抽出なので、 独立な 300 件より楽観的な見積もりである。 探索のステップを増やす前に、 データの量と CV の設計を見直すべき場合が多い。

読み取るポイント:

  • N=47 (47 都道府県) なら K=5 より K=10 や LOO-CV を選ぶ
  • 標本が小さいと、 CV で選ばれる最適 α のばらつきが大きい
  • 探索回数を増やしても N が小さければ性能向上は頭打ち
2023 年度 47 都道府県の KMeans の k=1〜10 の inertia と k=2〜10 のシルエット係数
図C: クラスタ数 k も典型的なハイパーパラメータである。 図 A と同じ 16 列を標準化した 2023 年度 47 都道府県に KMeans(n_init=10)をかけた。 左の inertia(クラスタ内平方和)は 752.0(k=1)→ 593.4(k=2)→ 509.0(k=3)→ 372.4(k=5)と下がり続け、 はっきりした「肘」は見えない。 右のシルエット係数は k = 2 で最大 0.167、 k = 5 で 0.161 と僅差で、 どれも 0.2 未満(クラスタの分離が弱い)。 実データでは目的関数がこのように平らで、 1 つの指標だけで k を決め切れないことが多い。 目的関数の形(肘・谷・平ら・複数の山)を見てから探索戦略(Grid / Random / Bayesian / Hyperband)と判断材料を選ぶ。

読み取るポイント:

  • 肘や谷がはっきりしない目的関数 → 指標を複数並べ、 解釈できる k を選ぶ
  • U 字の谷を狙う場合 → Bayesian Optimization が有利
  • 複数極小がある場合 → Hyperband や進化計算で大域探索

この 3 枚を順に読むと、 「目的関数の形 → 探索戦略 → サンプルサイズの確認」 というハイパーパラメータ最適化の正攻法が一筆書きで身につく。 図 A → C → B の順に逆向きに辿るのが実務で有効: まず最適化対象 (図 C/A の形) を確かめ、 次に検証データの大きさ (図 B) を点検する。 関連用語 Optuna / クロスバリデーション / 正則化 へ進むと、 各図の理論的背景を実装と結びつけられる。

📐 定義・数式

🍰 まずはやさしく

数式で表した設定値のことです。

一番いい数値を見つけるために使います。

買い物の予算を決めるように選びます。

詳しい定義と計算方法を読みましょう。

【最適なハイパーパラメータ】
$$\boldsymbol{\lambda}^* = \arg\min_{\boldsymbol{\lambda}} \; L_{\text{val}}\big(\boldsymbol{\theta}^*(\boldsymbol{\lambda}),\; D_{\text{val}}\big)$$
$\boldsymbol{\lambda}$=ハイパー、 $\boldsymbol{\theta}^*(\boldsymbol{\lambda})$=そのハイパーで訓練して得た最適パラメータ、 $L_{\text{val}}$=検証損失

つまり 二段階最適化 です。 内側ループで $\boldsymbol{\theta}$ を訓練、 外側ループで $\boldsymbol{\lambda}$ を探索。

🔬 記号・要素の読み解き

ハイパーパラメータ $\boldsymbol{\lambda}$
学習率、 木の深さ、 バッチサイズなど 学習開始前に固定 する値。
パラメータ $\boldsymbol{\theta}$
ニューラルネットの重み、 回帰係数など 学習で更新 される値。
訓練損失 $L_{\text{train}}$
$\boldsymbol{\theta}$ を決めるための指標。 これだけ最小化すると過学習。
検証損失 $L_{\text{val}}$
$\boldsymbol{\lambda}$ を決めるための指標。 訓練に使わなかったデータで評価。
クロスバリデーション (CV)
データを k 個に分割し、 順に検証セットとして $L_{\text{val}}$ を平均。 安定した $\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 OptimizationGP / TPE で次の点を推定Optuna 標準
Hyperband / ASHA悪い試行を早期打ち切りリソース効率最大
Evolution Strategy遺伝的探索NAS にも応用
Manual Tuning人間の経験則実は強い場合あり

💥 現場の失敗例 5 件

「うまくいくケース」より「失敗ケース」のほうが学びが多い。 実プロジェクトで頻発する 5 つを示す。

失敗例 1:Test データを使ってチューニング → 汎化誤差を過小評価。 必ず Train / Val / Test の 3 分割か Nested CV
失敗例 2:Grid search の刻みが粗すぎて最適点を飛び越す → 対数刻み(1e-4, 1e-3, 1e-2)が定石
失敗例 3:Early stopping を Train Loss で判定 → Val Loss で判定すべし
失敗例 4:Random Seed を固定せず最良値を報告 → 再現不能。 必ず seed リスト 5 個で平均
失敗例 5:1 つのハイパラだけ動かし他を固定 → 相互作用を見落とす。 多変量同時探索すべし

🧮 実値で計算してみる

Random Forest で max_depth を {3, 5, 7, 10, None} の中から選ぶ場合:

max_depth訓練精度CV 精度(5-fold)
30.820.81
50.880.86
70.920.85
100.970.82
None1.000.78

選択 = max_depth=5。 訓練精度は 5 のほうが None より低いが、 CV 精度が最高 → 汎化性能が良い。

🧮 数式に値を入れて手で計算する: ベイズ最適化の獲得関数

合成データで EI (Expected Improvement) スコアを計算する。

Step 1: 候補点の予測

候補予測 μ不確実度 σEI
p10.820.050.0315
p20.780.150.0504
p30.850.020.0500

現在最高 f* = 0.80

Step 2: 次に評価する候補

EI = (μ - f*) Φ(z) + σ φ(z), z=(μ-f*)/σ p1: z = 0.02/0.05 = 0.40 → 0.02×Φ(0.40) + 0.05×φ(0.40) = 0.02×0.6554 + 0.05×0.3683 = 0.0131 + 0.0184 = 0.0315 p2: z = −0.02/0.15 = −0.133 → −0.02×Φ(−0.133) + 0.15×φ(−0.133) = −0.02×0.4470 + 0.15×0.3954 = −0.0089 + 0.0593 = 0.0504 p3: z = 0.05/0.02 = 2.50 → 0.05×Φ(2.50) + 0.02×φ(2.50) = 0.05×0.9938 + 0.02×0.0175 = 0.0497 + 0.0004 = 0.0500 p2 は μ が f* を下回るが σ が大きく探索の価値が高い → EI 最大 (0.0504) → 次選択 p3 は μ 最高だが σ が小さく上積みがほぼ μ − f* の 0.05 だけ → 0.0500 で僅差の 2 位

🐍 Python で再現

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()}")

📤 実行結果

EI: [0.032 0.05 0.05 ] 次候補 index: 1

💬 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 の順位は不確実度の見積もり次第で入れ替わる。

🧮 Ridge の係数を手で計算する — α(ハイパーパラメータ)が β(パラメータ)を縮める

ハイパーパラメータとパラメータの関係は、説明変数 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(%)yx−x̄y−ȳ(x−x̄)(y−ȳ)(x−x̄)²
東京都10.70.99−1.32−0.2980.393361.7424
秋田県9.11.10−2.92−0.1880.548968.5264
愛知県12.41.290.380.0020.000760.1444
島根県11.81.46−0.220.172−0.037840.0484
沖縄県16.11.604.080.3121.2729616.6464
合計2.178227.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 のときは最小二乗の傾き)と一致するかを確かめる。

📥 入力例 SSDSE-B-2026.csv の 2023 年度から 5 都県(東京都・秋田県・愛知県・島根県・沖縄県) x = 15 歳未満割合(%)= A1301 / A1101 × 100(小数 1 桁に丸め) y = 合計特殊出生率 A4103
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}')
📤 実行例(実測) 15歳未満割合 x: [10.7 9.1 12.4 11.8 16.1] 合計特殊出生率 y: [0.99 1.1 1.29 1.46 1.6 ] x̄ = 12.02, ȳ = 1.288 Σ(x−x̄)(y−ȳ) = 2.1782 Σ(x−x̄)² = 27.108 α = 0: 手計算 β = 0.0804 sklearn β = 0.0804 α = 1: 手計算 β = 0.0775 sklearn β = 0.0775 α = 10: 手計算 β = 0.0587 sklearn β = 0.0587 α = 100: 手計算 β = 0.0171 sklearn β = 0.0171

💬 平均 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 は切片には罰を掛けないので、平均を引いてから計算した手計算と一致する。

🐍 Python での扱い

最小再現コード。 SSDSE-B のような実データを前提に、 4〜8 行で動く例です:

📥 入力例(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
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_)
📤 実行例(実測) best: {'max_depth': 3} 0.9714285714285715

💬 訓練 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 位以下との差も確かめる。

🐍 Python 実装 — 4 段構え narration 付き

🎯 このコードでやること:SSDSE-B-2026 の 47 都道府県データで「総人口 → 着工新設住宅戸数(H1800)」を予測するランダムフォレストを作り、 max_depth を grid search する

📥 入力例: 入力(2023 年、 47 都道府県) Prefecture A1101(総人口) H1800(着工新設住宅戸数・戸) 北海道 5092000 28469 東京都 14086000 124810
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 最良 CV R² = 0.878

💬 結果の読み方: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 次元同時探索

📥 入力例: 前回と同じ X, y(47 サンプル、 1 特徴) X.shape = (47, 1) y.shape = (47,)
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)
📤 実行例(実測) Best R²: 0.878 Best params: {'n_estimators': 220, 'max_depth': 15, 'min_samples_leaf': 1}

💬 結果の読み方:Optuna の TPE は 30 trial で R² 0.878。 Grid search の 0.878 と同等の水準に到達した。 Bayesian は「次に試すべき点」を過去 trial から推定するため、 少ない試行で同水準へ効率よく収束する。

🎯 このコードでやること:Train/Val/Test を 60/20/20 で分け、 ハイパラ選択は Val、 最終評価は Test で行う 3 分割プロトコル

📥 入力例: 47 都道府県を 28/9/10 に分割 X.shape (47, 1)
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 最良 depth: 10, Val R²: 0.908 Test R²(最終評価): 0.950

💬 結果の読み方:Val で max_depth=10 が選ばれ、 別に取っておいた Test で 0.950。 今回は Test が Val をやや上回ったが、 これは N=47 と小さく分割の当たり外れ(分散)が大きいため。 いずれにせよ Test は探索に一切使わない独立評価であり、 これが honest な汎化性能。

🎯 このコードでやること:学習率 lr を 1e-4 〜 1e-1 まで対数刻みで grid 探索する場合のコード(決定木系では使わないが SGD/Neural Net 系を想定)

📥 入力例: 学習率の選択肢 lr_list = [1e-4, 1e-3, 1e-2, 1e-1]
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=0.0001 R² (train)=0.975 lr=0.0010 R² (train)=0.975 lr=0.0100 R² (train)=0.975 lr=0.1000 R² (train)=0.936

💬 結果の読み方:標準化後は 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² を表示する。

📥 入力例 SSDSE-B-2026.csv の 2023 年度 47 都道府県(年度で絞った 47 行) 目的変数: A4103 合計特殊出生率(平均 1.293、最小 東京都 0.99、最大 沖縄県 1.60) 説明変数: 高齢化率 = A1303/A1101、15 歳未満割合 = A1301/A1101、婚姻率 = A9101/A1101×1000、保育定員/15 歳未満 = J2505/A1301
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}')
📤 実行例(実測) alpha 高齢化率 15歳未満割合 婚姻率 保育定員/15歳未満 CV R² 0.01 0.1165 0.1318 0.0322 0.0006 0.811 1 0.1012 0.1273 0.0202 0.0025 0.807 10 0.0524 0.0989 -0.0080 0.0083 0.695 100 0.0112 0.0319 -0.0054 0.0064 0.215 1000 0.0012 0.0042 -0.0006 0.0010 -0.019

💬 α = 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² の平均を出す。

📥 入力例 SSDSE-B-2026.csv の 2023 年度 47 都道府県(年度で絞った 47 行) 目的変数: A4103 合計特殊出生率(平均 1.293、最小 東京都 0.99、最大 沖縄県 1.60) 説明変数: 高齢化率 = A1303/A1101、15 歳未満割合 = A1301/A1101、婚姻率 = A9101/A1101×1000、保育定員/15 歳未満 = J2505/A1301
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² 検証 R²(5-fold 平均) 差 1 0.486 0.389 0.097 2 0.708 0.459 0.249 3 0.887 0.480 0.407 4 0.958 0.369 0.589 5 0.985 0.380 0.606 6 0.997 0.477 0.520 8 1.000 0.430 0.570 12 1.000 0.430 0.570 検証 R² が最大の深さ: 3

💬 訓練 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² を表示する。

📥 入力例 SSDSE-B-2026.csv の 2023 年度 47 都道府県(年度で絞った 47 行) 目的変数: A4103 合計特殊出生率 説明変数: 高齢化率 = A1303/A1101、15 歳未満割合 = A1301/A1101、婚姻率 = A9101/A1101×1000、保育定員/15 歳未満 = J2505/A1301 探索: alpha = 10^-6 〜 10^3 を対数で 19 点、KFold(5, shuffle=True, random_state=0)
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}')
📤 実行例(実測) 各列の標準偏差: {'高齢化率': 0.0334, '15歳未満割合': 0.0107, '婚姻率': 0.4487, '保育定員/15歳未満': 0.0456} 標準化なし: 最良 α = 1e-06 CV R² = 0.811 (最良から 0.01 以内の α: 1e-06〜0.0001) α=1 のときの CV R² = -0.115 標準化あり: 最良 α = 0.1 CV R² = 0.811 (最良から 0.01 以内の α: 1e-06〜1) α=1 のときの CV R² = 0.807

💬 どちらも最良の 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 問

手を動かさないと身につかない。 5 問とも SSDSE-B-2026 を必ず触る前提で設計。

  1. SSDSE-B-2026 の総人口 A1101 を用いて、 ランダムフォレストで「総人口 → 着工新設住宅戸数(H1800)」回帰し、 `max_depth ∈ {2,3,5,10}` を grid search せよ
  2. 上記を `RandomizedSearchCV` で n_iter=20 にして比較せよ
  3. Optuna で `n_estimators ∈ [10, 500]`, `max_depth ∈ [2, 20]` を 50 trial 探索せよ
  4. Train/Val/Test を 60/20/20 で分け、 ハイパラ選択は Val、 最終評価は Test で行う一連の流れを書け
  5. 47 都道府県は標本が少ない。 5-fold CV と LeaveOneOut CV の MSE 差を比較し、 どちらを採用すべきか論じよ
解答の手がかり(本ページの実測値)
  • 問 1:上の 4 段構えの 1 つ目と同じ設定なら、最良は max_depth=5・CV R² 0.878。深さ 2 は 0.70 と表現力が足りず、5 以上(None でも 0.877)はほぼ横ばいになる。横ばいの範囲では、浅いほうを選んでも性能はほとんど変わらない。
  • 問 3:4 段構えの 2 つ目では、Optuna の 30 trial で R² 0.878(n_estimators=220・max_depth=15・min_samples_leaf=1)と、グリッドサーチと同じ水準に届いた。説明変数が総人口 1 列だけなので、どの方法で探しても頭打ちは同じになる。
  • 問 4:4 段構えの 3 つ目では、28/9/10 県に分けて検証 R² 0.908 で深さ 10 を選び、テスト R² は 0.950。テストが検証より高く出るのは、10 県しかないテストの顔ぶれによる揺れで、良い知らせとは読まない。
  • 問 5:LeaveOneOut は 47 回学習して 1 県ずつ測るので、1 県ごとの誤差のばらつき(東京都の 1 県で MSE が大きく動く)がそのまま平均に入る。5-fold と LOO で値を比べ、分け方による揺れ(⚠️ の実データで確かめる④で外側の分け方を 5 通り変えた方法)も合わせて見る。

🎮 触って理解する — 「人が決める k」vs「学習が決める傾き」

本ページの独自テーマは パラメータ(学習で決まる)とハイパーパラメータ(人が決める)の区別 です(探索手法の詳細は ハイパーパラメータチューニング のページへ)。 下の 2 つのデモを並べて触ると、 「同じ"数値"でも決まり方が正反対」であることが体感できます。 使用データはどちらも 乱数 seed 固定で生成した合成データ です(実測データではありません)。

🅰 kNN 分類 — k はハイパーパラメータ
k(近傍の数)はスライダーであなたが決めます。 kNN は訓練データを覚えるだけで、 k を学習で更新することは決してありません。
k = 7
●=訓練データ(40点) □=検証データ(20点・色付き枠) 黄色い縁の大きい点=新規の点(ドラッグ/タッチで移動可)。 線は k 個の近傍。
訓練精度
—
検証精度(これで k を選ぶ)
—
📋 簡易グリッドサーチ表(検証精度で k を比較)
k訓練精度検証精度(★=最高)
k=1 は訓練精度 100% なのに検証精度が低い(過学習)。 k を大きくしすぎても境界が鈍って精度が落ちる。 検証精度は山型 — その頂上を「人が」選ぶのがハイパーパラメータ選択。
🅱 線形回帰 — 傾き・切片はパラメータ
傾き β₁ と切片 β₀ を決めるスライダーはありません。 「学習実行」を押すと最小二乗法がデータから自動的に決めます。
空白をタッチ=点を追加、 点をドラッグ=移動。 データを変えてもパラメータは勝手に変わらない — もう一度「学習実行」して初めて更新されます。
傾き β₁
—
切片 β₀
—
RMSE
—
灰色の縦線は残差。 最小二乗法は「残差の 2 乗和が最小になる β₁, β₀」を計算で一意に求める — あなたの出る幕はないのがパラメータ。
👀 2 つを並べて観察するポイント
  1. k を動かすと境界の滑らかさが即座に変わる。 学習は走っていない — 変えたのはあなた。 検証精度は k=1 で低く(過学習)、 中間で最高、 大きすぎるとまた低下(山型)。
  2. 回帰の傾きはボタンを押すまで動かない。 押した瞬間、 データだけから β₁, β₀ が決まる。 スライダーで β₁ を選ぶ操作は存在しない。
  3. 回帰の点を動かしてもパラメータは変わらない(⚠ 表示が出る)。 再学習して初めて更新。 「パラメータの持ち主は学習アルゴリズム」だと分かる。
  4. グリッドサーチ表は「候補の k を全部試して検証精度で比べる」= グリッドサーチ の最小例そのもの。

🎨 直感の言語化 — 学習前に決める「ツマミ」 vs 学習で決まる「中身」

ハイパーパラメータは炊飯器の「炊き込みモード・火加減ボタン」=炊く前に人が押すツマミ。 パラメータは炊き上がったご飯の「水分量・柔らかさ」=炊く過程で自動的に決まる中身です。 コードで言えば、 KNeighborsClassifier(n_neighbors=7) の 7 は引数としてコンストラクタに渡す(=学習前に固定)。 一方 LinearRegression().fit(X, y) の後に model.coef_ を見ると係数が入っている(=fit が書き込んだ)。 scikit-learn の命名規則で末尾にアンダースコアが付く属性(coef_, intercept_, feature_importances_)は「学習で決まったパラメータ」、 コンストラクタ引数はハイパーパラメータ、 と覚えると迷いません。

⚠️ この区別を混同したときの落とし穴

🚀 発展 — 「人が決める」を自動化する

ハイパーパラメータは「人が決める」ものですが、 決め方自体はアルゴリズム化できます。 上の表のように全候補を試すのが グリッドサーチ、 候補をランダムに打つのがランダムサーチ(高次元では格子より効率的、 Bergstra & Bengio 2012)、 過去の試行から「次に有望な点」を推定するのがベイズ最適化(Optuna の TPE など)。 ただし自動化しても「探索空間・試行回数・評価指標を決める」のは依然として人間であり、 ハイパーパラメータの階層が一段上に移るだけという点は覚えておく価値があります。 探索手法の詳細は ハイパーパラメータチューニング、 個別モデルの例は kNN・線形回帰、 選び損ねた場合に起きる現象は 過学習 を参照してください。

⚠️ よくある落とし穴

ハイパーパラメータ を実務で扱うとき、 多くの分析者が同じところでつまずきます。 代表的な失敗パターンを先回りで押さえておくと、 後工程のトラブルを大幅に減らせます。

❌ テストデータでチューニング
テスト精度をハイパー選択に使うと、 テストデータが事実上の検証データ になり「真の性能」を評価できなくなります(情報リーク)。
❌ グリッドの粗さ
{0.01, 0.1, 1} のような粗いグリッドだと、 最適値 0.03 を逃します。 対数刻みかランダムサーチを。
❌ CV を省略
1回のホールドアウトだけでハイパーを決めると、 偶然の良い分割に最適化されがち。 k-fold CV で安定化を。
❌ 計算コストを軽視
5 ハイパー × 各 5 値で 3125 通り。 ランダムサーチ や Optuna など効率的手法を検討。
❌ Nested CV を怠る
ハイパー選択そのものに不確実性があります。 厳密な性能評価には nested CV が必要。

📊 実データで確かめる④:チューニングの効き目は入れ子 CV で測る — best_score_ は甘い

GridSearchCV の best_score_ は「候補の中でいちばん良かった CV スコア」であり、選んだ後の性能ではない。既定値のままのモデルと、GridSearchCV で調整したモデルを、外側の交差検証(入れ子 CV)で公平に比べる。

🎯 このコードでやること:Ridge(α を 13 通り)・k 近傍(k を 7 通り)・決定木(深さ 6 × 葉の最小 4 通り)について、既定値のままの CV R² と、内側 5 分割で調整してから外側 5 分割で測った R² を、外側の分け方 5 通りで平均する。比較のため、47 県全体に GridSearchCV を当てたときの best_score_ も表示する。

📥 入力例 SSDSE-B-2026.csv の 2023 年度 47 都道府県(年度で絞った 47 行) 目的変数: A4103 合計特殊出生率(平均 1.293、最小 東京都 0.99、最大 沖縄県 1.60) 説明変数: 高齢化率 = A1303/A1101、15 歳未満割合 = A1301/A1101、婚姻率 = A9101/A1101×1000、保育定員/15 歳未満 = J2505/A1301 既定値: Ridge(alpha=1)、KNeighborsRegressor(n_neighbors=5)、DecisionTreeRegressor(max_depth=None, min_samples_leaf=1)
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) 調整の効果 GridSearchCV の best_score_ Ridge 0.775 0.781 +0.006 0.811 k近傍 0.504 0.488 -0.016 0.582 決定木 0.294 0.302 +0.008 0.520

💬 入れ子 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 の値であり、小さなデータでは「調整してもほとんど変わらない」こと自体が大事な結果になる。

✅ 理解度チェック — このページの実測値で答える

  1. 🧮 の手計算で、α を 0 から 100 に上げると 15 歳未満割合の係数 β はいくつからいくつになるか。α が β を縮める理由を式で説明せよ。
    答え0.0804 → 0.0171。β = Σ(x−x̄)(y−ȳ) / (Σ(x−x̄)² + α) の分母に α が足されるため、分子(データで決まる)が同じでも α が大きいほど β は 0 に近づく。
  2. 🐍 の最初のコードで best_params_ が max_depth=3 になったのはなぜか。
    答え5 つの候補がすべて CV 正解率 0.971 で同点で、GridSearchCV は同点のとき候補リストの先頭を返すため。深さ 3 が良いという根拠にはならない。
  3. 実データで確かめる①で、婚姻率の係数の符号が変わるのは α がいくつのときか。そこから何を学ぶべきか。
    答えα = 1 で +0.020、α = 10 で −0.008 と符号が変わる。係数の解釈は α に依存するので、α の値と、それが CV で妥当な範囲かを必ず併記する。
  4. 実データで確かめる②で、訓練 R² が 1.000 になる深さと、検証 R² が最大の深さは?
    答え訓練 R² は深さ 8 以上で 1.000、検証 R² は深さ 3 の 0.480 が最大。訓練スコアで選ぶと最も深い木が選ばれてしまう。
  5. 実データで確かめる④で、決定木の best_score_ と入れ子 CV の値の差は? どちらを報告すべきか。
    答え0.520 と 0.302 で約 0.22 の差。選んだ後の性能として報告すべきは入れ子 CV の 0.302。
  6. 実データで確かめる③で、標準化せずに既定値 α=1 の Ridge を使うと CV R² はいくつになるか。なぜそうなるか。
    答え−0.115。15 歳未満割合の標準偏差が 0.0107 と小さく、その二乗和に比べて α=1 が大きすぎて係数が 0 近くに潰れるため。標準化すると同じ α=1 で 0.807 になる。

🧭 さらに一歩踏み込む — 直感・落とし穴・発展(追補)

ここは既存の解説を壊さずに追記した 深掘りセクション です。 数値はすべて 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 つを、 実測値とともに押さえます。

  1. テストデータでのチューニング(リーク):k や α をテスト精度で選ぶと、 テストが事実上の検証データになり、 最後に報告する性能が楽観的に水増しされます。 選択は検証セットか 交差検証で行い、 テストは一度きりの独立評価に温存します(本ページ Python 節の 60/20/20 分割例を参照)。
  2. 検証セットへの過剰適合:同じ検証データで何十通りも試すと、 その検証データにだけ当たる値を「引いて」しまいます。 とくに N=47 と小さいと分割の当たり外れ(分散)が大きく、 検証データの取り方 1 つで最適値が揺れます。 検証による選択と厳密評価は分けて考えます。
  3. 探索範囲・スケールの誤り(対数で刻む):正則化強度のように効きが桁で変わるツマミは、 等間隔の線形グリッドが致命傷になります。 下の Ridge の実測で、 最適は α≈1(CV R²=0.949)付近にあり、 α=100 で 0.473、 α=10000 では −0.022 と急速に崩壊します。
  4. 交互作用の無視:1 つのツマミだけ動かして他を固定すると、 max_depth×min_samples_leaf のような組み合わせでしか出ない最適を取り逃します。 多変量同時探索(グリッド/ランダム/Optuna)が基本です。
  5. 乱数シード依存:ランダムフォレストや分割は random_state で結果が動きます。 seed を 1 つだけ引いて最良値を報告すると再現不能。 複数 seed の平均で語ります。
  6. 計算コストと探索効率のトレードオフ:5 ハイパラ × 各 5 値で 3125 通り。 全探索が高価ならランダムサーチや早期打ち切りで効率を買います(後述)。
  7. デフォルト値の盲信:ライブラリの初期値は「無難な出発点」であって最適ではありません。 下の kNN 実測では、 既定の 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 を固定して初めて表の数字が再現できます。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) H1800(着工新設住宅戸数) 北海道 5,092,000 28,469 東京都 14,086,000 124,810 沖縄県 1,468,000 10,007 …(全 47 行)
 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}")

📤 実行結果:

α CV R² 0.01 0.944 0.1 0.944 1 0.949 10 0.938 100 0.473 1000 0.047 10000 -0.022 k CV R² 1 0.886 3 0.859 5 0.853 7 0.788 10 0.731 15 0.533 20 0.418

実測 ① 正則化強度 α のスイープ(Ridge・標準化・5-fold CV・R²)

α(正則化強度)0.010.1110100100010000
CV R²0.9440.9440.9490.9380.4730.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(近傍数)135(既定)7101520
CV R²0.8860.8590.8530.7880.7310.5330.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 は本用語集に単独ページが未整備のため、 本節のテキスト説明を参照してください。

🗺 概念マップ

関連概念を視覚的に整理した概念マップ。

hyperparameter グリッドサーチ ランダムサーチ ベイズ最適化 Successive Halving AutoML 交差検証 (CV)

上図の中心「ハイパーパラメータ」から、 探索戦略 (グリッド / ランダム / ベイズ / Successive Halving)、 自動化 (AutoML)、 落とし穴 (テストリーク等) の 5 軸が放射する。 これらは互いに代替・補完関係にあり、 計算予算とパラメータ数に応じて選び分ける (例: 5 次元以下ならグリッド、 10 次元以上ならランダム / ベイズ)。

🔗 隣接手法への橋渡し

ハイパーパラメータ調整は、 学習アルゴリズム本体 (パラメータ θ 推定) を外側から包む「メタ最適化」層で、 評価指標と組み合わせて初めて機能する。

ハイパーパラメータと学習パラメータの違いを混同するとリークが発生する: 学習データから推定するのが学習パラメータ (θ)、 学習データ外で決めるのがハイパーパラメータ (λ)。 探索時は必ず validation セットを分離する。

🌳 手法選択フロー

ハイパーパラメータ探索戦略を選ぶとき、 パラメータ次元数と計算予算で判定する。

  1. パラメータ数が 3 個以下か? Yes → グリッドサーチで全組み合わせ評価 (例: SVM の C, γ, kernel)、 No → 次へ
  2. 計算予算が制限される (数時間以内) か? Yes → ランダムサーチ + ホールドアウト で 50-100 試行、 No → ベイズ最適化 (Optuna / hyperopt) で目的関数を逐次更新しながら 200-500 試行
  3. 結果の最終評価: 探索で選んだ λ* を test セット(または入れ子 CV の外側)で一度だけ評価し、 探索中の best_score_ との差で「選び過ぎ」の度合いを確認する。 本ページの 47 県の実測では、 決定木で best_score_ 0.520 に対し入れ子 CV 0.302 と大きく開いた(⚠️ の実データで確かめる④)。 差が大きいときは候補を減らすか、 より単純なモデルに切り替える

初心者は「グリッドで全部試そう」と考えがちだが、 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 で確かめてから採用する。