Написание популярного алгоритма дерева регрессии на 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, о котором упоминалось в первом абзаце.

Если какая-либо из концепций все еще неясна, не стесняйтесь оставлять комментарии.

Спасибо за чтение и удачного кодирования!