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

Что такое настройка гиперпараметров?

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

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

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

  1. Подготовка данных: процесс очистки ваших данных и подготовки их к машинному обучению.
  2. Исследовательский анализ данных. Этот шаг следует выполнять всегда. Процесс изучения нового набора данных и понимания распределения данных, корреляций и многого другого. Смотрите этот пост для пошагового руководства.
  3. Разработка и выбор функций: процесс создания новых функций (столбцов) из ваших данных и выбора лучших функций в зависимости от того, какой вклад они вносят. производительность модели.
  4. Выбор модели. Использование перекрестной проверки для выбора алгоритма с наибольшей эффективностью на основе показателя оценки.
  5. Настройка гиперпараметров: процесс, описанный в этом посте.
  6. Оценка модели: выбор правильного показателя производительности и оценка результатов.

Примеры гиперпараметров

Некоторые примеры гиперпараметров:

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

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

Выбор модели

Для краткости мы пропустим первоначальную очистку и выбор функций. Этот код доступен в этом Блокноте на GitHub. Мы возьмем результаты нашего выбора функций и создадим переменные X и y.

X = df[['categories', 'postal_code', 'text_len', 'review_count', 'text_clean']]
y = df['target']

Далее у нас есть функция, которая позволяет нам многократно создавать конвейер вместе с экземпляром классификатора.

def create_pipe(clf, ngrams=(1,1)):

    column_trans = ColumnTransformer(
        [('Text', TfidfVectorizer(stop_words='english', ngram_range=ngrams), 'text_clean'),
         ('Categories', TfidfVectorizer(), 'categories'), 
         ('OHE', OneHotEncoder(dtype='int', handle_unknown='ignore'),['postal_code']),
         ('Numbers', MinMaxScaler(), ['review_count', 'text_len'])],
        remainder='drop') 

    pipeline = Pipeline([('prep',column_trans),
                         ('over', SMOTE(random_state=42)),
                         ('under', RandomUnderSampler(random_state=42)),
                         ('clf', clf)])

    return pipeline

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

models = {'RandForest' : RandomForestClassifier(random_state=42),
          'LogReg' : LogisticRegression(random_state=42)
          }

for name, model, in models.items():
    clf = model
    pipeline = create_pipe(clf)
    scores = cross_val_score(pipeline, 
                             X, 
                             y, 
                             scoring='f1_macro', 
                             cv=3, 
                             n_jobs=1, 
                             error_score='raise')
    print(name, ': Mean f1 Macro: %.3f and Standard Deviation: (%.3f)' % (np.mean(scores), np.std(scores)))
[OUT]
RandForest : Mean f1 Macro: 0.785 and Standard Deviation: (0.003)
LogReg : Mean f1 Macro: 0.854 and Standard Deviation: (0.001)

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

Доступ к параметрам модели в конвейерах

Одна из первых вещей, на которые я хочу обратить внимание, это как получить доступ к параметрам модели в конвейере. Обычно, когда у вас есть экземпляр оценщика (модели), вы вызываете estimator.get_params() и видите их. Процесс такой же в трубопроводах; однако результирующий результат немного отличается.

При доступе к параметрам непосредственно из оценщика на выходе будет значение, например C. Напротив, в конвейере выходные данные сначала будут иметь имя, которое вы дали оценщику, вместе с двойным подчеркиванием, а затем, наконец, имя параметра, например clf__C; знать, как получить доступ к параметрам, важно, поскольку вам нужны эти имена для построения таблицы параметров для поиска.

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

pipeline.get_params()
{'memory': None,
 'steps': [('prep',
   ColumnTransformer(transformers=[('Text', TfidfVectorizer(stop_words='english'),
                                    'text_clean'),
                                   ('Categories', TfidfVectorizer(), 'categories'),
                                   ('OHE',
                                    OneHotEncoder(dtype='int',
                                                  handle_unknown='ignore'),
                                    ['postal_code']),
                                   ('Numbers', MinMaxScaler(),
                                    ['review_count', 'text_len'])])),

... truncated for brevity ...

 'clf__C': 1.0,
 'clf__class_weight': None,
 'clf__dual': False,
 'clf__fit_intercept': True,
 'clf__intercept_scaling': 1,
 'clf__l1_ratio': None,
 'clf__max_iter': 500,
 'clf__multi_class': 'auto',
 'clf__n_jobs': None,
 'clf__penalty': 'l2',
 'clf__random_state': 42,
 'clf__solver': 'lbfgs',
 'clf__tol': 0.0001,
 'clf__verbose': 0,
 'clf__warm_start': False}

Поиск по сетке

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

parameters = [{'clf__solver' : ['newton-cg', 'lbfgs', 'sag', 'liblinear'],'clf__C' : [.1, 1, 10, 100, 1000], 'prep__Text__ngram_range': [(1, 1), (2, 2), (1, 2)]}]

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

parameters = [
  {'clf__penalty': ['l1', 'l2'], 'clf__solver' : ['liblinear']},
  {'clf__penalty': ['l1', 'none'], 'clf__solver' : ['newton-cg']},
  ]

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

clf = LogisticRegression(random_state=42, max_iter=500)
pipeline = create_pipe(clf)

Далее мы запустим поиск по сетке с помощью GridSearchCV.

%time grid = GridSearchCV(pipeline, 
                          parameters, 
                          scoring='f1_macro', 
                          cv=3,
                          random_state=0).fit(X_train, y_train)

print("Best cross-validation accuracy: {:.3f}".format(grid.best_score_))
print("Test set score: {:.3f}".format(grid.score(X_test, y_test))) 
print("Best parameters: {}".format(grid.best_params_))

log_C = grid.best_params_['clf__C']
log_solver = grid.best_params_['clf__solver']
log_ngram = grid.best_params_['prep__Text__ngram_range']
58m 3s

Best cross-validation accuracy: 0.867
Test set score: 0.872
Best parameters: {'clf__C': 100, 'clf__solver': 'newton-cg', 
'prep__Text__ngram_range': (1, 2)}

Наш поиск по сетке занял 58m 3s времени, и для каждого из них были получены наилучшие параметры.

Одна из вещей, которая может броситься вам в глаза при просмотре приведенного выше списка, заключается в том, что в нашей сетке параметров есть довольно много потенциальных комбинаций. В приведенном выше примере есть 4x solvers, 5x C, 3x n-grams, в результате чего общая сумма составляет 4 x 5 x 3 = 60. Поскольку обучение нашей модели заняло около одной минуты, однократное линейное прохождение сетки занимает около часа.

Примечание. Поиск по сетке можно распараллелить с помощью аргумента n_jobs=-1; однако я не показал относительную производительность для этого примера.

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

Сокращение поиска

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

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

%time grid = HalvingGridSearchCV(pipeline, 
                                 parameters, 
                                 scoring='f1_macro', 
                                 cv=3, 
                                 random_state=0).fit(X_train, y_train)


print("Best cross-validation accuracy: {:.3f}".format(grid.best_score_))
print("Test set score: {:.3f}".format(grid.score(X_test, y_test))) 
print("Best parameters: {}".format(grid.best_params_))

log_C_b = grid.best_params_['clf__C']
log_solver_b = grid.best_params_['clf__solver']
log_ngram_b = grid.best_params_['prep__Text__ngram_range']
14m 28s

Best cross-validation accuracy: 0.867
Test set score: 0.872
Best parameters: {'clf__C': 100, 'clf__solver': 'lbfgs', 
'prep__Text__ngram_range': (1, 2)}

Довольно впечатляющий! с часа до 15 минут! В некоторых случаях я видел, что он работает даже быстрее. Мы также можем видеть, что результаты очень похожи. На этот раз было выбрано решение lbfgs против newton-cg. Теперь мы можем сравнить производительность обоих.

Оценка результатов

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

Примечание. Мы используем здесь показатель оценки F1-Macro; мы стремимся сбалансировать точность и отзыв.

def fit_and_print(pipeline, name):

    pipeline.fit(X_train, y_train)
    y_pred = pipeline.predict(X_test)
    score = metrics.f1_score(y_test, y_pred, average='macro')

    print(metrics.classification_report(y_test, y_pred, digits=3))

    ConfusionMatrixDisplay.from_predictions(y_test, 
                                            y_pred, 
                                            cmap=plt.cm.Greys)

    plt.tight_layout()
    plt.title(name)
    plt.ylabel('True Label')
    plt.xlabel('Predicted Label')
    plt.tight_layout()
    plt.savefig(name + '.png', dpi=300) 
    plt.show;
clf = LogisticRegression(random_state=42, max_iter=500)
pipeline = create_pipe(clf)
fit_and_print(pipeline, 'Default Parameters')
precision    recall  f1-score   support

           0      0.789     0.845     0.816      9545
           1      0.925     0.894     0.909     20370

    accuracy                          0.879     29915
   macro avg      0.857     0.869     0.863     29915
weighted avg      0.882     0.879     0.880     29915

Наша оценка F1-Macro здесь составляет 0.863. Далее давайте попробуем настроенную модель Grid Search.

clf = LogisticRegression(C=log_C, solver=log_solver, random_state=42, max_iter=500)
pipeline = create_pipe(clf, log_ngram)
fit_and_print(pipeline, 'GridSearch Parameters')
precision    recall  f1-score   support

           0      0.839     0.810     0.824      9545
           1      0.913     0.927     0.920     20370

    accuracy                          0.890     29915
   macro avg      0.876     0.869     0.872     29915
weighted avg      0.889     0.890     0.889     29915

Наша оценка F1-Macro здесь составляет 0.872. Наш процесс настройки улучшил общие результаты для режима, и мы увеличили оценку F1-Macro на 0.09. Далее давайте попробуем настроенную модель Halving Grid Search.

clf = LogisticRegression(C=log_C_b, solver=log_solver_b, random_state=42, max_iter=500)
pipeline = create_pipe(clf, log_ngram_b)
fit_and_print(pipeline, 'HalvingGridSearch Parameters')
precision    recall  f1-score   support

           0      0.839     0.811     0.824      9545
           1      0.913     0.927     0.920     20370

    accuracy                          0.890     29915
   macro avg      0.876     0.869     0.872     29915
weighted avg      0.889     0.890     0.889     29915

Наконец, мы видим результаты модели поиска по сетке Halving. Оценка F1-Macro такая же, как и у модели Grid Search. Мы сократили время настройки с 60 минут до 15 без ущерба для результатов настройки. Каждый раз, когда вы используете эти методы, ваши результаты могут отличаться, но это отличный способ настроить модель, не отнимая у вас массу времени.

Заключение

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

Если вы хотите воссоздать это, весь этот код доступен в Блокноте на GitHub.

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