11. 常见陷阱和建议做法#
本章的目的是说明使用scikit-learn时出现的一些常见陷阱和反模式。它提供了一些例子 not 要做的事情,以及相应的正确例子。
11.1. 预处理不一致#
scikit-learn提供了一个 数据集转换 ,可以清洁(请参阅 预处理数据 )、减少(请参阅 无监督降维 ),扩展(请参阅 核近似 )或生成(请参阅 特征提取 )特征表示。如果在训练模型时使用这些数据转换,则它们还必须用于后续数据集,无论是测试数据还是生产系统中的数据。否则,特征空间就会发生变化,模型将无法有效执行。
对于以下示例,让我们创建具有单个特征的合成数据集::
>>> from sklearn.datasets import make_regression
>>> from sklearn.model_selection import train_test_split
>>> random_state = 42
>>> X, y = make_regression(random_state=random_state, n_features=1, noise=1)
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, test_size=0.4, random_state=random_state)
Wrong
训练数据集已缩放,但测试数据集未缩放,因此测试数据集上的模型性能比预期差::
>>> from sklearn.metrics import mean_squared_error
>>> from sklearn.linear_model import LinearRegression
>>> from sklearn.preprocessing import StandardScaler
>>> scaler = StandardScaler()
>>> X_train_transformed = scaler.fit_transform(X_train)
>>> model = LinearRegression().fit(X_train_transformed, y_train)
>>> mean_squared_error(y_test, model.predict(X_test))
62.80...
Right
而不是通过未变形的 X_test
到 predict
,我们应该像转换训练数据一样转换测试数据::
>>> X_test_transformed = scaler.transform(X_test)
>>> mean_squared_error(y_test, model.predict(X_test_transformed))
0.90...
或者,我们建议使用 Pipeline
,这使得将变换与估计器链接起来变得更容易,并减少忘记变换的可能性::
>>> from sklearn.pipeline import make_pipeline
>>> model = make_pipeline(StandardScaler(), LinearRegression())
>>> model.fit(X_train, y_train)
Pipeline(steps=[('standardscaler', StandardScaler()),
('linearregression', LinearRegression())])
>>> mean_squared_error(y_test, model.predict(X_test))
0.90...
管道还有助于避免另一个常见陷阱:将测试数据泄露到训练数据中。
11.2. 数据泄露#
当构建模型时使用预测时不可用的信息时,就会发生数据泄露。这会导致过度乐观的性能估计,例如来自 cross-validation ,因此当模型用于实际新颖的数据时(例如在生产期间),性能较差。
A common cause is not keeping the test and train data subsets separate.
Test data should never be used to make choices about the model.
The general rule is to never call fit
on the test data. While this
may sound obvious, this is easy to miss in some cases, for example when
applying certain pre-processing steps.
尽管训练和测试数据子集都应该接受相同的预处理转换(如前一节所述),但重要的是,这些转换只能从训练数据中学习。例如,如果您有一个标准化步骤,其中您将除以平均值,则平均值应该是火车子集的平均值, not 所有数据的平均值。如果测试子集包含在平均计算中,则来自测试子集的信息将影响模型。
11.2.1. 如何避免数据泄露#
以下是一些避免数据泄露的提示:
始终首先将数据拆分为训练和测试子集,尤其是在任何预处理步骤之前。
Never include test data when using the
fit
andfit_transform
methods. Using all the data, e.g.,fit(X)
, can result in overly optimistic scores.相反,
transform
该方法应用于训练和测试子集,因为应对所有数据应用相同的预处理。这可以通过使用来实现fit_transform
在火车子集上和transform
在测试子集上。scikit-learn pipeline 是防止数据泄露的好方法,因为它可以确保对正确的数据子集执行适当的方法。该管道非常适合用于交叉验证和超参数调优功能。
下面详细介绍了预处理期间数据泄露的示例。
11.2.2. 预处理期间数据泄露#
备注
我们在这里选择用特征选择步骤来说明数据泄漏。然而,这种泄漏风险与scikit-learn中几乎所有的转换都相关,包括(但不限于) StandardScaler
, SimpleImputer
,而且 PCA
.
一些 特征选择 scikit-learn中提供了功能。它们可以帮助删除不相关、多余和有噪音的功能,并改善您的模型构建时间和性能。与任何其他类型的预处理一样,特征选择应该 only 使用训练数据。将测试数据包括在特征选择中将乐观地偏向您的模型。
为了演示,我们将用10,000个随机生成的特征创建这个二进制分类问题::
>>> import numpy as np
>>> n_samples, n_features, n_classes = 200, 10000, 2
>>> rng = np.random.RandomState(42)
>>> X = rng.standard_normal((n_samples, n_features))
>>> y = rng.choice(n_classes, n_samples)
Wrong
使用所有数据来执行特征选择会导致准确性分数远高于概率,尽管我们的目标是完全随机的。这种随机性意味着我们 X
和 y
是独立的,因此我们预计准确性在0.5左右。然而,由于特征选择步骤“看到”测试数据,因此该模型具有不公平的优势。在下面的错误示例中,我们首先使用所有数据进行特征选择,然后将数据拆分为训练和测试子集以进行模型匹配。结果是比预期高得多的准确性分数::
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.feature_selection import SelectKBest
>>> from sklearn.ensemble import HistGradientBoostingClassifier
>>> from sklearn.metrics import accuracy_score
>>> # Incorrect preprocessing: the entire data is transformed
>>> X_selected = SelectKBest(k=25).fit_transform(X, y)
>>> X_train, X_test, y_train, y_test = train_test_split(
... X_selected, y, random_state=42)
>>> gbc = HistGradientBoostingClassifier(random_state=1)
>>> gbc.fit(X_train, y_train)
HistGradientBoostingClassifier(random_state=1)
>>> y_pred = gbc.predict(X_test)
>>> accuracy_score(y_test, y_pred)
0.76
Right
为了防止数据泄露,最好将数据拆分为训练和测试子集 first .然后,只需使用火车数据集即可形成特征选择。请注意,每当我们使用 fit
或 fit_transform
,我们只使用火车数据集。现在的分数是我们对数据的预期,接近机会::
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, random_state=42)
>>> select = SelectKBest(k=25)
>>> X_train_selected = select.fit_transform(X_train, y_train)
>>> gbc = HistGradientBoostingClassifier(random_state=1)
>>> gbc.fit(X_train_selected, y_train)
HistGradientBoostingClassifier(random_state=1)
>>> X_test_selected = select.transform(X_test)
>>> y_pred = gbc.predict(X_test_selected)
>>> accuracy_score(y_test, y_pred)
0.5
在这里,我们再次建议使用 Pipeline
将特征选择和模型估计器链接在一起。管道确保执行时仅使用训练数据 fit
且测试数据仅用于计算准确度评分::
>>> from sklearn.pipeline import make_pipeline
>>> X_train, X_test, y_train, y_test = train_test_split(
... X, y, random_state=42)
>>> pipeline = make_pipeline(SelectKBest(k=25),
... HistGradientBoostingClassifier(random_state=1))
>>> pipeline.fit(X_train, y_train)
Pipeline(steps=[('selectkbest', SelectKBest(k=25)),
('histgradientboostingclassifier',
HistGradientBoostingClassifier(random_state=1))])
>>> y_pred = pipeline.predict(X_test)
>>> accuracy_score(y_test, y_pred)
0.5
管道还可以输入交叉验证功能,例如 cross_val_score
.同样,管道确保在匹配和预测期间使用正确的数据子集和估计器方法:
>>> from sklearn.model_selection import cross_val_score
>>> scores = cross_val_score(pipeline, X, y)
>>> print(f"Mean accuracy: {scores.mean():.2f}+/-{scores.std():.2f}")
Mean accuracy: 0.43+/-0.05
11.3. 控制随机性#
一些scikit学习对象本质上是随机的。这些通常是估计量(例如 RandomForestClassifier
)和交叉验证拆分器(例如 KFold
).这些物体的随机性是通过它们的 random_state
参数,如 Glossary .本节将详细介绍术语表条目,并描述良好实践和常见陷阱。这个微妙的参数。
备注
建议摘要
为了获得交叉验证(CV)结果的最佳稳健性,通过 RandomState
创建估计值时的实例,或离开 random_state
到 None
.将整数传递给CV拆分器通常是最安全的选择,也是最好的选择;传递 RandomState
拆分器的实例有时可能有助于实现非常特定的用例。对于估计器和拆分器来说,传递一个整点与传递一个实例(或 None
)导致微妙但显着的差异,尤其是对于CV程序。在报告结果时了解这些差异非常重要。
对于执行过程中的可重复结果,请删除任何使用 random_state=None
.
11.3.1. 使用 None
或 RandomState
实例,并反复调用 fit
和 split
#
的 random_state
参数确定是否多次调用 fit (for估计者)或到 split (for根据以下规则,CV拆分器)将产生相同的结果:
如果传递了一个integer,则调用
fit
或split
多次总是产生相同的结果。如果
None
或RandomState
实例已传递:fit
和split
每次被调用时都会产生不同的结果,并且连续的调用探索了所有信息的来源。None
是所有的默认值random_state
参数
我们在这里说明了估计器和CV拆分器的这些规则。
备注
Since passing random_state=None
is equivalent to passing the global
RandomState
instance from numpy
(random_state=np.random.mtrand._rand
), we will not explicitly mention
None
here. Everything that applies to instances also applies to using
None
.
11.3.1.1. 估计#
传递实例意味着调用 fit
即使估计器在相同的数据上并使用相同的超参数进行了匹配,多次也不会产生相同的结果::
>>> from sklearn.linear_model import SGDClassifier
>>> from sklearn.datasets import make_classification
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> X, y = make_classification(n_features=5, random_state=rng)
>>> sgd = SGDClassifier(random_state=rng)
>>> sgd.fit(X, y).coef_
array([[ 8.85418642, 4.79084103, -3.13077794, 8.11915045, -0.56479934]])
>>> sgd.fit(X, y).coef_
array([[ 6.70814003, 5.25291366, -7.55212743, 5.18197458, 1.37845099]])
从上面的片段我们可以看到,反复调用 sgd.fit
产生了不同的模型,即使数据是相同的。这是因为估计器的随机数生成器(RNG)在以下情况下被消耗(即突变): fit
被调用,并且这个突变的RNG将用于后续的调用 fit
.此外该 rng
对象在所有使用它的对象之间共享,因此,这些对象变得有些相互依赖。例如,两个共享相同的估计量 RandomState
实例会相互影响,正如我们稍后在讨论克隆时所看到的那样。调试时要记住这一点很重要。
如果我们向 random_state
参数 SGDClassifier
,我们会获得相同的模型,因此每次都会获得相同的分数。当我们传递一个整除时,所有调用都会使用相同的RNG fit
.内部发生的情况是,即使RNG被消耗时 fit
被调用时,它总是在开始时重置为其原始状态 fit
.
11.3.1.2. CV分配器#
Randomized CV splitters have a similar behavior when a RandomState
instance is passed; calling split
multiple times yields different data
splits:
>>> from sklearn.model_selection import KFold
>>> import numpy as np
>>> X = y = np.arange(10)
>>> rng = np.random.RandomState(0)
>>> cv = KFold(n_splits=2, shuffle=True, random_state=rng)
>>> for train, test in cv.split(X, y):
... print(train, test)
[0 3 5 6 7] [1 2 4 8 9]
[1 2 4 8 9] [0 3 5 6 7]
>>> for train, test in cv.split(X, y):
... print(train, test)
[0 4 6 7 8] [1 2 3 5 9]
[1 2 3 5 9] [0 4 6 7 8]
我们可以看到这次分裂与第二次不同 split
被称为。如果您通过调用比较多个估计器的性能,这可能会导致意外结果 split
很多次,正如我们将在下一节中看到的那样。
11.3.2. 常见陷阱和微妙之处#
而管理的规则 random_state
参数看起来很简单,但确实有一些微妙的含义。在某些情况下,这甚至可能导致错误的结论。
11.3.2.1. 估计#
Different `random_state` types lead to different cross-validation procedures
取决于类型 random_state
参数,估计器的行为会有所不同,尤其是在交叉验证过程中。考虑以下片段::
>>> from sklearn.ensemble import RandomForestClassifier
>>> from sklearn.datasets import make_classification
>>> from sklearn.model_selection import cross_val_score
>>> import numpy as np
>>> X, y = make_classification(random_state=0)
>>> rf_123 = RandomForestClassifier(random_state=123)
>>> cross_val_score(rf_123, X, y)
array([0.85, 0.95, 0.95, 0.9 , 0.9 ])
>>> rf_inst = RandomForestClassifier(random_state=np.random.RandomState(0))
>>> cross_val_score(rf_inst, X, y)
array([0.9 , 0.95, 0.95, 0.9 , 0.9 ])
We see that the cross-validated scores of rf_123
and rf_inst
are
different, as should be expected since we didn't pass the same random_state
parameter. However, the difference between these scores is more subtle than
it looks, and the cross-validation procedures that were performed by
cross_val_score
significantly differ in
each case:
以来
rf_123
每次调用都传递了一个整数fit
使用相同的RNG:这意味着随机森林估计器的所有随机特征对于CV程序的5个折叠中的每一个都是相同的。特别是,估计器的(随机选择的)特征子集在所有折叠中都是相同的。以来
rf_inst
被通过了RandomState
例如,每次调用fit
从不同的RNG开始。因此,特征的随机子集对于每个折叠来说都是不同的。
虽然在折叠中使用恒定估计器RNG本质上并不是错误的,但我们通常希望获得可靠的CV结果。估计者的随机性。因此,传递实例而不是整个可能是更好的,因为这将允许估计器RNG针对每个折叠而变化。
备注
在这里, cross_val_score
将使用非随机CV拆分器(默认情况),因此将在相同的拆分上评估两个估计量。本节不是关于分裂的变异性。此外,无论我们传递一个integer还是一个实例 make_classification
与我们的说明目的无关:重要的是我们传递给 RandomForestClassifier
估计者。
克隆#
传球的另一个微妙副作用 RandomState
实例是如何 clone
将工作::
>>> from sklearn import clone
>>> from sklearn.ensemble import RandomForestClassifier
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> a = RandomForestClassifier(random_state=rng)
>>> b = clone(a)
由于 RandomState
实例已传递给 a
, a
和 b
不是严格意义上的克隆,而是统计意义上的克隆: a
和 b
仍然是不同的模型,即使在调用 fit(X, y)
基于相同的数据。此外, a
和 b
将相互影响,因为它们共享相同的内部RNG:呼叫 a.fit
将消耗 b
的RNG,并呼叫 b.fit
将消耗 a
是RNG,因为它们是一样的。对于任何共享一个 random_state
参数;它不特定于克隆。
如果传递了一个整词, a
和 b
将是精确的克隆,它们不会相互影响。
警告
即使 clone
很少在用户代码中使用,它在整个scikit-learn代码库中普遍被调用:特别是,大多数接受非匹配估计量的元估计量调用 clone
境内 (GridSearchCV
, StackingClassifier
, CalibratedClassifierCV
等)。
11.3.2.2. CV分配器#
当经过一个 RandomState
例如,CV拆分器每次都会产生不同的拆分 split
被称为。当比较不同的估计器时,这可能会导致高估估计器之间性能差异的方差::
>>> from sklearn.naive_bayes import GaussianNB
>>> from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
>>> from sklearn.datasets import make_classification
>>> from sklearn.model_selection import KFold
>>> from sklearn.model_selection import cross_val_score
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> X, y = make_classification(random_state=rng)
>>> cv = KFold(shuffle=True, random_state=rng)
>>> lda = LinearDiscriminantAnalysis()
>>> nb = GaussianNB()
>>> for est in (lda, nb):
... print(cross_val_score(est, X, y, cv=cv))
[0.8 0.75 0.75 0.7 0.85]
[0.85 0.95 0.95 0.85 0.95]
直接比较 LinearDiscriminantAnalysis
估计者与 GaussianNB
估计器 on each fold 那将是一个错误: the splits on which the estimators are evaluated are different .事实上, cross_val_score
将在内部调用 cv.split
在同一 KFold
例如,但每次拆分都会不同。对于任何通过交叉验证执行模型选择的工具来说,这也是如此,例如 GridSearchCV
和 RandomizedSearchCV
:不同电话中的分数不具有可比性 search.fit
,自从 cv.split
会被多次呼叫。只需一次通话即可 search.fit
然而,由于搜索估计器仅调用 cv.split
一次
对于所有场景下的可比折叠结果,应向CV拆分器传递一个整数: cv = KFold(shuffle=True, random_state=0)
.
备注
虽然不建议进行折叠比较 RandomState
然而,在实例中,只要使用足够的折叠和数据,平均分数就可以得出一个估计器是否比另一个估计器更好的结论。
备注
在这个例子中重要的是传递给了什么 KFold
.无论我们通过一个 RandomState
实例或一个整个 make_classification
与我们的说明目的无关。此外,两者都没有 LinearDiscriminantAnalysis
也不 GaussianNB
是随机估计者。
11.3.3. 一般性建议#
11.3.3.1. 在多次执行中获得可重复的结果#
为了获得多个可重复(即恒定)的结果 program executions ,我们需要删除所有使用 random_state=None
,这是默认值。建议的方式是声明 rng
程序顶部的变量,并将其传递给任何接受 random_state
参数::
>>> from sklearn.ensemble import RandomForestClassifier
>>> from sklearn.datasets import make_classification
>>> from sklearn.model_selection import train_test_split
>>> import numpy as np
>>> rng = np.random.RandomState(0)
>>> X, y = make_classification(random_state=rng)
>>> rf = RandomForestClassifier(random_state=rng)
>>> X_train, X_test, y_train, y_test = train_test_split(X, y,
... random_state=rng)
>>> rf.fit(X_train, y_train).score(X_test, y_test)
0.84
现在,无论我们运行它多少次,我们都可以保证该脚本的结果始终为0.84。更改全局 rng
正如预期的那样,变量为不同值应该会影响结果。
也可以宣布 rng
变量为一个整数。然而,这可能会导致交叉验证结果不那么稳健,正如我们将在下一节中看到的那样。
备注
我们不建议将全局 numpy
seed by calling np.random.seed(0)
. See here 进行讨论。
11.3.3.2. 交叉验证结果的稳健性#
当我们通过交叉验证评估随机估计器的性能时,我们希望确保估计器能够对新数据产生准确的预测,但我们也希望确保估计器在w.r.t.时是稳健的。其随机初始化。例如,我们希望初始化 SGDClassifier
在所有方面始终保持良好状态:否则,当我们根据新数据训练估计器时,我们可能会变得不幸,并且随机初始化可能会导致性能不佳。类似地,我们希望随机森林在w.r.t.时具有稳健性。每棵树将使用的一组随机选择的特征。
出于这些原因,最好通过让估计器在每个折叠上使用不同的RNG来评估交叉验证性能。这是通过传递 RandomState
实例(或 None
)到估计器初始化。
当我们传递一个整元时,估计器将在每个倍数上使用相同的RNG:如果估计器表现良好(或差)(如CV所评估的那样),那可能只是因为我们对该特定种子很幸运(或不幸)。传递实例会产生更稳健的CV结果,并使各种算法之间的比较更加公平。它还有助于限制将估计器的RNG视为可以调整的超参数的诱惑。
无论我们通过 RandomState
CV拆分器的实例或整数对稳健性没有影响,只要 split
只被呼叫一次。当 split
被多次调用,则不再可能进行折叠间比较。因此,向CV拆分器传递integer通常更安全,并且涵盖大多数用例。