Лекция 14
Лекция 14¶
Написаное ниже может содержать ошибки, не стоит верить всему на 100%.
Деревья решений¶
Деревья решений, или решающие деревья, распространенный алгоритм машинного обучения.
Рассмотрим типичную задачу студента во время семестра: пора ли делать задание или же можно подождать.
На картинке выше представлено два почти одинаковых дерева. Отличаются они лишь порядком вопросов.
Какое из деревьев подходит лучше для решения задачи? Попробуем рассмотреть данный вопрос в рамках решения тренировочной задачи.
Описание тренировочной задачи¶
Имеется набор данных об ирисах, содержащий petal length/width (длина/ширина лепестка) и sepal length/width (длина/ширина чашелистка). По этим параметрам ирисы определяются в три разных класса: setosa,vesicilor и virginica.
Загрузим данные из sklearn. Если это не удалось, то обратитесь напрямую к данным по ссылке.
from sklearn.datasets import load_iris
dataset = load_iris(as_frame=True)
df = dataset.frame
print(dataset.DESCR)
df.loc[::30]
Рассмотрим совместное распределение данных.
from seaborn import pairplot
pairplot(df, hue="target");
Разберем принцип построения дерева на основе двух конечных классов. Для этого объединим классы с нулевой и единичной метками.
df.loc[df["target"] == 0] = 1
pairplot(df, hue="target");
Рассмотрим два признака: petal width b sepal width. Построим распределение меток и попробуем добавить разделяющую прямую между двумя классами. Пока что положение прямой поставим наугад.
from matplotlib import pyplot as plt
feature_name0 = "petal width (cm)"
feature_name1 = "sepal width (cm)"
feature0 = df[feature_name0]
feature1 = df[feature_name1]
actual_mask = df["target"] == 2
plt.scatter(feature0[actual_mask], feature1[actual_mask], label="target=1")
plt.scatter(feature0[~actual_mask], feature1[~actual_mask], label="target=2")
plt.axvline(1.7, color="black", linestyle="--", label="decision margin")
plt.xlabel(feature_name0)
plt.ylabel(feature_name1)
plt.legend(loc="lower right")
plt.tight_layout();
Критерий информативности¶
В первом приближении качество модели можно оценить, если посчитатить долю ошибочно определенных объектов: $$ F(X) = \dfrac{1}{|X|} \sum[y_k \neq y_k^{\prime}],$$ где $y_k$ - истинная метка, а $y_k^{\prime}$ - предсказание.
def F(y_actual, y_pred):
if len(y_actual):
return sum(y_actual != y_pred) / len(y_actual)
return 1
Посчитаем зависимость такой ошибки от значения разделяющего критерия.
$$ Q(X) = F(X) - w_l F(X_l) - w_r F(X_r) $$$$ Q(X) = F(X) - \dfrac{|X_l|}{|X|} F(X_l) - \dfrac{|X_r|}{|X|} F(X_r) $$result = {}
feature_name0 = "petal width (cm)"
feature0 = df[feature_name0]
unique_values = feature0.unique()
for idx, decision_value in enumerate(unique_values):
target_pred = feature0.apply(lambda x: 2 if x < decision_value else 1)
mask = feature0 < decision_value
df_l = df[mask]
df_r = df[~mask]
w_l = df_l.shape[0] / df.shape[0]
w_r = 1.0 - w_l
result[decision_value] = {
"Q_F": F(df["target"], 1) - w_l * F(df_l["target"][mask], 1) - w_r * F(df_r["target"][~mask], 2)
}
Подойдем к вопросу с вероятностной стороны.
Какова вероятность получить тот или иной класс? Давайте посчитаем.
p1 = sum(df["target"] == 1) / df.shape[0]
p2 = sum(df["target"] == 2) / df.shape[0] # p2 = 1 - p1
print(f"Probability(target=1)={p1:1.3f}", f"Prbability(target=2)={p2:1.3f}", sep="\n")
Посчитаем величину $$ G = \sum p_k (1 - p_k) = p_1 (1 - p_1) + p_2 (1 - p_2). $$ Данную величину называют критерием Джини.
def G(y_actual, y_pred):
p1 = F(y_actual, y_pred)
p2 = 1.0 - p1
return 1 - p1 * p1 - p2 * p2
Посчитаем зависимость такой ошибки от значения разделяющего критерия.
feature_name0 = "petal width (cm)"
feature0 = df[feature_name0]
unique_values = feature0.unique()
for idx, decision_value in enumerate(unique_values):
target_pred = feature0.apply(lambda x: 2 if x < decision_value else 1)
mask = feature0 < decision_value
df_l = df[mask]
df_r = df[~mask]
w_l = df_l.shape[0] / df.shape[0]
w_r = 1.0 - w_l
result[decision_value].update({
"Q_G": G(df["target"], 2) - w_l * G(df_l["target"], 2) - w_r * G(df_r["target"], 2)
})
Посчитаем для полученой системы энтропию Шенона: $$ H(x) = -\sum\limits_{i} p_{i} \log_2 p_i = -p_1\log_2 p_1 - p_2 \log_2 p_2.$$
import numpy as np
def H(y_actual, y_pred):
p1 = F(y_actual, y_pred)
p2 = 1.0 - p1
if p1 == 1.0 or p2 == 1.0: # 1 * log2(1) + 0 * log2(0) == 0
return 0
return - p1 * np.log2(p1) - p2 * np.log2(p2)
У хорошей системы должна быть энтропия равна нулю. Ведь тогда только один из $p_i$ равен 1. Стоит оговориться, что если $p_j = 0$, то такое элемент считаем за 0.
Что же вносит введение энтропии в нашу задачу? Возможность получения функции выбора разделения на основе следующего критерия: $$Q(X) = H(X_m) - \dfrac{|X_l|}{|X_m|} H(X_l) - \dfrac{|X_r|}{|X_m|} H(X_r).$$ Здесь $X_m$ - количество объектов на входе критерия, $X_l/X_r$ - количество объектов в левой/правой ветке решения. Попробуем оценить изменение энтропии для нескольких вариантов расположения решающего критерия.
import pandas as pd
feature_name0 = "petal width (cm)"
feature0 = df[feature_name0]
unique_values = feature0.unique()
for idx, decision_value in enumerate(unique_values):
mask = feature0 > decision_value
df_l = df[mask]
df_r = df[~mask]
w_l = df_l.shape[0] / df.shape[0]
w_r = df_r.shape[0] / df.shape[0]
result[decision_value].update({"Q_H": H(df["target"], 2) - w_l * H(df_l["target"], 2) - w_r * H(df_r["target"], 2)})
result = pd.DataFrame([{"decision value": key, **value} for key, value in result.items()]).sort_values(by="decision value")
result
from matplotlib import pyplot as plt
criteria = result["decision value"]
plt.scatter(criteria, result["Q_F"], s=25, label="Error")
plt.scatter(criteria, result["Q_G"], s=15, label="Gini")
plt.scatter(criteria, result["Q_H"], s=10, label="Entropy")
plt.legend()
Найдем такое значение критерия, которое минимизирует изменение начальной энтропии после разделения.
best_row0 = result.loc[result["Q_H"].idxmax()]
feature0_best = best_row0["decision value"]
best_row0
Есть ли у нас гарантии, что полученое значение дает лучший результат? Стоит проверить изменение энтропии с использованием альтернативного признака.
import pandas as pd
result_alt = []
feature_name1 = "sepal width (cm)"
feature1 = df[feature_name1]
unique_values = feature1.unique()
for idx, decision_value in enumerate(unique_values):
mask = feature1 > decision_value
df_l = df[mask]
df_r = df[~mask]
w_l = df_l.shape[0] / df.shape[0]
w_r = df_r.shape[0] / df.shape[0]
result_alt.append({"decision value": decision_value, "Q_H": H(df["target"], 2) - w_l * H(df_l["target"], 2) - w_r * H(df_r["target"], 2)})
result_alt = pd.DataFrame(result_alt).sort_values(by="decision value").reset_index(drop=["index"])
print(f"Initial entropy: {H(df['target'], 2)}")
result_alt
best_row0_alt = result_alt.loc[result_alt["Q_H"].idxmax()]
feature_best_alt = best_row0_alt["decision value"]
print(f"{best_row0['Q_H'] > best_row0_alt['Q_H']}")
best_row0_alt
best_row0, best_row0_alt
Использование альтернативного признака приводит к большему уменьшению энтропию. Соотвественно, мы выбираем первый признак для разделения (согласно жадному выбору).
from matplotlib import pyplot as plt
feature_name0 = "petal width (cm)"
feature_name1 = "sepal width (cm)"
feature0 = df[feature_name0]
feature1 = df[feature_name1]
actual_mask = df["target"] == 2
plt.scatter(feature0[actual_mask], feature1[actual_mask], label="target=1")
plt.scatter(feature0[~actual_mask], feature1[~actual_mask], label="target=2")
plt.axvline(feature0_best, color="black", linestyle="--", label="decision margin")
plt.xlabel(feature_name0)
plt.ylabel(feature_name1)
plt.legend(loc="lower right")
plt.tight_layout();
Возьмем правую половину и уже внутри него повторим операцию минимизации потери энтропии. Но теперь пойдем не по оптимальному пути, а уже с использованием альтернативного признака.
import pandas as pd
from matplotlib import pyplot as plt
result2 = []
feature_name0 = "petal width (cm)"
feature0 = df[feature_name0]
mask = feature0 > feature0_best
df_selected = df[mask]
feature1_selected = df_selected[feature_name1]
I_selected = H(df_selected["target"], 2)
unique_values = feature1_selected.unique()
for idx, decision_value in enumerate(unique_values):
mask = feature1_selected > decision_value
df_l = df_selected[mask]
df_r = df_selected[~mask]
w_l = df_l.shape[0] / df_selected.shape[0]
w_r = df_r.shape[0] / df_selected.shape[0]
result2.append({"decision value": decision_value, "Q_H": I_selected - w_l * H(df_l["target"], 2) - w_r * H(df_r["target"], 2)})
result2 = pd.DataFrame(result2).sort_values(by="decision value").reset_index(drop=["index"])
result2
plt.scatter(result2["decision value"], result2["Q_H"])
best_row1 = result2.loc[result2["Q_H"].idxmax()]
feature1_best = best_row1["decision value"]
best_row1
from matplotlib import pyplot as plt
feature_name0 = "petal width (cm)"
feature_name1 = "sepal width (cm)"
feature0 = df[feature_name0]
feature1 = df[feature_name1]
actual_mask = df["target"] == 2
plt.scatter(feature0[actual_mask], feature1[actual_mask], label="target=1")
plt.scatter(feature0[~actual_mask], feature1[~actual_mask], label="target=2")
plt.axvline(feature0_best, color="black", linestyle="--", label="decision margin 0")
plt.axhline(feature1_best, 0.475, 1, color="black", linestyle="--", label="decision margin 0-1")
plt.xlabel(feature_name0)
plt.ylabel(feature_name1)
plt.legend(loc="lower right")
plt.tight_layout();
Класс в sklearn¶
Воспользуемся уже готовым алгоритмом из библиотеки sklearn. Класс содержит несколько параметров. В частности:
max_depthмаксимально возможная глубина нашего дерева;criterionкритерий для выбора разбиения;random_stateспособ зафиксировать случайность внутри алгоритма.
Более подробно параметры можно изучить в соответствующей документации (eng|ru).
Не будем заморачиваться с разбиением на обучающую и тестовую выборки. Обучим алгоритм сразу на всех объектах.
from sklearn.tree import DecisionTreeClassifier
X = df[[feature_name0, feature_name1]].to_numpy()
y = df["target"].to_numpy()
tree = DecisionTreeClassifier(max_depth=3, criterion="entropy", random_state=1)
tree.fit(X, y)
Попробуем отобразить дерево несколькими способами.
Первый из: нарисовать граф.
from sklearn.tree import plot_tree
plot_tree(tree, filled=True);
Второй: визуализировать плоскость и границы критериев.
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt
disp = DecisionBoundaryDisplay.from_estimator(
tree,
X,
response_method="predict",
xlabel=feature_name0,
ylabel=feature_name1,
alpha=0.5,
cmap=plt.cm.coolwarm
)
accuracy_tree = accuracy_score(y, tree.predict(X))
print(f'Tree Accuracy: {accuracy_tree:.2f}')
disp.ax_.scatter(
X[:, 0],
X[:, 1],
c=y,
edgecolor="k",
cmap=plt.cm.coolwarm
)
plt.title(f"Decision surface for tree trained on {feature_name0} and {feature_name1}")
plt.tight_layout()
from sklearn.tree import DecisionTreeClassifier
X = df[[feature_name0, feature_name1]].to_numpy()
y = df["target"].to_numpy()
tree = DecisionTreeClassifier(max_depth=10, criterion="entropy", random_state=1)
tree.fit(X, y)
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt
disp = DecisionBoundaryDisplay.from_estimator(
tree,
X,
response_method="predict",
xlabel=feature_name0,
ylabel=feature_name1,
alpha=0.5,
cmap=plt.cm.coolwarm
)
accuracy_tree = accuracy_score(y, tree.predict(X))
print(f'Tree Accuracy: {accuracy_tree:.2f}')
disp.ax_.scatter(
X[:, 0],
X[:, 1],
c=y,
edgecolor="k",
cmap=plt.cm.coolwarm
)
plt.title(f"Decision surface for tree trained on {feature_name0} and {feature_name1}")
plt.tight_layout()
from sklearn.tree import plot_tree
plot_tree(tree, filled=True);
Вернемся к изначальной задаче, будем предсказывать три финальных класса.
Попробуем переобучить дерево, добавим ему глубины.
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
dataset = load_iris(as_frame=True)
df = dataset.frame
X = df[[feature_name0, feature_name1]].to_numpy()
y = df["target"].to_numpy()
tree = DecisionTreeClassifier(max_depth=4, criterion="entropy", random_state=1)
tree.fit(X, y)
from sklearn.tree import plot_tree
plot_tree(tree, filled=True);
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt
disp = DecisionBoundaryDisplay.from_estimator(
tree,
X,
response_method="predict",
xlabel=feature_name0,
ylabel=feature_name1,
alpha=0.5,
cmap=plt.cm.coolwarm
)
accuracy_tree = accuracy_score(y, tree.predict(X))
print(f'Tree Accuracy: {accuracy_tree:.2f}')
disp.ax_.scatter(
X[:, 0],
X[:, 1],
c=y,
edgecolor="k",
cmap=plt.cm.coolwarm
)
plt.title(f"Decision surface for tree trained on {feature_name0} and {feature_name1}")
plt.tight_layout()
Деревья можно применить и в задаче регрессии.
from sklearn.tree import DecisionTreeRegressor
from seaborn import load_dataset
df_diamonds = load_dataset("diamonds")
df_diamonds.head()
X_diamonds = df_diamonds[["carat"]].to_numpy()
y_diamonds = df_diamonds["price"].to_numpy()
tree = DecisionTreeRegressor(max_depth=3, random_state=1)
tree.fit(X_diamonds, y_diamonds)
X_copy = X_diamonds.copy()
plt.scatter(X_diamonds[:, 0], y_diamonds)
plt.scatter(X_copy[:, 0], tree.predict(X_copy), color="C1")
from sklearn.tree import plot_tree
plot_tree(tree, filled=True);
from sklearn.ensemble import RandomForestClassifier
random_forest = RandomForestClassifier(random_state=1, max_depth=3)
print(X.shape)
random_forest.fit(X, y)
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt
disp = DecisionBoundaryDisplay.from_estimator(
random_forest.estimators_[10],
X,
response_method="predict",
xlabel=feature_name0,
ylabel=feature_name1,
alpha=0.5,
cmap=plt.cm.coolwarm
)
accuracy_bagging = accuracy_score(y, random_forest.predict(X))
print(f'Random Forest Accuracy: {accuracy_bagging:.2f}')
disp.ax_.scatter(
X[:, 0],
X[:, 1],
c=y,
edgecolor="k",
cmap=plt.cm.coolwarm
)
plt.title(f"Decision surface for tree trained on {feature_name0} and {feature_name1}")
plt.tight_layout()
random_forest.estimator_params
from sklearn.ensemble import RandomForestClassifier
random_forest = RandomForestClassifier(random_state=1, max_depth=10)
random_forest.fit(X, y)
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt
disp = DecisionBoundaryDisplay.from_estimator(
random_forest,
X,
response_method="predict",
xlabel=feature_name0,
ylabel=feature_name1,
alpha=0.5,
cmap=plt.cm.coolwarm
)
accuracy_bagging = accuracy_score(y, random_forest.predict(X))
print(f'Random Forest Accuracy: {accuracy_bagging:.2f}')
disp.ax_.scatter(
X[:, 0],
X[:, 1],
c=y,
edgecolor="k",
cmap=plt.cm.coolwarm
)
plt.title(f"Decision surface for tree trained on {feature_name0} and {feature_name1}")
plt.tight_layout()
from sklearn.ensemble import BaggingClassifier, BaggingRegressor
from sklearn.tree import DecisionTreeClassifier
base_model = DecisionTreeClassifier(max_depth=10)
bagging_model = BaggingClassifier(estimator=base_model, n_estimators=10000, random_state=42)
bagging_model.fit(X, y)
accuracy_bagging = accuracy_score(y, bagging_model.predict(X))
print(f'Bagging Test Accuracy: {accuracy_bagging:.2f}')
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt
disp = DecisionBoundaryDisplay.from_estimator(
bagging_model,
X,
response_method="predict",
xlabel=feature_name0,
ylabel=feature_name1,
alpha=0.5,
cmap=plt.cm.coolwarm
)
accuracy_bagging = accuracy_score(y, random_forest.predict(X))
print(f'Random Forest Accuracy: {accuracy_bagging:.2f}')
disp.ax_.scatter(
X[:, 0],
X[:, 1],
c=y,
edgecolor="k",
cmap=plt.cm.coolwarm
)
plt.title(f"Decision surface for tree trained on {feature_name0} and {feature_name1}")
plt.tight_layout()
from sklearn.ensemble import BaggingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
base_model = LogisticRegression(max_iter=100)
bagging_model = BaggingClassifier(estimator=base_model, n_estimators=100, random_state=42)
bagging_model.fit(X, y)
accuracy_bagging = accuracy_score(y, bagging_model.predict(X))
print(f'Bagging Test Accuracy: {accuracy_bagging:.2f}')
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.metrics import accuracy_score
from matplotlib import pyplot as plt
disp = DecisionBoundaryDisplay.from_estimator(
bagging_model.estimators_[2],
X,
response_method="predict",
xlabel=feature_name0,
ylabel=feature_name1,
alpha=0.5,
cmap=plt.cm.coolwarm
)
accuracy_bagging = accuracy_score(y, random_forest.predict(X))
print(f'Random Forest Accuracy: {accuracy_bagging:.2f}')
disp.ax_.scatter(
X[:, 0],
X[:, 1],
c=y,
edgecolor="k",
cmap=plt.cm.coolwarm
)
plt.title(f"Decision surface for tree trained on {feature_name0} and {feature_name1}")
plt.tight_layout()
from sklearn.ensemble import AdaBoostRegressor
from sklearn.tree import DecisionTreeRegressor
regressor = AdaBoostRegressor(
DecisionTreeRegressor(max_depth=2), n_estimators=10000,
)
regressor.fit(X_diamonds, y_diamonds)
X_diamonds = df_diamonds[["carat"]].to_numpy()
y_diamonds = df_diamonds["price"].to_numpy()
X_copy = X_diamonds.copy()
plt.scatter(X_diamonds[:, 0], y_diamonds)
plt.scatter(X_copy[:, 0], regressor.predict(X_copy), color="C1")