Лабораторная работа №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: Загрузка данных
- Загрузите датасет
load_wine. - Создайте DataFrame
Xи Seriesy. - Разделите на 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: Подбор гиперпараметров
- Создайте новую модель с ограничениями:
max_depth=3(не глубже 3 уровней).min_samples_leaf=5(минимум 5 объектов в листе).
- Обучите и сравните метрики.
# 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 > порог”.