Перейти к содержанию

Модели машинного обучения и их интерпретация#

Материалы для самостоятельного изучения Деревья решений (Decision Trees) и Ансамблевые методы: Градиентный бустинг, случайные леса, бэггинг, метод голосования, стекинг, Как интерпретировать предсказания моделей в SHAP

Что такое SHAP#

SHAP (SHapley Additive exPlanations) — это теоретико-игровой подход к объяснению результатов работы любой модели машинного обучения. Он связывает оптимальное распределение кредитов с локальными объяснениями, используя классические значения Шепли из теории игр и связанные с ними расширения.

В библиотеке SHAP для оценки важности фичей рассчитываются значения Шэпли (по имени американского математика и названа библиотека).

Для оценки важности фичи происходит оценка предсказаний модели с и без данной фичи.

Внутренности макетной платы

SHAP можно установить из PyPI

pip install shap

Пример загрузки пользовательской древовидной модели в SHAP#

В этом разделе показано, как передать пользовательскую древовидную модель в SHAP для объяснения.

import graphviz
import numpy as np
import scipy
import sklearn

import shap

Простая регрессионная древовидная модель#

Здесь мы определяем простое дерево регрессии, а затем загружаем его в SHAP как пользовательскую модель.

X, y = shap.datasets.adult()

orig_model = sklearn.tree.DecisionTreeRegressor(max_depth=2)
orig_model.fit(X, y)
dot_data = sklearn.tree.export_graphviz(orig_model, out_file=None, filled=True, rounded=True, special_characters=True)
graph = graphviz.Source(dot_data)
graph

Дополнительную информацию о том, что именно означают эти атрибуты, можно найти в документации scikit-learn

# Извлекаем массивы, описывающие структуру дерева
children_left = orig_model.tree_.children_left
children_right = orig_model.tree_.children_right
children_default = children_right.copy()  # в scikit-learn отсутствует обработка пропущенных значений, поэтому используем правое поддерево по умолчанию
features = orig_model.tree_.feature
thresholds = orig_model.tree_.threshold
values = orig_model.tree_.value.reshape(orig_model.tree_.value.shape[0], 1)
node_sample_weight = orig_model.tree_.weighted_n_node_samples

print("     children_left", children_left)   # отрицательное значение означает, что узел является листом
print("    children_right", children_right)
print("  children_default", children_default)
print("          features", features)
print("        thresholds", thresholds.round(3))  # значение -2 указывает, что узел является листом
print("            values", values.round(3))
print("node_sample_weight", node_sample_weight)
# Определение пользовательской модели дерева в виде словаря
tree_dict = {
    "children_left": children_left,              # индексы левых дочерних узлов
    "children_right": children_right,            # индексы правых дочерних узлов
    "children_default": children_default,        # индексы дочерних узлов по умолчанию (в sklearn равны правым, так как отсутствуют пропущенные значения)
    "features": features,                        # индекс признака, по которому происходит разбиение в каждом узле
    "thresholds": thresholds,                    # пороговое значение для разбиения
    "values": values,                            # значения, предсказываемые в узлах (например, средние значения по классам)
    "node_sample_weight": node_sample_weight,    # количество (взвешенных) объектов, попавших в каждый узел
}

# Представление модели в виде словаря, содержащего одно дерево
model = {"trees": [tree_dict]}
explainer = shap.TreeExplainer(model)
# Убедимся, что интерпретатор SHAP (модель TreeEnsemble) выдаёт те же предсказания,
# что и исходная модель
assert np.abs(explainer.model.predict(X) - orig_model.predict(X)).max() < 1e-4

# Убедимся, что сумма SHAP-значений даёт точное значение прогноза модели
# (свойство локальной точности: сумма вкладов признаков плюс базовое значение равна предсказанию)
assert np.abs(explainer.expected_value + explainer.shap_values(X).sum(1) - orig_model.predict(X)).max()

Объяснение потери древовидной модели#

Интерпретация функции потерь модели может оказаться весьма полезной как для отладки, так и для мониторинга качества обучения.
В этом блокноте приводится простой пример того, как работает такой подход.

Обратите внимание: объяснение функции потерь требует передачи истинных меток (labels) и поддерживается только при использовании параметра feature_perturbation="independent" в TreeExplainer.

Полное описание метода будет представлено позже, вместе с более подробным примером.

import numpy as np
import xgboost

import shap

Обучение классификатор XGBoost#

X, y = shap.datasets.adult()

model = xgboost.XGBClassifier()
model.fit(X, y)

model_loss = -np.log(model.predict_proba(X)[:, 1]) * y + -np.log(model.predict_proba(X)[:, 0]) * (1 - y)

model_loss[:10]

Объясние логарифмической потери модели с помощью TreeExplainer#

Обратите внимание: expected_value для функции потерь модели зависит от значения метки (label),
поэтому вместо одного числового значения это теперь функция, возвращающая значение в зависимости от конкретной метки.

explainer = shap.TreeExplainer(model, X, feature_perturbation="interventional", model_output="log_loss")
explainer.shap_values(X.iloc[:10, :], y[:10]).sum(1) + np.array([explainer.expected_value(v) for v in y[:10]])

Вывод

array([8.43887488e-04, 2.47898585e-01, 1.17997435e-02, 7.11527711e-02,
       6.41849874e-01, 1.76084475e+00, 5.70285151e-03, 8.60033255e-01,
       4.78233521e-04, 6.43796897e-03])

Сводная гистограмма показывает общую значимость каждого признака#

shap_values = shap.TreeExplainer(model).shap_values(X_test)
shap_interaction_values = shap.TreeExplainer(model).shap_interaction_values(X_test)

shap.summary_plot(shap_values, X_test, plot_type="bar")

Внутренности макетной платы

Сводный график показывает глобальную значимость каждой характеристики и распределение величин эффекта#

Сводный график показывает глобальную значимость каждой характеристики и распределение величин эффекта

shap.summary_plot(shap_values, X_test)

Внутренности макетной платы

Еще несколько примеров визуализации#

Код из примера на главной странице с использованием XGBoost.

import xgboost
import shap

# Загрузка данных по ценам на жильё в Калифорнии
X, y = shap.datasets.california()

# Обучение модели XGBoost на этих данных
model = xgboost.XGBRegressor().fit(X, y)

# Построение интерпретатора SHAP для модели
# (тот же синтаксис работает для моделей LightGBM, CatBoost, scikit-learn, HuggingFace, Spark и др.)
explainer = shap.Explainer(model)

# Расчёт SHAP-значений для всех объектов в выборке
shap_values = explainer(X)

# Визуализация объяснения для первого объекта (waterfall-график)
shap.plots.waterfall(shap_values[0])

Внутренности макетной платы

shap.plots.initjs()
# визуализируем объяснение первого прогноза
shap.plots.force(shap_values[0])

Внутренности макетной платы

# визуализируем все прогнозы по обучающей выборке 
shap.plots.force(shap_values[:500])

Внутренности макетной платы

# создаём точечную диаграмму зависимости, чтобы показать влияние одного признака на весь набор данных 
shap.plots.scatter(shap_values[:, "Latitude"], color=shap_values)

Внутренности макетной платы

#суммируем влияние всех признаков 
shap.plots.beeswarm(shap_values)

Внутренности макетной платы

shap.plots.bar(shap_values)

Внутренности макетной платы