машинное обучение
February 15, 2023

Крутой способ оценки влияния признака на модель, о котором мало кто знает

Для моделей машинного обучения методы диагностики влияния признаков на прогноз представляют исключительную важность.

Одним из ключевых способов является разбиение значений признака на интервалы и подсчет для каждого из них усредненного прогноза для всех объектов датасета. То есть фактически для всех точек подсчитывается значение прогноза при подмене признака на заданное значение, затем усредняется, и процедура повторяется для всех бинов признака. Для моделей, основанных на деревьях решений, оценка влияния признака может быть подсчитана еще путем определения разницы в прогнозах модели при наличии признака и удалении его из деревьев.

Загрузим экспериментальный датасет california_housing_train (расположен в Colab-е по адресу /content/sample_data/):

import pandas as pd
import numpy as np

df = pd.read_csv('/content/sample_data/california_housing_train.csv')
df.head()

Обучим модель HistGradientBoostingRegressor:

from sklearn.ensemble import HistGradientBoostingRegressor

model = HistGradientBoostingRegressor().fit(df.drop(columns='median_house_value'), \
                                            df['median_house_value'])

Теперь оценим влияние признака на прогноз модели с методом PartialDependenceDisplay.from_estimator модуля sklearn.inspection. Первый и второй параметры - обученная модель и матрица с точками, на которых делается прогноз, grid_resolution - размерность сетки (бинов) для исследования, features - список исследуемых признаков, method - способ подсчета прогноза (brute - общий способ, recursion - для деревянных моделей, о которых писал выше):

%%time
from sklearn.inspection import PartialDependenceDisplay

PartialDependenceDisplay.from_estimator(model, df.drop(columns='median_house_value'),
                                        n_jobs=-1, grid_resolution=20, 
                                        features=['median_income'], method='brute')

Теперь применим метод recursion:

%%time
PartialDependenceDisplay.from_estimator(model, df.drop(columns='median_house_value'),
                                        n_jobs=-1, grid_resolution=20, 
                                        features=['median_income'], method='recursion')

Второй метод (для деревянных моделей по умолчанию) намного быстрее. Также примечательна разница в области прогноза, которая обусловлена тем, что бустинговые модели при методе recursion первое дерево (константу равную среднему) не учитывают. Функция partial_dependence является неграфическим аналогом вышеуказанного способа, которая позволяет вывести значения цели (ключ average результата) и сетки признаков (ключ values):

from sklearn.inspection import partial_dependence

res = partial_dependence(model, df.drop(columns='median_house_value'), 
                         grid_resolution=20,
                         features=['median_income'], method='brute')

res['average'][0][:2], res['values'][0][:2]

Убедимся, что величины "значимости" получаются путем усреднения прогноза по всем точкам с подменой значений колонки признака на заданные (для первых двух точек):

(model.predict(df.drop(columns='median_house_value').assign(median_income=res['values'][0][0])).mean(),
 model.predict(df.drop(columns='median_house_value').assign(median_income=res['values'][0][1])).mean()
)

Если признак категориальный, то укажите его в параметре categorical_features, чтобы он не разбивался по бинам при анализе:

bins = np.linspace(df['median_income'].min()-1, df['median_income'].max()+1, 10)
df['median_income_cat'] = pd.cut(df['median_income'], bins=bins, labels=False)
categorical = ['median_income_cat']

model.fit(df.drop(columns='median_house_value'), df['median_house_value'])

PartialDependenceDisplay.from_estimator(model, df.drop(columns='median_house_value'), 
                                        n_jobs=-1, grid_resolution=20,
                                        features=['median_income', 'median_income_cat'], 
                                        categorical_features=categorical, method='brute')

В функции PartialDependenceDisplay также предусмотрена возможность отображения изменения прогноза от пары признаков, а не только одного (для этого укажите их в кортеже в параметре features):

PartialDependenceDisplay.from_estimator(model, df.drop(columns='median_house_value'), 
                                        features= [('longitude', 'latitude')], 
                                        n_jobs=-1, grid_resolution=20, method='brute')

Построенные уже графики выводили усредненный прогноз по всем точкам датасета при фиксации значения признака в неком интервале. Однако можно выводить и индивидуальные графики для отдельных точек. Это регулируется параметром kind: kind='individual' - активирует режим отображения частных графиков, kind='both'- оба режима (+ усреднение). При визуализации графиков для точек также важны параметры subsample, который определяет их количество или долю, random_state - инициализатор счетчика случайных чисел, centered - флаг центрирования прогнозов графиков:

PartialDependenceDisplay.from_estimator(model, df.drop(columns='median_house_value'), centered=True, 
                                        n_jobs=-1, grid_resolution=20, subsample = 0.1, random_state = 0, 
                                        features=['median_income'], kind='both', method='brute')

Полезные ссылки:

  1. Объяснение принципа построения графиков частичной зависимости
  2. Пример построения
  3. Документация по PartialDependenceDisplay
  4. Документация по partial_dependence