.. _minimal_reproducer: ============================================== 制作 scikit-learn 的最小复现示例 ============================================== 无论是提交错误报告、设计测试套件,还是仅仅在讨论中提出问题,能够制作最小、可复现的示例(或最小、可运行的示例)是与社区有效且高效沟通的关键。 互联网上有非常好的指南,例如 `这个 StackOverflow 文档 `_ 或 `Matthew Rocklin 的这篇博客文章 `_ 关于制作最小完整可验证示例(以下简称 MCVE)。我们的目标不是重复这些参考资料,而是提供一个逐步指南,说明如何缩小错误范围,直到你得到最短的可能代码来复现它。 在向 scikit-learn 提交错误报告之前,第一步是阅读 `Issue 模板 `_ 。它已经相当详细地说明了你将被要求提供的信息。 .. _good_practices: 良好实践 ============== 在本节中,我们将重点放在 `Issue 模板 `_ 的 **Steps/Code to Reproduce** 部分。我们将从一个已经提供失败示例但仍有可读性改进空间的代码片段开始。然后我们从它制作一个 MCVE。 **示例** .. code-block:: python # 我目前正在做一个机器学习项目,当我尝试将 GradientBoostingRegressor 实例拟合到 my_data.csv 时,我收到了一个 UserWarning: # "X 有特征名称,但 DecisionTreeRegressor 在没有特征名称的情况下被拟合"。你可以从 # https://example.com/my_data.csv 并验证我的特征确实有名称。问题似乎在传递整数给 n_iter_no_change 参数时在 fit 过程中出现。 df = pd.read_csv('my_data.csv') X = df[["feature_name"]] # 我的特征确实有名称 y = df["target"] # 我们为 train_test_split 设置 random_state=42 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) # 使用默认的 n_iter_no_change 不会引发错误或警告 gbdt = GradientBoostingRegressor(random_state=0) gbdt.fit(X_train, y_train) default_score = gbdt.score(X_test, y_test) # 当我更改 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 代码片段中展示,以避免任何歧义。此外,通过这一点,你已经在 `Issue template `_ 的 **Describe the bug** 部分提供了简洁的描述。 以下代码虽然**仍然不是最简化的**,但已经**好得多**,因为它可以复制粘贴到 Python 终端中一步重现问题。特别是: - 它包含了**所有必要的导入语句**; - 它可以获取公共数据集,无需手动下载文件并将其放置在磁盘上的预期位置。 **改进的示例** .. code-block:: 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` 预处理数据的情况下重现。 **改进的例子** .. code-block:: python 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) # 无警告 gbdt = GradientBoostingRegressor(n_iter_no_change=5) gbdt.fit(X, y) # 引发警告 **不要** 报告你的数据,除非极其必要 ------------------------------------------------------------ 我们的目标是尽可能使代码自包含。为此,你可以使用 :ref:`合成数据` 。这些数据可以通过 numpy、pandas 或 :mod:`sklearn.datasets` 模块生成。大多数情况下,错误与数据的特定结构无关。即使有关,也请尝试找到一个具有类似特征且能重现问题的可用数据集。在这个特定情况下,我们关注的是具有标记特征名称的数据。 **改进的示例** .. code-block:: python 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) # 无警告 gbdt = GradientBoostingRegressor(n_iter_no_change=5) gbdt.fit(X, y) # 引发警告 如前所述,沟通的关键在于代码的可读性,良好的格式确实能加分。注意在前面的代码片段中我们: - 尽量将所有行限制在最多 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 代码片段 .. code-block:: python from sklearn.datasets import make_blobs n_samples = 100 n_components = 3 X, y = make_blobs(n_samples=n_samples, centers=n_components) 在提交错误报告时,不需要创建多个代码块。记住其他审阅者将会复制粘贴您的代码,拥有一个单元格将使他们的任务更简单。 在 `Issue 模板 `_ 的 **Actual results** 部分,您需要提供包括完整回溯的错误消息。在这种情况下,使用 `python-traceback` 限定符。例如:: python-traceback --------------------------------------------------------------------------- TypeError Traceback (most recent call last) in 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' 渲染后产生如下结果: .. code-block:: python --------------------------------------------------------------------------- TypeError Traceback (most recent call last) in 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' .. _synth_data: 合成数据集 ========== 在选择特定的合成数据集之前,首先需要确定您正在解决的问题类型:是分类、回归、聚类等? 一旦确定了问题类型,您需要相应地提供一个合成数据集。大多数情况下,您只需要一个极简的数据集。以下是一些可能对您有帮助的工具的非详尽列表。 NumPy ----- NumPy 工具,如 `numpy.random.randn `_ 和 `numpy.random.randint `_ 可以用来创建虚拟数值数据。 - 回归 回归使用连续的数值数据作为特征和目标。 .. code-block:: python 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) 类似的代码片段可以用作测试缩放工具(如 :class:`sklearn.preprocessing.StandardScaler` )时的合成数据。 - 分类 如果错误不是在编码分类变量时引发的,你可以将数值数据输入分类器。只需确保目标确实是一个整数。 .. code-block:: python 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) # 二元目标,值为 {0, 1} 如果错误仅在非数值类别标签时发生,你可能希望使用 `numpy.random.choice `_ 生成随机目标。 .. code-block:: python 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.DataFrame `_ 或 `pandas.Series `_ 将 numpy 数组转换为 pandas 对象。 .. code-block:: python 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 包含各种 :ref:`sample_generators` ,可用于构建控制大小和复杂度的人工数据集。 `make_regression` ----------------- 顾名思义,:class:`sklearn.datasets.make_regression` 生成回归数据集。 带有噪声的回归目标作为随机特征的稀疏可选随机线性组合。 .. code-block:: python from sklearn.datasets import make_regression X, y = make_regression(n_samples=1000, n_features=20) `make_classification` --------------------- :class:`sklearn.datasets.make_classification` 创建多类数据集,每个类包含多个高斯簇。可以通过相关、冗余或无信息特征引入噪声。 .. code-block:: python 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` 类似,:class:`sklearn.datasets.make_blobs` 使用正态分布的点簇创建多类数据集。它提供了关于每个簇的中心和标准偏差的更多控制,因此对于演示聚类非常有用。 .. code-block:: python from sklearn.datasets import make_blobs X, y = make_blobs(n_samples=10, centers=3, n_features=2) 数据集加载工具 --------------- 您可以使用 :ref:`datasets` 来加载和获取几个流行的参考数据集。当错误与数据的特定结构相关时,例如处理缺失值或图像识别,此选项非常有用。 .. code-block:: python from sklearn.datasets import load_breast_cancer X, y = load_breast_cancer(return_X_y=True)