为scikit-learn创建最小复现器#

无论是提交错误报告、设计一套测试,还是仅仅在讨论区提问,能够创建最小、可复现的示例(或最小、可工作的示例)是与社区有效和高效沟通的关键。

互联网上有很多非常好的指南,例如这篇StackOverflow文档Matthew Rocklin的这篇博文关于创建最小完整可验证示例(以下简称MCVE)。我们的目标不是重复这些参考资料,而是提供一个逐步指南,说明如何缩小错误范围,直到您获得最短的代码来复现它。

在向scikit-learn提交错误报告之前,第一步是阅读问题模板。它已经提供了关于您将被要求提供的信息的相当详细的说明。

良好实践#

在本节中,我们将重点关注问题模板的“复现步骤/代码”部分。我们将从一个已经提供了失败示例但仍有可读性改进空间的片段开始。然后,我们将从中创建一个MCVE。

示例

# I am currently working in a ML project and when I tried to fit a
# GradientBoostingRegressor instance to my_data.csv I get a UserWarning:
# "X has feature names, but DecisionTreeRegressor was fitted without
# feature names". You can get a copy of my dataset from
# https://example.com/my_data.csv and verify my features do have
# names. The problem seems to arise during fit when I pass an integer
# to the n_iter_no_change parameter.

df = pd.read_csv('my_data.csv')
X = df[["feature_name"]] # my features do have names
y = df["target"]

# We set random_state=42 for the train_test_split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.33, random_state=42
)

scaler = StandardScaler(with_mean=False)
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# An instance with default n_iter_no_change raises no error nor warnings
gbdt = GradientBoostingRegressor(random_state=0)
gbdt.fit(X_train, y_train)
default_score = gbdt.score(X_test, y_test)

# the bug appears when I change the value for n_iter_no_change
gbdt = GradientBoostingRegressor(random_state=0, n_iter_no_change=5)
gbdt.fit(X_train, y_train)
other_score = gbdt.score(X_test, y_test)

other_score = gbdt.score(X_test, y_test)

提供一个带有最少注释的失败代码示例#

用英文书写重现问题的说明通常会产生歧义。最好确保所有重现问题所需的详细信息都在Python代码片段中得到说明,以避免任何歧义。此外,此时您已经在问题模板的“描述错误”部分提供了简洁的描述。

以下代码,虽然仍不最小,但已经好得多,因为它可以被复制粘贴到Python终端中,一步即可重现问题。特别是:

  • 它包含所有必要的导入语句

  • 它可以获取公共数据集,而无需手动下载文件并将其放置在磁盘上的预期位置。

改进示例

import pandas as pd

df = pd.read_csv("https://example.com/my_data.csv")
X = df[["feature_name"]]
y = df["target"]

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.33, random_state=42
)

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler(with_mean=False)
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

from sklearn.ensemble import GradientBoostingRegressor

gbdt = GradientBoostingRegressor(random_state=0)
gbdt.fit(X_train, y_train)  # no warning
default_score = gbdt.score(X_test, y_test)

gbdt = GradientBoostingRegressor(random_state=0, n_iter_no_change=5)
gbdt.fit(X_train, y_train)  # raises warning
other_score = gbdt.score(X_test, y_test)
other_score = gbdt.score(X_test, y_test)

将您的脚本精简到尽可能小#

您必须问自己,哪些代码行与重现错误相关,哪些不相关。删除不必要的代码行或通过省略不相关的非默认选项来简化函数调用,将有助于您和其他贡献者缩小错误原因的范围。

特别是,对于此特定示例:

  • 警告与train_test_split无关,因为它在训练步骤中就已经出现,在使用了测试集之前。

  • 类似地,计算测试集得分的行不是必需的;

  • 该错误可以在random_state的任何值下重现,因此将其保留为默认值;

  • 无需使用StandardScaler对数据进行预处理即可重现此错误。

改进示例

import pandas as pd
df = pd.read_csv("https://example.com/my_data.csv")
X = df[["feature_name"]]
y = df["target"]

from sklearn.ensemble import GradientBoostingRegressor

gbdt = GradientBoostingRegressor()
gbdt.fit(X, y)  # no warning

gbdt = GradientBoostingRegressor(n_iter_no_change=5)
gbdt.fit(X, y)  # raises warning

不要报告您的数据,除非极其必要#

其思想是使代码尽可能自包含。为此,您可以使用合成数据集。它可以使用numpy、pandas或sklearn.datasets模块生成。大多数情况下,错误与您的数据的特定结构无关。即使有关,也请尝试找到一个具有与您的数据相似特征且能重现问题的可用数据集。在这种特殊情况下,我们对具有标记特征名称的数据感兴趣。

改进示例

import pandas as pd
from sklearn.ensemble import GradientBoostingRegressor

df = pd.DataFrame(
    {
        "feature_name": [-12.32, 1.43, 30.01, 22.17],
        "target": [72, 55, 32, 43],
    }
)
X = df[["feature_name"]]
y = df["target"]

gbdt = GradientBoostingRegressor()
gbdt.fit(X, y) # no warning
gbdt = GradientBoostingRegressor(n_iter_no_change=5)
gbdt.fit(X, y) # raises warning

如前所述,沟通的关键是代码的可读性,良好的格式确实能增色不少。请注意,在之前的片段中,我们:

  • 尽量将所有行限制在最多79个字符,以避免GitHub问题中代码片段块出现水平滚动条;

  • 使用空行分隔相关函数组;

  • 将所有导入放在开头自己的组中。

本指南中介绍的简化步骤可以以与此处所示进展不同的顺序实施。重要的是:

  • 最小复现器应该可以通过简单地复制粘贴到Python终端中运行;

  • 它应该通过删除任何与重现原始问题无关的代码步骤来尽可能简化;

  • 如果可能的话,它理想情况下应该只依赖于通过运行代码动态生成的最小数据集,而不是依赖外部数据。

使用Markdown格式#

要将代码或文本格式化成自己的独立块,请使用三个反引号。Markdown支持可选的语言标识符,以在您的围栏代码块中启用语法高亮显示。例如:

```python
from sklearn.datasets import make_blobs

n_samples = 100
n_components = 3
X, y = make_blobs(n_samples=n_samples, centers=n_components)
```

将呈现一个Python格式的片段,如下所示:

from sklearn.datasets import make_blobs

n_samples = 100
n_components = 3
X, y = make_blobs(n_samples=n_samples, centers=n_components)

提交bug报告时无需创建多个代码块。请记住,其他审阅者将复制粘贴您的代码,拥有单个单元格会使他们的任务更容易。

问题模板中名为“实际结果”的部分,您需要提供错误消息,包括异常的完整回溯。在这种情况下,请使用python-traceback限定符。例如:

```python-traceback
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-1-a674e682c281> in <module>
    4 vectorizer = CountVectorizer(input=docs, analyzer='word')
    5 lda_features = vectorizer.fit_transform(docs)
----> 6 lda_model = LatentDirichletAllocation(
    7     n_topics=10,
    8     learning_method='online',

TypeError: __init__() got an unexpected keyword argument 'n_topics'
```

渲染后显示如下:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-1-a674e682c281> in <module>
    4 vectorizer = CountVectorizer(input=docs, analyzer='word')
    5 lda_features = vectorizer.fit_transform(docs)
----> 6 lda_model = LatentDirichletAllocation(
    7     n_topics=10,
    8     learning_method='online',

TypeError: __init__() got an unexpected keyword argument 'n_topics'

合成数据集#

在选择特定的合成数据集之前,首先您必须确定您正在解决的问题类型:是分类、回归、聚类等等?

一旦您缩小了问题类型,您就需要相应地提供一个合成数据集。大多数情况下,您只需要一个极简数据集。这里提供一个可能对您有帮助的非详尽工具列表。

NumPy#

NumPy工具,例如numpy.random.randnnumpy.random.randint,可用于创建虚拟数值数据。

  • 回归

    回归将连续数值数据作为特征和目标。

    import numpy as np
    
    rng = np.random.RandomState(0)
    n_samples, n_features = 5, 5
    X = rng.randn(n_samples, n_features)
    y = rng.randn(n_samples)
    

类似的片段可以在测试缩放工具(如sklearn.preprocessing.StandardScaler)时用作合成数据。

  • 分类

    如果错误不是在编码分类变量时引发的,则可以将数值数据馈送给分类器。只需记住确保目标确实是整数。

    import numpy as np
    
    rng = np.random.RandomState(0)
    n_samples, n_features = 5, 5
    X = rng.randn(n_samples, n_features)
    y = rng.randint(0, 2, n_samples)  # binary target with values in {0, 1}
    

    如果bug只发生在非数值类别标签的情况下,您可能希望使用numpy.random.choice生成随机目标。

    import numpy as np
    
    rng = np.random.RandomState(0)
    n_samples, n_features = 50, 5
    X = rng.randn(n_samples, n_features)
    y = np.random.choice(
        ["male", "female", "other"], size=n_samples, p=[0.49, 0.49, 0.02]
    )
    

Pandas#

一些scikit-learn对象期望将pandas数据框作为输入。在这种情况下,您可以使用pandas.DataFramepandas.Series将numpy数组转换为pandas对象。

import numpy as np
import pandas as pd

rng = np.random.RandomState(0)
n_samples, n_features = 5, 5
X = pd.DataFrame(
    {
        "continuous_feature": rng.randn(n_samples),
        "positive_feature": rng.uniform(low=0.0, high=100.0, size=n_samples),
        "categorical_feature": rng.choice(["a", "b", "c"], size=n_samples),
    }
)
y = pd.Series(rng.randn(n_samples))

此外,scikit-learn包含各种生成的数据集,可用于构建受控大小和复杂性的人工数据集。

make_regression#

正如其名称所示,sklearn.datasets.make_regression生成带有噪声的回归目标,其形式为随机特征的稀疏随机线性组合。

from sklearn.datasets import make_regression

X, y = make_regression(n_samples=1000, n_features=20)

make_classification#

sklearn.datasets.make_classification创建多类数据集,每类具有多个高斯聚类。可以通过相关、冗余或无信息特征引入噪声。

from sklearn.datasets import make_classification

X, y = make_classification(
    n_features=2, n_redundant=0, n_informative=2, n_clusters_per_class=1
)

make_blobs#

make_classification类似,sklearn.datasets.make_blobs使用正态分布的点簇创建多类数据集。它在每个簇的中心和标准差方面提供了更大的控制,因此对于演示聚类非常有用。

from sklearn.datasets import make_blobs

X, y = make_blobs(n_samples=10, centers=3, n_features=2)

数据集加载工具#

您可以使用数据集加载工具来加载和获取几个流行的参考数据集。当错误与数据的特定结构相关时,例如处理缺失值或图像识别时,此选项很有用。

from sklearn.datasets import load_breast_cancer

X, y = load_breast_cancer(return_X_y=True)