Физтех.Статистика
Информация не актуальна. Сайт переехал на miptstats.github.io
Скачать ipynb
Введение в анализ данных¶
Решающие деревья¶
Цель этого ноутбука — знакомство с решающими деревьями, с их параметрами и свойствами. В ноутбуке рассмотрены примеры применения решающих деревьев для решения задач классификации и регрессии.
import numpy as np
import pandas as pd
import scipy.stats
import warnings
import graphviz
from tqdm import tqdm_notebook
from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import datasets
from sklearn.metrics import accuracy_score, mean_squared_error
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor, export_graphviz
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.model_selection import train_test_split
sns.set(font_scale=1.5)
warnings.filterwarnings("ignore")
1. Решающие деревья в задаче классификации¶
Для начала рассмотрим задачу классификации на простом датасете, состоящем только из двух признаков. Это сделает удобным процесс визуализации решающего дерева. Для генерации такого простого датасета воспользуемся методом `make_classification` модуля sklearn.datasets
.
Генерация данных¶
Основные аргументы функции:
n_samples
— размер выборки;n_features
— количество признаков всего (кроме целевого);n_informative
— количество "информативных"признаков, от которых непосредственно зависит целевой признак;n_classes
— количество классов;n_redundant
— количество "лишних" признаков, не оказывающих воздействие на целевой.
data, target = datasets.make_classification(
n_samples=100, n_features=2, n_informative=2, n_classes=3,
n_redundant=0, n_clusters_per_class=1, random_state=3
)
data.shape, target.shape
Сопоставим каждому классу цвет
colors = ListedColormap(['#FF3300', '#0099CC', '#00CC66'])
light_colors = ListedColormap(['lightcoral', 'lightblue', 'lightgreen'])
Посмотрим на данные.
plt.figure(figsize=(8, 6))
grid_x1 = data[:, 0]
grid_x2 = data[:, 1]
plt.scatter(grid_x1, grid_x2, c=target, cmap=colors, s=100, alpha=0.7)
plt.xlabel('Признак 1'), plt.ylabel('Признак 2');
Разобьём данные на обучающую и тестовую выборки.
X_train, X_test, y_train, y_test = train_test_split(
data, target, test_size=0.3, random_state=777
)
Инициализируем и обучим решающее дерево для классификации с помощью `DecisionTreeClassifier`. Общий интерфейс класса аналогичен классу LinearRegression
, который мы подробно разбирали ранее. Гиперпараметры дерева мы разберем далее.
clf = DecisionTreeClassifier(random_state=42)
clf.fit(X_train, y_train)
Посчитаем предсказания дерева и посчитаем точность полученной классификации. Точность классификации вычисляется как доля правильно предсказанных классов.
predictions = clf.predict(X_test)
accuracy_score(predictions, y_test)
Неплохой результат. Но на таком простом датасете могло быть и лучше.
Визуализация решающей поверхности (decision surface)¶
При использовании решающего дерева в простых задачах, где в данных один или два признака, бывает полезно посмотреть на разделяющую поверхность. По виду разделяющей поверхности можно получить представление, действительно ли дерево улавливает важные закономерности в данных и не возникло ли переобучения. Если данные имеют большое число признаков, то визуализировать разделяющую поверхность довольно сложно. Но в нашем случае (в данных ровно два признака) всё достаточно просто.
def get_meshgrid(data, step=.05, border=.5):
'''
Функция для получения сетки точек (x1, x2)
для дальнейшего отображения их на графиках
Параметры:
1) data - входной датасет, набор точек (x1_i, x2_i);
2) step - мелкость сетки;
3) border - отступ от минимальных и максимальных значений x1, x2 в data
в сетке
'''
x1_min, x1_max = data[:, 0].min() - border, data[:, 0].max() + border
x2_min, x2_max = data[:, 1].min() - border, data[:, 1].max() + border
return np.meshgrid(np.arange(x1_min, x1_max, step),
np.arange(x2_min, x2_max, step))
def plot_decision_surface(
estimator, X_train, y_train, X_test, y_test, colors=colors,
light_colors=light_colors, title='', metric=accuracy_score
):
'''
Функция для отображения разделяющей поверхности классификатора
Параметры:
1) estimator - классификатор;
2) X_train, y_train - данные и разметка обучающей выборки;
3) X_test, y_test - данные и разметка тестовой выборки;
4) colors - цвета для отображения точек из разных классов;
5) light_colors - цветовая схема для отображения разделяющей поверхности;
6) title - заголовок графика.
7) metric - метрика качества классификации.
'''
estimator.fit(X_train, y_train) # обучаем модель
plt.figure(figsize=(16, 6))
# отображаем разделяющую поверхность и точки обучающей выборки
plt.subplot(1,2,1)
x1_values, x2_values = get_meshgrid(X_train)
x1_ravel, x2_ravel = x1_values.ravel(), x2_values.ravel()
mesh_predictions_ravel = estimator.predict(np.c_[x1_ravel, x2_ravel])
mesh_predictions = np.array(mesh_predictions_ravel).reshape(x1_values.shape)
plt.grid(False)
plt.pcolormesh(x1_values, x2_values, mesh_predictions, cmap=light_colors)
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train,
s=100, cmap=colors, edgecolors='black')
plt.xlabel('Признак 1'), plt.ylabel('Признак 2')
plt.title('Обучающая выборка, {}={:.2f}'.format(
metric.__name__,metric(y_train, estimator.predict(X_train))
))
# отображаем разделяющую поверхность и точки тестовой выборки
plt.subplot(1,2,2)
plt.grid(False)
plt.pcolormesh(x1_values, x2_values, mesh_predictions, cmap=light_colors)
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test,
s=100, cmap=colors, edgecolors='black')
plt.title('Тестовая выборка, {}={:.2f}'.format(
metric.__name__, metric(y_test, estimator.predict(X_test))
))
plt.xlabel('Признак 1'), plt.ylabel('Признак 2')
plt.suptitle(title, fontsize=20)
Визуализация разделяющей поверхности при изменении параметра max_depth
¶
Посмотрим, как будет меняться разделяющая поверхность при изменении значения параметра max_depth
.
estimator = DecisionTreeClassifier(random_state=42, max_depth=1)
plot_decision_surface(
estimator, X_train, y_train,
X_test, y_test, title='max_depth=1'
)
Разделяющая поверхность оказалась довольно простой. Ведь если глубина дерева равна одному, то в нём происходит разделение выборки ровно по одному признаку.
Несложно заметить, что если в датасете для классификации $k$ классов, то необходимо брать дерево с глубиной не менее $\log_2 k$, так как мы хотим, чтобы в полученном дереве было не менее $k$ листьев. Иначе дерево будет предсказывать $< k$ классов, чего мы хотим избежать.
Попробуем увеличить максимальную глубину дерева.
estimator = DecisionTreeClassifier(random_state=42, max_depth=2)
plot_decision_surface(
estimator, X_train, y_train,
X_test, y_test, title='max_depth=2'
)
estimator = DecisionTreeClassifier(random_state=42, max_depth=3)
plot_decision_surface(
estimator, X_train, y_train,
X_test, y_test, title='max_depth=3'
)
Заметим, что сложность разделяющей поверхности заметно увеличилась. Точность предсказания дерева заметно возросла.
А теперь посмотрим, что произойдёт, если резко увеличить значение параметра max_depth
.
estimator = DecisionTreeClassifier(random_state=42, max_depth=20)
plot_decision_surface(
estimator, X_train, y_train,
X_test, y_test, title='max_depth=20'
)
Заметим, что accuracy на обучающей выборке стало равно единице, а на тестовой выборке стало хуже, чем при максимальной возможной глубине, равной трем. Это означает, что произошло переобучение дерева. Этот пример демонстрирует проявление на практике следующих свойств решающих деревьев:
- Решающие деревья очень легко могут быть переобучены, причём склонность к переобучению возрастает с возрастанием глубины дерева.
- Для любой выборки для классификации существует решающее дерево, идеально восстанавливающее истинный отклик.
Вывод.
Как правило,
- увеличение значения параметра
max_depth
приводит к увеличению точности классификации на обучающей выборке, - но с некоторого момента увеличение значения
max_depth
приводит к ухудшению точности на тестовой выборке, так как начинается стадия переобучения.
Визуализация разделяющей поверхности при изменении параметра min_samples_leaf
¶
Другим важным параметром решающего дерева является min_samples_leaf
— минимальное количество элементов выборки, которые могут находиться в листовой вершине дерева. При разбиении вершины дерева проверяется, что после разбиения количество элементов выборки, находящихся как в левой, так и в правой дочерних вершинах не меньше min_samples_leaf
. Если это условие не выполняется, то такое разбиение отвергается.
Такое условие необходимо для того, чтобы предсказание для данного листа было достаточно устойчиво. Например, если попал только один объект, то предсказание для данного листа будет равно таргету данного объекта, что является достаточно шумным предсказанием, а если попало 5 объектов, то предсказание будет более устойчиво к шуму и выбросам. Кроме того, без ограничения возможна ситуация, при которой один объект может определить метку для большой области в пространстве, находясь при этом на границе этой области.
for min_samples_leaf in [1, 2, 10, 20]:
estimator = DecisionTreeClassifier(
random_state=42, min_samples_leaf=min_samples_leaf
)
plot_decision_surface(
estimator, X_train, y_train, X_test, y_test,
title=f'min_samples_leaf={min_samples_leaf}'
)
Построим график зависимости accuracy от min_samples_leaf
на обучающей и на тестовой выборках.
def get_train_and_test_accuracy(param_name, grid):
'''
Функция для оценки точности классификации
для заданных значений параметра param_name
Параметры:
1) param_name - название параметра, который собираемся варьировать,
2) grid - сетка значений параметра
'''
train_acc, test_acc = [], []
for param_value in grid:
estimator = DecisionTreeClassifier(**{param_name: param_value})
estimator.fit(X_train, y_train)
train_acc.append(accuracy_score(y_train, estimator.predict(X_train)))
test_acc.append(accuracy_score(y_test, estimator.predict(X_test)))
return train_acc, test_acc
def plot_dependence(param_name, grid=range(2, 20), title=''):
'''
Функция для отображения графика зависимости accuracy
от значения параметра c названием param_name
Параметры:
1) param_name - название параметра, который собираемся варьировать,
2) grid - сетка значений параметра,
3) title - заголовок графика
'''
plt.figure(figsize=(12, 6))
train_acc, test_acc = get_train_and_test_accuracy(param_name, grid)
plt.plot(grid, train_acc, label='train', lw=3)
plt.plot(grid, test_acc, label='test', lw=3)
plt.legend(fontsize=14)
plt.xlabel(param_name)
plt.ylabel('Точность классификации')
plt.title(title, fontsize=20)
plt.show()
plot_dependence(
'min_samples_leaf', range(1, 50),
title='Зависимость метрики от min_samples_leaf'
)
Вывод.
В целом наблюдается следующая закономерность. С увеличением значения min_samples_leaf
качество
- на обучающей выборке падает,
- на тестовой выборке сначала возрастает, а затем начинает убывать.
Получается, увеличение значения параметра min_samples_leaf
— один из способов борьбы с переобучением при использовании решающих деревьев.
Но, всё же, стоит заметить, что повышение значения min_samples_leaf
делает разделяющую поверхность проще. Значит, при слишком больших значениях min_samples_leaf
модель становится слишком простой и перестаёт улавливать закономерности из данных.
estimator = DecisionTreeClassifier(
random_state=42, min_samples_leaf=80
)
plot_decision_surface(
estimator, X_train, y_train,
X_test, y_test, title='min_samples_leaf=80'
)
Здесь мы привели пример решающего дерева при использовании min_samples_leaf=80
.
Визуализация разделяющей поверхности при изменении параметра min_samples_split
¶
Последний параметр, который мы будем подробно визуализировать — min_samples_split
, минимальное количество элементов, которое должно попасть в вершину, чтобы её можно было делить.
for min_samples_split in [2, 10, 20]:
estimator = DecisionTreeClassifier(
random_state=42, min_samples_leaf=1,
min_samples_split=min_samples_split
)
plot_decision_surface(
estimator, X_train, y_train, X_test, y_test,
title=f'min_samples_split={min_samples_split}'
)
А теперь попробуем резко увеличить значение min_samples_split
.
estimator = DecisionTreeClassifier(
random_state=42, min_samples_leaf=1, min_samples_split=50
)
plot_decision_surface(
estimator, X_train, y_train,
X_test, y_test, title='min_samples_split=50'
)
Вывод.
При изменении значения min_samples_split
происходит ситуация, аналогичной случаю, когда мы варьируем min_samples_leaf
. И здесь наблюдается следующая закономерность: с увеличением значения min_samples_split
качество
- на обучающей выборке падает,
- на тестовой выборке — до некоторого момента возрастает.
Кроме того, с увеличением min_samples_split
разделяющая поверхность становится проще.
Другие параметры.¶
Кроме того, обратите внимание на другие параметры класса DecisionTreeClassifier
в sklearn
:
criterion
— критерий, по которому происходит разбиение вершины дерева. Стандартные критерии для классификации — критерий Джини (giny
) и энтропийный критерий (entropy
), при этомginy
— критерий по умолчанию. В этом ноутбуке мы брали для классификации критерий по умолчанию. Более подробную информацию по критериям можно найти в документации sklearn.
splitter
— способ разбиения вершины решающего дерева. Есть два возможных варианта:best
иrandom
. В первом случае рассматриваются все возможные способы разбить вершину дерева на две и берётся тот из них, значение критерия для которого оптимально. Приsplitter=random
берётся несколько случайных возможных разбиений и среди них выбирается то, значение критерия для которого оптимально.
max_features
— максимальное количество признаков, которые могут быть перебраны при разбиении вершины дерева. Перед каждым разбиением вершины генерируется выборка изmin(d, max_features)
случайных признаков (d
— количество признаков в датасете) и только эти признаки рассматриваются как разделяющие.
min_impurity_decrease
— минимальное значение уменьшения взвешенного критерия неопределенности (impurity
), чтобы можно было разбить выборку в данной вершине.
Об остальных гиперпараметрах класса решающего дерева в sklearn
можно прочитать в документации.
2. Регрессия с использованием решающего дерева¶
В регрессии будем использовать все те же параметры разбиения. Но в качестве критерия неопределенности (параметр criterion
) используются squared_error
, friedman_mse
, absolute_error
, причём squared_error
— критерий по умолчанию..
Сгенерируем регрессионные данные. Принцип работы функции такой же, как и для генерации датасета для классификации, что мы разбирали выше.
data, target = datasets.make_regression(
n_features=2, n_informative=2, random_state=3, n_samples=200
)
Визуализируем. Отклик показан цветом точки.
plt.figure(figsize=(8, 6))
grid_x1 = data[:, 0]
grid_x2 = data[:, 1]
plt.scatter(grid_x1, grid_x2, c=target, s=100, alpha=0.7, cmap='winter')
plt.xlabel('Признак 1'), plt.ylabel('Признак 2');
Разобьём данные на обучение и тест.
X_train, X_test, y_train, y_test = train_test_split(
data, target, random_state=42
)
Исследуем зависимость качества работы регрессионного дерева в зависимости от максимально возможной его глубины.
Цветом поверхности обозначается ответ в листе. Но для регрессит это не класс, а некоторое число.
for max_depth in [1, 2, 3, 5, 10, 20]:
estimator = DecisionTreeRegressor(random_state=42, max_depth=max_depth)
plot_decision_surface(
estimator, X_train, y_train, X_test, y_test,
title='max_depth={}'.format(max_depth), colors='winter',
light_colors='winter', metric=mean_squared_error
)
Для решения задачи регрессии недостаточно малой глубины дерева, но как в задаче классификации при слишком большой глубине может происходить переобучение. Регрессионная зависимость, восстанавливаемая деревом, выглядит сильно сложно.
3. Визуализация решающих деревьев¶
Посмотрим, как можно визуализировать структуру решающего дерева, в этом нам поможет библиотека graphviz
.
Для установки библиотеки через pip:
pip install graphiz
Для установки библиотеки через conda:
conda install -c conda-forge python-graphiz
В качестве данных возьмём датасет с Kaggle по определению цены камеры по её имеющимся характеристикам.
К её характеристикам относятся:
Release date
— дата выпуска,Max resolution
— максимальное разрешение,Low resolution
— нижнее разрешение,Effective pixels
— число эффективных пикселей,Zoom wide (W)
— ширина зума,Zoom tele (T)
— телезум,Normal focus range
— нормальный фокусный диапазон,Macro focus range
— макрофокусный диапазон,Storage included
— встроенный объём памяти,Weight (inc. batteries)
— вес,Dimensions
— габариты.
Для того, чтобы деревья не получались слишком широкими, будем их обучать на небольшой части датасета
camera_df = pd.read_csv('camera_dataset.csv')[:100]
camera_df = camera_df.drop('Model', axis=1)
print(camera_df.shape)
camera_df.head()
Выделим отдельно признаки и таргет
X, y = camera_df.loc[:, camera_df.columns != 'Price'], camera_df['Price']
Напишем функцию визуализации дерева
def get_graphviz_graph(decision_tree_regressor):
'''
Функция для обучения решающего дерева и построения
по нему графа graphviz для визуализации
'''
decision_tree_regressor.fit(X, y)
dot_data = export_graphviz(
decision_tree_regressor, out_file=None,
feature_names=X.columns, class_names='Price',
filled=True, rounded=True, special_characters=True
)
return graphviz.Source(dot_data)
Визуализируем деревья для разной глубины
camera_regressor = DecisionTreeRegressor(
criterion='absolute_error', max_depth=2, min_samples_split=10
)
graph = get_graphviz_graph(camera_regressor)
graph
camera_regressor = DecisionTreeRegressor(
criterion='absolute_error', max_depth=3, min_samples_split=10
)
graph = get_graphviz_graph(camera_regressor)
graph
camera_regressor = DecisionTreeRegressor(
criterion='absolute_error', max_depth=8, min_samples_split=8
)
graph = get_graphviz_graph(camera_regressor)
graph
По построенной визуализации дерева можно увидеть, что большая часть разбиений вершин вполне соотносится со здравым смыслом и логикой принятия решений человеком. Например, чем больше zoom камеры, тем больше должна быть цена при прочих равных или чем больше встроенной памяти имеет камера, тем больше должна быть её цена.
Случайный лес¶
Цель этого раздела — знакомство со случайными лесами, с их параметрами и свойствами. В нем будут рассмотрены примеры применения случайного леса для решения задач регрессии.
Основные параметры¶
Реализации: RandomForestClassifier
, RandomForestRegressor
Набор гиперпараметров случайного леса очень похож на набор гиперпараметров решающего дерева. Основным отличием является наличие у случайного леса параметра n_estimators
, задающего количество решающих деревьев, используемых для получения предсказаний. Это основной гиперпараметр для случайного леса.
О других гиперпараметрах случайного леса можно почитать в документации.
Решение задачи регрессии с помощью Random Forest¶
Сгенерируем выборку из многомерного нормального распределения и в качестве целевой функции возьмем расстояние от точки до центра координат.
X_train = scipy.stats.multivariate_normal.rvs(
size=500, mean=[0, 0], cov=[[3, 0], [0, 3]]
)
y_train = (X_train[:, 0] ** 2 + X_train[:, 1] ** 2) ** 0.5
Визуализируем
plt.figure(figsize=(10, 8))
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap='spring',
s=100, alpha=0.8, linewidths=1, edgecolors='black')
plt.show()
Напишем функции для визуализации
def generate_grid(sample, border=1, step=0.05):
''' Создает сетку на основе выборки для раскраски пространства '''
return np.meshgrid(np.arange(min(sample[:, 0]) - border,
max(sample[:, 1]) + border,
step),
np.arange(min(sample[:, 1]) - border,
max(sample[:, 1]) + border,
step))
def create_picture(X_train, y_train, model, border=1, step=0.05,
cmap='spring', alpha=1, create_new_figure=True,
figsize=(7, 7), s=100, linewidths=1, points=True):
'''
Раскрашивает пространство в соответствии с предсказаниями
случайного леса/решающего дерева
Параметры.
1) X_train — данные обучающей выборки,
2) y_train — метки обучающей выборки,
3) model — визуализируемая модель,
4) border — размер границы между областями пространства,
полученными моделью,
5) step — точность сетки пространства,
6) cmap — цветовая схема,
7) aplha — прозрачность точек обучающей выборки,
8) create_new_figure — флаг, определяющий создавать ли
новую фигуру,
9) figsize — размер создаваемой фигуры,
10) s — размер точек обучающей выборки,
11) linewidths — размер границы каждой точки,
12) point — флаг, определяющий, отображать ли точки
обучающей выборки на графике.
'''
# Создание сетки
grid = generate_grid(X_train, border, step)
# Выворачивание сетки для применения модели
grid_ravel = np.c_[grid[0].ravel(), grid[1].ravel()]
# Предсказание значений для сетки
grid_predicted_ravel = model.predict(grid_ravel)
grid_predicted = grid_predicted_ravel.reshape(grid[0].shape) # Подгоняем размер
# Построение фигуры
if create_new_figure:
plt.figure(figsize=figsize)
plt.pcolormesh(grid[0], grid[1], grid_predicted, cmap=cmap)
if points:
plt.scatter(
X_train[:, 0], X_train[:, 1], c=y_train, s=s,
alpha=alpha, cmap=cmap, linewidths=linewidths, edgecolors='black'
)
plt.xlim((min(grid_ravel[:, 0]), max(grid_ravel[:, 0])))
plt.ylim((min(grid_ravel[:, 1]), max(grid_ravel[:, 1])))
plt.title('max_depth = ' + str(model.get_params()['max_depth']))
if create_new_figure:
plt.show()
Визуализация предсказания решающими деревьями с различным значением максимальной глубины¶
for max_depth in range(2, 6):
create_picture(
X_train, y_train,
DecisionTreeRegressor(max_depth=max_depth).fit(X_train, y_train)
)
Визуализация предсказаний случайного леса¶
Обучим случайный лес на 35 деревьев, такое число взято для удобства визуализации
n_estimators = 35
model = RandomForestRegressor(n_estimators=n_estimators)
model.fit(X_train, y_train)
Визуализация предсказания по каждому из деревьев по отдельности. Они все такие разные и такие переобученные...
plt.figure(figsize=(20, 25))
for i in range(n_estimators):
plt.subplot(int(np.floor(n_estimators / 5)), 5, i+1)
create_picture(X_train, y_train, model.estimators_[i],
create_new_figure=False, s=20, linewidths=0, points=False)
plt.title('')
plt.xticks([]), plt.yticks([])
Усредненное предсказание
create_picture(X_train, y_train, model, figsize=(12, 12))
То же самое с 100 деревьями
n_estimators = 100
model = RandomForestRegressor(n_estimators=n_estimators)
model.fit(X_train, y_train)
create_picture(X_train, y_train, model, figsize=(12, 12))