Случайные леса всегда упоминаются как более мощная и стабильная версия «деревоподобных» моделей. В этом посте мы докажем, почему применение мудрости толпы к деревьям решений — хорошая идея.

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

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

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

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

В основном исследователи нашли два способа обойти эту проблему:

  • Повышение — когда вы объединяете и повторяете несколько слабых учеников в единый алгоритм — XGBoost — одна из самых известных реализаций.
  • Пакетирование — когда вы объединяете несколько моделей в ансамбль — например, Random Forests (RF).

RF используют результаты нескольких деревьев решений, объединяя их в единую систему голосования, которая помогает смягчить некоторые из их насущных проблем. Используя аналогию, если деревья решений — это диктатура и для принятия решений используется только мнение одного «разума», то случайные леса — это демократия, рассчитывающая на мудрость толпы для выполнения прогнозов.

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

Подгонка нескольких деревьев решений

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

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

  • Пассажир выжил;
  • Пассажир не выжил;

Давайте сначала подгоним 3 разных дерева одиночных решений. Один из самых распространенных способов контролировать и создавать совершенно разные деревья — это задавать им собственный набор гиперпараметров. Это означает, что полученные разные деревья решений будут немного отличаться друг от друга с разными разбиениями и переменными, которые будут использоваться для прогнозирования результата.

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

Чтобы оценить наши различные деревья решений, мы будем использовать 30% задержку в качестве чистого тестового набора:

# Reading the titanic train dataset
titanic <- read.csv('./train.csv')
# Obtaining the number of rows for training (70%)
size <- ceiling(0.7*nrow(titanic))
# Use an indexer to perform train and test split
set.seed(999)
train_index <- sample(
  seq_len(nrow(titanic)), size = size
  )
train_df <- titanic[train_index, ]
test_df <- titanic[-train_index, ]

Фрейм данных train_df содержит 624 пассажира, а test_df — 267.

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

Например, позвольте мне подобрать дерево решений номер 1, которое я назову oak_tree (называя наши разные деревья разными именами, это поможет нам немного лучше визуализировать наше упражнение). Вот мой код для его обучения:

library(rpart)
set.seed(9990)
oak_tree <- rpart(Survived ~ Fare + Age + Sex + Pclass,
                       data = sample_n(train_df, 600), 
                       method = 'class',
                       control = list(maxdepth = 2,
                                      minsplit=30))

(Не забудьте запустить обучающий код одновременно с set.seed, чтобы воспроизвести эти результаты)

Обратите внимание, что мы используем только 4 функции, чтобы предсказать, выжил ли кто-то после крушения Титаника: стоимость проезда, возраст, пол и класс билета. Чем мое дубовое_дерево отличается от других? Мое дубовое_дерево – это очень маленькое дерево глубиной всего 2, что позволяет выполнять разбиение в каждом узле не менее чем на 30 примеров. Если этот жаргон гиперпараметров сбивает вас с толку, проверьте это.

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

Итак, наше дубовое_дерево выглядит следующим образом:

Первая переменная (и только одна), используемая для определения результата в этом дереве, — это Пол. Кажется, что эта переменная обладает высокой дискриминантной силой, потому что, если пассажир — мужчина, у него больше шансов не выжить — около 82% против 26%, если пассажир не мужчина. Небольшая иллюстрация того, как работает эта схема:

Имейте в виду, что это конкретное дерево было построено таким образом из-за конкретной подвыборки из 600 клиентов и набора определенных нами гиперпараметров.

Теперь давайте обучим совершенно другое дерево, на этот раз я назову его pine_tree.

set.seed(9991)
pine_tree <- rpart(Survived ~ Fare + Age + Sex + Pclass,
                      data = sample_n(train_df, 600), 
                      method = ‘class’,
                      control = list(maxdepth = 3, 
                                     minsplit=3, 
                                     minbucket=4, 
                                     cp=0.01))

Нашему pine_tree разрешено идти немного глубже, чем нашему oak_tree. Кроме того, мы позволяем нашему дереву строить узлы с меньшим количеством примеров — как при разделении, так и при построении конечных конечных узлов.

Давайте проверим результат — обратите внимание на разницу между этой древовидной диаграммой и диаграммой oak_tree:

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

Показаны интересные потоки:

  • У детей, заплативших более 16 долларов и младше 2 лет, вероятность выживания составляет 86%.
  • Женщины высокого класса имеют наибольшую вероятность выживания (95%).

Теперь мы построили два разных дерева! Обратите внимание, что они показывают совершенно разные результаты, хотя наше pine_tree кажется естественным продолжением нашего oak_tree. Также ожидается, что pine_tree имеет большее переобучение, чем oak_tree, только из-за набора гиперпараметров.

Наконец, давайте подгоним еще одно дерево, elm_tree.

Elm_tree похож на pine_tree с существенным отличием — мы допускаем только значение max_depth, равное 2 в дереве.

set.seed(9992)
elm_tree <- rpart(Survived ~ Fare + Age + Sex + Pclass,
                data = sample_n(train_df, 600), 
                method = 'class',
                control = list(maxdepth = 2, 
                               minsplit=2, 
                               minbucket=4, 
                               cp=0.01))

Elm_tree выглядит следующим образом:

Обратите внимание, что Пол по-прежнему является переменной, определяющей первое разделение. Класс идет сразу после.

Эти 3 дерева кажутся похожими друг на друга — только с разной глубиной. Конечно, теперь нам нужно их оценить! Насколько они отличаются по производительности?

Оценка производительности каждого дерева

Для быстрой оценки производительности и независимости от любого порога давайте использовать AUC в качестве метрики для оценки наших моделей.

Более высокий AUC означает, что наша модель лучше разделяет классы 0 и 1, что делает нашу модель более эффективной и правильной в своих прогнозах.

Я использую библиотеку ROCR, чтобы получить AUC немного быстрее:

library(ROCR)
obtainauc <- function(model) {
  predictions <- predict(model, test_df)[,2]
  pred <- prediction(predictions, test_df$Survived)
  perf <- performance(pred, measure = 'auc')
  return ([email protected][[1]])
}

Производительность для наших деревьев следующая:

Из сюжета выше:

  • Дуб имеет AUC 0,778.
  • Pine Tree имеет AUC 0,832.
  • Вяз имеет AUC 0,807.

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

Таким образом, случайные леса сосредоточены на одном ключевом вопросе: «Даже если моя сосна лучшая, должен ли я полностью отказаться от дубаи вяза? мнение?»

Давайте построим еще один эксперимент — мы сделаем вариант голосования по большинству — усредним результат всех деревьев и сравним этот «ансамбль» мнений с каждым деревом в отдельности. Поскольку мы имеем дело с вероятностными результатами, мы просто усредним вероятность между тремя разными деревьями и будем считать это нашим объединенным голосованием.

Усреднение результатов

Чтобы было легче понять, что на самом деле означает «усреднение результатов», давайте немного поиграем.

Давайте представим, что я спрашиваю мнение Дуба, Сосны и Вяза о следующем человеке:

  • Выживет ли мужчина, заплативший менее 16 долларов за посадку на «Титаник»?

Оук Три говорит, что вероятность того, что этот пассажир выживет, составляет 18%. Pine Tree говорит, что шанс выжить составляет всего 10%. Вяз также говорит о том, что вероятность того, что этот человек выживет, составляет 18%.

Если мы усредним рекомендации каждого дерева, мы получим (18%+18%+10%)/3, что даст 15,3%используя среднее значение голосов. , это должно быть нашей окончательной вероятностью того, что этот пассажир выживет.

Посмотрим на другого пассажира:

  • Женщина-пассажир, которая путешествовала 1-м или 2-м классом.

Оук Три говорит, что вероятность того, что этот пассажир выживет, составляет 74%. И сосна, и вязы показывают вероятность 95%.

Если мы усредним эти рекомендации, мы получим 88% вероятности выживания.

Главный вопрос: как вы думаете, использование этого обоснования для всех наших пассажиров в тестовой выборке улучшит нашу производительность? Или из-за их худших характеристик наше мнение о дубах и вязах только занижает наш результат?

Давайте посмотрим!

Мудрость толпы

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

ensemble <- (
  predict(oak_tree, test_df)[,2]
  +
  predict(pine_tree, test_df)[,2]
  +
  predict(elm_tree, test_df)[,2]
)/3

Как насчет производительности этой простой модели ансамбля? Снова проверим с помощью библиотеки ROCR:

# Ensemble Performance
prediction <- prediction(ensemble, test_df$Survived)
perf <- performance(prediction, measure = 'auc')
performance_ensemble <- [email protected][[1]]

У нас есть следующие результаты AUC:

Ух ты! Несмотря на то, что наши дуб и вяз хуже, чем наша сосна, было важно учитывать их мнение при составлении прогнозов. Обратите внимание, что наша модель Ensemble имеет немного лучший AUC, чем лучшее отдельное дерево. Вывод: даже комбинируя это дерево с «более слабыми» деревьями, мы смогли повысить производительность.

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

Когда вы увеличиваете количество обучаемых деревьев — иногда до тысяч деревьев — это поведение становится еще более мощным. И, конечно же, по мере того, как вы увеличиваете количество обученных деревьев, вы повышаете вероятность того, что ваш «ансамбль» будет лучше, чем одно дерево решений, составляющее тот же самый «ансамбль».

Вот и все! Спасибо, что нашли время, чтобы прочитать этот пост, и я надеюсь, вам понравилось. Моя цель состояла в том, чтобы объяснить, почему случайные леса обычно лучше, чем отдельные деревья решений, и почему они являются отличным алгоритмом для рассмотрения в ваших проектах машинного обучения. Этот пример должен дать вам хорошее представление о допущении «мудрости толпы», стоящем за RF.

Я разработал курс по изучению Науки о данных с помощью R на Udemy — курс рассчитан на начинающих, содержит более 50 упражнений, и мне бы хотелось, чтобы вы были рядом! Вы изучите древовидные модели, регрессии и узнаете, как построить проект по науке о данных от начала до исходного уровня.

Вот небольшая суть с кодом, используемым в этом посте:

Лицензия на набор данных. Набор данных, использованный в этом сообщении, общедоступен для использования по адресу https://www.openml.org/d/40945.