Быстрый совет по науке о данных № 004: использование пользовательских преобразователей в конвейерах Scikit-Learn!

Узнайте, как использовать пользовательские преобразователи данных в одном и том же конвейере Scikit-Learn.

Всем привет. Мы снова вернулись с постом, дополняющим совет из предыдущего поста о том, как вообще создавать пайплайны Scikit-Learn. Если вы пропустили это, теперь вы можете проверить это по этой ссылке. (Где он теперь официально опубликован в Towards Data Science. w00t!) И, как всегда, если вы хотите напрямую следовать коду этого поста, вы можете найти его здесь, на моем личном GitHub.

Чтобы быстро завершить то, на чем мы остановились в предыдущем посте, мы успешно создали конвейер Scikit-Learn, который выполняет все преобразования данных, масштабирование и вывод в одном чистом маленьком пакете. Но до сих пор нам приходилось использовать стандартные преобразователи Scikit-Learn в нашем конвейере. Какими бы замечательными ни были эти трансформеры, было бы здорово, если бы мы могли использовать наши собственные пользовательские трансформации? Да, конечно! Я бы сказал, что это не только здорово, но и необходимо. Если вы помните из поста на прошлой неделе, мы построили модель на основе одной функции. Это не очень предсказуемо!

Поэтому мы собираемся исправить это, добавив два преобразователя для преобразования двух дополнительных полей из набора обучающих данных. (Я знаю, переход от 1 к 3 функциям все еще не очень хорош. Но эй, по крайней мере, мы увеличиваемся на 300%?) Исходной переменной, с которой мы начали, был «Пол» (он же пол), а теперь мы мы добавим трансформаторы для соответствующих столбцов «Возраст» и «Отправление».

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

# Importing the libraries we’ll be using for this project
import pandas as pd
import joblibfrom sklearn.preprocessing import OneHotEncoder, StandardScaler, FunctionTransformer
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score, accuracy_score, confusion_matrix

И мы продолжим и сделаем быстрый импорт наших обучающих данных.

# Importing the training dataset
raw_train = pd.read_csv(‘../data/titanic/train.csv’)
# Splitting the training data into appropriate training and validation sets
X = raw_train.drop(columns = [‘Survived’])
y = raw_train[[‘Survived’]]
X_train, X_val, y_train, y_val = train_test_split(X, y, random_state = 42)

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

# Creating a preprocessor to transform the ‘Sex’ column
data_preprocessor = ColumnTransformer(transformers = [
   (‘sex_transformer’, OneHotEncoder(), [‘Sex’])
])
# Creating our pipeline that first preprocesses the data, then scales the data, then fits the data to a RandomForestClassifier
rfc_pipeline = Pipeline(steps = [
   (‘data_preprocessing’, data_preprocessor),
   (‘data_scaling’, StandardScaler()),
   (‘model’, RandomForestClassifier(max_depth = 10,
                                    min_samples_leaf = 3,
                                    min_samples_split = 4,
                                    n_estimators = 200))
])

Первое, что мы можем сделать перед добавлением наших пользовательских преобразователей в конвейер, — это, конечно же, создать функциональные преобразователи! Итак, как вы могли догадаться, пользовательские преобразователи строятся прямо поверх обычных функций, поэтому вы можете написать любую функцию Python для преобразователя.**** (Мы вернемся ко всем этим звездочкам позже… )

Итак, мы поговорили о добавлении двух преобразователей для двух новых переменных, так что давайте приступим к созданию двух наших пользовательских функций Python! Сначала коснувшись столбца «Возраст», мы собираемся немного повеселиться с этой переменной. Теперь я действительно не знаю, является ли сам возраст здесь прогностической переменной, но я предположил, что если «Возраст» можно предсказать каким-либо осмысленным образом, то это будут возрастные категории / возрастные интервалы. Тем не менее, я разделил возраст на категории, такие как «Ребенок», «Взрослый», «Пожилой» и другие. Опять же, я понятия не имею, будет ли это более производительным, чем использование прямых целых чисел, но это позволяет нам немного повеселиться! Вот как выглядит код для этого:

# Creating a function to appropriately engineer the ‘Age’ column
def create_age_bins(col):
    ‘’’Engineers age bin variables for pipeline’’’
 
    # Defining / instantiating the necessary variables
    age_bins = [-1, 12, 18, 25, 50, 100]
    age_labels = [‘child’, ‘teen’, ‘young_adult’, ‘adult’, ‘elder’]
    age_imputer = SimpleImputer(strategy = ‘median’)
    age_ohe = OneHotEncoder()
 
    # Performing basic imputation for nulls
    imputed = age_imputer.fit_transform(col)
    ages_filled = pd.DataFrame(data = imputed, columns = [‘Age’])
 
    # Segregating ages into age bins
    age_cat_cols = pd.cut(ages_filled[‘Age’], bins = age_bins, labels = age_labels)
    age_cats = pd.DataFrame(data = age_cat_cols, columns = [‘Age’])
 
    # One hot encoding new age bins
    ages_encoded = age_ohe.fit_transform(age_cats[[‘Age’]])
    ages_encoded = pd.DataFrame(data = ages_encoded.toarray())
 
    return ages_encoded

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

# Creating function to appropriately engineer the ‘Embarked’ column
def create_embarked_columns(col):
    ‘’’Engineers the embarked variables for pipeline’’’
 
    # Instantiating the transformer objects
    embarked_imputer = SimpleImputer(strategy = ‘most_frequent’)
    embarked_ohe = OneHotEncoder()
 
    # Performing basic imputation for nulls
    imputed = embarked_imputer.fit_transform(col)
    embarked_filled = pd.DataFrame(data = imputed, columns = [‘Embarked’])
 
    # Performing OHE on the col data
    embarked_columns = embarked_ohe.fit_transform(embarked_filled[[‘Embarked’]])
    embarked_columns_df = pd.DataFrame(data = embarked_columns.toarray())
 
 return embarked_columns_df

Теперь, когда у нас есть написанные пользовательские функции, мы наконец можем добавить их в наш конвейер. И разве вы не знаете, но в Scikit-Learn есть специальный метод для обработки этих специальных пользовательских преобразователей, который называется FunctionTransformer. Его довольно легко реализовать, поэтому давайте посмотрим, как это будет выглядеть, когда мы добавим его в наш исходный конвейер.

# Creating a preprocessor to transform the ‘Sex’ column
data_preprocessor = ColumnTransformer(transformers = [
    (‘sex_transformer’, OneHotEncoder(), [‘Sex’]),
    (‘age_transformer’, FunctionTransformer(create_age_bins, validate = False), [‘Age’]),
    (‘embarked_transformer’, FunctionTransformer(create_embarked_columns, validate = False), [‘Embarked’])
])
# Creating our pipeline that first preprocesses the data, then scales the data, then fits the data to a RandomForestClassifier
rfc_pipeline = Pipeline(steps = [
    (‘data_preprocessing’, data_preprocessor),
    (‘data_scaling’, StandardScaler()),
    (‘model’, RandomForestClassifier(max_depth = 10,
                                     min_samples_leaf = 3,
                                     min_samples_split = 4,
                                     n_estimators = 200))
])

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

# Fitting the training data to our pipeline
rfc_pipeline.fit(X_train, y_train)
# Saving our pipeline to a binary pickle file
joblib.dump(rfc_pipeline, ‘model/rfc_pipeline.pkl’)

****ВЕРНИТЕ ВРЕМЯ ЗВЕЗДОЧКИ!!!

Итак…….. есть своего рода недостаток в использовании пользовательских трансформаторов….

Сериализованная модель НЕ хранит сам код для ЛЮБОЙ пользовательской функции Python. (По крайней мере… не так, как я понял.) Тем не менее, чтобы использовать эту десериализованную модель, pickle должен иметь возможность ссылаться на тот же код, написанный для функции преобразователь вне его собственных двоичных значений. Или, с точки зрения непрофессионала, вам нужно добавить свои пользовательские функции Python в любой сценарий развертывания, который вы пишете для такой модели.

Теперь это раздражает? да. Но дает ли это мне повод *не* использовать пользовательские преобразования? Это простое и твердое НЕТ. Я понимаю, что неудобно предоставлять дополнительный пользовательский код для запуска вашего конвейера, но компромисс заключается в преобразовании, которое, вероятно, сделает производительность вашей модели намного лучше, чем она была бы в противном случае.

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

И на этом пост заканчивается! Надеюсь, вам понравилось. Если вы хотите, чтобы я рассказал о чем-то конкретном в будущем посте, дайте мне знать! У меня есть еще несколько идей, которые крутятся в моей голове, так что обязательно следите за обновлениями. 😃