実務に載せる ― 解釈・運用(MLOps-lite)とオンライン最適化への接続
公開ノート後半の定番(チューニング・重要度・SHAP・スタッキング)を解剖し、そこから実務に載せる工程へ降ろします。データの取り決め・ドリフト監視・再学習トリガの運用、そしてオフライン予測からオンライン意思決定へ渡すバンディット/ベイズ最適化の入口まで。コンペには無い実務固有の出口を扱います。
by Shin
モデルができた後、実務で「載せて・効かせ続ける」には何が要るのか。答えを先に言うと、重要度を正しく解釈して説明責任を果たし、ドリフトを監視して再学習で運用し、A/Bの先にあるバンディットやベイズ最適化で配分を回す、という三つです。この回では、公開ノート後半の定番(チューニング・重要度・SHAP・スタッキング)を解剖したうえで、コンペには無い「実務の出口」――運用とオンライン最適化――まで降ろします。
前提は第6回(決定木からGBDTまで)と第7回(評価指標と、予測確率を実際の発生率に合わせる較正)です。学習済みのGBDTがあり、その出力を意思決定に翻訳する段階に来ている、というところから始めます。
この回で解けるようになること:
- ハイパラ最適化が「何を最適化しているのか」を言える
- 重要度3種(gain / permutation / SHAP)の違いと、「重要≠因果」を説明できる
- スタッキングの過学習リスクと、OOFがなぜ要るかを言える
- 運用(データの取り決め・ドリフト監視・再学習)の論点を持てる
- A/Bの先(バンディット・ベイズ最適化)の位置づけを言える
公開ノートの後半は、写経すると一番「動くのに分からない」区画です。study.optimize() も shap.summary_plot() も一行で動いてしまう。だからこそ、その下で何が起きているかを解読しておかないと、実務に移したときに静かに事故ります。
ハイパラ最適化 ― Optunaのベイズ最適化が探しているもの
公開ノートの定番に、Optunaでハイパーパラメータを自動探索するセルがあります。写経すると「とりあえずスコアが上がる箱」に見えますが、中でやっているのは素朴な総当たりではありません。
グリッドサーチは候補を格子状に全部試し、ランダムサーチは範囲からランダムに引きます。どちらも「過去の試行結果を次の試行に使わない」のが特徴です。一方、Optunaの既定(TPE)は過去の試行のうち良かった領域とそうでない領域を確率分布として推定し、次に試す価値が高そうな点を選ぶ――これがベイズ最適化の発想です。少ない試行で良い領域へ寄せていくため、探索回数あたりの効率が違います。
要点だけ見れば、目的関数は「ハイパラを受け取り、CVスコアを返す関数」に過ぎません。
import optuna
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score
import lightgbm as lgb
import numpy as np
def objective(trial):
params = {
"objective": "binary",
"metric": "auc",
"learning_rate": trial.suggest_float("learning_rate", 1e-3, 0.3, log=True),
"num_leaves": trial.suggest_int("num_leaves", 16, 256),
"min_child_samples": trial.suggest_int("min_child_samples", 5, 200),
"subsample": trial.suggest_float("subsample", 0.5, 1.0),
"colsample_bytree": trial.suggest_float("colsample_bytree", 0.5, 1.0),
"reg_lambda": trial.suggest_float("reg_lambda", 1e-3, 10.0, log=True),
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = []
for tr, va in cv.split(X, y):
# 前処理(エンコード・補完)はこのループの内側で fit する=リーク防止
model = lgb.LGBMClassifier(n_estimators=2000, **params)
model.fit(
X.iloc[tr], y.iloc[tr],
eval_set=[(X.iloc[va], y.iloc[va])],
callbacks=[lgb.early_stopping(100, verbose=False)],
)
pred = model.predict_proba(X.iloc[va])[:, 1]
scores.append(roc_auc_score(y.iloc[va], pred))
return np.mean(scores)
study = optuna.create_study(direction="maximize") # 既定はTPE(ベイズ的探索)
study.optimize(objective, n_trials=50)その下にある数学は、第4回の勾配降下と同じ「目的関数を動かす」発想です。違うのは、目的関数(CVスコア)が滑らかでも微分可能でもないこと。だから勾配は使えず、代わりに獲得関数――「ここを試すとどれだけ改善が期待できるか」を測る関数――を最大化する点を次に選びます。期待改善(EI)なら のような形で、現在の最良値 をどれだけ更新できそうかを評価します。式の細部より、「未知の関数を、観測しながら賢く探索する」という枠組みを掴むのが先です。
実務での注意は別にあります。コンペのスコアは固定データに対する一発勝負ですが、実務のスコアは運用が始まると劣化します。50回チューニングして絞り出した小数第三位の改善は、分布シフトが一度来れば消えます。チューニングは「効くが、効く範囲が狭い」道具だと見積もっておくのが安全です。
特徴量重要度の落とし穴
公開ノート終盤の「重要度ランキング」は、実務でそのまま施策会議に持ち込まれやすい区画です。ここで「写経」と「解読」の差が一番効きます。重要度には少なくとも三種あり、測っているものが違います。
import shap
from sklearn.inspection import permutation_importance
# ① gain(学習中の分割で得た損失減少の合計)― モデル内部の指標
gain = model.booster_.feature_importance(importance_type="gain")
# ② permutation(検証データで特徴量をシャッフルし、性能劣化を測る)
perm = permutation_importance(
model, X_valid, y_valid, scoring="roc_auc",
n_repeats=10, random_state=42,
)
# ③ SHAP(各予測への各特徴量の寄与を加法的に分解する)
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_valid)gain / split による重要度
gain は「学習中にその特徴量での分割が損失をどれだけ減らしたか」の合計、split は「何回分割に使われたか」です。どちらも学習データ・モデル内部の都合を映します。だから連続値やカーディナリティの高いカテゴリは、分割の機会が多いぶん過大評価されやすい。検証性能と無関係に高く出ることがあるため、施策の根拠にするには弱い指標です。
permutation importance
permutationは「その特徴量の値をシャッフルすると、検証性能がどれだけ落ちるか」を測ります。汎化への寄与を直接見る点でgainより実務向きです。ただし弱点があり、相関の強い特徴量が複数あると、片方をシャッフルしてももう片方が情報を補ってしまい、両方とも重要度が低く出ます(多重共線性の罠。第2回で予告した論点です)。
SHAP(寄与の分解)と「重要≠因果」
SHAPは協力ゲーム理論のシャープレイ値を使い、一つひとつの予測を「ベースライン+各特徴量の寄与」に加法分解します。「全体でどれが効くか」だけでなく「この顧客のスコアがなぜ高いのか」を説明できるのが強みで、運用での説明責任に向きます。
ただし、ここで連載を貫く誤解の一つを置き直します。重要度が高い ≠ 因果がある。SHAPが大きい特徴量は「モデルの予測を強く動かす」だけで、「その変数を操作すれば結果が変わる」とは限りません。解約予測で「最終ログインからの経過日数」のSHAPが大きくても、ログインを無理に増やせば解約が減る、とは言えない――経過日数は解約しかけている状態の結果かもしれないからです。
架空のD2CブランドSOLNAでの実演はこうです。第6回で subscriptions と orders から作ったChurn(解約)モデルのSHAPを見ると、「直近90日の購入回数」「サポート問い合わせ回数」などが上位に来ます。これを施策仮説に翻訳するとき、購入回数は施策(リマインド配信・クーポン)で動かせる候補、問い合わせ回数は不満の兆候――と仕分けます。重要度ランキングをそのまま「ここに予算を」とやると、結果変数に介入して空振りします。
スタッキング / ブレンドの仕組みと過学習リスク
コンペ上位ノートの最終盤に出てくるのがスタッキングです。複数モデルの予測を、さらに別のモデル(メタモデル)でまとめます。仕組み自体は単純ですが、メタモデルに何を食わせるかで天国と地獄が分かれます。
素朴にやると、各ベースモデルが学習に使ったのと同じ行に対する予測をメタモデルへ渡してしまいます。これはベースモデルが「答えを見た行」での予測なので、楽観的すぎる入力になり、メタモデルはそれに過学習します。これを避けるのがOOF(out-of-fold)予測――各モデルが学習に使っていない行だけの予測を集める手続きです。
from sklearn.model_selection import StratifiedKFold
from sklearn.linear_model import LogisticRegression
import numpy as np
def make_oof(model_factory, X, y, n_splits=5):
oof = np.zeros(len(X))
cv = StratifiedKFold(n_splits, shuffle=True, random_state=42)
for tr, va in cv.split(X, y):
m = model_factory()
m.fit(X.iloc[tr], y.iloc[tr])
oof[va] = m.predict_proba(X.iloc[va])[:, 1] # 学習に使っていない行だけに予測
return oof
oof_lgb = make_oof(lambda: lgb.LGBMClassifier(**best_params), X, y)
oof_lr = make_oof(lambda: LogisticRegression(max_iter=1000), X, y)
# メタモデルは「各モデルが見ていない予測(OOF)」だけを入力に学習する
meta_X = np.column_stack([oof_lgb, oof_lr])
meta = LogisticRegression().fit(meta_X, y)OOFはこの連載で何度も出てきた「学習と評価を分ける」という同じ原則の、もう一つの顔です。第5回のCVも、第7回の較正も、ここのスタッキングも、根は「答えを見た行で性能を測ってはいけない」という一点に帰着します。
実務では、もう一段引いた判断が要ります。スタッキングはコンペでは数bps(小数点以下)を取りにいく価値がありますが、運用するモデルが増えるほど、監視・再学習・障害対応のコストが線形に増えます。単一のGBDTで足りる業務に三段スタックを積むのは、たいてい割に合いません。「コンペで効く ≠ 実務で効く」の典型です。
実務に載せる① ― 運用(MLOps-lite)
ここからがコンペには無い区画です。公開ノートはモデルができたら終わりますが、実務はそこから始まります。
データの取り決めと特徴量の鮮度・供給遅延
学習時に使えた特徴量が、推論時にも同じ鮮度で手に入るとは限りません。たとえば「直近30日の購入金額」は、データ基盤の集計が日次バッチなら最大1日遅れます。「サポート評価スコア」が別システム由来なら、連携が週次のこともある。学習データでは未来まで揃っている値を、推論時には間に合わないのに使ってしまうのが供給遅延の事故です。
これを防ぐのがデータの取り決め(data contract)――各特徴量について「どこから来て、どの頻度で更新され、推論時に何時間前まで揃うか」を明文化することです。コンペデータは固定・クリーンなので不要ですが、実務では契約のない特徴量が事故の温床になります。新カテゴリ(学習時に存在しなかったチャネルや商品)が来たときの既定値も、契約の一部として決めておきます。
ドリフト監視と再学習トリガ
コンペは固定、実務は動く。これが運用の根本前提です。入力分布(特徴量の分布)や、入力と出力の関係そのものが時間とともにずれていくのがドリフトで、放っておくとモデルは静かに劣化します。
監視するのは二系統です。一つは入力ドリフト(特徴量分布の変化。PSIやKLダイバージェンスで検知)、もう一つは性能ドリフト(正解ラベルが付いてから測る実性能の低下)。再学習のトリガは「定期(毎月)」と「閾値割れ(性能が基準を下回ったら)」を併用するのが素直です。
このholdoutの考え方は、次の「オンライン最適化」と地続きです。
実務に載せる② ― オフライン予測からオンライン意思決定への橋
ここまでのモデルはオフライン予測――蓄積データから「この顧客の解約確率は0.7」とスコアを出すバッチ処理です。しかし業務が本当に欲しいのは予測値ではなく、「打つか/打たないか」「誰にいくら配るか」という意思決定です。
橋渡しは二段あります。第7回でやった較正と閾値設計が一段目(スコアを行動に変える)。二段目が、意思決定の結果を観測して、次の意思決定に反映する逐次のループです。バッチ推論は「予測して終わり」ですが、逐次意思決定は「打って、結果を見て、配分を更新する」。この後者に踏み込むと、固定A/Bテストの限界が見えてきます。
A/Bの先 ― バンディットとベイズ最適化(探索と活用)
A/Bテストは強力ですが、検証期間中ずっと、劣る案にもトラフィックの半分を流し続けるという機会損失があります。配信クリエイティブが3案・5案と増えると、この損失は無視できません。「学びながら、良い案に配分を寄せていく」のがバンディットの発想です。
中心にあるのは探索と活用のトレードオフです。今いちばん良さそうな案に全部寄せる(活用)と、まだ試していない案が実は最良だった可能性を捨てる。全案を均等に試し続ける(探索)と、劣る案に払い続ける。バンディットはこの綱引きを、データが溜まるにつれて自動で調整します。
多腕/文脈付きバンディット
多腕バンディットは「各腕(案)の真のCVRを、観測しながら推定し、有望な腕ほど多く引く」枠組みです。トンプソンサンプリングなら、各腕のCVRの事後分布からサンプリングし、最大の腕を選びます。二値のコンバージョンなら事後は で、成功で 、失敗で を更新するだけです。
import numpy as np
# SOLNAの messages テーブルを「腕=配信クリエイティブ」に見立てる
# 各腕の事後分布 Beta(α, β) を、観測した converted の成功/失敗で更新する
arms = ["email_A", "email_B", "line_A"]
alpha = {a: 1.0 for a in arms} # 事前: Beta(1,1) =一様
beta = {a: 1.0 for a in arms}
def choose_arm():
# 各腕の事後からCVRをサンプリングし、最大の腕を引く(探索と活用の両立)
samples = {a: np.random.beta(alpha[a], beta[a]) for a in arms}
return max(samples, key=samples.get)
def update(arm, converted):
if converted:
alpha[arm] += 1
else:
beta[arm] += 1文脈付きバンディットは、ここに顧客の特徴量(membership_tier・region など)を加え、「この顧客層にはこの案」という条件付きの最適配分を学びます。Churnモデルのスコアを文脈の一部として渡せば、第6回までの予測がそのまま意思決定エンジンの入力になります。オフライン予測とオンライン最適化が、ここで一本につながります。
ベイズ最適化による配分
ad_spend のチャネル×地域への予算配分のように、選択肢が連続量(金額)の場合は、ベイズ最適化が向きます。冒頭のOptunaのTPEと同じ枠組みで、「未知の応答関数(配分→成果)を、観測しながら獲得関数で次に試す点を選ぶ」。違いは、最適化する対象がハイパラではなく実際の予算配分だという点だけです。
対応書籍と読みどころ
- 森下『機械学習を解釈する技術』―― 重要度3種・permutation・SHAPの違いと落とし穴を、本記事より一段深く扱います。「重要≠因果」を実装の言葉で詰めたいときの主軸です。
- 門脇ら『Kaggleで勝つデータ分析の技術』―― チューニングとアンサンブル(スタッキング・OOF)の実務語彙。公開ノート後半の作法はこの一冊で輪郭が掴めます。
ベイズ最適化の事前分布・獲得関数の素地は、第4回で触れた最適化の発想と、PRMLのベイズ的な見方が下敷きになります。
PRMLへの一歩
この回はモデル比較・複雑度(PRML 1.3・3.4)に対応します。「どのモデル・どの複雑さを選ぶか」をベイズ的に扱う見方が、チューニングとスタッキングの背骨です。さらに、ベイズ最適化の事前/獲得関数は、PRML 3章のベイズ線形回帰(事後分布と予測分布)の考え方と地続きで、TPEやガウス過程による最適化を読む準備になります。次の第9回(卒業回)で、章と回の対応マップをまとめて渡します。
章末チェックリスト
- チューニングが何を最適化しているか(CVスコア=固定データへの当てはめ)を言える
- 重要度3種(gain / permutation / SHAP)の測るものの違いを説明できる
- 「重要≠因果」を、介入可能変数と結果代理変数の区別として言える
- スタッキングの過学習リスクと、OOFがそれをどう防ぐか言える
- データの取り決め・供給遅延・新カテゴリの論点を持っている
- 入力ドリフトと性能ドリフトを分けて監視できる
- フィードバックループの汚染と、holdoutでの切り分けを理解した
- A/Bの機会損失と、バンディット(探索と活用)の位置づけを言える
実装はここまで降ろしました。ここで作ったスコアと配分を「自社で何を測り、何を動かすか」という判断に接続する全体像は、対になる記述・予測編を合わせてどうぞ。重要度を施策の根拠に使うとき避けて通れない「効いたと言えるか」は因果編へ。逐次に実験しながら配分を最適化する話は、別途予定しているオンライン最適化(実験)の連載で本格的に扱います。次回は卒業回。公開コードを自分の問いに作り替え、PRMLを開きます。