Это сложно, но благодаря великолепным объяснительным способностям Джереми Ховарда задача теперь кажется намного проще! Джереми реализует случайные леса с нуля в своей лекции РФ с нуля.
Для более глубокого понимания того, как работают случайные леса, вы можете обратиться к моей предыдущей статье Что такое случайные леса и как они на самом деле работают? »Или эту лекцию Глубокое погружение в случайные леса Джереми Ховарда.
Чтобы реализовать случайные леса с нуля, мы будем использовать нисходящий подход, предполагая, что у нас есть все части, необходимые для реализации классов. Если вы новичок в объектно-ориентированном программировании, вы можете обратиться к моей предыдущей статье Ужасное ООП - простой учебник по Python, чтобы пройти ускоренный курс.
Предполагая, что вы теперь знакомы с тем, как работают случайные леса и ООП, давайте приступим к реализации нашей собственной версии случайных лесов на Python. Затем мы сравним результаты с реализацией RandomForestRegressor из Sci-kit learn.
Шаг 1: определение класса RF
Здесь мы определяем класс под названием TreeEnsemble.
Конструктор __init__
создает объект TreeEnsemble каждый раз, когда он инициируется. Параметры поясняются ниже:
x
: входная матрица X
y
: Зависимая переменная
n_trees
: Количество деревьев в ансамбле
sample_sz
: Размер выборки матрицы X, переданной каждому дереву (уменьшает переоснащение, поскольку каждое дерево не имеет доступа ко всем входным данным X)
min_leaf
: Количество элементов, после которого дальнейшее разделение прекращается
self.trees = [self.create_tree() for i in range(n_trees)]
- это список в Python, который создает n_trees
количество деревьев, которые являются частью ансамбля, то есть случайного леса.
create_tree()
сначала выбирает случайные индексы, равные размеру выборки из X-матрицы. Затем эти случайные индексы передаются в дерево решений. Каждое дерево решений получает разные перестановки индексов с одинаковым размером выборки. (Если вы не понимаете, почему мы передаем случайные индексы в дерево решений, это необходимо для минимизации корреляции между каждым оценщиком и создания слабых классификаторов, которые вместе образуют сильный классификатор)
predict()
метод возвращает среднее значение прогноза каждого дерева для каждой строки, что приводит к окончательному прогнозу случайного леса. Следовательно, вызов np.mean([t.predict() for t in self.trees], axis = 0)
приводит к len(self.trees)
количеству предсказаний из каждого дерева, а взятие их mean()
результатов в окончательном предсказании нашим TreeEnsemble
, то есть случайным лесом.
Шаг 2: Определение дерева решений
Как вы могли заметить, в нашем нисходящем подходе мы предполагали, что дерево решений уже существует, и поэтому отложили размышления о его реализации до шага 2. Теперь мы реализуем объект дерева решений, и вместе это создаст наш ансамбль случайного леса.
Здесь мы определяем class DecisionTree()
и передаем все аргументы, которые были переданы нашему TreeEnsemble()
, кроме n_trees
, который здесь не имеет значения.
Конструктор DecisionTree
__init__
создает экземпляр дерева решений с теми же переменными и значением, что и наш случайный лес. Следует отметить одно важное отличие: мы не имеем дело со случайностью внутри дерева решений. К тому времени, когда индексы переданы в дерево решений, они уже выбраны TreeEnsemble
как случайные индексы. Следовательно, нас интересует только создание разбиений и возвращение прогнозов для каждого Дерева.
self.n, self.c = len(idxs), shape[1]
просто сохраняет количество строк в self.n
и количество столбцов в self.c
. self.val
- это прогнозируемое значение в данном узле, которое является mean()
значения зависимой переменной всех выборок, присутствующих в данном узле.
Взгляните на это дерево решений, созданное с использованием набора данных Ames Housing.
В каждом узле, начиная сверху, у нас есть 123, 250, 90, 133… количество выборок, из которых 67 выборок находятся в самом нижнем узле. Для каждого из этих узлов значение, предсказанное деревом решений, представляет собой просто среднее значение значений зависимой переменной для каждой выборки, присутствующей в этом узле.
Следовательно, прогнозируемое значение для самого верхнего узла составляет 11,807, а прогнозируемое значение для самого нижнего узла - 12,775.
Затем нам понадобится способ отслеживать information gain
в дереве решений, чтобы получить наилучшее разбиение. Это делается с помощью переменной оценки, которая показывает, насколько удачным было разделение. Так как все остальные узлы, другие leaf_nodes
, имеют счет, и на данный момент мы не делали никаких разделений, начальным score
является infinity
.
Затем мы переходим к методу find_varsplit()
, который находит лучший результат для пары переменная-значение, которая дает нам максимальный выигрыш информации или максимальную потерю ошибок. Для этого мы вызываем метод find_bettersplit()
, который мы определим позже в этой статье, чтобы отложить размышления о сложности и сначала подготовить все остальные части.
Мы также определяем определенные свойства, такие как:
split_name
, который возвращает столбец / переменную, в которой было выполнено разделение, split_col
: значение в этом конкретном столбце и is_leaf
, которое возвращает, является ли данный узел листом или нет.
Шаг 3: Найдите разделение
На этом этапе мы инициализировали наш класс Random Forest, а также класс Decision Tree. Следующая часть головоломки - определить метод, реализующий разделение на каждом узле. Это достигается с помощью метода find_better_split()
, как описано ниже.
Если вы помните, дерево решений находит разбиение, просматривая каждую переменную и возможные значения, и возвращает пару variable-value
, которая имеет наибольшее информационное усиление.
Разделение разветвляет выборку на левую и правую части на основе условия разделения. self.n
представляет количество строк или наблюдений. Поэтому для каждого индекса мы перебираем каждую строку, разделяем выборки для каждого значения строки, определяем lhs
и rhs
на основе разделения.
Если количество выборок в lhs
или rhs
больше, чем в min_samples_leaf
, продолжайте и сохраните значения standard_deviation
для каждой из левой и правой стороны. Минимизировать std
отклонение - это то же самое, что минимизировать RMSE
. (Почему? Думайте о каждом разбиении как о двоичном разбиении между кошками и собаками. Мы хотим сделать каждое разбиение как таковое, чтобы иметь однородные классы с каждой стороны, то есть кошки с одной стороны и собаки с другой. Таким образом, минимизация std_error
)
curr_score
разделения по каждому значению / наблюдению определяется как средний балл lhs
и rhs
. Если найдена лучшая variable-value
пара с более низким показателем, эта переменная передается в var_idx
, а строка передается в self.split
. Мы продолжаем этот процесс, пока не найдем лучший раскол.
Шаг 4: объединение дерева решений
На этом этапе у нас есть метод, который находит лучшее разбиение на каждом узле. Однако дерево решений может дополнительно разделить этот узел на большее количество узлов, пока мы не получим наименьшее std_error
. Мы еще не определили этот метод в нашем классе DecisionTree
. Это достигается путем изменения метода find_varsplit()
, как показано ниже.
Дерево проходит через каждый столбец от self.c
до find_better_split
для каждого наблюдения. Если узел is_leaf
, мы можем остановить процесс, так как листовой узел не будет разделяться дальше. Однако, если это не листовой узел, определите x
как столбец, по которому выполняется разделение, создайте lhs
и rhs
на основе значения разделения и рекурсивно создайте дерево решений слева и справа. сторона, которая разделяется дальше, пока мы не достигнем листового узла.
Шаг 5: Собираем все вместе
Теперь у нас есть все элементы, необходимые для построения ансамбля случайного леса. На этом этапе я просто скопировал код, чтобы собрать все вместе как единое целое gist
.
Так что у нас это! Наш собственный Случайный лес, построенный с нуля.
Не стесняйтесь обращаться ко мне по адресу https://linkedin.com/in/aroraaman/. Жду ваших отзывов.