
Quand les valeurs de Shapley s’effondrent : un guide pour une explicabilité robuste des modèles
L’explicabilité de l’IA est essentielle pour gagner la confiance dans les prédictions des modèles et est très importante pour améliorer la robustesse des modèles. Une bonne explicabilité agit souvent comme un outil de débogage, révélant des défauts dans le processus de formation du modèle. Bien que les valeurs Shapley soient devenues la norme de l’industrie pour cette tâche, nous devons nous demander : fonctionnent-elles toujours ? Et surtout, où échouent-ils ?
Pour comprendre où les valeurs de Shapley échouent, la meilleure approche consiste à contrôler la vérité terrain. Nous commencerons par un modèle linéaire simple, puis décomposerons systématiquement l’explication. En observant comment les valeurs de Shapley réagissent à ces changements contrôlés, nous pouvons identifier précisément où ils donnent des résultats trompeurs et comment y remédier.
Le modèle de jouet
Nous commencerons par un modèle à 100 variables aléatoires uniformes.
import numpy as np
from sklearn.linear_model import LinearRegression
import shap
def get_shapley_values_linear_independent_variables(
weights: np.ndarray, data: np.ndarray
) -> np.ndarray:
return weights * data
# Top compare the theoretical results with shap package
def get_shap(weights: np.ndarray, data: np.ndarray):
model = LinearRegression()
model.coef_ = weights # Inject your weights
model.intercept_ = 0
background = np.zeros((1, weights.shape[0]))
explainer = shap.LinearExplainer(model, background) # Assumes independent between all features
results = explainer.shap_values(data)
return results
DIM_SPACE = 100
np.random.seed(42)
# Generate random weights and data
weights = np.random.rand(DIM_SPACE)
data = np.random.rand(1, DIM_SPACE)
# Set specific values to test our intuition
# Feature 0: High weight (10), Feature 1: Zero weight
weights[0] = 10
weights[1] = 0
# Set maximal value for the first two features
data[0, 0:2] = 1
shap_res = get_shapley_values_linear_independent_variables(weights, data)
shap_res_pacakge = get_shap(weights, data)
idx_max = shap_res.argmax()
idx_min = shap_res.argmin()
print(
f"Expected: idx_max 0, idx_min 1\nActual: idx_max {idx_max}, idx_min: {idx_min}"
)
print(abs(shap_res_pacakge - shap_res).max()) # No difference
Dans cet exemple simple, où toutes les variables sont indépendantes, le calcul est considérablement simplifié.
Rappelons que la formule de Shapley est basée sur contribution marginale de chaque fonctionnalité, la différence entre les résultats du modèle lorsqu’une variable est ajoutée à une coalition de fonctionnalités connues et lorsqu’elle est absente.
\[ V(S∪{i}) – V(S)
\]
Les variables étant indépendantes, la combinaison spécifique de fonctionnalités présélectionnées (S) n’influence pas la contribution de la fonctionnalité i. L’effet des caractéristiques présélectionnées et non sélectionnées s’annulent lors de la soustraction, n’ayant aucun impact sur l’influence de la caractéristique i. Ainsi, le calcul se réduit à mesurer l’effet marginal de la caractéristique i directement sur la sortie du modèle :
\[ W_i · X_i \]
Le résultat est à la fois intuitif et fonctionne comme prévu. Puisqu’il n’y a aucune interférence provenant d’autres fonctionnalités, la contribution dépend uniquement du poids de la fonctionnalité et de sa valeur actuelle. Par conséquent, la caractéristique présentant la plus grande combinaison de poids et de valeur est la caractéristique qui contribue le plus. Dans notre cas, l’indice de fonctionnalité 0 a un poids de 10 et une valeur de 1.
Brisons les choses
Nous allons maintenant introduire les dépendances pour voir où les valeurs de Shapley commencent à échouer.
Dans ce scénario, nous induirons artificiellement une corrélation parfaite en dupliquant 100 fois la caractéristique la plus influente (indice 0). Cela aboutit à un nouveau modèle avec 200 fonctionnalités, où 100 fonctionnalités sont des copies identiques de notre principal contributeur d’origine et indépendantes du reste des 99 fonctionnalités. Pour terminer la configuration, nous attribuons un poids nul à toutes ces fonctionnalités en double ajoutées. Cela garantit que les prédictions du modèle restent inchangées. Nous modifions uniquement la structure des données d’entrée, pas la sortie. Bien que cette configuration semble extrême, elle reflète un scénario réel courant : prendre un signal important connu et créer plusieurs fonctionnalités dérivées (telles que des moyennes mobiles, des décalages ou des transformations mathématiques) pour mieux capturer ses informations.
Cependant, comme la fonctionnalité 0 d’origine et ses nouvelles copies sont parfaitement dépendantes, le calcul de Shapley change.
Basé sur le Axiome de symétrie : si deux fonctionnalités contribuent de manière égale au modèle (dans ce cas, en transportant les mêmes informations), elles doivent recevoir le même crédit.
Intuitivement, connaître la valeur d’un clone révèle toutes les informations du groupe. En conséquence, la contribution massive que nous avons constatée précédemment pour la fonctionnalité unique est désormais répartie à parts égales entre celle-ci et ses 100 clones. Le « signal » reçoit diluéce qui fait que le principal moteur du modèle apparaît beaucoup moins important qu’il ne l’est en réalité.
Voici le code correspondant :
import numpy as np
from sklearn.linear_model import LinearRegression
import shap
def get_shapley_values_linear_correlated(
weights: np.ndarray, data: np.ndarray
) -> np.ndarray:
res = weights * data
duplicated_indices = np.array(
[0] + list(range(data.shape[1] - DUPLICATE_FACTOR, data.shape[1]))
)
# we will sum those contributions and split contribution among them
full_contrib = np.sum(res[:, duplicated_indices], axis=1)
duplicate_feature_factor = np.ones(data.shape[1])
duplicate_feature_factor[duplicated_indices] = 1 / (DUPLICATE_FACTOR + 1)
full_contrib = np.tile(full_contrib, (DUPLICATE_FACTOR+1, 1)).T
res[:, duplicated_indices] = full_contrib
res *= duplicate_feature_factor
return res
def get_shap(weights: np.ndarray, data: np.ndarray):
model = LinearRegression()
model.coef_ = weights # Inject your weights
model.intercept_ = 0
explainer = shap.LinearExplainer(model, data, feature_perturbation="correlation_dependent")
results = explainer.shap_values(data)
return results
DIM_SPACE = 100
DUPLICATE_FACTOR = 100
np.random.seed(42)
weights = np.random.rand(DIM_SPACE)
weights[0] = 10
weights[1] = 0
data = np.random.rand(10000, DIM_SPACE)
data[0, 0:2] = 1
# Duplicate copy of feature 0, 100 times:
dup_data = np.tile(data[:, 0], (DUPLICATE_FACTOR, 1)).T
data = np.concatenate((data, dup_data), axis=1)
# We will put zero weight for all those added features:
weights = np.concatenate((weights, np.tile(0, (DUPLICATE_FACTOR))))
shap_res = get_shapley_values_linear_correlated(weights, data)
shap_res = shap_res[0, :] # Take First record to test results
idx_max = shap_res.argmax()
idx_min = shap_res.argmin()
print(f"Expected: idx_max 0, idx_min 1\nActual: idx_max {idx_max}, idx_min: {idx_min}")
Ce n’est clairement pas ce que nous souhaitions et ne parvient pas à fournir une bonne explication du comportement du modèle. Idéalement, nous voulons que l’explication reflète la vérité terrain : la fonctionnalité 0 est le pilote principal (avec un poids de 10), tandis que les fonctionnalités dupliquées (indices 101 à 200) ne sont que des copies redondantes avec un poids nul. Au lieu de diluer le signal sur toutes les copies, nous préférerions clairement une attribution mettant en évidence la véritable source du signal.
Remarque : Si vous exécutez ceci à l’aide du package Python shap, vous remarquerez peut-être que les résultats sont similaires mais pas identiques à notre calcul manuel. En effet, le calcul des valeurs de Shapley est irréalisable sur le plan informatique. Par conséquent, les bibliothèques comme shap s’appuient sur des méthodes d’approximation qui introduisent légèrement de la variance.

Pouvons-nous résoudre ce problème ?
Étant donné que la corrélation et les dépendances entre les fonctionnalités sont extrêmement courantes, nous ne pouvons ignorer ce problème.
D’une part, les valeurs de Shapley tiennent compte de ces dépendances. Une fonctionnalité avec un coefficient de 0 dans un modèle linéaire et sans effet direct sur la sortie reçoit une contribution non nulle car elle contient des informations partagées avec d’autres fonctionnalités. Cependant, ce comportement, piloté par l’axiome de symétrie, n’est pas toujours ce que nous souhaitons pour une explicabilité pratique. Bien que répartir « équitablement » le crédit entre les caractéristiques corrélées soit mathématiquement judicieux, cela cache souvent les véritables moteurs du modèle.
Plusieurs techniques peuvent gérer cela, et nous allons les explorer.
Fonctionnalités de regroupement
Cette approche est particulièrement critique pour les modèles spatiaux de fonctionnalités de grande dimension, où la corrélation des fonctionnalités est inévitable. Dans ces contextes, tenter d’attribuer des contributions spécifiques à chaque variable est souvent bruyant et instable sur le plan informatique. Au lieu de cela, nous pouvons regrouper des fonctionnalités similaires qui représentent le même concept en un seul groupe. Une analogie utile est celle de la classification des images : si nous voulons expliquer pourquoi un modèle prédit « chat » au lieu d’un « chien », l’examen de pixels individuels n’a pas de sens. Cependant, si l’on regroupe les pixels en « patches » (par exemple, oreilles, queue), l’explication devient immédiatement interprétable. En appliquant cette même logique aux données tabulaires, nous pouvons calculer la contribution du groupe plutôt que de la diviser arbitrairement entre ses composants.
Ceci peut être réalisé de deux manières : en additionnant simplement les valeurs de Shapley au sein de chaque groupe ou en calculer directement la contribution du groupe. Dans la méthode directe, nous traitons le groupe comme une seule entité. Au lieu de basculer les fonctionnalités individuelles, nous traitons la présence et l’absence du groupe comme la présence ou l’absence simultanée de toutes les fonctionnalités qui le composent. Cela réduit la dimensionnalité du problème, rendant l’estimation plus rapide, plus précise et plus stable.

Le gagnant remporte tout
Bien que le regroupement soit efficace, il présente des limites. Cela nécessite de définir les groupes au préalable et ignore souvent les corrélations entre ces groupes.
Cela conduit à une « redondance des explications ». Pour revenir à notre exemple, si les 101 fonctionnalités clonées ne sont pas pré-regroupées, la sortie répétera ces 101 fonctionnalités avec la même contribution 101 fois. C’est écrasant, répétitif et fonctionnellement inutile. Une explicabilité efficace devrait réduire la redondance et montrer à chaque fois quelque chose de nouveau à l’utilisateur.
Pour y parvenir, nous pouvons créer un processus itératif gourmand. Au lieu de calculer toutes les valeurs en même temps, nous pouvons sélectionner les caractéristiques étape par étape :
- Sélectionnez le « Gagnant »: identifiez la fonctionnalité unique (ou le groupe) avec la contribution individuelle la plus élevée
- Conditionner la prochaine étape: Réévaluez les fonctionnalités restantes, en supposant que les fonctionnalités de l’étape précédente sont déjà connues. Nous les incorporerons à chaque fois dans le sous-ensemble de caractéristiques présélectionnées S dans la valeur shapley.
- Répéter: Demandez au modèle : « Étant donné que l’utilisateur connaît déjà les fonctionnalités A, B, C, quelle fonctionnalité restante apporte le plus d’informations ?
En recalculant les valeurs de Shapley (ou contributions marginales) conditionnées aux fonctionnalités présélectionnées, nous garantissons que les fonctionnalités redondantes tombent effectivement à zéro. Si la fonctionnalité A et la fonctionnalité B sont identiques et que la fonctionnalité A est sélectionnée en premier, la fonctionnalité B ne fournit plus de nouvelles informations. Il est automatiquement filtré, laissant une liste claire et concise de pilotes distincts.

Note: Vous pouvez trouver une implémentation de ce groupe direct et du calcul itératif glouton dans notre package Python medpython.
Divulgation complète : Je suis co-auteur de ce package open source.
Validation dans le monde réel
Bien que ce modèle de jouet démontre des défauts mathématiques dans la méthode des valeurs Shapley, comment fonctionne-t-il dans des scénarios réels ?
Nous avons appliqué ces méthodes de Groupé Shapley avec Le gagnant remporte tout, en plus avec plus de méthodes (qui sont hors de portée de cet article, peut-être la prochaine fois), dans des contextes cliniques complexes utilisés dans les soins de santé. Nos modèles utilisent des centaines de fonctionnalités fortement corrélées qui ont été regroupées en dizaines de concepts.
Cette méthode a été validée sur plusieurs modèles en aveugle lorsque nos cliniciens ne savaient pas quelle méthode ils inspectaient et a surpassé les valeurs vanille de Shapley par leur classement. Chaque technique a contribué au-dessus de l’expérience précédente dans une expérience en plusieurs étapes. De plus, notre équipe a utilisé ces améliorations d’explicabilité dans le cadre de notre soumission au Défi IA santé CMSoù nous avons été sélectionnés comme lauréats.

Conclusion
Les valeurs de Shapley constituent la référence en matière d’explicabilité des modèles, fournissant un moyen mathématiquement rigoureux d’attribuer le crédit.
Cependant, comme nous l’avons vu, la « justesse » mathématique ne se traduit pas toujours par une explicabilité efficace.
Lorsque les caractéristiques sont fortement corrélées, le signal peut être dilué, cachant les véritables moteurs de votre modèle derrière un mur de redondance.
Nous avons exploré deux façons de résoudre ce problème :
- Regroupement: Regrouper les fonctionnalités en un seul concept
- Itératif Sélection: conditionner des concepts déjà présentés pour extraire uniquement de nouvelles informations, éliminant ainsi la redondance.
En reconnaissant ces limites, nous pouvons garantir que nos explications sont significatives et utiles.
Si vous avez trouvé cela utile, connectons-nous sur LinkedIn



