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