开发 scikit-learn 估计器#
无论您是提议将一个估计器纳入 scikit-learn,还是开发一个与 scikit-learn 兼容的独立包,亦或是为自己的项目实现自定义组件,本章都将详细介绍如何开发能够安全地与 scikit-learn 的流水线(pipelines)和模型选择工具交互的对象。
本节详细介绍了您应该为 scikit-learn 兼容的估计器使用的公共 API。在 scikit-learn 内部,我们进行实验并使用一些私有工具,我们的目标是一旦这些工具足够稳定就将其公开,以便您也能在自己的项目中使用它们。
scikit-learn 对象的 API#
有两种主要的估计器类型。第一类是简单估计器,包括大多数估计器,例如 LogisticRegression 或 RandomForestClassifier。第二类是元估计器(meta-estimators),它们是包装了其他估计器的估计器。 Pipeline 和 GridSearchCV 是元估计器的两个例子。
这里我们先介绍一些术语,然后说明如何实现您自己的估计器。
scikit-learn 对象的 API 元素在 通用术语和 API 元素词汇表 中有更明确的描述。
不同的对象#
scikit-learn 中的主要对象是(一个类可以实现多个接口)
- 估计器(Estimator):
基础对象,实现一个
fit方法来从数据中学习,或者estimator = estimator.fit(data, targets)
或者
estimator = estimator.fit(data)
- 预测器(Predictor):
用于监督学习,或一些无监督问题,实现
prediction = predictor.predict(data)
分类算法通常还提供一种方法来量化预测的确定性,无论是使用
decision_function还是predict_proba。probability = predictor.predict_proba(data)
- 转换器(Transformer):
用于以监督或无监督的方式修改数据(例如,通过添加、更改或删除列,但不添加或删除行)。实现
new_data = transformer.transform(data)
当拟合(fitting)和转换(transforming)一起执行比单独执行效率更高时,实现
new_data = transformer.fit_transform(data)
- 模型(Model):
一个能够给出 拟合优度(goodness of fit)度量或未知数据似然的模型,实现(越高越好)
score = model.score(data)
估计器(Estimators)#
API 中有一个占主导地位的对象:估计器。估计器是一个基于一些训练数据拟合模型并能够推断新数据属性的对象。它可以是一个分类器或回归器。所有估计器都实现 fit 方法。
estimator.fit(X, y)
在估计器实现的所有方法中,fit 通常是您自己需要实现的方法。其他方法,如 set_params, get_params 等,都在 BaseEstimator 中实现,您应该继承它。您可能需要继承更多的 mixins,我们稍后会解释。
实例化(Instantiation)#
这涉及对象的创建。对象的 __init__ 方法可能会接受常量作为参数,这些参数决定了估计器的行为(例如 SGDClassifier 中的 alpha 常量)。然而,它不应将实际的训练数据作为参数,因为这留给了 fit() 方法。
clf2 = SGDClassifier(alpha=2.3)
clf3 = SGDClassifier([[1, 2], [2, 3]], [-1, 1]) # WRONG!
理想情况下,__init__ 接受的所有参数都应该是带有默认值的关键字参数。换句话说,用户应该能够实例化一个估计器而不传递任何参数给它。在某些情况下,如果某个参数没有合理的默认值,可以不设置默认值。在 scikit-learn 内部,我们只有在一些元估计器中,参数 sub-estimator(s) 是必需参数。
大多数参数对应于描述模型或估计器试图解决的优化问题的超参数。其他参数可能定义估计器的行为,例如定义用于存储某些数据的缓存位置。这些初始参数(或配置项)总是会被估计器记住。另请注意,它们不应在“Attributes”部分下记录,而应在该估计器的“Parameters”部分下记录。
此外,__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__ 方法中设置。关于非 init 参数的属性的更多细节将在稍后介绍。
拟合(Fitting)#
您接下来可能想做的事情是估计模型中的一些参数。这在 fit() 方法中实现,并且是训练发生的地方。例如,这是您计算学习或估计线性模型系数的地方。
fit() 方法将训练数据作为参数,这在无监督学习的情况下可以是一个数组,在监督学习的情况下可以是两个数组。其他随训练数据一起出现的元数据,如 sample_weight,也可以作为关键字参数传递给 fit。
请注意,模型是使用 X 和 y 进行拟合的,但对象本身不保留对 X 和 y 的引用。但是,有一些例外情况,例如在预计算核(precomputed kernels)的情况下,这些数据必须存储起来供 predict 方法使用。
参数 |
|
|---|---|
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 参数应限制为直接与数据相关的变量。例如,Gram 矩阵或亲和矩阵,它们是从数据矩阵 X 中预计算出来的,是数据相关的。容差停止条件 tol 不是直接数据相关的(尽管根据某些评分函数,最优值可能是)。
当调用 fit 时,任何先前的 fit 调用都应被忽略。总的来说,调用 estimator.fit(X1) 然后 estimator.fit(X2) 应该等同于只调用 estimator.fit(X2)。然而,在 fit 依赖于某些随机过程的情况下,这在实践中可能不成立,请参阅 random_state。此规则的另一个例外是当超参数 warm_start 设置为 True(对于支持该选项的估计器)时。warm_start=True 意味着重用估计器的训练参数的先前状态,而不是使用默认的初始化策略。
估计的属性(Estimated Attributes)#
根据 scikit-learn 的约定,您希望向用户公开为公共属性并且已从数据中估计或学习到的属性,其名称必须始终以_(下划线)结尾,例如,某些回归估计器的系数将在调用 fit 后存储在 coef_ 属性中。类似地,您在过程中学习到并希望存储但不向用户公开的属性,应以_(下划线)开头,例如 _intermediate_coefs。您需要将第一组(以下划线结尾)记录在“Attributes”部分,而无需记录第二组(以下划线开头)。
当您第二次调用 fit 时,估计的属性预期会被覆盖。
通用属性(Universal attributes)#
期望表格输入(tabular input)的估计器应在 fit 时设置 n_features_in_ 属性,以指示估计器在后续调用 predict 或 transform 时期望的特征数量。有关详细信息,请参阅 SLEP010。
类似地,如果估计器接收到如 pandas 或 polars 的数据框(dataframes),它们应设置 feature_names_in_ 属性,以指示输入数据的特征名称,详细信息请参阅 SLEP007。使用 validate_data 将会自动为您设置这些属性。
自己实现估计器(Rolling your own estimator)#
如果您想实现一个与 scikit-learn 兼容的新估计器,除了上述 scikit-learn API 外,您还需要了解 scikit-learn 的一些内部机制。您可以通过在估计器实例上运行 check_estimator 来检查您的估计器是否符合 scikit-learn 的接口和标准。 parametrize_with_checks pytest 装饰器也可以使用(请参阅其文档字符串以获取详细信息和与 pytest 的可能交互)。
>>> 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,该参数接收一个布尔值,决定方法是否应返回子估计器的参数(仅适用于元估计器)。 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 -> 0.0
subestimator__max_iter -> 100
subestimator__n_jobs -> None
subestimator__penalty -> deprecated
subestimator__random_state -> None
subestimator__solver -> lbfgs
subestimator__tol -> 0.0001
subestimator__verbose -> 0
subestimator__warm_start -> False
subestimator -> LogisticRegression()
如果元估计器接受多个子估计器,通常这些子估计器有名称(例如 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 函数用于在网格搜索等过程中设置参数。
克隆(Cloning)#
如前所述,当构造函数参数是可变的时,在修改它们之前应该对其进行复制。这也适用于作为估计器的构造函数参数。这就是为什么像 GridSearchCV 这样的元估计器在修改给定估计器之前会创建一个副本。
然而,在 scikit-learn 中,当我们复制一个估计器时,我们会得到一个未拟合的估计器,其中只有构造函数参数被复制(有一些例外,例如与某些内部机制相关的属性,如元数据路由)。
负责此行为的函数是 clone。
估计器可以通过覆盖 base.BaseEstimator.__sklearn_clone__ 方法来定制 base.clone 的行为。__sklearn_clone__ 必须返回一个估计器实例。__sklearn_clone__ 在估计器需要保留某些状态时很有用,例如当 base.clone 在估计器上调用时。例如,FrozenEstimator 就利用了这一点。
估计器类型(Estimator types)#
在简单估计器(与元估计器相对)中,最常见的类型是转换器(transformers)、分类器(classifiers)、回归器(regressors)和聚类算法(clustering algorithms)。
转换器继承自 TransformerMixin,并实现 transform 方法。这些是接受输入并以某种方式转换它的估计器。请注意,它们绝不应该改变输入样本的数量,并且 transform 的输出应与输入样本以相同的顺序对应。
回归器继承自 RegressorMixin,并实现 predict 方法。它们应该在其 fit 方法中接受数值 y。回归器在其 score 方法中默认使用 r2_score。
分类器继承自 ClassifierMixin。如果适用,分类器可以实现 decision_function 来返回原始决策值,基于这些值 predict 可以做出决策。如果支持计算概率,分类器还可以实现 predict_proba 和 predict_log_proba。
分类器应该接受 y(目标)参数到 fit 方法中,这些参数是字符串或整数的序列(列表、数组)。它们不应假设类别标签是连续的整数范围;相反,它们应该将类别列表存储在 classes_ 属性或属性中。此属性中的类别标签顺序应与 predict_proba, predict_log_proba 和 decision_function 返回值的顺序相匹配。最简单的方法是在 fit 中进行:
self.classes_, y = np.unique(y, return_inverse=True)
这段代码会返回一个新的 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 的模块包含用于处理多类(multiclass)和多标签(multilabel)问题的有用函数。
聚类算法继承自 ClusterMixin。理想情况下,它们应该在其 fit 方法中接受一个 y 参数,但该参数应被忽略。聚类算法应该设置一个 labels_ 属性,存储分配给每个样本的标签。如果适用,它们还可以实现一个 predict 方法,返回分配给新样本的标签。
如果需要检查给定估计器的类型,例如在元估计器中,可以检查给定对象是否实现了 transform 方法(对于转换器),否则可以使用辅助函数,如 is_classifier 或 is_regressor。
set_output 的开发者 API#
通过 SLEP018,scikit-learn 引入了 set_output API,用于配置转换器输出 pandas DataFrames。如果转换器定义了 get_feature_names_out 并继承了 base.TransformerMixin,则 set_output API 会被自动定义。get_feature_names_out 用于获取 pandas 输出的列名。
base.OneToOneFeatureMixin 和 base.ClassNamePrefixFeaturesOutMixin 是定义 get_feature_names_out 的有用 mixins。当转换器具有输入特征和输出特征之间的一对一对应关系时,例如 StandardScaler,base.OneToOneFeatureMixin 很有用。当转换器需要生成自己的输出特征名称时,例如 PCA,base.ClassNamePrefixFeaturesOutMixin 很有用。
您可以通过在定义自定义子类时设置 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 的示例,请参阅 介绍 set_output API。
check_is_fitted 的开发者 API#
默认情况下,check_is_fitted 检查实例中是否有任何以_(下划线)结尾的属性,例如 coef_。估计器可以通过实现一个不接受任何输入并返回布尔值的 __sklearn_is_fitted__ 方法来改变其行为。如果存在此方法,check_is_fitted 将直接返回其输出。
有关使用该 API 的示例,请参阅 __sklearn_is_fitted__ 作为开发者 API。
HTML 表示的开发者 API#
警告
HTML 表示 API 是实验性的,API 可能会发生变化。
继承自 BaseEstimator 的估计器在其 HTML 表示中显示了它们自身在交互式编程环境(如 Jupyter notebook)中的图示。例如,我们可以显示这个 HTML 图:
from sklearn.base import BaseEstimator
BaseEstimator()
通过调用估计器实例上的 estimator_html_repr 函数来获取原始 HTML 表示。
要自定义指向估计器文档的 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。
编码指南(Coding guidelines)#
以下是一些关于如何为纳入 scikit-learn 编写新代码的指南,这些指南也可能适用于外部项目。当然,存在特殊情况,这些规则也会有例外。但是,在提交新代码时遵循这些规则可以使代码审查更容易,从而更快地集成新代码。
统一格式化的代码使代码所有权更容易共享。scikit-learn 项目试图严格遵循 PEP8 中详细说明的官方 Python 指南,该指南详细介绍了代码应如何格式化和缩进。请阅读并遵循它。
此外,我们添加了以下指南:
在非类名中使用下划线分隔单词:
n_samples而不是nsamples。避免一行有多个语句。在控制流语句(
if/for)后换行。使用绝对导入
单元测试应使用与客户端代码完全相同的导入方式。如果
sklearn.foo导出了一个在sklearn.foo.bar.baz中实现的类或函数,则测试应从sklearn.foo导入它。在任何情况下都请不要使用
import *。它被 官方 Python 建议 视为有害的。它使代码更难阅读,因为符号的来源不再明确引用,但最重要的是,它阻止了使用像 pyflakes 这样的静态分析工具来自动查找 scikit-learn 中的错误。在所有文档字符串中使用 numpy 文档字符串标准。
一个我们喜欢的代码的优秀示例可以在 这里 找到。
输入验证(Input validation)#
模块 sklearn.utils 包含用于执行输入验证和转换的各种函数。有时,np.asarray 足以进行验证;不要使用 np.asanyarray 或 np.atleast_2d,因为这些允许 NumPy 的 np.matrix 通过,它具有不同的 API(例如,* 在 np.matrix 上表示点积,但在 np.ndarray 上表示逐元素乘积)。
在其他情况下,请务必对传递给 scikit-learn API 函数的任何类似数组的参数调用 check_array。要使用的确切参数主要取决于是否必须接受 scipy.sparse 矩阵以及接受哪种矩阵。
有关更多信息,请参阅 开发者工具 页面。
随机数(Random Numbers)#
如果您的代码依赖于随机数生成器,请不要使用 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__ 中。
测试中的数值断言(Numerical assertions in tests)#
当断言连续值数组的近似相等性时,请使用 sklearn.utils._testing.assert_allclose。
相对容差会自动从提供的数组数据类型(特别是 float32 和 float64 数据类型)中推断出来,但您可以通过 rtol 来覆盖它。
在比较零元素数组时,请通过 atol 提供一个非零值作为绝对容差。
有关更多信息,请参阅 sklearn.utils._testing.assert_allclose 的文档字符串。