Красивые графики, которые врут
Открываешь тетрадку с Jupyter, запускаешь shap.summary_plot() и получаешь красивые цветные полоски. Кажется, теперь ты понимаешь свою модель. Знаешь, какие фичи важны. Можешь объяснить бизнесу, почему клиент получил отказ.
А потом заказчик спрашивает: "Почему у этого человека высокий доход, но модель предсказывает дефолт?" Ты смотришь на Shapley values и видишь, что фича 'возраст' имеет отрицательный вклад. Но возраст у клиента 45 лет - вроде нормально. Что-то не так.
Или ещё хуже: запускаешь shap.plots.waterfall() для двух почти одинаковых наблюдений, а значения признаков скачут на 300%. Без видимой причины.
Shapley Values - не волшебная палочка. Это математический инструмент с кучей подводных камней. И если ты не знаешь, где эти камни лежат, разобьёшься об них при первом же серьёзном запросе от регулятора.
Три типа ошибок, которые не покажет документация SHAP
Библиотека shap работает по принципу "black box" - дал модель, получи объяснения. Но что происходит внутри этого black box? Ты доверяешь результатам, потому что они математически обоснованы. И это первая ошибка.
1. Проблема с baseline: откуда берутся эти странные значения?
Открой любой пример из документации shap:
import shap
import xgboost
import numpy as np
# Обучаем модель
X, y = shap.datasets.adult()
model = xgboost.XGBClassifier().fit(X, y)
# Считаем SHAP значения
explainer = shap.Explainer(model)
shap_values = explainer(X)
Всё просто, правда? А теперь посмотри на это:
print(f"Baseline (ожидаемое значение модели): {explainer.expected_value}")
print(f"Это что вообще такое? {explainer.expected_value}")
Ты получишь какое-то число. Например, -1.37. Что это значит? Это значение модели на "нулевом" входе. Но что такое "нулевой" вход для XGBoost? Это не нули в массиве. Это... зависит от реализации.
Представь: у тебя есть модель кредитного скоринга. Baseline = 0.3. Это значит, что "средний" клиент имеет 30% вероятность дефолта. Все Shapley values показывают отклонение от этой цифры.
А если твой тренировочный датасет несбалансирован? 90% хороших клиентов, 10% плохих. Baseline будет около 0.1. И все объяснения будут относительно этого значения. Клиент с реальной вероятностью дефолта 50% получит огромные положительные Shapley values для всех рискованных признаков.
Но заказчик спрашивает: "Почему у этого клиента такой высокий риск?" Ты показываешь график, где фича 'просрочки_90_дней' добавляет +0.2 к вероятности. А заказчик: "Но у него же нет просрочек 90 дней!"
Вот она, первая ловушка. Shapley values показывают вклад относительно среднего по тренировочным данным. Не относительно нуля. Не относительно какого-то "нейтрального" клиента. Относительно среднего.
2. Корреляции убивают объяснимость
Ещё одна классика. Берёшь датасет, где 'возраст' и 'стаж_работы' коррелируют на 0.8. Обучаешь модель. Смотришь на Shapley values.
Фича 'возраст' важна. 'Стаж_работы' тоже важна. Всё логично?
А теперь сделай так:
# Создаём синтетические данные с сильной корреляцией
np.random.seed(42)
n_samples = 1000
age = np.random.normal(40, 10, n_samples)
experience = age * 0.7 + np.random.normal(0, 3, n_samples) # Сильная корреляция
salary = 1000 * age + 500 * experience + np.random.normal(0, 5000, n_samples)
X = pd.DataFrame({'age': age, 'experience': experience})
y = salary
model = xgboost.XGBRegressor().fit(X, y)
explainer = shap.Explainer(model)
shap_values = explainer(X)
# Смотрим важность признаков
shap.plots.bar(shap_values)
Увидишь, что обе фичи получат примерно равную важность. Но подожди. Целевая переменная линейно зависит от обеих фичей. Модель XGBoost может выучить эту линейную зависимость. Но Shapley values распределят "заслуги" между коррелированными признаками.
Почему это проблема? Потому что в реальности 'стаж_работы' может быть производным от 'возраста'. Или наоборот. Shapley values не могут это определить. Они работают в предположении, что признаки независимы.
Коррелированные признаки - бич Shapley values. Модель может использовать только один из них, но SHAP покажет важность обоих. Или распределит вклад как попало.
3. Категориальные переменные и их кодирование
One-hot encoding. Все его используют. Все его ненавидят. И Shapley values его тоже ненавидят.
Возьмём фичу 'город'. 100 уникальных значений. После one-hot encoding получаем 100 бинарных признаков. Запускаем SHAP.
Что увидим? Каждый бинарный признак получит крошечный Shapley value. Потому что вклад одного хот-энкодированного признака в модель минимален. Но вместе они могут определять решение!
SHAP этого не покажет. На графике важности признаков эти 100 бинарных колонок займут последние места. И ты решишь, что 'город' не важен для модели. Хотя на самом деле модель сильно зависит от географического региона.
Практическая отладка: пошаговый план
1Проверь baseline
Первое, что делаешь после вычисления SHAP значений:
print(f"Expected value: {explainer.expected_value}")
print(f"Average prediction: {model.predict_proba(X_train)[:, 1].mean()}")
# Они должны быть примерно равны
# Если нет - что-то не так с explainer
Потом смотри на распределение предсказаний модели:
import matplotlib.pyplot as plt
preds = model.predict_proba(X_train)[:, 1]
plt.hist(preds, bins=50)
plt.axvline(explainer.expected_value, color='red', linestyle='--', label='SHAP baseline')
plt.legend()
plt.show()
Baseline должен быть где-то в центре распределения. Если он с краю - проверь, не переобучилась ли модель. Или не сбалансирован ли датасет.
2Найди коррелированные признаки
Не доверяй глазам. Посчитай:
import seaborn as sns
corr_matrix = X_train.corr().abs()
high_corr_pairs = []
for i in range(len(corr_matrix.columns)):
for j in range(i+1, len(corr_matrix.columns)):
if corr_matrix.iloc[i, j] > 0.7: # Порог 0.7
high_corr_pairs.append((
corr_matrix.columns[i],
corr_matrix.columns[j],
corr_matrix.iloc[i, j]
))
print(f"Найдено {len(high_corr_pairs)} пар с корреляцией > 0.7")
for feat1, feat2, corr in high_corr_pairs[:10]: # Покажем первые 10
print(f"{feat1} - {feat2}: {corr:.3f}")
Для каждой такой пары сделай простой тест. Обучи две модели:
- Со всеми признаками
- Без одного из коррелированных признаков
Сравни важность оставшегося признака в обеих моделях. Если важность сильно меняется - Shapley values для этих признаков ненадёжны.
3Проверь согласованность
Shapley values должны быть согласованы. Если фича X увеличивает предсказание на 0.1 для наблюдения A, и у наблюдения B значение фичи X больше, то вклад фичи X для наблюдения B должен быть не меньше 0.1.
Проверяем:
def check_monotonicity(shap_values, feature_idx, X_data):
"""Проверяем монотонность вклада фичи"""
# Берем значения фичи и соответствующие SHAP значения
feature_vals = X_data.iloc[:, feature_idx]
shap_vals = shap_values.values[:, feature_idx]
# Сортируем по значению фичи
sorted_idx = np.argsort(feature_vals)
sorted_shap = shap_vals[sorted_idx]
# Проверяем монотонность
# (упрощённо - считаем, сколько раз следующий элемент меньше предыдущего)
violations = 0
for i in range(1, len(sorted_shap)):
if sorted_shap[i] < sorted_shap[i-1]:
violations += 1
violation_rate = violations / len(sorted_shap)
return violation_rate
# Проверяем каждую фичу
for i in range(X_train.shape[1]):
rate = check_monotonicity(shap_values, i, X_train)
if rate > 0.3: # Если больше 30% нарушений
print(f"Фича {X_train.columns[i]}: {rate:.1%} нарушений монотонности")
Больше 30% нарушений - красный флаг. Значит, Shapley values для этой фичи нестабильны.
Что делать, если SHAP врёт?
Случай 1: Модель слишком сложная
XGBoost с depth=10 на маленьком датасете. Random Forest со 1000 деревьев. Нейросеть с кучей слоёв.
Сложные модели имеют сложные, нелинейные, перекрученные decision boundaries. Shapley values пытаются объяснить эту сложность простыми аддитивными вкладами. Получается ерунда.
Решение: упрости модель. Или используй более подходящий explainer. Для глубоких нейросетей попробуй Integrated Gradients вместо SHAP. Для ансамблей - посмотри на альтернативные методы валидации.
Случай 2: Признаки взаимодействуют нелинейно
Возраст и доход. По отдельности не очень предсказывают дефолт. Но вместе - молодой с высоким доходом = риск. Пожилой с низким доходом = риск.
SHAP покажет вклад каждого признака по отдельности. Взаимодействие потеряется.
Решение: используй shap.interaction_values(). Или создай фичи взаимодействия вручную. Или переходи на модели, которые лучше捕获 взаимодействия, вроде SplineTransformer.
Случай 3: Данные имеют временную зависимость
Кредитная история. Погода. Цены на акции. Всё это временные ряды.
SHAP предполагает, что наблюдения независимы. Во временных рядах это не так. Вчерашняя цена влияет на сегодняшнюю. SHAP этого не учтёт.
Решение: используй специальные методы для временных рядов. Или агрегируй данные так, чтобы временная зависимость ослабла.
Контрпримеры: когда Shapley values точно не работают
Есть ситуации, где SHAP гарантированно даст неправильные результаты. Запомни их:
| Ситуация | Почему SHAP ломается | Что делать |
|---|---|---|
| Модель использует только взаимодействия признаков | SHAP показывает нулевой вклад для каждого признака в отдельности | Использовать shap.interaction_values() или перейти на другую модель |
| Выбросы в данных | Shapley values для выбросов нестабильны и могут доминировать на графиках | Убрать выбросы перед вычислением SHAP или использовать robust scaler |
| Очень разреженные данные | Большинство значений признаков = 0, SHAP значения становятся шумными | Агрегировать признаки или использовать модели для sparse данных |
| Модель переобучена | SHAP значения отражают шум, а не реальные зависимости | Упростить модель, добавить регуляризацию |
Альтернативы, когда SHAP не справляется
SHAP - не единственный метод объяснимости. Иногда лучше использовать другие инструменты:
- LIME - локальные объяснения, лучше работает с нелинейностями
- Partial Dependence Plots (PDP) - показывает усреднённый эффект признака
- ALE plots - как PDP, но лучше справляется с корреляциями
- Permutation Importance - проще, устойчивее к переобучению
- Anchors - правила типа "если X > 5, то модель предсказывает класс 1"
Главное - не цепляться за один инструмент. Если SHAP показывает странности, проверь другими методами. Если все методы показывают разное - возможно, модель действительно неинтерпретируема. Или данные такие грязные, что никакая объяснимость не поможет.
Чеклист перед показом SHAP графиков заказчику
- Проверил baseline? Он имеет смысл для бизнеса?
- Нашёл коррелированные признаки? Знаю, как они влияют на интерпретацию?
- Проверил монотонность для ключевых признаков?
- Убрал выбросы из визуализации?
- Агрегировал one-hot encoded фичи?
- Сравнил с другими методами объяснимости (хотя бы с permutation importance)?
- Подготовил ответ на вопрос "А почему вот этот признак так влияет?"
- Знаю ограничения SHAP для своей конкретной модели и данных?
Если на все вопросы ответил "да" - можно показывать. Если нет - вернись и исправь. Потому что однажды тебя спросят: "А вы уверены в этих цифрах?" И лучше подготовить ответ заранее.
Самый опасный сценарий: ты используешь SHAP, не понимая его ограничений. Строишь красивые графики. Убеждаешь заказчика в правильности модели. А потом выясняется, что ключевое решение было основано на артефакте вычислений.
Такие истории заканчиваются потерянными контрактами. А иногда - судебными исками. Особенно в регулируемых отраслях вроде кредитования или медицины.
Поэтому отлаживай объяснимость так же тщательно, как отлаживаешь саму модель. Проверяй предположения. Тестируй на синтетических данных. И всегда имей запасной вариант объяснения.
Потому что когда придёт аудитор и спросит "почему?", красивых картинок будет недостаточно. Нужны будут доказательства. И понимание, как эти доказательства получены.