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

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

Распределение классов

После очистки и выбора функций я посмотрел на распределение меток и обнаружил очень несбалансированный набор данных.

Существует три класса, перечисленные в порядке убывания: функциональные, нефункциональные и функциональные, нуждающиеся в ремонте. Их частота составила 54,3%, 38,4% и 7,3% соответственно.

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

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

Подготовка функции

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

Этот набор данных содержал в основном номинальные категориальные характеристики плюс несколько непрерывных числовых характеристик, которые необходимо обрабатывать по-своему.

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

Для своих номинальных категориальных характеристик я буду использовать OneHotEncoder от sklearn. Это займет каждую категориальную функцию с n уникальными значениями и создаст n функций, соответствующих n уникальным значениям, где для каждого экземпляра новая функция соответствует значению исходного объекта присваивается значение 1, а всем остальным функциям n-1 присваивается значение 0.

Я использовал конвейер и определил свой препроцессор следующим образом:

from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from imblearn.pipeline import Pipeline
numeric_transformer = Pipeline(steps=[('scaler', StandardScaler())])
categorical_transformer = Pipeline(steps=[('onehot', OneHotEncoder(handle_unknown='ignore'))])
preprocessor = ColumnTransformer(remainder='drop',
                                 transformers=[('num', numeric_transformer, cols_to_scale),
                                               ('cat', categorical_transformer, categorical_features)])

Передискретизация

Теперь пора заняться передискретизацией, о которой я говорил выше. В данном случае я использовал SMOTE from imblearn. Мне также нужно будет использовать Pipeline from imblearn, а не обычный класс Pipeline. Причина в том, что мы не хотим передискретизировать тестовый набор, и imblearn Pipeline пропустит SMOTE для тестового набора во время прогнозирования.

from imblearn.over_sampling import SMOTE
smote = SMOTE(sampling_strategy='not majority')

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

Классификатор случайного леса

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

from sklearn.ensemble import RandomForestClassifier
rfc = RandomForestClassifier()

Определите конвейер

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

clf = Pipeline(steps=[('preprocessor', preprocessor),
('sampling', smote),
('classifier', rfc)])

Тренировка тестового сплита

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

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(features, labels)

Кодировать метки / классы

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

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

Подходит для модели

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

clf.fit(X_train, y_train_encoded)

Проверить производительность модели

А теперь давайте поговорим о том, как модель работала? Я сосредоточил свою оценку модели на сбалансированном отзыве и высокой точности. Простой способ увидеть все эти показатели в одном месте - использовать classification_report from sklearn.

from sklearn.metrics import classification_report
# predict the labels for the test set using the model
y_hat_test = clf.predict(X_test)
print(classification_report(y_test_encoded, y_hat_test, target_names=le.classes_))

clf.predict запустит препроцессор, установленный на X_test, но пропустит SMOTE.

Результат будет выглядеть примерно так, как показано ниже.

                         precision    recall  f1-score   support

             functional       0.81      0.78      0.80      8031
functional needs repair       0.29      0.48      0.36      1084
         non functional       0.81      0.75      0.78      5692

               accuracy                           0.75     14807
              macro avg       0.64      0.67      0.65     14807
           weighted avg       0.77      0.75      0.76     14807

Как вы можете видеть, глядя на точность, отзыв и оценку f1 для класса меньшинства «функциональные потребности в ремонте», даже с SMOTE модель имеет проблемы с классом меньшинства, в то время как довольно хорошо справляется с двумя классами большинства. Точность 0,75 кажется довольно хорошей, но не отражает более низкий уровень отзыва (истинно положительный показатель) для класса меньшинства.

Стоило ли SMOTE того?

У нас были проблемы и время, чтобы настроить и запустить SMOTE, но стоило ли это вообще делать? Отзыв 0,48 для класса меньшинства не кажется таким уж хорошим без некоторого контекста. Я запустил вышеупомянутый конвейер с опущенным шагом SMOTE, и в результате был получен следующий отчет о классификации.

                         precision    recall  f1-score   support

             functional       0.78      0.87      0.83      8031
functional needs repair       0.47      0.25      0.33      1084
         non functional       0.81      0.75      0.78      5692

               accuracy                           0.78     14807
              macro avg       0.69      0.63      0.64     14807
           weighted avg       0.77      0.78      0.77     14807

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

Сравнение отчетов о классификации все еще может оставить вас в недоумении, а стоило ли этого SMOTE. Как и во многих других случаях, ответ может быть «в зависимости от обстоятельств». В моем случае я беспокоился о том, чтобы уровень отзыва, или истинно положительный показатель каждого класса, был высоким и как можно более ровным, поэтому падение отзыва с 0,48 до 0,25 было не очень хорошо. Если посмотреть на фактическое количество истинных положительных результатов для класса меньшинства, в версии с SMOTE это число было 520 против 270 без SMOTE. Таким образом, SMOTE дал мне почти вдвое больше истинных положительных результатов.

Заключение

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

Если вы хотите ознакомиться с моим полным проектом и дополнительным кодом, он размещен на github здесь.