Spec-Zone.ru › scikit-learn

Примечание

Перейти в конец, чтобы скачать полный код примера или запустить этот пример в браузере с помощью JupyterLite или Binder.

Статистическое сравнение моделей с помощью поиска по сетке

В этом примере показано, как статистически сравнить производительность моделей, обученных и оцененных с помощью GridSearchCV.

# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause

Сначала мы смоделируем данные в форме полумесяцев (где идеальное разделение классов нелинейно) и добавим к ним умеренный уровень шума. Точки данных будут принадлежать к одному из двух возможных классов, которые нужно предсказать по двум признакам. Мы смоделируем по 50 образцов для каждого класса:

import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.datasets import make_moons

X, y = make_moons(noise=0.352, random_state=1, n_samples=100)

sns.scatterplot(
    x=X[:, 0], y=X[:, 1], hue=y, marker="o", s=25, edgecolor="k", legend=False
).set_title("Data")
plt.show()
Data

Мы сравним производительность оценивателей SVC, различающихся параметром kernel, чтобы определить, какой вариант этого гиперпараметра лучше всего предсказывает смоделированные данные. Мы оценим производительность моделей с помощью RepeatedStratifiedKFold, 10 раз повторив стратифицированную 10-блочную перекрёстную проверку, каждый раз используя другую случайную перестановку данных. Производительность будет оцениваться с помощью roc_auc_score.

from sklearn.model_selection import GridSearchCV, RepeatedStratifiedKFold
from sklearn.svm import SVC

param_grid = [
    {"kernel": ["linear"]},
    {"kernel": ["poly"], "degree": [2, 3]},
    {"kernel": ["rbf"]},
]

svc = SVC(random_state=0)

cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=10, random_state=0)

search = GridSearchCV(estimator=svc, param_grid=param_grid, scoring="roc_auc", cv=cv)
search.fit(X, y)
GridSearchCV(cv=RepeatedStratifiedKFold(n_repeats=10, n_splits=10, random_state=0),
             estimator=SVC(random_state=0),
             param_grid=[{'kernel': ['linear']},
                         {'degree': [2, 3], 'kernel': ['poly']},
                         {'kernel': ['rbf']}],
             scoring='roc_auc')
В среде Jupyter повторно выполните эту ячейку, чтобы отобразить представление HTML, или доверьте ноутбуку.
На GitHub представление HTML отобразить не удаётся; попробуйте открыть эту страницу на nbviewer.org.
Параметры
estimator estimator: объект-оцениватель

Предполагается, что он реализует интерфейс оценивателя scikit-learn.
Оцениватель должен предоставлять функцию ``score``
или необходимо передать ``scoring``.
SVC(random_state=0)
param_grid param_grid: словарь или список словарей

Словарь, где ключами являются названия параметров (`str`), а значениями — списки
значений параметров для проверки, либо список таких
словарей. В последнем случае выполняется поиск по сеткам,
заданным каждым словарём в списке. Это позволяет искать среди любых последовательностей
значений параметров.
[{'kernel': ['linear']}, {'degree': [2, 3], 'kernel': ['poly']}, ...]
scoring scoring: str, вызываемый объект, список, кортеж или словарь, по умолчанию=None

Способ оценки производительности модели, проверенной перекрёстной проверкой, на
тестовом наборе.

Если `scoring` задаёт одну метрику, можно использовать:

- одну строку (см. :ref:`scoring_string_names`);
- вызываемый объект (см. :ref:`scoring_callable`), возвращающий одно значение;
- `None` — используется :ref:`критерий оценки по умолчанию <scoring_api_overview>` оценивателя `estimator`.

Если `scoring` задаёт несколько метрик, можно использовать:

- список или кортеж уникальных строк;
- вызываемый объект, возвращающий словарь, в котором ключами являются названия метрик,
а значениями — их оценки;
- словарь, в котором ключами являются названия метрик, а значениями — вызываемые объекты.

Пример см. в разделе :ref:`multimetric_grid_search`.
'roc_auc'
cv cv: int, генератор перекрёстной проверки или итерируемый объект, по умолчанию=None

Определяет стратегию разбиения для перекрёстной проверки.
Для cv можно указать:

- None — используется перекрёстная проверка по умолчанию с 5 блоками;
- целое число — количество блоков в `(Stratified)KFold`;
- :term:`разделитель CV`;
- итерируемый объект, выдающий пары (train, test) в виде массивов индексов.

Для целочисленных значений и None, если оцениватель является классификатором, а ``y``
содержит два или более классов, используется :class:`StratifiedKFold`. Во всех
остальных случаях используется :class:`KFold`. Эти разделители создаются
с параметром `shuffle=False`, поэтому при каждом вызове разбиения будут одинаковыми.

Различные стратегии перекрёстной проверки, которые можно здесь использовать,
описаны в :ref:`руководстве пользователя <cross_validation>`.

.. versionchanged:: 0.22
Значение ``cv`` по умолчанию при None изменено с 3 блоков на 5.
RepeatedStrat...andom_state=0)
n_jobs n_jobs: int, по умолчанию=None

Количество задач, выполняемых параллельно.
``None`` означает 1, кроме случаев, когда используется контекст :obj:`joblib.parallel_backend`.
``-1`` означает использование всех процессоров. Подробнее см. :term:`глоссарий <n_jobs>`.

.. versionchanged:: v0.20
Значение `n_jobs` по умолчанию изменено с 1 на None
None
refit refit: bool, str или вызываемый объект, по умолчанию=True

Повторно обучить оцениватель на всём наборе данных, используя найденные лучшие параметры.

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

Если при выборе лучшего оценивателя важно учитывать не только максимальную оценку,
для ``refit`` можно задать функцию, которая возвращает выбранный ``best_index_``
на основе ``cv_results_``. В этом случае значения ``best_estimator_`` и
``best_params_`` будут определяться возвращённым ``best_index_``, а атрибут
``best_score_`` будет недоступен.

Повторно обученный оцениватель доступен через атрибут ``best_estimator_`` и позволяет
напрямую вызывать ``predict`` для этого экземпляра
``GridSearchCV``.

Также при оценке по нескольким метрикам атрибуты ``best_index_``,
``best_score_`` и ``best_params_`` будут доступны только при заданном
``refit`` и будут определяться относительно этого конкретного
оценщика.

Подробнее об оценке по нескольким метрикам см. параметр ``scoring``.

Чтобы узнать, как создать пользовательскую стратегию выбора с помощью вызываемого объекта
через `refit`, см. :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py`.

Пример использования ``refit=callable`` для балансировки сложности модели
и оценки перекрёстной проверки см. в :ref:`этом примере
<sphx_glr_auto_examples_model_selection_plot_grid_search_refit_callable.py>`.

.. versionchanged:: 0.20
Добавлена поддержка вызываемых объектов.
True
verbose verbose: int, по умолчанию=0

Управляет подробностью сообщений, выводимых во время обучения: более высокие
значения обеспечивают более подробный журнал.

- 0 : сообщения не выводятся;
- >=1 : сводка общего числа запусков обучения;
- >=2 : время вычислений для каждого блока и кандидата параметров;
- >=3 : индексы блоков и оценки;
- >=10 : индексы кандидатов параметров и сообщения START перед каждым обучением.
0
pre_dispatch pre_dispatch: int или str, по умолчанию='2*n_jobs'

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

- None — все задачи сразу создаются и запускаются. Используйте это значение
для лёгких и быстро выполняющихся задач, чтобы избежать задержек из-за запуска задач по требованию;
- int — точное общее количество запускаемых задач;
- str — выражение, заданное как функция от n_jobs, например '2*n_jobs'
'2*n_jobs'
error_score error_score: 'raise' или числовое значение, по умолчанию=np.nan

Значение, присваиваемое оценке, если при обучении оценивателя возникает ошибка.
Если задано 'raise', ошибка вызывается. Если задано числовое значение,
вызывается FitFailedWarning. Этот параметр не влияет на этап повторного обучения,
который всегда вызывает ошибку.
nan
return_train_score return_train_score: bool, по умолчанию=False

Если значение равно ``False``, атрибут ``cv_results_`` не будет содержать оценки
на обучающих данных.
Вычисление оценок на обучающих данных позволяет понять, как разные
значения параметров влияют на компромисс между переобучением и недообучением.
Однако вычисление оценок на обучающем наборе может быть затратным
с вычислительной точки зрения и не является строго необходимым для выбора параметров,
обеспечивающих наилучшую обобщающую способность.

.. versionadded:: 0.19

.. versionchanged:: 0.21
Значение по умолчанию изменено с ``True`` на ``False``
False
Обученные атрибуты
Название Тип Значение
best_estimator_ best_estimator_: оцениватель

Оцениватель, выбранный в ходе поиска, то есть оцениватель,
который показал наивысшую оценку (или наименьшую потерю, если она задана)
на отложенных данных. Недоступен, если ``refit=False``.

Подробнее о допустимых значениях см. параметр ``refit``.
SVC SVC(random_state=0)
best_index_ best_index_: int

Индекс (в массивах ``cv_results_``), соответствующий наилучшему
набору параметров-кандидатов.

Словарь ``search.cv_results_['params'][search.best_index_]`` содержит
настройки параметров для лучшей модели, которая даёт наивысшую
среднюю оценку (``search.best_score_``).

При оценке по нескольким метрикам этот атрибут присутствует только при заданном ``refit``.
int64 np.int64(3)
best_params_ best_params_: dict

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

При оценке по нескольким метрикам этот атрибут присутствует только при заданном ``refit``.
dict {'kernel': 'rbf'}
best_score_ best_score_: float

Средняя оценка лучшего оценивателя, полученная перекрёстной проверкой.

При оценке по нескольким метрикам этот атрибут присутствует только при заданном ``refit``.

Этот атрибут недоступен, если ``refit`` является функцией.
float64 0.94
classes_ classes_: ndarray формы (n_classes,)

Метки классов. Присутствует только при заданном ``refit`` и
если базовый оцениватель является классификатором.
ndarray[int64](2,) [0,1]
cv_results_ cv_results_: словарь маскированных и обычных массивов numpy

Словарь, где ключи — названия столбцов, а значения — столбцы; его можно
импортировать в ``DataFrame`` pandas.

Например, приведённая ниже таблица

+------------+-----------+------------+-----------------+---+---------+
|param_kernel|param_gamma|param_degree|split0_test_score|...|rank_t...|
+============+===========+============+=================+===+=========+
| 'poly' | -- | 2 | 0.80 |...| 2 |
+------------+-----------+------------+-----------------+---+---------+
| 'poly' | -- | 3 | 0.70 |...| 4 |
+------------+-----------+------------+-----------------+---+---------+
| 'rbf' | 0.1 | -- | 0.80 |...| 3 |
+------------+-----------+------------+-----------------+---+---------+
| 'rbf' | 0.2 | -- | 0.93 |...| 1 |
+------------+-----------+------------+-----------------+---+---------+

будет представлена словарём ``cv_results_`` следующего вида::

{
'param_kernel': masked_array(data = ['poly', 'poly', 'rbf', 'rbf'],
mask = [False False False False]...)
'param_gamma': masked_array(data = [-- -- 0.1 0.2],
mask = [ True True False False]...),
'param_degree': masked_array(data = [2.0 3.0 -- --],
mask = [False False True True]...),
'split0_test_score' : [0.80, 0.70, 0.80, 0.93],
'split1_test_score' : [0.82, 0.50, 0.70, 0.78],
'mean_test_score' : [0.81, 0.60, 0.75, 0.85],
'std_test_score' : [0.01, 0.10, 0.05, 0.08],
'rank_test_score' : [2, 4, 3, 1],
'split0_train_score' : [0.80, 0.92, 0.70, 0.93],
'split1_train_score' : [0.82, 0.55, 0.70, 0.87],
'mean_train_score' : [0.81, 0.74, 0.70, 0.90],
'std_train_score' : [0.01, 0.19, 0.00, 0.03],
'mean_fit_time' : [0.73, 0.63, 0.43, 0.49],
'std_fit_time' : [0.01, 0.02, 0.01, 0.01],
'mean_score_time' : [0.01, 0.06, 0.04, 0.04],
'std_score_time' : [0.00, 0.00, 0.00, 0.01],
'params' : [{'kernel': 'poly', 'degree': 2}, ...],
}

Пример визуализации и интерпретации результатов GridSearch см. в :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_stats.py`.

ПРИМЕЧАНИЕ

Ключ ``'params'`` используется для хранения списка словарей
настроек параметров для всех параметров-кандидатов.

Значения ``mean_fit_time``, ``std_fit_time``, ``mean_score_time`` и
``std_score_time`` указаны в секундах.

При оценке по нескольким метрикам оценки всех оценщиков доступны
в словаре ``cv_results_`` по ключам, оканчивающимся на имя соответствующего
оценщика (``'_<scorer_name>'``), а не на ``'_score'``, как показано
выше. ('split0_test_precision', 'mean_train_precision' и т. д.)
dict {'me...me': array([0., 0., 0., 0.]), 'me...me': array([0., 0., 0., 0.]), 'me...re': array([0.93, ..., 0.9 , 0.94]), 'pa...ee': masked_array(..._value=999999), ...}
multimetric_ multimetric_: bool

Вычисляют ли оценщики несколько метрик.
bool False
n_features_in_ n_features_in_: int

Количество признаков, наблюдавшихся во время :term:`обучения`. Определён только если
задан `best_estimator_` (подробнее см. документацию к параметру `refit`) и если
у `best_estimator_` после обучения есть атрибут
`n_features_in_`.

.. versionadded:: 0.24
int 2
n_splits_ n_splits_: int

Количество разбиений (блоков/итераций) перекрёстной проверки.
int 100
refit_time_ refit_time_: float

Время в секундах, затраченное на повторное обучение лучшей модели на всём наборе данных.

Присутствует только если ``refit`` не равен False.

.. versionadded:: 0.20
float 0.002359
scorer_ scorer_: функция или словарь

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

При оценке по нескольким метрикам этот атрибут содержит проверенный
словарь ``scoring``, связывающий ключ оценщика с вызываемым объектом оценщика.
_Scorer make_scorer(r...edict_proba'))
SVC(random_state=0)
Параметры
random_state random_state: int, экземпляр RandomState или None, по умолчанию=None

Управляет генерацией псевдослучайных чисел для перемешивания данных при
оценке вероятностей. Игнорируется, если `probability` имеет значение False.
Передайте int, чтобы получать воспроизводимые результаты при нескольких вызовах функции.
См. :term:`Глоссарий <random_state>`.
0
C C: float, по умолчанию=1.0

Параметр регуляризации. Сила регуляризации
обратно пропорциональна C. Значение должно быть строго положительным. Штраф
представляет собой квадратичный штраф l2. Для наглядной визуализации влияния
масштабирования параметра регуляризации C см.
:ref:`sphx_glr_auto_examples_svm_plot_svm_scale_c.py`.
1.0
kernel kernel: {'linear', 'poly', 'rbf', 'sigmoid', 'precomputed'} или вызываемый объект, по умолчанию='rbf'

Задает тип ядра, используемого алгоритмом. Если
значение не указано, будет использоваться 'rbf'. Если задан вызываемый объект, он используется для
предварительного вычисления матрицы ядра по матрицам данных; эта матрица должна быть
массивом формы ``(n_samples, n_samples)``. Для наглядной
визуализации различных типов ядер см.
:ref:`sphx_glr_auto_examples_svm_plot_svm_kernels.py`.
'rbf'
degree degree: int, по умолчанию=3

Степень полиномиальной функции ядра ('poly').
Должна быть неотрицательной. Игнорируется для всех остальных ядер.
3
gamma gamma: {'scale', 'auto'} или float, по умолчанию='scale'

Коэффициент ядра для 'rbf', 'poly' и 'sigmoid'.

- если передано ``gamma='scale'`` (значение по умолчанию), в качестве gamma используется
1 / (n_features * X.var()),
- если указано 'auto', используется 1 / n_features
- если задано число с плавающей точкой, оно должно быть неотрицательным.

.. versionchanged:: 0.22
Значение ``gamma`` по умолчанию изменено с 'auto' на 'scale'.
'scale'
coef0 coef0: float, по умолчанию=0.0

Свободный член в функции ядра.
Имеет значение только для 'poly' и 'sigmoid'.
0.0
shrinking shrinking: bool, по умолчанию=True

Использовать ли эвристику сокращения.
См. :ref:`Руководство пользователя <shrinking_svm>`.
True
probability probability: bool, по умолчанию=False

Включить ли оценку вероятностей. Этот параметр необходимо включить до
вызова `fit`; он замедлит выполнение этого метода, поскольку внутри используется
5-кратная перекрестная проверка, а результаты `predict_proba` могут не совпадать с
результатами `predict`. Подробнее см. :ref:`Руководство пользователя <scores_probabilities>`.

..deprecated:: 1.9
Параметр `probability` объявлен устаревшим и будет удален в версии 1.11.
Вместо `SVC(probability=True)` используйте
`CalibratedClassifierCV(SVC(), ensemble=False)`.
'deprecated'
tol tol: float, по умолчанию=1e-3

Допуск для критерия остановки.
0.001
cache_size cache_size: float, по умолчанию=200

Задает размер кэша ядра (в МБ).
200
class_weight class_weight: dict или 'balanced', по умолчанию=None

Задает параметр C для класса i равным class_weight[i]*C
для SVC. Если параметр не указан, предполагается, что вес
всех классов равен единице.
Режим "balanced" использует значения y для автоматической корректировки
весов, обратно пропорциональных частотам классов во входных данных,
по формуле ``n_samples / (n_classes * np.bincount(y))``.
None
verbose verbose: bool, по умолчанию=False

Включить подробный вывод. Обратите внимание, что этот параметр использует
настройку времени выполнения в libsvm для отдельного процесса; при включении она может работать
некорректно в многопоточной среде.
False
max_iter max_iter: int, по умолчанию=-1

Жесткое ограничение числа итераций решателя; -1 означает отсутствие ограничения.
-1
decision_function_shape decision_function_shape: {'ovo', 'ovr'}, по умолчанию='ovr'

Возвращать ли функцию принятия решений «один против остальных» ('ovr') формы
(n_samples, n_classes), как у всех остальных классификаторов, или исходную
функцию принятия решений «один против одного» ('ovo') из libsvm, имеющую форму
(n_samples, n_classes * (n_classes - 1) / 2). Однако обратите внимание, что
внутри для обучения моделей всегда используется стратегия «один против одного» ('ovo')
для многоклассовой классификации; матрица ovr строится только на основе матрицы ovo.
При бинарной классификации параметр игнорируется.

.. versionchanged:: 0.19
По умолчанию для decision_function_shape используется 'ovr'.

.. versionadded:: 0.17
Рекомендуется *decision_function_shape='ovr'*.

.. versionchanged:: 0.17
*decision_function_shape='ovo' и None* объявлены устаревшими.
'ovr'
break_ties break_ties: bool, по умолчанию=False

Если значение True, ``decision_function_shape='ovr'`` и число классов > 2,
:term:`predict` будет разрешать неоднозначности, используя значения уверенности
:term:`decision_function`; в противном случае возвращается первый из классов с одинаковым результатом. Обратите внимание, что разрешение неоднозначностей связано с
относительно высокими вычислительными затратами по сравнению с обычным predict. Пример
использования с ``decision_function_shape='ovr'`` см. в
:ref:`sphx_glr_auto_examples_svm_plot_svm_tie_breaking.py`.

.. versionadded:: 0.22
False
Обученные атрибуты
Имя Тип Значение
class_weight_ class_weight_: ndarray формы (n_classes,)

Множители параметра C для каждого класса.
Вычисляются на основе параметра ``class_weight``.
ndarray[float64](2,) [1.,1.]
classes_ classes_: ndarray формы (n_classes,)

Метки классов.
ndarray[int64](2,) [0,1]
dual_coef_ dual_coef_: ndarray или разреженный массив/матрица формы (n_classes -1, n_SV)

Двойственные коэффициенты опорных векторов в функции принятия
решений (см. :ref:`sgd_mathematical_formulation`), умноженные на
соответствующие целевые значения.
Для многоклассовой классификации — коэффициенты всех классификаторов 1-vs-1.
Структура коэффициентов в многоклассовом случае несколько
нетривиальна. Подробности см. в :ref:`разделе о многоклассовой классификации Руководства пользователя
<svm_multi_class>`.
Если `X` является разреженной матрицей, эти данные также будут разреженными.
ndarray[float64](1, 48) [[-1. ,-0.09,-1. ,..., 1. , 1. , 0.19]]
fit_status_ fit_status_: int

0, если обучение завершено успешно; в противном случае 1 (будет выдано предупреждение)
int 0
intercept_ intercept_: ndarray формы (n_classes * (n_classes - 1) / 2,)

Константы в функции принятия решений.
ndarray[float64](1,) [0.01]
n_features_in_ n_features_in_: int

Количество признаков, использованных во время :term:`fit`.

.. versionadded:: 0.24
int 2
n_iter_ n_iter_: ndarray формы (n_classes * (n_classes - 1) // 2,)

Количество итераций, выполненных процедурой оптимизации для обучения модели.
Форма этого атрибута зависит от числа оптимизируемых моделей,
которое, в свою очередь, зависит от количества классов.

.. versionadded:: 1.1
ndarray[int32](1,) [47]
n_support_ n_support_: ndarray формы (n_classes,), dtype=int32

Количество опорных векторов для каждого класса.
ndarray[int32](2,) [24,24]
probA_ probA_: ndarray формы (n_classes * (n_classes - 1) / 2)

Если `probability=True`, содержит параметры, полученные при
масштабировании Платта для вычисления оценок вероятностей по значениям функции принятия решений.
Если `probability=False`, это пустой массив. При масштабировании Платта используется
логистическая функция
ndarray[float64](0,) []
probB_ probB_: ndarray формы (n_classes * (n_classes - 1) / 2)

Если `probability=True`, содержит параметры, полученные при
масштабировании Платта. При масштабировании Платта используется логистическая функция
``1 / (1 + exp(decision_value * probA_ + probB_))``
где ``probA_`` и ``probB_`` оцениваются по набору данных [2]_. Подробнее о многоклассовом случае и процедуре обучения см.
раздел 8 в [1]_.

.. deprecated:: 1.9
Атрибуты `probA_` и `probB_` объявлены устаревшими в версии 1.9 и будут
удалены в версии 1.11.
ndarray[float64](0,) []
shape_fit_ shape_fit_: кортеж int формы (n_dimensions_of_X,)

Размерности массива обучающего вектора ``X``.
tuple (100, 2)
support_ support_: ndarray формы (n_SV)

Индексы опорных векторов.
ndarray[int32](48,) [ 2, 6,20,...,89,93,97]
support_vectors_ support_vectors_: ndarray или разреженный массив/матрица формы (n_SV, n_features)

Опорные векторы. Если ядро предварительно вычислено, возвращается пустой массив.
Если `X` является разреженной матрицей, эти данные также будут разреженными.
ndarray[float64](48, 2) [[-0.27, 0.07], [-1.14, 1.21], [ 0.45, 0.06], ..., [ 1.36,-0.15], [ 1.26,-0.14], [ 0.52,-1.45]]


Теперь мы можем изучить результаты поиска, отсортированные по mean_test_score:

import pandas as pd

results_df = pd.DataFrame(search.cv_results_)
results_df = results_df.sort_values(by=["rank_test_score"])
results_df = results_df.set_index(
    results_df["params"].apply(lambda x: "_".join(str(val) for val in x.values()))
).rename_axis("kernel")
results_df[["params", "rank_test_score", "mean_test_score", "std_test_score"]]
params rank_test_score mean_test_score std_test_score
kernel
rbf {'kernel': 'rbf'} 1 0.9400 0.079297
linear {'kernel': 'linear'} 2 0.9300 0.077846
3_poly {'degree': 3, 'kernel': 'poly'} 3 0.9044 0.098776
2_poly {'degree': 2, 'kernel': 'poly'} 4 0.6852 0.169106


Видно, что классификатор с ядром 'rbf' показал наилучший результат, немного опередив 'linear'. Оба классификатора с ядром 'poly' показали худшие результаты; модель с полиномом второй степени продемонстрировала значительно более низкую эффективность, чем все остальные модели.

Обычно на этом анализ заканчивается, но тогда мы упускаем половину картины. Результат работы GridSearchCV не содержит сведений о достоверности различий между моделями. Мы не знаем, являются ли эти различия статистически значимыми. Чтобы это оценить, необходимо провести статистический тест. В частности, для сравнения производительности двух моделей нужно статистически сопоставить их оценки AUC. Для каждой модели имеется 100 выборок (оценок AUC), поскольку мы 10 раз повторили 10-блочную перекрестную проверку.

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

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

# create df of model scores ordered by performance
model_scores = results_df.filter(regex=r"split\d*_test_score")

# plot 30 examples of dependency between cv fold and AUC scores
fig, ax = plt.subplots()
sns.lineplot(
    data=model_scores.transpose().iloc[:30],
    dashes=False,
    palette="Set1",
    marker="o",
    alpha=0.5,
    ax=ax,
)
ax.set_xlabel("CV test fold", size=12, labelpad=10)
ax.set_ylabel("Model AUC", size=12)
ax.tick_params(bottom=True, labelbottom=False)
plt.show()

# print correlation of AUC scores across folds
print(f"Correlation of models:\n {model_scores.transpose().corr()}")
plot grid search stats
Correlation of models:
 kernel       rbf    linear    3_poly    2_poly
kernel
rbf     1.000000  0.882561  0.783392  0.351390
linear  0.882561  1.000000  0.746492  0.298688
3_poly  0.783392  0.746492  1.000000  0.355440
2_poly  0.351390  0.298688  0.355440  1.000000

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

В результате, если предположить независимость выборок, мы недооценим дисперсию, вычисляемую в статистических тестах, и увеличим число ошибок первого рода (то есть будем обнаруживать значимое различие между моделями там, где его нет) [1].

Для таких случаев разработано несколько статистических тестов с поправкой на дисперсию. В этом примере мы покажем, как реализовать один из них (так называемый скорректированный t-тест Наду и Бенжио) в рамках двух разных статистических подходов: частотного и байесовского.

Сравнение двух моделей: частотный подход

Для начала можно задаться вопросом: «Является ли первая модель статистически значимо лучше второй (при ранжировании по mean_test_score)?»

Чтобы ответить на этот вопрос с помощью частотного подхода, можно провести парный t-тест и вычислить p-значение. В литературе по прогнозированию он также известен как тест Диболда—Мариано [5]. Разработано множество вариантов такого t-теста, учитывающих описанную в предыдущем разделе проблему «не независимых выборок». Мы воспользуемся вариантом, для которого доказано, что он обеспечивает наибольшую воспроизводимость (то есть сходство производительности модели при оценке на разных случайных разбиениях одного набора данных), сохраняя при этом низкую частоту ошибок первого и второго рода: скорректированным t-тестом Наду и Бенжио [2] с 10-кратным повторением 10-блочной перекрестной проверки [3].

Этот скорректированный парный t-тест вычисляется следующим образом:

\[t=\frac{\frac{1}{k \cdot r}\sum_{i=1}^{k}\sum_{j=1}^{r}x_{ij}} {\sqrt{(\frac{1}{k \cdot r}+\frac{n_{test}}{n_{train}})\hat{\sigma}^2}}\]

где \(k\) — число блоков, \(r\) — число повторений перекрестной проверки, \(x\) — разница в производительности моделей, \(n_{test}\) — число выборок, используемых для тестирования, \(n_{train}\) — число выборок, используемых для обучения, а \(\hat{\sigma}^2\) — дисперсия наблюдаемых различий.

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

import numpy as np
from scipy.stats import t


def corrected_std(differences, n_train, n_test):
    """Corrects standard deviation using Nadeau and Bengio's approach.

    Parameters
    ----------
    differences : ndarray of shape (n_samples,)
        Vector containing the differences in the score metrics of two models.
    n_train : int
        Number of samples in the training set.
    n_test : int
        Number of samples in the testing set.

    Returns
    -------
    corrected_std : float
        Variance-corrected standard deviation of the set of differences.
    """
    # kr = k times r, r times repeated k-fold crossvalidation,
    # kr equals the number of times the model was evaluated
    kr = len(differences)
    corrected_var = np.var(differences, ddof=1) * (1 / kr + n_test / n_train)
    corrected_std = np.sqrt(corrected_var)
    return corrected_std


def compute_corrected_ttest(differences, df, n_train, n_test):
    """Computes right-tailed paired t-test with corrected variance.

    Parameters
    ----------
    differences : array-like of shape (n_samples,)
        Vector containing the differences in the score metrics of two models.
    df : int
        Degrees of freedom.
    n_train : int
        Number of samples in the training set.
    n_test : int
        Number of samples in the testing set.

    Returns
    -------
    t_stat : float
        Variance-corrected t-statistic.
    p_val : float
        Variance-corrected p-value.
    """
    mean = np.mean(differences)
    std = corrected_std(differences, n_train, n_test)
    t_stat = mean / std
    p_val = t.sf(np.abs(t_stat), df)  # right-tailed t-test
    return t_stat, p_val
model_1_scores = model_scores.iloc[0].values  # scores of the best model
model_2_scores = model_scores.iloc[1].values  # scores of the second-best model

differences = model_1_scores - model_2_scores

n = differences.shape[0]  # number of test sets
df = n - 1
n_train = len(next(iter(cv.split(X, y)))[0])
n_test = len(next(iter(cv.split(X, y)))[1])

t_stat, p_val = compute_corrected_ttest(differences, df, n_train, n_test)
print(f"Corrected t-value: {t_stat:.3f}\nCorrected p-value: {p_val:.3f}")
Corrected t-value: 0.750
Corrected p-value: 0.227

Сравним скорректированные значения t и p с нескорректированными:

t_stat_uncorrected = np.mean(differences) / np.sqrt(np.var(differences, ddof=1) / n)
p_val_uncorrected = t.sf(np.abs(t_stat_uncorrected), df)

print(
    f"Uncorrected t-value: {t_stat_uncorrected:.3f}\n"
    f"Uncorrected p-value: {p_val_uncorrected:.3f}"
)
Uncorrected t-value: 2.611
Uncorrected p-value: 0.005

При общепринятом уровне значимости alpha, равном p=0.05, нескорректированный t-тест показывает, что первая модель статистически значимо лучше второй.

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

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

Сравнение двух моделей: байесовский подход

Мы можем использовать байесовское оценивание, чтобы вычислить вероятность того, что первая модель лучше второй. Байесовское оценивание даст распределение, а также среднее значение \(\mu\) разностей в качестве двух моделей.

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

На наш вопрос можно ответить с помощью байесовского оценивания разными способами, но в этом примере мы реализуем подход, предложенный Бенаволи и коллегами [4].

Один из способов определить апостериорное распределение в замкнутой форме — выбрать априорное распределение, сопряжённое функции правдоподобия. Бенаволи и коллеги [4] показывают, что при сравнении качества двух классификаторов можно моделировать априорное распределение как нормальное-гамма-распределение (с неизвестными средним значением и дисперсией), сопряжённое нормальному распределению правдоподобия, и таким образом выразить апостериорное распределение как нормальное. Исключив дисперсию из этого нормального апостериорного распределения, можно определить апостериорное распределение параметра среднего значения как распределение Стьюдента. В частности:

\[St(\mu;n-1,\overline{x},(\frac{1}{n}+\frac{n_{test}}{n_{train}}) \hat{\sigma}^2)\]

где \(n\) — общее число образцов, \(\overline{x}\) — средняя разность оценок, \(n_{test}\) — число образцов, использованных для тестирования, \(n_{train}\) — число образцов, использованных для обучения, а \(\hat{\sigma}^2\) — дисперсия наблюдаемых разностей.

Обратите внимание, что в нашем байесовском подходе мы также используем скорректированную дисперсию Надо и Бенжио.

Вычислим и построим график апостериорного распределения:

# initialize random variable
t_post = t(
    df, loc=np.mean(differences), scale=corrected_std(differences, n_train, n_test)
)

Построим график апостериорного распределения:

x = np.linspace(t_post.ppf(0.001), t_post.ppf(0.999), 100)

plt.plot(x, t_post.pdf(x))
plt.xticks(np.arange(-0.04, 0.06, 0.01))
plt.fill_between(x, t_post.pdf(x), 0, facecolor="blue", alpha=0.2)
plt.ylabel("Probability density")
plt.xlabel(r"Mean difference ($\mu$)")
plt.title("Posterior distribution")
plt.show()
Posterior distribution

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

better_prob = 1 - t_post.cdf(0)

print(
    f"Probability of {model_scores.index[0]} being more accurate than "
    f"{model_scores.index[1]}: {better_prob:.3f}"
)
print(
    f"Probability of {model_scores.index[1]} being more accurate than "
    f"{model_scores.index[0]}: {1 - better_prob:.3f}"
)
Probability of rbf being more accurate than linear: 0.773
Probability of linear being more accurate than rbf: 0.227

В отличие от частотного подхода, мы можем вычислить вероятность того, что одна модель лучше другой.

Обратите внимание, что результаты похожи на полученные с помощью частотного подхода. При выбранных нами априорных распределениях мы, по сути, выполняем те же вычисления, но можем делать другие выводы.

Область практической эквивалентности

Иногда нас интересуют вероятности того, что качество наших моделей эквивалентно, причём «эквивалентность» определяется с практической точки зрения. В рамках наивного подхода [4] можно считать оцениватели практически эквивалентными, если точность различается менее чем на 1%. Но мы также можем определить практическую эквивалентность с учётом решаемой задачи. Например, разница в точности в 5% может означать увеличение продаж на $1000, и мы считаем значимым для нашего бизнеса любое превышающее это значение.

В этом примере мы определим область практической эквивалентности (ROPE) как \([-0.01, 0.01]\). То есть мы будем считать две модели практически эквивалентными, если качество их работы различается менее чем на 1%.

Чтобы вычислить вероятности практической эквивалентности классификаторов, найдём площадь под кривой апостериорного распределения в пределах интервала ROPE:

rope_interval = [-0.01, 0.01]
rope_prob = t_post.cdf(rope_interval[1]) - t_post.cdf(rope_interval[0])

print(
    f"Probability of {model_scores.index[0]} and {model_scores.index[1]} "
    f"being practically equivalent: {rope_prob:.3f}"
)
Probability of rbf and linear being practically equivalent: 0.432

Можно построить график апостериорного распределения в пределах интервала ROPE:

x_rope = np.linspace(rope_interval[0], rope_interval[1], 100)

plt.plot(x, t_post.pdf(x))
plt.xticks(np.arange(-0.04, 0.06, 0.01))
plt.vlines([-0.01, 0.01], ymin=0, ymax=(np.max(t_post.pdf(x)) + 1))
plt.fill_between(x_rope, t_post.pdf(x_rope), 0, facecolor="blue", alpha=0.2)
plt.ylabel("Probability density")
plt.xlabel(r"Mean difference ($\mu$)")
plt.title("Posterior distribution under the ROPE")
plt.show()
Posterior distribution under the ROPE

Как предложено в [4], мы можем интерпретировать эти вероятности с помощью тех же критериев, что и в частотном подходе: превышает ли вероятность попадания внутрь ROPE 95% (уровень альфа 5%)? В таком случае можно заключить, что обе модели практически эквивалентны.

Байесовский подход к оцениванию также позволяет вычислить неопределённость нашей оценки разности. Её можно вычислить с помощью доверительных интервалов. Для заданной вероятности они показывают диапазон значений, которые может принимать оцениваемая величина — в нашем случае средняя разность в качестве. Например, 50%-й доверительный интервал [x, y] говорит о том, что вероятность того, что истинная (средняя) разность в качестве моделей находится между x и y, равна 50%.

Определим доверительные интервалы для наших данных с уровнями 50%, 75% и 95%:

cred_intervals = []
intervals = [0.5, 0.75, 0.95]

for interval in intervals:
    cred_interval = list(t_post.interval(interval))
    cred_intervals.append([interval, cred_interval[0], cred_interval[1]])

cred_int_df = pd.DataFrame(
    cred_intervals, columns=["interval", "lower value", "upper value"]
).set_index("interval")
cred_int_df
нижняя граница верхняя граница
интервал
0.50 0.000977 0.019023
0.75 -0.005422 0.025422
0.95 -0.016445 0.036445


Как показано в таблице, вероятность того, что истинная средняя разность между моделями находится между 0.000977 и 0.019023, равна 50%; вероятность того, что она находится между -0.005422 и 0.025422, равна 70%, а вероятность того, что она находится между -0.016445 и 0.036445, равна 95%.

Попарное сравнение всех моделей: частотный подход

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

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

Сравним качество моделей с помощью скорректированного t-теста:

from itertools import combinations
from math import factorial

n_comparisons = factorial(len(model_scores)) / (
    factorial(2) * factorial(len(model_scores) - 2)
)
pairwise_t_test = []

for model_i, model_k in combinations(range(len(model_scores)), 2):
    model_i_scores = model_scores.iloc[model_i].values
    model_k_scores = model_scores.iloc[model_k].values
    differences = model_i_scores - model_k_scores
    t_stat, p_val = compute_corrected_ttest(differences, df, n_train, n_test)
    p_val *= n_comparisons  # implement Bonferroni correction
    # Bonferroni can output p-values higher than 1
    p_val = 1 if p_val > 1 else p_val
    pairwise_t_test.append(
        [model_scores.index[model_i], model_scores.index[model_k], t_stat, p_val]
    )

pairwise_comp_df = pd.DataFrame(
    pairwise_t_test, columns=["model_1", "model_2", "t_stat", "p_val"]
).round(3)
pairwise_comp_df
model_1 model_2 t_stat p_val
0 rbf linear 0.750 1.000
1 rbf 3_poly 1.657 0.302
2 rbf 2_poly 4.565 0.000
3 linear 3_poly 1.111 0.807
4 linear 2_poly 4.276 0.000
5 3_poly 2_poly 3.851 0.001


Мы видим, что после поправки на множественные сравнения единственная модель, качество которой статистически значимо отличается от остальных, — это '2_poly'. 'rbf' — модель, занявшая первое место по версии GridSearchCV — статистически значимо не отличается от 'linear' или '3_poly'.

Попарное сравнение всех моделей: байесовский подход

При использовании байесовского оценивания для сравнения нескольких моделей поправка на множественные сравнения не требуется (объяснение см. в [4]).

Попарные сравнения можно выполнить так же, как в первом разделе:

pairwise_bayesian = []

for model_i, model_k in combinations(range(len(model_scores)), 2):
    model_i_scores = model_scores.iloc[model_i].values
    model_k_scores = model_scores.iloc[model_k].values
    differences = model_i_scores - model_k_scores
    t_post = t(
        df, loc=np.mean(differences), scale=corrected_std(differences, n_train, n_test)
    )
    worse_prob = t_post.cdf(rope_interval[0])
    better_prob = 1 - t_post.cdf(rope_interval[1])
    rope_prob = t_post.cdf(rope_interval[1]) - t_post.cdf(rope_interval[0])

    pairwise_bayesian.append([worse_prob, better_prob, rope_prob])

pairwise_bayesian_df = pd.DataFrame(
    pairwise_bayesian, columns=["worse_prob", "better_prob", "rope_prob"]
).round(3)

pairwise_comp_df = pairwise_comp_df.join(pairwise_bayesian_df)
pairwise_comp_df
model_1 model_2 t_stat p_val worse_prob better_prob rope_prob
0 rbf linear 0.750 1.000 0.068 0.500 0.432
1 rbf 3_poly 1.657 0.302 0.018 0.882 0.100
2 rbf 2_poly 4.565 0.000 0.000 1.000 0.000
3 linear 3_poly 1.111 0.807 0.063 0.750 0.187
4 linear 2_poly 4.276 0.000 0.000 1.000 0.000
5 3_poly 2_poly 3.851 0.001 0.000 1.000 0.000


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

Результаты показывают, что у модели, занявшей первое место по версии GridSearchCV 'rbf', вероятность оказаться хуже 'linear' составляет примерно 6.8%, а вероятность оказаться хуже '3_poly' — 1.8%. Вероятность практической эквивалентности 'rbf' и 'linear' составляет 43%, тогда как для 'rbf' и '3_poly' она равна 10%.

Как и при использовании частотного подхода, вероятность того, что все модели лучше '2_poly', составляет 100%, и ни одна из них не обладает практически эквивалентным качеством.

Основные выводы

  • Небольшие различия в показателях качества вполне могут оказаться случайными, а не свидетельствовать о том, что одна модель систематически предсказывает лучше другой. Как показано в этом примере, статистика позволяет оценить вероятность этого.
  • При статистическом сравнении качества двух моделей, оценённых с помощью GridSearchCV, необходимо скорректировать вычисленную дисперсию, которая может быть занижена, поскольку оценки моделей не являются независимыми друг от друга.
  • Частотный подход с использованием парного t-теста (с поправкой на дисперсию) позволяет определить, лучше ли качество одной модели по сравнению с другой с уверенностью, превышающей случайный уровень.
  • Байесовский подход позволяет вычислить вероятности того, что одна модель лучше, хуже или практически эквивалентна другой. Он также позволяет оценить нашу уверенность в том, что истинные различия между моделями лежат в заданном диапазоне значений.
  • При статистическом сравнении нескольких моделей в частотном подходе необходимо применять поправку на множественные сравнения.

Литература

[1]

Dietterich, T. G. (1998). Приближённые статистические тесты для сравнения алгоритмов обучения с учителем для классификации. Neural computation, 10(7).

[2]

Nadeau, C., & Bengio, Y. (2000). Выводы о погрешности обобщения. В материалах конференции Advances in neural information processing systems.

[3]

Bouckaert, R. R., & Frank, E. (2004). Оценка воспроизводимости тестов значимости для сравнения алгоритмов обучения. В материалах конференции Pacific-Asia Conference on Knowledge Discovery and Data Mining.

[4] (1,2,3,4,5)

Benavoli, A., Corani, G., Demšar, J., & Zaffalon, M. (2017). Пора перемен: руководство по сравнению нескольких классификаторов с помощью байесовского анализа. The Journal of Machine Learning Research, 18(1). См. здесь библиотеку Python, сопровождающую эту статью.

[5]

Diebold, F.X. & Mariano R.S. (1995). Сравнение точности прогнозирования Journal of Business & economic statistics, 20(1), 134-144.

Общее время выполнения скрипта: (0 минут 2.407 секунды)

Launch binder
Launch JupyterLite

Download Jupyter notebook: plot_grid_search_stats.ipynb

Download Python source code: plot_grid_search_stats.py

Download zipped: plot_grid_search_stats.zip

Связанные примеры

Проверка значимости оценки классификации с помощью перестановок

Сравнение калибровки классификаторов

Распространённые ошибки при интерпретации коэффициентов линейных моделей

Параметры SVM с ядром RBF

© 2007–2026 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.9/auto_examples/model_selection/plot_grid_search_stats.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API