Поднимем оптимизацию на ступеньку выше

Введение

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

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

Так, например, мы дадим ему полный набор данных со 150 столбцами, а оптимизация выберет подмножество столбцов (скажем, 50), а также саму структуру конвейера. Таким образом, нам не нужно выбирать, как вводить или предварительно обрабатывать данные; сама модель (последний шаг конвейера, на котором делаются прогнозы) также будет оптимизирована.

Гипер-гиперпараметры?

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

Посмотрите на это так: гиперпараметры (например, max_depth или n_estimators) — это переменные, которые так или иначе определяют, как будет происходить обучение; поэтому по определению гиперпараметры — это то, что не оценивается во время обучения. С этой точки зрения мы могли бы добавить алгоритм, импутер, скейлер в качестве новых переменных в оптимизацию, где первый выбирает алгоритм обучения (логистическая регрессия, случайный лес и т. д.), второй выбирает метод импутирования (среднее, медиана и т. д.). .), а последний выбирает метод масштабирования (стандартизация, мин-макс масштабирование и т. д.).

Вы готовы? Большой.

Рабочий процесс

Эта статья будет построена следующим образом:

  1. Напишите функцию Choose_columns, которая позволит optuna напрямую взаимодействовать с набором данных.
  2. Напишите функцию instanceiate_learnner, которая позволит optuna протестировать несколько разных алгоритмов во время оптимизации.
  3. Аккуратно переопределите функции instanceiate_processor и instanceiate_model.

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

Выбор столбца

Давайте сразу:

from optuna import Trial

def choose_columns(trial : Trial, columns : list[str]) -> list[str]:
  choose = lambda column: trial.suggest_categorical(column, [True, False])
  choices = [*filter(choose, columns)]
  return choices

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

Теперь, очевидно, есть много разных способов сделать это. Еще один, я думаю, достойный упоминания, это использование ColumnSelector scikit-lego:

from sklego.preprocessing import ColumnSelector

def instantiate_column_selector(trial : Trial, columns : list[str]) -> ColumnSelector:
  choose = lambda column: trial.suggest_categorical(column, [True, False])
  choices = [*filter(choose, columns)]
  selector = ColumnSelector(choices)
  return selector

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

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

Выбор оценщика

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

from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier

Classifier = (
  RandomForestClassifier |
  ExtraTreesClassifier |
  SVC |
  LogisticRegression |
  KNeighborsClassifier
)

def instantiate_learner(trial : Trial) -> Classifier:
  algorithm = trial.suggest_categorical(
    'algorithm', ['logistic', 'forest', 'extra_forest', 'svm', 'knn']
  )
  if algorithm=='logistic':
    model = instantiate_logistic_regression(trial)
  elif algorithm=='forest':
    model = instantiate_random_forest(trial)
  elif algorithm=='extra_forest':
    model = instantiate_extra_forest(trial)
  elif algorithm=='svm':
    model = instantiate_svm(trial)
  elif algorithm=='knn':
    model = instantiate_knn(trial)
  
  return model

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

from sklearn.preprocessing import (
  StandardScaler, MinMaxScaler, MaxAbsScaler, RobustScaler
)

Scaler = (
  StandardScaler |
  MinMaxScaler |
  MaxAbsScaler |
  RobustScaler
)

def instantiate_scaler(trial : Trial) -> Scaler:
  method = trial.suggest_categorical(
    'scaling_method', ['standard', 'minmax', 'maxabs', 'robust']
  )
  
  if method=='standard':
    scaler = instantiate_standard_scaler(trial)
  elif method=='minmax':
    scaler = instantiate_minmax_scaler(trial)
  elif method=='maxabs':
    scaler = instantiate_maxabs_scaler(trial)
  elif method=='robust':
    scaler = instantiate_robust_scaler(trial)
  
  return scaler

Просто, верно? Следует, однако, отметить, что некоторые функции инстанцирования не могут выполнять оптимизацию сами по себе. Например, MaxAbsScaler не имеет гиперпараметров для настройки, поэтому функция инстанцирования — не более чем инструмент унификации API.

То же самое для стратегии кодирования с использованием как scikit-learn, так и кодировщиков категорий:

from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder
from category_encoders import WOEEncoder

Encoder = (
  OrdinalEncoder |
  OneHotEncoder |
  WOEENcoder
)

def instantiate_encoder(trial : Trial) -> Encoder:
  method = trial.suggest_categorical(
    'encoding_method', ['ordinal', 'onehot', 'woe']
  )
  
  if method=='ordinal':
    encoder = instantiate_ordinal_encoder(trial)
  elif method=='onehot':
    encoder = instantiate_onehot_encoder(trial)
  elif method=='woe':
    encoder = instantiate_woe_encoder(trial)
  
  return encoder

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

Окончательное воплощение

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

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

def instantiate_numerical_pipeline(trial : Trial) -> Pipeline:
  pipeline = Pipeline([
    ('imputer', instantiate_numerical_simple_imputer(trial)),
    ('scaler', instantiate_scaler(trial))
  ])
  return pipeline

def instantiate_categorical_function(trial : Trial) -> Pipeline:
  pipeline = Pipeline([
    ('imputer', instantiate_categorical_simple_imputer(trial)),
    ('encoder', instantiate_encoder(trial))
  ])
  return pipeline

def instantiate_processor(trial : Trial, numerical_columns : list[str], categorical_columns : list[str]) -> ColumnTransformer:
  
  numerical_pipeline = instantiate_numerical_pipeline(trial)
  categorical_pipeline = instantiate_categorical_pipeline(trial)
  
  selected_numerical_columns = choose_columns(numerical_columns)
  selected_categorical_columns = choose_columns(categorical_columns)
  
  processor = ColumnTransformer([
    ('numerical_pipeline', numerical_pipeline, selected_numerical_columns),
    ('categorical_pipeline', categorical_pipeline, selected_categorical_columns)
  ])
  
  return processor

def instantiate_model(trial : Trial, numerical_columns : list[str], categorical_columns : list[str]) -> Pipeline:
  
  processor = instantiate_processor(
    trial, numerical_columns, categorical_columns
  )
  
  learner = instantiate_learner(trial)
  
  model = Pipeline([
    ('processor', processor),
    ('model', learner)
  ])
  
  return model

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

Теперь о целевой функции… Подождите! Нам действительно нужно написать новую целевую функцию?

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

Некоторые мысли

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

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

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

С учетом этого, Части 3 и 4 мы будем ориентировать дискуссию на то, чтобы узнать optuna немного лучше, так что следите за обновлениями…

Спасибо за прочтение!