machine-learning Лабораторная работа

Введение в ML: Деревья Решений (Decision Trees)

Лабораторная работа №10: Деревья Решений (Decision Trees)

Цель: Изучить работу алгоритма Decision Tree. Мы увидим своими глазами, как дерево принимает решения, визуализируем его структуру, намеренно добьемся переобучения (Overfitting) и научимся бороться с ним с помощью “стрижки” (Pruning).

Инструменты:

  • sklearn.tree: DecisionTreeClassifier, plot_tree.
  • graphviz (опционально, но мы будем использовать встроенный plot_tree).
  • matplotlib: для отрисовки границ решений.

Данные: Wine Dataset (Встроенный в sklearn). Классификация вин на 3 сорта по химическому составу (алкоголь, магний, флавоноиды и т.д.).


Часть 1: Загрузка и Подготовка

Особенность деревьев

Деревьям решений не нужно масштабирование данных (StandardScaler). Дерево просто ищет порог (например, Alcohol <= 13), и ему совершенно неважно, в каких единицах и масштабах измерены данные.

Задание 1.1: Загрузка данных

  1. Загрузите датасет load_wine.
  2. Создайте DataFrame X и Series y.
  3. Разделите на Train/Test (70/30, random_state=42).
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split

# Загрузка
data = load_wine()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target

print(f"Признаки: {data.feature_names}")
print(f"Целевые классы: {data.target_names}")

# TODO: Разделите данные (StandardScaler НЕ нужен!)
# X_train, X_test, y_train, y_test = train_test_split(...)

Часть 2: “Дикое” дерево (Overfitting)

Если не ограничивать дерево, оно будет расти до тех пор, пока в каждом листе не окажется “чистый” класс. Это приводит к идеальной точности на Train, но сложной, переобученной структуре.


Часть 3: Регуляризация (Pruning)

Теперь мы “подстрижем” дерево, ограничив его глубину. Это упростит модель и, возможно, улучшит метрики на тесте (или хотя бы сократит разрыв между train и test).

Задание 3.1: Подбор гиперпараметров

  1. Создайте новую модель с ограничениями:
    • max_depth=3 (не глубже 3 уровней).
    • min_samples_leaf=5 (минимум 5 объектов в листе).
  2. Обучите и сравните метрики.
# TODO: Модель с ограничениями
# tree_pruned = DecisionTreeClassifier(max_depth=3, min_samples_leaf=5, random_state=42)
# tree_pruned.fit(...)

# y_pred_train_p = ...
# y_pred_test_p = ...

# print(f"Pruned Train Accuracy: {accuracy_score(y_train, y_pred_train_p):.4f}")
# print(f"Pruned Test Accuracy:  {accuracy_score(y_test, y_pred_test_p):.4f}")

Задание 3.2: Визуализация компактного дерева

Посмотрите, насколько понятнее стала логика принятия решений.

plt.figure(figsize=(15, 8))

# TODO: Визуализируйте tree_pruned
# plot_tree(..., feature_names=data.feature_names, filled=True, fontsize=12, class_names=data.target_names)

plt.title("Подстриженное дерево (Good Generalization)")
plt.show()

Часть 4: Важность признаков (Feature Importance)

Деревья позволяют легко понять, какие признаки самые важные. Чем выше признак находится в дереве (ближе к корню), тем лучше он разделяет данные.

Задание 4.1: Feature Importance Plot

Извлеките атрибут feature_importances_ из обученной модели (tree_pruned) и постройте Barplot.

import seaborn as sns

# TODO: Создайте DataFrame важности
# feat_importances = pd.DataFrame({
#     'Feature': data.feature_names,
#     'Importance': tree_pruned.feature_importances_
# })

# Сортировка
# feat_importances = feat_importances.sort_values(by='Importance', ascending=False)

# TODO: Постройте Barplot
# plt.figure(figsize=(10, 6))
# sns.barplot(...)
# plt.title("Важность признаков в Дереве Решений")
# plt.show()

Часть 5: Границы решений (Decision Boundary)

Чтобы понять геометрический смысл “нарезания пространства”, обучим дерево только на ДВУХ признаках и нарисуем карту решений.

Задание 5.1: 2D Визуализация

Мы возьмем два самых важных признака (из предыдущего пункта, скорее всего это proline и od280/od315_of_diluted_wines или flavanoids).

# Выберем 2 признака: индекс 6 (Flavanoids) и 12 (Proline) - проверьте индексы по feature_names!
# Или просто возьмем columns по именам
X_2d = X[['flavanoids', 'proline']].values
y_2d = y

# Обучаем дерево только на них
clf_2d = DecisionTreeClassifier(max_depth=3, random_state=42)
clf_2d.fit(X_2d, y_2d)

# Создаем сетку для рисования (Meshgrid)
x_min, x_max = X_2d[:, 0].min() - 1, X_2d[:, 0].max() + 1
y_min, y_max = X_2d[:, 1].min() - 1, X_2d[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
                     np.arange(y_min, y_max, 10)) # шаг 10 для proline так как там сотни

# Предсказываем для каждой точки сетки
Z = clf_2d.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

# Рисуем
plt.figure(figsize=(10, 6))
plt.contourf(xx, yy, Z, alpha=0.4, cmap='viridis')
plt.scatter(X_2d[:, 0], X_2d[:, 1], c=y_2d, s=40, edgecolor='k', cmap='viridis')
plt.xlabel('Flavanoids')
plt.ylabel('Proline')
plt.title('Границы решений Дерева (Прямоугольные области)')
plt.show()

Ключевое наблюдение

Обратите внимание: границы всегда параллельны осям. Это фундаментальная геометрическая особенность деревьев решений, так как они строят правила вида “x > порог”.


🧠 Проверка знаний

Нужно ли применять StandardScaler для масштабирования признаков перед обучением модели DecisionTreeClassifier?

Что произойдет, если обучить Decision Tree без ограничения глубины (max_depth=None) на сложных данных с шумом?