機械学習モデルの説明可能性では、SHAP / Shapley value 系の手法がよく使われます。
ただ、普通のShapley value / SHAPには少し気になる点があります。
それは、特徴量の順序をすべて対称に扱うことです。
現実のデータでは、特徴量どうしに因果関係があることがあります。
たとえば、かなり単純化すると次のような関係です。
education -> income -> risk_score
このとき education と income と risk_score を完全に対称な特徴量として扱ってよいのか、という疑問があります。
income は education の影響を受け、risk_score はさらに income の影響を受けるかもしれない。
それなら、特徴量重要度を計算するときにも、この向きを無視したくありません。
だからこそ、因果の向きを尊重した特徴量帰属を計算するコア部品として causasv を作ることにしました。
causasv は、ユーザが与えた DAG 上で非対称 Shapley 値(Asymmetric Shapley Values / ASV)を計算する Rust-first のライブラリです。Python bindings も用意し、Rust側の計算エンジンを Python の XAI ワークフローからも使えるようにしています。
リポジトリはこちらです。
ASVとは何か
Asymmetric Shapley Values は、すべての特徴量順列を平均するのではなく、因果DAGと整合する順序だけを使って平均する Shapley value の変種です。
通常のSHAPでは、特徴量が n 個あるとき、原理的にはすべての n! 個の順列を考えます。
一方ASVでは、DAGのトポロジカル順序、つまり「原因が結果より前に来る」順序だけを考えます。
ASVの定義は次のとおりです。
$$\phi_i = \frac{1}{|\Pi(G)|} \sum_{\pi \in \Pi(G)} \left[ v(\text{pre}(i,\pi) \cup {i}) - v(\text{pre}(i,\pi)) \right]$$
ここで $\Pi(G)$ は DAG $G$ の線形拡張、つまりトポロジカル順序の集合です。$\text{pre}(i,\pi)$ は、順序 $\pi$ の中で特徴量 $i$ より前に現れる特徴量集合です。
標準的なSHAPがすべての順列を平均するのに対し、ASVはDAGと整合する順列だけを平均します。これにより、既知の因果構造を反映した特徴量帰属を計算できます。
何を作っているか
causasv の目的は、かなり狭くしています。
ユーザが与えた因果DAGに対して、ASVを計算すること。
逆に、以下はやりません。
- 因果探索
- モデル訓練
- 自動グラフ構築
- 汎用SHAPフレームワークの置き換え
このスコープを狭くしたのは、メンテナンス性を維持したかったからです。
「全部入りのXAIライブラリ」にしようとすると、データ処理、モデル対応、可視化、因果探索、統計的推定まで抱え込むことになります。
それよりも、causasv では DAG + value function を受け取り、ASVを返す というコアに寄せています。
causasv は SHAP の代替や汎用説明可能性フレームワークではなく、「ユーザ提供の因果DAGに対するASV計算」にフォーカスしたツールです。
Rustでの使用例
Rust側では、まずDAGを作り、そこにエッジを追加します。
use causasv::{AsvExplainer, Dag, SamplingConfig};
fn main() -> Result<(), causasv::CausasvError> {
let mut dag = Dag::new();
let education = dag.add_node("education");
let income = dag.add_node("income");
let risk = dag.add_node("risk_score");
dag.add_edge(education, income)?;
dag.add_edge(income, risk)?;
dag.validate()?;
let explainer = AsvExplainer::new(dag);
let values = explainer.approximate(
|coalition| {
// coalition に含まれる特徴量集合に対する価値関数
Ok(coalition.len() as f64)
},
SamplingConfig::new(10_000).with_seed(42),
)?;
for (node, value) in &values.values {
println!("Node {:?}: ASV = {:.4}", node, value);
}
Ok(())
}
ポイントは、ライブラリ側がモデルを訓練するのではなく、ユーザが value_fn を渡すことです。
これにより、モデルの種類に依存しない形で ASV を計算できます。
Pythonからも使えるようにした
機械学習の実験は Python で行うことが多いので、PyO3 / maturin による Python bindings も用意しています。
from causasv import CausalDAG, ASVExplainer
dag = CausalDAG.from_edges([
("education", "income"),
("income", "risk_score"),
])
explainer = ASVExplainer(dag)
values = explainer.explain(
value_fn=lambda feature_names: my_model_score(feature_names),
method="auto",
n_samples=10_000,
seed=42,
)
print(values)
Python側では、CausalDAG.from_edges() でDAGを作り、ASVExplainer に渡します。
method="auto" を指定すると、DAGのサイズや形に応じて、厳密計算と近似計算を自動で選ぶ設計にしています。auto は小規模なら exact、有根木なら exact_tree、一般DAGなら exact_dag / exact_dag_sparse、大きい場合は approx に切り替わります。
実装している計算方法
ASVは素朴に計算すると、トポロジカル順序の数が爆発します。
そのため、causasv では複数の計算方法を用意しています。
| method | 役割 |
|---|---|
exact |
全線形拡張を列挙するブルートフォース実装 |
exact_tree |
有根木向けの順序イデアルDP |
exact_dag |
一般DAG向けの 2^n 状態DP |
exact_dag_sparse |
疎なDAG向けに有効な順序イデアルのみを探索 |
approx |
重要度サンプリングによる近似計算 |
auto |
DAGに応じて計算方法を自動選択 |
exact_dag_sparse は疎なDAGで有効な順序イデアルだけを訪問し、n_order_ideals、state_ratio、memory_mb といった診断値も返します。近似推定では自己正規化重要度サンプリングを使い、ESS(effective sample size)も返します。
個人的には、このへんが実装していて一番面白い部分でした。
「Shapley valueを計算する」と言うとシンプルに聞こえますが、実際には順列数・DAG構造・メモリ使用量・近似誤差の扱いが絡みます。
特に、厳密計算をどこまでやるか、どこから近似に逃がすかは、ライブラリの使い勝手にかなり影響するんじゃないかと思っています。
なぜRustで書くのか
Rustで書いている理由は、主に次の3つです。
1. グラフやDPのような計算を安全かつ高速に書きたい
ASV計算では、bitmask DP、トポロジカル順序、サンプリング、キャッシュなどが出てきます。このあたりは Rust と相性が良いと思っています。
2. Pythonから呼べるコアエンジンにしたかった
機械学習ユーザはPythonを使うことが多い一方、重い計算部分はRustに寄せたい。causasv はその構成を意識しています。
3. API境界をきれいに保ちやすい
「DAGを受け取る」「value functionを呼ぶ」「ASVを返す」という責務を分けておくことで、機能追加をしても破綻しにくくなるんじゃないかと思っています。
便利にするために入れている機能
単にASV値を返すだけだと、実験で使うには少し物足りません。
そのため、次のような機能も用意しています。
- ESS付きの近似ASV
- 適応型近似と信頼区間
- seed付きの決定論的並列近似
- バッチ連合評価(
value_fn_batch) - sklearn / NumPy向けの
TabularExplainer - 複数DAG間の感度分析(
ASVEnsembleExplainer) - DOT / JSON / networkx 向けのDAGエクスポート
たとえば、近似計算では explain_with_diagnostics() を使って、ASV値だけでなく ESS や seed といった診断情報も一緒に受け取れます。
info = explainer.explain_with_diagnostics(
value_fn=lambda feature_names: my_model_score(feature_names),
method="approx",
n_samples=10_000,
seed=42,
)
print(info["values"]) # dict[str, float] — 特徴量ごとのASV値
print(info["ess"]) # float — ESS (n_samplesに近いほど推定が安定)
print(info["ess_ratio"]) # float — ESS / n_samples
print(info["method"]) # str — 渡した method 名 ("approx")
print(info["selected_method"]) # str — auto のとき実際に選ばれた method
ASVのような近似計算では、「値が出た」だけでは不十分です。
推定がどの程度安定していそうか、seedを変えてランキングがどれくらい揺れるか、といった診断も重要になります。
ベンチマーク
Apple M シリーズ (arm64)、release build、value function は v(S) = |S| での計測です。
厳密計算
| DAG | n | method | time |
|---|---|---|---|
| Chain | 7 |
exact(ブルートフォース) |
2.7 µs |
| Balanced binary tree | 7 |
exact(ブルートフォース) |
39.9 µs |
| Balanced binary tree | 15 |
exact_tree(DP) |
2.79 ms |
| Caterpillar | 10 |
exact_tree(DP) |
169 µs |
| Chain | 16 |
exact_dag(密DP) |
5.28 ms |
exact_dag vs exact_dag_sparse — 直接比較
| DAG | n | method | 探索状態数 | time |
|---|---|---|---|---|
| 2本の並列チェーン | 20 |
exact_dag(密) |
2^20 = 1,048,576 | 87.9 ms |
| 2本の並列チェーン | 20 | exact_dag_sparse |
(10+1)² = 121 | 91 µs |
同じノード数でも、疎なDAGでは探索状態数が劇的に減ります。2本の並列チェーンの場合、有効な順序イデアルは (k+1)² 個しかないため、全 2^n 状態をなめる密DPの約1000倍高速になっています。
近似計算
| DAG | n | サンプル数 | time |
|---|---|---|---|
| Chain | 10 | 1,000 | 916 µs |
| Balanced tree | 15 | 1,000 | 1.94 ms |
並列化の効果(Chain n=20、10k samples):
| mode | スレッド数 | time | 速度比 |
|---|---|---|---|
| serial seeded | 1 | 18.2 ms | 1.0× |
| parallel seeded | 2 | 12.0 ms | 1.5× |
| parallel seeded | 4 | 7.4 ms | 2.5× |
適応型近似では収束検知による早期終了が効くケースもあります(Chain n=10、最大10k samples でおよそ5倍速)。
参考にした論文
このライブラリを作るうえで参考にした・関連する研究をまとめておきます。
ASVの原論文
Frye, Feige, Rowat による "Asymmetric Shapley values: incorporating causal knowledge into model-agnostic explainability" (NeurIPS 2020) で提案されており、因果構造をモデル非依存の説明可能性に組み込む枠組みが説明されています(直接の引用元というよりは、ASVの概念的な背景として参照しています)。
SHAPの問題点を論じた論文
Kumar et al. による "Problems with Shapley-value-based explanations as feature importance measures" (2020) は、SHAP を特徴量重要度として使うときの数学的な問題点を整理した論文です。因果構造を無視することへの問題提起として、causasv の動機と重なる部分があると思っています。
因果Shapleyの別アプローチ
Heskes et al. による "Causal Shapley Values: Exploiting Causal Knowledge to Explain Individual Predictions" (NeurIPS 2020) は、do-calculus をベースにした別の因果 Shapley 手法です。ASV とはアプローチが異なり、こちらは介入分布を使って直接効果と間接効果を分離する方向性をとっています。
最近の関連研究
"Causal SHAP: Feature Attribution with Dependency Awareness through Causal Discovery" (2025) は、因果探索を自動で行ってから SHAP を計算するアプローチです。causasv とは逆に「DAGをライブラリ側が推定する」方向性で、スコープが異なりますが、同じ問題意識から出ている研究だと思います。
今後やりたいこと
- 実データに近いサンプルの追加
- SHAPとASVの差が直感的に分かる可視化
- Python APIの使いやすさ改善
- 近似計算の診断情報の充実
- ドキュメントの整備
特に、causasv で一番伝えたいのは次の点です。
causasv は「因果探索をするライブラリ」ではありません。
ユーザが持っている因果DAGを使って、特徴量帰属を因果構造に沿って計算するための小さなエンジンです。
このスコープを守ることで、機能追加をしながらもメンテナンス性を保っていきたいと思っています。
まとめ
causasv は、因果DAGを考慮した Asymmetric Shapley Values を計算する Rust / Python ライブラリです。
普通のSHAPでは、特徴量の順序をすべて対称に扱います。
一方ASVでは、DAGと整合するトポロジカル順序だけを使います。
これにより、既知の因果構造を尊重した特徴量重要度を計算できます。
まだ実験的な段階ですが、Rustで計算コアを書き、Pythonからも使えるようにすることで、実験にも組み込みやすい形を目指しています。