為 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 語句

  • 它可以取得公開資料集,而無需手動下載檔案並將其放置在磁碟上的預期位置。

改進的範例

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 問題上呈現的程式碼片段區塊中出現水平捲軸;

  • 使用空白行分隔相關函數組;

  • 將所有 import 放在開頭的它們自己的群組中。

本指南中介紹的簡化步驟可以以與我們在此處展示的進程不同的順序實施。重點是

  • 最小可重現範例應該可以透過簡單地複製貼上到 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)