Быстрый совет по науке о данных № 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 в любой сценарий развертывания, который вы пишете для такой модели.
Теперь это раздражает? да. Но дает ли это мне повод *не* использовать пользовательские преобразования? Это простое и твердое НЕТ. Я понимаю, что неудобно предоставлять дополнительный пользовательский код для запуска вашего конвейера, но компромисс заключается в преобразовании, которое, вероятно, сделает производительность вашей модели намного лучше, чем она была бы в противном случае.
Так что да, это воняет, но эй, я бы, скорее всего, каждый раз добавлял кастомные трансформеры. Большинство наборов данных содержат широкий спектр функций, которые, безусловно, не будут разбиты на простые, простые преобразования, такие как вменение или одно горячее кодирование. Реальные данные беспорядочны и часто требуют специальной очистки, и эти специальные преобразователи как раз подходят для этой работы.
И на этом пост заканчивается! Надеюсь, вам понравилось. Если вы хотите, чтобы я рассказал о чем-то конкретном в будущем посте, дайте мне знать! У меня есть еще несколько идей, которые крутятся в моей голове, так что обязательно следите за обновлениями. 😃