本ページでは、 モデル選択を統合的に解説します。 AIC・BIC・ホールドアウト・k-fold CV・Stratified/Group/TimeSeries CV・グリッドサーチ・ベイズ最適化を一気通貫で扱います。
モデル選択は「複数候補モデルから汎化性能が最良のものを選ぶ」プロセス。 評価指標・分割設計・ハイパラ探索を統一的に理解することが重要です。
論文記事から各用語のリンクをクリックすると、 該当箇所が開きます:
| 章 | 内容 |
|---|---|
| 1. なぜモデル選択 | 汎化誤差の推定 |
| 2. バイアス・分散 | 汎化誤差分解 |
| 3. ホールドアウト | 最も単純な分割 |
| 4. k-fold CV | 標準的な交差検証 |
| 5. 特殊な分割 | 層化・グループ・時系列 |
| 6. AIC・BIC | 情報量規準 |
| 7. ハイパラ探索 | グリッド・ランダム・ベイズ |
| 8. データリーク | 最大の落とし穴 |
同じデータに対して多くのモデル候補(線形・木・NN等)と、 多くのハイパラ組合せ(深さ・学習率等)が存在。 訓練誤差は常にモデルを複雑にするほど小さくなるため、 訓練誤差で選ぶと過学習モデルが採用されてしまう。
そこで「独立な検証データでの誤差」を推定し、 それを最小化するモデルを選ぶ。 これがモデル選択の本質。
汎化誤差は次のように分解できる:
$$\mathbb{E}[(y - \hat{f}(x))^2] = \underbrace{(\mathbb{E}[\hat{f}]-f)^2}_{\text{バイアス}^2} + \underbrace{\mathbb{V}[\hat{f}]}_{\text{分散}} + \sigma^2$$
「単純すぎ」も「複雑すぎ」も悪く、 最適な複雑さは中間にある。 これを探すのがモデル選択。
データを訓練 / 検証 / テストに 3 分割(例 60/20/20)。
🎯 このコードでやること: 学習用と評価用にデータを分割。
1 2 3 4 | from sklearn.model_selection import train_test_split X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.4, random_state=42) X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42) # 60 / 20 / 20 |
💬 読み方: random_state=42 を固定すると再現性が確保される。
欠点:小さなデータでは分割の偶然で評価がぶれる。
データを k 個に分け、 順番に 1 つを検証、 残り k-1 で学習。 k 回の評価値を平均。
🎯 このコードでやること: SSDSE-B-2026 を読み込み、K-分割交差検証で評価、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import pandas as pd from sklearn.model_selection import cross_val_score, KFold from sklearn.linear_model import Ridge from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1) X = df[['一人当たり県民所得', '世帯人員', '高齢化率', '人口密度', '就業率']] y = df['持ち家比率'] pipe = Pipeline([('scale', StandardScaler()), ('ridge', Ridge(alpha=1.0))]) cv = KFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(pipe, X, y, cv=cv, scoring='neg_root_mean_squared_error') print(f'CV RMSE: {-scores.mean():.3f} ± {scores.std():.3f}') |
💬 読み方: skiprows=1 で英語ヘッダ行を飛ばし、 encoding='cp932' で文字化けを回避 / 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。
k=n の特殊ケース。 1 サンプルだけ抜いて学習を n 回繰り返す。 バイアスは小さいが計算コスト大・分散も大きい。
ハイパラ選定とモデル評価を別の CV で行う。 内側 CV でハイパラ選択、 外側 CV で性能評価。 「ハイパラ選択も含めた」公平な汎化推定。
🎯 このコードでやること: K-分割交差検証で評価、精度を評価。
1 2 3 4 5 6 | from sklearn.model_selection import GridSearchCV, KFold, cross_val_score inner = KFold(5, shuffle=True, random_state=1) outer = KFold(5, shuffle=True, random_state=2) grid = GridSearchCV(pipe, param_grid={'ridge__alpha': [0.01, 0.1, 1, 10]}, cv=inner) nested_scores = cross_val_score(grid, X, y, cv=outer, scoring='neg_root_mean_squared_error') print(f'Nested CV RMSE: {-nested_scores.mean():.3f}') |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。
分類タスクでクラス比率を各 fold で保つ。 不均衡データ・小データで必須。
🎯 このコードでやること: K-分割交差検証で評価。
1 2 3 4 | from sklearn.model_selection import StratifiedKFold skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for train_idx, val_idx in skf.split(X, y_binary): pass |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増。
同一グループ(個人・店舗等)のサンプルが訓練と検証に分かれないように。 リーク防止。
🎯 このコードでやること: K-分割交差検証で評価。
1 2 3 4 | from sklearn.model_selection import GroupKFold gkf = GroupKFold(n_splits=5) for train_idx, val_idx in gkf.split(X, y, groups=df['都道府県']): pass |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増。
時系列データで「未来→過去への漏れ」を防ぐ。 訓練は過去、 検証は未来。
🎯 このコードでやること: 「モデル選択」の最小コード。
1 2 3 4 | from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5, test_size=12, gap=0) for train_idx, val_idx in tscv.split(X_time): pass |
💬 読み方: 「モデル選択」の典型パターン。 列名や引数を変えると応用可能。
$$AIC = -2\log L + 2k$$
$L$ は最大尤度、 $k$ はパラメータ数。 小さいほど良いモデル。
$$BIC = -2\log L + k\log n$$
サンプル数 $n$ に対するペナルティが大きく、 真のモデルを選ぶ性質(一致性)を持つ。
🎯 このコードでやること: モデルを学習。
1 2 3 4 5 6 7 8 9 | import statsmodels.api as sm import statsmodels.formula.api as smf m1 = smf.ols('持ち家比率 ~ 一人当たり県民所得', data=df).fit() m2 = smf.ols('持ち家比率 ~ 一人当たり県民所得 + 世帯人員', data=df).fit() m3 = smf.ols('持ち家比率 ~ 一人当たり県民所得 + 世帯人員 + 高齢化率', data=df).fit() for name, m in [('M1', m1), ('M2', m2), ('M3', m3)]: print(f'{name}: AIC={m.aic:.1f}, BIC={m.bic:.1f}, R²={m.rsquared:.3f}') |
💬 読み方: 「モデル選択」の典型パターン。 列名や引数を変えると応用可能。
全組合せを試す。 確実だが指数爆発する。
🎯 このコードでやること: モデルを学習、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 12 | from sklearn.model_selection import GridSearchCV from sklearn.ensemble import RandomForestRegressor param_grid = { 'n_estimators': [100, 300, 500], 'max_depth': [None, 5, 10, 20], 'min_samples_leaf': [1, 2, 5], } gs = GridSearchCV(RandomForestRegressor(random_state=42), param_grid, cv=5, scoring='neg_root_mean_squared_error', n_jobs=-1) gs.fit(X, y) print('Best:', gs.best_params_, 'Score:', -gs.best_score_) |
💬 読み方: テスト指標が学習指標より極端に低い場合は過学習を疑う。
パラメータ空間からランダムサンプル。 グリッドより効率的(Bergstra & Bengio 2012)。
🎯 このコードでやること: モデルを学習、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 12 13 | from sklearn.model_selection import RandomizedSearchCV from scipy.stats import randint, uniform param_dist = { 'n_estimators': randint(100, 1000), 'max_depth': [None] + list(range(3, 30)), 'min_samples_leaf': randint(1, 20), } rs = RandomizedSearchCV(RandomForestRegressor(random_state=42), param_dist, n_iter=50, cv=5, scoring='neg_root_mean_squared_error', random_state=42, n_jobs=-1) rs.fit(X, y) print('Best:', rs.best_params_) |
💬 読み方: テスト指標が学習指標より極端に低い場合は過学習を疑う。
過去の試行から「次に試すべきパラメータ」をモデル化(ガウス過程・TPE)。 Optuna が標準。
🎯 このコードでやること: K-分割交差検証で評価、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import optuna from sklearn.model_selection import cross_val_score def objective(trial): params = { 'n_estimators': trial.suggest_int('n_estimators', 100, 1000), 'max_depth': trial.suggest_int('max_depth', 3, 30), 'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 20), } model = RandomForestRegressor(**params, random_state=42, n_jobs=-1) sc = cross_val_score(model, X, y, cv=5, scoring='neg_root_mean_squared_error') return -sc.mean() study = optuna.create_study(direction='minimize') study.optimize(objective, n_trials=100) print('Best:', study.best_params) |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。
検証データの情報が訓練に「漏れ」、 過大評価される現象。 モデル選択で最も致命的。
🎯 このコードでやること: K-分割交差検証で評価。
1 2 3 4 5 6 7 8 9 10 11 | from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score pipe = Pipeline([ ('scale', StandardScaler()), # 各 fold 内で fit ('clf', LogisticRegression()), ]) # Pipeline を渡すことで、 各 fold の訓練のみで前処理を fit score = cross_val_score(pipe, X, y, cv=5) |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増。
| 状況 | 推奨手法 |
|---|---|
| 大規模データ(n > 1万) | ホールドアウト or 3-fold CV |
| 中規模(n ≈ 100〜1000) | 5-fold or 10-fold CV |
| 小規模(n < 100) | LOOCV or 5-fold ×繰り返し |
| 分類・不均衡 | Stratified k-fold |
| 時系列 | TimeSeriesSplit |
| 同一個体複数 | GroupKFold |
| GLM・回帰の説明モデル | AIC / BIC |
| ML の予測モデル | CV + Pipeline |
| ハイパラ少(< 10 組) | グリッドサーチ |
| ハイパラ多(多次元) | ランダム or ベイズ最適化(Optuna) |
| 落とし穴 | 対処 |
|---|---|
| 前処理を全データに fit | 必ず Pipeline を使い、 各 fold 内で fit。 |
| テストデータでハイパラ選択 | 検証データを使う。 テストは最後の 1 回だけ。 |
| 同じCVでハイパラ選択と性能評価 | ネスト CV で分離。 |
| 時系列に通常 k-fold | TimeSeriesSplit を使う。 |
| 同一ユーザーが train/val 両方に | GroupKFold で防ぐ。 |
| 平均だけ報告 | 標準偏差 / 95% CI も報告。 |
| 乱数固定を忘れる | 必ず random_state を指定。 |
🎯 このコードでやること: K-分割交差検証で評価、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 | from sklearn.linear_model import LinearRegression, Ridge from sklearn.ensemble import RandomForestRegressor from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_score for name, m in [('LR', LinearRegression()), ('Ridge', Ridge(alpha=10)), ('RF', RandomForestRegressor(n_estimators=300, random_state=42))]: p = Pipeline([('s', StandardScaler()), ('m', m)]) sc = cross_val_score(p, X, y, cv=5, scoring='neg_root_mean_squared_error') print(f'{name}: {-sc.mean():.3f} ± {sc.std():.3f}') |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。
🎯 このコードでやること: 回帰モデルを学習、精度を評価。
1 2 3 4 5 6 | from sklearn.model_selection import GridSearchCV import numpy as np p = Pipeline([('s', StandardScaler()), ('m', Ridge())]) g = GridSearchCV(p, {'m__alpha': np.logspace(-3, 3, 10)}, cv=5, scoring='neg_root_mean_squared_error') g.fit(X, y) print('Best α:', g.best_params_, 'CV RMSE:', -g.best_score_) |
💬 読み方: テスト指標が学習指標より極端に低い場合は過学習を疑う。
🎯 このコードでやること: K-分割交差検証で評価、精度を評価。
1 2 3 4 5 6 | from sklearn.model_selection import KFold, cross_val_score, GridSearchCV inner = KFold(5, shuffle=True, random_state=1) outer = KFold(5, shuffle=True, random_state=2) g = GridSearchCV(p, {'m__alpha': np.logspace(-3, 3, 10)}, cv=inner) sc = cross_val_score(g, X, y, cv=outer, scoring='neg_root_mean_squared_error') print(f'Nested CV RMSE: {-sc.mean():.3f} ± {sc.std():.3f}') |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。
「Random Forest が最良でした。」
「Linear (RMSE 2.81)・Ridge (2.43)・RandomForest (2.12)・LightGBM (1.98) を Pipeline 化し 5-fold CV(n=47、 シャッフル、 random_state=42)で比較。 LightGBM が最良で、 RandomForest との差は標準偏差を考慮しても有意(5回繰り返し CV で paired t-test、 p < .01)。 LightGBM の最終ハイパラは Optuna 100 試行で選定(学習率 0.05、 num_leaves 31、 early_stopping 50)。 ネスト CV による汎化推定は 2.05 ± 0.18。」
| 用途 | クラス・関数 |
|---|---|
| 基本分割 | sklearn.model_selection.train_test_split |
| k-fold | KFold, StratifiedKFold, GroupKFold, RepeatedKFold |
| 時系列 | TimeSeriesSplit |
| CV 評価 | cross_val_score, cross_validate, cross_val_predict |
| グリッド | GridSearchCV, HalvingGridSearchCV |
| ランダム | RandomizedSearchCV |
| ベイズ最適化 | optuna, scikit-optimize, hyperopt |
| AIC/BIC | statsmodels の .aic, .bic 属性 |
| Pipeline | Pipeline, make_pipeline, ColumnTransformer |
| 学習曲線 | learning_curve, validation_curve |
関連概念をクリックで該当箇所へジャンプ。 既存索引と補完関係:
「持ち家比率」を目的変数として、 複数モデルを 5-fold CV で比較する完全再現例。 SSDSE-B-2026.csv の data/raw/ ディレクトリ配置を前提とします。
🎯 このコードでやること: SSDSE-B-2026 を読み込み、K-分割交差検証で評価、精度を評価。
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, numpy as np from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import KFold, cross_val_score, RepeatedKFold df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1) X = df[['一人当たり県民所得','世帯人員','高齢化率','人口密度','就業率']] y = df['持ち家比率'] models = { 'OLS': LinearRegression(), 'Ridge(α=1)': Ridge(alpha=1.0), 'Ridge(α=10)': Ridge(alpha=10.0), 'Lasso(α=0.1)': Lasso(alpha=0.1, max_iter=10000), 'ElasticNet': ElasticNet(alpha=0.1, l1_ratio=0.5, max_iter=10000), 'RF': RandomForestRegressor(n_estimators=300, random_state=42), 'GBM': GradientBoostingRegressor(n_estimators=200, random_state=42), } cv = RepeatedKFold(n_splits=5, n_repeats=10, random_state=42) print(f'{"model":15} {"RMSE_mean":>10} {"RMSE_std":>10}') for name, m in models.items(): p = Pipeline([('s', StandardScaler()), ('m', m)]) sc = cross_val_score(p, X, y, cv=cv, scoring='neg_root_mean_squared_error') print(f'{name:15} {-sc.mean():10.3f} {sc.std():10.3f}') |
💬 読み方: skiprows=1 で英語ヘッダ行を飛ばし、 encoding='cp932' で文字化けを回避 / 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。
| モデル | RMSE 平均 | RMSE SD | 解釈 |
|---|---|---|---|
| OLS | 3.24 | 0.71 | ベースライン |
| Ridge(α=10) | 3.18 | 0.68 | わずかに改善 |
| Lasso(α=0.1) | 3.22 | 0.72 | 変数選択効果 |
| RF | 3.45 | 0.81 | n=47 では葉が深くなりすぎ過学習 |
| GBM | 3.41 | 0.78 | 同上、 早期停止が必要 |
👉 結論:n=47 と小さいため、 線形系(Ridge)が安定。 木モデルは過学習傾向。 「データ量が少ない時は単純なモデル」というモデル選択の鉄則を体感できる例。
🎯 このコードでやること: K-分割交差検証で評価、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 12 13 | from scipy import stats from sklearn.model_selection import cross_val_score cv = RepeatedKFold(n_splits=5, n_repeats=20, random_state=42) s_ridge = -cross_val_score(Pipeline([('s', StandardScaler()), ('m', Ridge(alpha=10))]), X, y, cv=cv, scoring='neg_root_mean_squared_error') s_ols = -cross_val_score(Pipeline([('s', StandardScaler()), ('m', LinearRegression())]), X, y, cv=cv, scoring='neg_root_mean_squared_error') # 同じ fold での対応比較 t, p = stats.ttest_rel(s_ols, s_ridge) print(f't={t:.3f}, p={p:.4f}') print(f'Ridge は OLS より RMSE が小さい?: {(s_ridge < s_ols).mean():.0%} の fold で勝利') |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。
独立 t-test ではなく paired t-test を使うのが正解(同じ fold 内で評価したペアが対応関係にある)。 Dietterich (1998) の 5x2cv paired t-test も古典的選択肢。
Pipeline でくるみ、 各 fold 内で fit させる。 SSDSE-B(n=47)では fold 間で平均が大きく違うため特に深刻。 Optuna 等のハイパラ探索でも、 全データで fit した特徴量を使ってはいけない。
best_score_ を最終性能として報告するのは楽観バイアスを含む。 ハイパラ探索の過程で「たまたまその fold で当たった」結果が混入。 公平な汎化推定にはネスト CVを使い、 内側でハイパラ選択、 外側で性能評価と分離する。 計算コストは k²倍だが、 論文・コンペ・本番展開前には必須。 Cawley & Talbot (2010) の警告参照。
TimeSeriesSplit(ウォークフォワード)を使う。 季節性が強い場合は purged + embargo の k-fold(López de Prado)も検討。 SSDSE 時系列(パネル)データの分析では特に注意。
GroupKFold(県単位)または LeaveOneGroupOut で汎化評価する。 医療データの患者単位、 マーケの顧客単位も同様の注意が必要。
KFold(shuffle=True) や train_test_split で random_state を省略すると、 実行ごとに結果が変わる。 「先週は Ridge が勝ったが今週は OLS が勝った」となり議論不能。 全ての分割・モデル初期化・ハイパラ探索で random_state を指定。 さらに NumPy・PyTorch の seed も固定。 加えて PYTHONHASHSEED、 GPU 演算の決定論オプションまで考慮するのが完全再現の鉄則。
StratifiedKFold に pd.qcut(y, 5) をラベルとして渡す実装。 sklearn 1.5 以降は StratifiedShuffleSplit でも同様。
🎯 このコードでやること: K-分割交差検証で評価、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from sklearn.model_selection import RepeatedKFold, cross_validate cv = RepeatedKFold(n_splits=5, n_repeats=20, random_state=42) results = cross_validate( Pipeline([('s', StandardScaler()), ('m', Ridge(alpha=10))]), X, y, cv=cv, scoring=['neg_root_mean_squared_error', 'r2', 'neg_mean_absolute_error'], return_train_score=True, n_jobs=-1 ) import numpy as np print('RMSE:', -np.mean(results['test_neg_root_mean_squared_error'])) print('R² :', np.mean(results['test_r2'])) print('MAE :', -np.mean(results['test_neg_mean_absolute_error'])) print('train-test gap (R²):', np.mean(results['train_r2']) - np.mean(results['test_r2'])) |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。
🎯 このコードでやること: K-分割交差検証で評価、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import matplotlib.pyplot as plt from sklearn.model_selection import cross_val_score cv = RepeatedKFold(n_splits=5, n_repeats=30, random_state=42) scores = {} for name, m in [('OLS', LinearRegression()), ('Ridge', Ridge(alpha=10)), ('Lasso', Lasso(alpha=0.1, max_iter=10000)), ('RF', RandomForestRegressor(300, random_state=42))]: p = Pipeline([('s', StandardScaler()), ('m', m)]) scores[name] = -cross_val_score(p, X, y, cv=cv, scoring='neg_root_mean_squared_error') fig, ax = plt.subplots(figsize=(8, 4.5)) ax.boxplot(scores.values(), labels=scores.keys()) ax.set_ylabel('CV RMSE'); ax.set_title('Repeated 5-fold × 30 — 持ち家比率予測') plt.tight_layout(); plt.savefig('cv_compare.png', dpi=110) |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。
🎯 このコードでやること: モデルを学習。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import statsmodels.api as sm candidates = { 'M1: 県民所得のみ': ['一人当たり県民所得'], 'M2: + 高齢化率': ['一人当たり県民所得', '高齢化率'], 'M3: + 人口密度': ['一人当たり県民所得', '高齢化率', '人口密度'], 'M4: フル': ['一人当たり県民所得', '世帯人員', '高齢化率', '人口密度', '就業率'], } print(f'{"model":25} {"AIC":>8} {"BIC":>8} {"adj R²":>8}') for name, cols in candidates.items(): Xc = sm.add_constant(df[cols]) res = sm.OLS(df['持ち家比率'], Xc).fit() print(f'{name:25} {res.aic:8.1f} {res.bic:8.1f} {res.rsquared_adj:8.3f}') |
💬 読み方: 「モデル選択」の典型パターン。 列名や引数を変えると応用可能。
🎯 このコードでやること: K-分割交差検証で評価、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import optuna from sklearn.ensemble import RandomForestRegressor def objective(trial): params = { 'n_estimators': trial.suggest_int('n_estimators', 100, 800), 'max_depth': trial.suggest_int('max_depth', 2, 20), 'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 10), 'max_features': trial.suggest_float('max_features', 0.3, 1.0), } p = Pipeline([('s', StandardScaler()), ('m', RandomForestRegressor(random_state=42, **params))]) sc = cross_val_score(p, X, y, cv=5, scoring='neg_root_mean_squared_error') return -sc.mean() study = optuna.create_study(direction='minimize', sampler=optuna.samplers.TPESampler(seed=42)) study.optimize(objective, n_trials=50, show_progress_bar=False) print('Best RMSE:', study.best_value, ' params:', study.best_params) |
💬 読み方: 分割数 K を増やすほど分散は小さいが計算コスト増 / テスト指標が学習指標より極端に低い場合は過学習を疑う。
🎯 このコードでやること: 回帰モデルを学習、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 12 | from sklearn.experimental import enable_halving_search_cv # noqa from sklearn.model_selection import HalvingGridSearchCV import numpy as np p = Pipeline([('s', StandardScaler()), ('m', Ridge())]) search = HalvingGridSearchCV( p, param_grid={'m__alpha': np.logspace(-3, 3, 20)}, cv=5, scoring='neg_root_mean_squared_error', factor=3, random_state=42 ) search.fit(X, y) print('Best α:', search.best_params_, 'CV RMSE:', -search.best_score_) |
💬 読み方: テスト指標が学習指標より極端に低い場合は過学習を疑う。
🎯 このコードでやること: 回帰モデルを学習、精度を評価。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from sklearn.model_selection import GridSearchCV import numpy as np alphas = np.logspace(-3, 3, 30) g = GridSearchCV(Pipeline([('s', StandardScaler()), ('m', Ridge())]), {'m__alpha': alphas}, cv=10, scoring='neg_root_mean_squared_error', return_train_score=False) g.fit(X, y) # 最良 α より RMSE が「最良 + 1SE」以内で、 α が最大(最も正則化が強い=最も単純)を選ぶ means = -g.cv_results_['mean_test_score'] sds = g.cv_results_['std_test_score'] best_idx = means.argmin() threshold = means[best_idx] + sds[best_idx] eligible = np.where(means <= threshold)[0] one_se_idx = eligible[np.argmax(alphas[eligible])] print(f'best α = {alphas[best_idx]:.3f}, RMSE = {means[best_idx]:.3f}') print(f'1-SE α = {alphas[one_se_idx]:.3f}, RMSE = {means[one_se_idx]:.3f}(より頑健)') |
💬 読み方: テスト指標が学習指標より極端に低い場合は過学習を疑う。
🎯 このコードでやること: 学習用と評価用にデータを分割、モデルを学習、予測を取得。
1 2 3 4 5 6 7 8 9 10 11 12 | from sklearn.ensemble import HistGradientBoostingRegressor from sklearn.model_selection import train_test_split X_tr, X_va, y_tr, y_va = train_test_split(X, y, test_size=0.25, random_state=42) m = HistGradientBoostingRegressor( max_iter=1000, learning_rate=0.05, early_stopping=True, validation_fraction=0.2, n_iter_no_change=20, random_state=42 ) m.fit(X_tr, y_tr) print('停止 iter:', m.n_iter_) print('val RMSE :', ((m.predict(X_va)-y_va)**2).mean()**0.5) |
💬 読み方: random_state=42 を固定すると再現性が確保される。
「モデル選択」を本当に使いこなすには、 教科書的な定義だけでは足りません。 ここでは現場で役立つ追加の比喩・実例を整理します。 上の「🎨 直感で掴む」を補強する内容です。
「モデル選択」を厳密に書き下すと、 以下の形になります。 既出の数式と合わせて読むと、 概念の骨格が見えてきます。
追加の数式についても、 各記号を 1 つずつ「日本語」で言い換えます。 「数式を音読する」とは、 こういう作業のことです。
『教育用標準データセット SSDSE-B-2026』(47 都道府県、 約 100 変数)を題材に、 「モデル選択」を実際の数値で確認します。 数式が「動く感覚」を得ることが目的です。
| 対象 | 計算結果 |
|---|---|
| 線形回帰(k=3)と尤度 lnL=−120 → AIC | 246 |
| 同じデータで Polynomial 5 次(k=6, lnL=−110) → AIC | 232(より良い) |
| BIC(n=47, k=6, lnL=−110) → | 243(k=3 が選ばれる) |
5-fold CV を使って線形回帰/Ridge/Lasso/RandomForest を比較し、 最適モデルを選びます。
n=47 程度では RF など複雑モデルは分散が大きく、 線形系が有利になりやすい。 必ず CV の標準偏差を確認して「安定して良い」モデルを選ぶ。
既出の落とし穴に加えて、 中級者でも踏みやすい応用フェーズの罠を集めました。 1 度経験するか、 ここで読んでおけば回避できます。
「モデル選択」を題材にした 3 つの典型的な学習シナリオを示します。 自分のレベルに近いものから手を動かしてみてください。
この 3 ステップを 1 回でも回すと、 「知っている」から「使える」へと一段進めます。 学習効率の最も高い順序は、 「直感 → 数式 → コード → 別データ転用」の循環です。
「モデル選択」の理解度を 3 問で自己診断しましょう。 即答できなければ該当セクションに戻って復習。
3 問すべて即答できれば、 「モデル選択」は実用レベルに達しています。 関連用語ページに進みましょう。
「モデル選択」を実装に落とす際に、 教科書ではあまり強調されない実務的注意点を整理します。
numpy.float64 または decimal で明示。del、 もしくは numpy のビュー(view)で参照のみ。n_jobs=-1、 pandas は swifter、 NumPy は numexpr で高速化できる場面が多い。pytest)で境界条件(n=0, 1, 巨大値、 NaN)を必ず確認。logging で出力し、 後から再現できるようにする。 デバッグの時短に直結。pip freeze > requirements.txt で固定。 半年後の自分が泣かない最低限の保険。これらは「動けばよい」では済まされない場面、 たとえばコンペ提出・本番デプロイ・論文投稿で必須になります。 普段から意識すると、 いざという時に慌てません。
「モデル選択」を学んだ後、 次のチェックリストを 1 つずつ満たしているか確認してください。 これは『データサイエンス・リテラシー』として身につけるべき汎用スキルにも相当します。
8 項目すべてチェックがつけば、 「モデル選択」は実務でも論文でも自信を持って使えるレベルです。
「モデル選択」がどんな業界・分野で使われているか、 ざっと俯瞰しておくと、 「自分のドメインで使えるか?」の判断が早くなります。
| ドメイン | 「モデル選択」の典型用途 |
|---|---|
| 公的統計 | SSDSE のような都道府県データで、 地域特性の把握や政策効果の評価に使う |
| 金融 | 株価・為替・金利の予測、 リスク管理、 ポートフォリオ最適化 |
| 医療 | 疫学調査、 薬効評価、 画像診断、 遺伝子解析 |
| マーケティング | 顧客セグメンテーション、 LTV 予測、 A/B テスト、 推薦システム |
| 製造業 | 品質管理、 異常検知、 予知保全、 サプライチェーン最適化 |
| 教育 | 学習者モデル、 アダプティブ教材、 教育効果測定 |
自分のドメインがリストにあれば、 そこからすぐに着想を得られます。 リストにない場合も、 似たドメインの応用例から類推することで使い方が見えてきます。
「モデル選択」を起点に、 同カテゴリ「機械学習」を体系的に学ぶ推奨順序を示します。
📚 備考:6 週間は目安です。 自分のペースで進めて構いません。 重要なのは「定義 → 実装 → 関連用語 → 再構成」のサイクルを 1 度回し切ること。
tidyverse、 Julia では DataFrames.jl、 SQL では集約関数とウィンドウ関数で同様の処理が可能。 概念は言語によらず共通です。