開發 scikit-learn 估算器#

無論您是提議將估算器納入 scikit-learn 中、開發與 scikit-learn 相容的獨立套件,還是為您自己的專案實作自訂元件,本章將詳細說明如何開發可安全地與 scikit-learn 管線和模型選擇工具互動的物件。

本節詳細說明您應該使用和實作的 scikit-learn 相容估算器的公開 API。在 scikit-learn 內部,我們會實驗並使用一些私有工具,我們的目標始終是讓它們在足夠穩定後公開,以便您也可以在自己的專案中使用它們。

scikit-learn 物件的 API#

估算器主要有兩種型別。您可以將第一組視為簡單估算器,其中包含大多數估算器,例如 LogisticRegressionRandomForestClassifier。第二組是元估算器,也就是封裝其他估算器的估算器。PipelineGridSearchCV 是元估算器的兩個範例。

這裡我們先從一些詞彙開始,然後說明如何實作您自己的估算器。

scikit-learn API 的元素在常用術語和 API 元素詞彙表中有更明確的說明。

不同的物件#

scikit-learn 中的主要物件有(一個類別可以實作多個介面)

估算器

基礎物件,實作 fit 方法以從資料中學習,可以是

estimator = estimator.fit(data, targets)

estimator = estimator.fit(data)
預測器

對於監督式學習或某些非監督式問題,實作

prediction = predictor.predict(data)

分類演算法通常也會提供一種量化預測確定性的方法,可以使用 decision_functionpredict_proba

probability = predictor.predict_proba(data)
轉換器

用於以監督式或非監督式方式修改資料(例如,透過新增、變更或移除欄位,但不新增或移除列)。實作

new_data = transformer.transform(data)

當擬合和轉換可以比單獨執行更有效率地一起執行時,實作

new_data = transformer.fit_transform(data)
模型

可以提供擬合優度測量或未見資料似然性的模型,實作(越高越好)

score = model.score(data)

估算器#

API 有一個主要的物件:估算器。估算器是一個物件,可根據某些訓練資料擬合模型,並能夠推斷新資料的某些屬性。例如,它可以是分類器或迴歸器。所有估算器都實作了 fit 方法

estimator.fit(X, y)

在估算器實作的所有方法中,fit 通常是您想要自己實作的方法。其他方法(例如 set_paramsget_params 等)在 BaseEstimator 中實作,您應該從該類別繼承。您可能需要從更多 mixin 繼承,我們稍後會說明。

實例化#

這關係到物件的建立。物件的 __init__ 方法可能會接受常數作為決定估算器行為的引數(例如 SGDClassifier 中的 alpha 常數)。但是,它不應將實際的訓練資料作為引數,因為這留給 fit() 方法處理

clf2 = SGDClassifier(alpha=2.3)
clf3 = SGDClassifier([[1, 2], [2, 3]], [-1, 1]) # WRONG!

理想情況下,__init__ 接受的引數都應該是具有預設值的關鍵字引數。換句話說,使用者應該能夠在不傳遞任何引數的情況下實例化估算器。在某些情況下,如果引數沒有合理的預設值,則可以不設定預設值。在 scikit-learn 本身中,我們很少有這種情況,只有在某些元估算器中,子估算器引數才是必要引數。

大多數引數對應於描述模型或估算器嘗試解決的最佳化問題的超參數。其他參數可能會定義估算器的行為,例如定義快取的位置以儲存某些資料。這些初始引數(或參數)始終由估算器記住。另請注意,它們不應記錄在「屬性」區段下,而應記錄在該估算器的「參數」區段下。

此外,**__init__ 接受的每個關鍵字引數都應對應於實例上的屬性**。Scikit-learn 依賴此來尋找相關屬性,以便在執行模型選擇時設定在估算器上。

總結來說,__init__ 應如下所示

def __init__(self, param1=1, param2=2):
    self.param1 = param1
    self.param2 = param2

不應有邏輯,甚至不應有輸入驗證,並且不應變更參數;這也意味著理想情況下,它們不應是可變物件,例如列表或字典。如果它們是可變的,則應在修改之前複製它們。相應的邏輯應放在使用參數的地方,通常在 fit 中。以下是錯誤的

def __init__(self, param1=1, param2=2, param3=3):
    # WRONG: parameters should not be modified
    if param1 > 1:
        param2 += 1
    self.param1 = param1
    # WRONG: the object's attributes should have exactly the name of
    # the argument in the constructor
    self.param3 = param2

延遲驗證的原因是,如果 __init__ 包含輸入驗證,則必須在 set_params 中執行相同的驗證,而 set_params 用於 GridSearchCV 等演算法中。

此外,尾隨 _ 的參數**不應在 __init__ 方法內部設定**。有關非初始化引數的屬性的更多詳細資訊將很快介紹。

擬合#

接下來您可能想要做的是估計模型中的一些參數。這在 fit() 方法中實作,並且是訓練發生的位置。例如,這是您計算以學習或估計線性模型係數的位置。

fit() 方法會將訓練資料作為引數,在非監督式學習的情況下,可以是單個陣列,在監督式學習的情況下,可以是兩個陣列。與訓練資料一起提供的其他中繼資料(例如 sample_weight)也可以作為關鍵字引數傳遞給 fit

請注意,模型是使用 Xy 擬合的,但物件不保留對 Xy 的參考。但是,有一些例外情況,例如在預先計算的核函數中,必須儲存此資料以供預測方法使用。

參數

X

形狀為 (n_samples, n_features) 的類陣列

y

形狀為 (n_samples,) 的類陣列

kwargs

選擇性的資料相關參數

樣本數,即 X.shape[0] 應與 y.shape[0] 相同。如果未滿足此要求,則應引發 ValueError 型別的例外。

在非監督式學習的情況下,可能會忽略 y。但是,為了使估算器能夠用作可混合監督式和非監督式轉換器的管線的一部分,即使是非監督式估算器也需要在第二個位置接受 y=None 關鍵字引數,估算器會直接忽略它。基於相同的原因,如果實作了 fit_predictfit_transformscorepartial_fit 方法,則需要在第二個位置接受 y 引數。

該方法應傳回物件 (self)。此模式對於能夠在 IPython 會話中實作快速單行程式碼非常有用,例如

y_predicted = SGDClassifier(alpha=10).fit(X_train, y_train).predict(X_test)

根據演算法的性質,fit 有時也可以接受其他關鍵字引數。但是,在存取資料之前可以指派值的任何參數都應是 __init__ 關鍵字引數。理想情況下,**fit 參數應限制為直接與資料相關的變數**。例如,從資料矩陣 X 預先計算的 Gram 矩陣或親和力矩陣是與資料相關的。容差停止條件 tol 並非直接與資料相關(儘管根據某些評分函數的最佳值可能是)。

當呼叫 fit 時,任何先前對 fit 的呼叫都應被忽略。一般而言,呼叫 estimator.fit(X1),然後呼叫 estimator.fit(X2),應與僅呼叫 estimator.fit(X2) 相同。然而,當 fit 依賴於某些隨機過程時,這在實務上可能不成立,請參閱 random_state。此規則的另一個例外是當超參數 warm_start 對於支援它的估算器設定為 True 時。warm_start=True 表示會重複使用估算器可訓練參數的先前狀態,而不是使用預設的初始化策略。

估計的屬性#

根據 scikit-learn 的慣例,您希望作為公開屬性向使用者公開,並且已從資料中估計或學習到的屬性,其名稱必須始終以尾隨底線結尾,例如,在呼叫 fit 後,某些迴歸估算器的係數會儲存在 coef_ 屬性中。同樣地,您在過程中學習到,但不想向使用者公開的屬性,應該以開頭底線命名,例如 _intermediate_coefs。您需要將第一組(帶有尾隨底線)記錄為「屬性」,而不需要記錄第二組(帶有開頭底線)。

當您第二次呼叫 fit 時,預期估計的屬性會被覆蓋。

通用屬性#

期望表格輸入的估算器應該在 fit 時設定 n_features_in_ 屬性,以指示估算器預期後續呼叫 predicttransform 時的特徵數量。詳情請參閱 SLEP010

同樣地,如果估算器被給予如 pandas 或 polars 的資料框,它們應設定 feature_names_in_ 屬性以指示輸入資料的特徵名稱,詳情請參閱 SLEP007。使用 validate_data 會自動為您設定這些屬性。

自行建立估算器#

如果您想實作與 scikit-learn 相容的新估算器,除了上述概述的 scikit-learn API 之外,您還應該了解 scikit-learn 的幾個內部結構。您可以透過在實例上執行 check_estimator 來檢查您的估算器是否符合 scikit-learn 介面和標準。parametrize_with_checks pytest 修飾器也可以使用(有關詳細資訊以及與 pytest 可能的互動,請參閱其 docstring)。

>>> from sklearn.utils.estimator_checks import check_estimator
>>> from sklearn.tree import DecisionTreeClassifier
>>> check_estimator(DecisionTreeClassifier())  # passes

使類別與 scikit-learn 估算器介面相容的主要動機可能是您希望將其與模型評估和選擇工具(例如 GridSearchCVPipeline)一起使用。

在詳細說明下面所需的介面之前,我們先描述兩種更輕鬆地實現正確介面的方法。

您可以檢查上面的估算器是否通過所有常見檢查

>>> from sklearn.utils.estimator_checks import check_estimator
>>> check_estimator(TemplateClassifier())  # passes

get_params 和 set_params#

所有 scikit-learn 估算器都具有 get_paramsset_params 函式。

get_params 函式不接受任何引數,並傳回估算器的 __init__ 參數及其值的字典。

它接受一個關鍵字引數 deep,它接收一個布林值,用於確定方法是否應傳回子估算器的參數(僅與 meta-estimator 相關)。deep 的預設值為 True。例如,考慮以下估算器

>>> from sklearn.base import BaseEstimator
>>> from sklearn.linear_model import LogisticRegression
>>> class MyEstimator(BaseEstimator):
...     def __init__(self, subestimator=None, my_extra_param="random"):
...         self.subestimator = subestimator
...         self.my_extra_param = my_extra_param

參數 deep 控制是否應報告 subestimator 的參數。因此,當 deep=True 時,輸出將為

>>> my_estimator = MyEstimator(subestimator=LogisticRegression())
>>> for param, value in my_estimator.get_params(deep=True).items():
...     print(f"{param} -> {value}")
my_extra_param -> random
subestimator__C -> 1.0
subestimator__class_weight -> None
subestimator__dual -> False
subestimator__fit_intercept -> True
subestimator__intercept_scaling -> 1
subestimator__l1_ratio -> None
subestimator__max_iter -> 100
subestimator__multi_class -> deprecated
subestimator__n_jobs -> None
subestimator__penalty -> l2
subestimator__random_state -> None
subestimator__solver -> lbfgs
subestimator__tol -> 0.0001
subestimator__verbose -> 0
subestimator__warm_start -> False
subestimator -> LogisticRegression()

如果 meta-estimator 採用多個子估算器,則這些子估算器通常具有名稱(例如,Pipeline 物件中的具名步驟),在這種情況下,金鑰應該變成 <name>__C<name>__class_weight 等。

deep=False 時,輸出將為

>>> for param, value in my_estimator.get_params(deep=False).items():
...     print(f"{param} -> {value}")
my_extra_param -> random
subestimator -> LogisticRegression()

另一方面,set_params__init__ 的參數作為關鍵字引數,將它們解壓縮為 'parameter': value 形式的字典,並使用此字典設定估算器的參數。它會傳回估算器本身。

set_params 函式用於在網格搜尋期間設定參數。

複製#

如前所述,當建構函式引數可變時,應先複製它們,然後再修改它們。這也適用於作為估算器的建構函式引數。這就是為什麼 meta-estimator(例如 GridSearchCV)會在修改給定的估算器之前建立其副本。

然而,在 scikit-learn 中,當我們複製估算器時,我們會得到一個未擬合的估算器,其中只複製建構函式引數(有一些例外,例如與某些內部機制(例如中繼資料路由)相關的屬性)。

負責此行為的函式是 clone

估計器可以透過覆寫 base.BaseEstimator.__sklearn_clone__ 方法來自訂 base.clone 的行為。__sklearn_clone__ 必須傳回估計器的實例。__sklearn_clone__ 在估計器需要在 base.clone 在該估計器上被呼叫時保留某些狀態時很有用。例如,FrozenEstimator 就有用到這個方法。

估計器類型#

在簡單的估計器(相對於元估計器)中,最常見的類型是轉換器、分類器、迴歸器和分群演算法。

轉換器 (Transformers) 繼承自 TransformerMixin,並實作 transform 方法。這些是接收輸入並以某種方式轉換輸入的估計器。請注意,它們不應更改輸入樣本的數量,且 transform 的輸出應以相同的給定順序對應於其輸入樣本。

迴歸器 (Regressors) 繼承自 RegressorMixin,並實作 predict 方法。它們應該在其 fit 方法中接受數值的 y。迴歸器在其 score 方法中預設使用 r2_score

分類器 (Classifiers) 繼承自 ClassifierMixin。如果適用,分類器可以實作 decision_function 以傳回原始決策值,predict 可以根據這些值做出決策。如果支援計算機率,分類器也可以實作 predict_probapredict_log_proba

分類器應接受 y(目標)參數給 fit,這些參數是字串或整數的序列(列表、陣列)。它們不應假設類別標籤是連續的整數範圍;相反地,它們應將類別列表儲存在 classes_ 屬性或特性中。此屬性中類別標籤的順序應與 predict_probapredict_log_probadecision_function 傳回其值的順序相符。實現此目的最簡單的方法是放入

self.classes_, y = np.unique(y, return_inverse=True)

fit 中。這會傳回一個新的 y,其中包含類別索引,而不是標籤,範圍在 [0, n_classes) 之間。

分類器的 predict 方法應傳回包含 classes_ 中類別標籤的陣列。在實作 decision_function 的分類器中,可以使用

def predict(self, X):
    D = self.decision_function(X)
    return self.classes_[np.argmax(D, axis=1)]

multiclass 模組包含用於處理多類別和多標籤問題的有用函式。

分群演算法 (Clustering algorithms) 繼承自 ClusterMixin。理想情況下,它們應該在其 fit 方法中接受 y 參數,但應忽略該參數。分群演算法應設定 labels_ 屬性,以儲存指派給每個樣本的標籤。如果適用,它們還可以實作 predict 方法,傳回指派給新給定樣本的標籤。

如果需要檢查給定估計器的類型,例如在元估計器中,可以檢查給定物件是否為轉換器實作 transform 方法,否則可以使用輔助函式,例如 is_classifieris_regressor

估計器標籤#

注意

Scikit-learn 在 0.21 版中引入了估計器標籤作為私有 API,主要用於測試中。然而,這些標籤隨著時間推移而擴展,許多第三方開發人員也需要使用它們。因此,在 1.6 版中,標籤的 API 進行了修改,並作為公共 API 公開。

估計器標籤是對估計器的註解,允許以程式方式檢查它們的功能,例如稀疏矩陣支援、支援的輸出類型和支援的方法。估計器標籤是由方法 __sklearn_tags__ 傳回的 Tags 的實例。這些標籤在不同的地方使用,例如 is_regressor 或由 check_estimatorparametrize_with_checks 執行的常見檢查中,標籤決定要執行哪些檢查以及什麼輸入資料是適當的。標籤可以取決於估計器參數甚至系統架構,並且通常只能在執行時確定,因此是實例屬性,而不是類別屬性。有關個別標籤的詳細資訊,請參閱 Tags

每個標籤的預設值不太可能符合特定估計器的需求。您可以透過定義一個 __sklearn_tags__() 方法來變更預設值,該方法會傳回估計器標籤的新值。例如

class MyMultiOutputEstimator(BaseEstimator):

    def __sklearn_tags__(self):
        tags = super().__sklearn_tags__()
        tags.target_tags.single_output = False
        tags.non_deterministic = True
        return tags

如果您希望向現有集合新增新標籤,您可以建立 Tags 的新子類別。請注意,您在子類別中新增的所有屬性都需要有預設值。它可以採用以下形式

from dataclasses import dataclass, asdict

@dataclass
class MyTags(Tags):
    my_tag: bool = True

class MyEstimator(BaseEstimator):
    def __sklearn_tags__(self):
        tags_orig = super().__sklearn_tags__()
        as_dict = {
            field.name: getattr(tags_orig, field.name)
            for field in fields(tags_orig)
        }
        tags = MyTags(**as_dict)
        tags.my_tag = True
        return tags

set_output 的開發人員 API#

使用 SLEP018,scikit-learn 引入了 set_output API,用於設定轉換器以輸出 pandas DataFrames。set_output API 會在轉換器定義 get_feature_names_out 並繼承 base.TransformerMixin 時自動定義。get_feature_names_out 用於取得 pandas 輸出的欄名稱。

base.OneToOneFeatureMixinbase.ClassNamePrefixFeaturesOutMixin 是定義 get_feature_names_out 的有用混合類別。base.OneToOneFeatureMixin 在轉換器的輸入特徵和輸出特徵之間具有一對一對應關係時很有用,例如 StandardScalerbase.ClassNamePrefixFeaturesOutMixin 在轉換器需要產生自己的輸出特徵名稱時很有用,例如 PCA

您可以在定義自訂子類別時設定 auto_wrap_output_keys=None 來選擇退出 set_output API

class MyTransformer(TransformerMixin, BaseEstimator, auto_wrap_output_keys=None):

    def fit(self, X, y=None):
        return self
    def transform(self, X, y=None):
        return X
    def get_feature_names_out(self, input_features=None):
        ...

auto_wrap_output_keys 的預設值為 ("transform",),它會自動包裝 fit_transformtransformTransformerMixin 使用 __init_subclass__ 機制來使用 auto_wrap_output_keys 並將所有其他關鍵字參數傳遞給它的父類別。父類別的 __init_subclass__ **不應** 依賴 auto_wrap_output_keys

對於在 transform 中傳回多個陣列的轉換器,自動包裝只會包裝第一個陣列,而不會改變其他陣列。

有關如何使用 API 的範例,請參閱 Introducing the set_output API

用於 check_is_fitted 的開發者 API#

預設情況下,check_is_fitted 會檢查實例中是否有任何帶有底線結尾的屬性,例如 coef_。估計器可以透過實作一個不帶輸入並返回布林值的 __sklearn_is_fitted__ 方法來更改此行為。如果此方法存在,check_is_fitted 只會返回其輸出。

請參閱 __sklearn_is_fitted__ 作為開發者 API,了解如何使用此 API 的範例。

用於 HTML 表示的開發者 API#

警告

HTML 表示 API 是實驗性的,API 可能會變更。

繼承自 BaseEstimator 的估計器,會在互動式程式設計環境(例如 Jupyter 筆記本)中顯示它們的 HTML 表示。例如,我們可以顯示這個 HTML 圖表

from sklearn.base import BaseEstimator

BaseEstimator()

原始的 HTML 表示是透過在估計器實例上調用函數 estimator_html_repr 來取得的。

若要自訂連結到估計器文件 (即點擊「?」圖示時) 的 URL,請覆寫 _doc_link_module_doc_link_template 屬性。此外,您可以提供 _doc_link_url_param_generator 方法。將 _doc_link_module 設定為包含您的估計器的 (頂層) 模組名稱。如果該值與頂層模組名稱不符,則 HTML 表示將不會包含連結到文件的連結。對於 scikit-learn 估計器,此設定為 "sklearn"

_doc_link_template 用於建構最終 URL。預設情況下,它可以包含兩個變數:estimator_module (包含估計器的模組完整名稱) 和 estimator_name (估計器的類別名稱)。如果您需要更多變數,您應該實作 _doc_link_url_param_generator 方法,該方法應該返回一個包含變數及其值的字典。此字典將用於呈現 _doc_link_template

程式碼撰寫準則#

以下是一些關於如何在 scikit-learn 中包含新程式碼的撰寫準則,這些準則可能也適合在外部專案中採用。當然,會有特殊情況,這些規則也會有例外。然而,在提交新程式碼時遵循這些規則可以讓審查更容易,因此可以更快地整合新程式碼。

格式一致的程式碼可以更容易地分享程式碼所有權。scikit-learn 專案嘗試密切遵循 PEP8 中詳述的官方 Python 指南,其中詳細說明了程式碼應該如何格式化和縮排。請閱讀並遵循它。

此外,我們添加了以下準則

  • 在非類別名稱中使用底線來分隔單字:n_samples 而不是 nsamples

  • 避免在同一行上使用多個語句。在控制流程語句 (if/for) 後面偏好換行符號。

  • 對於 scikit-learn 內部的參考,請使用相對匯入。

  • 單元測試是前一條規則的例外;它們應該使用絕對匯入,就像客戶端程式碼一樣。一個推論是,如果 sklearn.foo 匯出一個在 sklearn.foo.bar.baz 中實作的類別或函式,則測試應該從 sklearn.foo 匯入它。

  • 請不要使用 import * 在任何情況下官方 Python 建議認為它是有害的。它會使程式碼更難以閱讀,因為符號的來源不再被明確引用,但最重要的是,它會阻止使用靜態分析工具,例如 pyflakes 來自動尋找 scikit-learn 中的錯誤。

  • 在您的所有文件字串中使用 numpy 文件字串標準

您可以在這裡找到我們喜歡的程式碼好範例。

輸入驗證#

模組 sklearn.utils 包含用於執行輸入驗證和轉換的各種函式。有時,np.asarray 足以進行驗證;請不要使用 np.asanyarraynp.atleast_2d,因為這些會讓 NumPy 的 np.matrix 通過,而 np.matrix 有不同的 API(例如,* 表示 np.matrix 上的點積,但在 np.ndarray 上是 Hadamard 乘積)。

在其他情況下,請務必在傳遞給 scikit-learn API 函式的任何類似陣列的引數上呼叫 check_array。要使用的確切參數主要取決於是否必須接受哪些 scipy.sparse 矩陣。

如需更多資訊,請參閱開發人員實用工具頁面。

隨機數#

如果您的程式碼依賴隨機數產生器,請不要使用 numpy.random.random() 或類似的常式。為了確保錯誤檢查的可重複性,常式應接受關鍵字 random_state,並使用它來建構 numpy.random.RandomState 物件。請參閱 sklearn.utils.check_random_state 中的 開發人員實用工具

以下是一個使用上述某些準則的程式碼簡單範例

from sklearn.utils import check_array, check_random_state

def choose_random_sample(X, random_state=0):
    """Choose a random point from X.

    Parameters
    ----------
    X : array-like of shape (n_samples, n_features)
        An array representing the data.
    random_state : int or RandomState instance, default=0
        The seed of the pseudo random number generator that selects a
        random sample. Pass an int for reproducible output across multiple
        function calls.
        See :term:`Glossary <random_state>`.

    Returns
    -------
    x : ndarray of shape (n_features,)
        A random point selected from X.
    """
    X = check_array(X)
    random_state = check_random_state(random_state)
    i = random_state.randint(X.shape[0])
    return X[i]

如果您在估計器而不是獨立函式中使用隨機性,則會適用一些額外的準則。

首先,估計器應該在其 __init__ 中採用一個 random_state 引數,預設值為 None。它應該將該引數的值未修改地儲存在屬性 random_state 中。fit 可以呼叫該屬性上的 check_random_state 以取得實際的隨機數產生器。如果由於某些原因,需要在 fit 之後使用隨機性,則應將 RNG 儲存在屬性 random_state_ 中。以下範例應該可以清楚地說明這一點

class GaussianNoise(BaseEstimator, TransformerMixin):
    """This estimator ignores its input and returns random Gaussian noise.

    It also does not adhere to all scikit-learn conventions,
    but showcases how to handle randomness.
    """

    def __init__(self, n_components=100, random_state=None):
        self.random_state = random_state
        self.n_components = n_components

    # the arguments are ignored anyway, so we make them optional
    def fit(self, X=None, y=None):
        self.random_state_ = check_random_state(self.random_state)

    def transform(self, X):
        n_samples = X.shape[0]
        return self.random_state_.randn(n_samples, self.n_components)

這種設定的原因是可重複性:當一個估計器對相同的資料 fit 兩次時,它應該每次都產生相同的模型,因此驗證是在 fit 而不是 __init__ 中進行。

測試中的數值斷言#

在斷言連續數值的陣列的準相等性時,請使用 sklearn.utils._testing.assert_allclose

相對容差會自動從提供的陣列資料類型中推斷 (特別是對於 float32 和 float64 資料類型),但您可以透過 rtol 覆寫。

在比較零元素陣列時,請透過 atol 為絕對容差提供非零值。

如需更多資訊,請參閱 sklearn.utils._testing.assert_allclose 的文件字串。