算法链与管道(下):通用的管道接口
1、通用的管道接口
Pipeline 类不但可用于预处理和分类,实际上还可以将任意数量的估计器连接在一起。
- 例如,你可以构建一个包含特征提取、特征选择、缩放和分类的管道,总共有 4 个步骤。同样,最后一步可以用回归或聚类代替分类。
对于管道中估计器的唯一要求就是,除了最后一步之外的所有步骤都需要具有 transform 方法,这样它们可以生成新的数据表示,以供下一个步骤使用。
在调用 Pipeline.fit 的过程中,管道内部依次对每个步骤调用 fit 和 transform,其输入是前一个步骤中 transform 方法的输出。对于管道中的最后一步,则仅调用 fit。
#pipeline.steps 是由元组组成的列表, 所以 pipeline.steps[0][1] 是第一个估计器,pipeline.steps[1][1] 是第二个估计器。
from sklearn.pipeline import Pipeline
def fit(self,X,y):
X_transformed = X
for name, estimator in self.steps[:-1]:
#遍历出最后一步之外的所有步骤
#对数据进行拟合和变换
X_transformed = estimator.fit_transform(X_transformed,y)
#对最后一步进行拟合
self.steps[-1][1].fit(X_transformed,y)
return self
#使用 Pipeline 进行预测时,我们同样利用除最后一步之外的所有步骤对数据进行变换 (transform),然后对最后一步调用 predict:
def predict(self,X):
X_transformed = X
for step in self.steps[:-1]:
X_transformed = step[1].transform(X_transformed)
return self.steps[-1][1].predict(X_transformed)
2、用make_pipeline方便地创建管道
利用上述语法创建管道有时有点麻烦,我们通常不需要为每一个步骤提供用户指定的名称。有一个很方便的函数 make_pipeline,可以为我们创建管道并根据每个步骤所属的类为其自动命名。make_pipeline 的语法如下所示:
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import MinMaxScaler
from sklearn.svm import SVC
#标准写法
pipe1 = Pipeline([("scaler",MinMaxScaler()),("svm",SVC(C=100))])
pipe2 = make_pipeline(MinMaxScaler(),SVC(C=100))
#管道对象 pipe1 和 pipe2 的作用完全相同,但 pipe2 的步骤是自动命名的。 我们可以通过查看 steps 属性来查看步骤的名称:
print("pipeline1 steps:{}".format(pipe1.steps))
print("pipeline2 steps:{}".format(pipe2.steps))
```
pipeline1 steps:[('scaler', MinMaxScaler()), ('svm', SVC(C=100))]
pipeline2 steps:[('minmaxscaler', MinMaxScaler()), ('svc', SVC(C=100))]
```
📣
这两个步骤被命名为 minmaxscaler 和 svc。
-
一般来说,步骤名称只是类名称的小写版本。
-
如果多个步骤属于同一个类,则会附加一个数字。
from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA pipe = make_pipeline(StandardScaler(), PCA(n_components=2), StandardScaler()) print("Pipeline steps:\n{}".format(pipe.steps)) ``` Pipeline steps: [('standardscaler-1', StandardScaler()), ('pca', PCA(n_components=2)), ('standardscaler-2', StandardScaler())] ```
如你所见,第一个 StandardScaler 步骤被命名为 standardscaler-1,而第二个被命名为 standardscaler-2。但在这种情况下,使用具有明确名称的 Pipeline 构建可能更好,以便 为每个步骤提供更具语义的名称。
3、访问步骤属性
检查管道中某一步骤的属性——比如线性模型的系数或 PCA 提取的成分。
-
要想访问管道中的步骤,最简单的方法是通过 named_steps 属性,它是一个字典,将步骤名称映射为估计器:
from sklearn.datasets import load_breast_cancer cancer = load_breast_cancer() #用前面定义的管道对cancer数据集进行拟合 pipe.fit(cancer.data) #从“pca”步骤中提取前两个主成分 components = pipe.named_steps["pca"].components_ print("conmponents.shape:{}".format(components.shape)) ''' `conmponents.shape:(2, 30)` '''
4、访问网格搜索管道中的属性
本章前面说过,使用管道的主要原因之一就是进行网格搜索。
- 一个常见的任务是在网格搜索内访问管道的某些步骤。
- 我们对 cancer 数据集上的 LogisticRegression 分类器进行网格搜索,在将数据传入 LogisticRegression 分类器之前,先用 Pipeline 和 StandardScaler 对数据进行缩放。
#首先,我们用 make_pipeline 函数创建一个管道:
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
#管道
pipe = make_pipeline(StandardScaler(),LogisticRegression())
#参数网格
pagram_grid = {'logisticregression__C':[0.01,0.1,1,10,100]}
#划分数据集
X_train,X_test,y_train,y_test = train_test_split(cancer.data,cancer.target,random_state=0)
#网格搜索交叉验证实例化
grid = GridSearchCV(pipe,param_grid=pagram_grid,cv=5)
grid.fit(X_train,y_train)
#GridSearchCV 找到的最佳模型(在所有训练数据上训练得到的模型)保存在 grid.best_estimator_ 中
print("Best estimator:{}".format(grid.best_estimator_))
'''
```
Best estimator:Pipeline(steps=[('standardscaler', StandardScaler()),
('logisticregression', LogisticRegression(C=1))])
```
'''
📣
在我们的例子中,best_estimator_ 是一个管道, 它包含两个步骤:standardscaler 和 logisticregression。
如前所述, 我们可以使用管道的 named_steps 属性来访问 logisticregression 步骤
print("Logistic regression coefficients:\n{}".format(
grid.best_estimator_.named_steps["logisticregression"].coef_))
'''
```
Logistic regression coefficients:
[[-0.29792942 -0.58056355 -0.3109406 -0.377129 -0.11984232 0.42855478
-0.71131106 -0.85371164 -0.46688191 0.11762548 -1.38262136 0.0899184
-0.94778563 -0.94686238 0.18575731 0.99305313 0.11090349 -0.3458275
0.20290919 0.80470317 -0.91626377 -0.91726667 -0.8159834 -0.86539197
-0.45539191 0.10347391 -0.83009341 -0.98445173 -0.5920036 -0.61086989]]
```
'''
5、网格搜索预处理步骤与模型参数
我们可以利用管道将机器学习工作流程中的所有处理步骤封装成一个 scikit-learn 估计器。
这么做的另一个好处在于,现在我们可以使用监督任务(比如回归或分类)的输出来调节预处理参数。
在前几章里,我们在应用岭回归之前使用了 boston 数据集的多项式特征。
下面我们用一个管道来重复这个建模过程。
管道包含 3 个步骤:缩放数据、计算多项式特征与岭回归:
from sklearn.datasets import load_boston
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import Ridge
import mglearn
from matplotlib import pyplot as plt
#加载数据,划分数据集
boston = load_boston()
X_train, X_test, y_train, y_test = train_test_split(boston.data, boston.target, random_state=0)
#建立管道实例
pipe = make_pipeline(StandardScaler(),PolynomialFeatures(),Ridge())
#网格参数
pagram_grid = {'polynomialfeatures__degree': [1, 2, 3],
'ridge__alpha': [0.001, 0.01, 0.1, 1, 10, 100]}
#使用网格搜索
grid = GridSearchCV(pipe,param_grid=pagram_grid,cv=5,n_jobs=-1)
grid.fit(X_train,y_train)
#可视化
mglearn.tools.heatmap(grid.cv_results_['mean_test_score'].reshape(3, -1),
xlabel="ridge__alpha", ylabel="polynomialfeatures__degree",
xticklabels=param_grid['ridge__alpha'],
yticklabels=param_grid['polynomialfeatures__degree'], vmin=0)
print("Test-set socre:{:.2f}".format(grid.score(X_test,y_test)))
print("Best parameters: {}".format(grid.best_params_))
'''
`Test-set socre:0.77`
`Best parameters: {'polynomialfeatures__degree': 2, 'ridge__alpha': 10}`
'''
#运行一个没有多项式特征的网格搜索
param_grid = {'ridge__alpha': [0.001, 0.01, 0.1, 1, 10, 100]}
pipe = make_pipeline(StandardScaler(), Ridge())
grid = GridSearchCV(pipe, param_grid, cv=5)
grid.fit(X_train, y_train)
print("Score without poly features: {:.2f}".format(grid.score(X_test, y_test)))
'''
`Score without poly features: 0.63`
'''
正与我们观察上图中的网格搜索结果所预料的那样,不使用多项式特征得到了明显更差的结果。
同时搜索预处理参数与模型参数是一个非常强大的策略。但是要记住,GridSearchCV 会尝试指定参数的所有可能组合。因此,向网格中添加更多参数,需要构建的模型数量将呈指数增长。
6、网格搜索选择使用哪个模型
将 GridSearchCV 和 Pipeline 结合起来:还可以搜索管道中正在执行的实际步骤(比如用 StandardScaler 还是用 MinMaxScaler)。
这样会导致更大的搜索空间, 应该予以仔细考虑。
尝试所有可能的解决方案,通常并不是一种可行的机器学习策略。
但下面是一个例子:在 iris 数据集上比较 RandomForestClassifier 和 SVC。
- SVC 可能需要对数据进行缩放,所以我们还需要搜索是使用 StandardScaler 还是不使用预处理。
- RandomForestClassifier 不需要预处理。
我们先定义管道。
- 这里我们显式地对步骤命名。
- 我们需要两个步骤,一个用于预处理,然后是一个分类器。
- 我们可以用 SVC 和 StandardScaler 来将其实例化:
from sklearn.ensemble import RandomForestClassifier
#创建管道,包含两个步骤:1、预处理,2、分类器
pipe = Pipeline([('preprocessing', StandardScaler()), ('classifier', SVC())])
#网格参数我们希望 classifier 是 RandomForestClassifier 或 SVC。
#由于这两种分类器需要调节不同的参数,并且需要不同的预处理,所以我们可以使用 “在非网格的空间中搜索” 中所讲的搜索网格列表。
#为了将一个估计器分配 给一个步骤,我们使用步骤名称作为参数名称。
#如果我们想跳过管道中的某个步骤(例如,RandomForest 不需要预处理),则可以将该步骤设置为 None:
param_grid = [
{'classifier': [SVC()], 'preprocessing': [StandardScaler(), None],
'classifier__gamma': [0.001, 0.01, 0.1, 1, 10, 100],
'classifier__C': [0.001, 0.01, 0.1, 1, 10, 100]},
{'classifier': [RandomForestClassifier(n_estimators=100)],
'preprocessing': [None], 'classifier__max_features': [1, 2, 3]}]
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, random_state=0)
grid = GridSearchCV(pipe, param_grid, cv=5)
grid.fit(X_train, y_train)
print("Best params:\n{}\n".format(grid.best_params_))
print("Best cross-validation score: {:.2f}".format(grid.best_score_))
print("Test-set score: {:.2f}".format(grid.score(X_test, y_test)))
'''
```
Best params:
{'classifier': SVC(C=10, gamma=0.01), 'classifier__C': 10, 'classifier__gamma': 0.01, 'preprocessing': StandardScaler()}
Best cross-validation score: 0.99
Test-set score: 0.98
```
'''
本章介绍了 Pipeline 类,这是一种通用工具,可以将机器学习工作流程中的多个处理步骤链接在一起。现实世界中的机器学习应用很少仅涉及模型的单独使用,而是需要一系列处理步骤。使用管道可以将多个步骤封装为单个 Python 对象,这个对象具有我们熟悉的 scikit-learn 接口 fit、predict 和 transform。特别是使用交叉验证进行模型评估与使用网格搜索进行参数选择时,使用 Pipeline 类来包括所有处理步骤对正确的评估至关重要。 利用 Pipeline 类还可以让代码更加简洁,并减少不用 pipeline 类构建处理链时可能会犯的错误(比如忘记将所有变换器应用于测试集,或者应用顺序错误)的可能性。选择特征提取、预处理和模型的正确组合,这在某种程度上是一门艺术,通常需要一些试错。但是有了管道,这种 “尝试” 多个不同的处理步骤是非常简单的。在进行试验时,要小心不要将处理过程复杂化,并且一定要评估一下模型中的每个组件是否必要。
7、参考文献
《python机器学习基础教程》