И разбираемся в статье Model Evaluation, Model Selection, and Algorithm Selection in Machine Learning by Sebastian Raschka из arxiv (https://arxiv.org/abs/1811.12808). 

При изучении учебных программ по machine learning я заметила недостаток материалов, посвященных сравнению моделей. Меня зовут Виолетта, я как data scientist в QIWI ежедневно занимаюсь оценкой данных и обучением моделей машинного обучения. В этой статье я рассмотрю три метода сравнения моделей.

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

Когда нужно сравнивать модели

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

  2. Тестирование различных алгоритмов. Сравнение двух разных алгоритмов. Например, линейная модель или бустинг.

  3. Поменялись значения в таблице из-за изменения методологии сбора данных. Например, нужно проанализировать две футбольные команды, но у вас данные по игрокам. Когда команда меняет состав команды, может поменять распределение признаков. 

Как сравнивать

Если обобщить, можно выделить два подхода:

  1. Оценка по пороговому значению (threshold) на перекрестной проверке или отложенной выборке (cross-validation или holdout). Если качество одной модели больше другой на (?) пунктов, то эксперимент удачный. И это нормально, если вам такая точность подходит. 

  2. На основе статистических тестов. Проводим A/B-тест, чтобы принять решение относительно p-value. 

Я расскажу о трёх методах с применением статистических тестов, которые показались (на мой авторский взгляд) простыми и интересными. Эти методы рекомендуется использовать на больших датасетах. А если данных немного, советую обратиться к источнику.

Три метода на основе статистических тестов

Метод 1. Получаем среднее и дисперсию по схеме «Холдаут 50/50»

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

Картинка из статьи

Для примера автор статьи взял датасет Ирисы Фишера

Левый график на рисунке 6 был создан путем выполнения разделения датасета 50/50 на трейн и тест. То есть было всего 150 строк, после разделения размер трейна = 75 строк, теста = 75 строк. Средняя точность составила 95%.

Та же процедура использовалась для создания правого графика на рисунке 6. Здесь разделение было по схеме 90/10, а средняя точность составила 96%. В тест попало всего 15 строк, а в трейн 135 строк. 

Рисунок 6 демонстрирует два момента:

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

Во-вторых, видим увеличение средней точности модели по мере увеличения обучающих данных. А именно на левом графике точность составила 95% при тестовом датасете в 75 строк, а на правом 96% при тестовом датасете в 15 строк.

Наша цель – получить среднее значение при меньшей дисперсии для сравнения на статистическом тесте, так как размер дисперсии влияет на количество повторений для статистического теста. 

Исходя из распределения, выбираем подходящий статистический тест.

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

# Шаг 1 - Оценим кол-во повторений
from statsmodels.stats.power import TTestIndPower
mu_control, sigma_control = 0.95,  0.002 # среднее и дисперсия контроля
mu_experiment, sigma_experiment = mu_control + 0.001, sigma_control # ожидаемый прирост среднего значения в тесте = 0.001
d = (mu_experiment - mu_control) / ((sigma_control**2 + sigma_experiment**2) / 2) ** 0.5
effect = d
alpha = 0.05
power = 0.8
analysis = TTestIndPower()
result = analysis.solve_power(effect, power=power, nobs1=None, ratio=1.0, alpha=alpha, alternative='larger')
print('Sample Size: %.3f' % result)


# Шаг 2 – Обучаем модель
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from sklearn.datasets import make_classification
from scipy.stats import ttest_ind

SAMPLE_SIZE = 50 # кол-во повторений 

X, y = make_classification(n_samples=1000, n_features=4,
                           n_informative=2, n_redundant=0,
                           random_state=0, shuffle=False)

score_list_control_group = []
score_list_test_group = []
for i in range(SAMPLE_SIZE):
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=i)
    # обучаем модель 1
    model_control = LogisticRegression(random_state=i)
    model_control.fit(X_train, y_train)
    prediction = model_control.predict(X_test)
    score = accuracy_score(y_test, prediction)
    score_list_control_group.append(score)
    # обучаем модель 2
    model_test = RandomForestClassifier(random_state=i)
    model_test.fit(X_train, y_train)
    prediction = model_test.predict(X_test)
    score = accuracy_score(y_test, prediction)
    score_list_test_group.append(score)

stat, p_val = ttest_ind(score_list_control_group, score_list_test_group, equal_var=False)
print('stat, p_val:', stat, round(p_val, 2))

Подведем итоги:

Плюсы:

  • Универсальность метода. Можно посчитать любую метрику. 

  • Схема 50/50 дает меньшую дисперсию, что положительно влияет на количество повторений разделения. 

  • Более надежная оценка модели на случайном тесте. 

Минусы:

  • Долго считать. 

  • Высокий false positive rate. 

Метод 2. Таблица сопряженности для сравнения бинарной классификации

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

В данном случае используем тест Макнемара (McNemar's test), который представляет собой непараметрический аналог хи-квадрат для парных сравнений.

Картинка из источника

На левом рисунке видим, что модель 1 дала 11 правильных, где модель 2 ошиблась. И модель 2 сделала 1 правильное предсказание, где модель 1 ошиблась. Таким образом, из соотношения 11:1 можем заключить, что модель 1 работает лучше, чем модель 2. Однако на правом рисунке соотношение составляет 25:15, что менее убедительно, чем 11:1. Это хороший пример, когда нужен тест Макнемара. Точность модели 1 = 99.7%, а точность модели 2 = 99,6%, данное различие в точности не выглядит убедительным. 

Код
from mlxtend.evaluate import mcnemar
import numpy as np

tb_a = np.array ( [[9959, 11], [1, 2911]])
chi2, p = mcnemar(ary=tb_a, exact=True)
print('chi-squared:', chi2)
print('p-value:', p)

chi-squared: None
p-value: 0.005859375

Однако, если посчитать статистический тест, то p-value (model1) < 0.05, это говорит нам, что мы можем опровергнуть нулевую гипотезу о том, что модели перформят одинаково. Так как точность модели 1 статистически значимо лучше точности модели 2.  

Код
import numpy as np
from mlxtend.evaluate import mcnemar

tb_b = np.array([[9945, 25], [15, 15]])
chi2, p = mcnemar(ary=tb_b, corrected=True)
print(“chi-squared:”, chi2)
print(“p-value:”, p)

chi-squared: 2.025
p-value: 0.154728923458

Во втором случае мы не можем утверждать, что точность модели 1 статистически значимо лучше точности модели 2. 

Подведем итоги:

Плюсы:

  • Быстро, оценка за одну итерацию

  • Низкий уровень false positive

Минусы:

  • Нужен большой датасет

  • Не работает на регрессии или вероятности отнесения к классу в случае классификации. 

Метод 3. От таблицы сопряженности к бакетному тестированию для сравнения регрессионных моделей или rocauc на моделях классификации

(наше дополнение, материал не из рассматриваемой статьи Sebastian Raschka)

ID

Feature_1

Feature_2

Feature_3

Target

Bucket

Score

1

1

1

0.8

2

0

3

1

2

0.81

4

0

5

0

Данный метод применяется как метод оптимизации времени, если дисперсия позволяет. Разобьем строки случайным образом на N бакетов. Далее обучим две модели на перекрестной проверке (cross-validation). Разделим предсказания по бакетам и посчитаем метрику в каждом бакете. 

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

Код в работе:

import pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score


BUCKETS = 50

def get_auc(data_sample):
    rocauc_control = roc_auc_score(data_sample['target'], data_sample['prediction_control'])
    rocauc_test = roc_auc_score(data_sample['target'], data_sample['prediction_test'])
    return rocauc_control, rocauc_test

X, y = make_classification(n_samples=10000, n_features=4, n_informative=2, n_redundant=0)

# Сплитуем данные
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=42)
df = pd.DataFrame()
df['target'] = y_test
df['id'] = range(y_test.shape[0])  # id или идентификатор

# обучаем модель 1
model_control = LogisticRegression()
model_control.fit(X_train, y_train)
prediction = model_control.predict_proba(X_test)[:, 0]
df['prediction_control'] = prediction

# обучаем модель 2
model_test = RandomForestClassifier()
model_test.fit(X_train, y_train)
prediction = model_test.predict_proba(X_test)[:, 0]
df['prediction_test'] = prediction

# считаем метрику по бакетам
df['bucket'] = pd.util.hash_pandas_object(df['id'], index=False) % BUCKETS
tmp = df.groupby(['bucket']).apply(get_auc).reset_index().rename(columns={0:'rocauc'})
tmp[['auc_control', 'auc_test']] = tmp['rocauc'].tolist()
stat, p_val = ttest_ind(tmp['auc_test'], tmp['auc_control'], equal_var=False)
print('stat, p_val:', stat, round(p_val, 2))

Плюсы:

  • Быстро считать

Минусы:

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

Почему это нужно делать?

  1. Участие в кагл-соревновании, где битва идет на сотые, а может, даже и тысячные доли. Тут для победы важна точность. 

  2. Генерация новых признаков для модели, где нужно оценить business-value, так как обработка и хранение фич требует ресурсов. 

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

А как вы сравниваете модели?

Использованные источники

Комментарии (1)


  1. Big_Russian_Vlad
    20.07.2023 10:37
    +1

    Очень полезная статья. Спасибо за подробный материал :)