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