Это сложно, но благодаря великолепным объяснительным способностям Джереми Ховарда задача теперь кажется намного проще! Джереми реализует случайные леса с нуля в своей лекции РФ с нуля.

Для более глубокого понимания того, как работают случайные леса, вы можете обратиться к моей предыдущей статье Что такое случайные леса и как они на самом деле работают? »Или эту лекцию Глубокое погружение в случайные леса Джереми Ховарда.

Чтобы реализовать случайные леса с нуля, мы будем использовать нисходящий подход, предполагая, что у нас есть все части, необходимые для реализации классов. Если вы новичок в объектно-ориентированном программировании, вы можете обратиться к моей предыдущей статье Ужасное ООП - простой учебник по 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/. Жду ваших отзывов.