6.1. 管道和复合估计器#

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

6.1.1. 管道:链接估计器#

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

便捷性和封装

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

联合参数选择

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

安全性

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

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

注意

在管道上调用fit与依次调用每个估计器的fit方法,转换输入并将输入传递到下一步相同。管道拥有管道中最后一个估计器所拥有的所有方法,即如果最后一个估计器是分类器,则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())])
使用 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. 您可能希望在参数搜索中包含预处理器的参数。

ColumnTransformer有助于在Pipeline中对数据的不同列执行不同的转换,这样可以避免数据泄露,并且可以进行参数化。ColumnTransformer适用于数组、稀疏矩阵和pandas DataFrame

可以对每一列应用不同的转换,例如预处理或特定的特征提取方法。

>>> 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'列编码为分类变量,但对'title'列应用CountVectorizer。由于我们可能对同一列使用多种特征提取方法,因此我们为每个转换器赋予一个唯一的名称,例如'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_selectormake_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')])

如果使用 DataFrame 拟合ColumnTransformer,并且 DataFrame 只有字符串列名,则转换 DataFrame 将使用列名来选择列。

>>> 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 Notebook 中显示时,估计器将以 HTML 表示形式显示。这对于诊断或可视化包含许多估计器的 Pipeline 非常有用。此可视化默认启用。

>>> column_trans  

可以通过在set_config中将display选项设置为“text”来禁用它。

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

可以在混合类型列转换器Pipeline 的 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))

示例