На этом шаге мы рассмотрим реализацию такой проверки.
В предыдущих примерах мы прошли путь от использования одного разбиения данных на обучающий, проверочный и тестовый наборы (143 шаг: "Опасность переобучения параметров и проверочный набор данных") до разбиения данных на обучающий и тестовый наборы с проведением перекрестной проверки на обучающем наборе (144 шаг: "Решетчатный поиск с перекрестной проверкой"). Но при использовании GridSearchCV ранее описанным способом мы все еще выполняем всего лишь одно разбиение на обучающий и тестовый наборы, что может привести к получению нестабильных результатов и ставит нас в зависимость от этого единственного разбиения данных. Мы можем пойти дальше и вместо однократного разбиения исходных данных на обучающий и тестовый наборы использовать несколько разбиений перекрестной проверки. В результате мы получим вложенную перекрестную проверку (nested cross-validation). Во вложенной перекрестной проверке используется внешний цикл по разбиениям данных на обучающий и тестовый наборы. Для каждого из них выполняется решетчатый поиск (в результате чего для каждого разбиения внешнего цикла можно получить разные наилучшие параметры). Затем для каждого внешнего разбиения выводится правильность на тестовом наборе с использованием наилучших параметров.
Результатом этой процедуры является не модель и не настройки параметров, а список значений правильности. Значения правильности указывают нам на обобщающую способность модели с использованием лучших параметров, найденных в ходе решетчатого поиска. Поскольку вложенная перекрестная проверка не дает модель, которую можно использовать на новых данных, ее редко используют при поиске прогнозной модели для применения к новым данным. Тем не менее, она может быть полезна для оценки работы модели на конкретном наборе данных.
Реализовать вложенную перекрестную проверку в scikit-learn довольно просто. Мы вызываем cross_val_score и передаем ей экземпляр GridSearchCV в качестве модели.
[In 36]: scores = cross_val_score(GridSearchCV(SVC(), param_grid, cv=5), iris.data, iris.target, cv=5) print("Значения правильности перекрестной проверки: ", scores) print("Среднеее значение правильности перекрестной проверки: ", scores.mean()) Значения правильности перекрестной проверки: [0.96666667 1. 0.9 0.96666667 1. ] Среднеее значение правильности перекрестной проверки: 0.9666666666666668
Результат нашей вложенной перекрестной проверки можно резюмировать так: "на наборе данных iris модель SVC может достигнуть средней правильности перекрестной проверки 97%" - ни больше, ни меньше.
В данном случае мы использовали стратифицированную пятиблочную перекрестную проверку как во внутреннем, так и во внешнем циклах. Поскольку наша сетка param_grid содержит 36 комбинаций параметров, будет построено целых 36 * 5 * 5 = 900 моделей, что делает процедуру вложенной перекрестной проверки очень затратной с вычислительной точки зрения. В данном случае во внутреннем и внешнем циклах мы использовали один и тот же генератор разбиений, однако это не является необходимым условием и поэтому для внутреннего и внешнего циклов вы можете использовать любую комбинацию стратегий перекрестной проверки. Понимание процесса, который происходит внутри одной строки, приведенной выше, может представлять определенную сложность. Данный процесс можно визуализировать с помощью циклов for, как это сделано в следующей упрощенной реализации программного кода:
[In 37]: def nested_cv(X, y, inner_cv, outer_cv, Classifier, parameter_grid): outer_scores = [] # для каждого разбиения данных во внешней перекрестной проверке # (метод split возвращает индексы) for training_samples, test_samples in outer_cv.split(X, y): # находим наилучшие параметры с помощью внутренней перекрестной проверки best_parms = {} best_score = -np.inf # итерируем по параметрам for parameters in parameter_grid: # собираем значения правильности по всем внутренним разбиениям cv_scores = [] # итерируем по разбиениям внутренней перекрестной проверки for inner_train, inner_test in inner_cv.split( X[training_samples], y[training_samples]): # строим классификатор с данными параметрами на внутреннем обучающем наборе clf = Classifier(**parameters) clf.fit(X[inner_train], y[inner_train]) # оцениваем качество на внутреннем тестовом наборе score = clf.score(X[inner_test], y[inner_test]) cv_scores.append(score) # вычисляем среднее значение правильности по внутренним блокам mean_score = np.mean(cv_scores) if mean_score > best_score: # если лучше, чем предыдущие, запоминаем параметры best_score = mean_score best_params = parameters # строим классификатор с лучшими параметрами на внешнем обучающем наборе clf = Classifier(**best_params) clf.fit(X[training_samples], y[training_samples]) # оцениваем качество на внешнем тестовом наборе outer_scores.append(clf.score(X[test_samples], y[test_samples])) return np.array(outer_scores)
Теперь давайте применим эту функцию к набору данных iris:
[In 38]: from sklearn.model_selection import ParameterGrid, StratifiedKFold scores = nested_cv(iris.data, iris.target, StratifiedKFold(5), StratifiedKFold(5), SVC, ParameterGrid(param_grid)) print("Значения правильности перекрестной проверки: {}".format(scores)) Значения правильности перекрестной проверки: [0.96666667 1. 0.96666667 0.96666667 1. ]
На следующем шаге мы рассмотрим распараллеливание перекрестной проверки и решетчатого поиска.