跳到主要内容
Ctrl+K
scikit-learn homepage scikit-learn homepage
  • 安装
  • 用户指南
  • API
  • 示例
  • 社区
    • 入门
    • 发布历史
    • 词汇表
    • 开发
    • 常见问题
    • 支持
    • 相关项目
    • 路线图
    • 治理
    • 关于我们
  • GitHub
  • 安装
  • 用户指南
  • API
  • 示例
  • 社区
  • 入门
  • 发布历史
  • 词汇表
  • 开发
  • 常见问题
  • 支持
  • 相关项目
  • 路线图
  • 治理
  • 关于我们
  • GitHub

章节导航

  • 1. 监督学习
    • 1.1. 线性模型
    • 1.2. 线性判别分析和二次判别分析
    • 1.3. 核岭回归
    • 1.4. 支持向量机
    • 1.5. 随机梯度下降
    • 1.6. 最近邻
    • 1.7. 高斯过程
    • 1.8. 交叉分解
    • 1.9. 朴素贝叶斯
    • 1.10. 决策树
    • 1.11. 集成:梯度提升、随机森林、Bagging、投票、Stacking
    • 1.12. 多类别和多输出算法
    • 1.13. 特征选择
    • 1.14. 半监督学习
    • 1.15. 等度回归
    • 1.16. 概率校准
    • 1.17. 神经网络模型(监督式)
  • 2. 无监督学习
    • 2.1. 高斯混合模型
    • 2.2. 流形学习
    • 2.3. 聚类
    • 2.4. 双聚类
    • 2.5. 信号分量分解(矩阵分解问题)
    • 2.6. 协方差估计
    • 2.7. 新颖性和异常检测
    • 2.8. 密度估计
    • 2.9. 神经网络模型(无监督式)
  • 3. 模型选择和评估
    • 3.1. 交叉验证:评估估计器性能
    • 3.2. 调整估计器的超参数
    • 3.3. 调整类别预测的决策阈值
    • 3.4. 度量和评分:量化预测质量
    • 3.5. 验证曲线:绘制分数以评估模型
  • 4. 元数据路由
  • 5. 检查
    • 5.1. 部分依赖和个体条件期望图
    • 5.2. 置换特征重要性
  • 6. 可视化
  • 7. 数据集变换
    • 7.1. 管道和复合估计器
    • 7.2. 特征提取
    • 7.3. 数据预处理
    • 7.4. 缺失值填充
    • 7.5. 无监督降维
    • 7.6. 随机投影
    • 7.7. 核近似
    • 7.8. 成对度量、相似度和核函数
    • 7.9. 变换预测目标(y)
  • 8. 数据集加载工具
    • 8.1. 玩具数据集
    • 8.2. 真实世界数据集
    • 8.3. 生成数据集
    • 8.4. 加载其他数据集
  • 9. 使用 scikit-learn 进行计算
    • 9.1. 计算扩展策略:大数据
    • 9.2. 计算性能
    • 9.3. 并行、资源管理和配置
  • 10. 模型持久化
  • 11. 常见陷阱和推荐实践
  • 12. 调度
    • 12.1. 数组API支持(实验性)
  • 13. 选择合适的估计器
  • 14. 外部资源、视频和讲座
  • 用户指南
  • 11. 常见陷阱和推荐实践

11. 常见陷阱和推荐实践#

本章旨在阐述使用 scikit-learn 时常见的一些陷阱和反模式。它提供了**不应**做的事情的示例,以及相应的正确示例。

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)

错误示例

训练数据集被缩放,但测试数据集没有,因此模型在测试数据集上的性能低于预期

>>> 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...

正确示例

我们不应该将未经变换的 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. 数据泄露#

当构建模型时使用了在预测时不可用的信息时,就会发生数据泄露。这会导致性能估计过于乐观,例如来自交叉验证的结果,从而导致模型在实际新数据(例如在生产环境中)上使用时性能较差。

一个常见原因是未将测试和训练数据子集分开。测试数据绝不应用于做出关于模型的选择。**一般规则是绝不在测试数据上调用** fit。虽然这听起来很明显,但在某些情况下很容易被忽视,例如在应用某些预处理步骤时。

尽管训练和测试数据子集都应接受相同的预处理变换(如上一节所述),但重要的是这些变换只能从训练数据中学习。例如,如果你有一个归一化步骤,其中你除以平均值,那么这个平均值应该是训练子集的平均值,而**不是**所有数据的平均值。如果测试子集被包含在平均值计算中,则来自测试子集的信息会影响模型。

11.2.1. 如何避免数据泄露#

以下是避免数据泄露的一些提示

  • 始终首先将数据分成训练和测试子集,尤其是在任何预处理步骤之前。

  • 在使用 fit 和 fit_transform 方法时,切勿包含测试数据。使用所有数据,例如 fit(X),可能会导致过于乐观的分数。

    相反,transform 方法应同时应用于训练和测试子集,因为所有数据都应应用相同的预处理。这可以通过在训练子集上使用 fit_transform 并在测试子集上使用 transform 来实现。

  • scikit-learn 管道是防止数据泄露的好方法,因为它确保在正确的数据子集上执行适当的方法。管道非常适合在交叉验证和超参数调优函数中使用。

预处理期间数据泄露的一个示例如下所述。

11.2.2. 预处理期间的数据泄露#

注意

我们在此选择通过特征选择步骤来演示数据泄露。然而,这种泄露风险几乎与 scikit-learn 中的所有变换都相关,包括(但不限于)StandardScaler、SimpleImputer 和 PCA。

scikit-learn 中提供了许多特征选择函数。它们可以帮助删除不相关、冗余和噪声特征,并提高模型的构建时间和性能。与任何其他类型的预处理一样,特征选择应**仅**使用训练数据。在特征选择中包含测试数据将乐观地偏向你的模型。

为了演示,我们将创建这个具有 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)

错误示例

使用所有数据执行特征选择会导致准确率分数远高于随机水平,尽管我们的目标是完全随机的。这种随机性意味着我们的 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

正确示例

为了防止数据泄露,最佳实践是**首先**将数据分成训练和测试子集。然后,可以使用训练数据集进行特征选择。请注意,无论何时使用 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-learn 对象本质上是随机的。这些通常是估计器(例如 RandomForestClassifier)和交叉验证拆分器(例如 KFold)。这些对象的随机性通过它们的 random_state 参数控制,如词汇表中所述。本节将扩展词汇表中的条目,并描述有关此微妙参数的最佳实践和常见陷阱。

注意

建议总结

为了获得交叉验证(CV)结果的最佳鲁棒性,在创建估计器时传递 RandomState 实例,或者将 random_state 留空(None)。将整数传递给 CV 拆分器通常是最安全且首选的选项;有时将 RandomState 实例传递给拆分器可能有助于实现非常特定的用例。对于估计器和拆分器,传递整数与传递实例(或 None)会导致微妙但显著的差异,特别是对于 CV 过程。在报告结果时,理解这些差异非常重要。

为了在多次执行中获得可重现的结果,请移除所有对 random_state=None 的使用。

11.3.1. 使用 None 或 RandomState 实例,以及对 fit 和 split 的重复调用#

random_state 参数根据以下规则决定多次调用fit(对于估计器)或split(对于 CV 拆分器)是否会产生相同的结果:

  • 如果传递一个整数,多次调用 fit 或 split 总是会产生相同的结果。

  • 如果传递 None 或 RandomState 实例:每次调用 fit 和 split 都会产生不同的结果,并且连续的调用会探索所有熵源。None 是所有 random_state 参数的默认值。

我们在此为估计器和 CV 拆分器说明这些规则。

注意

由于传递 random_state=None 等同于传递来自 numpy 的全局 RandomState 实例(即 random_state=np.random.mtrand._rand),我们在此不会明确提及 None。所有适用于实例的规则也适用于使用 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 产生了不同的模型,即使数据是相同的。这是因为当调用 fit 时,估计器的随机数生成器(RNG)被消耗(即发生变异),并且这个变异后的 RNG 将用于后续对 fit 的调用。此外,rng 对象在所有使用它的对象之间共享,因此这些对象在某种程度上相互依赖。例如,共享相同 RandomState 实例的两个估计器将相互影响,我们将在稍后讨论克隆时看到这一点。在调试时,这一点很重要。

如果我们向 SGDClassifier 的 random_state 参数传递一个整数,我们将每次都获得相同的模型,从而得到相同的分数。当我们传递一个整数时,所有对 fit 的调用都使用相同的 RNG。内部发生的情况是,即使在调用 fit 时 RNG 被消耗,它总是在 fit 开始时重置为其原始状态。

11.3.1.2. 交叉验证拆分器#

当传递 RandomState 实例时,随机化 CV 拆分器具有类似的行为;多次调用 split 会产生不同的数据拆分。

>>> 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. 估计器#

不同的 `random_state` 类型导致不同的交叉验证过程

根据 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 ])

我们看到 rf_123 和 rf_inst 的交叉验证分数不同,这在意料之中,因为我们没有传递相同的 random_state 参数。然而,这些分数之间的差异比看起来更微妙,并且**由** cross_val_score **执行的交叉验证过程在每种情况下都有显著差异。**

  • 由于 rf_123 传递了一个整数,每次调用 fit 都使用相同的 RNG:这意味着随机森林估计器的所有随机特性在 CV 过程的每个 5 折中都将相同。特别是,估计器的(随机选择的)特征子集将在所有折中保持一致。

  • 由于 rf_inst 传递了一个 RandomState 实例,每次调用 fit 都从不同的 RNG 开始。因此,每个折的随机特征子集将不同。

虽然在各折中拥有恒定的估计器 RNG 本身并没有错,但我们通常希望 CV 结果对估计器的随机性具有鲁棒性。因此,传递实例而不是整数可能更可取,因为它将允许估计器 RNG 在每个折中变化。

注意

在这里,cross_val_score 将使用一个非随机化的 CV 拆分器(这是默认设置),因此两个估计器都将在相同的拆分上进行评估。本节不涉及拆分中的变异性。此外,我们是否将整数或实例传递给 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 并非严格意义上的克隆,而是统计意义上的克隆:即使在相同数据上调用 fit(X, y),a 和 b 仍然是不同的模型。此外,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. 交叉验证拆分器#

当传递一个 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 估计器在**每个折叠**上的性能将是一个错误:**评估估计器的拆分是不同的**。事实上,cross_val_score 将在内部对相同的 KFold 实例调用 cv.split,但每次的拆分都将不同。这对于任何通过交叉验证执行模型选择的工具也是如此,例如 GridSearchCV 和 RandomizedSearchCV:由于 cv.split 会被多次调用,因此在不同调用 search.fit 之间,分数在折叠间是不可比较的。然而,在对 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. 在多次执行中获得可重现结果#

为了在多次*程序执行*中获得可重现(即恒定)的结果,我们需要移除所有对 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 变量声明为整数。但是,这可能会导致交叉验证结果的鲁棒性降低,我们将在下一节中看到。

注意

我们不建议通过调用 np.random.seed(0) 来设置全局 numpy 种子。有关讨论,请参阅此处。

11.3.3.2. 交叉验证结果的鲁棒性#

当我们通过交叉验证评估随机估计器的性能时,我们希望确保估计器能够对新数据产生准确的预测,但我们也希望确保估计器对其随机初始化具有鲁棒性。例如,我们希望 SGDClassifier 的随机权重初始化在所有折叠中保持一致的良好表现:否则,当我们在新数据上训练该估计器时,我们可能会运气不佳,导致随机初始化带来糟糕的性能。同样,我们希望随机森林对每棵树将使用的随机选择的特征集具有鲁棒性。

由于这些原因,最好通过让估计器在每个折叠上使用不同的 RNG 来评估交叉验证性能。这可以通过在估计器初始化时传递一个 RandomState 实例(或 None)来实现。

当我们传递一个整数时,估计器将在每个折叠上使用相同的 RNG:如果估计器表现良好(或不佳),这可能是因为我们运气好(或不好),恰好选择了那个特定的随机种子。传递实例会带来更鲁棒的 CV 结果,并使得不同算法之间的比较更加公平。它还有助于限制将估计器的 RNG 视为可调超参数的诱惑。

无论我们是向 CV 拆分器传递 RandomState 实例还是整数,只要 split 只被调用一次,就不会对鲁棒性产生影响。当 split 被多次调用时,就无法进行折叠间比较了。因此,向 CV 拆分器传递整数通常更安全,并涵盖了大多数用例。

上一条

10. 模型持久化

下一条

12. 调度

在本页
  • 11.1. 不一致的预处理
  • 11.2. 数据泄露
    • 11.2.1. 如何避免数据泄露
    • 11.2.2. 预处理期间的数据泄露
  • 11.3. 控制随机性
    • 11.3.1. 使用 None 或 RandomState 实例,以及对 fit 和 split 的重复调用
      • 11.3.1.1. 估计器
      • 11.3.1.2. 交叉验证拆分器
    • 11.3.2. 常见陷阱和微妙之处
      • 11.3.2.1. 估计器
      • 11.3.2.2. 交叉验证拆分器
    • 11.3.3. 一般建议
      • 11.3.3.1. 在多次执行中获得可重现结果
      • 11.3.3.2. 交叉验证结果的鲁棒性

本页

  • 显示源文件

© 版权所有 2007 - 2025,scikit-learn 开发者(BSD 许可证)。