Статьи
October 27, 2022

Как найти слабые места в моделях машинного обучения

Каждый раз, когда вы упрощаете данные с помощью расчёта сводных статистик, вы теряете полезную информацию. Расчёт точности моделей – не исключение. Упрощая результаты обучения модели до одной метрики, вы теряете возможность определить, в каких частях данных метрика высокая/низкая и почему.

Рисунок 1. Дерево решений с наименьшей точностью, обученное с использованием POC FreaAI от IBM.

В этой статье мы обсудим код, лежащий в основе IBM’s FreaAI — эффективного метода нахождения срезов данных с низкой точностью — и рассмотрим реализацию MVP-подхода для бинарного классификатора.

Начинаем!

0. Техническое отступление

FreaAI — эффективный метод для определения срезов данных низкого качества. В нём используются методы highest prior density (HPD) и деревья решений для поиска в пространстве признаков мест с наименьшей точностью и предлагает подход для улучшения интерпретируемости моделей.

1. Загружаем данные

Для начала нам нужно загрузить данные классификации отсюда. Они являются общедоступными и имеют лицензию CC0. Данные показаны ниже на рисунке №2.

Рисунок №2. Последние 4 столбца нашего датасета.
import pandas as pd
df = pd.read_csv('https://raw.githubusercontent.com/obulygin/content/main/How%20to%20Find%20Weaknesses%20in%20Your%20Machine%20Learning%20Models/pima-indians-diabetes.data.csv',
header=None)

Создадим несколько фиктивных категориальных столбцов, чтобы в т.ч. на очень зашумлённых искусственных данных проверить результаты.

# добавляем категориальные столбцы с рандомными значениями
np.random.seed(0)
enc_df = pd.DataFrame(dict(
        color=['red' if x > 0.25 else 'green' for x in
np.random.rand(len(df.index))],
        gender=['male' if x > 0.55 else 'female' for x in
np.random.rand(len(df.index))]
))

enc = OneHotEncoder(handle_unknown='ignore')
enc_df =
pd.DataFrame(enc.fit_transform(enc_df[['color','gender']]).toarray())

df = pd.concat([df, enc_df], ignore_index=True, axis=1)

df.columns = ['num_pregnancies','glucose','blood_pressure','skin_thickness','insuli
n','bmi','diabetes_pedigree','age','outcome',
'color_red','color_green','gender_male','gender_female']

2. Обучаем неинтерпретируемую модель

Теперь нам нужно обучить модель XGBoost.

# разбиваем данные на X и y
mask = np.array(list(df)) == 'outcome'
X = df.loc[:,~mask]
Y = df.loc[:,mask]

# разбиваем данные на тестовую и тренировочную выборки
seed = 7
test_size = 0.33
X_train, X_test, y_train, y_test = train_test_split(
                                     X, Y,
                                     test_size=test_size,
                                     random_state=seed)
                                     
# обучаем модель
model = XGBClassifier()
model.fit(X_train, y_train)

# делаем прогноз для тестовых данных
y_pred = model.predict(X_test)
predictions = [round(value) for value in y_pred]

# оцениваем качество
accuracy = accuracy_score(y_test, predictions)
print("Accuracy: %.2f%%" % (accuracy * 100.0))

out = pd.concat([X_test, y_test], axis=1, ignore_index=True)
out.columns = list(df)accuracy_bool = (np.array(y_test).flatten() ==
                np.array(predictions))

out['accuracy_bool'] = accuracy_bool
out

В такой конфигурации эта модель имеет точность около 71%. Чтобы оценить прогноз для каждого объекта, мы создали столбец accuracy_bool, который имеет логический тип: True – если категория предсказана верно, иначе – False.

3. Тренируем интерпретируемое дерево решений

Более подробно разберём часть нашего кода, связанную с деревом решений. Как и в случае с HPD, мы стремимся выявить части наших данных, которые демонстрируют аномально низкую точность. Итак, мы тренируем дерево решений, где:

  • признаки – один или два признака, которые используются в модели XGBoost.
  • целевая переменная – это созданный выше столбец accuracy_bool.

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

Первым делом создадим простую функцию, которое обучает наше дерево решений.

def fit_DT(df, predictors = ['age']):
    """Функция для обучения бинарного классификатора"""
    
    X = df[predictors]
    y = df['accuracy_bool']
    
    model = DecisionTreeClassifier(max_depth=3,
                                        criterion='entropy',
                                        random_state=1)
    model.fit(X, y)
    
    preds = model.predict(X)
    acc = accuracy_score(y, preds)
    
    return model, preds, acc, X

С её помощью мы можем передавать интересующие признаки в параметр predictors и получать срезы данных с низкой точностью.

3.1 Одномерное дерево принятия решений

В качестве примера давайте используем числовой столбец возраста, а затем визуализируем его с помощью функции export_graphviz из sklearn.

Рисунок №3. Дерево решений, обученное по возрасту.

На рисунке №3 каждый узел (синий прямоугольник) соответствует срезу данных. Мы ищем те, что:

  • имеют низкую энтропию;
  • имеют достаточный размер;
  • демонстрируют низкую точность в сравнении с базовым уровнем в 71%.

Следуя приведённой выше логике мы видим, что узлы 3, 4 и 12 имеют энтропию <0.3. Узел 12 мы можем сразу исключить, так как в нём всего 2 экземпляра (samples=2), что делает его бесполезным. Узлы 3 и 4 имеют достаточный размер выборки, но их точность составляет 94,7% и 95,8% соответственно, что указывает на то, что они улучшают качество модели, а не ухудшают.

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

3.2 Двумерное дерево принятия решений

Здесь мы воспользуемся способностью дерева решений обрабатывать несколько признаков, что поможет нам понять их взаимодействие. В этом примере интерес представляют признаки diabetes_pedigree и blood_pressure.

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

Используя параметр filled=True в библиотеке graph_viz, мы видим, что на Рисунке №4 оранжевые узлы имеют больше значений в 1-м индексе, чем во 2-м. То есть, у нас больше ошибок, чем верных классификаций.

Итак, ещё раз — мы ищем узлы с низкой энтропией, с достаточным количеством элементов и низкой точностью. Узлы 10, 11 и 12 чётко показывают области низкой точности, но имеют относительно небольшие размеры выборки. Точность узла 9, рассчитанная с использованием values[1] / sum(values), составляет 55%, что на 16% ниже нашего базового уровня. Эти узлы следует изучить дополнительно.

Полученный нами результат можно легко интерпретировать:

  • Для людей с glucose ≤ 104,5 мы видим точность 88%, как показано в нашем корневом узле. Если glucose ≤ 87,5, то мы получаем 100% точность, как показано в узле 2. Глюкоза — чрезвычайно полезная характеристика для классификации в левой части своего распределения.
  • Для людей с glucose > 104,5 и blood_pressure > 91,0 наша точность падает до 18%. Одновременно высокие кровяное давление и глюкоза являются зашумленными областями наших данных и не позволяют делать хороший прогноз.

Такие выводы смогут полноценно оценить только специалисты по диабету, а мы можем продолжать анализ, используя визуализации из библиотеки dtreeviz:

Рисунок №5. График данных дерева решений по артериальному давлению и уровню глюкозы.

На рисунке 5 у нас есть то же самое дерево решений, что и на Рисунке 4, но на этот раз мы визуализируем распределения данных на каждом узле или листе.

Сразу же обнаруживаем некоторые аномалии. Если посмотреть на самый правый путь дерева, мы видим, что крайний правый лист соответствует 5 ошибочным классификациям, где уровень глюкозы очень высок, что согласуется с выводами из предыдущего дерева. Кроме того, мы получаем хорошую картину нашей области высокой точности на крайнем левом листе — это просто часть распределения корней с glucose ≤ 87,5.

Прекрасно! Теперь, когда мы рассмотрели ключевые визуализации, давайте обсудим следующий шаг для этого двумерного пространства признаков. Этот код возвращает индексы наших проблемных срезов данных.

Например, для узла 11 строковые индексы таких 6 точек данных — это: [58, 87, 105, 108, 222, 230]. И тут старый-добрый EDA даст больше информации.

Не будем описывать эти шаги для краткости, но общая суть понятна.

4. Определение срезов с низкой точностью

Теперь, когда мы рассмотрели очень конкретный пример, давайте соберём всё вместе в одном метаанализе нашей модели.

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

Рисунок №6. Вывод нашего метаанализа.

Как мы видим на Рисунке №6, наш вывод имеет 4 колонки:

  1. names: название признака(-ов) для HPD;
  2. indices: индексы строк этого среза данных;
  3. accuracies: точность этого среза данных;
  4. method: использовали ли мы дерево решений (DT) или метод highest prior density (HPD).

Во-первых, мы видим, что срезы с наименьшей точностью получаются в двумерных деревьях решений. Ранее были созданы столбцы со случайными данными (gender и color) для оценки точности на категориальных случайных переменных, и, как и ожидалось, в этих столбцах точность минимальная.

Первое действительно проблемное взаимодействие возникает в признаках blood_pressure и num_pregnancies. Если бы нам было интересно копнуть глубже, мы могли бы изучить индексы и посмотреть, какие строки это дерево решений считает проблемными.

Мы также видим, что применение метода HPD не было эффективным для определения срезов с низкой точностью — самый слабый срез показал точность 66%. Хоть метод HPD и может быть полезным, дерево решений, обученное разделением по энтропии, нашло более проблемные срезы.

В конце концов, это простенький пример, который мы слепили на коленке для проверки концепции. Можно получить значительно более хорошие результаты, и FreaAI от IBM, несомненно, обладает огромной функциональностью и простором для оптимизации. Этот код можно использовать в качестве отправной точки :)

Оригинал: towardsdatascience.com

PythonTalk в Telegram

Чат PythonTalk в Telegram

Предложить материал | Поддержать канал