開發 scikit-learn 估算器#
無論您是提議將估算器納入 scikit-learn 中、開發與 scikit-learn 相容的獨立套件,還是為您自己的專案實作自訂元件,本章將詳細說明如何開發可安全地與 scikit-learn 管線和模型選擇工具互動的物件。
本節詳細說明您應該使用和實作的 scikit-learn 相容估算器的公開 API。在 scikit-learn 內部,我們會實驗並使用一些私有工具,我們的目標始終是讓它們在足夠穩定後公開,以便您也可以在自己的專案中使用它們。
scikit-learn 物件的 API#
估算器主要有兩種型別。您可以將第一組視為簡單估算器,其中包含大多數估算器,例如 LogisticRegression
或 RandomForestClassifier
。第二組是元估算器,也就是封裝其他估算器的估算器。Pipeline
和 GridSearchCV
是元估算器的兩個範例。
這裡我們先從一些詞彙開始,然後說明如何實作您自己的估算器。
scikit-learn API 的元素在常用術語和 API 元素詞彙表中有更明確的說明。
不同的物件#
scikit-learn 中的主要物件有(一個類別可以實作多個介面)
- 估算器:
基礎物件,實作
fit
方法以從資料中學習,可以是estimator = estimator.fit(data, targets)
或
estimator = estimator.fit(data)
- 預測器:
對於監督式學習或某些非監督式問題,實作
prediction = predictor.predict(data)
分類演算法通常也會提供一種量化預測確定性的方法,可以使用
decision_function
或predict_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_params
、get_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
。
請注意,模型是使用 X
和 y
擬合的,但物件不保留對 X
和 y
的參考。但是,有一些例外情況,例如在預先計算的核函數中,必須儲存此資料以供預測方法使用。
參數 |
|
---|---|
X |
形狀為 (n_samples, n_features) 的類陣列 |
y |
形狀為 (n_samples,) 的類陣列 |
kwargs |
選擇性的資料相關參數 |
樣本數,即 X.shape[0]
應與 y.shape[0]
相同。如果未滿足此要求,則應引發 ValueError
型別的例外。
在非監督式學習的情況下,可能會忽略 y
。但是,為了使估算器能夠用作可混合監督式和非監督式轉換器的管線的一部分,即使是非監督式估算器也需要在第二個位置接受 y=None
關鍵字引數,估算器會直接忽略它。基於相同的原因,如果實作了 fit_predict
、fit_transform
、score
和 partial_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_
屬性,以指示估算器預期後續呼叫 predict 或 transform 時的特徵數量。詳情請參閱 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 估算器介面相容的主要動機可能是您希望將其與模型評估和選擇工具(例如 GridSearchCV
和 Pipeline
)一起使用。
在詳細說明下面所需的介面之前,我們先描述兩種更輕鬆地實現正確介面的方法。
您可以檢查上面的估算器是否通過所有常見檢查
>>> from sklearn.utils.estimator_checks import check_estimator
>>> check_estimator(TemplateClassifier()) # passes
get_params 和 set_params#
所有 scikit-learn 估算器都具有 get_params
和 set_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_proba
和 predict_log_proba
。
分類器應接受 y
(目標)參數給 fit
,這些參數是字串或整數的序列(列表、陣列)。它們不應假設類別標籤是連續的整數範圍;相反地,它們應將類別列表儲存在 classes_
屬性或特性中。此屬性中類別標籤的順序應與 predict_proba
、predict_log_proba
和 decision_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_classifier
或 is_regressor
。
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.OneToOneFeatureMixin
和 base.ClassNamePrefixFeaturesOutMixin
是定義 get_feature_names_out 的有用混合類別。base.OneToOneFeatureMixin
在轉換器的輸入特徵和輸出特徵之間具有一對一對應關係時很有用,例如 StandardScaler
。base.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_transform
和 transform
。TransformerMixin
使用 __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.asanyarray
或 np.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
的文件字串。