6.1. Конвейеры и составные модели¶
Для построения составной модели трансформаторы обычно комбинируются с другими трансформаторами или с предикторами (такими как классификаторы или регрессоры).
Наиболее распространенным инструментом, используемым для объединения моделей, является Pipeline (Конвейер). Конвейеры требуют, чтобы все шаги, кроме последнего, были трансформатором. Последний шаг может быть чем угодно, трансформатором, predictor или кластерной моделью, который может иметь или не иметь метод .predict(...)
.
Конвейер раскрывает все методы, предоставляемые последней моделью: если последний шаг предоставляет метод transform
, то конвейер будет иметь метод transform
и вести себя как трансформатор. Если последний шаг предоставляет метод predict
, то конвейер будет раскрывать этот метод и, получив данные X, использовать все шаги, кроме последнего, для преобразования данных, а затем передавать эти преобразованные данные методу predict
последнего шага конвейера.
Класс Pipeline
часто используется в сочетании с ColumnTransformer или FeatureUnion, которые объединяют выход трансформаторов в составное пространство признаков.
TransformedTargetRegressor занимается преобразованием target (например, log-transform y).
6.1.1. Конвейер: цепочка оценок¶
Pipeline
(Конвейер) можно использовать для объединения нескольких обработок в одну последовательность. Это полезно, так как часто существует фиксированная последовательность шагов при обработке данных, например, выбор признаков, нормализация и классификация. Pipeline
служит нескольким целям:
Удобство и инкапсуляция
Совместный подбор параметров
Вы можете grid search по параметрам всех моделей в конвейере одновременно.
Безопасность
Конвейеры помогают избежать утечки статистики из тестовых данных в обученную модель при кросс-валидации, гарантируя, что для обучения трансформаторов и предикторов используются одни и те же выборки.
Все модели в конвейере, кроме последнего, должны быть трансформаторами (т.е. должны иметь метод transform).
Последняя модель может быть любого типа (трансформатор, классификатор и т.д.).
Примечание
Вызов fit
на конвейере - это то же самое, что вызов fit
на каждой модели по очереди, transform
входных данных и передача их на следующий шаг. Конвейер имеет все методы, которые имеет последняя модель в конвейере, т.е. если последняя модель является классификатором, то Pipeline
может быть использован в качестве классификатора. Если последняя модель является трансформатором, то и конвеер тоже.
6.1.1.1. Использование¶
6.1.1.1.1. Построение конвеера¶
Конвейер Pipeline
строится с помощью списка пар (key, value)
, где key
- это строка, содержащая имя, которое вы хотите дать этому шагу, а value
- объект модели:
>>> from sklearn.pipeline import Pipeline
>>> from sklearn.svm import SVC
>>> from sklearn.decomposition import PCA
>>> estimators = [('reduce_dim', PCA()), ('clf', SVC())]
>>> pipe = Pipeline(estimators)
>>> pipe
Pipeline(steps=[('reduce_dim', PCA()), ('clf', SVC())])
Вспомогательная функция
Укороченная версия с использованием функции :func:`make_pipeline`
Click for more details
¶
make_pipeline
является сокращением для построения конвейеров; она принимает переменное количество моделей и возвращает конвейер, автоматически заполняя имена:>>> from sklearn.pipeline import make_pipeline
>>> make_pipeline(PCA(), SVC())
Pipeline(steps=[('pca', PCA()), ('svc', SVC())])
6.1.1.1.2. Доступ к шагам конвейера¶
Модели конвеера хранятся в виде списка в атрибуте steps
.
Подконвейер может быть извлечен с помощью нотации нарезки, обычно используемой для последовательностей Python, таких как списки или строки (хотя допускается только шаг 1). Это удобно для выполнения только некоторых преобразований (или их обратных):
>>> pipe[:1]
Pipeline(steps=[('reduce_dim', PCA())])
>>> pipe[-1:]
Pipeline(steps=[('clf', SVC())])
К конкретному шагу можно также получить доступ по индексу или имени, проиндексировав (с помощью Атрибут
Получение шага по имени или позиции.
Click for more details
¶
[idx]
) конвеер:>>> pipe.steps[0]
('reduce_dim', PCA())
>>> pipe[0]
PCA()
>>> pipe['reduce_dim']
PCA()
named_steps
в Pipeline
позволяет обращаться к шагам по имени с завершением табуляции в интерактивных средах:>>> pipe.named_steps.reduce_dim is pipe['reduce_dim']
True
6.1.1.1.3. Отслеживание имен признаков в конвеере¶
Чтобы обеспечить проверку модели, в Pipeline
есть метод get_feature_names_out()
, как и во всех трансформаторах. Вы можете использовать срез конвейера, чтобы получить имена функций на каждом шаге:
>>> from sklearn.datasets import load_iris
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.feature_selection import SelectKBest
>>> iris = load_iris()
>>> pipe = Pipeline(steps=[
... ('select', SelectKBest(k=2)),
... ('clf', LogisticRegression())])
>>> pipe.fit(iris.data, iris.target)
Pipeline(steps=[('select', SelectKBest(...)), ('clf', LogisticRegression(...))])
>>> pipe[:-1].get_feature_names_out()
array(['x2', 'x3'], ...)
Вы также можете задать пользовательские имена признаков для входных данных с помощью
Настройка имен функций
Click for more details
¶
get_feature_names_out
:>>> pipe[:-1].get_feature_names_out(iris.feature_names)
array(['petal length (cm)', 'petal width (cm)'], ...)
6.1.1.1.4. Доступ к вложенным параметрам¶
Обычно параметры модели настраиваются внутри конвейера. Поэтому этот параметр является вложенным, поскольку он принадлежит определенному подэтапу. Доступ к параметрам модели в конвейере осуществляется с помощью синтаксиса <estimator>__<parameter>
:
>>> pipe = Pipeline(steps=[("reduce_dim", PCA()), ("clf", SVC())])
>>> pipe.set_params(clf__C=10)
Pipeline(steps=[('reduce_dim', PCA()), ('clf', SVC(C=10))])
Это особенно важно для поиска по сетке: Отдельные шаги также могут быть заменены в качестве параметров, а нефинальные шаги могут быть проигнорированы путем установки для них значения
Когда это имеет значение?
Click for more details
¶
>>> from sklearn.model_selection import GridSearchCV
>>> param_grid = dict(reduce_dim__n_components=[2, 5, 10],
... clf__C=[0.1, 10, 100])
>>> grid_search = GridSearchCV(pipe, param_grid=param_grid)
passthrough
:>>> param_grid = dict(reduce_dim=['passthrough', PCA(5), PCA(10)],
... clf=[SVC(), LogisticRegression()],
... clf__C=[0.1, 10, 100])
>>> grid_search = GridSearchCV(pipe, param_grid=param_grid)
6.1.1.2. Кэширующие трансформаторы: избежать повторных вычислений¶
Обучение трансформаторов может требовать больших вычислительных затрат. С установленным параметром memory
, Pipeline
будет кэшировать каждый трансформатор после вызова fit
.
Эта функция используется для того, чтобы не вычислять пройденые трансформаторы в рамках одного конвейера, если параметры и входные данные идентичны. Типичным примером является поиск по сетке, в котором трансформаторы могут быть подобраны только один раз и повторно использованы для каждой конфигурации. Последний шаг никогда не будет кэшироваться, даже если он является трансформатором.
Для кэширования трансформаторов необходим параметр memory
. memory
может быть либо строкой, содержащей каталог, в котором кэшируются трансформаторы, либо объектом joblib.Memory:
>>> from tempfile import mkdtemp
>>> from shutil import rmtree
>>> from sklearn.decomposition import PCA
>>> from sklearn.svm import SVC
>>> from sklearn.pipeline import Pipeline
>>> estimators = [('reduce_dim', PCA()), ('clf', SVC())]
>>> cachedir = mkdtemp()
>>> pipe = Pipeline(estimators, memory=cachedir)
>>> pipe
Pipeline(memory=...,
steps=[('reduce_dim', PCA()), ('clf', SVC())])
>>> # Clear the cache directory when you don't need it anymore
>>> rmtree(cachedir)
Используя Включение кэширования вызывает клонирование трансформаторов перед установкой. Поэтому экземпляр трансформатора, переданный конвейеру, не может быть проверен напрямую. В следующем примере обращение к экземпляру
Предупреждение: Побочный эффект кэширования трансформаторов
Click for more details
¶
Pipeline
без включенного кэша, можно просмотреть исходный экземпляр, например:>>> from sklearn.datasets import load_digits
>>> X_digits, y_digits = load_digits(return_X_y=True)
>>> pca1 = PCA()
>>> svm1 = SVC()
>>> pipe = Pipeline([('reduce_dim', pca1), ('clf', svm1)])
>>> pipe.fit(X_digits, y_digits)
Pipeline(steps=[('reduce_dim', PCA()), ('clf', SVC())])
>>> # The pca instance can be inspected directly
>>> print(pca1.components_)
[[-1.77484909e-19 ... 4.07058917e-18]]
PCA
pca2
вызовет ошибку AttributeError
, так как pca2
будет необработанным трансформатором. Вместо этого используйте атрибут named_steps
для проверки моделей внутри конвейера:>>> cachedir = mkdtemp()
>>> pca2 = PCA()
>>> svm2 = SVC()
>>> cached_pipe = Pipeline([('reduce_dim', pca2), ('clf', svm2)],
... memory=cachedir)
>>> cached_pipe.fit(X_digits, y_digits)
Pipeline(memory=...,
steps=[('reduce_dim', PCA()), ('clf', SVC())])
>>> print(cached_pipe.named_steps['reduce_dim'].components_)
[[-1.77484909e-19 ... 4.07058917e-18]]
>>> # Remove the cache directory
>>> rmtree(cachedir)
6.1.2. Преобразование цели в регрессии¶
TransformedTargetRegressor
преобразует цели y
перед обучением регрессионной модели. Прогнозы возвращаются в исходное пространство с помощью обратного преобразования. В качестве аргумента принимает регрессор, который будет использоваться для предсказания, и трансформатор, который будет применен к целевой переменной:
>>> import numpy as np
>>> from sklearn.datasets import fetch_california_housing
>>> from sklearn.compose import TransformedTargetRegressor
>>> from sklearn.preprocessing import QuantileTransformer
>>> from sklearn.linear_model import LinearRegression
>>> from sklearn.model_selection import train_test_split
>>> X, y = fetch_california_housing(return_X_y=True)
>>> X, y = X[:2000, :], y[:2000] # select a subset of data
>>> transformer = QuantileTransformer(output_distribution='normal')
>>> regressor = LinearRegression()
>>> regr = TransformedTargetRegressor(regressor=regressor,
... transformer=transformer)
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
>>> regr.fit(X_train, y_train)
TransformedTargetRegressor(...)
>>> print('R2 score: {0:.2f}'.format(regr.score(X_test, y_test)))
R2 score: 0.61
>>> raw_target_regr = LinearRegression().fit(X_train, y_train)
>>> print('R2 score: {0:.2f}'.format(raw_target_regr.score(X_test, y_test)))
R2 score: 0.59
Для простых преобразований вместо объекта Transformer можно передать пару функций, определяющих преобразование и его обратное отображение:
>>> def func(x):
... return np.log(x)
>>> def inverse_func(x):
... return np.exp(x)
Впоследствии объект создается как:
>>> regr = TransformedTargetRegressor(regressor=regressor,
... func=func,
... inverse_func=inverse_func)
>>> regr.fit(X_train, y_train)
TransformedTargetRegressor(...)
>>> print('R2 score: {0:.2f}'.format(regr.score(X_test, y_test)))
R2 score: 0.51
По умолчанию при каждом обучении переданные функции проверяются на то, что они являются обратными друг другу. Однако можно обойти эту проверку, установив check_inverse
в False
:
>>> def inverse_func(x):
... return x
>>> regr = TransformedTargetRegressor(regressor=regressor,
... func=func,
... inverse_func=inverse_func,
... check_inverse=False)
>>> regr.fit(X_train, y_train)
TransformedTargetRegressor(...)
>>> print('R2 score: {0:.2f}'.format(regr.score(X_test, y_test)))
R2 score: -1.57
Примечание
Преобразование можно запустить, задав либо transformer
, либо пару функций func
и inverse_func
. Однако установка обеих опций приведет к ошибке.
6.1.3. FeatureUnion: составные пространства признаков¶
FeatureUnion
объединяет несколько объектов-трансформеров в новый трансформатор, который объединяет их выходные данные. В FeatureUnion
берется список объектов-трансформеров. Во время обучения каждый из них подгоняется к данным независимо. Трансформаторы применяются параллельно, а матрицы признаков, которые они выдают, объединяются в большую матрицу.
Если вы хотите применить различные преобразования к каждому полю данных, обратитесь к связанному классу ColumnTransformer
(см. руководство пользователя).
FeatureUnion
служит тем же целям, что и Pipeline
- удобство и совместная оценка параметров и валидация.
FeatureUnion
и Pipeline
могут быть объединены для создания сложных моделей.
(У FeatureUnion
нет возможности проверить, могут ли два трансформатора дать одинаковые признаки. Он создает объединение только в том случае, если наборы признаков расходятся, а убедиться в этом должен вызывающий).
6.1.3.1. Применение¶
FeatureUnion
строится с помощью списка пар (key, value)
, где key
- это имя, которое вы хотите дать данному преобразованию (произвольная строка; она служит только идентификатором), а value
- объект модели:
>>> from sklearn.pipeline import FeatureUnion
>>> from sklearn.decomposition import PCA
>>> from sklearn.decomposition import KernelPCA
>>> estimators = [('linear_pca', PCA()), ('kernel_pca', KernelPCA())]
>>> combined = FeatureUnion(estimators)
>>> combined
FeatureUnion(transformer_list=[('linear_pca', PCA()),
('kernel_pca', KernelPCA())])
Как и конвейеры, объединенные признаки имеют сокращенный конструктор make_union
, который не требует явного именования компонентов.
Как и в Pipeline
, отдельные шаги могут быть заменены с помощью set_params
, а также проигнорированы установкой на 'drop
:
>>> combined.set_params(kernel_pca='drop')
FeatureUnion(transformer_list=[('linear_pca', PCA()),
('kernel_pca', 'drop')])
6.1.4. ColumnTransformer для разнородных данных¶
Многие наборы данных содержат признаки разных типов, например, текст, числа плавающие точкой и даты, и для каждого типа признаков требуются отдельные шаги предварительной обработки или извлечения признаков. Часто проще всего предварительно обработать данные перед применением методов scikit-learn, например, используя pandas. Обработка данных перед передачей их в scikit-learn может быть проблематичной по одной из следующих причин:
Включение статистики из тестовых данных в препроцессоры делает оценки кросс-валидации ненадежными (известная как утечка данных (data leakage)), например, в случае маштабирования или обработка недостающих значений.
Возможно, вы захотите включить параметры препроцессоров в поиск параметров.
ColumnTransformer
помогает выполнять различные преобразования для разных столбцов данных в рамках Pipeline
, который защищен от утечки данных и может быть параметризован. ColumnTransformer
работает с массивами, разреженными матрицами и pandas DataFrames.
К каждому столбцу можно применить различные преобразования, например, предварительную обработку или особый метод извлечения признаков:
>>> import pandas as pd
>>> X = pd.DataFrame(
... {'city': ['London', 'London', 'Paris', 'Sallisaw'],
... 'title': ["His Last Bow", "How Watson Learned the Trick",
... "A Moveable Feast", "The Grapes of Wrath"],
... 'expert_rating': [5, 3, 4, 5],
... 'user_rating': [4, 5, 4, 3]})
Для этих данных мы можем захотеть закодировать столбец 'city'
как категориальную переменную с помощью OneHotEncoder
, но применить CountVectorizer
к столбцу 'title'
. Поскольку мы можем использовать несколько методов извлечения признаков для одного и того же столбца, мы даем каждому трансформатору уникальное имя, например 'city_category'
и 'title_bow'
. По умолчанию остальные столбцы рейтинга игнорируются (remainder='drop'
):
>>> from sklearn.compose import ColumnTransformer
>>> from sklearn.feature_extraction.text import CountVectorizer
>>> from sklearn.preprocessing import OneHotEncoder
>>> column_trans = ColumnTransformer(
... [('categories', OneHotEncoder(dtype='int'), ['city']),
... ('title_bow', CountVectorizer(), 'title')],
... remainder='drop', verbose_feature_names_out=False)
>>> column_trans.fit(X)
ColumnTransformer(transformers=[('categories', OneHotEncoder(dtype='int'),
['city']),
('title_bow', CountVectorizer(), 'title')],
verbose_feature_names_out=False)
>>> column_trans.get_feature_names_out()
array(['city_London', 'city_Paris', 'city_Sallisaw', 'bow', 'feast',
'grapes', 'his', 'how', 'last', 'learned', 'moveable', 'of', 'the',
'trick', 'watson', 'wrath'], ...)
>>> column_trans.transform(X).toarray()
array([[1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0],
[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1]]...)
В приведенном выше примере CountVectorizer
ожидает получить на вход одномерный массив, поэтому столбцы были указаны в виде строки ('title'
).
Однако OneHotEncoder
, как и большинство других трансформаторов, ожидает 2D-данные, поэтому в этом случае необходимо указать столбец в виде списка строк (['city']
).
Кроме скаляра или списка из одного элемента, выбор столбца может быть задан в виде списка из нескольких элементов, целочисленного массива, среза, булевой маски или с помощью make_column_selector
. Функция make_column_selector
используется для выбора столбцов на основе типа данных или имени столбца:
>>> from sklearn.preprocessing import StandardScaler
>>> from sklearn.compose import make_column_selector
>>> ct = ColumnTransformer([
... ('scale', StandardScaler(),
... make_column_selector(dtype_include=np.number)),
... ('onehot',
... OneHotEncoder(),
... make_column_selector(pattern='city', dtype_include=object))])
>>> ct.fit_transform(X)
array([[ 0.904..., 0. , 1. , 0. , 0. ],
[-1.507..., 1.414..., 1. , 0. , 0. ],
[-0.301..., 0. , 0. , 1. , 0. ],
[ 0.904..., -1.414..., 0. , 0. , 1. ]])
Строки могут ссылаться на столбцы, если входные данные представляют собой DataFrame, целые числа всегда интерпретируются как позиционные столбцы.
Мы можем сохранить оставшиеся столбцы рейтинга, задав remainder='passthrough'
. Значения будут добавлены в конец преобразования:
>>> column_trans = ColumnTransformer(
... [('city_category', OneHotEncoder(dtype='int'),['city']),
... ('title_bow', CountVectorizer(), 'title')],
... remainder='passthrough')
>>> column_trans.fit_transform(X)
array([[1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 5, 4],
[1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 3, 5],
[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 4],
[0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 5, 3]]...)
Параметр remainder
может быть установлен в оценочное значение для преобразования оставшихся столбцов рейтинга. Преобразованные значения добавляются в конец преобразования:
>>> from sklearn.preprocessing import MinMaxScaler
>>> column_trans = ColumnTransformer(
... [('city_category', OneHotEncoder(), ['city']),
... ('title_bow', CountVectorizer(), 'title')],
... remainder=MinMaxScaler())
>>> column_trans.fit_transform(X)[:, -2:]
array([[1. , 0.5],
[0. , 1. ],
[0.5, 0.5],
[1. , 0. ]])
Функция make_column_transformer
доступна для более простого создания объекта ColumnTransformer
. В частности, имена будут заданы автоматически. Эквивалентом для приведенного выше примера будет:
>>> from sklearn.compose import make_column_transformer
>>> column_trans = make_column_transformer(
... (OneHotEncoder(), ['city']),
... (CountVectorizer(), 'title'),
... remainder=MinMaxScaler())
>>> column_trans
ColumnTransformer(remainder=MinMaxScaler(),
transformers=[('onehotencoder', OneHotEncoder(), ['city']),
('countvectorizer', CountVectorizer(),
'title')])
Если ColumnTransformer
установлен с датафреймом, и датафрейм имеет только строковые имена столбцов, то преобразование датафрейма будет использовать имена столбцов для выбора столбцов:
>>> ct = ColumnTransformer(
... [("scale", StandardScaler(), ["expert_rating"])]).fit(X)
>>> X_new = pd.DataFrame({"expert_rating": [5, 6, 1],
... "ignored_new_col": [1.2, 0.3, -0.1]})
>>> ct.transform(X_new)
array([[ 0.9...],
[ 2.1...],
[-3.9...]])
6.1.5. Визуализация композитных оценок¶
При отображении в блокноте jupyter оценочные показатели отображаются в виде HTML-представления. Это полезно для диагностики или визуализации конвейера с большим количеством оценок. Эта визуализация активирована по умолчанию:
>>> column_trans
Ее можно отключить, установив опцию display
в set_config
на ‘text’:
>>> from sklearn import set_config
>>> set_config(display='text')
>>> # displays text representation in a jupyter context
>>> column_trans
Пример HTML-вывода можно посмотреть в разделе HTML-представление конвейера в Column Transformer with Mixed Types. В качестве альтернативы HTML можно записать в файл с помощью estimator_html_repr
:
>>> from sklearn.utils import estimator_html_repr
>>> with open('my_estimator.html', 'w') as f:
... f.write(estimator_html_repr(clf))