ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

scikit-learn 复合估计器完全指南:Pipeline、FeatureUnion 与 ColumnTransformer 的合并与构造

scikit-learn 复合估计器完全指南:Pipeline、FeatureUnion 与 ColumnTransformer 的合并与构造
  • 文档
  • 机器学习
  • 教程

【免费下载链接】sklearn-doc-zh

:book: [译] scikit-learn(sklearn) 中文文档

项目地址:https://gitcode.com/gh_mirrors/sk/sklearn-doc-zh
点击查看免费下载

复合估计器(composite estimators)是 scikit-learn 中把多个"变换器 + 学习器"组装成完整工作流的核心机制。本文以中文文档第 5.1 节《Pipeline 和 FeatureUnion:合并的评估器》(docs/master/38.md)为骨架,系统讲解Pipeline(链式评估器)、TransformedTargetRegressor(目标变量变换)、FeatureUnion(复合特征空间)与ColumnTransformer(异构数据列变换)的构造方法、嵌套参数语义、网格搜索配合方式与缓存机制,并结合仓库内示例验证其实际用法。读完本文,你将能自由组合预处理、特征提取与模型训练步骤,写出可参数化、无数据泄露的完整学习流程。

本文对应章节位于仓库用户指南"数据集转换"部分(docs/master/37.md 的 5.1 节),并延伸关联 预测目标 (y) 的转换 与 调整估计器的超参数 两章内容。

为什么需要"合并的评估器"

变换器(Transformers)通常需要与分类器、回归器或其他学习器组合在一起,才能构成一个完整的复合估计器。scikit-learn 为此提供四类核心工具,分工各不相同:

工具所在模块核心作用
Pipelinesklearn.pipeline把多个评估器链接成链,前一阶段的输出作为后一阶段的输入
FeatureUnionsklearn.pipeline将多个变换器的输出并联,串联到复合特征空间中
TransformedTargetRegressorsklearn.compose在拟合回归模型前变换目标y,预测时再逆变换回原始空间
ColumnTransformersklearn.compose对不同数据列应用不同的变换,用于异构数据

需要特别区分的是:Pipeline只负责变换观测数据X;而TransformedTargetRegressor专门用来处理被变换的目标y(例如对y取对数)。FeatureUnion与Pipeline经常结合使用,可以构建出相当复杂的模型结构。

Pipeline:链式评估器

Pipeline可以把多个评估器链接成一个整体。由于处理数据的步骤通常是固定的——例如特征选择、标准化、分类——管道能显著提升工程效率,其价值主要体现在三点:

  • 便捷性和封装性:只需对数据调用一次fit和predict,即可适配整条评估器链。
  • 联合的参数选择:可以一次对管道中所有评估器的参数做grid search,而不是分别调参。
  • 安全性:训练转换器和预测器使用的是相同样本,管道有助于防止来自测试数据的统计信息泄露到交叉验证的训练模型中(即数据泄露问题)。

一个关键约束是:管道中除最后一个评估器外,其余所有评估器都必须是变换器(即必须有transform方法);最后一个评估器的类型不限(可以是变换器、分类器或回归器)。

构造管道:Pipeline与make_pipeline

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(memory=None, steps=[('reduce_dim', PCA(copy=True,...)), ('clf', SVC(C=1.0,...))], verbose=False)

简写函数make_pipeline是构建管道的快捷方式:它接收多个评估器并返回一个管道,自动填充评估器名(由类名小写化而来):

>>> from sklearn.pipeline import make_pipeline >>> from sklearn.naive_bayes import MultinomialNB >>> from sklearn.preprocessing import Binarizer >>> make_pipeline(Binarizer(), MultinomialNB()) Pipeline(memory=None, steps=[('binarizer', Binarizer(copy=True, threshold=0.0)), ('multinomialnb', MultinomialNB(alpha=1.0, class_prior=None, fit_prior=True))], verbose=False)

访问管道中的步骤

管道中的评估器作为一个列表保存在steps属性内,可以通过索引或名称访问:

>>> pipe.steps[0] ('reduce_dim', PCA(copy=True, iterated_power='auto', n_components=None, random_state=None, svd_solver='auto', tol=0.0, whiten=False)) >>> pipe[0] PCA(copy=True, iterated_power='auto', n_components=None, random_state=None, svd_solver='auto', tol=0.0, whiten=False) >>> pipe['reduce_dim'] PCA(copy=True, ...)

管道的named_steps属性允许在交互式环境中使用 tab 补全,以属性方式按名称访问步骤:

>>> pipe.named_steps.reduce_dim is pipe['reduce_dim'] True

还可以使用通常用于 Python 序列(如列表或字符串)的切片表示法提取子管道(注意只允许步长为 1 的切片)。这对于只执行部分转换(或它们的逆变换)很方便:

>>> pipe[:1] Pipeline(memory=None, steps=[('reduce_dim', PCA(copy=True, ...))],...) >>> pipe[-1:] Pipeline(memory=None, steps=[('clf', SVC(C=1.0, ...))],...)

嵌套参数:<estimator>__<parameter>语义

管道中评估器的参数可以通过步骤名__参数名的嵌套语义访问和设置:

>>> pipe.set_params(clf__C=10) Pipeline(memory=None, steps=[('reduce_dim', PCA(copy=True, iterated_power='auto',...)), ('clf', SVC(C=10, cache_size=200, class_weight=None,...))], verbose=False)

这一语义对网格搜索尤其重要——它让GridSearchCV能够一次性搜索整条管道上所有评估器的参数:

>>> 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'(原样透传,跳过该步)来参与搜索:

>>> from sklearn.linear_model import LogisticRegression >>> 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)

关于网格搜索的完整方法论(GridSearchCV与RandomizedSearchCV、参数空间设计、交叉验证方案与最佳实践),可参阅中文文档 3.2 调整估计器的超参数。

管道的行为语义

对管道调用fit方法的效果,与依次对每个评估器调用fit等价:每一步fit之后都把输入transform,再传递给下一步。管道会暴露最后一个评估器的所有方法——如果最后的评估器是分类器,Pipeline整体就可以当分类器用(predict、score等);如果最后一个评估器是变换器,管道也同样是变换器。

缓存转换器:避免重复计算

适配转换器往往是计算密集的。通过设置memory参数,Pipeline会在调用fit方法后缓存每个转换器。如果参数和输入数据相同,该特性可以避免重复计算已适配的转换器——典型场景是网格搜索:转换器只需适配一次,就可以被多个参数组合复用。

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(..., steps=[('reduce_dim', PCA(copy=True,...)), ('clf', SVC(C=1.0,...))], verbose=False) >>> # Clear the cache directory when you don't need it anymore >>> rmtree(cachedir)

警告:缓存转换器的副作用。不使用缓存时,可以通过查看原始实例直接检查管道内部的转换器状态。例如:

>>> from sklearn.datasets import load_digits >>> digits = load_digits() >>> pca1 = PCA() >>> svm1 = SVC(gamma='scale') >>> pipe = Pipeline([('reduce_dim', pca1), ('clf', svm1)]) >>> pipe.fit(digits.data, digits.target) Pipeline(memory=None, steps=[('reduce_dim', PCA(...)), ('clf', SVC(...))], verbose=False) >>> # The pca instance can be inspected directly >>> print(pca1.components_) [[-1.77484909e-19 ... 4.07058917e-18]]

而开启缓存会在适配前触发转换器的克隆,因此管道外的转换器实例无法被直接查看。下面的例子中,访问pca2会引发AttributeError,因为pca2是一个未适配的转换器;此时应使用named_steps来检查管道内部的评估器:

>>> cachedir = mkdtemp() >>> pca2 = PCA() >>> svm2 = SVC(gamma='scale') >>> cached_pipe = Pipeline([('reduce_dim', pca2), ('clf', svm2)], memory=cachedir) >>> cached_pipe.fit(digits.data, digits.target) ... Pipeline(memory=..., steps=[('reduce_dim', PCA(...)), ('clf', SVC(...))], verbose=False) >>> print(cached_pipe.named_steps['reduce_dim'].components_) ... [[-1.77484909e-19 ... 4.07058917e-18]] >>> # Remove the cache directory >>> rmtree(cachedir)

TransformedTargetRegressor:回归中的目标转换

TransformedTargetRegressor在拟合回归模型之前对目标y进行变换,预测时再通过逆变换把结果映射回原始空间。它接收两个核心组件:预测所用的回归器(regressor),以及应用于目标变量的变换器(transformer):

>>> import numpy as np >>> from sklearn.datasets import load_boston >>> from sklearn.compose import TransformedTargetRegressor >>> from sklearn.preprocessing import QuantileTransformer >>> from sklearn.linear_model import LinearRegression >>> from sklearn.model_selection import train_test_split >>> boston = load_boston() >>> X = boston.data >>> y = boston.target >>> 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.67 >>> 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.64

上例中,把偏态的波士顿房价目标y经QuantileTransformer(output_distribution='normal')变换为正态分布后,线性回归的 R2 分数从 0.64 提升到 0.67。

对于简单变换,也可以不传 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.65

默认情况下,所提供的函数在每次拟合时都会被检查是否为彼此的逆函数。可以通过将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: -4.50

注意:可以通过设置transformer,或通过函数对func和inverse_func来触发目标变换,但同时设置两者会产生错误。

若只希望变换监督学习的目标本身(而非借助回归器做逆变换),可进一步阅读 5.9 预测目标 (y) 的转换,那里介绍了LabelBinarizer、LabelEncoder、OneHotEncoder等直接作用于y的变换器。

FeatureUnion:复合特征空间

FeatureUnion合并多个变换器对象,形成一个输出为各变换器输出拼接的新变换器。在适配期间,每个变换器都独立地与数据适配;变换数据时,各变换器可以并发使用,输出的样本向量被连接成更大的向量。

FeatureUnion与Pipeline具有相同的价值:便捷性,以及联合参数的估计与验证。二者可以嵌套组合,创造出复杂模型。

注意:FeatureUnion无法检查两个变换器是否会产出相同的特征。它仅在特征集合互不相关时产出联合,确认特征不重复是调用者的职责。

使用 FeatureUnion

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(n_jobs=None, transformer_list=[('linear_pca', PCA(copy=True,...)), ('kernel_pca', KernelPCA(alpha=1.0,...))], transformer_weights=None, verbose=False)

与Pipeline类似,FeatureUnion也有一个精简版构造器make_union,无需显式给每个组件起名字。

同样地,单独的步骤可以用set_params替换,并可以设置为'drop'来跳过该步骤:

>>> combined.set_params(kernel_pca='drop') ... FeatureUnion(n_jobs=None, transformer_list=[('linear_pca', PCA(copy=True,...)), ('kernel_pca', 'drop')], transformer_weights=None, verbose=False)

ColumnTransformer:用于异构数据的列变换器

警告:sklearn.compose.ColumnTransformer在对应版本的 scikit-learn 中仍处于实验阶段,其 API 可能发生变动。

许多数据集包含不同类型的特征——文本、浮点数、日期等,每种类型都需要单独的预处理或特征提取步骤。通常,最简单的方式是在应用 scikit-learn 之前,先用 pandas 等工具预处理数据。但在传入 scikit-learn 之前处理数据会带来两个问题:

  • 数据泄露:把测试数据的统计信息(如缩放、缺失值填充所依赖的统计量)集成进预处理程序,会使得交叉验证分数不可靠;
  • 无法参数化:你可能希望在参数搜索(parameter search)中包含预处理器参数。

ColumnTransformer对数据的不同列执行不同的变换,从而既不存在数据泄漏,又可以参数化。它可以处理数组、稀疏矩阵和 pandasDataFrame。

基本用法

对每一列应用一个不同的变换(如预处理或某个特定的特征抽取方法)。以下面的 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]})

我们希望用preprocessing.OneHotEncoder将city列编码为分类变量,同时用feature_extraction.text.CountVectorizer处理title列。由于可能把多个特征抽取器用在同一列上,需要给每个变换器取唯一名字(如city_category、title_bow)。默认情况下,其余列被忽略(remainder='drop'):

>>> from sklearn.compose import ColumnTransformer >>> from sklearn.feature_extraction.text import CountVectorizer >>> column_trans = ColumnTransformer( ... [('city_category', CountVectorizer(analyzer=lambda x: [x]), 'city'), ... ('title_bow', CountVectorizer(), 'title')], ... remainder='drop') >>> column_trans.fit(X) ColumnTransformer(n_jobs=None, remainder='drop', sparse_threshold=0.3, transformer_weights=None, transformers=...) >>> column_trans.get_feature_names() ... ['city_category__London', 'city_category__Paris', 'city_category__Sallisaw', 'title_bow__bow', 'title_bow__feast', 'title_bow__grapes', 'title_bow__his', 'title_bow__how', 'title_bow__last', 'title_bow__learned', 'title_bow__moveable', 'title_bow__of', 'title_bow__the', 'title_bow__trick', 'title_bow__watson', 'title_bow__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'])。

列选择方式

除了标量或单个项列表外,列选择还可以指定为多个项的列表、整数数组、切片或布尔掩码。如果输入是 DataFrame,字符串可以引用列名;整数则总是被解释为位置列。

remainder='passthrough':保留其余列

通过设置remainder='passthrough',可以保留其余的 rating 列,这些值会被追加到变换结果的末尾:

>>> 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=estimator:变换其余列

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:简写构造

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(n_jobs=None, remainder=MinMaxScaler(copy=True, ...), sparse_threshold=0.3, transformer_weights=None, transformers=[('onehotencoder', ...)

ColumnTransformer与Pipeline天然配合:把它作为管道的第一步或中间步骤,即可对异构数据完成"分列预处理 → 特征拼接 → 模型训练"的完整流程,且全程支持网格搜索参数化。

仓库中的实战印证

本仓库 docs/examples 下保留了多个与本文主题直接对应的示例,可作为实操参考:

  • 文本特征提取与分类的完整管道:docs/examples/Generalized_Linear_Models/plot_document_classification_20newsgroups.md 对 20 个新闻组数据集先使用HashingVectorizer(alternate_sign=False, n_features=2**16)或TfidfVectorizer(sublinear_tf=True, max_df=0.5, stop_words='english')做文本特征提取,再构建Pipeline([('feature_selection', SelectFromModel(LinearSVC(penalty="l1", dual=False, tol=1e-3))), ('classification', LinearSVC(penalty="l2"))]),演示了"特征选择 + 分类"两阶段管道与稀疏矩阵处理,训练与测试共用一个矢量化器以避免数据泄露,正是本文所述管道安全性的真实用例。
  • 管道串联 PCA 与逻辑回归(对应官方示例Pipelining: chaining a PCA and a logistic regression):docs/examples/img/sphx_glr_plot_digits_pipe_thumb.png缩略图展示了管道在 digits 数据集上的效果。
  • 异构数据列变换(对应官方示例Column Transformer with Mixed Types):docs/examples/img/sphx_glr_plot_column_transformer_mixed_types_thumb.png缩略图对应混合类型数据的列变换可视化。

此外,docs/master/SUMMARY.md 显示本章(5.1)属于用户指南"5. 数据集转换"部分,与其并列的还有特征提取(5.2)、预处理(5.3)、缺失值插补(5.4)与无监督降维(5.5)等章节,这些章节中的变换器都可以作为管道与特征联合的组件。

小结

复合估计器是 scikit-learn 工程化使用的核心抽象:

  • Pipeline解决"步骤固定、参数联合搜索、防数据泄露"的链式组装问题,支持steps访问、named_steps属性、切片提取与<step>__<param>嵌套参数;
  • TransformedTargetRegressor解决目标变量y的变换与逆变换问题,支持transformer对象与func/inverse_func函数对两种方式,并提供check_inverse校验;
  • FeatureUnion把多个变换器的输出并联拼接成复合特征空间,步骤可set_params替换或置为'drop';
  • ColumnTransformer为异构数据的不同列分别施加变换,remainder支持'drop'、'passthrough'与估计器三种策略,配合make_pipeline/make_union/make_column_transformer简写函数可显著降低样板代码。

需要留意的是,原文档示例基于其对应的 scikit-learn 版本编写(如使用load_boston数据与get_feature_names等接口),实际运行时请以所安装版本的行为为准。

  • 文档
  • 机器学习
  • 教程

【免费下载链接】sklearn-doc-zh

:book: [译] scikit-learn(sklearn) 中文文档

项目地址:https://gitcode.com/gh_mirrors/sk/sklearn-doc-zh
点击查看免费下载

相关推荐

上一篇:GoPro视频GPS数据提取:从隐藏轨迹到可视化分析的完整指南
下一篇:AI应用开发技术指南:从API调用到智能体集成的全栈实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进