コンテンツにスキップ

model_risk_basics 学習メモ(tabular XGBoost のモデルリスク管理)

O'Reilly『Machine Learning for High-Risk Applications』(2023, MIT) の公式ノートブックを .py 写経に再構成した ex00〜ex07 を、実際に動かして実出力を確認したうえで読み解いた学習メモ。 数値はすべて uv run python exNN_*.py の実出力(教材の想定値ではなく現物・Python 3.12・seed=12345 固定)。

一言で言うと: 「精度の良い黒箱を作る」から「壊れ方・偏り方・攻撃され方を測って管理する」への視点移動。 モデルは同じ tabular XGBoost 1 本。それを 4 つのレンズ(説明可能性・デバッグ・公平性・セキュリティ)で 点検する道具を 1 概念 1 ファイルで揃える。

全体像

素材は 1 本のクレジットカード延滞データ(30,000 行・延滞率 22.1%)。これを決定論分割し、 「単調制約付き」と「無制約」の XGBoost を学習して、4 レンズで点検していく。

flowchart TD
    Data[("credit_line_increase.csv<br>30000行 延滞率22%")] --> Split[train/valid/test<br>決定論分割 19919/4967/5114]
    Split --> Mono[monotone_from_spearman<br>相関符号で単調制約を決める]
    Mono --> Con[単調制約付き XGBoost]
    Split --> Unc[無制約 XGBoost]

    Con --> Interp{{説明可能性}}
    Unc --> Interp
    Interp -->|ex01| PD[partial dependence<br>で単調性を目視]
    Interp -->|ex02| EBM[GLM / EBM<br>本質的に解釈可能]
    Interp -->|ex03| SHAP[SHAP post-hoc]

    Con --> Debug{{デバッグ}}
    Debug -->|ex04| Resid[残差をセグメント別に見る]
    Debug -->|ex05| Stress[感度・ストレス・敵対的サンプル]

    Con --> Fair{{公平性}}
    Fair -->|ex06| AIR[AIR / 4-5 ルール検定と是正]

    Con --> Sec{{セキュリティ}}
    Sec -->|ex07| Attack[poisoning / backdoor]

背骨は「同じデータ・同じハイパラで無制約と制約付きを並べ、リスク管理の各観点で差を実測する」こと。 制約付きモデルは AUC をほんの少し(後述の実測で -0.0101)落とす代わりに、単調性・毒への頑健性という 「管理しやすさ」を買っている。

使用ライブラリ・原理

ライブラリ 役割 内部で何をしているか
xgboost(native API) 勾配ブースティング木。monotone_constraints で単調性を焼き込める 各分割で「制約方向に反する分割を禁止」して木を成長させる。だから学習後に検証しなくても単調が保証される
interpret(EBM) Explainable Boosting Machine = GA2M 各特徴の形状関数 f_i(x_i) を丸ごと木で学習し、出力は Σf_i(x_i)(+少数のペア交互作用)の足し算。非線形を拾いつつ加法構造なので中身が読める
shap(TreeExplainer) post-hoc 説明(学習後に外から寄与を配分) 木の構造を辿って各特徴の Shapley 値を厳密計算。base + ΣSHAP = 予測 の完全加法分解になる
scikit-learn GLM(ロジスティック回帰)・AUC・標準化 ここでは比較対象の線形モデルと評価指標。おまけに wheel 同梱の libomp.dylib が macOS の xgboost を救う(後述)
scipy.stats AIR のカイ二乗検定 群×承認可否の 2×2 分割表で「格差が偶然か」を検定

単調制約(monotone constraint)の直感: 「返済遅延(PAY_0)が増えたら、延滞確率は絶対に下げない」を モデルの成長規則に組み込む。審査モデルで「遅延が増えたのにスコアが良くなった」は説明不能なので、 それを構造的に起こせなくする。方向はデータ任せにせず Spearman 相関の符号で機械的に決める。

ファイル別の役割

ファイル 役割 実出力の要点
_common.py データ取得・分割・単調制約・XGBoost 学習を集約。macOS OpenMP ブートストラップも担う 全 ex がここを import C する
ex00_fetch_data.py Data.zip 取得とデータ概観 延滞率 22.1%、RACE 別延滞率が 4 倍差(hispanic 40% / white 11%)
ex01_constrained_xgboost.py 単調制約 vs 無制約、PD で単調性検証 test AUC 無制約 0.7826 / 制約付 0.7725(差 -0.0101)。LIMIT_BAL の PD が無制約で非単調→制約付で単調
ex02_glm_gam_ebm.py GLM→EBM の実力測定 AUC: EBM 0.7835 > XGB(mono) 0.7725 > GLM 0.7231。解釈可能モデルが黒箱に勝つ
ex03_posthoc_shap.py SHAP のグローバル/ローカル/配分方式差 PAY_0 が支配的(mean
ex04_residual_analysis.py logloss 残差のセグメント分析 平均残差 0.4502。PAY_0=2 の層で 0.659(高)、RACE hispanic 1.44x・少数派クラス 1.235 vs 0.218
ex05_sensitivity_stress.py 感度・ストレス・敵対的サンプル PAY_0+1 で +0.057・+2 で +0.190、LIMIT_BAL-20% は +0.002。景気後退で承認率 0.755→0.682。PAY_0+1 で承認→否認に反転
ex06_bias_testing.py AIR 検定と是正 cutoff 0.15 で hispanic AIR 0.740・black 0.789(4/5 違反)。cutoff 0.17 に緩めて是正、承認延滞者 149→232 人
ex07_ml_security.py poisoning / backdoor 毒 0.25% で無制約は 3025 行動くが制約付は 79 行。backdoor は平常 AUC 保持で否認者の 100% を承認に反転
ex08_fairness_inprocessing.py fairlearn EG で in-processing 是正、3 方式比較 最小 AIR: (a)素 0.740 / (b)しきい値 0.809 / (c)EG 0.876。ただし (c) は承認率 0.364((a) 並み)でも実延滞者 149→272 人と露出最大
ex09_proxy_features.py 敵対的モデルで proxy 特定 → 除去実験 adversary AUC 0.5634(proxy は弱いが存在)。上位 proxy=PAY_0/PAY_2/PAY_4。除去で最小 AIR +0.011・AUC -0.0271
ex10_poisoning_detection.py poisoning 検知の実力測定(負の結果) スキーマ検査 recall 100%/誤検知 0。残差検知 precision@50 0%(毒 OOF p 中央値 0.23 vs 清浄上位1% p>0.75、最上位毒 884 位)。セグメント監視も p=0.27 で不検出

学んだこと(要点)

  • interpretability(構造で保証)と explainability(後付けで観察)は別レンズ。ex01 の単調制約は前者、 ex03 の SHAP は後者。制約付きモデルの partial dependence は必ず単調になるが、無制約モデルは LIMIT_BAL で実際に非単調になった(無制約=False / 制約付=True)。「与信枠が増えたら延滞リスクが 途中で上がる」という審査で説明できない挙動は、後付け説明では直せない — 学習時に禁止するのが筋
  • 単調方向はデータの相関符号で決められる(ハマりどころ)。monotone_from_spearman|Spearman| < 0.1 の弱い特徴(BILL_AMT1〜6)には制約を掛けない(実出力で全 BILL_AMT が 0)。 制約は万能ではなく「効く特徴にだけ掛ける」取捨がある
  • 「解釈可能 = 低精度」は思い込み。EBM(0.7835) が無制約 XGBoost(0.7826) すら上回った。 黒箱に飛びつく前に本質的に解釈可能なモデルを試す価値がある(設計判断: 高リスク用途ほど EBM を第一候補に)
  • 集計は必ずセグメント別に(ex04)。全体平均残差 0.4502 は「PAY_0=2 の層で 0.659」「少数派クラスで 1.235」を隠す。しかも残差(=誤り)が保護群(hispanic 1.44x・black 1.35x)に偏る — 公平性の懸念は デバッグの残差分析で既に顔を出す
  • disparate treatment を避けても disparate impact は残る(ex06 の核)。モデルは RACE を一切見ないのに、 厳しめカットオフ 0.15 では hispanic の承認率が white の 0.740 倍(4/5 ルール違反・p=2.2e-07)。 格差は RACE と相関した特徴(PAY_* 等)を通じて漏れる。「保護属性を落とせば公平」は誤り
  • 是正はトレードオフ(ex06)。カットオフを 0.15→0.17 に緩めると最小 AIR が 0.8 を超える(hispanic 0.809)が、 全体承認率 0.337→0.466、承認した実延滞者 149→232 人とリスク露出が増える。公平性と健全性は綱引き
  • 単調制約は毒への防御にもなる(ex07 の一番の驚き)。学習データのわずか 0.25%(50 行)を毒化しただけで、 無制約の過学習モデルは 3 万件中 3025 行がスコア変化するのに、制約付きは 79 行しか動かない。 制約が「ありえない形の毒」を吸収する(確率の防御ではなく構造の防御)
  • backdoor は静けさが武器。トリガ PAY_AMT6=999999 を仕込んだモデルは平常時 AUC が 0.7806→0.7799 と ほぼ不変(静的検査では気づけない)なのに、トリガを押すと否認者の 100% が承認に化ける。防御は 精度検査ではなく学習データの来歴管理(data provenance)と OOD 検知の側
  • 公平性は「承認量」ではなく「リスク構成」で払う(ex08)。EG+DemographicParity はしきい値操作なしで 最小 AIR 0.876 を達成し 3 方式中最良。だが承認率をベースライン (a) 並み(0.364)に揃えても 実延滞者は 149→272 人と最大 — 沈んでいた群のより高リスクな人を承認することで群間差を埋めている。 さらに出力が 0/1 の混合分類器(17→9 個のモデルの重み付き混合)になり確率スコア・AUC の物差しを失う
  • proxy 除去は「効くが足りない、しかも高い」(ex09)。adversary(19特徴→被害群)の AUC は 0.5634 で proxy の存在を示すが、上位3(PAY_0/PAY_2/PAY_4)を除去しても最小 AIR は 0.740→0.752 止まり。 代わりに AUC を 0.0271 失う — PAY_* は proxy であると同時に最重要の信用情報だから。 格差の出所と予測力の源泉が同じ特徴に同居しているのが disparate impact の厄介さ
  • 統計検知は自然なラベルノイズの海で溺れる(ex10・負の結果)。ex07 の毒 50 行は、透かし(PAY_0=1.5)への スキーマ検査なら recall 100% で捕まるが、透かし抜きの残差検知では precision@50 = 0% (「特徴は最悪だが延滞しなかった」清浄行が毒より遥かに高残差)。毒の層を知っていてすら セグメント別ラベル率の検定は p=0.27。0.25% のラベル反転は統計では見えない — だから主軸は 予防(単調制約)+ 構造検査(スキーマ・来歴)。「検知できるはず」も既知の毒で recall を実測してから信じる

用語集(混同しやすい対比を階層・対比で)

説明の 2 軸

対比 判定の一文
interpretability ↔ explainability interpretability(設計で分かる) explainability(後付けで説明する) モデルの内部構造そのものが読めるか(前者)、外から近似して覗くか(後者)
inherently interpretable ↔ post-hoc GLM / EBM(本質的に解釈可能) SHAP / LIME(post-hoc) 説明がモデル本体の一部か(EBM の形状関数)、別途当てる近似か(SHAP 値)
  • なぜ効くか: post-hoc は方式(tree_path_dependent / interventional)で答えが変わる(ex03 で上位要因が入れ替わった)。 だから「説明が絶対に一貫すべき」要件では inherently interpretable か構造制約(単調制約)を選ぶ
  • 具体例: ex01=単調制約(interpretability)、ex02=EBM(inherently interpretable)、ex03=SHAP(post-hoc explainability)

デバッグの 3 手法

手法 揺らす対象 見つけるもの 対応 ex
残差分析 何も揺らさない(学習済みの誤りを解剖) どのセグメントで系統的に外すか ex04
感度分析 1 特徴を系統的にシフト どの特徴に予測が敏感か(急峻=危険) ex05
ストレステスト 分布まるごと(悪いシナリオ注入) 分布シフト下で方針が破綻しないか ex05
  • 判定の一文: 「静的な誤りの解剖」なら残差分析、「入力を動かして反応を見る」なら感度分析、 「悪い時期を丸ごと作る」ならストレステスト
  • 具体例: ex04=PAY_0 レベル別残差、ex05=PAY_0 を +1/+2 して平均予測の動き(感度)+景気後退シナリオ(ストレス)

公平性

意味 このデータでの現れ
disparate treatment 保護属性を直接使って差別 本レクチャーは RACE を学習に使わない=これは避けている
disparate impact 保護属性を使わなくても結果に格差 それでも hispanic の承認率が white の 0.740 倍(ex06)
AIR(Adverse Impact Ratio) 保護群の承認率 / 参照群の承認率 参照=white。hispanic 0.740 / black 0.789 / asian 0.929
4/5(80%)ルール AIR < 0.8 は要注意の赤旗 hispanic・black が違反(統計的にも有意 p<1e-4)
  • ハマりどころ: favorable outcome の向き。target=1 が「延滞」なので、favorable(承認)は 0 側p <= cutoff。 向きを取り違えると AIR が反転する
  • 判定の一文: 「保護属性を式に入れたか」=treatment、「結果が群で偏ったか」=impact。合法でも impact は残りうる

セキュリティ(仕込む時点で分類する)

攻撃 仕込む時点 発火条件 平常時の精度 対応
data poisoning 学習時・データ全体 常時(汎化が劣化) 落ちる 来歴管理 + 構造制約(単調制約)で吸収
backdoor 学習時・トリガ条件付き トリガ入力のときだけ 保たれる(気づけない) 来歴管理 + OOD 検知
adversarial example 推論時 攻撃者が入力を微調整したとき 保たれる 入力検証・堅牢化(ex05 の敵対的サンプル探索)
  • 判定の一文: 毒を仕込んだのが「学習か推論か」×「常時かトリガ付きか」で 3 つに割れる
  • 具体例: ex07=poisoning(0.25% の毒)+ backdoor(PAY_AMT6=999999 トリガ)、ex05=adversarial(PAY_0 を最小限動かして反転)

コードの中心ロジック

① ドメイン知識をコードに焼き込む: 相関符号で単調制約(_common.py:159-172

def monotone_from_spearman(df, features=FEATURES, target=TARGET, cutoff=0.1):
    corr = df[features + [target]].corr(method="spearman")[target].drop(target)  # ①
    return {
        f: (1 if corr[f] > cutoff else (-1 if corr[f] < -cutoff else 0))          # ②
        for f in features
    }
やってること なぜそうする
各特徴とターゲットの Spearman 相関を取り、target 自身を落とす 順位相関なので単調な関係だけを拾う(線形でなくてよい)。「増えると延滞が増える特徴」を符号で判別
相関 > +0.1 なら +1、< -0.1 なら -1、その間は 0 弱い特徴(|corr|<0.1)に無理な制約を掛けない。BILL_AMT1〜6 は全部 0 になった(実出力)

これで得た制約を train_xgbparams["monotone_constraints"] = tuple(...) で XGBoost に渡す。 「遅延が増えたら延滞は絶対下げない」というドメイン知識が、たった数行で学習アルゴリズムの制約になる。

② 公平性の物差し: AIR(ex06_bias_testing.py:28-44

def air_table(frame, p, cutoff):
    approved = p <= cutoff                                    # ① favorable=承認=延滞と予測しない
    ref_rate = approved[frame["RACE"].values == REFERENCE].mean()
    for g in RACE_LEVELS:
        mask = frame["RACE"].values == g
        n, fav = int(mask.sum()), int(approved[mask].sum())
        rate = fav / n
        air = rate / ref_rate                                # ② 参照群(white)との承認率比
        table = np.array([[fav, n - fav], [ref_fav, ref_n - ref_fav]])
        _, pval, _, _ = chi2_contingency(table)              # ③ 格差が偶然か検定
やってること なぜそうする
p <= cutoff を承認とみなす target=1 が延滞なので favorable は 0 側。向きが命(逆にすると AIR が反転)
群の承認率 ÷ 参照群の承認率 これが AIR。< 0.8 で 4/5 ルール違反の赤旗
2×2 分割表でカイ二乗検定 AIR が小さくても偶然かもしれない。有意性まで見て初めて「格差」と言える

拡張アイデア

当初の拡張アイデア 1〜3 は ex08〜ex10 として実装済み(1=公平性制約付き再学習→ex08、 2=proxy 特徴の特定と除去→ex09、3=poisoning 検知→ex10。3 は「検知できる」想定だったが 実測は負の結果になった — それ自体が収穫)。残りの発展課題:

  1. conformal prediction で不確実性: 点推定でなく予測区間を出し、区間が広い(自信のない)申込を 人手審査に回すルート(ex04 の高残差セグメントと接続)
  2. ディープラーニング版(書籍 Ch7&9): 画像/転移学習モデルに同じ 4 レンズ(SHAP・残差・バイアス・ 敵対的)を当て、tabular との違い(勾配ベース攻撃・特徴量の非可読性)を対比する
  3. EqualizedOdds 等の別制約で ex08 を再実験: DemographicParity(承認率を揃える)と EqualizedOdds(誤り率を揃える)で、露出コストの出方がどう変わるかを比べる
  4. 群レベル poisoning 検知の限界曲線: ex10 の毒を 50→200→500 行と増やし、セグメント監視の カイ二乗 p が有意に落ちる「検出可能な毒の最小規模」を実測する

現代版に移植するなら(書籍からの実際の変更点)

  • H2O XGBoost → native xgboost: 書籍 Ch11(poisoning/backdoor)は JVM 必須の H2OXGBoostEstimator を使う。Ch6/8/10 は既に native xgboostxgb.train + DMatrix)なので そちらに全 ex を統一した。h2o.init() の 4GB JVM 起動が消え、mono_constraints はそのまま tuple で渡せる
  • XGBoost==1.6 ピン止め → 3.3: 書籍は Colab で %pip install 'XGBoost==1.6'。現行 3.3.0 で monotone_constraintsbase_score・early stopping API はそのまま動いた
  • interpret / shap の現行版: EBM は interpret 0.7.8、SHAP は 0.52.0。SHAP は shap.TreeExplainer(model, feature_perturbation=...)explainer(X) の Explanation API に統一
  • Colab の files.upload() → 自動ダウンロード: _common.ensure_data() が Data.zip を GitHub raw から 取得し、入れ子 zip(Data/Data/...)から必要 CSV だけ平坦化して展開する
  • 決定論の徹底: 学習は seed 固定 + nthread=1(マルチスレッドの非決定性を排除)。分割も np.random.RandomState(seed) の 2 段で固定。so 何度回しても同じ数値が出る

既知の不具合・注意点

  • macOS の OpenMP: xgboost の wheel は @rpath/libomp.dylib を要求するが、Homebrew の libomp を 入れていない Mac だと import 時に落ちる。_common._bootstrap_openmp() が scikit-learn 同梱の libomp.dylibDYLD_LIBRARY_PATH に載せて 1 回だけ os.execv で自分を貼り替える(DYLD 系の 環境変数はプロセス起動時にしか読まれないため、import 前の self-exec が必要)。 → ex07 だけは import xgboost を直接書くので、import _common先頭に置いてブートストラップを xgboost import より前に走らせている(順序が命)
  • pandas 3.0 の厳格化: int 列へ 1.5 を代入すると TypeError: Invalid value '1.5' for dtype 'int64'。 ex07 の毒透かし(PAY_0=1.5)は列を astype(float) してから代入する
  • Python 3.13 を避けて 3.12: 3.13 だと shap → numba → llvmlite の解決が py<3.10 用の古い llvmlite==0.36 を掴んで build に失敗する。.python-version を 3.12 に固定し、numba>=0.60 / llvmlite>=0.43 の床を pyproject に入れて回避
  • AUC は書籍と一致しない: 書籍は H2O・別 seed・別分割なので数値は違う。本メモの数値はこの実装の 実出力であって書籍の再現値ではない(絶対値でなく「制約で -0.01」「毒で無制約 3025 行 vs 制約 79 行」の 相対差が学びの本体)

既存 lecture との接続

このレクチャーは「古典 ML」だが、観点は LLM のリスク管理にそのまま持ち上がる。

接続先 対応する概念 一言
eval_basics/ golden dataset ≒ ここのストレステスト どちらも「本番の前に固定シナリオで挙動を測る」。ex05 の景気後退シナリオは tabular 版 golden dataset
guardrails_basics/ 確率の防御 vs 構造の防御 ≒ post-hoc 説明 vs 単調制約 LLM の「LLM ガード(確率)vs 決定論フィルタ(構造)」と同じ対立。ex01 の単調制約 = 構造の防御、ex03 の SHAP = 確率的な観察
guardrails_basics/ prompt injection ≒ adversarial example(ex05) どちらも「推論時に入力を細工して挙動を乗っ取る」。仕込む時点が推論時という点で backdoor(学習時)と対
sandbox_basics/ 構造で能力を剥奪する発想 sandbox が「実行能力そのものを構造的に奪う」のと、単調制約が「非単調な予測を構造的に禁じる」のは同じ思想

参考


作成: 2026-07-14 / 最終更新: 2026-07-14