13_算法链与管道
构建管道
可以通过使用 MinMaxScaler 进行预处理来大大提高核 SVM 在 cancer 数据集上的性能。下面这些代码实现了划分数据、计算最小值和最大值、缩放数据与训练 SVM:
from sklearn.svm import SVC
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScalercancer = load_breast_cancer()
X_train,X_test,Y_train,Y_test = train_test_split(cancer.data,cancer.target,random_state=0)scaler = MinMaxScaler().fit(X_train)
X_train_scaled = scaler.transform(X_train)
svm = SVC()
svm.fit(X_train_scaled,Y_train)
X_test_scaled = scaler.transform(X_test)
print('Test score:{:f}'.format(svm.score(X_test_scaled,Y_test)))
使用管道可以简化上例的流程,用 Pipeline 类来表示在使用 MinMaxScaler 缩放数据之后再训练一个SVM 的工作流程。
from sklearn.pipeline import Pipelinepipe = Pipeline([('scaler',MinMaxScaler()),('svm',SVC())]) # 第一个步骤叫作 "scaler",是 MinMaxScaler 的实例;第二个步骤叫作"svm",是 SVC 的实例。
pipe.fit(X_train,Y_train)
print('test score :{:f}'.format(pipe.score(X_test,Y_test)))
利用管道,减少了“预处理 + 分类”过程所需要的代码量。
在网格搜索中使用管道
网格搜索中使用管道的工作原理与使用任何其他估计器都相同。定义一个需要搜索的参数网格,并利用管道和参数网格构建一个 GridSearchCV。不过在指定参数网格时存在一处细微的变化:需要为每个参数指定它在管道中所属的步骤。要调节的两个参数 C 和 gamma 都是 SVC 的参数,属于第二个步骤。给这个步骤的名称是 “svc”。为管道定义参数网格的语法是为每个参数指定步骤名称,后面加上 __(双下划线),然后是参数名称。
from sklearn.model_selection import GridSearchCV
param_grid = {'svc__C':[0.001, 0.01, 0.1, 1, 10, 100],'svc__gamma':[0.001, 0.01, 0.1, 1, 10, 100]}
pipe = Pipeline([('scaler',MinMaxScaler()),('svc',SVC())])
grid = GridSearchCV(pipe,param_grid=param_grid,cv=5)
grid.fit(X_train,Y_train)print("Best cross-validation accuracy: {:.2f}".format(grid.best_score_))
print("Test set score: {:.2f}".format(grid.score(X_test, Y_test)))
print("Best parameters: {}".format(grid.best_params_))
与前面所做的网格搜索不同,现在对于交叉验证的每次划分来说,仅使用训练部分对MinMaxScaler 进行拟合,测试部分的信息没有泄露到参数搜索中。
GridSearchCV 遇到数据缩放时的陷阱
param_grid = {'svc__C':[0.001, 0.01, 0.1, 1, 10, 100],'svc__gamma':[0.001, 0.01, 0.1, 1, 10, 100]}
grid = GridSearchCV(SVC(),param_grid=param_grid,cv=5)
grid.fit(X_train_scaled,Y_train)print("Best cross-validation accuracy: {:.2f}".format(grid.best_score_))
print("Best set score: {:.2f}".format(grid.score(X_test_scaled, y_test)))
print("Best parameters: ", grid.best_params_)
上例有个不易觉察的陷阱:利用缩放后的数据对 SVC 参数进行网格搜索时,使用了训练集中的所有数据来找到训练的方法。然后使用缩放后的训练数据来运行带交叉验证的网格搜索。对于交叉验证中的每次划分,原始训练集的一部分被划分为训练部分,另一部分被划分为测试部分。测试部分用于度量在训练部分上所训练的模型在新数据上的表现。但是,在缩放数据时已经使用过测试部分中所包含的信息。请记住,交叉验证每次划分的测试部分都是训练集的一部分,使用整个训练集的信息来找到数据的正确缩放。
对于模型来说,这些数据与新数据看起来截然不同。如果观察新数据(比如测试集中的数据),那么这些数据并没有用于对训练数据进行缩放,其最大值和最小值也可能与训练数据不同。
(直白一点:带交叉验证的网格搜索,如果数据需要缩放,那么必须保证在每次划分数据后进行缩放。但上例明显犯了一个错误,fit函数传入的时缩放后的数据,这个缩放是整体数据集上的缩放)
通用的管道接口
Pipeline 类不但可用于预处理和分类,实际上还可以将任意数量的估计器连接在一起。
对于管道中估计器的唯一要求就是,除了最后一步之外的所有步骤都需要具有 transform方法,这样它们可以生成新的数据表示,以供下一个步骤使用。在调用 Pipeline.fit 的过程中,管道内部依次对每个步骤调用 fit 和 transform,其输入是前一个步骤中 transform 方法的输出。对于管道中的最后一步,则仅调用 fit。
pipeline.steps 是由元组组成的列表,所以 pipeline.steps[0][1]是第一个估计器,pipeline.steps[1][1] 是第二个估计器,以此类推。
用make_pipeline创建管道
利用前面语法创建管道有时有点麻烦,通常不需要为每一个步骤提供用户指定的名称。有一个很方便的函数 make_pipeline,可以创建管道并根据每个步骤所属的类为其自动命名。
from sklearn.pipeline import make_pipelinepipe_short = make_pipeline(MinMaxScaler(),SVC(C=100))
print("Pipeline steps:\n{}".format(pipe_short.steps))
这两个步骤被命名为 minmaxscaler 和 svc。一般来说,步骤名称只是类名称的小写版本。如果多个步骤属于同一个类,则会附加一个数字。
访问步骤属性
要想访问管道中的步骤,最简单的方法是通过 named_steps 属性,它是一个字典,将步骤名称映射为估计器:
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCApipe = make_pipeline(StandardScaler(),PCA(n_components=2),StandardScaler())
pipe.fit(cancer.data)
# 从pca中提取前两个主要成分
print(pipe.named_steps['pca'].components_)
访问网格搜索管道中的属性
一个常见的任务是在网格搜索内访问管道的某些步骤。对 cancer 数据集上的 LogisticRegression 分类器进行网格搜索,在将数据传入 LogisticRegression 分类器之前,先用 Pipeline 和 StandardScaler对数据进行缩放。
from sklearn.linear_model import LogisticRegressionpipe = make_pipeline(StandardScaler(),LogisticRegression())
param_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=4)
grid = GridSearchCV(pipe,param_grid=param_grid,cv=5)
grid.fit(X_train,Y_train)print('Best estimator:\n{}'.format(grid.best_estimator_))
print("Logistic regression step:\n{}".format(grid.best_estimator_.named_steps["logisticregression"])) # 可以看到最佳参模型
print("Logistic regression step:\n{}".format(grid.best_estimator_.named_steps["logisticregression"].coef_)) # 可以查看LogisticRegression实例属性
上例中,best_estimator_ 是一个管道,可以使用管道的named_steps属性来访问logisticregression步骤。
网格搜索预处理步骤与模型参数
可以利用管道将机器学习工作流程中的所有处理步骤封装成一个 scikit-learn 估计器。这么做的另一个好处在于,可以使用监督任务(比如回归或分类)的输出来调节预处理参数。
在应用岭回归之前使用了 boston 数据集的多项式特征,希望根据分类结果来选择 degree 参数。可以利用管道搜索 degree 参数以及 Ridge 的 alpha参数。
from mglearn.datasets import load_boston
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import Ridgeboston = 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())param_grid = {'polynomialfeatures__degree':[1,2,3],'ridge__alpha':[0.001, 0.01, 0.1, 1, 10, 100]
}grid = GridSearchCV(pipe,param_grid=param_grid,cv=5,n_jobs=-1)
grid.fit(X_train,Y_train)
PolynomialFeatures:生成一个新的特征矩阵,该矩阵包含所有特征的多项式组合,其阶数小于或等于指定的阶数。degree参数是控制多项式的次数;
为了对比,运行一个没有多项式特征的网格搜索:
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)))
不使用多项式特征得到了明显更差,同时搜索预处理参数与模型参数是一个非常强大的策略。GridSearchCV 会尝试指定参数的所有可能组合,因此,向网格中添加更多参数,需要构建的模型数量将呈指数增长。
网格搜索选择使用哪个模型
可以进一步将 GridSearchCV 和 Pipeline 结合起来,用来判断那个模型更好。
可以定义需要搜索的 parameter_grid。希望 classifier 是 RandomForestClassifier或 SVC。由于这两种分类器需要调节不同的参数,并且需要不同的预处理,可以参考“在非网格的空间中搜索”中的搜索网格列表。为了将一个估计器分配给一个步骤,使用步骤名称作为参数名称。如果想跳过管道中的某个步骤(例如,RandomForest 不需要预处理),则可以将该步骤设置为 None:
from sklearn.ensemble import RandomForestClassifier# 必须先示例化Pipeline,指定第一个步骤(param_grid的一个参数)
pipe = Pipeline([('preprocessing', StandardScaler()), ('classifier', SVC())])param_grid = [ {'classifier':[SVC()],'preprocessing':[StandardScaler()],'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)],'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,n_jobs=-1)
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)))
网格搜索的结果是 SVC 与 StandardScaler 预处理,在 C=10 和 gamma=0.01 时给出最佳结果。