6.1. Конвейеры и составные модели

Для построения составной модели трансформаторы обычно комбинируются с другими трансформаторами или с предикторами (такими как классификаторы или регрессоры).

Наиболее распространенным инструментом, используемым для объединения моделей, является Pipeline (Конвейер). Конвейеры требуют, чтобы все шаги, кроме последнего, были трансформатором. Последний шаг может быть чем угодно, трансформатором, predictor или кластерной моделью, который может иметь или не иметь метод .predict(...).

Конвейер раскрывает все методы, предоставляемые последней моделью: если последний шаг предоставляет метод transform, то конвейер будет иметь метод transform и вести себя как трансформатор. Если последний шаг предоставляет метод predict, то конвейер будет раскрывать этот метод и, получив данные X, использовать все шаги, кроме последнего, для преобразования данных, а затем передавать эти преобразованные данные методу predict последнего шага конвейера. Класс Pipeline часто используется в сочетании с ColumnTransformer или FeatureUnion, которые объединяют выход трансформаторов в составное пространство признаков. TransformedTargetRegressor занимается преобразованием target (например, log-transform y).

6.1.1. Конвейер: цепочка оценок

Pipeline (Конвейер) можно использовать для объединения нескольких обработок в одну последовательность. Это полезно, так как часто существует фиксированная последовательность шагов при обработке данных, например, выбор признаков, нормализация и классификация. Pipeline служит нескольким целям:

Удобство и инкапсуляция

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

Совместный подбор параметров

Вы можете 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 может быть проблематичной по одной из следующих причин:

  1. Включение статистики из тестовых данных в препроцессоры делает оценки кросс-валидации ненадежными (известная как утечка данных (data leakage)), например, в случае маштабирования или обработка недостающих значений.

  2. Возможно, вы захотите включить параметры препроцессоров в поиск параметров.

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))