машинное обучение
January 19, 2023

Корректное использование сида при сравнении моделей на кросс-валидации

Как я рассказывал ранее, использование класса np.random.RandomState для инициализации счетчика случайных чисел в моделях является предпочтительным для оценки качества. В то же время при сравнении результатов кросс-валидации моделей в сплиттер лучше вместо данного объекта передавать целый сид, иначе наборы фолдов будут отличаться между собой, поэтому и модели сравнивать не совсем корректно. Сгенерируем демонстрационный набор данных:

from sklearn.datasets import make_classification
import numpy as np
SEED = 0
RNG = np.random.RandomState(SEED)

X, y = make_classification(n_samples=10000, n_features=5, n_informative=2, 
                           n_redundant=0, class_sep = 2, random_state=SEED, shuffle=True, 
                           flip_y=0.3, n_clusters_per_class=2)

В переменную SEED поместим целое значение сида, и инициализируем им объект np.random.RandomState. Теперь рассмотрим два случайных набора сплитов с заданным целым сидом и оценим качество классификаторов:

from sklearn.model_selection import ShuffleSplit
from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeClassifier

sp = ShuffleSplit(n_splits=3, random_state=SEED, test_size=0.25)

for tr_idx, val_idx in sp.split(X):
    print(np.mean(val_idx))
display(cross_val_score(DecisionTreeClassifier(random_state=SEED), X, y, cv=sp).mean())
print()   

for tr_idx, val_idx in sp.split(X):
    print(np.mean(val_idx))
display(cross_val_score(DecisionTreeClassifier(random_state=SEED), X, y, cv=sp).mean())

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

Теперь проведем то же с сплиттером, инициализированным случайным генератором RNG, который меняет счетчик случайных чисел с каждым вызовом метода split в отличие от целого сида:

sp = ShuffleSplit(n_splits=3, random_state=RNG, test_size=0.25)

for tr_idx, val_idx in sp.split(X):
    print(np.mean(val_idx))
display(cross_val_score(DecisionTreeClassifier(random_state=SEED), X, y, cv=sp).mean())
print()

for tr_idx, val_idx in sp.split(X):
    print(np.mean(val_idx))
display(cross_val_score(DecisionTreeClassifier(random_state=SEED), X, y, cv=sp).mean())
    

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

Замечу, если пересоздавать объект ShuffleSplit с np.random.RandomState с одним SEED, то результаты вновь станут одинаковые, так как эффект такой же, как при передаче целого (с пересозданием np.random.RandomState цепочка изменений счетчика случайных значений повторяется):

sp = ShuffleSplit(n_splits=3, random_state=np.random.RandomState(SEED), test_size=0.25)
for tr_idx, val_idx in sp.split(X):
    print(np.mean(val_idx))
display(cross_val_score(DecisionTreeClassifier(random_state=SEED), X, y,cv=sp).mean())
print()

sp = ShuffleSplit(n_splits=3, random_state=np.random.RandomState(SEED), test_size=0.25)
for tr_idx, val_idx in sp.split(X):
    print(np.mean(val_idx))
display(cross_val_score(DecisionTreeClassifier(random_state=SEED), X, y,cv=sp).mean())