11. Распространенные ошибки и рекомендуемые практики#
Цель этой главы — проиллюстрировать некоторые распространенные ошибки и антипаттерны, возникающие при использовании scikit-learn. Она предоставляет примеры того, что не сделать, вместе с соответствующим правильным примером.
11.1. Несогласованная предобработка#
scikit-learn предоставляет библиотеку Преобразования наборов данных, что может очистить (см. Предобработка данных), reduce (см. Неконтролируемое снижение размерности), расширить (см. Аппроксимация ядра) или сгенерировать (см. Извлечение признаков) представления признаков. Если эти преобразования данных используются при обучении модели, они также должны применяться к последующим наборам данных, будь то тестовые данные или данные в производственной системе. В противном случае пространство признаков изменится, и модель не сможет эффективно работать.
Для следующего примера создадим синтетический набор данных с одним признаком:
>>> from sklearn.datasets import make_regression
>>> from sklearn.model_selection import train_test_split
>>> random_state = 42
>>> X, y = make_regression(random_state=random_state, n_features=1, noise=1)
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, test_size=0.4, random_state=random_state)
Неправильно
Обучающий набор данных масштабирован, но тестовый набор данных — нет, поэтому производительность модели на тестовом наборе данных хуже, чем ожидалось:
>>> from sklearn.metrics import mean_squared_error
>>> from sklearn.linear_model import LinearRegression
>>> from sklearn.preprocessing import StandardScaler
>>> scaler = StandardScaler()
>>> X_train_transformed = scaler.fit_transform(X_train)
>>> model = LinearRegression().fit(X_train_transformed, y_train)
>>> mean_squared_error(y_test, model.predict(X_test))
62.80...
Право
Вместо передачи нетрансформированного X_test to predict, мы должны преобразовать тестовые данные так же, как мы преобразовали обучающие данные:
>>> X_test_transformed = scaler.transform(X_test)
>>> mean_squared_error(y_test, model.predict(X_test_transformed))
0.90...
В качестве альтернативы мы рекомендуем использовать Pipeline, что упрощает цепочку преобразований с оценщиками и снижает вероятность забыть преобразование:
>>> from sklearn.pipeline import make_pipeline
>>> model = make_pipeline(StandardScaler(), LinearRegression())
>>> model.fit(X_train, y_train)
Pipeline(steps=[('standardscaler', StandardScaler()),
('linearregression', LinearRegression())])
>>> mean_squared_error(y_test, model.predict(X_test))
0.90...
Конвейеры также помогают избежать другой распространённой ловушки: утечки тестовых данных в обучающие данные.
11.2. Утечка данных#
Утечка данных происходит, когда информация, которая не была бы доступна во время предсказания, используется при построении модели. Это приводит к чрезмерно оптимистичным оценкам производительности, например, из кросс-валидация, и, следовательно, худшую производительность, когда модель используется на действительно новых данных, например, в производственной среде.
Распространенная причина — неразделение тестовых и обучающих подмножеств данных. Тестовые данные никогда не должны использоваться для принятия решений о модели.
Общее правило — никогда не вызывать fit на тестовых данных. Хотя это
может показаться очевидным, это легко упустить в некоторых случаях, например, при
применении определенных шагов предварительной обработки.
Хотя как обучающее, так и тестовое подмножества данных должны получать одинаковые преобразования предобработки (как описано в предыдущем разделе), важно, чтобы эти преобразования изучались только на обучающих данных. Например, если у вас есть шаг нормализации, где вы делите на среднее значение, среднее должно быть средним обучающего подмножества, не среднее значение всех данных. Если тестовое подмножество включено в расчет среднего, информация из тестового подмножества влияет на модель.
11.2.1. Как избежать утечки данных#
Ниже приведены некоторые советы по предотвращению утечки данных:
Всегда сначала разделяйте данные на обучающее и тестовое подмножества, особенно перед любыми шагами предобработки.
Никогда не включайте тестовые данные при использовании
fitиfit_transformметоды. Использование всех данных, например,fit(X), может привести к чрезмерно оптимистичным оценкам.И наоборот,
transformметод должен использоваться как на обучающей, так и на тестовой подвыборках, так как одна и та же предобработка должна применяться ко всем данным. Это можно достичь с помощьюfit_transformна обучающем подмножестве иtransformна тестовом подмножестве.Библиотека scikit-learn pipeline является отличным способом предотвращения утечки данных, так как гарантирует, что соответствующий метод выполняется на правильном подмножестве данных. Конвейер идеально подходит для использования в функциях кросс-валидации и настройки гиперпараметров.
Пример утечки данных во время предобработки подробно описан ниже.
11.2.2. Утечка данных во время предварительной обработки#
Примечание
Здесь мы решили проиллюстрировать утечку данных на этапе отбора признаков. Однако этот риск утечки актуален практически для всех преобразований в scikit-learn, включая (но не ограничиваясь)
StandardScaler,
SimpleImputer, и
PCA.
Ряд Выбор признаков функции доступны в scikit-learn. Они могут помочь удалить нерелевантные, избыточные и шумовые признаки, а также улучшить время построения модели и ее производительность. Как и с любым другим типом предобработки, выбор признаков должен only используйте обучающие данные. Включение тестовых данных в выбор признаков оптимистично сместит вашу модель.
Для демонстрации мы создадим эту задачу бинарной классификации с 10 000 случайно сгенерированных признаков:
>>> import numpy as np
>>> n_samples, n_features, n_classes = 200, 10000, 2
>>> rng = np.random.RandomState(42)
>>> X = rng.standard_normal((n_samples, n_features))
>>> y = rng.choice(n_classes, n_samples)
Неправильно
Использование всех данных для выбора признаков приводит к оценке точности
намного выше случайной, даже если наши цели полностью случайны.
Эта случайность означает, что наши X и y независимы, и поэтому мы ожидаем точность около 0.5. Однако, поскольку шаг выбора признаков 'видит' тестовые данные, модель имеет несправедливое преимущество. В некорректном примере ниже мы сначала используем все данные для выбора признаков, а затем разделяем данные на обучающую и тестовую выборки для подгонки модели. Результат — значительно более высокая, чем ожидалось, оценка точности:
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.feature_selection import SelectKBest
>>> from sklearn.ensemble import HistGradientBoostingClassifier
>>> from sklearn.metrics import accuracy_score
>>> # Incorrect preprocessing: the entire data is transformed
>>> X_selected = SelectKBest(k=25).fit_transform(X, y)
>>> X_train, X_test, y_train, y_test = train_test_split(
... X_selected, y, random_state=42)
>>> gbc = HistGradientBoostingClassifier(random_state=1)
>>> gbc.fit(X_train, y_train)
HistGradientBoostingClassifier(random_state=1)
>>> y_pred = gbc.predict(X_test)
>>> accuracy_score(y_test, y_pred)
0.76
Право
Чтобы предотвратить утечку данных, рекомендуется разделить ваши данные на обучающую
и тестовую подвыборки первый. Затем выбор признаков может быть выполнен с использованием только обучающего набора данных. Обратите внимание, что всякий раз, когда мы используем fit или fit_transform, мы используем только обучающий набор данных. Теперь оценка соответствует ожиданиям для данных, близка к случайной:
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, random_state=42)
>>> select = SelectKBest(k=25)
>>> X_train_selected = select.fit_transform(X_train, y_train)
>>> gbc = HistGradientBoostingClassifier(random_state=1)
>>> gbc.fit(X_train_selected, y_train)
HistGradientBoostingClassifier(random_state=1)
>>> X_test_selected = select.transform(X_test)
>>> y_pred = gbc.predict(X_test_selected)
>>> accuracy_score(y_test, y_pred)
0.5
Здесь снова мы рекомендуем использовать Pipeline объединить
вместе оценщики отбора признаков и модели. Конвейер гарантирует,
что только обучающие данные используются при выполнении fit а тестовые данные используются только для расчета оценки точности:
>>> from sklearn.pipeline import make_pipeline
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, random_state=42)
>>> pipeline = make_pipeline(SelectKBest(k=25),
... HistGradientBoostingClassifier(random_state=1))
>>> pipeline.fit(X_train, y_train)
Pipeline(steps=[('selectkbest', SelectKBest(k=25)),
('histgradientboostingclassifier',
HistGradientBoostingClassifier(random_state=1))])
>>> y_pred = pipeline.predict(X_test)
>>> accuracy_score(y_test, y_pred)
0.5
Конвейер также может быть передан в функцию перекрестной проверки, такую как cross_val_score.
Опять же, конвейер гарантирует, что правильное подмножество данных и метод
оценщика используются во время обучения и предсказания:
>>> from sklearn.model_selection import cross_val_score
>>> scores = cross_val_score(pipeline, X, y)
>>> print(f"Mean accuracy: {scores.mean():.2f}+/-{scores.std():.2f}")
Mean accuracy: 0.43+/-0.05
11.3. Управление случайностью#
Некоторые объекты scikit-learn по своей природе случайны. Обычно это оцениватели (например, RandomForestClassifier) и сплиттеры перекрёстной проверки
(например, KFold). Случайность этих объектов контролируется через их random_state параметр, как описано в ГлоссарийЭтот раздел расширяет словарную статью и описывает лучшие практики и распространённые ошибки относительно этого тонкого параметра.
Примечание
Резюме рекомендаций
Для оптимальной надежности результатов перекрестной проверки (CV) передайте
RandomState экземпляры при создании оценщиков или оставить random_state
to None. Передача целых чисел в разделители CV обычно является самым безопасным вариантом и предпочтительна; передача RandomState экземпляры в сплиттеры иногда могут быть полезны для достижения очень специфичных случаев использования. Как для оценщиков, так и для сплиттеров, передача целого числа против передачи экземпляра (или None) приводит к тонким, но значительным различиям,
особенно для процедур перекрестной проверки. Эти различия важно
понимать при представлении результатов.
Для воспроизводимых результатов между запусками удалите любое использование
random_state=None.
11.3.1. Используя None или RandomState экземпляры, и повторные вызовы fit и split#
The random_state параметр определяет, будут ли множественные вызовы fit
(для оценщиков) или к split (для сплиттеров перекрестной проверки) дадут одинаковые
результаты согласно этим правилам:
Если передано целое число, вызов
fitилиsplitмногократное выполнение всегда даёт одинаковые результаты.Если
NoneилиRandomStateэкземпляр передается:fitиsplitбудут давать разные результаты при каждом вызове, и последовательность вызовов исследует все источники энтропии.Noneявляется значением по умолчанию для всехrandom_stateпараметры.
Здесь мы иллюстрируем эти правила как для оценщиков, так и для разделителей перекрестной проверки.
Примечание
Поскольку передача random_state=None эквивалентно передаче глобального
RandomState экземпляр из numpy
(random_state=np.random.mtrand._rand), мы не будем явно упоминать
None здесь. Все, что применимо к экземплярам, также применимо к использованию
None.
11.3.1.1. Оценщики#
Передача экземпляров означает, что вызов fit несколько раз не даст одинаковых результатов, даже если оценщик обучен на одних и тех же данных и с одними и теми же гиперпараметрами:
>>> from sklearn.linear_model import SGDClassifier
>>> from sklearn.datasets import make_classification
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> X, y = make_classification(n_features=5, random_state=rng)
>>> sgd = SGDClassifier(random_state=rng)
>>> sgd.fit(X, y).coef_
array([[ 8.85418642, 4.79084103, -3.13077794, 8.11915045, -0.56479934]])
>>> sgd.fit(X, y).coef_
array([[ 6.70814003, 5.25291366, -7.55212743, 5.18197458, 1.37845099]])
Мы видим из приведенного выше фрагмента, что многократный вызов sgd.fit произвел разные модели, даже если данные были одинаковыми. Это связано с тем, что генератор случайных чисел (RNG) оценщика потребляется (т.е. изменяется), когда fit вызывается, и этот измененный RNG будет использоваться в последующих
вызовах fit. Кроме того, rng объект является общим для всех объектов, которые его используют, и, как следствие, эти объекты становятся в некоторой степени взаимозависимыми. Например, два оценщика, которые используют один и тот же
RandomState экземпляры будут влиять друг на друга, как мы увидим позже при обсуждении клонирования. Этот момент важно иметь в виду при отладке.
Если бы мы передали целое число в random_state параметр
SGDClassifier, мы бы получили те же модели и, следовательно, те же оценки каждый раз. Когда мы передаём целое число, тот же ГСЧ используется во всех вызовах fit. Внутренне происходит следующее:
даже если ГСЧ используется, когда fit вызывается, он всегда сбрасывается в исходное состояние в начале fit.
11.3.1.2. Разделители перекрестной проверки#
Рандомизированные разделители CV имеют аналогичное поведение, когда RandomState
экземпляр передан; вызов split многократное выполнение дает разные разделения
данных:
>>> from sklearn.model_selection import KFold
>>> import numpy as np
>>> X = y = np.arange(10)
>>> rng = np.random.RandomState(0)
>>> cv = KFold(n_splits=2, shuffle=True, random_state=rng)
>>> for train, test in cv.split(X, y):
... print(train, test)
[0 3 5 6 7] [1 2 4 8 9]
[1 2 4 8 9] [0 3 5 6 7]
>>> for train, test in cv.split(X, y):
... print(train, test)
[0 4 6 7 8] [1 2 3 5 9]
[1 2 3 5 9] [0 4 6 7 8]
Мы видим, что разделения отличаются от второго раза split вызывается. Это может привести к неожиданным результатам, если вы сравниваете производительность
нескольких оценщиков, вызывая split много раз, как мы увидим в следующем разделе.
11.3.2. Распространенные ошибки и тонкости#
Хотя правила, которые управляют random_state параметр кажутся простыми, однако они имеют некоторые тонкие последствия. В некоторых случаях это может даже привести к неверным выводам.
11.3.2.1. Оценщики#
Различные random_state типы приводят к различным процедурам
кросс-валидации
В зависимости от типа random_state параметр, оценщики будут вести себя по-разному, особенно в процедурах перекрестной проверки. Рассмотрим следующий фрагмент кода:
>>> from sklearn.ensemble import RandomForestClassifier
>>> from sklearn.datasets import make_classification
>>> from sklearn.model_selection import cross_val_score
>>> import numpy as np
>>> X, y = make_classification(random_state=0)
>>> rf_123 = RandomForestClassifier(random_state=123)
>>> cross_val_score(rf_123, X, y)
array([0.85, 0.95, 0.95, 0.9 , 0.9 ])
>>> rf_inst = RandomForestClassifier(random_state=np.random.RandomState(0))
>>> cross_val_score(rf_inst, X, y)
array([0.9 , 0.95, 0.95, 0.9 , 0.9 ])
Мы видим, что перекрестно проверенные оценки rf_123 и rf_inst различны, как и следовало ожидать, поскольку мы не передали тот же random_state
параметр. Однако разница между этими оценками более тонкая, чем
кажется, и процедуры перекрёстной проверки, выполненные
cross_val_score значительно различаются в каждом случае:
Поскольку
rf_123было передано целое число, каждый вызовfitиспользует тот же ГСЧ: это означает, что все случайные характеристики оценщика случайного леса будут одинаковыми для каждого из 5 фолдов процедуры CV. В частности, (случайно выбранное) подмножество признаков оценщика будет одинаковым во всех фолдах.Поскольку
rf_instбыл переданRandomStateэкземпляр, каждый вызовfitначинается с другого ГСЧ. В результате случайное подмножество признаков будет разным для каждого фолда.
Хотя наличие постоянного ГСЧ оценщика по фолдам не является по своей сути ошибкой, мы обычно хотим получить результаты CV, устойчивые к случайности оценщика. Поэтому передача экземпляра вместо целого числа может быть предпочтительнее, поскольку это позволит ГСЧ оценщика варьироваться для каждого фолда.
Примечание
Здесь, cross_val_score будет использовать нерандомизированный разделитель перекрестной проверки (как по умолчанию), поэтому оба оценщика будут оцениваться на одних и тех же разбиениях. Этот раздел не касается изменчивости в разбиениях. Также, передаем ли мы целое число или экземпляр в
make_classification не имеет значения для нашей иллюстрации: важно то, что мы передаем в
RandomForestClassifier оценщик.
Клонирование#
Еще один тонкий побочный эффект передачи RandomState экземпляров заключается в том, как
clone будет работать:
>>> from sklearn import clone
>>> from sklearn.ensemble import RandomForestClassifier
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> a = RandomForestClassifier(random_state=rng)
>>> b = clone(a)
Поскольку RandomState экземпляр был передан в a, a и b не являются клонами в строгом смысле, а скорее клонами в статистическом смысле: a и b
все равно будут разными моделями, даже при вызове fit(X, y) на тех же данных. Более того, a и b будут влиять друг на друга, поскольку они используют один и тот же внутренний ГСЧ: вызов a.fit будет потреблять b’s RNG, и вызов
b.fit будет потреблять aRNG, поскольку они одинаковы. Это верно для любых оценщиков, которые используют общий random_state параметр; он не специфичен для
клонов.
Если бы было передано целое число, a и b были бы точными клонами и не влияли бы друг на друга.
Предупреждение
Хотя clone редко используется в пользовательском коде, он
вызывается повсеместно во всей кодовой базе scikit-learn: в частности, большинство
мета-оценщиков, принимающих необученные оценщики, вызывают
clone внутренне
(GridSearchCV,
StackingClassifier,
CalibratedClassifierCV, и т.д.).
11.3.2.2. Разделители перекрестной проверки#
При передаче RandomState экземпляра, CV сплиттеры дают разные разбиения каждый раз split вызывается. При сравнении различных оценщиков это может привести к завышению дисперсии разницы в производительности между оценщиками:
>>> from sklearn.naive_bayes import GaussianNB
>>> from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
>>> from sklearn.datasets import make_classification
>>> from sklearn.model_selection import KFold
>>> from sklearn.model_selection import cross_val_score
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> X, y = make_classification(random_state=rng)
>>> cv = KFold(shuffle=True, random_state=rng)
>>> lda = LinearDiscriminantAnalysis()
>>> nb = GaussianNB()
>>> for est in (lda, nb):
... print(cross_val_score(est, X, y, cv=cv))
[0.8 0.75 0.75 0.7 0.85]
[0.85 0.95 0.95 0.85 0.95]
Прямое сравнение производительности
LinearDiscriminantAnalysis estimator vs the GaussianNB estimator на каждом фолде would
be a mistake: разбиения, на которых оцениваются оценщики, различны. Действительно, cross_val_score будет внутренне вызывать cv.split на том же
KFold экземпляра, но разбиения будут разными каждый раз. Это также верно для любого инструмента, выполняющего выбор модели через перекрестную проверку, например,
GridSearchCV и
RandomizedSearchCV: оценки не
сопоставимы между сгибами при разных вызовах search.fit, поскольку
cv.split был бы вызван несколько раз. В рамках одного вызова
search.fit, однако, сравнение между фолдами возможно, поскольку поисковый оценщик вызывает только cv.split один раз.
Для сопоставимых результатов от фолда к фолду во всех сценариях следует передавать целое число в CV-сплиттер: cv = KFold(shuffle=True, random_state=0).
Примечание
Хотя сравнение между фолдами не рекомендуется с RandomState
экземпляров, однако можно ожидать, что средние оценки позволят сделать вывод о том, лучше ли один оценщик другого, при условии использования достаточного количества фолдов и данных.
Примечание
Что важно в этом примере, так это то, что было передано в
KFold. Независимо от того, передаём ли мы RandomState
экземпляр или целое число для make_classification
не имеет значения для нашей иллюстрации. Также ни
LinearDiscriminantAnalysis ни
GaussianNB являются рандомизированными оценщиками.
11.3.3. Общие рекомендации#
11.3.3.1. Получение воспроизводимых результатов при многократных запусках#
Для получения воспроизводимых (т.е. постоянных) результатов при нескольких
выполнения программ, нам нужно удалить все использования random_state=None, что является значением по умолчанию. Рекомендуемый способ — объявить rng переменная вверху программы и передать ее вниз любому объекту, который принимает random_state
параметр:
>>> from sklearn.ensemble import RandomForestClassifier
>>> from sklearn.datasets import make_classification
>>> from sklearn.model_selection import train_test_split
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> X, y = make_classification(random_state=rng)
>>> rf = RandomForestClassifier(random_state=rng)
>>> X_train, X_test, y_train, y_test = train_test_split(X, y,
... random_state=rng)
>>> rf.fit(X_train, y_train).score(X_test, y_test)
0.84
Теперь мы гарантируем, что результат этого скрипта всегда будет 0.84, независимо от
того, сколько раз мы его запускаем. Изменение глобального rng Изменение переменной на другое значение должно влиять на результаты, как и ожидалось.
Также возможно объявить rng переменную как целое число. Однако это может привести к менее надежным результатам перекрестной проверки, как мы увидим в следующем разделе.
Примечание
Мы не рекомендуем устанавливать глобальный numpy seed путем вызова
np.random.seed(0). См. здесь
для обсуждения.
11.3.3.2. Устойчивость результатов перекрёстной проверки#
Когда мы оцениваем производительность рандомизированного оценщика с помощью перекрестной проверки, мы хотим убедиться, что оценщик может давать точные прогнозы для новых данных, но также хотим убедиться, что оценщик устойчив относительно своей случайной инициализации. Например, мы хотели бы, чтобы случайная инициализация весов SGDClassifier быть стабильно хорошим по всем фолдам: в противном случае, когда мы обучаем этот оценщик на новых данных, нам может не повезти, и случайная инициализация может привести к плохой производительности. Аналогично, мы хотим, чтобы случайный лес был устойчив к набору случайно выбранных признаков, которые будет использовать каждое дерево.
По этим причинам предпочтительнее оценивать производительность перекрёстной проверки, позволяя оценщику использовать разные RNG на каждом сгибе. Это делается путём передачи RandomState экземпляр (или None) для инициализации оценщика.
Когда мы передаем целое число, оценщик будет использовать один и тот же ГСЧ на каждом сгибе: если оценщик работает хорошо (или плохо), как оценивается перекрестной проверкой, это может быть просто потому, что нам повезло (или не повезло) с этим конкретным начальным значением. Передача экземпляров приводит к более надежным результатам перекрестной проверки и делает сравнение различных алгоритмов более справедливым. Это также помогает ограничить искушение рассматривать ГСЧ оценщика как гиперпараметр, который можно настраивать.
Передаем ли мы RandomState экземпляры или целые числа для CV сплиттеров не
влияет на устойчивость, пока split вызывается только один раз. Когда split
вызывается несколько раз, сравнение между фолдами становится невозможным. Как
результат, передача целого числа в CV сплиттеры обычно безопаснее и покрывает большинство
случаев использования.