6.1. 管道和复合估计器#

为了构建复合估计器,转换器通常与其他转换器或与 预测器(如分类器或回归器)组合在一起。用于组合估计器的最常见工具是 管道。管道要求除最后一步之外的所有步骤都是 转换器。最后一步可以是任何东西,转换器、 预测器 或聚类估计器,它可能具有或不具有 .predict(...) 方法。管道公开最后估计器提供的所有方法:如果最后一步提供 transform 方法,则管道将具有 transform 方法并像转换器一样工作。如果最后一步提供 predict 方法,则管道将公开该方法,并且给定数据 X,使用除最后一步之外的所有步骤来转换数据,然后将该转换后的数据提供给管道最后一步的 predict 方法。类 Pipeline 通常与 ColumnTransformerFeatureUnion 结合使用,后者将转换器的输出连接到一个复合特征空间中。 TransformedTargetRegressor 用于转换 目标(即对数转换 y)。

6.1.1. 管道:链接估计器#

Pipeline 可用于将多个估计器链接在一起。这很有用,因为在处理数据时通常存在固定的步骤序列,例如特征选择、规范化和分类。 Pipeline 在这里有多种用途

便利性和封装

您只需对数据调用一次 fitpredict 即可拟合整个估计器序列。

联合参数选择

您可以 网格搜索 所有管道中估计器的参数。

安全性

管道通过确保使用相同的样本训练转换器和预测器,有助于避免在交叉验证中将测试数据的统计信息泄漏到训练模型中。

管道中的所有估计器(最后一个除外)必须是转换器(即必须具有 transform 方法)。最后一个估计器可以是任何类型(转换器、分类器等)。

注意

在管道上调用 fit 等同于依次调用每个估计器的 fittransform 输入并将其传递给下一步。管道具有管道中最后一个估计器具有的所有方法,即如果最后一个估计器是分类器,则 Pipeline 可用作分类器。如果最后一个估计器是转换器,那么管道也是如此。

6.1.1.1. 用法#

6.1.1.1.1. 构建管道#

使用 (key, value) 对列表构建 Pipeline,其中 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())])
使用 make_pipeline 的简写版本#

实用函数 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())])
按名称或位置访问步骤#

也可以通过索引(使用 [idx])管道来按索引或名称访问特定步骤。

>>> pipe.steps[0]
('reduce_dim', PCA())
>>> pipe[0]
PCA()
>>> pipe['reduce_dim']
PCA()

Pipelinenamed_steps 属性允许在交互式环境中使用选项卡完成按名称访问步骤。

>>> 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'], ...)
自定义特征名称#

您还可以使用 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))])
何时重要?#

这对于进行网格搜索尤其重要。

>>> 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)
缓存转换器的副作用#

使用没有启用缓存的 Pipeline,可以检查原始实例,例如

>>> from sklearn.datasets import load_digits
>>> X_digits, y_digits = load_digits(return_X_y=True)
>>> pca1 = PCA(n_components=10)
>>> svm1 = SVC()
>>> pipe = Pipeline([('reduce_dim', pca1), ('clf', svm1)])
>>> pipe.fit(X_digits, y_digits)
Pipeline(steps=[('reduce_dim', PCA(n_components=10)), ('clf', SVC())])
>>> # The pca instance can be inspected directly
>>> pca1.components_.shape
(10, 64)

启用缓存会在拟合之前触发转换器的克隆。因此,无法直接检查传递给管道的转换器实例。在以下示例中,访问 PCA 实例 pca2 将引发 AttributeError,因为 pca2 将是一个未拟合的转换器。相反,请使用属性 named_steps 检查管道内的估计器。

>>> cachedir = mkdtemp()
>>> pca2 = PCA(n_components=10)
>>> 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(n_components=10)), ('clf', SVC())])
>>> cached_pipe.named_steps['reduce_dim'].components_.shape
(10, 64)
>>> # 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 或函数对 funcinverse_func 来触发转换。但是,设置这两个选项将引发错误。

示例

6.1.3. FeatureUnion:组合特征空间#

FeatureUnion 将多个转换器对象组合成一个新的转换器,该转换器组合了它们的输出。 FeatureUnion 接受一个转换器对象列表。在拟合过程中,每个转换器都独立地拟合到数据。转换器并行应用,它们输出的特征矩阵并排连接成一个更大的矩阵。

当您想对数据的每个字段应用不同的转换时,请参阅相关的类 ColumnTransformer(请参阅 用户指南)。

FeatureUnionPipeline 具有相同的目的 - 方便性和联合参数估计和验证。

FeatureUnionPipeline 可以组合起来创建复杂的模型。

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. 将来自测试数据的统计信息纳入预处理器会使交叉验证分数不可靠(称为数据泄漏),例如在缩放器或插补缺失值的情况下。

  2. 您可能希望将预处理器的参数包含在 参数搜索 中。

The 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]})

对于此数据,我们可能希望使用 OneHotEncoder'city' 列编码为分类变量,但将 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 以及大多数其他转换器都期望二维数据,因此在这种情况下,您需要将列指定为字符串列表 (['city'])。

除了标量或单个项目列表之外,列选择还可以指定为多个项目的列表、整数数组、切片、布尔掩码或使用 make_column_selector。The 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]]...)

The 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. ]])

The 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  

可以通过在 set_config 中将 display 选项设置为 'text' 来停用它

>>> from sklearn import set_config
>>> set_config(display='text')  
>>> # displays text representation in a jupyter context
>>> column_trans  

可以在 混合类型列转换器管道 HTML 表示部分看到 HTML 输出的示例。作为替代方案,可以使用 estimator_html_repr 将 HTML 写入文件

>>> from sklearn.utils import estimator_html_repr
>>> with open('my_estimator.html', 'w') as f:  
...     f.write(estimator_html_repr(clf))

示例