Рубрики
Без рубрики

Развитие пользовательских трансформаторов и оценок Scikit –

Scikit-Suart предлагает широкий ассортимент моделей машинного обучения, но выходит за пределы этого, обеспечивая … Теги с обучением машины, наукой данных, Python.

Scikit-Suart предлагает широкий ассортимент моделей машинного обучения, но выходит за пределы того, что предоставляя другие инструменты, такие как оптимизация гиперпараметра, используя Gridsearchcv или созданные оценки через Трубопровод . Одной из характеристик, которые мне нравятся больше всего о Scikit-Suart, является их последовательным API, все оценки реализуют те же основные методы (посадки и предсказывания). Эта согласованность была чрезвычайно полезна для сообщества OL открытого исходного кода, поскольку многие из сторонних пакетов разрабатываются с этим (например, KERAS), следовательно, они могут взаимодействовать друг с другом.

Часто нам нужно реализовать некоторые функциональные возможности, которые не существуют в Scikit-Learn или любых других пакетов, если мы соответствующим API Scikit-Learn, мы можем ограничить себя разработать пользовательский трансформатор/оценщик, и наш код будет хорошо интерфейс с модулями Scikit – Отказ

В этом посте в блоге я покажу, как построить пользовательские трансформаторы и оценки, а также обсудить детали реализации, чтобы сделать это правильно. Официальные документы Содержать все, что вам нужно знать, но вот самые важные факты:

  1. Все параметры конструктора (функция __init__ Функция) должны иметь значения по умолчанию
  2. Параметры конструктора должны быть добавлены в качестве атрибутов без каких-либо модификаций
  3. Атрибуты, оцененные от данных, должны иметь имя с трейлинг подчеркивания

Есть и другие правила, но вы можете использовать функции утилиты, предоставляемые Scikit – научитесь позаботиться о них. А check_estimator Функция также обеспечивается исчерпывающим убедитесь, что ваша реализация правильная. Официальный код код также предоставляется.

Трансформатор Используйте корпус: проверка ввода модели

Оценки для изучения Scikit – были первоначально разработаны для работы на Numpy Armays (хотя существует текущая текущая работа для лучшего интерфейса с помощью кадров данных PandaS). Для практических целей это означает, что наши оценки не имеют понятия имен столбцов (только входная форма подтверждается для повышения ошибок): если столбцы перетасовываются, трансформатор/оценщик не будет жаловаться, но прогноз будет бессмысленным.

Наш пользовательский трансформатор (объект, который надеждает и преобразует), добавляет эту способность: при использовании в Трубопровод Объект, это проверит, что мы получаем правильные входные столбцы. Реализация (прокомментированная) выглядит следующим образом:

# We inherit from TransformerMixin to get the fit_transform implementation.
# Transformers in scikit-learn also have to inherit from BaseEstimator
# see: https://github.com/scikit-learn/scikit-learn/blob/b194674c4/sklearn/preprocessing/_data.py#L520
class InputGuard(TransformerMixin, BaseEstimator):
    """
    Verify column names at predict time match the ones used when fitting

    Parameters
    ----------
    strict : bool, optional
        If True, it will raise an error if the input does not match
        exactly (same columns, same order), if False, it will ignore
        order and extra columns (will only show a warning), defaults
        to True

    Notes
    -----
    Must be used in a Pipeline object and must be the first step. fit
    and predict should be called with a pandas.DataFrame object
    """

    def __init__(self, strict=True):
        # no logic allowed here, just assign attributes
        # from __init__ args
        self.strict = strict

    def fit(self, X, y=None):
        # we need this to pass check_estimator
        X_out, y = check_X_y(X, y)
        X = X if hasattr(X, 'columns') else X_out

        # our estimator is designed to work on structures
        # that have a columns attribute (such as pandas Data Frame)
        if hasattr(X, 'columns'):
            self.expected_ = list(X.columns)
            self.expected_n_ = X.shape[1]
        # ...but we still need to support numpy.arrays to
        # pass check_estimator
        else:
            self.expected_ = None
            self.expected_n_ = X.shape[1]
            warnings.warn('Input does not have a columns attribute, '
                          'only number of columns will be validated')
        return self

    def transform(self, X):
        # these two are to pass check_estimator
        check_is_fitted(self)
        X_out = check_array(X)
        X = X if hasattr(X, 'columns') else X_out

        # if column names are available...
        if self.expected_:
            return self._transform(X)
        else:
            # this is raised to pass check_estimator
            if self.expected_n_ != X.shape[1] and self.strict:
                raise ValueError('Number of columns from fit {} is different from transform {}'
                                 .format(self.expected_n_, X.shape[1]))

            return X

    def _transform(self, X):
        # this function implements our core logic and it
        # will only be called when fit received an X with a columns attribute

        if not hasattr(X, 'columns'):
            raise ValueError('{}.fit ran with a X object that had '
                             'a columns attribute, but the current '
                             'X does not have it'.format(type(self).__name__))

        columns_got = list(X.columns)

        if self.strict:
            if self.expected_ != columns_got:
                missing = set(self.expected_) - set(columns_got)
                raise ValueError('Columns during fit were: {}, but got {} '
                                 'for predict.'
                                 ' Missing: {}'.format(self.expected_,
                                                       columns_got,
                                                       missing))
        else:
            missing = set(self.expected_) - set(columns_got)
            extra = set(columns_got) - set(self.expected_)

            if missing:
                raise ValueError('Missing columns: {}'.format(missing))
            elif extra:
                extra = set(columns_got) - set(self.expected_)
                warnings.warn('Got extra columns: {}, ignoring'
                              .format(extra))
                return X[self.expected_]

        return X

Sklearn.utils.validation Модуль предоставляет полезные функции для передачи некоторых из check_estimator Тесты без необходимости реализации логики. check_estimator ). Эти полезные функции преобразуют входы ( check_x_y , check_array ) Чтобы вернуть ожидаемый формат (Numpy массивы) и бросить соответствующие исключения, когда это невозможно. check_is_fated только вызывает ошибку, если звонок на прогнозировать Попытка сначала попытается сначала установить модель.

Теперь мы подтверждаем, что наш трансформатор проходит все тесты:

# if no exceptions are raised, we're good
check_estimator(InputGuard)

Передача всех тестов не является абсолютно необходимым для вашего трансформатора (или оценщика), чтобы правильно интегрироваться с другими модулями Scikit-Learn, но при этом гарантирует, что ваша реализация является надежным путем обработки общих сценариев от имени пользователя (например, прохождение 2D массива с одним столбец как Y вместо 1D массива) и бросать информативные ошибки. Учитывая большие пользовательские базы Scikit-Suart, это обязательно, однако, для некоторого очень настроенного внедрения, проходящие все тесты просто невозможно, так как мы увидим в кастомном корпусе использования оценки.

На данный момент давайте проверим, что наш трансформатор хорошо играет с трубопроводом и GRIDSearchcv:

# our transformer *has* to be the first step in the pipeline,
# to make sure it gets a pandas Data Frame as input and not
# a numpy array (which strips column names)
pipe = Pipeline([('guard', InputGuard()),
                 ('scaler', StandardScaler()),
                 ('reg', ElasticNet())])

# perform hyperparameter tuning
grid = GridSearchCV(pipe, param_grid={'reg__alpha': [0.5, 1.0, 2.0]})
best_pipe = grid.fit(X_train, y_train).best_estimator_

# make predictions using the best model
y_pred = best_pipe.predict(X_test)
print(f'MAE: {np.abs(y_test - y_pred).mean():.2f}')
# Output:
MAE: 3.47

Теперь проверим, что наш трансформатор бросает ошибку, если столбец отсутствует:

# drop a feature used during training
X_corrupted = X_test.drop('CRIM', axis='columns')

try:
    best_pipe.predict(X_corrupted)
except ValueError as e:
    print('Error message: ', e)
# Output:
Error message:  Columns during fit were: ['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT'], but got ['ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT'] for predict. Missing: {'CRIM'}

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

warnings.filterwarnings('default')
X_corrupted = X_test.copy()
X_corrupted['extra'] = 1
best_pipe.named_steps['guard'].strict = False
_ = best_pipe.predict(X_corrupted)
# Output:
/Users/Edu/miniconda3/envs/test4/lib/python3.6/site-packages/ipykernel_launcher.py:91: UserWarning: Got extra columns: {'extra'}, ignoring
warnings.filterwarnings('ignore')

Корпус использования оценки: прогнозы модели регистрации

Скажем, мы хотим войти все наши прогнозы для мониторинга модели производства, ради примера, мы просто будем использовать Регистрация модуль Но эта же логика применяется к другим методам, таким как сэкономить прогнозы в базу данных. Здесь есть несколько нюансов. Трубопровод Требуется все промежуточные шаги для трансформаторов (Fit/Transform), что означает, что мы можем добавить только нашу модель (тот, который реализует предсказать) в конце.

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

3 соображения, которые применяются для трансформаторов, применяются для оценщиков, плюс четвертый (скопирован непосредственно из документации Scikit-Surrey):

  1. Оценщики имеют get_params и set_params Функции. Функция get_params не принимает аргументов и возвращает дикт __init__ Параметры оценки вместе со своими значениями. Это должен принимать одно ключевое слово аргумент, Глубоко , который получает логическое значение, которое определяет, должен ли метод возвращать параметры подоценки (для большинства оценок, это можно игнорировать). Значение по умолчанию для глубокого должно быть правдой.
class LoggingEstimator(BaseEstimator):
    """
    A wrapper for scikit-learn estimators that logs every prediction

    Parameters
    ----------
    est_class
        The estimator class to use
    **kwargs
        Keyword arguments to initialize the estimator
    """

    # NOTE: we arbitrarily selected a default estimator class
    # so check_estimator does not fail when doing LoggingEstimator()
    def __init__(self, est_class=LinearRegression, **kwargs):

        self.est_class = est_class

        # kwargs depend on the model used, so assign them whatever they are
        for key, value in kwargs.items():
            setattr(self, key, value)

        # these attributes support the logging functionality
        self._logger = logging.getLogger(__name__)
        self._logging_enabled = False
        self._param_names = ['est_class'] + list(kwargs.keys())

    # in the transformer case, we did not implement get_params
    # nor set_params since we inherited them from BaseEstimator
    # but such implementation will not work here due to the **kwargs
    # in the constructor, so we implemented it

    def get_params(self, deep=True):
        # Note: we are ignoring the deep parameter
        # this will not work with estimators that have sub-estimators
        # see https://scikit-learn.org/stable/developers/develop.html#get-params-and-set-params
        return {param: getattr(self, param)
                for param in self._param_names}

    def set_params(self, **parameters):
        for parameter, value in parameters.items():
            setattr(self, parameter, value)

        return self

    # our fit method instantiates the actual model, and
    # it forwards any extra keyword arguments
    def fit(self, X, y, **kwargs):
        est_kwargs = self.get_params()
        del est_kwargs['est_class']
        # remember the trailing underscore
        self.model_ = self.est_class(**est_kwargs)
        self.model_.fit(X, y, **kwargs)
        # fit must return self
        return self

    def predict(self, X):
        check_is_fitted(self)

        # we use the fitted model and log if logging is enabled
        y_pred = self.model_.predict(X)

        if self._logging_enabled:
            self._logger.info('Logging predicted values: %s', y_pred)

        return y_pred

    # requiring a score method is not documented but throws an
    # error if not implemented
    def score(self, X, y, **kwargs):
        return self.model_.score(X, y, **kwargs)

    # some models implement custom methods. Anything that is not implemented here
    # will be delegated to the underlying model. There is one condition we have
    # to cover: if the underlying estimator has class attributes they won't
    # be accessible until we fit the model (since we instantiate the model there)
    # to fix it, we try to look it up attributes in the instance, if there
    # is no instance, we look up the class. More info here:
    # https://scikit-learn.org/stable/developers/develop.html#estimator-types
    def __getattr__(self, key):
        if key != 'model_':
            if hasattr(self, 'model_'):
                return getattr(self.model_, key)
            else:
                return getattr(self.est_class, key)
        else:
            raise AttributeError(
                "'{}' object has no attribute 'model_'".format(type(self).__name__))

    # these two control logging

    def enable_logging(self):
        self._logging_enabled = True

    def disable_logging(self):
        self._logging_enabled = False

    # ignore the following two for now, more info in the Appendix

    def __getstate__(self):
        state = self.__dict__.copy()
        del state['_logger']
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        self._logger = logging.getLogger(__name__)

check_estimator имеет generate_only Параметр, который давайте запустим, проверяет один за другим, вместо того, чтобы не удавать с первой ошибки. Давайте использовать эту опцию, чтобы проверить ЛогизащисткиМатор Отказ

for est, check in check_estimator(LoggingEstimator, generate_only=True):
    try:
        check(est)
    except AssertionError as e:
        print('Failed: ', check, e)
# Output:
Failed:  functools.partial(, 'LoggingEstimator') 
Failed:  functools.partial(, 'LoggingEstimator') Estimator LoggingEstimator should not set any attribute apart from parameters during init. Found attributes ['_logger', '_logging_enabled', '_param_names'].
Failed:  functools.partial(, 'LoggingEstimator') expected 1 DataConversionWarning, got: 

Имена не очень информативны, поэтому я посмотрел на Исходный код Отказ

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

check_no_attributes_set_in_init Также о __init__ Аргументы, согласно спецификации, мы не должны устанавливать какие-либо атрибуты, отличные от аргументов, но мы нуждаемся в них для работы на работу, оно не должно повлиять.

Наконец check_supervised_y_2d , проверяет, что если 2D Numpy Marray передается на соответствовать Предупреждение выдается, поскольку он должен быть преобразован в 1-м массива, наш пользовательский оценщик обернут любой оценщик, который может иметь многофункциональную производительность, поэтому мы не можем использовать функции утилиты для исправления этого.

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

Давайте теперь посмотрим наш трубопровод в действии, обратите внимание, что мы также включаем в себя нашу Inputcuard. мы меняем базовую модель в ЛогизащисткиМатор Чтобы продемонстрировать, что он работает с любым оценщиком.

pipe = Pipeline([('guard', InputGuard()),
                 ('scaler', StandardScaler()),
                 ('reg', LoggingEstimator(est_class=ElasticNet))])

# perform hyperparameter tuning
grid = GridSearchCV(
    pipe, param_grid={'reg__alpha': [0.5, 1.0, 2.0]}, n_jobs=-1)
best_pipe = grid.fit(X_train, y_train).best_estimator_

# make predictions using the best model
y_pred = best_pipe.predict(X_test)
print(f'MAE: {np.abs(y_test - y_pred).mean():.2f}')
# Output:
MAE: 3.68

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

logging.basicConfig(level=logging.INFO)
best_pipe.named_steps['reg'].enable_logging()

Следующая строка показывает нашу ведение журнала:

best_pipe.predict(X_test.iloc[0:2])
# Output:
INFO:__main__:Logging predicted values: [27.63414143 32.52647906]

array([27.63414143, 32.52647906])

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

best_pipe.named_steps['reg'].coef_
# Output:
array([-0.34454552,  0.15182933, -0.1996373 ,  0.50187929, -0.3307912 ,
        2.9541881 , -0.        , -0.        , -0.        , -0.41526202,
       -1.41850718,  0.43883102, -2.06167772])

Приложение: Создание нашей оценки работать с сортировочным (или любым другим механизмом кровообращения)

Маркировка объекта означает сохранение его на диск. Это полезно, если мы хотим соответствовать, а затем развернуть модель ( Будьте осторожны при этом! ) Но это также нужно, если мы хотим нашу модель работать с Многопроцессор модуль. Некоторые объекты являются подборщик Но некоторые другие не являются (это также зависит от того, в какой библиотеке вы используете). Логин Объекты не работают с Парил модуль Но мы можем легко исправить это, удалив его перед сохранением на диск и инициализацию его после загрузки, это сводится к добавлению еще двух методов: __getstate__ и __setState__ , если вы заинтересованы в деталях, Прочитайте это Отказ

# showing pickling and unpickling works
pickle.loads(pickle.dumps(best_pipe))
# Output:
Pipeline(memory=None,
         steps=[('guard', InputGuard(strict=True)),
                ('scaler',
                 StandardScaler(copy=True, with_mean=True, with_std=True)),
                ('reg',
                 LoggingEstimator(est_class=))],
         verbose=False)

Закрытие замечаний

В этом посте мы показали, как разрабатывать трансформаторы и оценки, совместимые с Scikit-Suart. Учитывая количество деталей API, используя check_estimator Функция проведет вас через процесс. Однако, если ваши реализации содержит нестандартное поведение (например, наши), ваши пользовательские объекты не будут провалиться тесты, даже если они правильно интегрируются с другими модулями. В таком случае вы должны быть осторожны в вашей реализации, используя check_estimator. с generate_only = Правда Полезно для получения списка неудачных тестов и принятия решения о том, является ли оно приемлемым или нет.

После спецификации API Scikit-Learn дает вам доступ к широкому набору инструментов ML, чтобы вы могли сосредоточиться на реализации вашей пользовательской модели и все еще используете другие модули для поиска сетки, перекрестной проверки и т. Д. Это огромное время для проектов ML.

Исходный код для этого поста доступен здесь Отказ

Нашел ошибку в этом посте? Нажмите здесь, чтобы сообщить нам об этом Отказ

Этот пост был сгенерирован с использованием версии Scikit-Learn:

# Output:
0.22.2

Оригинал: “https://dev.to/edublancas/developing-custom-scikit-learn-transformers-and-estimators-540h”