TL;DR
- 階層型アンサンブル(Hierarchical Ensemble)は、モデルを段階(レベル)に分けて組み合わせる設計パターンの総称
- 例:
- ①セグメント別モデル
- ②ゲート+専門家(Mixture of Experts / HME)
- ③多段スタッキング
- 例:
- 目的は異質性(セグメント差)や複雑な非線形を捉えつつ、過学習を制御して汎化性能を高めること
- 成功の鍵はリーク防止(OOF生成・ネストCV)、ゲートの安定化、セグメントの十分なサンプル確保
1. 階層型アンサンブルとは?
1.1 位置づけと用語
- Bagging:同一学習器を並列に学習(例:Random Forest)
- Boosting:弱学習器を逐次的に学習(例:XGBoost, LightGBM)
- Stacking:複数モデルの予測をメタ学習器に入力して最終予測
- 階層型アンサンブル:上記を段階的・階層的に組み合わせる設計
- 例:
- レベル1で分割
- レベル2で専門家
- 任意でレベル3でブレンディング/校正
- 例:
1.2 代表パターン
- セグメント別モデル(Segment-wise Models):セグメントごとに最適化したモデルを学習
- ゲーティング+専門家(Mixture of Experts / HME):ゲートが入力ごとの担当“専門家”を選び、重み付き合成で最終予測
- 多段スタッキング(Multi-level Stacking):レベル1のOOF予測→レベル2メタ学習器→必要なら更に多段化
直感イメージ
(入力X)
│
[レベル1: 分割/基学習器] ──→ 例A: ゲートで {専門家1,2,3} へ振分
│ 例B: 複数基学習器のOOFを作る
▼
[レベル2: 専門家 or メタ学習器]
▼
[レベル3: ブレンディング/校正(任意)]
▼
(予測 ŷ)
2. いつ使うべき?
- ユーザー群や商品群で挙動が違う(異質性が強い、ロングテール)
- 単一モデルの改善が頭打ちで、多様な視点を組み合わせたい
- 入力モダリティが多様(数値+テキスト+画像など)で、部分ごとに最適なモデルが異なる
- 業務ロジックの反映(例:法人/個人で別決定構造)
3. 設計手順
- 問題の分解:KPI/制約(レイテンシ・解釈性・メモリ)を明確化
- 異質性の診断:探索的可視化、残差の群間差、エラー分布
- パターン選定:セグメントが明瞭→セグメント別モデル、連続的な切替/自動化→ゲート+専門家、多様性の活用→多段スタッキング
- データ分割設計:外側CV(ネストCV)を先に固定
- OOF生成(スタッキング):メタ学習器にはOOFのみ渡す
- ハイパラ探索:上位ほど保守的、下位専門家は柔軟
- 評価:全体指標+セグメント別指標、安定性テスト
- 解釈/運用設計:ゲート挙動監視、フォールバック、モデル登録
- 本番導入:推論経路ロギングと可観測性
- 継続学習:ドリフト検知、再学習スケジュール
4. 評価とリーク防止
- ネスト交差検証(Outer/Inner)で外側の完全な保留セットを確保。
- OOF予測でメタ学習器に渡す特徴を作成(学習時に見た対象の予測は使わない)。
- セグメント別評価:各セグメントでの Precision/Recall/MAE/AUC など。
- 安定性:ブートストラップでスコア分布、勝率(一定改善を達成した割合)を併記。
- キャリブレーション:分類は信頼性図(reliability diagram)で校正を確認。
5. 実装例(Python / scikit-learn)
注意:最小構成です。実運用ではネストCV、前処理、キャリブレーション等を追加してください。
5.1 多段スタッキング(回帰)
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split, KFold
from sklearn.ensemble import RandomForestRegressor, HistGradientBoostingRegressor
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.base import clone
# データ
X, y = fetch_california_housing(return_X_y=True, as_frame=False)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
def oof_predictions(model, X, y, X_test, n_splits=5, seed=42):
kf = KFold(n_splits=n_splits, shuffle=True, random_state=seed)
oof = np.zeros(len(X))
test_preds = np.zeros((len(X_test), n_splits))
for i, (trn_idx, val_idx) in enumerate(kf.split(X)):
m = clone(model)
m.fit(X[trn_idx], y[trn_idx])
oof[val_idx] = m.predict(X[val_idx])
test_preds[:, i] = m.predict(X_test)
return oof.reshape(-1, 1), test_preds.mean(axis=1).reshape(-1, 1)
# レベル1:基学習器
base1 = RandomForestRegressor(n_estimators=300, random_state=42, n_jobs=-1)
base2 = HistGradientBoostingRegressor(random_state=42)
# レベル1:OOF 生成(リーク防止の肝)
oof1_train, oof1_test = oof_predictions(base1, X_train, y_train, X_test)
oof2_train, oof2_test = oof_predictions(base2, X_train, y_train, X_test)
# レベル2:メタ学習器(単純なRidge)
meta = make_pipeline(StandardScaler(), Ridge(alpha=1.0))
# メタに渡す特徴は OOF のみ
X_meta_train = np.hstack([oof1_train, oof2_train])
X_meta_test = np.hstack([oof1_test, oof2_test])
meta.fit(X_meta_train, y_train)
y_pred = meta.predict(X_meta_test)
from sklearn.metrics import mean_absolute_error
print('MAE:', mean_absolute_error(y_test, y_pred))
ポイント
- メタ学習器に与えるのはOOFで、ここでリークを断つ
- レベル1のテスト予測は分割平均で頑健化
- レベル2以降を増やす場合も同じ作法を踏襲
5.2 セグメント別モデル+ゲート(分類)
import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin, clone
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.cluster import KMeans
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
X, y = load_breast_cancer(return_X_y=True, as_frame=False)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
class SegmentWiseClassifier(BaseEstimator, ClassifierMixin):
'''
1) KMeansでセグメント(例示)
2) セグメントごとに学習器(LogisticRegression)を学習
3) 予測時は担当セグメントの学習器で確率を出す
'''
def __init__(self, n_segments=3, base_estimator=None, random_state=42):
self.n_segments = n_segments
self.random_state = random_state
self.base_estimator = base_estimator or LogisticRegression(max_iter=1000)
def fit(self, X, y):
self.kmeans_ = KMeans(n_clusters=self.n_segments, random_state=self.random_state, n_init=10)
seg = self.kmeans_.fit_predict(X)
self.models_ = []
for s in range(self.n_segments):
est = clone(self.base_estimator)
est.fit(X[seg==s], y[seg==s])
self.models_.append(est)
return self
def predict_proba(self, X):
seg = self.kmeans_.predict(X)
proba = np.zeros((len(X), 2))
for s in range(self.n_segments):
idx = np.where(seg==s)[0]
if len(idx)==0:
continue
proba[idx] = self.models_[s].predict_proba(X[idx])
return proba
def predict(self, X):
return (self.predict_proba(X)[:,1] >= 0.5).astype(int)
clf = SegmentWiseClassifier(n_segments=3)
clf.fit(X_train, y_train)
proba = clf.predict_proba(X_test)[:,1]
print('AUC:', roc_auc_score(y_test, proba))
ポイント
- ここでは簡単のためKMeansでセグメント化しましたが、業務ロジックや決定木の葉でセグメント化する方が解釈しやすいことが多いです
- 実運用では、ゲート(セグメント決定)も外側CV内で学習し、セグメント別スコアを必ず確認します
6. 運用・MLOps 観点
- 推論経路のロギング:入力→どのゲート/セグメント→どの専門家→最終予測を記録
- フォールバック:ゲート信頼度が低い/セグメント外れ値は全体モデルにフォールバック
- 監視:全体スコアだけでなくセグメント別スコア/分布を監視。入力分布・エラー分布のドリフト、ゲートの分配比率変化を検知
- 軽量化:専門家が多いとレイテンシ悪化するため、蒸留や特徴量の共有で抑制
7. よくある落とし穴と対策
| 落とし穴 | 何が起きるか | 対策 |
|---|---|---|
| OOFを作らない | メタ学習器でリーク → 過大評価 | OOF徹底、ネストCV |
| セグメント細分化しすぎ | 各セグメントのサンプル不足 | 最小件数しきい値、セグメント統合 |
| 不安定なゲート | 入力の微小変化で予測が揺れる | ゲート正則化、温度付きSoftmax、スムージング |
| 本番分布の変化 | セグメント分布が移動 | 分配比率監視、再学習・再セグメント |
| 監視が1指標のみ | 劣化に気づけない | 全体+セグメント別の複数指標を儀式化 |
8. 設計チェックリスト
- KPI・制約(レイテンシ/メモリ/解釈性)を明確化
- 外側CVを先に固定(分割の単位=リーク源の単位で)
- スタッキングは必ずOOFで作成
- セグメントは最小件数・意味解釈の両軸で妥当性確認
- セグメント別評価+安定性評価(CI/ブートストラップ)
- 推論経路ロギング・可観測性・フォールバック設計
- 再学習・ドリフト対応の運用計画
9. HME(Hierarchical Mixture of Experts)との関係
- HMEは、ゲートが専門家の出力を確率的に加重して最終予測を行う特定モデル族
- 階層型アンサンブルはより広い概念で、HMEはその一種
- 実務では、HME的な連続重み付けと、セグメント別の離散振り分けを使い分ける
10. まとめ
- 階層型アンサンブルは、異質性と複雑性を扱うためのアーキテクチャ
- OOF・ネストCV・セグメント別評価が成功の三本柱
- 小さく始めて、分割→専門家→ブレンディングを順に足していくと安全に伸ばせる