为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)

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

问题模板中名为实际结果的部分,要求您提供错误消息,包括异常的完整回溯。在这种情况下,请使用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}
    

    如果错误只发生在非数字类标签上,您可能希望使用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)