6.1. 流水线和复合估计器#
要构建一个复合估计器,通常将转换器与其他转换器或 预测器 (如分类器或回归器)组合在一起。用于组合估计器最常用的工具是 Pipeline 。流水线要求除最后一步之外的所有步骤都是 转换器 。最后一步可以是任何东西,一个转换器、一个 预测器 ,或者一个可能具有或不具有 .predict(...)
方法的聚类估计器。流水线暴露最后估计器提供的所有方法:如果最后一步提供了一个 transform
方法,那么流水线将具有一个 transform
方法并表现得像一个转换器。如果最后一步提供了一个 predict
方法,那么流水线将暴露该方法,并且给定数据 X ,使用除最后一步之外的所有步骤来转换数据,然后将转换后的数据传递给流水线最后一步的 predict
方法。类 Pipeline
通常与 ColumnTransformer 或 FeatureUnion 结合使用,这些类将转换器的输出连接成一个复合特征空间。TransformedTargetRegressor 处理转换 目标 (即对 y 进行对数变换)。
6.1.1. 流水线:链接估计器#
Pipeline
可用于将多个估计器链接成一个。这在处理数据时通常有一个固定的步骤序列,例如特征选择、归一化和分类。Pipeline
在这里有多个用途:
你可以一次性对管道中所有估计器的参数进行 网格搜索 。
- 安全性
管道通过确保使用相同的样本训练转换器和预测器,避免了在交叉验证中将测试数据的统计信息泄露到训练模型中。
管道中的所有估计器(最后一个除外)必须是转换器(即必须具有 transform 方法)。 最后一个估计器可以是任何类型(转换器、分类器等)。
Note
在管道上调用 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())])
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()
Pipeline
的 named_steps
属性允许在交互环境中通过名称访问步骤,并支持Tab补全:
>>> 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)
See also
示例
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())])
>>> # 当你不再需要缓存目录时,清除它
>>> 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())])
>>> # 可以直接检查 pca 实例
>>> 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)
>>> # 删除缓存目录
>>> 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] # 选择部分数据
>>> 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
Note
可以通过设置 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 之前预处理数据可能会因以下原因之一而出现问题:
将测试数据的统计信息纳入预处理器会使交叉验证分数不可靠(称为 数据泄露),例如在缩放器或填充缺失值的情况下。
您可能希望将预处理器的参数包含在 参数搜索 中。
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"],
对于这些数据,我们可能希望使用 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
期望输入为 1D 数组,因此列被指定为字符串( '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
可以通过在 set_config
中将 display
选项设置为 ‘text’ 来禁用它:
>>> from sklearn import set_config
>>> set_config(display='text')
>>> # 在 Jupyter 环境中显示文本表示
>>> column_trans
HTML 输出的示例可以在
带有混合类型的列转换器 的
Pipeline 的 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))
示例