Написание популярного алгоритма дерева регрессии на Python и объяснение того, что скрывается под капотом
Эта статья призвана представить читателям код и интуицию, лежащую в основе алгоритма дерева регрессии в Python. Я считаю, что просмотр кода алгоритма является очень хорошим обучающим инструментом, чтобы понять, что происходит под капотом. Надеюсь, читателям это тоже пригодится.
Объясняемый алгоритм - это алгоритм дерева регрессии. Он используется для моделирования взаимосвязи между непрерывной переменной Y и набором функций X:
Y = f(X)
Функция f - это набор правил свойств и значений характеристик, который «наилучшим образом» объясняет переменную Y с учетом характеристик X.
Эта статья является продолжением предыдущей статьи о деревьях решений:
Я предполагаю, что читатель знаком с концепцией узла, разбиения и уровня дерева. Все эти концепции объясняются в предыдущей статье.
В предыдущей статье переменная Y была двоичной переменной, содержащей два значения - 0 и 1.
Деревья регрессии - это средства оценки, которые имеют дело с переменной непрерывного отклика Y. Например, рост, зарплата, клики и т. д. Алгоритм написан и реализован (а также с дополнительной записной книжкой) в моем репозитории GitHub:
Класс Node для регрессии очень похож на класс дерева двоичной классификации из предыдущей статьи:
Каждый узел, независимо от его уровня, имеет следующие основные атрибуты:
Все вышеперечисленные атрибуты будут использоваться в процедуре разделения.
Подготовка объектов перед разделением такая же, как и в дереве классификационных решений:
Для каждой числовой характеристики в нашем наборе данных мы сортируем значения функций и получаем средние двух соседних значений.
Например, предположим, что наша функция_1 следующая:
feature_1 = [7, 5, 9, 1, 2, 8]
Сортировано:
feature_1 = [1, 2, 5, 7, 8, 9]
Средства соседей:
feature_1 = [1.5, 3.5, 6.5, 7.5, 8.5]
Мы делаем то же самое для всех числовых функций.
Для категориальных функций, таких как
feature_2 = [‘cat’, ‘dog’, ‘dog’, ‘cow’, ‘cat’, ‘cow’]
Находим уникальные значения признака:
feature_2 = [‘cat’, ‘dog’, ‘cow’]
С такой предварительной обработкой для каждой функции и каждого значения функции мы разделяем данные узла на два подмножества, левое и правое:
Для числовых функций (например, feature_p):
left = X[X[feature_p]<value] right = X[X[feature_p]≥value]
Для категориальных функций (например, feature_c):
left = X[X[feature_c]==feature_c] right = X[X[feature_c]!=feature_c]
В дереве классификации нам нужно было подсчитать количество наблюдений в каждом из наборов данных (слева и справа). В деревьях регрессии мы объединяем все остатки обоих наборов данных и вычисляем общую статистику среднеквадратичной ошибки. Признак и значение признака, которое дает самое низкое значение mse, являются критериями разделения.
Например, рассмотрим два возможных разделения: X на 4 и X на 6,5:
Невязки в первом примере левого узла равны [3.5, 4.5]
Невязки в первом примере правого узла равны [2, 0, -2]
Невязки во втором примере левого узла равны [-0,67, 0,33, 0,33].
Невязки во втором примере правого узла равны [1, -1]
Таким образом, остатки первого разбиения равны:
r1 = [3.5, 4.5, 2, 0, -2]
И второе:
r2 = [-0.67, 0.33, 0.33, 1, -1]
Затем мы находим статистику mse для обоих наборов остатков и сравниваем их:
Как видим, второй mse намного меньше первого. Таким образом, между двумя возможными разбиениями второй вариант (X ≥ 6) лучше.
Давайте попробуем использовать настраиваемый класс NodeRegression на примере.
Мы хотим создать дерево f такое, что:
mpg = f(horsepower, weight)
Здесь
mpg - миль на галлон
лошадиные силы - количество лошадиных сил в машине; мощность двигателя
вес - вес автомобиля в килограммах.
Реализация очень похожа на предыдущий метод дерева:
# Reading data d = pd.read_csv("data/regression/auto-mpg.csv") # Subsetting d = d[d['horsepower']!='?'] # Constructing the X and Y matrices features = ['horsepower', 'weight'] # Ensuring the correct types for ft in features: d[ft] = pd.to_numeric(d[ft]) # Constructing the X and Y matrices X = d[features] Y = d['mpg'].values.tolist()
Запуск узла и выращивание дерева:
# Initiating the Node root = NodeRegression(Y, X, max_depth=2, min_samples_split=3) # Growing the tree root.grow_tree()
И наконец распечатайте дерево:
# Printing tree root.print_tree()
Мы можем сравнить результаты с реализацией дерева регрессии в scikit-learn https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeRegressor.html с теми же гиперпараметрами:
Как мы видим, значения и функции разделения одинаковы.
Последнее, что следует отметить, это то, что прогноз узла - это среднее значение Y наблюдений в узле. В дереве решений классификатора прогноз - это класс, который имеет наибольшее количество наблюдений в узле.
На выращенных выше деревьях, если соблюдать правила:
weight ≤2764.5 → horsepower ≤70.5
Мы бы получили, что этот узел содержит 69 наблюдений. Если мы усредним переменную миль на галлон этих переменных, мы получим, что эти автомобили в среднем имеют значение 33,68 миль на галлон. Это прогноз для всех будущих автомобилей, которые пойдут по вышеупомянутому пути.
В заключение я настоятельно рекомендую просмотреть код класса NodeRegressor, потому что я пытался написать его очень явно. Загружая данные из моего репозитория GitHub и просматривая код построчно, вы скоро увидите, что дерево регрессии - очень простой и интуитивно понятный алгоритм.
Не стесняйтесь разветвлять, копировать, клонировать код и создавать запросы на вытягивание в моем репозитории git, о котором упоминалось в первом абзаце.
Если какая-либо из концепций все еще неясна, не стесняйтесь оставлять комментарии.
Спасибо за чтение и удачного кодирования!