本章介绍了Pipeline类,这是一种通用工具,可以将机器学习工作流程中的多个处理步骤链接在一起。现实世界中的机器学习应用很少仅涉及模型的单独使用,而是需要一系列处理步骤。 使用管道可以将多个步骤封装为单个Python对象,这个对象具有我们熟悉的scikit-learn接口fit、predict、和transform。特别是使用交叉验证进行模型评估与使用网格搜索进行参数选择时,使用Pipeline类来包括所有处理步骤对正确的评估至关重要。利用Pipeline类还可以让代码更加简洁,并减少不用Pipeline类构建处理链时可能会犯的错误(比如忘记将所有变换器应用于测试集,或者应用顺序错误)的可能性。选择特征提取、预处理和模型的正确组合,这在某种程度上是一种艺术,通常需要一些试错。但是有了管道,这种“尝试”多个不同的处理步骤是非常简单的。在进行试验时,要小心不要将处理过程复杂化,并且一定要评估一下模型的每个组件是否必要。
举一个例子来说明模型的重要性:
from sklearn.svm import SVC
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
# load and split the data
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, random_state=0)
# compute minimum and maximum on the training data
scaler = MinMaxScaler().fit(X_train)
# rescale the training data
X_train_scaled = scaler.transform(X_train)
svm = SVC()
# learn an SVM on the scaled training data
svm.fit(X_train_scaled, y_train)
# scale the test data and score the scaled data
X_test_scaled = scaler.transform(X_test)
print("Test score:{:.2f}".format(svm.score(X_test_scaled, y_test)))
Test score: 0.95
一、用预处理进行参数选择
一个不易察觉的问题的例子:
from sklearn.model_selection import GridSearchCV
# for illustration purposes only, don't use this code!
param_grid = {'C': [0.001, 0.01, 0.1, 1, 10, 100],
'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 parameters: ", grid.best_params_)
print("Test set accuracy:{:.2f}".format(grid.score(X_test_scaled, y_test)))
Best cross-validation accuracy: 0.98
Best parameters: {'C': 1, 'gamma': 1}
Test set accuracy: 0.97
这里我们利用缩放后的数据对SVC参数进行网格搜索。但是,上面的代码中有一个不易察觉的陷阱。在缩放数据时,我们使用了训练集中的所有数据来找到训练的方法。然后,我们使用缩放后的训练数据来运行带交叉验证的网格搜索。对于交叉验证中的每次划分,原始训练集的一部分被划分为训练部分,另一部分被划分为测试部分。测试部分用于度量在训练部分上所训练的模型在新数据上的表现。但是,我们在缩放数据时已经使用过测试部分中所包含的信息。请记住,交叉验证每次划分的测试部分都是训练集的一部分,我们使用整个训练集的信息来找到数据的正确缩放。
对于模型来说,这些数据与新数据看起来截然不同。如果我们观察新数据(比如测试集中的数据),那么这些数据并没有用于对训练数据进行缩放,其最大值和最小值也可能与训练数据不同。下面这个例子显示了交叉验证与最终评估这两个过程中数据处理的不同之处:
mglearn.plots.plot_improper_processing()
对于建模过程,交叉验证中的划分无法正确地反映新数据的特征。我们已经将这部分数据的信息泄露给建模过程。这将导致在交叉验证过程中得到过于乐观的结果,并可能会导致选择次优的参数。
为了解决这个问题,在交叉验证过程中,应该在进行任何预处理之前完成数据集的划分。任何从数据集中提取信息的处理过程都应该仅应用于数据集的训练部分,因此,任何交叉验证都应该位于处理过程的“最外层循环”。
在scikit-learn中,要想使用cross_val_score函数和GridSearchCV函数实现这一点,可以使用Pipeline类。Pipeline类可以将多个处理步骤合并(glue)为单个scikit-learn估计器。Pipeline类本身具有fit、predict和score方法,其行为与scikit-learn中的其他模型相同。Pipeline类最常见的用例是将预处理步骤(比如数据缩放)与一个监督模型(比如分类器)链接在一起。
二、构建管道
下面是使用Pipeline类来表示在使用MinMaxScaler缩放数据之后再训练一个SVM的工作流程(暂时不用网格搜索)。
from sklearn.pipeline import Pipeline
pipe = Pipeline([("scaler", MinMaxScaler()), ("svm", SVC())])
pipe.fit(X_train, y_train)
Pipeline(memory=None,
steps=[('scaler', MinMaxScaler(copy=True, feature_range=(0, 1))), ('svm', SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape='ovr', degree=3, gamma='auto', kernel='rbf',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False))])
print("Test score:{:.2f}".format(pipe.score(X_test, y_test)))
Test score: 0.95
三、在网格搜索中使用管道
在网格搜索中使用管道的工作原理与使用任何其他估计器都相同。我们定义一个需要搜索的参数网格,并利用管道和参数网格构建一个GridSearchCV。不过在指定参数网格时存在一处细微的变化。我们需要为每个参数指定它在管道中所属的步骤。我们要调节的两个参数C和gamma都是SVC的参数,属于第二个步骤。我们给这个步骤的名称是“svm”。为管道定义参数网格的语法是为每个参数指定步骤名称,后面加上__(双下划线),然后是参数名称。因此,要想搜索SVC的C参数,必须使用“svm_C”作为参数网格字典的键,对于gamma参数也是同理:
param_grid = {'svm__C': [0.001, 0.01, 0.1, 1, 10, 100],
'svm__gamma': [0.001, 0.01, 0.1, 1, 10, 100]}
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_))
Best cross-validation accuracy: 0.98
Test set score: 0.97
Best parameters: {'svm__C': 1, 'svm__gamma': 1}
与前面所做的网格搜索不同,现在对于交叉验证的每次划分来说,仅使用训练部分对MinMaxScaler进行拟合,测试部分的信息没有泄露到参数搜索中。
mglearn.plots.plot_proper_processing()
在交叉验证中,信息泄露的影响大小取决于预处理步骤的性质。使用测试部分来估计数据的范围,通常不会产生可怕的影响,但在特征提取和特征选择中使用测试部分,则会导致结果的显著差异。
四、通用的管道接口
def fit(self, X, y):
X_transformed = X
for name, estimator in self.steps[:-1]:
# iterate over all but the final step
# fit and transform the data
X_transformed = estimator.fit_transform(X_transformed, y)
# fit the last step
self.steps[-1][1].fit(X_transformed, y)
return self
def predict(self, X):
X_transformed = X
for step in self.steps[:-1]:
# iterate over all but the final step
# transform the data
X_transformed = step[1].transform(X_transformed)
# predict using the last step
return self.steps[-1][1].predict(X_transformed)
1.用make_pipeline方便地创建管道
make_pipeline的语法:
from sklearn.pipeline import make_pipeline
# standard syntax
pipe_long = Pipeline([("scaler", MinMaxScaler()), ("svm", SVC(C=100))])
# abbreviated syntax
pipe_short = make_pipeline(MinMaxScaler(), SVC(C=100))
print("Pipeline steps:\n{}".format(pipe_short.steps))
Pipeline steps:
[('minmaxscaler', MinMaxScaler(copy=True, feature_range=(0, 1))), ('svc', SVC(C=100, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape='ovr', degree=3, gamma='auto', kernel='rbf',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False))]
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(copy=True, with_mean=True, with_std=True)), ('pca', PCA(copy=True, iterated_power='auto', n_components=2, random_state=None,
svd_solver='auto', tol=0.0, whiten=False)), ('standardscaler-2', StandardScaler(copy=True, with_mean=True, with_std=True))]
2.访问步骤属性
如果想检查管道中某一步骤的属性,最简单的方法是通过named_steps属性,它是一个字典,将步骤名称映射为估计器:
# fit the pipeline defined before to the cancer dataset
pipe.fit(cancer.data)
# extract the first two principal components from the "pca" step
components = pipe.named_steps["pca"].components_
print("components.shape:{}".format(components.shape))
components.shape: (2, 30)
3.访问网格搜索管道中的属性
from sklearn.linear_model import LogisticRegression
pipe = 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, cv=5)
grid.fit(X_train, y_train)
GridSearchCV(cv=5, error_score='raise',
estimator=Pipeline(memory=None,
steps=[('standardscaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('logisticregression', LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
verbose=0, warm_start=False))]),
fit_params=None, iid=True, n_jobs=1,
param_grid={'logisticregression__C': [0.01, 0.1, 1, 10, 100]},
pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
scoring=None, verbose=0)
print("Best estimator:\n{}".format(grid.best_estimator_))
Best estimator:
Pipeline(memory=None,
steps=[('standardscaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('logisticregression', LogisticRegression(C=0.1, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
verbose=0, warm_start=False))])
print("Logistic regression step:\n{}".format(
grid.best_estimator_.named_steps["logisticregression"]))
Logistic regression step:
LogisticRegression(C=0.1, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
verbose=0, warm_start=False)
print("Logistic regression coefficients:\n{}".format(
grid.best_estimator_.named_steps["logisticregression"].coef_))
Logistic regression coefficients:
[[-0.389 -0.375 -0.376 -0.396 -0.115 0.017 -0.355 -0.39 -0.058 0.209
-0.495 -0.004 -0.371 -0.383 -0.045 0.198 0.004 -0.049 0.21 0.224
-0.547 -0.525 -0.499 -0.515 -0.393 -0.123 -0.388 -0.417 -0.325 -0.139]]
五、网格搜索预处理步骤与模型参数
from sklearn.datasets import load_boston
boston = load_boston()
X_train, X_test, y_train, y_test = train_test_split(boston.data, boston.target,
random_state=0)
from sklearn.preprocessing import PolynomialFeatures
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)
GridSearchCV(cv=5, error_score='raise',
estimator=Pipeline(memory=None,
steps=[('standardscaler', StandardScaler(copy=True, with_mean=True, with_std=True)), ('polynomialfeatures', PolynomialFeatures(degree=2, include_bias=True, interaction_only=False)), ('ridge', Ridge(alpha=1.0, copy_X=True, fit_intercept=True, max_iter=None,
normalize=False, random_state=None, solver='auto', tol=0.001))]),
fit_params=None, iid=True, n_jobs=-1,
param_grid={'polynomialfeatures__degree': [1, 2, 3], 'ridge__alpha': [0.001, 0.01, 0.1, 1, 10, 100]},
pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
scoring=None, verbose=0)
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("Best parameters:{}".format(grid.best_params_))
Best parameters: {'polynomialfeatures__degree': 2, 'ridge__alpha': 10}
print("Test-set score: {:.2f}".format(grid.score(X_test, y_test)))
Test-set score: 0.77
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
上面是进行网格搜索及使用多项式特征及没有多项式特征的网格搜索对比。
六、网格搜索选择使用哪个模型
下面的例子是在iris数据集上比较RandomForestClassifier和SVC。
pipe = Pipeline([('preprocessing', StandardScaler()), ('classifier', SVC())])
from sklearn.ensemble import RandomForestClassifier
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, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape='ovr', degree=3, gamma=0.01, kernel='rbf',
max_iter=-1, probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False), 'classifier__C': 10, 'classifier__gamma': 0.01, 'preprocessing': StandardScaler(copy=True, with_mean=True, with_std=True)}
Best cross-validation score: 0.99
Test-set score: 0.98