Spec-Zone.ru › scikit-learn

Примечание

Перейти к концу для скачивания полного примера кода. или для запуска этого примера в вашем браузере через JupyterLite или Binder

Маршрутизация метаданных

В этом документе показано, как можно использовать механизм маршрутизации метаданных в scikit-learn для маршрутизации метаданных к оценщикам, оценщикам и разделителям кросс-валидации, потребляющим их.

Для лучшего понимания следующего документа нам нужно ввести два понятия: маршрутизаторы и потребители. Маршрутизатор — это объект, который передает некоторые данные и метаданные другим объектам. В большинстве случаев маршрутизатор является мета-оценщиком, т. е. оценщиком, принимающим другой оценщик в качестве параметра. Функция, такая как sklearn.model_selection.cross_validate, которая принимает оценщик в качестве параметра и передает данные и метаданные, также является маршрутизатором.

Потребитель, с другой стороны, — это объект, который принимает и использует заданные метаданные. Например, оценщик, учитывающий sample_weight в своем методе подгонки, является потребителем sample_weight.

Объект может быть одновременно маршрутизатором и потребителем. Например, мета-оценщик может учитывать sample_weight в определенных вычислениях, но также может перенаправить его на базовый оценщик.

Сначала несколько импортов и случайные данные для остальной части скрипта.

# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause
import warnings
from pprint import pprint

import numpy as np

from sklearn import set_config
from sklearn.base import (
    BaseEstimator,
    ClassifierMixin,
    MetaEstimatorMixin,
    RegressorMixin,
    TransformerMixin,
    clone,
)
from sklearn.linear_model import LinearRegression
from sklearn.utils import metadata_routing
from sklearn.utils.metadata_routing import (
    MetadataRouter,
    MethodMapping,
    get_routing_for_object,
    process_routing,
)
from sklearn.utils.validation import check_is_fitted

n_samples, n_features = 100, 4
rng = np.random.RandomState(42)
X = rng.rand(n_samples, n_features)
y = rng.randint(0, 2, size=n_samples)
my_groups = rng.randint(0, 10, size=n_samples)
my_weights = rng.rand(n_samples)
my_other_weights = rng.rand(n_samples)

Маршрутизация метаданных доступна только при явном включении:

set_config(enable_metadata_routing=True)

Эта служебная функция является фиктивной, чтобы проверить, передаются ли метаданные:

def check_metadata(obj, **kwargs):
    for key, value in kwargs.items():
        if value is not None:
            print(
                f"Received {key} of length = {len(value)} in {obj.__class__.__name__}."
            )
        else:
            print(f"{key} is None in {obj.__class__.__name__}.")

Служебная функция для красивого вывода информации о маршрутизации объекта:

def print_routing(obj):
    pprint(obj.get_metadata_routing()._serialize())

Потребляющий оценщик

Здесь мы демонстрируем, как оценщик может экспонировать необходимый API для поддержки маршрутизации метаданных в качестве потребителя. Представьте себе простой классификатор, принимающий sample_weight в качестве метаданных в своих методах fit и groups в методе predict.

class ExampleClassifier(ClassifierMixin, BaseEstimator):
    def fit(self, X, y, sample_weight=None):
        check_metadata(self, sample_weight=sample_weight)
        # all classifiers need to expose a classes_ attribute once they're fit.
        self.classes_ = np.array([0, 1])
        return self

    def predict(self, X, groups=None):
        check_metadata(self, groups=groups)
        # return a constant value of 1, not a very smart classifier!
        return np.ones(len(X))

Указанный выше оценщик теперь имеет все необходимое для потребления метаданных. Это достигается с помощью некоторой магии, выполненной в BaseEstimator. Теперь класс экспонирует три метода: set_fit_request, set_predict_request, и get_metadata_routing. Также присутствует set_score_request для sample_weight, так как ClassifierMixin реализует метод score, принимающий sample_weight. То же самое относится к регрессорам, которые наследуют от RegressorMixin.

По умолчанию не запрашиваются метаданные, что мы можем увидеть так:

print_routing(ExampleClassifier())
{'fit': {'sample_weight': None},
 'predict': {'groups': None},
 'score': {'sample_weight': None}}

Вышеприведенный вывод означает, что sample_weight и groups не запрашиваются ExampleClassifier, и если маршрутизатор предоставляет эти метаданные, должно быть выведено сообщение об ошибке, так как пользователь явно не указал, необходимы ли они или нет. То же самое относится к sample_weight в методе score, который унаследован от ClassifierMixin. Для явного указания значений запросов для этих метаданных мы можем использовать эти методы:

est = (
    ExampleClassifier()
    .set_fit_request(sample_weight=False)
    .set_predict_request(groups=True)
    .set_score_request(sample_weight=False)
)
print_routing(est)
{'fit': {'sample_weight': False},
 'predict': {'groups': True},
 'score': {'sample_weight': False}}

Примечание

Обратите внимание, что до тех пор, пока вышеупомянутый оценщик не используется в мета-оценщике, пользователю не нужно устанавливать какие-либо запросы для метаданных, и установленные значения игнорируются, так как потребитель не проверяет или не маршрутизирует предоставленные метаданные. Простое использование вышеупомянутого оценщика будет работать как ожидается.

est = ExampleClassifier()
est.fit(X, y, sample_weight=my_weights)
est.predict(X[:3, :], groups=my_groups)
Received sample_weight of length = 100 in ExampleClassifier.
Received groups of length = 100 in ExampleClassifier.

array([1., 1., 1.])

Схема маршрутизации мета-оценщика

Теперь мы покажем, как спроектировать мета-оценщик в качестве маршрутизатора. В качестве упрощенного примера представлен мета-оценщик, который в основном выполняет маршрутизацию метаданных.

class MetaClassifier(MetaEstimatorMixin, ClassifierMixin, BaseEstimator):
    def __init__(self, estimator):
        self.estimator = estimator

    def get_metadata_routing(self):
        # This method defines the routing for this meta-estimator.
        # In order to do so, a `MetadataRouter` instance is created, and the
        # routing is added to it. More explanations follow below.
        router = MetadataRouter(owner=self.__class__.__name__).add(
            estimator=self.estimator,
            method_mapping=MethodMapping()
            .add(caller="fit", callee="fit")
            .add(caller="predict", callee="predict")
            .add(caller="score", callee="score"),
        )
        return router

    def fit(self, X, y, **fit_params):
        # `get_routing_for_object` returns a copy of the `MetadataRouter`
        # constructed by the above `get_metadata_routing` method, that is
        # internally called.
        request_router = get_routing_for_object(self)
        # Meta-estimators are responsible for validating the given metadata.
        # `method` refers to the parent's method, i.e. `fit` in this example.
        request_router.validate_metadata(params=fit_params, method="fit")
        # `MetadataRouter.route_params` maps the given metadata to the metadata
        # required by the underlying estimator based on the routing information
        # defined by the MetadataRouter. The output of type `Bunch` has a key
        # for each consuming object and those hold keys for their consuming
        # methods, which then contain key for the metadata which should be
        # routed to them.
        routed_params = request_router.route_params(params=fit_params, caller="fit")

        # A sub-estimator is fitted and its classes are attributed to the
        # meta-estimator.
        self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit)
        self.classes_ = self.estimator_.classes_
        return self

    def predict(self, X, **predict_params):
        check_is_fitted(self)
        # As in `fit`, we get a copy of the object's MetadataRouter,
        request_router = get_routing_for_object(self)
        # then we validate the given metadata,
        request_router.validate_metadata(params=predict_params, method="predict")
        # and then prepare the input to the underlying `predict` method.
        routed_params = request_router.route_params(
            params=predict_params, caller="predict"
        )
        return self.estimator_.predict(X, **routed_params.estimator.predict)

Давайте разберем разные части приведенного кода.

Сначала функция get_routing_for_object принимает наш мета-оценщик (self) и возвращает MetadataRouter или MetadataRequest, если объект является потребителем, в зависимости от результата метода оценки get_metadata_routing.

Затем в каждом методе мы используем метод route_params для создания словаря в формате {"object_name": {"method_name": {"metadata": value}}} для передачи методу базового оценщика. object_name (estimator в приведенном выше примере routed_params.estimator.fit) совпадает с добавленным в get_metadata_routing. validate_metadata гарантирует, что все заданные метаданные запрошены для предотвращения скрытых ошибок.

Далее мы иллюстрируем различные поведения, в особенности типы возникающих ошибок.

meta_est = MetaClassifier(
    estimator=ExampleClassifier().set_fit_request(sample_weight=True)
)
meta_est.fit(X, y, sample_weight=my_weights)
Received sample_weight of length = 100 in ExampleClassifier.
MetaClassifier(estimator=ExampleClassifier())
В среде Jupyter, пожалуйста, перезапустите ячейку, чтобы отобразить HTML-представление, или доверьтесь блокноту.
На GitHub HTML-представление не может быть отображено, пожалуйста, попробуйте загрузить эту страницу с nbviewer.org.
MetaClassifier(estimator=ExampleClassifier())
ExampleClassifier()
ExampleClassifier()


Обратите внимание, что в приведенном выше примере наша служебная функция check_metadata() вызывается через ExampleClassifier. Она проверяет, что sample_weight корректно передается ей. Если нет, как в следующем примере, будет выведено, что sample_weight равно None.

meta_est.fit(X, y)
sample_weight is None in ExampleClassifier.
MetaClassifier(estimator=ExampleClassifier())
В среде Jupyter, пожалуйста, перезапустите ячейку, чтобы отобразить HTML-представление, или доверьтесь блокноту.
На GitHub HTML-представление не может быть отображено, пожалуйста, попробуйте загрузить эту страницу с nbviewer.org.
MetaClassifier(estimator=ExampleClassifier())
ExampleClassifier()
ExampleClassifier()


Если мы передаем неизвестные метаданные, возникает ошибка:

try:
    meta_est.fit(X, y, test=my_weights)
except TypeError as e:
    print(e)
MetaClassifier.fit got unexpected argument(s) {'test'}, which are not routed to any object.

А если мы передаем метаданные, которые не были явно запрошены:

try:
    meta_est.fit(X, y, sample_weight=my_weights).predict(X, groups=my_groups)
except ValueError as e:
    print(e)
Received sample_weight of length = 100 in ExampleClassifier.
[groups] are passed but are not explicitly set as requested or not requested for ExampleClassifier.predict, which is used within MetaClassifier.predict. Call `ExampleClassifier.set_predict_request({metadata}=True/False)` for each metadata you want to request/ignore.

Также, если мы явно установили, что они не запрошены, но они были предоставлены:

meta_est = MetaClassifier(
    estimator=ExampleClassifier()
    .set_fit_request(sample_weight=True)
    .set_predict_request(groups=False)
)
try:
    meta_est.fit(X, y, sample_weight=my_weights).predict(X[:3, :], groups=my_groups)
except TypeError as e:
    print(e)
Received sample_weight of length = 100 in ExampleClassifier.
MetaClassifier.predict got unexpected argument(s) {'groups'}, which are not routed to any object.

Другой концепцией является метаданные с псевдонимами. Это когда оценщик запрашивает метаданные с другим именем переменной, отличным от стандартного. Например, в настройке с двумя оценщиками в конвейере один может запросить sample_weight1, а другой sample_weight2. Обратите внимание, что это не меняет ожиданий оценщика, а только сообщает мета-оценщику, как сопоставить предоставленные метаданные с необходимыми. Вот пример, где мы передаем aliased_sample_weight мета-оценщику, но мета-оценщик понимает, что aliased_sample_weight является псевдонимом для sample_weight, и передает его как sample_weight базовому оценщику:

meta_est = MetaClassifier(
    estimator=ExampleClassifier().set_fit_request(sample_weight="aliased_sample_weight")
)
meta_est.fit(X, y, aliased_sample_weight=my_weights)
Received sample_weight of length = 100 in ExampleClassifier.
MetaClassifier(estimator=ExampleClassifier())
В среде Jupyter, пожалуйста, перезапустите ячейку, чтобы отобразить HTML-представление, или доверьтесь блокноту.
На GitHub HTML-представление не может быть отображено, пожалуйста, попробуйте загрузить эту страницу с nbviewer.org.
MetaClassifier(estimator=ExampleClassifier())
ExampleClassifier()
ExampleClassifier()


Передача sample_weight приведет к ошибке, так как запрос с псевдонимом sample_weight с этим именем не запрошен:

try:
    meta_est.fit(X, y, sample_weight=my_weights)
except TypeError as e:
    print(e)
MetaClassifier.fit got unexpected argument(s) {'sample_weight'}, which are not routed to any object.

Это приводит нас к концепции get_metadata_routing. В scikit-learn маршрутизация работает так: потребители запрашивают необходимые данные, а маршрутизаторы их передают. Кроме того, маршрутизатор раскрывает свои собственные требования, чтобы его можно было использовать внутри другого маршрутизатора, например, конвейера внутри объекта поиска по сетке. Вывод get_metadata_routing, являющийся словарем, представляющим MetadataRouter, включает полное дерево запрошенных метаданных всеми вложенными объектами и соответствующими маршрутизациями методов, т.е. какой метод под-оценщика используется в каком методе мета-оценщика:

print_routing(meta_est)
{'estimator': {'mapping': [{'callee': 'fit', 'caller': 'fit'},
                           {'callee': 'predict', 'caller': 'predict'},
                           {'callee': 'score', 'caller': 'score'}],
               'router': {'fit': {'sample_weight': 'aliased_sample_weight'},
                          'predict': {'groups': None},
                          'score': {'sample_weight': None}}}}

Как вы можете видеть, единственные запрошенные метаданные для метода fit это "sample_weight" с "aliased_sample_weight" в качестве псевдонима. Класс ~utils.metadata_routing.MetadataRouter позволяет легко создать объект маршрутизации, который сгенерирует необходимый вывод для нашего get_metadata_routing.

Чтобы понять, как работают псевдонимы в мета-оценщиках, представьте наш мета-оценщик внутри другого:

meta_meta_est = MetaClassifier(estimator=meta_est).fit(
    X, y, aliased_sample_weight=my_weights
)
Received sample_weight of length = 100 in ExampleClassifier.

В приведенном выше примере метод fit мета-оценщика meta_meta_est будет вызывать методы fit своих под-оценщиков:

# user feeds `my_weights` as `aliased_sample_weight` into `meta_meta_est`:
meta_meta_est.fit(X, y, aliased_sample_weight=my_weights):
    ...

    # the first sub-estimator (`meta_est`) expects `aliased_sample_weight`
    self.estimator_.fit(X, y, aliased_sample_weight=aliased_sample_weight):
        ...

        # the second sub-estimator (`est`) expects `sample_weight`
        self.estimator_.fit(X, y, sample_weight=aliased_sample_weight):
            ...

Потребление и маршрутизация мета-оценщика

Для немного более сложного примера рассмотрим мета-оценщик, который маршрутизирует метаданные к базовому оценщику, как и прежде, но также использует некоторые метаданные в собственных методах. Этот мета-оценщик является одновременно потребителем и маршрутизатором. Реализация очень похожа на предыдущую, но с несколькими изменениями.

class RouterConsumerClassifier(MetaEstimatorMixin, ClassifierMixin, BaseEstimator):
    def __init__(self, estimator):
        self.estimator = estimator

    def get_metadata_routing(self):
        router = (
            MetadataRouter(owner=self.__class__.__name__)
            # defining metadata routing request values for usage in the meta-estimator
            .add_self_request(self)
            # defining metadata routing request values for usage in the sub-estimator
            .add(
                estimator=self.estimator,
                method_mapping=MethodMapping()
                .add(caller="fit", callee="fit")
                .add(caller="predict", callee="predict")
                .add(caller="score", callee="score"),
            )
        )
        return router

    # Since `sample_weight` is used and consumed here, it should be defined as
    # an explicit argument in the method's signature. All other metadata which
    # are only routed, will be passed as `**fit_params`:
    def fit(self, X, y, sample_weight, **fit_params):
        if self.estimator is None:
            raise ValueError("estimator cannot be None!")

        check_metadata(self, sample_weight=sample_weight)

        # We add `sample_weight` to the `fit_params` dictionary.
        if sample_weight is not None:
            fit_params["sample_weight"] = sample_weight

        request_router = get_routing_for_object(self)
        request_router.validate_metadata(params=fit_params, method="fit")
        routed_params = request_router.route_params(params=fit_params, caller="fit")
        self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit)
        self.classes_ = self.estimator_.classes_
        return self

    def predict(self, X, **predict_params):
        check_is_fitted(self)
        # As in `fit`, we get a copy of the object's MetadataRouter,
        request_router = get_routing_for_object(self)
        # we validate the given metadata,
        request_router.validate_metadata(params=predict_params, method="predict")
        # and then prepare the input to the underlying ``predict`` method.
        routed_params = request_router.route_params(
            params=predict_params, caller="predict"
        )
        return self.estimator_.predict(X, **routed_params.estimator.predict)

Ключевые моменты, в которых вышеупомянутый мета-оценщик отличается от предыдущего мета-оценщика, заключаются в явном приёме sample_weight в fit и включении его в fit_params. Поскольку sample_weight является явным аргументом, мы можем быть уверены, что set_fit_request(sample_weight=...) присутствует для этого метода. Мета-оценщик одновременно является потребителем и маршрутизатором sample_weight.

В get_metadata_routing, мы добавляем self в маршрутизацию, используя add_self_request, чтобы указать, что этот оценщик потребляет sample_weight, а также является маршрутизатором; что также добавляет ключ $self_request к информации о маршрутизации, как показано ниже. Теперь давайте рассмотрим несколько примеров:

  • Метаданные не запрашиваются
meta_est = RouterConsumerClassifier(estimator=ExampleClassifier())
print_routing(meta_est)
{'$self_request': {'fit': {'sample_weight': None},
                   'score': {'sample_weight': None}},
 'estimator': {'mapping': [{'callee': 'fit', 'caller': 'fit'},
                           {'callee': 'predict', 'caller': 'predict'},
                           {'callee': 'score', 'caller': 'score'}],
               'router': {'fit': {'sample_weight': None},
                          'predict': {'groups': None},
                          'score': {'sample_weight': None}}}}
  • sample_weight запрашивается дочерним оценщиком
meta_est = RouterConsumerClassifier(
    estimator=ExampleClassifier().set_fit_request(sample_weight=True)
)
print_routing(meta_est)
{'$self_request': {'fit': {'sample_weight': None},
                   'score': {'sample_weight': None}},
 'estimator': {'mapping': [{'callee': 'fit', 'caller': 'fit'},
                           {'callee': 'predict', 'caller': 'predict'},
                           {'callee': 'score', 'caller': 'score'}],
               'router': {'fit': {'sample_weight': True},
                          'predict': {'groups': None},
                          'score': {'sample_weight': None}}}}
  • sample_weight запрашивается мета-оценщиком
meta_est = RouterConsumerClassifier(estimator=ExampleClassifier()).set_fit_request(
    sample_weight=True
)
print_routing(meta_est)
{'$self_request': {'fit': {'sample_weight': True},
                   'score': {'sample_weight': None}},
 'estimator': {'mapping': [{'callee': 'fit', 'caller': 'fit'},
                           {'callee': 'predict', 'caller': 'predict'},
                           {'callee': 'score', 'caller': 'score'}],
               'router': {'fit': {'sample_weight': None},
                          'predict': {'groups': None},
                          'score': {'sample_weight': None}}}}

Обратите внимание на разницу в представлениях запрашиваемых метаданных выше.

  • Мы также можем использовать псевдоним для метаданных, чтобы передавать разные значения методам подгонки мета- и дочернего оценщика:
meta_est = RouterConsumerClassifier(
    estimator=ExampleClassifier().set_fit_request(sample_weight="clf_sample_weight"),
).set_fit_request(sample_weight="meta_clf_sample_weight")
print_routing(meta_est)
{'$self_request': {'fit': {'sample_weight': 'meta_clf_sample_weight'},
                   'score': {'sample_weight': None}},
 'estimator': {'mapping': [{'callee': 'fit', 'caller': 'fit'},
                           {'callee': 'predict', 'caller': 'predict'},
                           {'callee': 'score', 'caller': 'score'}],
               'router': {'fit': {'sample_weight': 'clf_sample_weight'},
                          'predict': {'groups': None},
                          'score': {'sample_weight': None}}}}

Однако, fit мета-оценщика требует псевдонима только для дочернего оценщика и обрабатывает собственный вес выборки как sample_weight, так как он не проверяет и не маршрутизирует свои собственные требуемые метаданные:

meta_est.fit(X, y, sample_weight=my_weights, clf_sample_weight=my_other_weights)
Received sample_weight of length = 100 in RouterConsumerClassifier.
Received sample_weight of length = 100 in ExampleClassifier.
RouterConsumerClassifier(estimator=ExampleClassifier())
В среде Jupyter, пожалуйста, перезапустите эту ячейку, чтобы показать HTML-представление, или доверьтесь блокноту.
На GitHub, HTML-представление не может быть отображено, пожалуйста, попробуйте загрузить эту страницу с nbviewer.org.
RouterConsumerClassifier(estimator=ExampleClassifier())
ExampleClassifier()
ExampleClassifier()


  • Псевдоним только для дочернего оценщика:

Это полезно, когда мы не хотим, чтобы мета-оценщик использовал метаданные, но дочерний оценщик должен.

meta_est = RouterConsumerClassifier(
    estimator=ExampleClassifier().set_fit_request(sample_weight="aliased_sample_weight")
)
print_routing(meta_est)
{'$self_request': {'fit': {'sample_weight': None},
                   'score': {'sample_weight': None}},
 'estimator': {'mapping': [{'callee': 'fit', 'caller': 'fit'},
                           {'callee': 'predict', 'caller': 'predict'},
                           {'callee': 'score', 'caller': 'score'}],
               'router': {'fit': {'sample_weight': 'aliased_sample_weight'},
                          'predict': {'groups': None},
                          'score': {'sample_weight': None}}}}

Мета-оценщик не может использовать aliased_sample_weight, потому что он ожидает, что он будет передан как sample_weight. Это будет справедливо, даже если set_fit_request(sample_weight=True) был установлен на нем.

Простая цепочка

Немного более сложный случай использования — это мета-оценщик, напоминающий Pipeline. Вот мета-оценщик, который принимает преобразователь и классификатор. При вызове метода fit, он применяет преобразование fit и transform до запуска классификатора на преобразованных данных. При predict, он применяет преобразование transform перед предсказанием с помощью метода predict классификатора на преобразованных новых данных.

class SimplePipeline(ClassifierMixin, BaseEstimator):
    def __init__(self, transformer, classifier):
        self.transformer = transformer
        self.classifier = classifier

    def get_metadata_routing(self):
        router = (
            MetadataRouter(owner=self.__class__.__name__)
            # We add the routing for the transformer.
            .add(
                transformer=self.transformer,
                method_mapping=MethodMapping()
                # The metadata is routed such that it retraces how
                # `SimplePipeline` internally calls the transformer's `fit` and
                # `transform` methods in its own methods (`fit` and `predict`).
                .add(caller="fit", callee="fit")
                .add(caller="fit", callee="transform")
                .add(caller="predict", callee="transform"),
            )
            # We add the routing for the classifier.
            .add(
                classifier=self.classifier,
                method_mapping=MethodMapping()
                .add(caller="fit", callee="fit")
                .add(caller="predict", callee="predict"),
            )
        )
        return router

    def fit(self, X, y, **fit_params):
        routed_params = process_routing(self, "fit", **fit_params)

        self.transformer_ = clone(self.transformer).fit(
            X, y, **routed_params.transformer.fit
        )
        X_transformed = self.transformer_.transform(
            X, **routed_params.transformer.transform
        )

        self.classifier_ = clone(self.classifier).fit(
            X_transformed, y, **routed_params.classifier.fit
        )
        return self

    def predict(self, X, **predict_params):
        routed_params = process_routing(self, "predict", **predict_params)

        X_transformed = self.transformer_.transform(
            X, **routed_params.transformer.transform
        )
        return self.classifier_.predict(
            X_transformed, **routed_params.classifier.predict
        )

Обратите внимание на использование MethodMapping для объявления того, какие методы дочернего оценщика (вызываемого) используются в каких методах мета-оценщика (вызывающего). Как вы можете видеть, SimplePipeline использует методы transform и fit преобразователя в fit, и метод transform в predict, и именно это вы видите, реализованным в структуре маршрутизации класса pipeline.

Еще одно отличие в вышеприведенном примере от предыдущих — использование process_routing, который обрабатывает входные параметры, выполняет необходимую проверку и возвращает routed_params, которое мы создавали в предыдущих примерах. Это сокращает объем кода, который разработчику нужно написать в каждом методе мета-оценщика. Разработчикам настоятельно рекомендуется использовать эту функцию, если нет веских причин против этого.

Для тестирования вышеупомянутой цепочки давайте добавим пример преобразователя.

class ExampleTransformer(TransformerMixin, BaseEstimator):
    def fit(self, X, y, sample_weight=None):
        check_metadata(self, sample_weight=sample_weight)
        return self

    def transform(self, X, groups=None):
        check_metadata(self, groups=groups)
        return X

    def fit_transform(self, X, y, sample_weight=None, groups=None):
        return self.fit(X, y, sample_weight).transform(X, groups)

Обратите внимание, что в приведенном выше примере мы реализовали fit_transform, который вызывает fit и transform с соответствующими метаданными. Это необходимо только в том случае, если transform принимает метаданные, так как реализация по умолчанию fit_transform в TransformerMixin не передает метаданные в transform.

Теперь мы можем протестировать нашу цепочку и увидеть, правильно ли передаются метаданные. В этом примере используются наш SimplePipeline, наш ExampleTransformer, и наш RouterConsumerClassifier, который использует наш ExampleClassifier.

pipe = SimplePipeline(
    transformer=ExampleTransformer()
    # we set transformer's fit to receive sample_weight
    .set_fit_request(sample_weight=True)
    # we set transformer's transform to receive groups
    .set_transform_request(groups=True),
    classifier=RouterConsumerClassifier(
        estimator=ExampleClassifier()
        # we want this sub-estimator to receive sample_weight in fit
        .set_fit_request(sample_weight=True)
        # but not groups in predict
        .set_predict_request(groups=False),
    )
    # and we want the meta-estimator to receive sample_weight as well
    .set_fit_request(sample_weight=True),
)
pipe.fit(X, y, sample_weight=my_weights, groups=my_groups).predict(
    X[:3], groups=my_groups
)
Received sample_weight of length = 100 in ExampleTransformer.
Received groups of length = 100 in ExampleTransformer.
Received sample_weight of length = 100 in RouterConsumerClassifier.
Received sample_weight of length = 100 in ExampleClassifier.
Received groups of length = 100 in ExampleTransformer.
groups is None in ExampleClassifier.

array([1., 1., 1.])

Замена/Изменение значения по умолчанию

В этом разделе показано, как следует обрабатывать случай, когда маршрутизатор также становится потребителем, особенно когда он потребляет те же метаданные, что и его дочерний оценщик, или когда потребитель начинает потреблять метаданные, которых не было в более ранней версии. В этом случае должно выдаваться предупреждение в течение некоторого времени, чтобы сообщить пользователям о том, что поведение изменено по сравнению с предыдущими версиями.

class MetaRegressor(MetaEstimatorMixin, RegressorMixin, BaseEstimator):
    def __init__(self, estimator):
        self.estimator = estimator

    def fit(self, X, y, **fit_params):
        routed_params = process_routing(self, "fit", **fit_params)
        self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit)

    def get_metadata_routing(self):
        router = MetadataRouter(owner=self.__class__.__name__).add(
            estimator=self.estimator,
            method_mapping=MethodMapping().add(caller="fit", callee="fit"),
        )
        return router

Как объяснено выше, это допустимое использование, если my_weights не должны передаваться как sample_weight в MetaRegressor:

reg = MetaRegressor(estimator=LinearRegression().set_fit_request(sample_weight=True))
reg.fit(X, y, sample_weight=my_weights)

Теперь представьте, что мы далее разрабатываем MetaRegressor, и теперь он также потребляет sample_weight:

class WeightedMetaRegressor(MetaEstimatorMixin, RegressorMixin, BaseEstimator):
    # show warning to remind user to explicitly set the value with
    # `.set_{method}_request(sample_weight={boolean})`
    __metadata_request__fit = {"sample_weight": metadata_routing.WARN}

    def __init__(self, estimator):
        self.estimator = estimator

    def fit(self, X, y, sample_weight=None, **fit_params):
        routed_params = process_routing(
            self, "fit", sample_weight=sample_weight, **fit_params
        )
        check_metadata(self, sample_weight=sample_weight)
        self.estimator_ = clone(self.estimator).fit(X, y, **routed_params.estimator.fit)

    def get_metadata_routing(self):
        router = (
            MetadataRouter(owner=self.__class__.__name__)
            .add_self_request(self)
            .add(
                estimator=self.estimator,
                method_mapping=MethodMapping().add(caller="fit", callee="fit"),
            )
        )
        return router

Вышеупомянутая реализация почти такая же, как MetaRegressor, и из-за значения запроса по умолчанию, определенного в __metadata_request__fit, при подгонке выдается предупреждение.

with warnings.catch_warnings(record=True) as record:
    WeightedMetaRegressor(
        estimator=LinearRegression().set_fit_request(sample_weight=False)
    ).fit(X, y, sample_weight=my_weights)
for w in record:
    print(w.message)
Received sample_weight of length = 100 in WeightedMetaRegressor.
Support for sample_weight has recently been added to this class. To maintain backward compatibility, it is ignored now. Using `set_fit_request(sample_weight={True, False})` on this method of the class, you can set the request value to False to silence this warning, or to True to consume and use the metadata.

Когда оценщик потребляет метаданные, которые он не потреблял ранее, можно использовать следующую схему, чтобы предупредить пользователей об этом.

class ExampleRegressor(RegressorMixin, BaseEstimator):
    __metadata_request__fit = {"sample_weight": metadata_routing.WARN}

    def fit(self, X, y, sample_weight=None):
        check_metadata(self, sample_weight=sample_weight)
        return self

    def predict(self, X):
        return np.zeros(shape=(len(X)))


with warnings.catch_warnings(record=True) as record:
    MetaRegressor(estimator=ExampleRegressor()).fit(X, y, sample_weight=my_weights)
for w in record:
    print(w.message)
sample_weight is None in ExampleRegressor.
Support for sample_weight has recently been added to this class. To maintain backward compatibility, it is ignored now. Using `set_fit_request(sample_weight={True, False})` on this method of the class, you can set the request value to False to silence this warning, or to True to consume and use the metadata.

В конце мы отключаем флаг конфигурации для маршрутизации метаданных:

set_config(enable_metadata_routing=False)

Разработка сторонних приложений и зависимость от scikit-learn

Как видно выше, информация передается между классами с помощью MetadataRequest и MetadataRouter. Сильно не рекомендуется, но возможно использовать сторонние инструменты для маршрутизации метаданных, если вы хотите получить совместимый с scikit-learn оценщик без зависимости от пакета scikit-learn. Если выполнены все следующие условия, вам НЕ нужно изменять свой код вообще:

  • ваш оценщик наследуется от BaseEstimator
  • параметры, потребляемые методами вашего оценщика, например, fit, явно определены в сигнатуре метода, а не *args или *kwargs.
  • ваш оценщик не маршрутизирует метаданные к внутренним объектам, т.е. он не является _маршрутизатором_.

Общее время выполнения скрипта: (0 минут 0,055 секунд)

Launch binder
Launch JupyterLite

Download Jupyter notebook: plot_metadata_routing.ipynb

Download Python source code: plot_metadata_routing.py

Download zipped: plot_metadata_routing.zip

Похожие примеры

Настройка Elastic Net с предварительно вычисленной матрицей Грама и взвешенными образцами

Основные моменты выпуска scikit-learn 1.4

SGD: Взвешенные образцы

Основные моменты выпуска scikit-learn 1.6

© 2007–2025 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.6/auto_examples/miscellaneous/plot_metadata_routing.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API