Как использовать пайплайны и добавлять пользовательские преобразователи в поток обработки

Ищете способ организовать поток машинного обучения, сохраняя при этом гибкость процесса обработки? Хотите работать с конвейерами, добавляя уникальные этапы обработки данных? Эта статья представляет собой простое пошаговое руководство по использованию пайплайнов Scikit-Learn и добавлению в пайплайн пользовательских преобразователей.

Почему трубопроводы?

Если вы достаточно долго работаете специалистом по данным, вы, вероятно, слышали о Skicit-Learn Pipelines. Возможно, вы сталкивались с ними после работы над беспорядочным исследовательским проектом и в итоге получили гигантский блокнот, полный шагов обработки и различных преобразований, не зная, какие шаги и параметры были применены в той последней успешной попытке, которая дала хорошие результаты. В противном случае вы, должно быть, сталкивались с ними, если у вас когда-либо была возможность развернуть модель в продакшене.

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

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

Очень краткое введение в пайплайны

Конвейер — это список последовательных преобразований, за которым следует объект оценки Scikit-Learn (то есть модель машинного обучения). Конвейер дает нам структурированную структуру для применения преобразований к данным и, в конечном счете, запуска нашей модели. В нем четко указано, какие этапы обработки мы выбрали для применения, их порядок и точные параметры, которые мы применили. он заставляет нас выполнять одинаковую обработку всех существующих выборок данных, обеспечивая четкий и воспроизводимый рабочий процесс. Важно отметить, что это позволяет нам позже выполнять те же самые шаги обработки на новых образцах. Этот последний пункт имеет решающее значение всякий раз, когда мы хотим применить нашу модель к новым данным — будь то для обучения нашей модели на тестовом наборе после обучения с использованием набора для обучения или для обработки новых точек данных и запуска модели в производстве.

Как применить трубопровод?

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

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

Давайте загрузим данные и посмотрим на первых пациентов:

import pandas as pd
train_df = pd.read_csv('toy_data.csv', index_col = 0)
test_df = pd.read_csv('toy_data_test.csv', index_col = 0)
train_df.head()

Наша предварительная обработка будет включать в себя вменение пропущенных значений и стандартное масштабирование. |Далее мы запустим оценщик RandomForestClassifier.

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

# import relevant packeges
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
# define our pipeline
pipe = Pipeline([('imputer', SimpleImputer()),('scaler', StandardScaler()), ('RF', RandomForestClassifier())])

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

Наконец, мы применяем полный конвейер к новым данным, используя метод predict(). Это запускает преобразования данных и прогнозирует результат с помощью оценщика.

X_train = train_df.drop(columns = ['High_risk'])
y_train = train_df['High_risk']
# fit and predict
pipe.fit (X_train, y_train)
pipe.predict (X_test)

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

pipe.fit_predict(X_train, y_train)

Настройте свой конвейер, написав свой собственный преобразователь

Как мы уже видели, конвейер — это просто последовательность преобразователей, за которыми следует оценщик, а это означает, что мы можем смешивать и сопоставлять различные этапы обработки, используя встроенные преобразователи Scikit-Learn (например, SimpleImputer, StandardScaler и т. д.).

Но что, если мы хотим добавить конкретный шаг обработки, который не является одним из обычных подозреваемых в обработке данных?

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

Однако, просмотрев полный набор данных, мы понимаем, что одна из характеристик — возраст — имеет отрицательные или подозрительно высокие значения:

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

Здесь мы напишем и добавим самодельный преобразователь: AgeImputer. Наш новый пайплайн теперь будет включать новый шаг перед импутером и скейлером:

pipe = Pipeline([('age_imputer', AgeImputer()),('imputer', SimpleImputer()),('scaler', StandardScaler()), ('RF', RandomForestClassifier())])

Как написать трансформатор?

Давайте начнем с рассмотрения структуры трансформатора и его методов.

Трансформатор — это класс Python. Чтобы любой преобразователь был совместим со Scikit-Learn, ожидается, что он будет состоять из определенных методов: fit(), transform(), fit_transform(), get_params() и set_params(). Метод fit() соответствует конвейеру; transform() применяет преобразование; и комбинированный метод fit_transform() подгоняет, а затем применяет преобразование к тому же набору данных.

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

Методы get_params() и set_params() унаследованы от класса BaseEstimator. Метод fit_transform() унаследован от класса TransformerMixin. Это делает нашу жизнь проще, потому что это означает, что нам нужно реализовать только методы fit() и transform() в нашем коде, а остальная магия произойдет сама по себе.

Приведенный ниже код иллюстрирует реализацию методов fit() и transform() нового преобразователя ImputeAge, описанного выше. Помните, мы хотим, чтобы наш преобразователь «запоминал» среднее значение возраста, а затем заменял невозможные значения этим значением. Метод __init__() (также называемый конструктором) инициирует экземпляр преобразователя с максимально допустимым возрастом в качестве входных данных. Метод fit() будет вычислять и сохранять среднее значение возраста (округленное для соответствия целочисленному формату возраста в данных), в то время как метод transform() будет использовать сохраненное значение среднего возраста для применения преобразования к данным.

# import packages
from sklearn.base import BaseEstimator, TransformerMixin
# define the transformer
class AgeImputer(BaseEstimator, TransformerMixin):
    
    def __init__(self, max_age):
        print('Initialising transformer...')
        self.max_age = max_age
        
    def fit(self, X, y = None):
        self.mean_age = round(X['Age'].mean())
        return self
    
    def transform(self, X):
        print ('replacing impossible age values')
        X.loc[(X[‘age’] > self.max_age) 
              |  (X[‘age’] < 0), “age”] = self.mean_age
        return X

Если мы хотим увидеть результат нашего преобразования, мы можем применить этот конкретный шаг конвейера и просмотреть преобразованные данные:

age_scaled = pipe[0].fit_transform(X_train)
age_imputed

Как и ожидалось, невозможные значения были заменены средним возрастом на основе состава поезда.

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

pipe.fit(X_train, y_train)
pipe.predict(X_test)

Приправьте его более сложными трансформерами

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

Но как решить, какие функциональные возможности принадлежат методу fit(), а какие — методу transform()?

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

  1. Требует ли этот этап какой-либо информации из исходных данных?
    Примеры такой информации включают среди прочего средние значения, стандартные отклонения, названия столбцов. Если ответ «да», необходимые базовые вычисления относятся к методу fit(), а сама стадия обработки относится к методу transform(). Это имело место в простом преобразователе ImputeAge(), где мы вычислили среднее значение в методе fit() и использовали его для изменения данных в методе transform().
  2. Требуется ли сам по себе этот этап обработки для извлечения информации, которая потребуется на более позднем этапе обработки? Например, теоретически у меня мог бы быть дополнительный этап ниже по течению, который требует стандартного отклонения каждой переменной. Предполагая, что я хочу, чтобы стандартное отклонение вычислялось для вмененных данных, мне нужно будет вычислить и сохранить стандартные значения преобразованного фрейма данных. В этом случае я включу этап обработки в метод transform(), а также в метод fit(), но в отличие от метода transform() метод fit() не будет возвращать преобразованные данные. Другими словами, метод fit() может применять преобразования к данным, если это необходимо для внутренних целей, если он не возвращает измененный набор данных.

В конечном итоге метод fit() последовательно выполнит все необходимые вычисления и сохранит их результаты, а метод transform() последовательно применит к данным все этапы обработки и вернет преобразованные данные.

Вот и все!

Заключить…

Мы начали с применения конвейера с использованием готовых трансформаторов. Затем мы рассмотрели структуру преобразователей и узнали, как написать собственный преобразователь и добавить его в наш конвейер. Наконец, мы рассмотрели основные правила, определяющие логику методов «подгонки» и «преобразования» преобразователя.

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

Если вы нашли эту статью полезной или у вас есть какие-либо отзывы, я буду рад прочитать их в комментариях!