第4章:高度なアンサンブル技術

スタッキング、ブレンディング、Kaggle戦略による実践的アンサンブル構築

📖 読了時間: 32分 📊 難易度: 上級 💻 コード例: 8個 📝 演習問題: 4問

学習目標

この章を読むことで、以下を習得できます:


4.1 スタッキング(Stacking)の実践

スタッキングの概念

スタッキングは、複数のベースモデルの予測結果をメタモデルで学習する手法です。Votingと異なり、メタモデルが最適な組み合わせ方を自動的に学習します。

```mermaid
graph TB
    A[訓練データ] --> B1[Random Forest]
    A --> B2[XGBoost]
    A --> B3[LightGBM]
    A --> B4[Neural Network]

    B1 --> C[交差検証予測]
    B2 --> C
    B3 --> C
    B4 --> C

    C --> D[メタモデルLogistic Regression]
    D --> E[最終予測]

    style A fill:#e3f2fd
    style E fill:#c8e6c9
    style D fill:#fff3e0
```

基本的なスタッキングの実装

import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_predict, StratifiedKFold
from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, roc_auc_score
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier

# データ生成
X, y = make_classification(n_samples=2000, n_features=20, n_informative=15,
                          n_redundant=5, random_state=42)

# 訓練・テストに分割
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# ベースモデルの定義
base_models = [
    ('rf', RandomForestClassifier(n_estimators=100, random_state=42)),
    ('et', ExtraTreesClassifier(n_estimators=100, random_state=42)),
    ('xgb', XGBClassifier(n_estimators=100, random_state=42, eval_metric='logloss')),
    ('lgb', LGBMClassifier(n_estimators=100, random_state=42, verbose=-1)),
    ('knn', KNeighborsClassifier(n_neighbors=5))
]

# メタ特徴量の作成(交差検証による予測)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
meta_features_train = np.zeros((X_train.shape[0], len(base_models)))
meta_features_test = np.zeros((X_test.shape[0], len(base_models)))

for i, (name, model) in enumerate(base_models):
    print(f"Training {name}...")

    # 訓練データでの交差検証予測(out-of-fold predictions)
    meta_features_train[:, i] = cross_val_predict(
        model, X_train, y_train, cv=cv, method='predict_proba'
    )[:, 1]

    # テストデータでの予測
    model.fit(X_train, y_train)
    meta_features_test[:, i] = model.predict_proba(X_test)[:, 1]

# メタモデルの訓練
meta_model = LogisticRegression(random_state=42)
meta_model.fit(meta_features_train, y_train)

# 最終予測
y_pred = meta_model.predict(meta_features_test)
y_pred_proba = meta_model.predict_proba(meta_features_test)[:, 1]

print(f"\n=== Stacking Results ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"AUC: {roc_auc_score(y_test, y_pred_proba):.4f}")

# メタモデルの重み(各ベースモデルの重要度)
print(f"\nMeta-model coefficients:")
for (name, _), coef in zip(base_models, meta_model.coef_[0]):
    print(f"  {name}: {coef:.4f}")

mlxtendを使った簡潔な実装

from mlxtend.classifier import StackingCVClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC

# ベースモデル
clf1 = RandomForestClassifier(n_estimators=100, random_state=42)
clf2 = GradientBoostingClassifier(n_estimators=100, random_state=42)
clf3 = SVC(probability=True, random_state=42)

# メタモデル
meta_clf = LogisticRegression()

# StackingCVClassifier(自動的にCV予測を行う)
stacking = StackingCVClassifier(
    classifiers=[clf1, clf2, clf3],
    meta_classifier=meta_clf,
    cv=5,
    use_probas=True,  # 確率を使用
    random_state=42
)

# 訓練と評価
stacking.fit(X_train, y_train)
y_pred = stacking.predict(X_test)

print(f"Stacking Accuracy: {accuracy_score(y_test, y_pred):.4f}")

# 各モデルの個別性能と比較
for name, clf in [('RF', clf1), ('GB', clf2), ('SVC', clf3)]:
    clf.fit(X_train, y_train)
    acc = accuracy_score(y_test, clf.predict(X_test))
    print(f"{name} Accuracy: {acc:.4f}")

多層スタッキング(Multi-level Stacking)

from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from sklearn.linear_model import LogisticRegression, RidgeClassifier
from sklearn.neural_network import MLPClassifier

# レベル1のベースモデル
level1_models = [
    ('rf', RandomForestClassifier(n_estimators=100, random_state=42)),
    ('et', ExtraTreesClassifier(n_estimators=100, random_state=42)),
    ('xgb', XGBClassifier(n_estimators=100, random_state=42, eval_metric='logloss')),
    ('lgb', LGBMClassifier(n_estimators=100, random_state=42, verbose=-1))
]

# レベル2のメタモデル
level2_models = [
    ('lr', LogisticRegression(random_state=42)),
    ('ridge', RidgeClassifier(random_state=42)),
]

# レベル3の最終メタモデル
level3_model = MLPClassifier(hidden_layer_sizes=(10,), max_iter=1000, random_state=42)

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# レベル1: ベースモデルの予測
meta_l1_train = np.zeros((X_train.shape[0], len(level1_models)))
meta_l1_test = np.zeros((X_test.shape[0], len(level1_models)))

for i, (name, model) in enumerate(level1_models):
    print(f"Level 1 - Training {name}...")
    meta_l1_train[:, i] = cross_val_predict(
        model, X_train, y_train, cv=cv, method='predict_proba'
    )[:, 1]
    model.fit(X_train, y_train)
    meta_l1_test[:, i] = model.predict_proba(X_test)[:, 1]

# レベル2: メタモデルの予測
meta_l2_train = np.zeros((X_train.shape[0], len(level2_models)))
meta_l2_test = np.zeros((X_test.shape[0], len(level2_models)))

for i, (name, model) in enumerate(level2_models):
    print(f"Level 2 - Training {name}...")
    meta_l2_train[:, i] = cross_val_predict(
        model, meta_l1_train, y_train, cv=cv, method='decision_function'
    )
    model.fit(meta_l1_train, y_train)
    meta_l2_test[:, i] = model.decision_function(meta_l1_test)

# レベル3: 最終予測
print("Level 3 - Training final meta-model...")
level3_model.fit(meta_l2_train, y_train)
y_pred = level3_model.predict(meta_l2_test)
y_pred_proba = level3_model.predict_proba(meta_l2_test)[:, 1]

print(f"\n=== Multi-level Stacking Results ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"AUC: {roc_auc_score(y_test, y_pred_proba):.4f}")

4.2 ブレンディング(Blending)

ブレンディングとスタッキングの違い

項目スタッキングブレンディング
データ分割交差検証(全データ使用)ホールドアウト(一部をメタ訓練用)
計算コスト高い(CV分のモデル訓練)低い(1回のみ)
データ効率高い(全データ活用)やや低い(分割が必要)
過学習リスク低い(CVによる正則化)やや高い(分割次第)
実装の簡潔性やや複雑シンプル

重み付き平均ブレンディング

from scipy.optimize import minimize
from sklearn.metrics import log_loss

# データを訓練/ブレンド/テストに分割
X_train_base, X_blend, y_train_base, y_blend = train_test_split(
    X_train, y_train, test_size=0.2, random_state=42
)

# ベースモデルの訓練と予測
base_models = [
    ('rf', RandomForestClassifier(n_estimators=100, random_state=42)),
    ('xgb', XGBClassifier(n_estimators=100, random_state=42, eval_metric='logloss')),
    ('lgb', LGBMClassifier(n_estimators=100, random_state=42, verbose=-1))
]

blend_train = np.zeros((X_blend.shape[0], len(base_models)))
blend_test = np.zeros((X_test.shape[0], len(base_models)))

for i, (name, model) in enumerate(base_models):
    print(f"Training {name}...")
    model.fit(X_train_base, y_train_base)
    blend_train[:, i] = model.predict_proba(X_blend)[:, 1]
    blend_test[:, i] = model.predict_proba(X_test)[:, 1]

# 最適な重みを探索(log_lossを最小化)
def blend_loss(weights):
    """重み付き平均のlog_loss"""
    weights = weights / weights.sum()  # 正規化
    blended = np.dot(blend_train, weights)
    return log_loss(y_blend, blended)

# 制約:重みの合計=1、各重み≥0
constraints = {'type': 'eq', 'fun': lambda w: np.sum(w) - 1}
bounds = [(0, 1) for _ in range(len(base_models))]
initial_weights = np.ones(len(base_models)) / len(base_models)

# 最適化
result = minimize(blend_loss, initial_weights, method='SLSQP',
                 bounds=bounds, constraints=constraints)

optimal_weights = result.x / result.x.sum()

print(f"\n=== Optimal Blending Weights ===")
for (name, _), weight in zip(base_models, optimal_weights):
    print(f"{name}: {weight:.4f}")

# 最終予測
y_pred_proba = np.dot(blend_test, optimal_weights)
y_pred = (y_pred_proba > 0.5).astype(int)

print(f"\nBlending Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"Blending AUC: {roc_auc_score(y_test, y_pred_proba):.4f}")

ランク平均ブレンディング

from scipy.stats import rankdata

# 各モデルの予測をランクに変換
rank_train = np.zeros_like(blend_train)
rank_test = np.zeros_like(blend_test)

for i in range(len(base_models)):
    rank_train[:, i] = rankdata(blend_train[:, i]) / len(blend_train)
    rank_test[:, i] = rankdata(blend_test[:, i]) / len(blend_test)

# ランクの平均
rank_avg_train = rank_train.mean(axis=1)
rank_avg_test = rank_test.mean(axis=1)

# 評価
y_pred = (rank_avg_test > 0.5).astype(int)

print(f"\n=== Rank Averaging Results ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"AUC: {roc_auc_score(y_test, rank_avg_test):.4f}")

# 各モデルの個別性能と比較
print(f"\n=== Individual Model Performance ===")
for i, (name, _) in enumerate(base_models):
    pred = (blend_test[:, i] > 0.5).astype(int)
    print(f"{name} - Accuracy: {accuracy_score(y_test, pred):.4f}, "
          f"AUC: {roc_auc_score(y_test, blend_test[:, i]):.4f}")

4.3 Kaggleでのアンサンブル戦略

モデル多様性の確保

高性能なアンサンブルには、異なる特性を持つモデルの組み合わせが不可欠です:

多様性の源泉実装例効果
アルゴリズムツリー系、線形、ニューラルネット異なる決定境界
特徴量セット異なる特徴量エンジニアリング異なる情報源
ハイパーパラメータ深さ、学習率、正則化の違いバイアス・バリアンスの調整
サンプリングブートストラップ、CV foldデータの異なる視点
ランダムシード異なる初期化局所解の多様性

特徴量セットの多様化

import pandas as pd
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.decomposition import PCA

# サンプルデータ(実際のKaggleではコンペのデータを使用)
df = pd.DataFrame(X_train, columns=[f'feat_{i}' for i in range(X_train.shape[1])])
df['target'] = y_train

# 特徴量セット1: 元の特徴量 + 統計量
def create_feature_set1(X):
    features = pd.DataFrame(X)
    features['mean'] = X.mean(axis=1)
    features['std'] = X.std(axis=1)
    features['max'] = X.max(axis=1)
    features['min'] = X.min(axis=1)
    return features.values

# 特徴量セット2: 多項式特徴量
def create_feature_set2(X):
    poly = PolynomialFeatures(degree=2, include_bias=False)
    return poly.fit_transform(X[:, :5])  # 計算量削減のため最初の5特徴量のみ

# 特徴量セット3: PCA特徴量
def create_feature_set3(X):
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(X)
    pca = PCA(n_components=10)
    return pca.fit_transform(X_scaled)

# 各特徴量セットでモデルを訓練
feature_sets = [
    ('original', lambda X: X),
    ('statistics', create_feature_set1),
    ('polynomial', create_feature_set2),
    ('pca', create_feature_set3)
]

ensemble_predictions = []

for name, feature_func in feature_sets:
    print(f"\n=== Training with {name} features ===")

    X_train_feat = feature_func(X_train)
    X_test_feat = feature_func(X_test)

    # XGBoostで訓練
    model = XGBClassifier(n_estimators=100, random_state=42, eval_metric='logloss')
    model.fit(X_train_feat, y_train)

    pred_proba = model.predict_proba(X_test_feat)[:, 1]
    ensemble_predictions.append(pred_proba)

    print(f"AUC: {roc_auc_score(y_test, pred_proba):.4f}")

# アンサンブル予測(単純平均)
ensemble_avg = np.mean(ensemble_predictions, axis=0)
print(f"\n=== Ensemble with Diverse Features ===")
print(f"Ensemble AUC: {roc_auc_score(y_test, ensemble_avg):.4f}")

Kaggle戦略のフルパイプライン

from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score, roc_auc_score
import warnings
warnings.filterwarnings('ignore')

class KaggleEnsemble:
    """Kaggleコンペ用アンサンブルパイプライン"""

    def __init__(self, n_folds=5, random_state=42):
        self.n_folds = n_folds
        self.random_state = random_state
        self.base_models = []
        self.meta_model = None

    def add_base_model(self, name, model):
        """ベースモデルを追加"""
        self.base_models.append((name, model))

    def set_meta_model(self, model):
        """メタモデルを設定"""
        self.meta_model = model

    def fit(self, X, y):
        """アンサンブルモデルの訓練"""
        cv = StratifiedKFold(n_splits=self.n_folds, shuffle=True,
                            random_state=self.random_state)

        # メタ特徴量の初期化
        self.meta_features_ = np.zeros((X.shape[0], len(self.base_models)))

        # 各ベースモデルでOOF予測を生成
        for i, (name, model) in enumerate(self.base_models):
            print(f"Training {name}...")
            oof_predictions = np.zeros(X.shape[0])

            for fold, (train_idx, val_idx) in enumerate(cv.split(X, y)):
                X_train_fold, X_val_fold = X[train_idx], X[val_idx]
                y_train_fold, y_val_fold = y[train_idx], y[val_idx]

                model.fit(X_train_fold, y_train_fold)
                oof_predictions[val_idx] = model.predict_proba(X_val_fold)[:, 1]

            self.meta_features_[:, i] = oof_predictions

            # 全データで再訓練
            model.fit(X, y)

            # OOFスコア
            auc = roc_auc_score(y, oof_predictions)
            print(f"  {name} OOF AUC: {auc:.4f}")

        # メタモデルの訓練
        print("Training meta-model...")
        self.meta_model.fit(self.meta_features_, y)

        return self

    def predict_proba(self, X):
        """確率予測"""
        # ベースモデルの予測
        base_predictions = np.zeros((X.shape[0], len(self.base_models)))
        for i, (name, model) in enumerate(self.base_models):
            base_predictions[:, i] = model.predict_proba(X)[:, 1]

        # メタモデルで最終予測
        return self.meta_model.predict_proba(base_predictions)

    def predict(self, X):
        """クラス予測"""
        return self.meta_model.predict(
            np.column_stack([model.predict_proba(X)[:, 1]
                           for _, model in self.base_models])
        )

# 使用例
ensemble = KaggleEnsemble(n_folds=5, random_state=42)

# ベースモデルの追加(多様性を確保)
ensemble.add_base_model('rf', RandomForestClassifier(
    n_estimators=200, max_depth=10, random_state=42
))
ensemble.add_base_model('xgb', XGBClassifier(
    n_estimators=200, max_depth=6, learning_rate=0.1,
    random_state=42, eval_metric='logloss'
))
ensemble.add_base_model('lgb', LGBMClassifier(
    n_estimators=200, max_depth=8, learning_rate=0.05,
    random_state=42, verbose=-1
))

# メタモデルの設定
ensemble.set_meta_model(LogisticRegression(random_state=42))

# 訓練
print("=== Training Kaggle Ensemble ===\n")
ensemble.fit(X_train, y_train)

# テストデータで評価
y_pred_proba = ensemble.predict_proba(X_test)[:, 1]
y_pred = ensemble.predict(X_test)

print(f"\n=== Final Ensemble Performance ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"AUC: {roc_auc_score(y_test, y_pred_proba):.4f}")

4.4 実践プロジェクト:コンペティション用パイプライン

プロジェクト概要

二値分類コンペティションを想定した、完全なアンサンブルパイプラインを構築します。

要件


4.5 ベストプラクティスとトラブルシューティング

よくある失敗とその対策

問題原因対策
OOF予測が訓練に使われるデータリークで過学習必ず交差検証でOOF予測を生成
類似モデルのみ多様性不足異なるアルゴリズム・特徴量を使用
アンサンブルが個別より悪い弱いモデルが悪影響モデル選択・重み付けを導入
過度に複雑な構造オーバーエンジニアリングシンプルな構造から始める
計算時間が長すぎる非効率なパイプラインブレンディング・並列化を検討

本番環境での運用Tips

モデルのバージョン管理

推論速度の最適化

デバッグ手法

まとめ

アンサンブルの成功の鍵


練習問題

問題1:2層スタッキングの実装(難易度:中)

以下の要件で2層スタッキングモデルを実装してください:

問題2:最適なブレンド重みの探索(難易度:中)

3つのモデルの予測に対して、AUCを最大化する最適な重みを探索してください。scipy.optimize.minimizeを使用すること。

# ヒント
from scipy.optimize import minimize

def objective(weights):
    # 重み付き平均の予測
    blended = ...
    # AUCを最大化 → 負のAUCを最小化
    return -roc_auc_score(y_true, blended)

問題3:モデル多様性の分析(難易度:低)

5つのベースモデルの予測値の相関行列を計算し、ヒートマップで可視化してください。相関が0.95以上のペアを特定してください。

問題4:コンペティション用パイプライン(難易度:高)

以下を含む完全なパイプラインを構築してください:


参考文献・リソース

論文・書籍

実装リソース

Kaggleカーネル